From a1d88a8588184de8983ee30fa347aad294f660eb Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 23 Aug 2018 14:45:16 -0700 Subject: [PATCH] Merge howard/wallet-locker2 into my branch --- .../qml/hifi/commerce/wallet/Help.qml | 51 +---- .../qml/hifi/commerce/wallet/Security.qml | 206 +----------------- .../qml/hifi/commerce/wallet/Wallet.qml | 15 ++ interface/src/commerce/Ledger.cpp | 36 ++- interface/src/commerce/Ledger.h | 3 +- interface/src/commerce/Wallet.cpp | 61 +++++- interface/src/commerce/Wallet.h | 4 + 7 files changed, 113 insertions(+), 263 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/Help.qml b/interface/resources/qml/hifi/commerce/wallet/Help.qml index b453509712..6d8fc3c33f 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Help.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Help.qml @@ -63,47 +63,6 @@ Item { question: "How can I get HFC?"; answer: "High Fidelity commerce is in open beta right now. Want more HFC? \ Get it by going to

BankOfHighFidelity. and meeting with the banker!"; - } - ListElement { - isExpanded: false; - question: "What are private keys and where are they stored?"; - answer: - "A private key is a secret piece of text that is used to prove ownership, unlock confidential information, and sign transactions. \ -In High Fidelity, your private key is used to securely access the contents of your Wallet and Purchases. \ -After wallet setup, a hifikey file is stored on your computer in High Fidelity Interface's AppData directory. \ -Your hifikey file contains your private key and is protected by your wallet passphrase. \ -

It is very important to back up your hifikey file! \ -Tap here to open the folder where your HifiKeys are stored on your main display." - } - ListElement { - isExpanded: false; - question: "How do I back up my private keys?"; - answer: "You can back up your hifikey file (which contains your private key and is encrypted using your wallet passphrase) by copying it to a USB flash drive, or to a service like Dropbox or Google Drive. \ -Restore your hifikey file by replacing the file in Interface's AppData directory with your backup copy. \ -Others with access to your back up should not be able to spend your HFC without your passphrase. \ -Tap here to open the folder where your HifiKeys are stored on your main display."; - } - ListElement { - isExpanded: false; - question: "What happens if I lose my private keys?"; - answer: "We cannot stress enough that you should keep a backup! For security reasons, High Fidelity does not keep a copy, and cannot restore it for you. \ -If you lose your private key, you will no longer have access to the contents of your Wallet or My Purchases. \ -Here are some things to try:

As a last resort, you can set up your Wallet again and generate a new hifikey file. \ -Unfortunately, this means you will start with 0 HFC and your purchased items will not be transferred over."; - } - ListElement { - isExpanded: false; - question: "What if I forget my wallet passphrase?"; - answer: "Your wallet passphrase is used to encrypt your private keys. Please write it down and store it securely! \ -

If you forget your passphrase, you will no longer be able to decrypt the hifikey file that the passphrase protects. \ -You will also no longer have access to the contents of your Wallet or My Purchases. \ -For security reasons, High Fidelity does not keep a copy of your passphrase, and can't restore it for you. \ -

If you still cannot remember your wallet passphrase, you can set up your Wallet again and generate a new hifikey file. \ -Unfortunately, this means you will start with 0 HFC and your purchased items will not be transferred over."; } ListElement { isExpanded: false; @@ -114,11 +73,9 @@ In your Wallet's Send Money tab, choose from your list of connections, or choose ListElement { isExpanded: false; question: "What is a Security Pic?" - answer: "Your Security Pic is an encrypted image that you select during Wallet Setup. \ -It acts as an extra layer of Wallet security. \ -When you see your Security Pic, you know that your actions and data are securely making use of your private keys.\ -

Don't enter your passphrase anywhere that doesn't display your Security Pic! \ -If you don't see your Security Pic on a page that requests your Wallet passphrase, someone untrustworthy may be trying to access your Wallet."; + answer: "Your Security Pic acts as an extra layer of Wallet security. \ +When you see your Security Pic, you know that your actions and data are securely making use of your account. \ +

Tap here to change your Security Pic."; } ListElement { isExpanded: false; @@ -260,6 +217,8 @@ At the moment, there is currently no way to convert HFC to other currencies. Sta } } else if (link === "#support") { Qt.openUrlExternally("mailto:support@highfidelity.com"); + } else if (link === "#securitypic") { + sendSignalToWallet({method: 'walletSecurity_changeSecurityImage'}); } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/Security.qml b/interface/resources/qml/hifi/commerce/wallet/Security.qml index 216d621bf8..e021328ebe 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Security.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Security.qml @@ -88,76 +88,9 @@ Item { color: hifi.colors.faintGray; } - Item { - id: changePassphraseContainer; - anchors.top: securityTextSeparator.bottom; - anchors.topMargin: 8; - anchors.left: parent.left; - anchors.leftMargin: 40; - anchors.right: parent.right; - anchors.rightMargin: 55; - height: 75; - - HiFiGlyphs { - id: changePassphraseImage; - text: hifi.glyphs.passphrase; - // Size - size: 80; - // Anchors - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - // Style - color: hifi.colors.white; - } - - RalewaySemiBold { - text: "Passphrase"; - // Anchors - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.left: changePassphraseImage.right; - anchors.leftMargin: 30; - width: 50; - // Text size - size: 18; - // Style - color: hifi.colors.white; - } - - // "Change Passphrase" button - HifiControlsUit.Button { - id: changePassphraseButton; - color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.dark; - anchors.right: parent.right; - anchors.verticalCenter: parent.verticalCenter; - width: 140; - height: 40; - text: "Change"; - onClicked: { - sendSignalToWallet({method: 'walletSecurity_changePassphrase'}); - } - } - } - - Rectangle { - id: changePassphraseSeparator; - // Size - width: parent.width; - height: 1; - // Anchors - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: changePassphraseContainer.bottom; - anchors.topMargin: 8; - // Style - color: hifi.colors.faintGray; - } - Item { id: changeSecurityImageContainer; - anchors.top: changePassphraseSeparator.bottom; + anchors.top: securityTextSeparator.bottom; anchors.topMargin: 8; anchors.left: parent.left; anchors.leftMargin: 40; @@ -207,143 +140,6 @@ Item { } } } - - Rectangle { - id: privateKeysSeparator; - // Size - width: parent.width; - height: 1; - // Anchors - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: changeSecurityImageContainer.bottom; - anchors.topMargin: 8; - // Style - color: hifi.colors.faintGray; - } - - Item { - id: yourPrivateKeysContainer; - anchors.top: privateKeysSeparator.bottom; - anchors.left: parent.left; - anchors.leftMargin: 40; - anchors.right: parent.right; - anchors.rightMargin: 55; - anchors.bottom: parent.bottom; - - onVisibleChanged: { - if (visible) { - Commerce.getKeyFilePathIfExists(); - } - } - - HiFiGlyphs { - id: yourPrivateKeysImage; - text: hifi.glyphs.walletKey; - // Size - size: 80; - // Anchors - anchors.top: parent.top; - anchors.topMargin: 20; - anchors.left: parent.left; - // Style - color: hifi.colors.white; - } - - RalewaySemiBold { - id: yourPrivateKeysText; - text: "Private Keys"; - size: 18; - // Anchors - anchors.top: parent.top; - anchors.topMargin: 32; - anchors.left: yourPrivateKeysImage.right; - anchors.leftMargin: 30; - anchors.right: parent.right; - height: 30; - // Style - color: hifi.colors.white; - } - - // Text below "private keys" - RalewayRegular { - id: explanitoryText; - text: "Your money and purchases are secured with private keys that only you have access to."; - // Text size - size: 18; - // Anchors - anchors.top: yourPrivateKeysText.bottom; - anchors.topMargin: 10; - anchors.left: yourPrivateKeysText.left; - anchors.right: yourPrivateKeysText.right; - height: paintedHeight; - // Style - color: hifi.colors.white; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignVCenter; - } - - Rectangle { - id: removeHmdContainer; - z: 998; - visible: false; - - gradient: Gradient { - GradientStop { - position: 0.2; - color: hifi.colors.baseGrayHighlight; - } - GradientStop { - position: 1.0; - color: hifi.colors.baseGrayShadow; - } - } - anchors.fill: backupInstructionsButton; - radius: 5; - MouseArea { - anchors.fill: parent; - propagateComposedEvents: false; - hoverEnabled: true; - } - - RalewayBold { - anchors.fill: parent; - text: "INSTRUCTIONS OPEN ON DESKTOP"; - size: 15; - color: hifi.colors.white; - verticalAlignment: Text.AlignVCenter; - horizontalAlignment: Text.AlignHCenter; - } - - Timer { - id: removeHmdContainerTimer; - interval: 5000; - onTriggered: removeHmdContainer.visible = false - } - } - - HifiControlsUit.Button { - id: backupInstructionsButton; - text: "View Backup Instructions"; - color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.dark; - anchors.left: explanitoryText.left; - anchors.right: explanitoryText.right; - anchors.top: explanitoryText.bottom; - anchors.topMargin: 16; - height: 40; - - onClicked: { - var keyPath = "file:///" + root.keyFilePath.substring(0, root.keyFilePath.lastIndexOf('/')); - Qt.openUrlExternally(keyPath + "/backup_instructions.html"); - Qt.openUrlExternally(keyPath); - removeHmdContainer.visible = true; - removeHmdContainerTimer.start(); - } - } - } } // diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 603d7fb676..ffd06cb4a8 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -399,6 +399,9 @@ Rectangle { onSendSignalToWallet: { if (msg.method === 'walletReset' || msg.method === 'passphraseReset') { sendToScript(msg); + } else if (msg.method === 'walletSecurity_changeSecurityImage') { + securityImageChange.initModel(); + root.activeView = "securityImageChange"; } } } @@ -803,12 +806,24 @@ Rectangle { } function walletResetSetup() { + /* Bypass all this and do it automatically root.activeView = "walletSetup"; var timestamp = new Date(); walletSetup.startingTimestamp = timestamp; walletSetup.setupAttemptID = generateUUID(); UserActivityLogger.commerceWalletSetupStarted(timestamp, walletSetup.setupAttemptID, walletSetup.setupFlowVersion, walletSetup.referrer ? walletSetup.referrer : "wallet app", (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : '')); + */ + + var randomNumber = Math.floor(Math.random() * 34) + 1; + var securityImagePath = "images/" + addLeadingZero(randomNumber) + ".jpg"; + Commerce.getWalletAuthenticatedStatus(); // before writing security image, ensures that salt/account password is set. + Commerce.chooseSecurityImage(securityImagePath); + Commerce.generateKeyPair(); + } + + function addLeadingZero(n) { + return n < 10 ? '0' + n : '' + n; } function followReferrer(msg) { diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 702251f867..67303f2a9b 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -31,7 +31,9 @@ QJsonObject Ledger::apiResponse(const QString& label, QNetworkReply* reply) { QByteArray response = reply->readAll(); QJsonObject data = QJsonDocument::fromJson(response).object(); +#if defined(DEV_BUILD) // Don't expose user's personal data in the wild. But during development this can be handy. qInfo(commerce) << label << "response" << QJsonDocument(data).toJson(QJsonDocument::Compact); +#endif return data; } // Non-200 responses are not json: @@ -69,7 +71,9 @@ void Ledger::send(const QString& endpoint, const QString& success, const QString auto accountManager = DependencyManager::get(); const QString URL = "/api/v1/commerce/"; JSONCallbackParameters callbackParams(this, success, fail); +#if defined(DEV_BUILD) // Don't expose user's personal data in the wild. But during development this can be handy. qCInfo(commerce) << "Sending" << endpoint << QJsonDocument(request).toJson(QJsonDocument::Compact); +#endif accountManager->sendRequest(URL + endpoint, authType, method, @@ -117,7 +121,7 @@ void Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, cons signedSend("transaction", transactionString, hfc_key, "buy", "buySuccess", "buyFailure", controlled_failure); } -bool Ledger::receiveAt(const QString& hfc_key, const QString& signing_key) { +bool Ledger::receiveAt(const QString& hfc_key, const QString& signing_key, const QByteArray& locker) { auto accountManager = DependencyManager::get(); if (!accountManager->isLoggedIn()) { qCWarning(commerce) << "Cannot set receiveAt when not logged in."; @@ -125,11 +129,25 @@ bool Ledger::receiveAt(const QString& hfc_key, const QString& signing_key) { emit receiveAtResult(result); return false; // We know right away that we will fail, so tell the caller. } - - signedSend("public_key", hfc_key.toUtf8(), signing_key, "receive_at", "receiveAtSuccess", "receiveAtFailure"); + QJsonObject transaction; + transaction["public_key"] = hfc_key; + transaction["locker"] = QString::fromUtf8(locker); + QJsonDocument transactionDoc{ transaction }; + auto transactionString = transactionDoc.toJson(QJsonDocument::Compact); + signedSend("text", transactionString, signing_key, "receive_at", "receiveAtSuccess", "receiveAtFailure"); return true; // Note that there may still be an asynchronous signal of failure that callers might be interested in. } +bool Ledger::receiveAt() { + auto wallet = DependencyManager::get(); + auto keys = wallet->listPublicKeys(); + if (keys.isEmpty()) { + return false; + } + auto key = keys.first(); + return receiveAt(key, key, wallet->getWallet()); +} + void Ledger::balance(const QStringList& keys) { keysQuery("balance", "balanceSuccess", "balanceFailure"); } @@ -283,24 +301,30 @@ void Ledger::accountSuccess(QNetworkReply* reply) { auto iv = QByteArray::fromBase64(data["iv"].toString().toUtf8()); auto ckey = QByteArray::fromBase64(data["ckey"].toString().toUtf8()); QString remotePublicKey = data["public_key"].toString(); + const QByteArray locker = data["locker"].toString().toUtf8(); bool isOverride = wallet->wasSoftReset(); wallet->setSalt(salt); wallet->setIv(iv); wallet->setCKey(ckey); + if (!locker.isEmpty()) { + wallet->setWallet(locker); + wallet->setPassphrase("ACCOUNT"); // We only locker wallets that have been converted to account-based auth. + } QString keyStatus = "ok"; QStringList localPublicKeys = wallet->listPublicKeys(); if (remotePublicKey.isEmpty() || isOverride) { - if (!localPublicKeys.isEmpty()) { - QString key = localPublicKeys.first(); - receiveAt(key, key); + if (!localPublicKeys.isEmpty()) { // Let the metaverse know about a local wallet. + receiveAt(); } } else { if (localPublicKeys.isEmpty()) { keyStatus = "preexisting"; } else if (localPublicKeys.first() != remotePublicKey) { keyStatus = "conflicting"; + } else if (locker.isEmpty()) { // Matches metaverse data, but we haven't lockered it yet. + receiveAt(); } } diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index ba2f167f4b..427395ee11 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -26,7 +26,8 @@ class Ledger : public QObject, public Dependency { public: void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const bool controlled_failure = false); - bool receiveAt(const QString& hfc_key, const QString& signing_key); + bool receiveAt(const QString& hfc_key, const QString& signing_key, const QByteArray& locker); + bool receiveAt(); void balance(const QStringList& keys); void inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage); void history(const QStringList& keys, const int& pageNumber, const int& itemsPerPage); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index ef6b4654f5..5b8417be7c 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -131,7 +131,7 @@ bool Wallet::writeBackupInstructions() { QFile outputFile(outputFilename); bool retval = false; - if (getKeyFilePath() == "") + if (getKeyFilePath().isEmpty()) { return false; } @@ -190,6 +190,30 @@ bool writeKeys(const char* filename, EC_KEY* keys) { return retval; } +bool Wallet::setWallet(const QByteArray& wallet) { + QFile file(keyFilePath()); + if (!file.open(QIODevice::WriteOnly)) { + qCCritical(commerce) << "Unable to open wallet for write in" << keyFilePath(); + return false; + } + if (file.write(wallet) != wallet.count()) { + qCCritical(commerce) << "Unable to write wallet in" << keyFilePath(); + return false; + } + file.close(); + return true; +} +QByteArray Wallet::getWallet() { + QFile file(keyFilePath()); + if (!file.open(QIODevice::ReadOnly)) { + qCInfo(commerce) << "No existing wallet in" << keyFilePath(); + return QByteArray(); + } + QByteArray wallet = file.readAll(); + file.close(); + return wallet; +} + QPair generateECKeypair() { EC_KEY* keyPair = EC_KEY_new_by_curve_name(NID_secp256k1); @@ -334,7 +358,7 @@ Wallet::Wallet() { uint status; QString keyStatus = result.contains("data") ? result["data"].toObject()["keyStatus"].toString() : ""; - if (wallet->getKeyFilePath() == "" || !wallet->getSecurityImage()) { + if (wallet->getKeyFilePath().isEmpty() || !wallet->getSecurityImage()) { if (keyStatus == "preexisting") { status = (uint) WalletStatus::WALLET_STATUS_PREEXISTING; } else{ @@ -524,15 +548,23 @@ bool Wallet::walletIsAuthenticatedWithPassphrase() { // FIXME: initialize OpenSSL elsewhere soon initialize(); + qCDebug(commerce) << "walletIsAuthenticatedWithPassphrase: checking" << (!_passphrase || !_passphrase->isEmpty()); // this should always be false if we don't have a passphrase // cached yet if (!_passphrase || _passphrase->isEmpty()) { - return false; + if (!getKeyFilePath().isEmpty()) { // If file exists, then it is an old school file that has not been lockered. Must get user's passphrase. + qCDebug(commerce) << "walletIsAuthenticatedWithPassphrase: No passphrase, but there is an existing wallet."; + return false; + } else { + qCDebug(commerce) << "walletIsAuthenticatedWithPassphrase: New setup."; + setPassphrase("ACCOUNT"); // Going forward, consider this an account-based client. + } } if (_publicKeys.count() > 0) { // we _must_ be authenticated if the publicKeys are there DependencyManager::get()->setWalletStatus((uint)WalletStatus::WALLET_STATUS_READY); + qCDebug(commerce) << "walletIsAuthenticatedWithPassphrase: wallet was ready"; return true; } @@ -545,10 +577,15 @@ bool Wallet::walletIsAuthenticatedWithPassphrase() { // be sure to add the public key so we don't do this over and over _publicKeys.push_back(publicKey.toBase64()); + + if (*_passphrase != "ACCOUNT") { + changePassphrase("ACCOUNT"); // Rewrites with salt and constant, and will be lockered that way. + } + qCDebug(commerce) << "walletIsAuthenticatedWithPassphrase: wallet now ready"; return true; } } - + qCDebug(commerce) << "walletIsAuthenticatedWithPassphrase: wallet not ready"; return false; } @@ -559,6 +596,7 @@ bool Wallet::generateKeyPair() { qCInfo(commerce) << "Generating keypair."; auto keyPair = generateECKeypair(); if (!keyPair.first) { + qCWarning(commerce) << "Empty keypair"; return false; } @@ -576,7 +614,7 @@ bool Wallet::generateKeyPair() { // 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, key); + return ledger->receiveAt(key, key, getWallet()); } QStringList Wallet::listPublicKeys() { @@ -666,11 +704,13 @@ void Wallet::chooseSecurityImage(const QString& filename) { // there _is_ a keyfile, we need to update it (similar to changing the // passphrase, we need to do so into a temp file and move it). if (!QFile(keyFilePath()).exists()) { + qCDebug(commerce) << "initial security pic set for empty wallet"; emit securityImageResult(true); return; } bool success = writeWallet(); + qCDebug(commerce) << "updated security pic" << success; emit securityImageResult(success); } @@ -715,6 +755,11 @@ QString Wallet::getKeyFilePath() { bool Wallet::writeWallet(const QString& newPassphrase) { EC_KEY* keys = readKeys(keyFilePath().toStdString().c_str()); + auto ledger = DependencyManager::get(); + // Remove any existing locker, because it will be out of date. + if (!_publicKeys.isEmpty() && !ledger->receiveAt(_publicKeys.first(), _publicKeys.first(), QByteArray())) { + return false; // FIXME: receiveAt could fail asynchronously. + } if (keys) { // we read successfully, so now write to a new temp file QString tempFileName = QString("%1.%2").arg(keyFilePath(), QString("temp")); @@ -722,6 +767,7 @@ bool Wallet::writeWallet(const QString& newPassphrase) { if (!newPassphrase.isEmpty()) { setPassphrase(newPassphrase); } + if (writeKeys(tempFileName.toStdString().c_str(), keys)) { if (writeSecurityImage(_securityImage, tempFileName)) { // ok, now move the temp file to the correct spot @@ -729,6 +775,11 @@ bool Wallet::writeWallet(const QString& newPassphrase) { QFile(tempFileName).rename(QString(keyFilePath())); qCDebug(commerce) << "wallet written successfully"; emit keyFilePathIfExistsResult(getKeyFilePath()); + if (!walletIsAuthenticatedWithPassphrase() || !ledger->receiveAt()) { + // FIXME: Should we fail the whole operation? + // Tricky, because we'll need the the key and file from the TEMP location... + qCWarning(commerce) << "Failed to update locker"; + } return true; } else { qCDebug(commerce) << "couldn't write security image to temp wallet"; diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index 665afd9a23..c096713058 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -73,6 +73,7 @@ private slots: void handleChallengeOwnershipPacket(QSharedPointer packet, SharedNodePointer sendingNode); private: + friend class Ledger; QStringList _publicKeys{}; QPixmap* _securityImage { nullptr }; QByteArray _salt; @@ -87,6 +88,9 @@ private: bool readSecurityImage(const QString& inputFilePath, unsigned char** outputBufferPtr, int* outputBufferLen); bool writeBackupInstructions(); + bool setWallet(const QByteArray& wallet); + QByteArray getWallet(); + void account(); };