Merge pull request #16375 from SimonWalton-HiFi/avatar-challenge-persist

DEV-2390: Persist in checking owner and sending challenges for failures
This commit is contained in:
Howard Stearns 2019-10-29 08:48:59 -07:00 committed by GitHub
commit 368b2bce90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 87 additions and 36 deletions

View file

@ -32,24 +32,42 @@ MixerAvatar::MixerAvatar() {
_challengeTimer.setSingleShot(true);
_challengeTimer.setInterval(CHALLENGE_TIMEOUT_MS);
_challengeTimer.callOnTimeout(this, [this]() {
if (_verifyState == challengeClient) {
_pendingEvent = false;
_verifyState = verificationFailed;
_needsIdentityUpdate = true;
qCDebug(avatars) << "Dynamic verification TIMED-OUT for" << getDisplayName() << getSessionUUID();
} else {
qCDebug(avatars) << "Ignoring timeout of avatar challenge";
}
});
_challengeTimer.callOnTimeout(this, &MixerAvatar::challengeTimeout);
// QTimer::start is a set of overloaded functions.
connect(this, &MixerAvatar::startChallengeTimer, &_challengeTimer, static_cast<void(QTimer::*)()>(&QTimer::start));
}
const char* MixerAvatar::stateToName(VerifyState state) {
return QMetaEnum::fromType<VerifyState>().valueToKey(state);
}
void MixerAvatar::challengeTimeout() {
switch (_verifyState) {
case challengeClient:
_verifyState = staticValidation;
_pendingEvent = true;
if (++_numberChallenges < NUM_CHALLENGES_BEFORE_FAIL) {
qCDebug(avatars) << "Retrying (" << _numberChallenges << ") timed-out challenge for" << getDisplayName()
<< getSessionUUID();
} else {
_certifyFailed = true;
_needsIdentityUpdate = true;
qCWarning(avatars) << "ALERT: Dynamic verification TIMED-OUT for" << getDisplayName() << getSessionUUID();
}
break;
case verificationFailed:
qCDebug(avatars) << "Retrying failed challenge for" << getDisplayName() << getSessionUUID();
_verifyState = staticValidation;
_pendingEvent = true;
break;
default:
qCDebug(avatars) << "Ignoring timeout of avatar challenge";
break;
}
}
void MixerAvatar::fetchAvatarFST() {
if (_verifyState >= requestingFST && _verifyState <= challengeClient) {
qCDebug(avatars) << "WARNING: Avatar verification restarted; old state:" << stateToName(_verifyState);
@ -58,7 +76,7 @@ void MixerAvatar::fetchAvatarFST() {
_pendingEvent = false;
QUrl avatarURL = getSkeletonModelURL();
QUrl avatarURL = _skeletonModelURL;
if (avatarURL.isEmpty() || avatarURL.isLocalFile() || avatarURL.scheme() == "qrc") {
// Not network FST.
return;
@ -210,6 +228,23 @@ void MixerAvatar::ownerRequestComplete() {
networkReply->deleteLater();
}
void MixerAvatar::requestCurrentOwnership() {
// Get registered owner's public key from metaverse.
static const QString POP_MARKETPLACE_API { "/api/v1/commerce/proof_of_purchase_status/transfer" };
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL();
requestURL.setPath(POP_MARKETPLACE_API);
networkRequest.setUrl(requestURL);
QJsonObject request;
request["certificate_id"] = _certificateIdFromFST;
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
connect(networkReply, &QNetworkReply::finished, this, &MixerAvatar::ownerRequestComplete);
}
void MixerAvatar::processCertifyEvents() {
if (!_pendingEvent) {
return;
@ -221,26 +256,15 @@ void MixerAvatar::processCertifyEvents() {
case receivedFST:
{
generateFSTHash();
_numberChallenges = 0;
if (_certificateIdFromFST.length() != 0) {
QString& marketplacePublicKey = EntityItem::_marketplacePublicKey;
bool staticVerification = validateFSTHash(marketplacePublicKey);
_verifyState = staticVerification ? staticValidation : verificationFailed;
if (_verifyState == staticValidation) {
static const QString POP_MARKETPLACE_API { "/api/v1/commerce/proof_of_purchase_status/transfer" };
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL();
requestURL.setPath(POP_MARKETPLACE_API);
networkRequest.setUrl(requestURL);
QJsonObject request;
request["certificate_id"] = _certificateIdFromFST;
requestCurrentOwnership();
_verifyState = requestingOwner;
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
connect(networkReply, &QNetworkReply::finished, this, &MixerAvatar::ownerRequestComplete);
} else {
_needsIdentityUpdate = true;
_pendingEvent = false;
@ -248,11 +272,21 @@ void MixerAvatar::processCertifyEvents() {
}
} else { // FST doesn't have a certificate, so noncertified rather than failed:
_pendingEvent = false;
_certifyFailed = false;
_needsIdentityUpdate = true;
_verifyState = nonCertified;
qCDebug(avatars) << "Avatar " << getDisplayName() << "(" << getSessionUUID() << ") isn't certified";
}
break;
}
case staticValidation:
{
requestCurrentOwnership();
_verifyState = requestingOwner;
break;
}
case ownerResponse:
{
QJsonDocument responseJson = QJsonDocument::fromJson(_dynamicMarketResponse.toUtf8());
@ -310,23 +344,28 @@ void MixerAvatar::processCertifyEvents() {
void MixerAvatar::sendOwnerChallenge() {
auto nodeList = DependencyManager::get<NodeList>();
QByteArray avatarID = ("{" + _marketplaceIdFromFST + "}").toUtf8();
QByteArray nonce = QUuid::createUuid().toByteArray();
if (_challengeNonce.isEmpty()) {
_challengeNonce = QUuid::createUuid().toByteArray();
QCryptographicHash nonceHash(QCryptographicHash::Sha256);
nonceHash.addData(_challengeNonce);
_challengeNonceHash = nonceHash.result();
}
auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership,
2 * sizeof(int) + nonce.length() + avatarID.length(), true);
2 * sizeof(int) + _challengeNonce.length() + avatarID.length(), true);
challengeOwnershipPacket->writePrimitive(avatarID.length());
challengeOwnershipPacket->writePrimitive(nonce.length());
challengeOwnershipPacket->writePrimitive(_challengeNonce.length());
challengeOwnershipPacket->write(avatarID);
challengeOwnershipPacket->write(nonce);
challengeOwnershipPacket->write(_challengeNonce);
nodeList->sendPacket(std::move(challengeOwnershipPacket), *(nodeList->nodeWithUUID(getSessionUUID())) );
QCryptographicHash nonceHash(QCryptographicHash::Sha256);
nonceHash.addData(nonce);
nonceHash.addData(_challengeNonce);
_challengeNonceHash = nonceHash.result();
_pendingEvent = false;
// QTimer::start is a set of overloaded functions.
QMetaObject::invokeMethod(&_challengeTimer, static_cast<void(QTimer::*)()>(&QTimer::start));
emit startChallengeTimer();
}
void MixerAvatar::processChallengeResponse(ReceivedMessage& response) {
@ -337,7 +376,7 @@ void MixerAvatar::processChallengeResponse(ReceivedMessage& response) {
QByteArray responseData = response.readAll();
if (responseData.length() < 8) {
_verifyState = error;
qCDebug(avatars) << "Avatar challenge response packet too small, length:" << responseData.length();
qCWarning(avatars) << "ALERT: Avatar challenge response packet too small, length:" << responseData.length();
return;
}
@ -354,11 +393,14 @@ void MixerAvatar::processChallengeResponse(ReceivedMessage& response) {
bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, _challengeNonceHash,
QByteArray::fromBase64(signedNonce));
_verifyState = challengeResult ? verificationSucceeded : verificationFailed;
_certifyFailed = !challengeResult;
_needsIdentityUpdate = true;
if (_verifyState == verificationFailed) {
if (_certifyFailed) {
qCDebug(avatars) << "Dynamic verification FAILED for" << getDisplayName() << getSessionUUID();
emit startChallengeTimer();
} else {
qCDebug(avatars) << "Dynamic verification SUCCEEDED for" << getDisplayName() << getSessionUUID();
_challengeNonce.clear();
}
} else {

View file

@ -27,7 +27,7 @@ public:
void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; }
void fetchAvatarFST();
virtual bool isCertifyFailed() const override { return _verifyState == verificationFailed; }
virtual bool isCertifyFailed() const override { return _certifyFailed; }
bool needsIdentityUpdate() const { return _needsIdentityUpdate; }
void setNeedsIdentityUpdate(bool value = true) { _needsIdentityUpdate = value; }
@ -58,13 +58,18 @@ private:
QString _certificateIdFromFST;
QString _dynamicMarketResponse;
QString _ownerPublicKey;
QByteArray _challengeNonce;
QByteArray _challengeNonceHash;
QTimer _challengeTimer;
static constexpr int NUM_CHALLENGES_BEFORE_FAIL = 1;
int _numberChallenges { 0 };
bool _certifyFailed { false };
bool _needsIdentityUpdate { false };
bool generateFSTHash();
bool validateFSTHash(const QString& publicKey) const;
QByteArray canonicalJson(const QString fstFile);
void requestCurrentOwnership();
void sendOwnerChallenge();
static const QString VERIFY_FAIL_MODEL;
@ -72,6 +77,10 @@ private:
private slots:
void fstRequestComplete();
void ownerRequestComplete();
void challengeTimeout();
signals:
void startChallengeTimer();
};
using MixerAvatarSharedPointer = std::shared_ptr<MixerAvatar>;