From f654532fa81d2bfebb90bee57395b5954e12a2fd Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Wed, 28 Jun 2017 16:39:55 -1000 Subject: [PATCH] Handle UNVERIFIED sync verification messages (via contact sync) FREEBIE --- js/background.js | 8 +- js/models/conversations.js | 20 ++- js/signal_protocol_store.js | 36 +++- test/storage_test.js | 317 +++++++++++++++++++++++++----------- 4 files changed, 267 insertions(+), 114 deletions(-) diff --git a/js/background.js b/js/background.js index dea45238a8..11f67fd47c 100644 --- a/js/background.js +++ b/js/background.js @@ -321,10 +321,12 @@ key: key }; - if (state === 'DEFAULT') { - contact.setVerifiedDefault(options); - } else if (state === 'VERIFIED') { + if (state === 'VERIFIED') { contact.setVerified(options); + } else if (state === 'DEFAULT') { + contact.setVerifiedDefault(options); + } else { + contact.setUnverified(options); } } diff --git a/js/models/conversations.js b/js/models/conversations.js index c196821f14..ac96513eab 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -92,12 +92,19 @@ return this._setVerified(VERIFIED, options); }.bind(this)); }, + setUnverified: function(options) { + var UNVERIFIED = this.verifiedEnum.UNVERIFIED; + return this.queueJob(function() { + return this._setVerified(UNVERIFIED, options); + }.bind(this)); + }, _setVerified: function(verified, options) { options = options || {}; _.defaults(options, {viaSyncMessage: false, viaContactSync: false, key: null}); - var VERIFIED = this.verifiedEnum.VERIFIED; var DEFAULT = this.verifiedEnum.DEFAULT; + var VERIFIED = this.verifiedEnum.VERIFIED; + var UNVERIFIED = this.verifiedEnum.UNVERIFIED; if (!this.isPrivate()) { throw new Error('You cannot verify a group conversation. ' + @@ -127,12 +134,13 @@ // 1) The message came from an explicit verification in another client (not // a contact sync) // 2) The verification value received by the contact sync is different - // from what we have on record - // 3) Our local verification status is not DEFAULT and it hasn't changed, - // but the key did change (say from Key1/Verified to Key2/Verified) + // from what we have on record (and it's not a transition to UNVERIFIED) + // 3) Our local verification status is VERIFIED and it hasn't changed, + // but the key did change (Key1/VERIFIED to Key2/VERIFIED - but we don't + // want to show DEFAULT->DEFAULT or UNVERIFIED->UNVERIFIED) if (!options.viaContactSync - || beginningVerified !== verified - || (keychange && verified !== DEFAULT)) { + || (beginningVerified !== verified && verified !== UNVERIFIED) + || (keychange && verified === VERIFIED)) { var local = !options.viaSyncMessage && !options.viaContactSync; this.addVerifiedChange(this.id, verified === VERIFIED, {local: local}); diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index 30d24c60fc..85fc1b972a 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -579,6 +579,7 @@ }); }); }, + // Resolves to true if a new identity key was saved processVerifiedMessage: function(identifier, verifiedStatus, publicKey) { if (identifier === null || identifier === undefined) { throw new Error("Tried to set verified for undefined/null key"); @@ -599,26 +600,37 @@ isEqual = equalArrayBuffers(publicKey, identityRecord.get('publicKey')); } }).always(function() { + // Because new keys always start as DEFAULT, we don't need to create new record here if (!isPresent && verifiedStatus === VerifiedStatus.DEFAULT) { console.log('No existing record for default status'); - resolve(); + return resolve(); } + // If we had a key before and it's the same, and we're not changing to + // VERIFIED, then it's a simple update of the verified flag. if (isPresent && isEqual - && identityRecord.get('verified') !== VerifiedStatus.DEFAULT - && verifiedStatus === VerifiedStatus.DEFAULT) { + && identityRecord.get('verified') !== verifiedStatus + && verifiedStatus !== VerifiedStatus.VERIFIED) { - textsecure.storage.protocol.setVerified( + return textsecure.storage.protocol.setVerified( identifier, verifiedStatus, publicKey ).then(resolve, reject); } - if (verifiedStatus === VerifiedStatus.VERIFIED - && (!isPresent - || (isPresent && !isEqual) - || (isPresent && identityRecord.get('verified') !== VerifiedStatus.VERIFIED))) { + // We need to create a new record in three cases: + // 1. We had no key previously (checks above ensure that this is + // either VERIFIED/UNVERIFIED) + // 2. We had a key before, but we got a new key + // (no matter the VERIFIED state) + // 3. It's the same key, but we weren't VERIFIED before and are now + // (checks above handle the situation when 'state != VERIFIED') + if (!isPresent + || (isPresent && !isEqual) + || (isPresent + && identityRecord.get('verified') !== verifiedStatus + && verifiedStatus === VerifiedStatus.VERIFIED)) { - textsecure.storage.protocol.saveIdentityWithAttributes(identifier, { + return textsecure.storage.protocol.saveIdentityWithAttributes(identifier, { publicKey : publicKey, verified : verifiedStatus, firstUse : false, @@ -636,6 +648,12 @@ return resolve(); }.bind(this), reject); } + + // The situation which could get us here is: + // 1. had a previous key + // 2. new key is the same + // 3. desired new status is same as what we had before + return resolve(); }.bind(this)); }.bind(this)); }, diff --git a/test/storage_test.js b/test/storage_test.js index 3475342d9d..52161168dd 100644 --- a/test/storage_test.js +++ b/test/storage_test.js @@ -1,21 +1,27 @@ 'use strict'; describe("SignalProtocolStore", function() { + var identifier = '+5558675309'; + var store; + var identityKey; + var testKey; + before(function(done) { + store = textsecure.storage.protocol; + identityKey = { + pubKey: libsignal.crypto.getRandomBytes(33), + privKey: libsignal.crypto.getRandomBytes(32), + }; + testKey = { + pubKey: libsignal.crypto.getRandomBytes(33), + privKey: libsignal.crypto.getRandomBytes(32), + }; + storage.put('registrationId', 1337); storage.put('identityKey', identityKey); storage.fetch().then(done, done); }); - var store = textsecure.storage.protocol; - var identifier = '+5558675309'; - var identityKey = { - pubKey: libsignal.crypto.getRandomBytes(33), - privKey: libsignal.crypto.getRandomBytes(32), - }; - var testKey = { - pubKey: libsignal.crypto.getRandomBytes(33), - privKey: libsignal.crypto.getRandomBytes(32), - }; + describe('getLocalRegistrationId', function() { it('retrieves my registration id', function(done) { store.getLocalRegistrationId().then(function(reg) { @@ -202,16 +208,21 @@ describe("SignalProtocolStore", function() { }); }); describe('saveIdentityWithAttributes', function() { - var now = Date.now(); - var record = new IdentityKeyRecord({id: identifier}); - var validAttributes = { - publicKey : testKey.pubKey, - firstUse : true, - timestamp : now, - verified : store.VerifiedStatus.VERIFIED, - nonblockingApproval : false - }; + var now; + var record; + var validAttributes; + before(function(done) { + now = Date.now(); + record = new IdentityKeyRecord({id: identifier}); + validAttributes = { + publicKey : testKey.pubKey, + firstUse : true, + timestamp : now, + verified : store.VerifiedStatus.VERIFIED, + nonblockingApproval : false + }; + store.removeIdentityKey(identifier).then(function() { done(); }); }); describe('with valid attributes', function() { @@ -346,27 +357,32 @@ describe("SignalProtocolStore", function() { describe('processVerifiedMessage', function() { var record; var newIdentity = libsignal.crypto.getRandomBytes(33); - function fetchRecord() { + var keychangeTriggered; + + function wrapDeferred(deferred) { return new Promise(function(resolve, reject) { - record.fetch().then(resolve, reject); + return deferred.then(resolve, reject); }); } + function fetchRecord() { + return wrapDeferred(record.fetch()); + } + + beforeEach(function() { + keychangeTriggered = 0; + store.bind('keychange', function() { + keychangeTriggered++; + }); + }); + afterEach(function() { + store.unbind('keychange'); + }); + describe('when the new verified status is DEFAULT', function() { describe('when there is no existing record', function() { - var keychangeTriggered; - before(function() { - keychangeTriggered = 0; - store.bind('keychange', function() { - keychangeTriggered++; - }); record = new IdentityKeyRecord({ id: identifier }); - return new Promise(function(resolve, reject) { - record.destroy().then(resolve, reject); - }); - }); - after(function() { - store.unbind('keychange'); + return wrapDeferred(record.destroy()); }); it ('does nothing', function() { @@ -378,63 +394,174 @@ describe("SignalProtocolStore", function() { throw new Error("processVerifiedMessage should not save new records"); }, function() { assert.strictEqual(keychangeTriggered, 0); - return; }); }); }); - describe('when the record exists and is not DEFAULT and the key matches', function() { - var keychangeTriggered; + describe('when the record exists', function() { + describe('when the existing key is different', function() { + before(function() { + record = new IdentityKeyRecord({ + id : identifier, + publicKey : testKey.pubKey, + firstUse : true, + timestamp : Date.now(), + verified : store.VerifiedStatus.VERIFIED, + nonblockingApproval : false + }); + return wrapDeferred(record.save()); + }); + it ('saves the new identity and marks it DEFAULT', function() { + return store.processVerifiedMessage( + identifier, store.VerifiedStatus.DEFAULT, newIdentity + ).then(fetchRecord).then(function() { + assert.strictEqual(record.get('verified'), store.VerifiedStatus.DEFAULT); + assertEqualArrayBuffers(record.get('publicKey'), newIdentity); + assert.strictEqual(keychangeTriggered, 1); + }); + }); + }); + describe('when the existing key is the same but VERIFIED', function() { + before(function() { + record = new IdentityKeyRecord({ + id : identifier, + publicKey : testKey.pubKey, + firstUse : true, + timestamp : Date.now(), + verified : store.VerifiedStatus.VERIFIED, + nonblockingApproval : false + }); + return wrapDeferred(record.save()); + }); + + it ('updates the verified status', function() { + return store.processVerifiedMessage( + identifier, store.VerifiedStatus.DEFAULT, testKey.pubKey + ).then(fetchRecord).then(function() { + assert.strictEqual(record.get('verified'), store.VerifiedStatus.DEFAULT); + assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); + assert.strictEqual(keychangeTriggered, 0); + }); + }); + }); + describe('when the existing key is the same and already DEFAULT', function() { + before(function() { + record = new IdentityKeyRecord({ + id : identifier, + publicKey : testKey.pubKey, + firstUse : true, + timestamp : Date.now(), + verified : store.VerifiedStatus.DEFAULT, + nonblockingApproval : false + }); + return wrapDeferred(record.save()); + }); + + it ('does not hang', function() { + return store.processVerifiedMessage( + identifier, store.VerifiedStatus.DEFAULT, testKey.pubKey + ).then(fetchRecord).then(function() { + assert.strictEqual(keychangeTriggered, 0); + }); + }); + }); + }); + }); + describe('when the new verified status is UNVERIFIED', function() { + describe('when there is no existing record', function() { before(function() { - keychangeTriggered = 0; - store.bind('keychange', function() { - keychangeTriggered++; - }); - record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - firstUse : true, - timestamp : Date.now(), - verified : store.VerifiedStatus.VERIFIED, - nonblockingApproval : false - }); - return new Promise(function(resolve, reject) { - record.save().then(resolve, reject); - }); - }); - after(function() { - store.unbind('keychange'); + record = new IdentityKeyRecord({ id: identifier }); + return wrapDeferred(record.destroy()); }); - it ('updates the verified status', function() { + it ('saves the new identity and marks it verified', function() { return store.processVerifiedMessage( - identifier, store.VerifiedStatus.DEFAULT, testKey.pubKey + identifier, store.VerifiedStatus.UNVERIFIED, newIdentity ).then(fetchRecord).then(function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.DEFAULT); - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); + assert.strictEqual(record.get('verified'), store.VerifiedStatus.UNVERIFIED); + assertEqualArrayBuffers(record.get('publicKey'), newIdentity); assert.strictEqual(keychangeTriggered, 0); }); }); }); + describe('when the record exists', function() { + describe('when the existing key is different', function() { + before(function() { + record = new IdentityKeyRecord({ + id : identifier, + publicKey : testKey.pubKey, + firstUse : true, + timestamp : Date.now(), + verified : store.VerifiedStatus.VERIFIED, + nonblockingApproval : false + }); + return wrapDeferred(record.save()); + }); + + it ('saves the new identity and marks it UNVERIFIED', function() { + return store.processVerifiedMessage( + identifier, store.VerifiedStatus.UNVERIFIED, newIdentity + ).then(fetchRecord).then(function() { + assert.strictEqual(record.get('verified'), store.VerifiedStatus.UNVERIFIED); + assertEqualArrayBuffers(record.get('publicKey'), newIdentity); + assert.strictEqual(keychangeTriggered, 1); + }); + }); + }); + describe('when the key exists and is DEFAULT', function() { + before(function() { + record = new IdentityKeyRecord({ + id : identifier, + publicKey : testKey.pubKey, + firstUse : true, + timestamp : Date.now(), + verified : store.VerifiedStatus.DEFAULT, + nonblockingApproval : false + }); + return wrapDeferred(record.save()); + }); + + it ('updates the verified status', function() { + return store.processVerifiedMessage( + identifier, store.VerifiedStatus.UNVERIFIED, testKey.pubKey + ).then(fetchRecord).then(function() { + assert.strictEqual(record.get('verified'), store.VerifiedStatus.UNVERIFIED); + assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); + assert.strictEqual(keychangeTriggered, 0); + }); + }); + }); + describe('when the key exists and is already UNVERIFIED', function() { + before(function() { + record = new IdentityKeyRecord({ + id : identifier, + publicKey : testKey.pubKey, + firstUse : true, + timestamp : Date.now(), + verified : store.VerifiedStatus.UNVERIFIED, + nonblockingApproval : false + }); + return wrapDeferred(record.save()); + }); + + it ('does not hang', function() { + return store.processVerifiedMessage( + identifier, store.VerifiedStatus.UNVERIFIED, testKey.pubKey + ).then(fetchRecord).then(function() { + assert.strictEqual(keychangeTriggered, 0); + }); + }); + }); + }); }); describe('when the new verified status is VERIFIED', function() { describe('when there is no existing record', function() { - var keychangeTriggered; - before(function() { - keychangeTriggered = 0; - store.bind('keychange', function() { - keychangeTriggered++; - }); - record = new IdentityKeyRecord({ id: identifier }); return new Promise(function(resolve, reject) { record.destroy().then(resolve, reject); }); }); - after(function() { - store.unbind('keychange'); - }); it ('saves the new identity and marks it verified', function() { return store.processVerifiedMessage( @@ -448,14 +575,7 @@ describe("SignalProtocolStore", function() { }); describe('when the record exists', function() { describe('when the existing key is different', function() { - var keychangeTriggered; - before(function() { - keychangeTriggered = 0; - store.bind('keychange', function() { - keychangeTriggered++; - }); - record = new IdentityKeyRecord({ id : identifier, publicKey : testKey.pubKey, @@ -464,15 +584,10 @@ describe("SignalProtocolStore", function() { verified : store.VerifiedStatus.VERIFIED, nonblockingApproval : false }); - return new Promise(function(resolve, reject) { - record.save().then(resolve, reject); - }); - }); - after(function() { - store.unbind('keychange'); + return wrapDeferred(record.save()); }); - it ('saves the new identity and marks it verified', function() { + it ('saves the new identity and marks it VERIFIED', function() { return store.processVerifiedMessage( identifier, store.VerifiedStatus.VERIFIED, newIdentity ).then(fetchRecord).then(function() { @@ -482,15 +597,8 @@ describe("SignalProtocolStore", function() { }); }); }); - describe('when the existing key is the same and not verified', function() { - var keychangeTriggered; - + describe('when the existing key is the same but UNVERIFIED', function() { before(function() { - keychangeTriggered = 0; - store.bind('keychange', function() { - keychangeTriggered++; - }); - record = new IdentityKeyRecord({ id : identifier, publicKey : testKey.pubKey, @@ -499,12 +607,7 @@ describe("SignalProtocolStore", function() { verified : store.VerifiedStatus.UNVERIFIED, nonblockingApproval : false }); - return new Promise(function(resolve, reject) { - record.save().then(resolve, reject); - }); - }); - after(function() { - store.unbind('keychange'); + return wrapDeferred(record.save()); }); it ('saves the identity and marks it verified', function() { @@ -517,8 +620,30 @@ describe("SignalProtocolStore", function() { }); }); }); + describe('when the existing key is the same and already VERIFIED', function() { + before(function() { + record = new IdentityKeyRecord({ + id : identifier, + publicKey : testKey.pubKey, + firstUse : true, + timestamp : Date.now(), + verified : store.VerifiedStatus.VERIFIED, + nonblockingApproval : false + }); + return wrapDeferred(record.save()); + }); + + it ('does not hang', function() { + return store.processVerifiedMessage( + identifier, store.VerifiedStatus.VERIFIED, testKey.pubKey + ).then(fetchRecord).then(function() { + assert.strictEqual(keychangeTriggered, 0); + }); + }); + }); }); }); + }); describe('getVerified', function() { before(function(done) {