From a1d88a8588184de8983ee30fa347aad294f660eb Mon Sep 17 00:00:00 2001 From: Zach Fox <fox@highfidelity.io> Date: Thu, 23 Aug 2018 14:45:16 -0700 Subject: [PATCH 1/2] 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 <br><br><b><font color='#0093C5'><a href='#bank'>BankOfHighFidelity.</a></font></b> 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. \ -<br><br>It is very important to back up your hifikey file! \ -<b><font color='#0093C5'><a href='#privateKeyPath'>Tap here to open the folder where your HifiKeys are stored on your main display.</a></font></b>" - } - 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. \ -<b><font color='#0093C5'><a href='#privateKeyPath'>Tap here to open the folder where your HifiKeys are stored on your main display.</a></font></b>"; - } - 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:<ul>\ -<li>If you have backed up your hifikey file before, search your backup location</li>\ -<li>Search your AppData directory in the last machine you used to set up the Wallet</li>\ -<li>If you are a developer and have installed multiple builds of High Fidelity, your hifikey file might be in another folder</li>\ -</ul><br><br>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! \ -<br><br>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. \ -<br><br>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.\ -<br><br>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. \ +<br><br><b><font color='#0093C5'><a href='#securitypic'>Tap here to change your Security Pic.</a></font></b>"; } 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<AccountManager>(); 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<AccountManager>(); 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<Wallet>(); + 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<QByteArray*, QByteArray*> 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<WalletScriptingInterface>()->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<Ledger>(); - 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<Ledger>(); + // 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<ReceivedMessage> 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(); }; From dddb2141f041422c0b9365be84480003f28037c1 Mon Sep 17 00:00:00 2001 From: Zach Fox <fox@highfidelity.io> Date: Thu, 23 Aug 2018 15:45:29 -0700 Subject: [PATCH 2/2] Implement Wallet Security feature - Auto Logout checkbox --- .../qml/LoginDialog/LinkAccountBody.qml | 1 + .../qml/hifi/commerce/wallet/Security.qml | 75 +++++++++++++++++++ .../qml/hifi/commerce/wallet/Wallet.qml | 11 +++ interface/src/Application.cpp | 6 ++ scripts/system/commerce/wallet.js | 7 ++ 5 files changed, 100 insertions(+) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 814778a4b1..4c6e5f6fce 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -120,6 +120,7 @@ Item { TextField { id: usernameField + text: Settings.getValue("wallet/savedUsername", ""); width: parent.width focus: true label: "Username or Email" diff --git a/interface/resources/qml/hifi/commerce/wallet/Security.qml b/interface/resources/qml/hifi/commerce/wallet/Security.qml index e021328ebe..14ac696ef7 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Security.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Security.qml @@ -140,6 +140,81 @@ Item { } } } + + Item { + id: autoLogoutContainer; + anchors.top: changeSecurityImageContainer.bottom; + anchors.topMargin: 8; + anchors.left: parent.left; + anchors.leftMargin: 40; + anchors.right: parent.right; + anchors.rightMargin: 55; + height: 75; + + HiFiGlyphs { + id: autoLogoutImage; + text: hifi.glyphs.walletKey; + // Size + size: 80; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 20; + anchors.left: parent.left; + // Style + color: hifi.colors.white; + } + + HifiControlsUit.CheckBox { + id: autoLogoutCheckbox; + checked: Settings.getValue("wallet/autoLogout", false); + text: "Automatically Log Out when Exiting Interface" + // Anchors + anchors.verticalCenter: autoLogoutImage.verticalCenter; + anchors.left: autoLogoutImage.right; + anchors.leftMargin: 20; + anchors.right: autoLogoutHelp.left; + anchors.rightMargin: 12; + boxSize: 28; + labelFontSize: 18; + color: hifi.colors.white; + onCheckedChanged: { + Settings.setValue("wallet/autoLogout", checked); + if (checked) { + Settings.setValue("wallet/savedUsername", Account.username); + } else { + Settings.setValue("wallet/savedUsername", ""); + } + } + } + + RalewaySemiBold { + id: autoLogoutHelp; + text: '[?]'; + // Anchors + anchors.verticalCenter: autoLogoutImage.verticalCenter; + anchors.right: parent.right; + width: 30; + height: 30; + // Text size + size: 18; + // Style + color: hifi.colors.blueHighlight; + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + parent.color = hifi.colors.blueAccent; + } + onExited: { + parent.color = hifi.colors.blueHighlight; + } + onClicked: { + sendSignalToWallet({method: 'walletSecurity_autoLogoutHelp'}); + } + } + } + } } // diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index ffd06cb4a8..65d98af234 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -382,6 +382,17 @@ Rectangle { } else if (msg.method === 'walletSecurity_changeSecurityImage') { securityImageChange.initModel(); root.activeView = "securityImageChange"; + } else if (msg.method === 'walletSecurity_autoLogoutHelp') { + lightboxPopup.titleText = "Automatically Log Out"; + lightboxPopup.bodyText = "By default, after you log in to High Fidelity, you will stay logged in to your High Fidelity " + + "account even after you close and re-open Interface. This means anyone who opens Interface on your computer " + + "could make purchases with your Wallet.\n\n" + + "If you do not want to stay logged in across Interface sessions, check this box."; + lightboxPopup.button1text = "CLOSE"; + lightboxPopup.button1method = function() { + lightboxPopup.visible = false; + } + lightboxPopup.visible = true; } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1d515392b0..60d23c94dd 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -375,6 +375,7 @@ static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin"; static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; +static const QString AUTO_LOGOUT_SETTING_NAME = "wallet/autoLogout"; const std::vector<std::pair<QString, Application::AcceptURLMethod>> Application::_acceptedExtensions { { SVO_EXTENSION, &Application::importSVOFromURL }, @@ -1730,6 +1731,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo QTimer* settingsTimer = new QTimer(); moveToNewNamedThread(settingsTimer, "Settings Thread", [this, settingsTimer]{ connect(qApp, &Application::beforeAboutToQuit, [this, settingsTimer]{ + bool autoLogout = Setting::Handle<bool>(AUTO_LOGOUT_SETTING_NAME, false).get(); + if (autoLogout) { + auto accountManager = DependencyManager::get<AccountManager>(); + accountManager->logout(); + } // Disconnect the signal from the save settings QObject::disconnect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); // Stop the settings timer diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index ac25269e41..8730273e7c 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -406,6 +406,11 @@ sendMoneyRecipient = null; } + function onUsernameChanged() { + Settings.setValue("wallet/autoLogout", false); + Settings.setValue("wallet/savedUsername", ""); + } + // Function Name: fromQml() // // Description: @@ -581,6 +586,7 @@ var tablet = null; var walletEnabled = Settings.getValue("commerce", true); function startup() { + GlobalServices.myUsernameChanged.connect(onUsernameChanged); if (walletEnabled) { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ @@ -612,6 +618,7 @@ removeOverlays(); } function shutdown() { + GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); button.clicked.disconnect(onButtonClicked); tablet.removeButton(button); deleteSendMoneyParticleEffect();