diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 81c8a44baf..4ed95b59f7 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -183,6 +183,12 @@ if (WIN32) add_dependency_external_projects(steamworks) endif() +# include OPENSSL +include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}") + +# append OpenSSL to our list of libraries to link +target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES}) + # disable /OPT:REF and /OPT:ICF for the Debug builds # This will prevent the following linker warnings # LINK : warning LNK4075: ignoring '/INCREMENTAL' due to '/OPT:ICF' specification diff --git a/interface/resources/qml/hifi/commerce/Checkout.qml b/interface/resources/qml/hifi/commerce/Checkout.qml index 865bb72921..b9d15b61e4 100644 --- a/interface/resources/qml/hifi/commerce/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/Checkout.qml @@ -66,6 +66,14 @@ Rectangle { } } } + onSecurityImageResult: { + securityImage.source = securityImageSelection.getImagePathFromImageID(imageID); + } + } + + SecurityImageSelection { + id: securityImageSelection; + referrerURL: checkoutRoot.itemHref; } // @@ -80,6 +88,20 @@ Rectangle { anchors.left: parent.left; anchors.top: parent.top; + // Security Image + Image { + id: securityImage; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 16; + height: parent.height - 5; + width: height; + anchors.verticalCenter: parent.verticalCenter; + fillMode: Image.PreserveAspectFit; + mipmap: true; + } + // Title Bar text RalewaySemiBold { id: titleBarText; @@ -87,8 +109,11 @@ Rectangle { // Text size size: hifi.fontSizes.overlayTitle; // Anchors - anchors.fill: parent; + anchors.top: parent.top; + anchors.left: securityImage.right; anchors.leftMargin: 16; + anchors.bottom: parent.bottom; + width: paintedWidth; // Style color: hifi.colors.lightGrayText; // Alignment @@ -381,7 +406,7 @@ Rectangle { // "Buy" button HifiControlsUit.Button { id: buyButton; - enabled: balanceAfterPurchase >= 0 && !alreadyOwned && inventoryReceived && balanceReceived; + enabled: balanceAfterPurchase >= 0 && inventoryReceived && balanceReceived; color: hifi.buttons.black; colorScheme: hifi.colorSchemes.dark; anchors.top: parent.top; @@ -391,9 +416,16 @@ Rectangle { anchors.right: parent.right; anchors.rightMargin: 20; width: parent.width/2 - anchors.rightMargin*2; - text: (inventoryReceived && balanceReceived) ? (alreadyOwned ? "Already Owned" : "Buy") : "--"; + text: (inventoryReceived && balanceReceived) ? (alreadyOwned ? "Already Owned: Get Item" : "Buy") : "--"; onClicked: { - commerce.buy(itemId, parseInt(itemPriceText.text)); + if (!alreadyOwned) { + commerce.buy(itemId, parseInt(itemPriceText.text)); + } else { + if (urlHandler.canHandleUrl(itemHref)) { + urlHandler.handleUrl(itemHref); + } + sendToScript({method: 'checkout_buySuccess', itemId: itemId}); + } } } } @@ -427,6 +459,7 @@ Rectangle { itemHref = message.params.itemHref; commerce.balance(); commerce.inventory(); + commerce.getSecurityImage(); break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); diff --git a/interface/resources/qml/hifi/commerce/Inventory.qml b/interface/resources/qml/hifi/commerce/Inventory.qml index 298abebdab..d7ffae7c3c 100644 --- a/interface/resources/qml/hifi/commerce/Inventory.qml +++ b/interface/resources/qml/hifi/commerce/Inventory.qml @@ -43,6 +43,14 @@ Rectangle { inventoryContentsList.model = inventory.assets; } } + onSecurityImageResult: { + securityImage.source = securityImageSelection.getImagePathFromImageID(imageID); + } + } + + SecurityImageSelection { + id: securityImageSelection; + referrerURL: inventoryRoot.referrerURL; } // @@ -51,12 +59,26 @@ Rectangle { Item { id: titleBarContainer; // Size - width: inventoryRoot.width; + width: parent.width; height: 50; // Anchors anchors.left: parent.left; anchors.top: parent.top; + // Security Image + Image { + id: securityImage; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 16; + height: parent.height - 5; + width: height; + anchors.verticalCenter: parent.verticalCenter; + fillMode: Image.PreserveAspectFit; + mipmap: true; + } + // Title Bar text RalewaySemiBold { id: titleBarText; @@ -64,8 +86,11 @@ Rectangle { // Text size size: hifi.fontSizes.overlayTitle; // Anchors - anchors.fill: parent; + anchors.top: parent.top; + anchors.left: securityImage.right; anchors.leftMargin: 16; + anchors.bottom: parent.bottom; + width: paintedWidth; // Style color: hifi.colors.lightGrayText; // Alignment @@ -73,6 +98,25 @@ Rectangle { verticalAlignment: Text.AlignVCenter; } + // "Change Security Image" button + HifiControlsUit.Button { + id: changeSecurityImageButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.right: parent.right; + anchors.rightMargin: 20; + width: 200; + text: "Change Security Image" + onClicked: { + securityImageSelection.isManuallyChangingSecurityImage = true; + securityImageSelection.visible = true; + } + } + // Separator HifiControlsUit.Separator { anchors.left: parent.left; @@ -166,6 +210,7 @@ Rectangle { } ListView { id: inventoryContentsList; + clip: true; // Anchors anchors.top: inventoryContentsLabel.bottom; anchors.topMargin: 8; @@ -262,6 +307,7 @@ Rectangle { referrerURL = message.referrerURL; commerce.balance(); commerce.inventory(); + commerce.getSecurityImage(); break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); diff --git a/interface/resources/qml/hifi/commerce/SecurityImageModel.qml b/interface/resources/qml/hifi/commerce/SecurityImageModel.qml new file mode 100644 index 0000000000..2fbf28683f --- /dev/null +++ b/interface/resources/qml/hifi/commerce/SecurityImageModel.qml @@ -0,0 +1,42 @@ +// +// SecurityImageModel.qml +// qml/hifi/commerce +// +// SecurityImageModel +// +// Created by Zach Fox on 2017-08-15 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 + +ListModel { + id: root; + ListElement{ + sourcePath: "images/01cat.jpg" + securityImageEnumValue: 1; + } + ListElement{ + sourcePath: "images/02car.jpg" + securityImageEnumValue: 2; + } + ListElement{ + sourcePath: "images/03dog.jpg" + securityImageEnumValue: 3; + } + ListElement{ + sourcePath: "images/04stars.jpg" + securityImageEnumValue: 4; + } + ListElement{ + sourcePath: "images/05plane.jpg" + securityImageEnumValue: 5; + } + ListElement{ + sourcePath: "images/06gingerbread.jpg" + securityImageEnumValue: 6; + } +} diff --git a/interface/resources/qml/hifi/commerce/SecurityImageSelection.qml b/interface/resources/qml/hifi/commerce/SecurityImageSelection.qml new file mode 100644 index 0000000000..7775f1ff9c --- /dev/null +++ b/interface/resources/qml/hifi/commerce/SecurityImageSelection.qml @@ -0,0 +1,271 @@ +// +// SecurityImageSelection.qml +// qml/hifi/commerce +// +// SecurityImageSelection +// +// Created by Zach Fox on 2017-08-15 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +// references XXX from root context + +Rectangle { + HifiConstants { id: hifi; } + + id: securityImageSelectionRoot; + property string referrerURL: ""; + property bool isManuallyChangingSecurityImage: false; + anchors.fill: parent; + // Style + color: hifi.colors.baseGray; + z:999; // On top of everything else + visible: false; + + Hifi.QmlCommerce { + id: commerce; + onSecurityImageResult: { + if (!isManuallyChangingSecurityImage) { + securityImageSelectionRoot.visible = (imageID == 0); + } + if (imageID > 0) { + for (var itr = 0; itr < gridModel.count; itr++) { + var thisValue = gridModel.get(itr).securityImageEnumValue; + if (thisValue === imageID) { + securityImageGrid.currentIndex = itr; + break; + } + } + } + } + } + + Component.onCompleted: { + commerce.getSecurityImage(); + } + + // + // TITLE BAR START + // + Item { + id: titleBarContainer; + // Size + width: securityImageSelectionRoot.width; + height: 30; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // Title Bar text + RalewaySemiBold { + id: titleBarText; + text: "Select a Security Image"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 16; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } + } + // + // TITLE BAR END + // + + // + // EXPLANATION START + // + Item { + id: explanationContainer; + // Size + width: securityImageSelectionRoot.width; + height: 85; + // Anchors + anchors.top: titleBarContainer.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + + RalewayRegular { + id: explanationText; + text: "This image will be displayed on secure Inventory and Marketplace Checkout dialogs.
If you don't see your selected image on these dialogs, do not use them!
"; + // Text size + size: 16; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 4; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + // Style + color: hifi.colors.lightGrayText; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } + } + // + // EXPLANATION END + // + + // + // SECURITY IMAGE GRID START + // + Item { + id: securityImageGridContainer; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 8; + anchors.right: parent.right; + anchors.rightMargin: 8; + anchors.top: explanationContainer.bottom; + anchors.topMargin: 8; + anchors.bottom: actionButtonsContainer.top; + anchors.bottomMargin: 8; + + SecurityImageModel { + id: gridModel; + } + + GridView { + id: securityImageGrid; + clip: true; + // Anchors + anchors.fill: parent; + currentIndex: -1; + cellWidth: width / 2; + cellHeight: height / 3; + model: gridModel; + delegate: Item { + width: securityImageGrid.cellWidth; + height: securityImageGrid.cellHeight; + Item { + anchors.fill: parent; + Image { + width: parent.width - 8; + height: parent.height - 8; + source: sourcePath; + anchors.horizontalCenter: parent.horizontalCenter; + anchors.verticalCenter: parent.verticalCenter; + fillMode: Image.PreserveAspectFit; + mipmap: true; + } + } + MouseArea { + anchors.fill: parent; + onClicked: { + securityImageGrid.currentIndex = index; + } + } + } + highlight: Rectangle { + width: securityImageGrid.cellWidth; + height: securityImageGrid.cellHeight; + color: hifi.colors.blueHighlight; + } + } + } + // + // SECURITY IMAGE GRID END + // + + + // + // ACTION BUTTONS START + // + Item { + id: actionButtonsContainer; + // Size + width: securityImageSelectionRoot.width; + height: 40; + // Anchors + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + + // "Cancel" button + HifiControlsUit.Button { + id: cancelButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: parent.width/2 - anchors.leftMargin*2; + text: "Cancel" + onClicked: { + if (!securityImageSelectionRoot.isManuallyChangingSecurityImage) { + sendToScript({method: 'securityImageSelection_cancelClicked', referrerURL: securityImageSelectionRoot.referrerURL}); + } else { + securityImageSelectionRoot.visible = false; + } + } + } + + // "Confirm" button + HifiControlsUit.Button { + id: confirmButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.right: parent.right; + anchors.rightMargin: 20; + width: parent.width/2 - anchors.rightMargin*2; + text: "Confirm"; + onClicked: { + securityImageSelectionRoot.isManuallyChangingSecurityImage = false; + commerce.chooseSecurityImage(gridModel.get(securityImageGrid.currentIndex).securityImageEnumValue); + } + } + } + // + // ACTION BUTTONS END + // + + // + // FUNCTION DEFINITIONS START + // + signal sendToScript(var message); + + function getImagePathFromImageID(imageID) { + return (imageID ? gridModel.get(imageID - 1).sourcePath : ""); + } + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/images/01cat.jpg b/interface/resources/qml/hifi/commerce/images/01cat.jpg new file mode 100644 index 0000000000..6e7897cb82 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/images/01cat.jpg differ diff --git a/interface/resources/qml/hifi/commerce/images/02car.jpg b/interface/resources/qml/hifi/commerce/images/02car.jpg new file mode 100644 index 0000000000..5dd8091e57 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/images/02car.jpg differ diff --git a/interface/resources/qml/hifi/commerce/images/03dog.jpg b/interface/resources/qml/hifi/commerce/images/03dog.jpg new file mode 100644 index 0000000000..4a85b80c0c Binary files /dev/null and b/interface/resources/qml/hifi/commerce/images/03dog.jpg differ diff --git a/interface/resources/qml/hifi/commerce/images/04stars.jpg b/interface/resources/qml/hifi/commerce/images/04stars.jpg new file mode 100644 index 0000000000..8f2bf62f83 Binary files /dev/null and b/interface/resources/qml/hifi/commerce/images/04stars.jpg differ diff --git a/interface/resources/qml/hifi/commerce/images/05plane.jpg b/interface/resources/qml/hifi/commerce/images/05plane.jpg new file mode 100644 index 0000000000..6504459d8b Binary files /dev/null and b/interface/resources/qml/hifi/commerce/images/05plane.jpg differ diff --git a/interface/resources/qml/hifi/commerce/images/06gingerbread.jpg b/interface/resources/qml/hifi/commerce/images/06gingerbread.jpg new file mode 100644 index 0000000000..54c37faa2f Binary files /dev/null and b/interface/resources/qml/hifi/commerce/images/06gingerbread.jpg differ diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index ad79a836ad..8d7d47aca0 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -24,12 +24,12 @@ void Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, cons transaction["inventory_key"] = inventory_key; transaction["inventory_buyer_username"] = buyerUsername; QJsonDocument transactionDoc{ transaction }; - QString transactionString = transactionDoc.toJson(QJsonDocument::Compact); - + auto transactionString = transactionDoc.toJson(QJsonDocument::Compact); + auto wallet = DependencyManager::get(); QString signature = wallet->signWithKey(transactionString, hfc_key); QJsonObject request; - request["transaction"] = transactionString; + request["transaction"] = QString(transactionString); request["signature"] = signature; qCInfo(commerce) << "Transaction:" << QJsonDocument(request).toJson(QJsonDocument::Compact); @@ -78,4 +78,4 @@ void Ledger::inventory(const QStringList& keys) { inventoryObject.insert("assets", _inventory); qCInfo(commerce) << "Inventory:" << inventoryObject; emit inventoryResult(inventoryObject, ""); -} \ No newline at end of file +} diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 63bfca4f31..573740727f 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -19,9 +19,11 @@ HIFI_QML_DEF(QmlCommerce) QmlCommerce::QmlCommerce(QQuickItem* parent) : OffscreenQmlDialog(parent) { auto ledger = DependencyManager::get(); + auto wallet = DependencyManager::get(); connect(ledger.data(), &Ledger::buyResult, this, &QmlCommerce::buyResult); connect(ledger.data(), &Ledger::balanceResult, this, &QmlCommerce::balanceResult); connect(ledger.data(), &Ledger::inventoryResult, this, &QmlCommerce::inventoryResult); + connect(wallet.data(), &Wallet::securityImageResult, this, &QmlCommerce::securityImageResult); } void QmlCommerce::buy(const QString& assetId, int cost, const QString& buyerUsername) { @@ -48,4 +50,13 @@ void QmlCommerce::inventory() { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); ledger->inventory(wallet->listPublicKeys()); - } \ No newline at end of file +} + +void QmlCommerce::chooseSecurityImage(uint imageID) { + auto wallet = DependencyManager::get(); + wallet->chooseSecurityImage(imageID); +} +void QmlCommerce::getSecurityImage() { + auto wallet = DependencyManager::get(); + wallet->getSecurityImage(); +} \ No newline at end of file diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 0b1d232fd7..5b702bfeff 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -30,11 +30,14 @@ signals: // because we can't scalably know of out-of-band changes (e.g., another machine interacting with the block chain). void balanceResult(int balance, const QString& failureMessage); void inventoryResult(QJsonObject inventory, const QString& failureMessage); + void securityImageResult(uint imageID); protected: Q_INVOKABLE void buy(const QString& assetId, int cost, const QString& buyerUsername = ""); Q_INVOKABLE void balance(); Q_INVOKABLE void inventory(); + Q_INVOKABLE void chooseSecurityImage(uint imageID); + Q_INVOKABLE void getSecurityImage(); }; #endif // hifi_QmlCommerce_h diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 34d89b54b0..f47b174d88 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -9,30 +9,223 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include #include "CommerceLogging.h" #include "Ledger.h" #include "Wallet.h" +#include + +#include + +#include +#include +#include +#include +#include +#include + +static const char* KEY_FILE = "hifikey"; + +void initialize() { + static bool initialized = false; + if (!initialized) { + SSL_load_error_strings(); + SSL_library_init(); + OpenSSL_add_all_algorithms(); + initialized = true; + } +} + +QString keyFilePath() { + return PathUtils::getAppDataFilePath(KEY_FILE); +} + +// for now the callback function just returns the same string. Later we can hook +// this to the gui (some thought required) +int passwordCallback(char* password, int maxPasswordSize, int rwFlag, void* u) { + // just return a hardcoded pwd for now + static const char* pwd = "pwd"; + strcpy(password, pwd); + return static_cast(strlen(pwd)); +} + +// BEGIN copied code - this will be removed/changed at some point soon +// copied (without emits for various signals) from libraries/networking/src/RSAKeypairGenerator.cpp. +// We will have a different implementation in practice, but this gives us a start for now +QPair generateRSAKeypair() { + + RSA* keyPair = RSA_new(); + BIGNUM* exponent = BN_new(); + QPair retval; + + const unsigned long RSA_KEY_EXPONENT = 65537; + BN_set_word(exponent, RSA_KEY_EXPONENT); + + // seed the random number generator before we call RSA_generate_key_ex + srand(time(NULL)); + + const int RSA_KEY_BITS = 2048; + + if (!RSA_generate_key_ex(keyPair, RSA_KEY_BITS, exponent, NULL)) { + qCDebug(commerce) << "Error generating 2048-bit RSA Keypair -" << ERR_get_error(); + + // we're going to bust out of here but first we cleanup the BIGNUM + BN_free(exponent); + return retval; + } + + // we don't need the BIGNUM anymore so clean that up + BN_free(exponent); + + // grab the public key and private key from the file + unsigned char* publicKeyDER = NULL; + int publicKeyLength = i2d_RSAPublicKey(keyPair, &publicKeyDER); + + unsigned char* privateKeyDER = NULL; + int privateKeyLength = i2d_RSAPrivateKey(keyPair, &privateKeyDER); + + if (publicKeyLength <= 0 || privateKeyLength <= 0) { + qCDebug(commerce) << "Error getting DER public or private key from RSA struct -" << ERR_get_error(); + + + // cleanup the RSA struct + RSA_free(keyPair); + + // cleanup the public and private key DER data, if required + if (publicKeyLength > 0) { + OPENSSL_free(publicKeyDER); + } + + if (privateKeyLength > 0) { + OPENSSL_free(privateKeyDER); + } + + return retval; + } + + + + // now lets persist them to files + // TODO: figure out a scheme for multiple keys, etc... + FILE* fp; + if ((fp = fopen(keyFilePath().toStdString().c_str(), "wt"))) { + if (!PEM_write_RSAPublicKey(fp, keyPair)) { + fclose(fp); + qCDebug(commerce) << "failed to write public key"; + return retval; + } + + if (!PEM_write_RSAPrivateKey(fp, keyPair, EVP_des_ede3_cbc(), NULL, 0, passwordCallback, NULL)) { + fclose(fp); + qCDebug(commerce) << "failed to write private key"; + return retval; + } + fclose(fp); + } + + RSA_free(keyPair); + + // prepare the return values + retval.first = new QByteArray(reinterpret_cast(publicKeyDER), publicKeyLength ), + retval.second = new QByteArray(reinterpret_cast(privateKeyDER), privateKeyLength ); + + // cleanup the publicKeyDER and publicKeyDER data + OPENSSL_free(publicKeyDER); + OPENSSL_free(privateKeyDER); + return retval; +} +// END copied code (which will soon change) + +// the public key can just go into a byte array +QByteArray readPublicKey(const char* filename) { + FILE* fp; + RSA* key = NULL; + if ((fp = fopen(filename, "r"))) { + // file opened successfully + qCDebug(commerce) << "opened key file" << filename; + if ((key = PEM_read_RSAPublicKey(fp, NULL, NULL, NULL))) { + // file read successfully + unsigned char* publicKeyDER = NULL; + int publicKeyLength = i2d_RSAPublicKey(key, &publicKeyDER); + // TODO: check for 0 length? + + // cleanup + RSA_free(key); + fclose(fp); + + qCDebug(commerce) << "parsed public key file successfully"; + + QByteArray retval((char*)publicKeyDER, publicKeyLength); + OPENSSL_free(publicKeyDER); + return retval; + } else { + qCDebug(commerce) << "couldn't parse" << filename; + } + fclose(fp); + } else { + qCDebug(commerce) << "couldn't open" << filename; + } + return QByteArray(); +} + +// the private key should be read/copied into heap memory. For now, we need the RSA struct +// so I'll return that. Note we need to RSA_free(key) later!!! +RSA* readPrivateKey(const char* filename) { + FILE* fp; + RSA* key = NULL; + if ((fp = fopen(filename, "r"))) { + // file opened successfully + qCDebug(commerce) << "opened key file" << filename; + if ((key = PEM_read_RSAPrivateKey(fp, &key, passwordCallback, NULL))) { + // cleanup + fclose(fp); + + qCDebug(commerce) << "parsed private key file successfully"; + + } else { + qCDebug(commerce) << "couldn't parse" << filename; + } + fclose(fp); + } else { + qCDebug(commerce) << "couldn't open" << filename; + } + return key; +} + bool Wallet::createIfNeeded() { - // FIXME: persist in file between sessions. if (_publicKeys.count() > 0) return false; + + // FIXME: initialize OpenSSL elsewhere soon + initialize(); + + // try to read existing keys if they exist... + auto publicKey = readPublicKey(keyFilePath().toStdString().c_str()); + if (publicKey.size() > 0) { + if (auto key = readPrivateKey(keyFilePath().toStdString().c_str()) ) { + qCDebug(commerce) << "read private key"; + RSA_free(key); + // K -- add the public key since we have a legit private key associated with it + _publicKeys.push_back(QUrl::toPercentEncoding(publicKey.toBase64())); + return false; + } + } qCInfo(commerce) << "Creating wallet."; return generateKeyPair(); } bool Wallet::generateKeyPair() { - // FIXME: need private key, too, and persist in file. qCInfo(commerce) << "Generating keypair."; - QString key = QUuid::createUuid().toString(); + auto keyPair = generateRSAKeypair(); + + _publicKeys.push_back(QUrl::toPercentEncoding(keyPair.first->toBase64())); + qCDebug(commerce) << "public key:" << _publicKeys.last(); - _publicKeys.push_back(key); // Keep in memory for synchronous speed. // It's arguable whether we want to change the receiveAt every time, but: // 1. It's certainly needed the first time, when createIfNeeded answers true. // 2. It is maximally private, and we can step back from that later if desired. // 3. It maximally exercises all the machinery, so we are most likely to surface issues now. auto ledger = DependencyManager::get(); - return ledger->receiveAt(key); + return ledger->receiveAt(_publicKeys.last()); } QStringList Wallet::listPublicKeys() { qCInfo(commerce) << "Enumerating public keys."; @@ -40,7 +233,44 @@ QStringList Wallet::listPublicKeys() { return _publicKeys; } -QString Wallet::signWithKey(const QString& text, const QString& key) { +// for now a copy of how we sign in libraries/networking/src/DataServerAccountInfo - +// we sha256 the text, read the private key from disk (for now!), and return the signed +// sha256. Note later with multiple keys, we may need the key parameter (or something +// similar) so I left it alone for now. Also this will probably change when we move +// away from RSA keys anyways. Note that since this returns a QString, we better avoid +// the horror of code pages and so on (changing the bytes) by just returning a base64 +// encoded string representing the signature (suitable for http, etc...) +QString Wallet::signWithKey(const QByteArray& text, const QString& key) { qCInfo(commerce) << "Signing text."; - return "fixme signed"; -} \ No newline at end of file + RSA* rsaPrivateKey = NULL; + if ((rsaPrivateKey = readPrivateKey(keyFilePath().toStdString().c_str()))) { + QByteArray signature(RSA_size(rsaPrivateKey), 0); + unsigned int signatureBytes = 0; + + QByteArray hashedPlaintext = QCryptographicHash::hash(text, QCryptographicHash::Sha256); + + int encryptReturn = RSA_sign(NID_sha256, + reinterpret_cast(hashedPlaintext.constData()), + hashedPlaintext.size(), + reinterpret_cast(signature.data()), + &signatureBytes, + rsaPrivateKey); + + // free the private key RSA struct now that we are done with it + RSA_free(rsaPrivateKey); + + if (encryptReturn != -1) { + return QUrl::toPercentEncoding(signature.toBase64()); + } + } + return QString(); +} + + +void Wallet::chooseSecurityImage(uint imageID) { + _chosenSecurityImage = (SecurityImage)imageID; + emit securityImageResult(imageID); +} +void Wallet::getSecurityImage() { + emit securityImageResult(_chosenSecurityImage); +} diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index 79de5e81da..a1c7c7752b 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -25,10 +25,30 @@ public: bool createIfNeeded(); bool generateKeyPair(); QStringList listPublicKeys(); - QString signWithKey(const QString& text, const QString& key); + QString signWithKey(const QByteArray& text, const QString& key); + void chooseSecurityImage(uint imageID); + void getSecurityImage(); + +signals: + void securityImageResult(uint imageID); + +protected: + // ALWAYS add SecurityImage enum values to the END of the enum. + // They must be in the same order as the images are listed in + // SecurityImageSelection.qml + enum SecurityImage { + NONE = 0, + Cat, + Car, + Dog, + Stars, + Plane, + Gingerbread + }; private: QStringList _publicKeys{}; + SecurityImage _chosenSecurityImage = SecurityImage::NONE; }; #endif // hifi_Wallet_h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 3a31ccd25f..1b91737364 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -168,6 +168,8 @@ void Rig::destroyAnimGraph() { void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOffset) { _geometryOffset = AnimPose(geometry.offset); _invGeometryOffset = _geometryOffset.inverse(); + _geometryToRigTransform = modelOffset * geometry.offset; + _rigToGeometryTransform = glm::inverse(_geometryToRigTransform); setModelOffset(modelOffset); _animSkeleton = std::make_shared(geometry); @@ -1761,59 +1763,11 @@ void Rig::computeAvatarBoundingCapsule( return; } - AnimInverseKinematics ikNode("boundingShape"); - ikNode.setSkeleton(_animSkeleton); - - ikNode.setTargetVars("LeftHand", "leftHandPosition", "leftHandRotation", - "leftHandType", "leftHandWeight", 1.0f, {}, - QString(), QString(), QString()); - ikNode.setTargetVars("RightHand", "rightHandPosition", "rightHandRotation", - "rightHandType", "rightHandWeight", 1.0f, {}, - QString(), QString(), QString()); - ikNode.setTargetVars("LeftFoot", "leftFootPosition", "leftFootRotation", - "leftFootType", "leftFootWeight", 1.0f, {}, - QString(), QString(), QString()); - ikNode.setTargetVars("RightFoot", "rightFootPosition", "rightFootRotation", - "rightFootType", "rightFootWeight", 1.0f, {}, - QString(), QString(), QString()); glm::vec3 hipsPosition(0.0f); int hipsIndex = indexOfJoint("Hips"); if (hipsIndex >= 0) { hipsPosition = transformPoint(_geometryToRigTransform, _animSkeleton->getAbsoluteDefaultPose(hipsIndex).trans()); } - AnimVariantMap animVars; - animVars.setRigToGeometryTransform(_rigToGeometryTransform); - glm::quat handRotation = glm::angleAxis(PI, Vectors::UNIT_X); - animVars.set("leftHandPosition", hipsPosition); - animVars.set("leftHandRotation", handRotation); - animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); - animVars.set("rightHandPosition", hipsPosition); - animVars.set("rightHandRotation", handRotation); - animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); - - int rightFootIndex = indexOfJoint("RightFoot"); - int leftFootIndex = indexOfJoint("LeftFoot"); - if (rightFootIndex != -1 && leftFootIndex != -1) { - glm::vec3 geomFootPosition = glm::vec3(0.0f, _animSkeleton->getAbsoluteDefaultPose(rightFootIndex).trans().y, 0.0f); - glm::vec3 footPosition = transformPoint(_geometryToRigTransform, geomFootPosition); - glm::quat footRotation = glm::angleAxis(0.5f * PI, Vectors::UNIT_X); - animVars.set("leftFootPosition", footPosition); - animVars.set("leftFootRotation", footRotation); - animVars.set("leftFootType", (int)IKTarget::Type::RotationAndPosition); - animVars.set("rightFootPosition", footPosition); - animVars.set("rightFootRotation", footRotation); - animVars.set("rightFootType", (int)IKTarget::Type::RotationAndPosition); - } - - // call overlay twice: once to verify AnimPoseVec joints and again to do the IK - AnimNode::Triggers triggersOut; - AnimContext context(false, false, false, _geometryToRigTransform, _rigToGeometryTransform); - float dt = 1.0f; // the value of this does not matter - ikNode.overlay(animVars, context, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); - AnimPoseVec finalPoses = ikNode.overlay(animVars, context, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); - - // convert relative poses to absolute - _animSkeleton->convertRelativePosesToAbsolute(finalPoses); // compute bounding box that encloses all points Extents totalExtents; @@ -1824,15 +1778,15 @@ void Rig::computeAvatarBoundingCapsule( // even if they do not have legs (default robot) totalExtents.addPoint(glm::vec3(0.0f)); - // HACK to reduce the radius of the bounding capsule to be tight with the torso, we only consider joints + // To reduce the radius of the bounding capsule to be tight with the torso, we only consider joints // from the head to the hips when computing the rest of the bounding capsule. int index = indexOfJoint("Head"); while (index != -1) { const FBXJointShapeInfo& shapeInfo = geometry.joints.at(index).shapeInfo; - AnimPose pose = finalPoses[index]; + AnimPose pose = _animSkeleton->getAbsoluteDefaultPose(index); if (shapeInfo.points.size() > 0) { - for (size_t j = 0; j < shapeInfo.points.size(); ++j) { - totalExtents.addPoint((pose * shapeInfo.points[j])); + for (auto& point : shapeInfo.points) { + totalExtents.addPoint((pose * point)); } } index = _animSkeleton->getParentIndex(index); @@ -1846,7 +1800,6 @@ void Rig::computeAvatarBoundingCapsule( radiusOut = 0.5f * sqrtf(0.5f * (diagonal.x * diagonal.x + diagonal.z * diagonal.z)); heightOut = diagonal.y - 2.0f * radiusOut; - glm::vec3 rootPosition = finalPoses[geometry.rootJointIndex].trans(); - glm::vec3 rigCenter = transformPoint(_geometryToRigTransform, (0.5f * (totalExtents.maximum + totalExtents.minimum))); - localOffsetOut = rigCenter - transformPoint(_geometryToRigTransform, rootPosition); + glm::vec3 capsuleCenter = transformPoint(_geometryToRigTransform, (0.5f * (totalExtents.maximum + totalExtents.minimum))); + localOffsetOut = capsuleCenter - hipsPosition; } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 9378a1d95b..84d7d44689 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -21,6 +21,7 @@ var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); var MARKETPLACE_CHECKOUT_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/Checkout.qml"; var MARKETPLACE_INVENTORY_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/Inventory.qml"; + var MARKETPLACE_SECURITY_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/SecurityImageSelection.qml"; var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; // var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; @@ -87,7 +88,7 @@ function onScreenChanged(type, url) { onMarketplaceScreen = type === "Web" && url === MARKETPLACE_URL_INITIAL; - wireEventBridge(type === "QML" && (url === MARKETPLACE_CHECKOUT_QML_PATH || url === MARKETPLACE_INVENTORY_QML_PATH)); + wireEventBridge(type === "QML" && (url === MARKETPLACE_CHECKOUT_QML_PATH || url === MARKETPLACE_INVENTORY_QML_PATH || url === MARKETPLACE_SECURITY_QML_PATH)); // for toolbar mode: change button to active when window is first openend, false otherwise. marketplaceButton.editProperties({ isActive: onMarketplaceScreen }); if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { @@ -217,8 +218,11 @@ case 'inventory_backClicked': tablet.gotoWebScreen(message.referrerURL, MARKETPLACES_INJECT_SCRIPT_URL); break; + case 'securityImageSelection_cancelClicked': + tablet.gotoWebScreen(message.referrerURL, MARKETPLACES_INJECT_SCRIPT_URL); + break; default: - print('Unrecognized message from Checkout.qml or Inventory.qml: ' + JSON.stringify(message)); + print('Unrecognized message from Checkout.qml, Inventory.qml, or SecurityImageSelection.qml: ' + JSON.stringify(message)); } }