Zach FoxAlmost thered4e9a708 years agoHistory
911 lines · 31.8 KB · C++
RawDownload
////  Wallet.cpp//  interface/src/commerce////  Created by Howard Stearns on 8/4/17.//  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// #include "Wallet.h" #include <openssl/ssl.h>#include <openssl/err.h>#include <openssl/x509.h>#include <openssl/pem.h>#include <openssl/evp.h>#include <openssl/aes.h>#include <openssl/ecdsa.h>// I know, right?  But per https://www.openssl.org/docs/faq.html// this avoids OPENSSL_Uplink(00007FF847238000,08): no OPENSSL_Applink// at runtime.#ifdef Q_OS_WIN#include <openssl/applink.c>#endif #include <QFile>#include <QCryptographicHash>#include <QQmlContext>#include <QBuffer> #include <PathUtils.h>#include <OffscreenUi.h>#include <AccountManager.h>#include <ui/TabletScriptingInterface.h> #include "Application.h"#include "CommerceLogging.h"#include "Ledger.h"#include "ui/SecurityImageProvider.h"#include "scripting/HMDScriptingInterface.h" static const char* KEY_FILE = "hifikey";static const char* INSTRUCTIONS_FILE = "backup_instructions.html";static const char* IMAGE_HEADER = "-----BEGIN SECURITY IMAGE-----\n";static const char* IMAGE_FOOTER = "-----END SECURITY IMAGE-----\n"; void initialize() {    static bool initialized = false;    if (!initialized) {        SSL_load_error_strings();        SSL_library_init();        OpenSSL_add_all_algorithms();        initialized = true;    }} QString keyFilePath() {    auto accountManager = DependencyManager::get<AccountManager>();    return PathUtils::getAppDataFilePath(QString("%1.%2").arg(accountManager->getAccountInfo().getUsername(), KEY_FILE));}bool Wallet::copyKeyFileFrom(const QString& pathname) {    QString existing = getKeyFilePath();    qCDebug(commerce) << "Old keyfile" << existing;    if (!existing.isEmpty()) {        QString backup = QString(existing).insert(existing.indexOf(KEY_FILE) - 1,            QDateTime::currentDateTime().toString(Qt::ISODate).replace(":", ""));        qCDebug(commerce) << "Renaming old keyfile to" << backup;        if (!QFile::rename(existing, backup)) {            qCCritical(commerce) << "Unable to backup" << existing << "to" << backup;            return false;        }    }    QString destination = keyFilePath();    bool result = QFile::copy(pathname, destination);    qCDebug(commerce) << "copy" << pathname << "to" << destination << "=>" << result;    return result;} // use the cached _passphrase if it exists, otherwise we need to promptint passwordCallback(char* password, int maxPasswordSize, int rwFlag, void* u) {    // just return a hardcoded pwd for now    auto wallet = DependencyManager::get<Wallet>();    auto passphrase = wallet->getPassphrase();    if (passphrase && !passphrase->isEmpty()) {        QString saltedPassphrase(*passphrase);        saltedPassphrase.append(wallet->getSalt());        strcpy(password, saltedPassphrase.toUtf8().constData());        return static_cast<int>(passphrase->size());    } else {        // this shouldn't happen - so lets log it to tell us we have        // a problem with the flow...        qCCritical(commerce) << "no cached passphrase while decrypting!";        return 0;    }} EC_KEY* readKeys(const char* filename) {    FILE* fp;    EC_KEY *key = NULL;    if ((fp = fopen(filename, "rt"))) {        // file opened successfully        qCDebug(commerce) << "opened key file" << filename;         if ((key = PEM_read_EC_PUBKEY(fp, NULL, NULL, NULL))) {            // now read private key             qCDebug(commerce) << "read public key";             if ((key = PEM_read_ECPrivateKey(fp, &key, passwordCallback, NULL))) {                qCDebug(commerce) << "read private key";                fclose(fp);                return key;            }            qCDebug(commerce) << "failed to read private key";        } else {            qCDebug(commerce) << "failed to read public key";        }        fclose(fp);    } else {        qCDebug(commerce) << "failed to open key file" << filename;    }    return key;} bool Wallet::writeBackupInstructions() {    QString inputFilename(PathUtils::resourcesPath() + "html/commerce/backup_instructions.html");    QString outputFilename = PathUtils::getAppDataFilePath(INSTRUCTIONS_FILE);    QFile inputFile(inputFilename);    QFile outputFile(outputFilename);    bool retval = false;     if (getKeyFilePath().isEmpty())    {        return false;    }     if (QFile::exists(inputFilename) && inputFile.open(QIODevice::ReadOnly)) {        if (outputFile.open(QIODevice::ReadWrite)) {            // Read the data from the original file, then close it            QByteArray fileData = inputFile.readAll();            inputFile.close();             // Translate the data from the original file into a QString            QString text(fileData);             // Replace the necessary string            text.replace(QString("HIFIKEY_PATH_REPLACEME"), keyFilePath());             // Write the new text back to the file            outputFile.write(text.toUtf8());             // Close the output file            outputFile.close();               retval = true;            qCDebug(commerce) << "wrote html file successfully";        } else {            qCDebug(commerce) << "failed to open output html file" << outputFilename;        }    } else {        qCDebug(commerce) << "failed to open input html file" << inputFilename;    }    return retval;} bool writeKeys(const char* filename, EC_KEY* keys) {    FILE* fp;    bool retval = false;    if ((fp = fopen(filename, "wt"))) {        if (!PEM_write_EC_PUBKEY(fp, keys)) {            fclose(fp);            qCCritical(commerce) << "failed to write public key";            return retval;        }         if (!PEM_write_ECPrivateKey(fp, keys, EVP_des_ede3_cbc(), NULL, 0, passwordCallback, NULL)) {            fclose(fp);            qCCritical(commerce) << "failed to write private key";            return retval;        }         retval = true;        qCDebug(commerce) << "wrote keys successfully";        fclose(fp);    } else {        qCDebug(commerce) << "failed to open key file" << filename;    }    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);    QPair<QByteArray*, QByteArray*> retval{};     EC_KEY_set_asn1_flag(keyPair, OPENSSL_EC_NAMED_CURVE);    if (!EC_KEY_generate_key(keyPair)) {        qCDebug(commerce) << "Error generating EC Keypair -" << ERR_get_error();        return retval;    }     // grab the public key and private key from the file    unsigned char* publicKeyDER = NULL;    int publicKeyLength = i2d_EC_PUBKEY(keyPair, &publicKeyDER);     unsigned char* privateKeyDER = NULL;    int privateKeyLength = i2d_ECPrivateKey(keyPair, &privateKeyDER);     if (publicKeyLength <= 0 || privateKeyLength <= 0) {        qCDebug(commerce) << "Error getting DER public or private key from EC struct -" << ERR_get_error();          // cleanup the EC struct        EC_KEY_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;    }      if (!writeKeys(keyFilePath().toStdString().c_str(), keyPair)) {        qCDebug(commerce) << "couldn't save keys!";        return retval;    }     EC_KEY_free(keyPair);     // prepare the return values.  TODO: Fix this - we probably don't really even want the    // private key at all (better to read it when we need it?).  Or maybe we do, when we have    // multiple keys?    retval.first = new QByteArray(reinterpret_cast<char*>(publicKeyDER), publicKeyLength);    retval.second = new QByteArray(reinterpret_cast<char*>(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 arrayQByteArray readPublicKey(const char* filename) {    FILE* fp;    EC_KEY* key = NULL;    if ((fp = fopen(filename, "r"))) {        // file opened successfully        qCDebug(commerce) << "opened key file" << filename;        if ((key = PEM_read_EC_PUBKEY(fp, NULL, NULL, NULL))) {            // file read successfully            unsigned char* publicKeyDER = NULL;            int publicKeyLength = i2d_EC_PUBKEY(key, &publicKeyDER);            // TODO: check for 0 length?             // cleanup            EC_KEY_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 EC_KEY struct// so I'll return that.EC_KEY* readPrivateKey(const char* filename) {    FILE* fp;    EC_KEY* key = NULL;    if ((fp = fopen(filename, "r"))) {        // file opened successfully        qCDebug(commerce) << "opened key file" << filename;        if ((key = PEM_read_ECPrivateKey(fp, &key, passwordCallback, NULL))) {            qCDebug(commerce) << "parsed private key file successfully";         } else {            qCDebug(commerce) << "couldn't parse" << filename;            // if the passphrase is wrong, then let's not cache it            DependencyManager::get<Wallet>()->setPassphrase("");        }        fclose(fp);    } else {        qCDebug(commerce) << "couldn't open" << filename;    }    return key;} // QT's QByteArray will convert to Base64 without any embedded newlines.  This just// writes it with embedded newlines, which is more readable.void outputBase64WithNewlines(QFile& file, const QByteArray& b64Array) {    for (int i = 0; i < b64Array.size(); i += 64) {        file.write(b64Array.mid(i, 64));        file.write("\n");    }} void initializeAESKeys(unsigned char* ivec, unsigned char* ckey, const QByteArray& salt) {    // use the ones in the wallet    auto wallet = DependencyManager::get<Wallet>();    memcpy(ivec, wallet->getIv(), 16);    memcpy(ckey, wallet->getCKey(), 32);} Wallet::Wallet() {    auto nodeList = DependencyManager::get<NodeList>();    auto ledger = DependencyManager::get<Ledger>();    auto& packetReceiver = nodeList->getPacketReceiver();    _passphrase = new QString("");     packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "handleChallengeOwnershipPacket");    packetReceiver.registerListener(PacketType::ChallengeOwnershipRequest, this, "handleChallengeOwnershipPacket");     connect(ledger.data(), &Ledger::accountResult, this, [](QJsonObject result) {        auto wallet = DependencyManager::get<Wallet>();        auto walletScriptingInterface = DependencyManager::get<WalletScriptingInterface>();        uint status;        QString keyStatus = result.contains("data") ? result["data"].toObject()["keyStatus"].toString() : "";         if (wallet->getKeyFilePath().isEmpty() || !wallet->getSecurityImage()) {            if (keyStatus == "preexisting") {                status = (uint) WalletStatus::WALLET_STATUS_PREEXISTING;            } else{                status = (uint) WalletStatus::WALLET_STATUS_NOT_SET_UP;            }        } else if (!wallet->walletIsAuthenticatedWithPassphrase()) {            status = (uint) WalletStatus::WALLET_STATUS_NOT_AUTHENTICATED;        } else if (keyStatus == "conflicting") {            status = (uint) WalletStatus::WALLET_STATUS_CONFLICTING;        } else {            status = (uint) WalletStatus::WALLET_STATUS_READY;        }         walletScriptingInterface->setWalletStatus(status);    });     auto accountManager = DependencyManager::get<AccountManager>();    connect(accountManager.data(), &AccountManager::usernameChanged, this, [&]() {        getWalletStatus();        clear();    });} void Wallet::clear() {    _publicKeys.clear();     if (_securityImage) {        delete _securityImage;    }    _securityImage = nullptr;     // tell the provider we got nothing    updateImageProvider();    _passphrase->clear();} Wallet::~Wallet() {    if (_securityImage) {        delete _securityImage;    }     if (_passphrase) {        delete _passphrase;    }} bool Wallet::setPassphrase(const QString& passphrase) {    if (_passphrase) {        delete _passphrase;    }    _passphrase = new QString(passphrase);     _publicKeys.clear();     writeBackupInstructions();     return true;} bool Wallet::writeSecurityImage(const QPixmap* pixmap, const QString& outputFilePath) {    // aes requires a couple 128-bit keys (ckey and ivec).  For now, I'll just    // use the md5 of the salt as the ckey (md5 is 128-bit), and ivec will be    // a constant.  We can review this later - there are ways to generate keys    // from a password that may be better.    unsigned char ivec[16];    unsigned char ckey[32];     initializeAESKeys(ivec, ckey, _salt);     int tempSize, outSize;    QByteArray inputFileBuffer;    QBuffer buffer(&inputFileBuffer);    buffer.open(QIODevice::WriteOnly);     // another spot where we are assuming only jpgs    pixmap->save(&buffer, "jpg");     // reserve enough capacity for encrypted bytes    unsigned char* outputFileBuffer = new unsigned char[inputFileBuffer.size() + AES_BLOCK_SIZE];     EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();     // TODO: add error handling!!!    if (!EVP_EncryptInit_ex(ctx, EVP_des_ede3_cbc(), NULL, ckey, ivec)) {        qCDebug(commerce) << "encrypt init failure";        delete[] outputFileBuffer;        return false;    }    if (!EVP_EncryptUpdate(ctx, outputFileBuffer, &tempSize, (unsigned char*)inputFileBuffer.data(), inputFileBuffer.size())) {        qCDebug(commerce) << "encrypt update failure";        delete[] outputFileBuffer;        return false;    }    outSize = tempSize;    if (!EVP_EncryptFinal_ex(ctx, outputFileBuffer + outSize, &tempSize)) {        qCDebug(commerce) << "encrypt final failure";        delete[] outputFileBuffer;        return false;    }     outSize += tempSize;    EVP_CIPHER_CTX_free(ctx);    qCDebug(commerce) << "encrypted buffer size" << outSize;    QByteArray output((const char*)outputFileBuffer, outSize);     // now APPEND to the file,    QByteArray b64output = output.toBase64();    QFile outputFile(outputFilePath);    outputFile.open(QIODevice::Append);    outputFile.write(IMAGE_HEADER);    outputBase64WithNewlines(outputFile, b64output);    outputFile.write(IMAGE_FOOTER);    outputFile.close();     delete[] outputFileBuffer;    return true;} bool Wallet::readSecurityImage(const QString& inputFilePath, unsigned char** outputBufferPtr, int* outputBufferSize) {    unsigned char ivec[16];    unsigned char ckey[32];    initializeAESKeys(ivec, ckey, _salt);     // read encrypted file    QFile inputFile(inputFilePath);    if (!inputFile.exists()) {        qCDebug(commerce) << "cannot decrypt file" << inputFilePath << "it doesn't exist";        return false;    }    inputFile.open(QIODevice::ReadOnly | QIODevice::Text);    bool foundHeader = false;    bool foundFooter = false;     QByteArray base64EncryptedBuffer;     while (!inputFile.atEnd()) {        QString line(inputFile.readLine());        if (!foundHeader) {            foundHeader = (line == IMAGE_HEADER);

Lines 1–500 of 911.