From a3cd5ad3c5b45a5f8577adfcb1aeb540249f2515 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 17 Apr 2019 18:33:30 -0700 Subject: [PATCH] Avatar-owner challenge now working --- assignment-client/src/avatars/AvatarMixer.cpp | 11 +++ assignment-client/src/avatars/AvatarMixer.h | 1 + assignment-client/src/avatars/MixerAvatar.cpp | 82 +++++++++++++++++-- assignment-client/src/avatars/MixerAvatar.h | 8 +- 4 files changed, 95 insertions(+), 7 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 9816cebf43..b804e4a20f 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -82,6 +82,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket"); packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase }, this, "handleOctreePacket"); + packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "handleChallengeOwnership"); packetReceiver.registerListenerForTypes({ PacketType::ReplicatedAvatarIdentity, @@ -1123,6 +1124,16 @@ void AvatarMixer::entityChange() { _dirtyHeroStatus = true; } +void AvatarMixer::handleChallengeOwnership(QSharedPointer message, SharedNodePointer senderNode) { + if (senderNode->getType() == NodeType::Agent && senderNode->getLinkedData()) { + auto clientData = static_cast(senderNode->getLinkedData()); + auto avatar = clientData->getAvatarSharedPointer(); + if (avatar) { + avatar->handleChallengeResponse(message.data()); + } + } +} + void AvatarMixer::aboutToFinish() { DependencyManager::destroy(); DependencyManager::destroy(); diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 10dff5e8a4..93dc755f51 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -65,6 +65,7 @@ private slots: void domainSettingsRequestComplete(); void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); void handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode); + void handleChallengeOwnership(QSharedPointer message, SharedNodePointer senderNode); void start(); private: diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp index 46c98b7532..8165a7d91d 100644 --- a/assignment-client/src/avatars/MixerAvatar.cpp +++ b/assignment-client/src/avatars/MixerAvatar.cpp @@ -24,6 +24,15 @@ #include "MixerAvatar.h" #include "AvatarLogging.h" +#include +#include +#include +#include +#include +#include +#include + + void MixerAvatar::fetchAvatarFST() { _verifyState = kNoncertified; _certificateIdFromURL.clear(); @@ -137,17 +146,17 @@ bool MixerAvatar::generateFSTHash() { bool MixerAvatar::validateFSTHash(const QString& publicKey) { // Guess we should refactor this stuff into a Authorization namespace ... - return EntityItemProperties::verifySignature(publicKey, _certificateHash, - QByteArray::fromBase64(_certificateIdFromFST.toUtf8())); +return EntityItemProperties::verifySignature(publicKey, _certificateHash, + QByteArray::fromBase64(_certificateIdFromFST.toUtf8())); } QByteArray MixerAvatar::canonicalJson(const QString fstFile) { QStringList fstLines = fstFile.split("\n", QString::SkipEmptyParts); - static const QString fstKeywordsReg{ + static const QString fstKeywordsReg { "(marketplaceID|itemDescription|itemCategories|itemArtist|itemLicenseUrl|limitedRun|itemName|" "filename|texdir|script|editionNumber|certificateID)" }; - QRegularExpression fstLineRegExp{ QString("^\\s*") + fstKeywordsReg + "\\s*=\\s*(\\S.*)$" }; + QRegularExpression fstLineRegExp { QString("^\\s*") + fstKeywordsReg + "\\s*=\\s*(\\S.*)$" }; QStringListIterator fstLineIter(fstLines); QJsonObject certifiedItems; @@ -193,7 +202,7 @@ QByteArray MixerAvatar::canonicalJson(const QString fstFile) { void MixerAvatar::processCertifyEvents() { QMutexLocker certifyLocker(&_avatarCertifyLock); - if (_verifyState != kOwnerResponse) { + if (_verifyState != kOwnerResponse && _verifyState != kChallengeResponse) { return; } @@ -218,7 +227,10 @@ void MixerAvatar::processCertifyEvents() { } } if (ownerValid && !ownerPublicKey.isEmpty()) { - // Challenge owner ... + _ownerPublicKey = "-----BEGIN PUBLIC KEY-----\n" + + ownerPublicKey + + "\n-----END PUBLIC KEY-----\n"; + challengeOwner(); } else { _verifyState = kError; } @@ -230,5 +242,63 @@ void MixerAvatar::processCertifyEvents() { break; } + case kChallengeResponse: + { + int avatarIDLength; + int signedNonceLength; + if (_challengeResponse.length() < 8) { + _verifyState = kError; + break; + } + + QDataStream responseStream(_challengeResponse); + responseStream.setByteOrder(QDataStream::LittleEndian); + responseStream >> avatarIDLength >> signedNonceLength; + QByteArray avatarID(_challengeResponse.data() + 2 * sizeof(int), avatarIDLength); + QByteArray signedNonce(_challengeResponse.data() + 2 * sizeof(int) + avatarIDLength, signedNonceLength); + QCryptographicHash nonceHash(QCryptographicHash::Sha256); + nonceHash.addData(_challengeNonce); + + bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, nonceHash.result(), + QByteArray::fromBase64(signedNonce)); + _verifyState = challengeResult ? kVerificationSucceeded : kVerificationFailed; + if (_verifyState == kVerificationFailed) { + qCDebug(avatars) << "Dynamic verification FAILED for " << getDisplayName() << getSessionUUID(); + } + } + } // close switch } + +void MixerAvatar::challengeOwner() { + auto nodeList = DependencyManager::get(); + QByteArray avatarID = ("{" + _marketplaceIdFromURL + "}").toUtf8(); + QByteArray nonce = QUuid::createUuid().toByteArray(); + + auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership, + 2 * sizeof(int) + nonce.length() + avatarID.length(), true); + challengeOwnershipPacket->writePrimitive(avatarID.length()); + challengeOwnershipPacket->writePrimitive(nonce.length()); + challengeOwnershipPacket->write(avatarID); + challengeOwnershipPacket->write(nonce); + + nodeList->sendPacket(std::move(challengeOwnershipPacket), *(nodeList->nodeWithUUID(getSessionUUID())) ); + _challengeNonce = nonce; + + static constexpr int CHALLENGE_TIMEOUT_MS = 10 * 1000; // 10 s + _challengeTimeout.setInterval(CHALLENGE_TIMEOUT_MS); + _challengeTimeout.connect(&_challengeTimeout, &QTimer::timeout, [this]() { + _verifyState = kVerificationFailed; + }); +} + +void MixerAvatar::handleChallengeResponse(ReceivedMessage * response) { + QByteArray avatarID; + QByteArray encryptedNonce; + QMutexLocker certifyLocker(&_avatarCertifyLock); + if (_verifyState == kChallengeClient) { + _challengeTimeout.stop(); + _challengeResponse = response->readAll(); + _verifyState = kChallengeResponse; + } +} diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index e0b7b14429..57896b2876 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -27,13 +27,14 @@ public: void fetchAvatarFST(); bool isCertifyFailed() const { return _verifyState == kVerificationFailed; } void processCertifyEvents(); + void handleChallengeResponse(ReceivedMessage * response); private: bool _needsHeroCheck{ false }; // Avatar certification/verification: enum VerifyState { kNoncertified, kRequestingFST, kReceivedFST, kStaticValidation, kRequestingOwner, kOwnerResponse, - kChallengeClient, kVerified, kVerificationFailed, kVerificationSucceeded, kError }; + kChallengeClient, kChallengeResponse, kVerified, kVerificationFailed, kVerificationSucceeded, kError }; Q_ENUM(VerifyState); VerifyState _verifyState { kNoncertified }; QMutex _avatarCertifyLock; @@ -44,10 +45,15 @@ private: QString _certificateIdFromURL; QString _certificateIdFromFST; QString _dynamicMarketResponse; + QString _ownerPublicKey; + QByteArray _challengeNonce; + QByteArray _challengeResponse; + QTimer _challengeTimeout; bool generateFSTHash(); bool validateFSTHash(const QString& publicKey); QByteArray canonicalJson(const QString fstFile); + void challengeOwner(); private slots: void fstRequestComplete();