// 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) { EC_KEY* ecPrivateKey = NULL; auto keyFilePathString = keyFilePath().toStdString(); if ((ecPrivateKey = readPrivateKey(keyFilePath().toStdString().c_str()))) { unsigned char* sig = new unsigned char[ECDSA_size(ecPrivateKey)]; unsigned int signatureBytes = 0; qCInfo(commerce) << "Hashing and signing plaintext" << text << "with key at address" << ecPrivateKey; QByteArray hashedPlaintext = QCryptographicHash::hash(text, QCryptographicHash::Sha256); int retrn = ECDSA_sign(0, reinterpret_cast<const unsigned char*>(hashedPlaintext.constData()), hashedPlaintext.size(), sig, &signatureBytes, ecPrivateKey); EC_KEY_free(ecPrivateKey); QByteArray signature(reinterpret_cast<const char*>(sig), signatureBytes); if (retrn != -1) { return signature.toBase64(); } } return QString(); }
bool Wallet::walletIsAuthenticatedWithPassphrase() { // try to read existing keys if they exist... // FIXME: initialize OpenSSL elsewhere soon initialize(); // this should always be false if we don't have a passphrase // cached yet if (!_passphrase || _passphrase->isEmpty()) { return false; } if (_publicKeys.count() > 0) { // we _must_ be authenticated if the publicKeys are there DependencyManager::get<WalletScriptingInterface>()->setWalletStatus((uint)WalletStatus::WALLET_STATUS_READY); return true; } // otherwise, we have a passphrase but no keys, so we have to check auto publicKey = readPublicKey(keyFilePath().toStdString().c_str()); if (publicKey.size() > 0) { if (auto key = readPrivateKey(keyFilePath().toStdString().c_str())) { EC_KEY_free(key); // be sure to add the public key so we don't do this over and over _publicKeys.push_back(publicKey.toBase64()); DependencyManager::get<WalletScriptingInterface>()->setWalletStatus((uint)WalletStatus::WALLET_STATUS_READY); return true; } } return false; }
bool Wallet::getSecurityImage() { unsigned char* data; int dataLen; // if already decrypted, don't do it again if (_securityImage) { updateImageProvider(); emit securityImageResult(true); return true; } 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"); qCDebug(commerce) << "created pixmap from encrypted file"; updateImageProvider(); delete[] data; success = true; } emit securityImageResult(success); return success; }
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; }
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; }
bool Wallet::writeWallet(const QString& newPassphrase) { EC_KEY* keys = readKeys(keyFilePath()); 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")); QString oldPassphrase = *_passphrase; if (!newPassphrase.isEmpty()) { setPassphrase(newPassphrase); } if (writeKeys(tempFileName, 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"; 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"; } } 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; }
void Wallet::chooseSecurityImage(const QString& filename) { if (_securityImage) { delete _securityImage; } QString path = PathUtils::resourcesPath(); path.append("/qml/hifi/dialogs/security/"); path.append(filename); // now create a new security image pixmap _securityImage = new QPixmap(); qCDebug(commerce) << "loading data for pixmap from" << path; _securityImage->load(path); // 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()) { 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); }
bool Wallet::generateKeyPair() { // FIXME: initialize OpenSSL elsewhere soon initialize(); qCInfo(commerce) << "Generating keypair."; auto keyPair = generateECKeypair(); if (!keyPair.first) { qCWarning(commerce) << "Empty keypair"; return false; } writeBackupInstructions(); // TODO: redo this soon -- need error checking and so on writeSecurityImage(_securityImage, keyFilePath()); QString key = keyPair.first->toBase64(); _publicKeys.push_back(key); qCDebug(commerce) << "public key:" << key; _isOverridingServer = false; // 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<Ledger>(); return ledger->receiveAt(key, key, getWallet()); }
bool Wallet::writeBackupInstructions() { QString inputFilename(PathUtils::resourcesPath() + "html/commerce/backup_instructions.html"); QString outputFilename = PathUtils::getAppDataFilePath(INSTRUCTIONS_FILE); QFile outputFile(outputFilename); bool retval = false; if (QFile::exists(outputFilename) || getKeyFilePath() == "") { return false; } QFile::copy(inputFilename, outputFilename); if (QFile::exists(outputFilename) && outputFile.open(QIODevice::ReadWrite)) { QByteArray fileData = outputFile.readAll(); QString text(fileData); text.replace(QString("HIFIKEY_PATH_REPLACEME"), keyFilePath()); outputFile.seek(0); // go to the beginning of the file outputFile.write(text.toUtf8()); // write the new text back to the file outputFile.close(); // close the file handle. retval = true; qCDebug(commerce) << "wrote html file successfully"; } else { qCDebug(commerce) << "failed to open output html file" << outputFilename; } return retval; }
bool Wallet::walletIsAuthenticatedWithPassphrase() { // try to read existing keys if they exist... // 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()) { 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; } // otherwise, we have a passphrase but no keys, so we have to check auto publicKey = readPublicKey(keyFilePath()); if (publicKey.size() > 0) { if (auto key = readPrivateKey(keyFilePath())) { EC_KEY_free(key); // 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; }
QString Wallet::getKeyFilePath() { QString filePath(keyFilePath()); QFileInfo fileInfo(filePath); if (fileInfo.exists()) { return filePath; } else { return ""; } }
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; }
bool Wallet::writeWallet(const QString& newPassphrase) { EC_KEY* 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"; emit keyFilePathIfExistsResult(getKeyFilePath()); 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::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; }
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; }
void Wallet::sendChallengeOwnershipResponses() { if (_pendingChallenges.size() == 0 || getSalt().length() == 0) { return; } auto nodeList = DependencyManager::get<NodeList>(); EC_KEY* ec = readKeys(keyFilePath()); for (const auto& packet: _pendingChallenges) { // With EC keys, we receive a nonce from the metaverse server, which is signed // here with the private key and returned. Verification is done at server. QString sig; bool challengeOriginatedFromClient = packet->getType() == PacketType::ChallengeOwnershipRequest; int status; int idByteArraySize; int textByteArraySize; int challengingNodeUUIDByteArraySize; packet->readPrimitive(&idByteArraySize); packet->readPrimitive(&textByteArraySize); // returns a cast char*, size if (challengeOriginatedFromClient) { packet->readPrimitive(&challengingNodeUUIDByteArraySize); } // "encryptedText" is now a series of random bytes, a nonce QByteArray id = packet->read(idByteArraySize); QByteArray text = packet->read(textByteArraySize); QByteArray challengingNodeUUID; if (challengeOriginatedFromClient) { challengingNodeUUID = packet->read(challengingNodeUUIDByteArraySize); } if (ec) { ERR_clear_error(); sig = signWithKey(text, ""); // base64 signature, QByteArray cast (on return) to QString FIXME should pass ec as string so we can tell which key to sign with status = 1; } else { qCDebug(commerce) << "During entity ownership challenge, creating the EC-signed nonce failed."; status = -1; } QByteArray textByteArray; if (status > -1) { textByteArray = sig.toUtf8(); } textByteArraySize = textByteArray.size(); int idSize = id.size(); // setup the packet const SharedNodePointer sendingNode = nodeList->nodeWithLocalID(packet->getSourceID()); if (!sendingNode.isNull()) { if (challengeOriginatedFromClient) { auto textPacket = NLPacket::create(PacketType::ChallengeOwnershipReply, idSize + textByteArraySize + challengingNodeUUIDByteArraySize + 3 * sizeof(int), true); textPacket->writePrimitive(idSize); textPacket->writePrimitive(textByteArraySize); textPacket->writePrimitive(challengingNodeUUIDByteArraySize); textPacket->write(id); textPacket->write(textByteArray); textPacket->write(challengingNodeUUID); qCDebug(commerce) << "Sending ChallengeOwnershipReply Packet containing signed text" << textByteArray << "for id" << id; nodeList->sendPacket(std::move(textPacket), *sendingNode); } else { auto textPacket = NLPacket::create(PacketType::ChallengeOwnership, idSize + textByteArraySize + 2 * sizeof(int), true); textPacket->writePrimitive(idSize); textPacket->writePrimitive(textByteArraySize); textPacket->write(id); textPacket->write(textByteArray); qCDebug(commerce) << "Sending ChallengeOwnership Packet containing signed text" << textByteArray << "for id" << id; nodeList->sendPacket(std::move(textPacket), *sendingNode); } } else { qCDebug(commerce) << "Challenging Node Local ID" << packet->getSourceID() << "disconnected before response"; } if (status == -1) { qCDebug(commerce) << "During entity ownership challenge, signing the text failed."; long error = ERR_get_error(); if (error != 0) { const char* error_str = ERR_error_string(error, NULL); qCWarning(entities) << "EC error:" << error_str; } } } EC_KEY_free(ec); _pendingChallenges.clear(); }
void Wallet::handleChallengeOwnershipPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) { auto nodeList = DependencyManager::get<NodeList>(); // With EC keys, we receive a nonce from the metaverse server, which is signed // here with the private key and returned. Verification is done at server. bool challengeOriginatedFromClient = packet->getType() == PacketType::ChallengeOwnershipRequest; int status; int certIDByteArraySize; int textByteArraySize; int challengingNodeUUIDByteArraySize; packet->readPrimitive(&certIDByteArraySize); packet->readPrimitive(&textByteArraySize); // returns a cast char*, size if (challengeOriginatedFromClient) { packet->readPrimitive(&challengingNodeUUIDByteArraySize); } // "encryptedText" is now a series of random bytes, a nonce QByteArray certID = packet->read(certIDByteArraySize); QByteArray text = packet->read(textByteArraySize); QByteArray challengingNodeUUID; if (challengeOriginatedFromClient) { challengingNodeUUID = packet->read(challengingNodeUUIDByteArraySize); } EC_KEY* ec = readKeys(keyFilePath().toStdString().c_str()); QString sig; if (ec) { ERR_clear_error(); sig = signWithKey(text, ""); // base64 signature, QByteArray cast (on return) to QString FIXME should pass ec as string so we can tell which key to sign with status = 1; } else { qCDebug(commerce) << "During entity ownership challenge, creating the EC-signed nonce failed."; status = -1; } EC_KEY_free(ec); QByteArray textByteArray; if (status > -1) { textByteArray = sig.toUtf8(); } textByteArraySize = textByteArray.size(); int certIDSize = certID.size(); // setup the packet if (challengeOriginatedFromClient) { auto textPacket = NLPacket::create(PacketType::ChallengeOwnershipReply, certIDSize + textByteArraySize + challengingNodeUUIDByteArraySize + 3 * sizeof(int), true); textPacket->writePrimitive(certIDSize); textPacket->writePrimitive(textByteArraySize); textPacket->writePrimitive(challengingNodeUUIDByteArraySize); textPacket->write(certID); textPacket->write(textByteArray); textPacket->write(challengingNodeUUID); qCDebug(commerce) << "Sending ChallengeOwnershipReply Packet containing signed text" << textByteArray << "for CertID" << certID; nodeList->sendPacket(std::move(textPacket), *sendingNode); } else { auto textPacket = NLPacket::create(PacketType::ChallengeOwnership, certIDSize + textByteArraySize + 2 * sizeof(int), true); textPacket->writePrimitive(certIDSize); textPacket->writePrimitive(textByteArraySize); textPacket->write(certID); textPacket->write(textByteArray); qCDebug(commerce) << "Sending ChallengeOwnership Packet containing signed text" << textByteArray << "for CertID" << certID; nodeList->sendPacket(std::move(textPacket), *sendingNode); } if (status == -1) { qCDebug(commerce) << "During entity ownership challenge, signing the text failed."; long error = ERR_get_error(); if (error != 0) { const char* error_str = ERR_error_string(error, NULL); qCWarning(entities) << "EC error:" << error_str; } } }