diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp index 8f5c60a7d9..f5d598a5cf 100644 --- a/assignment-client/src/avatars/MixerAvatar.cpp +++ b/assignment-client/src/avatars/MixerAvatar.cpp @@ -27,6 +27,12 @@ #include "ClientTraitsHandler.h" #include "AvatarLogging.h" +MixerAvatar::~MixerAvatar() { + if (_challengeTimeout) { + _challengeTimeout->deleteLater(); + } +} + void MixerAvatar::fetchAvatarFST() { _verifyState = nonCertified; @@ -229,6 +235,7 @@ void MixerAvatar::processCertifyEvents() { QJsonDocument responseJson = QJsonDocument::fromJson(_dynamicMarketResponse.toUtf8()); QString ownerPublicKey; bool ownerValid = false; + _pendingEvent = false; if (responseJson["status"].toString() == "success") { QJsonValue jsonData = responseJson["data"]; if (jsonData.isObject()) { @@ -251,6 +258,7 @@ void MixerAvatar::processCertifyEvents() { } sendOwnerChallenge(); _verifyState = challengeClient; + _pendingEvent = true; } else { _verifyState = error; } @@ -259,7 +267,6 @@ void MixerAvatar::processCertifyEvents() { "message:" << responseJson["message"].toString(); _verifyState = error; } - _pendingEvent = false; break; } @@ -295,6 +302,7 @@ void MixerAvatar::processCertifyEvents() { } case requestingOwner: + case challengeClient: { // Qt networking done on this thread: QCoreApplication::processEvents(); break; @@ -324,12 +332,21 @@ void MixerAvatar::sendOwnerChallenge() { nonceHash.addData(nonce); _challengeNonceHash = nonceHash.result(); - static constexpr int CHALLENGE_TIMEOUT_MS = 10 * 1000; // 10 s - _challengeTimeout.setInterval(CHALLENGE_TIMEOUT_MS); - _challengeTimeout.connect(&_challengeTimeout, &QTimer::timeout, [this]() { - _verifyState = verificationFailed; - _needsIdentityUpdate = true; + static constexpr int CHALLENGE_TIMEOUT_MS = 5 * 1000; // 5 s + if (_challengeTimeout) { + _challengeTimeout->deleteLater(); + } + _challengeTimeout = new QTimer(); + _challengeTimeout->setInterval(CHALLENGE_TIMEOUT_MS); + _challengeTimeout->setSingleShot(true); + _challengeTimeout->connect(_challengeTimeout, &QTimer::timeout, this, [this]() { + if (_verifyState == challengeClient) { + _pendingEvent = false; + _verifyState = verificationFailed; + _needsIdentityUpdate = true; + } }); + _challengeTimeout->start(); } void MixerAvatar::handleChallengeResponse(ReceivedMessage* response) { @@ -337,7 +354,6 @@ void MixerAvatar::handleChallengeResponse(ReceivedMessage* response) { QByteArray encryptedNonce; QMutexLocker certifyLocker(&_avatarCertifyLock); if (_verifyState == challengeClient) { - _challengeTimeout.stop(); _challengeResponse = response->readAll(); _verifyState = challengeResponse; _pendingEvent = true; diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index bafc398a02..e8d9c959db 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -21,6 +21,7 @@ class ResourceRequest; class MixerAvatar : public AvatarData { public: + ~MixerAvatar(); bool getNeedsHeroCheck() const { return _needsHeroCheck; } void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; } @@ -53,7 +54,7 @@ private: QString _ownerPublicKey; QByteArray _challengeNonceHash; QByteArray _challengeResponse; - QTimer _challengeTimeout; + QTimer* _challengeTimeout { nullptr }; bool _needsIdentityUpdate { false }; bool generateFSTHash(); diff --git a/interface/resources/images/AvatarTheftBanner.png b/interface/resources/images/AvatarTheftBanner.png index 3dc76999e0..6b538ed7b9 100644 Binary files a/interface/resources/images/AvatarTheftBanner.png and b/interface/resources/images/AvatarTheftBanner.png differ diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index d72d896638..60fefa5878 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -317,13 +317,13 @@ void Ledger::accountSuccess(QNetworkReply* reply) { 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. } + wallet->setSalt(salt); QString keyStatus = "ok"; QStringList localPublicKeys = wallet->listPublicKeys(); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index ea2de73db3..127bca9eba 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -313,6 +313,8 @@ Wallet::Wallet() { walletScriptingInterface->setWalletStatus(status); }); + connect(ledger.data(), &Ledger::accountResult, this, &Wallet::sendChallengeOwnershipResponses); + auto accountManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::usernameChanged, this, [&]() { getWalletStatus(); @@ -823,88 +825,101 @@ bool Wallet::changePassphrase(const QString& newPassphrase) { } void Wallet::handleChallengeOwnershipPacket(QSharedPointer packet, SharedNodePointer sendingNode) { + _pendingChallenges.push_back(packet); + sendChallengeOwnershipResponses(); +} + +void Wallet::sendChallengeOwnershipResponses() { + if (_pendingChallenges.size() == 0 || getSalt().length() == 0) { + return; + } auto nodeList = DependencyManager::get(); - // 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 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); - } - EC_KEY* ec = readKeys(keyFilePath()); - 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; + 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 + Node& sendingNode = *nodeList->nodeWithLocalID(packet->getSourceID()); + 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); + } + + 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); - - QByteArray textByteArray; - if (status > -1) { - textByteArray = sig.toUtf8(); - } - textByteArraySize = textByteArray.size(); - int idSize = id.size(); - // setup the packet - 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); - } - - 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; - } - } + _pendingChallenges.clear(); } void Wallet::account() { diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index fdd6b5e2a6..52b956dc5b 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -94,6 +94,7 @@ signals: private slots: void handleChallengeOwnershipPacket(QSharedPointer packet, SharedNodePointer sendingNode); + void sendChallengeOwnershipResponses(); private: friend class Ledger; @@ -104,6 +105,7 @@ private: QByteArray _ckey; QString* _passphrase { nullptr }; bool _isOverridingServer { false }; + std::vector> _pendingChallenges; bool writeWallet(const QString& newPassphrase = QString("")); void updateImageProvider();