diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index a0664f4f9b..04cf5a671e 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -18,10 +18,12 @@ #include #include +#include #include #include #include +#include #include #include @@ -39,7 +41,8 @@ #endif static const char* KEY_FILE = "hifikey"; -static const char* IMAGE_FILE = "hifi_image"; // eventually this will live in keyfile +static const char* IMAGE_HEADER = "-----BEGIN SECURITY IMAGE-----\n"; +static const char* IMAGE_FOOTER = "-----END SECURITY IMAGE-----\n"; void initialize() { static bool initialized = false; @@ -52,11 +55,8 @@ void initialize() { } QString keyFilePath() { - return PathUtils::getAppDataFilePath(KEY_FILE); -} - -QString imageFilePath() { - return PathUtils::getAppDataFilePath(IMAGE_FILE); + auto accountManager = DependencyManager::get(); + return PathUtils::getAppDataFilePath(QString("%1.%2").arg(accountManager->getAccountInfo().getUsername(), KEY_FILE)); } // use the cached _passphrase if it exists, otherwise we need to prompt @@ -131,13 +131,12 @@ bool writeKeys(const char* filename, RSA* keys) { return retval; } - -// 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 // -// NOTE: we don't really use the private keys returned - we can see how this evolves, but probably +// TODO: we don't really use the private keys returned - we can see how this evolves, but probably // we should just return a list of public keys? +// or perhaps return the RSA* instead? QPair generateRSAKeypair() { RSA* keyPair = RSA_new(); @@ -265,6 +264,15 @@ RSA* readPrivateKey(const char* 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(); @@ -287,8 +295,7 @@ void Wallet::setPassphrase(const QString& passphrase) { _publicKeys.clear(); } -// encrypt some stuff -bool Wallet::encryptFile(const QString& inputFilePath, const QString& outputFilePath) { +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 @@ -299,16 +306,12 @@ bool Wallet::encryptFile(const QString& inputFilePath, const QString& outputFile initializeAESKeys(ivec, ckey, _salt); int tempSize, outSize; + QByteArray inputFileBuffer; + QBuffer buffer(&inputFileBuffer); + buffer.open(QIODevice::WriteOnly); - // read entire unencrypted file into memory - QFile inputFile(inputFilePath); - if (!inputFile.exists()) { - qCDebug(commerce) << "cannot encrypt" << inputFilePath << "file doesn't exist"; - return false; - } - inputFile.open(QIODevice::ReadOnly); - QByteArray inputFileBuffer = inputFile.readAll(); - inputFile.close(); + // 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]; @@ -337,16 +340,21 @@ bool Wallet::encryptFile(const QString& inputFilePath, const QString& outputFile 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::WriteOnly); - outputFile.write(output); + outputFile.open(QIODevice::Append); + outputFile.write(IMAGE_HEADER); + outputBase64WithNewlines(outputFile, b64output); + outputFile.write(IMAGE_FOOTER); outputFile.close(); delete[] outputFileBuffer; return true; } -bool Wallet::decryptFile(const QString& inputFilePath, unsigned char** outputBufferPtr, int* outputBufferSize) { +bool Wallet::readSecurityImage(const QString& inputFilePath, unsigned char** outputBufferPtr, int* outputBufferSize) { unsigned char ivec[16]; unsigned char ckey[32]; initializeAESKeys(ivec, ckey, _salt); @@ -357,9 +365,31 @@ bool Wallet::decryptFile(const QString& inputFilePath, unsigned char** outputBuf qCDebug(commerce) << "cannot decrypt file" << inputFilePath << "it doesn't exist"; return false; } - inputFile.open(QIODevice::ReadOnly); - QByteArray encryptedBuffer = inputFile.readAll(); + 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); + } else { + foundFooter = (line == IMAGE_FOOTER); + if (!foundFooter) { + base64EncryptedBuffer.append(line); + } + } + } inputFile.close(); + if (! (foundHeader && foundFooter)) { + qCDebug(commerce) << "couldn't parse" << inputFilePath << foundHeader << foundFooter; + return false; + } + + // convert to bytes + auto encryptedBuffer = QByteArray::fromBase64(base64EncryptedBuffer); // setup decrypted buffer unsigned char* outputBuffer = new unsigned char[encryptedBuffer.size()]; @@ -428,6 +458,9 @@ bool Wallet::generateKeyPair() { qCInfo(commerce) << "Generating keypair."; auto keyPair = generateRSAKeypair(); + + // TODO: redo this soon -- need error checking and so on + writeSecurityImage(_securityImage, keyFilePath()); sendKeyFilePathIfExists(); QString oldKey = _publicKeys.count() == 0 ? "" : _publicKeys.last(); QString key = keyPair.first->toBase64(); @@ -493,27 +526,30 @@ void Wallet::chooseSecurityImage(const QString& filename) { if (_securityImage) { delete _securityImage; } - // temporary... QString path = qApp->applicationDirPath(); path.append("/resources/qml/hifi/commerce/wallet/"); path.append(filename); + // now create a new security image pixmap _securityImage = new QPixmap(); qCDebug(commerce) << "loading data for pixmap from" << path; _securityImage->load(path); - // encrypt it and save. - if (encryptFile(path, imageFilePath())) { - qCDebug(commerce) << "emitting pixmap"; - - updateImageProvider(); + // update the image now + updateImageProvider(); + // we could be choosing the _inital_ security image. If so, there + // will be no hifikey file yet. If that is the case, we are done. If + // 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()) { emit securityImageResult(true); - } else { - qCDebug(commerce) << "failed to encrypt security image"; - emit securityImageResult(false); + return; } + + bool success = writeWallet(); + emit securityImageResult(success); } void Wallet::getSecurityImage() { @@ -526,10 +562,11 @@ void Wallet::getSecurityImage() { return; } - // decrypt and return - QString filePath(imageFilePath()); - QFileInfo fileInfo(filePath); - if (fileInfo.exists() && decryptFile(filePath, &data, &dataLen)) { + bool success = false; + // decrypt and return. Don't bother if we have no file to decrypt, or + // no salt set yet. + QFileInfo fileInfo(keyFilePath()); + if (fileInfo.exists() && _salt.size() > 0 && readSecurityImage(keyFilePath(), &data, &dataLen)) { // create the pixmap _securityImage = new QPixmap(); _securityImage->loadFromData(data, dataLen, "jpg"); @@ -538,11 +575,9 @@ void Wallet::getSecurityImage() { updateImageProvider(); delete[] data; - emit securityImageResult(true); - } else { - qCDebug(commerce) << "failed to decrypt security image (maybe none saved yet?)"; - emit securityImageResult(false); + success = true; } + emit securityImageResult(success); } void Wallet::sendKeyFilePathIfExists() { QString filePath(keyFilePath()); @@ -566,35 +601,47 @@ void Wallet::reset() { QFile keyFile(keyFilePath()); - QFile imageFile(imageFilePath()); keyFile.remove(); - imageFile.remove(); +} +bool Wallet::writeWallet(const QString& newPassphrase) { + RSA* keys = readKeys(keyFilePath().toStdString().c_str()); + if (keys) { + // we read successfully, so now write to a new temp file + QString tempFileName = QString("%1.%2").arg(keyFilePath(), QString("temp")); + QString oldPassphrase = *_passphrase; + 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 + QFile(QString(keyFilePath())).remove(); + QFile(tempFileName).rename(QString(keyFilePath())); + qCDebug(commerce) << "wallet written successfully"; + return true; + } else { + qCDebug(commerce) << "couldn't write security image to temp wallet"; + } + } else { + qCDebug(commerce) << "couldn't write keys to temp wallet"; + } + // if we are here, we failed, so cleanup + QFile(tempFileName).remove(); + if (!newPassphrase.isEmpty()) { + setPassphrase(oldPassphrase); + } + + } else { + qCDebug(commerce) << "couldn't read wallet - bad passphrase?"; + // TODO: review this, but it seems best to reset the passphrase + // since we couldn't decrypt the existing wallet (or is doesn't + // exist perhaps). + setPassphrase(""); + } + return false; } bool Wallet::changePassphrase(const QString& newPassphrase) { qCDebug(commerce) << "changing passphrase"; - RSA* keys = readKeys(keyFilePath().toStdString().c_str()); - if (keys) { - // we read successfully, so now write to a new temp file - // save old passphrase just in case - // TODO: force re-enter? - QString oldPassphrase = *_passphrase; - setPassphrase(newPassphrase); - QString tempFileName = QString("%1.%2").arg(keyFilePath(), QString("temp")); - if (writeKeys(tempFileName.toStdString().c_str(), keys)) { - // ok, now move the temp file to the correct spot - QFile(QString(keyFilePath())).remove(); - QFile(tempFileName).rename(QString(keyFilePath())); - qCDebug(commerce) << "passphrase changed successfully"; - return true; - } else { - qCDebug(commerce) << "couldn't write keys"; - QFile(tempFileName).remove(); - setPassphrase(oldPassphrase); - return false; - } - } - qCDebug(commerce) << "couldn't decrypt keys with current passphrase, clearing"; - setPassphrase(QString("")); - return false; + return writeWallet(newPassphrase); } diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index f3c2ac399b..b8913e9a5e 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -55,14 +55,15 @@ signals: private: QStringList _publicKeys{}; QPixmap* _securityImage { nullptr }; - QByteArray _salt {"iamsalt!"}; + QByteArray _salt; QByteArray _iv; QByteArray _ckey; QString* _passphrase { new QString("") }; + bool writeWallet(const QString& newPassphrase = QString("")); void updateImageProvider(); - bool encryptFile(const QString& inputFilePath, const QString& outputFilePath); - bool decryptFile(const QString& inputFilePath, unsigned char** outputBufferPtr, int* outputBufferLen); + bool writeSecurityImage(const QPixmap* pixmap, const QString& outputFilePath); + bool readSecurityImage(const QString& inputFilePath, unsigned char** outputBufferPtr, int* outputBufferLen); }; #endif // hifi_Wallet_h diff --git a/libraries/ui/src/ui/ImageProvider.cpp b/libraries/ui/src/ui/ImageProvider.cpp index c74ed0cb44..7bbad43f2e 100644 --- a/libraries/ui/src/ui/ImageProvider.cpp +++ b/libraries/ui/src/ui/ImageProvider.cpp @@ -51,5 +51,9 @@ QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize return _securityImage->copy(); } } - return QPixmap(); + // otherwise just return a grey pixmap. This avoids annoying error messages in qml we would get + // when sending a 'null' pixmap (QPixmap()) + QPixmap greyPixmap(200, 200); + greyPixmap.fill(QColor("darkGrey")); + return greyPixmap; }