diff --git a/ts/SignalProtocolStore.ts b/ts/SignalProtocolStore.ts index 516e25a406e..7df136d6a4b 100644 --- a/ts/SignalProtocolStore.ts +++ b/ts/SignalProtocolStore.ts @@ -120,6 +120,11 @@ export type VerifyAlternateIdentityOptionsType = Readonly<{ signature: Uint8Array; }>; +export type SetVerifiedExtra = Readonly<{ + firstUse?: boolean; + nonblockingApproval?: boolean; +}>; + export const GLOBAL_ZONE = new Zone('GLOBAL_ZONE'); async function _fillCaches, HydratedType>( @@ -1483,6 +1488,7 @@ export class SignalProtocolStore extends EventEmitter { return newRecord; } + // https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java#L128 async isTrustedIdentity( encodedAddress: Address, publicKey: Uint8Array, @@ -1495,8 +1501,9 @@ export class SignalProtocolStore extends EventEmitter { if (encodedAddress == null) { throw new Error('isTrustedIdentity: encodedAddress was undefined/null'); } - const ourUuid = window.textsecure.storage.user.getCheckedUuid(); - const isOurIdentifier = encodedAddress.uuid.isEqual(ourUuid); + const isOurIdentifier = window.textsecure.storage.user.isOurUuid( + encodedAddress.uuid + ); const identityRecord = await this.getOrMigrateIdentityRecord( encodedAddress.uuid @@ -1522,6 +1529,7 @@ export class SignalProtocolStore extends EventEmitter { } } + // https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java#L233 isTrustedForSending( publicKey: Uint8Array, identityRecord?: IdentityKeyType @@ -1597,6 +1605,7 @@ export class SignalProtocolStore extends EventEmitter { }); } + // https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java#L69 async saveIdentity( encodedAddress: Address, publicKey: Uint8Array, @@ -1640,8 +1649,21 @@ export class SignalProtocolStore extends EventEmitter { return false; } - const oldpublicKey = identityRecord.publicKey; - if (!constantTimeEqual(oldpublicKey, publicKey)) { + const identityKeyChanged = !constantTimeEqual( + identityRecord.publicKey, + publicKey + ); + + if (identityKeyChanged) { + const isOurIdentifier = window.textsecure.storage.user.isOurUuid( + encodedAddress.uuid + ); + + if (isOurIdentifier && identityKeyChanged) { + log.warn('saveIdentity: ignoring identity for ourselves'); + return false; + } + log.info('saveIdentity: Replacing existing identity...'); const previousStatus = identityRecord.verified; let verifiedStatus; @@ -1663,6 +1685,8 @@ export class SignalProtocolStore extends EventEmitter { nonblockingApproval, }); + // See `addKeyChange` in `ts/models/conversations.ts` for sender key info + // update caused by this. try { this.emit('keychange', encodedAddress.uuid); } catch (error) { @@ -1692,7 +1716,10 @@ export class SignalProtocolStore extends EventEmitter { return false; } - isNonBlockingApprovalRequired(identityRecord: IdentityKeyType): boolean { + // https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java#L257 + private isNonBlockingApprovalRequired( + identityRecord: IdentityKeyType + ): boolean { return ( !identityRecord.firstUse && isMoreRecentThan(identityRecord.timestamp, TIMESTAMP_THRESHOLD) && @@ -1746,10 +1773,12 @@ export class SignalProtocolStore extends EventEmitter { await this._saveIdentityKey(identityRecord); } + // https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalBaseIdentityKeyStore.java#L215 + // and https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.java#L544 async setVerified( uuid: UUID, verifiedStatus: number, - publicKey?: Uint8Array + extra: SetVerifiedExtra = {} ): Promise { if (uuid == null) { throw new Error('setVerified: uuid was undefined/null'); @@ -1764,14 +1793,12 @@ export class SignalProtocolStore extends EventEmitter { throw new Error(`setVerified: No identity record for ${uuid.toString()}`); } - if (!publicKey || constantTimeEqual(identityRecord.publicKey, publicKey)) { - identityRecord.verified = verifiedStatus; - - if (validateIdentityKey(identityRecord)) { - await this._saveIdentityKey(identityRecord); - } - } else { - log.info('setVerified: No identity record for specified publicKey'); + if (validateIdentityKey(identityRecord)) { + await this._saveIdentityKey({ + ...identityRecord, + ...extra, + verified: verifiedStatus, + }); } } @@ -1793,59 +1820,63 @@ export class SignalProtocolStore extends EventEmitter { return VerifiedStatus.DEFAULT; } - // See https://github.com/signalapp/Signal-iOS-Private/blob/e32c2dff0d03f67467b4df621d84b11412d50cdb/SignalServiceKit/src/Messages/OWSIdentityManager.m#L317 - // for reference. - async processVerifiedMessage( + // See https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/database/IdentityDatabase.java#L184 + async updateIdentityAfterSync( uuid: UUID, verifiedStatus: number, - publicKey?: Uint8Array + publicKey: Uint8Array ): Promise { - if (uuid == null) { - throw new Error('processVerifiedMessage: uuid was undefined/null'); - } - if (!validateVerifiedStatus(verifiedStatus)) { - throw new Error('processVerifiedMessage: Invalid verified status'); - } - if (publicKey !== undefined && !(publicKey instanceof Uint8Array)) { - throw new Error('processVerifiedMessage: Invalid public key'); - } + strictAssert( + validateVerifiedStatus(verifiedStatus), + `Invalid verified status: ${verifiedStatus}` + ); const identityRecord = await this.getOrMigrateIdentityRecord(uuid); + const hadEntry = identityRecord !== undefined; + const keyMatches = Boolean( + identityRecord?.publicKey && + constantTimeEqual(publicKey, identityRecord.publicKey) + ); + const statusMatches = + keyMatches && verifiedStatus === identityRecord?.verified; - let isEqual = false; - - if (identityRecord && publicKey) { - isEqual = constantTimeEqual(publicKey, identityRecord.publicKey); - } - - // Just update verified status if the key is the same or not present - if (isEqual || !publicKey) { - await this.setVerified(uuid, verifiedStatus, publicKey); - return false; + if (!keyMatches || !statusMatches) { + await this.saveIdentityWithAttributes(uuid, { + publicKey, + verified: verifiedStatus, + firstUse: !hadEntry, + timestamp: Date.now(), + nonblockingApproval: true, + }); } - await this.saveIdentityWithAttributes(uuid, { - publicKey, - verified: verifiedStatus, - firstUse: false, - timestamp: Date.now(), - nonblockingApproval: verifiedStatus === VerifiedStatus.VERIFIED, - }); - - if (identityRecord) { + if (hadEntry && !keyMatches) { try { this.emit('keychange', uuid); } catch (error) { log.error( - 'processVerifiedMessage error triggering keychange:', + 'updateIdentityAfterSync: error triggering keychange:', Errors.toLogFormat(error) ); } + } - // true signifies that we overwrote a previous key with a new one + // See: https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt#L921-L936 + if ( + verifiedStatus === VerifiedStatus.VERIFIED && + (!hadEntry || identityRecord?.verified !== VerifiedStatus.VERIFIED) + ) { + // Needs a notification. + return true; + } + if ( + verifiedStatus !== VerifiedStatus.VERIFIED && + hadEntry && + identityRecord?.verified === VerifiedStatus.VERIFIED + ) { + // Needs a notification. return true; } - return false; } diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index 836df44df78..06fd03f50e9 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -437,7 +437,6 @@ export type GroupV2PendingAdminApprovalType = { export type VerificationOptions = { key?: null | Uint8Array; - viaStorageServiceSync?: boolean; }; export type ShallowChallengeError = CustomError & { diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 44b7dfb95cc..21c7bf09146 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -215,8 +215,6 @@ export class ConversationModel extends window.Backbone typingPauseTimer?: NodeJS.Timer | null; - verifiedEnum?: typeof window.textsecure.storage.protocol.VerifiedStatus; - intlCollator = new Intl.Collator(undefined, { sensitivity: 'base' }); lastSuccessfulGroupFetch?: number; @@ -235,6 +233,8 @@ export class ConversationModel extends window.Backbone private isInReduxBatch = false; + private privVerifiedEnum?: typeof window.textsecure.storage.protocol.VerifiedStatus; + override defaults(): Partial { return { unreadCount: 0, @@ -286,7 +286,7 @@ export class ConversationModel extends window.Backbone this.storeName = 'conversations'; - this.verifiedEnum = window.textsecure.storage.protocol.VerifiedStatus; + this.privVerifiedEnum = window.textsecure.storage.protocol.VerifiedStatus; // This may be overridden by window.ConversationController.getOrCreate, and signify // our first save to the database. Or first fetch from the database. @@ -397,6 +397,11 @@ export class ConversationModel extends window.Backbone }; } + private get verifiedEnum(): typeof window.textsecure.storage.protocol.VerifiedStatus { + strictAssert(this.privVerifiedEnum, 'ConversationModel not initialize'); + return this.privVerifiedEnum; + } + private isMemberRequestingToJoin(uuid: UUID): boolean { if (!isGroupV2(this.attributes)) { return false; @@ -2633,13 +2638,14 @@ export class ConversationModel extends window.Backbone async safeGetVerified(): Promise { const uuid = this.getUuid(); if (!uuid) { - return window.textsecure.storage.protocol.VerifiedStatus.DEFAULT; + return this.verifiedEnum.DEFAULT; } - const promise = window.textsecure.storage.protocol.getVerified(uuid); - return promise.catch( - () => window.textsecure.storage.protocol.VerifiedStatus.DEFAULT - ); + try { + return await window.textsecure.storage.protocol.getVerified(uuid); + } catch { + return this.verifiedEnum.DEFAULT; + } } async updateVerified(): Promise { @@ -2668,24 +2674,21 @@ export class ConversationModel extends window.Backbone } setVerifiedDefault(options?: VerificationOptions): Promise { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { DEFAULT } = this.verifiedEnum!; + const { DEFAULT } = this.verifiedEnum; return this.queueJob('setVerifiedDefault', () => this._setVerified(DEFAULT, options) ); } setVerified(options?: VerificationOptions): Promise { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { VERIFIED } = this.verifiedEnum!; + const { VERIFIED } = this.verifiedEnum; return this.queueJob('setVerified', () => this._setVerified(VERIFIED, options) ); } setUnverified(options: VerificationOptions): Promise { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { UNVERIFIED } = this.verifiedEnum!; + const { UNVERIFIED } = this.verifiedEnum; return this.queueJob('setUnverified', () => this._setVerified(UNVERIFIED, options) ); @@ -2697,12 +2700,10 @@ export class ConversationModel extends window.Backbone ): Promise { const options = providedOptions || {}; window._.defaults(options, { - viaStorageServiceSync: false, key: null, }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { VERIFIED, DEFAULT } = this.verifiedEnum!; + const { VERIFIED, DEFAULT } = this.verifiedEnum; if (!isDirectConversation(this.attributes)) { throw new Error( @@ -2712,55 +2713,35 @@ export class ConversationModel extends window.Backbone } const uuid = this.getUuid(); - const beginningVerified = this.get('verified'); - let keyChange = false; - if (options.viaStorageServiceSync) { - strictAssert( - uuid, - `Sync message didn't update uuid for conversation: ${this.id}` - ); - - // handle the incoming key from the sync messages - need different - // behavior if that key doesn't match the current key - keyChange = - await window.textsecure.storage.protocol.processVerifiedMessage( - uuid, - verified, - options.key || undefined - ); - } else if (uuid) { - await window.textsecure.storage.protocol.setVerified(uuid, verified); + const beginningVerified = this.get('verified') ?? DEFAULT; + const keyChange = false; + if (uuid) { + if (verified === this.verifiedEnum.DEFAULT) { + await window.textsecure.storage.protocol.setVerified(uuid, verified); + } else { + await window.textsecure.storage.protocol.setVerified(uuid, verified, { + firstUse: false, + nonblockingApproval: true, + }); + } } else { log.warn(`_setVerified(${this.id}): no uuid to update protocol storage`); } this.set({ verified }); - // We will update the conversation during storage service sync - if (!options.viaStorageServiceSync) { - window.Signal.Data.updateConversation(this.attributes); - } + window.Signal.Data.updateConversation(this.attributes); - if (!options.viaStorageServiceSync) { - if (keyChange) { - this.captureChange('keyChange'); - } - if (beginningVerified !== verified) { - this.captureChange(`verified from=${beginningVerified} to=${verified}`); - } + if (beginningVerified !== verified) { + this.captureChange(`verified from=${beginningVerified} to=${verified}`); } const didVerifiedChange = beginningVerified !== verified; - const isExplicitUserAction = !options.viaStorageServiceSync; - const shouldShowFromStorageSync = - options.viaStorageServiceSync && verified !== DEFAULT; + const isExplicitUserAction = true; if ( // The message came from an explicit verification in a client (not // storage service sync) (didVerifiedChange && isExplicitUserAction) || - // The verification value received by the storage sync is different from what we - // have on record (and it's not a transition to UNVERIFIED) - (didVerifiedChange && shouldShowFromStorageSync) || // Our local verification status is VERIFIED and it hasn't changed, but the key did // change (Key1/VERIFIED -> Key2/VERIFIED), but we don't want to show DEFAULT -> // DEFAULT or UNVERIFIED -> UNVERIFIED @@ -2819,8 +2800,7 @@ export class ConversationModel extends window.Backbone isVerified(): boolean { if (isDirectConversation(this.attributes)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this.get('verified') === this.verifiedEnum!.VERIFIED; + return this.get('verified') === this.verifiedEnum.VERIFIED; } if (!this.contactCollection?.length) { @@ -2839,10 +2819,8 @@ export class ConversationModel extends window.Backbone if (isDirectConversation(this.attributes)) { const verified = this.get('verified'); return ( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - verified !== this.verifiedEnum!.VERIFIED && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - verified !== this.verifiedEnum!.DEFAULT + verified !== this.verifiedEnum.VERIFIED && + verified !== this.verifiedEnum.DEFAULT ); } @@ -3071,48 +3049,67 @@ export class ConversationModel extends window.Backbone } async addKeyChange(keyChangedId: UUID): Promise { - log.info( - 'adding key change advisory for', - this.idForLogging(), - keyChangedId.toString(), - this.get('timestamp') - ); - - const timestamp = Date.now(); - const message = { - conversationId: this.id, - type: 'keychange', - sent_at: this.get('timestamp'), - received_at: window.Signal.Util.incrementMessageCounter(), - received_at_ms: timestamp, - key_changed: keyChangedId.toString(), - readStatus: ReadStatus.Read, - seenStatus: SeenStatus.Unseen, - schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY, - // TODO: DESKTOP-722 - // this type does not fully implement the interface it is expected to - } as unknown as MessageAttributesType; + const keyChangedIdString = keyChangedId.toString(); + return this.queueJob(`addKeyChange(${keyChangedIdString})`, async () => { + log.info( + 'adding key change advisory for', + this.idForLogging(), + keyChangedIdString, + this.get('timestamp') + ); - const id = await window.Signal.Data.saveMessage(message, { - ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), - }); - const model = window.MessageController.register( - id, - new window.Whisper.Message({ - ...message, + const timestamp = Date.now(); + const message = { + conversationId: this.id, + type: 'keychange', + sent_at: this.get('timestamp'), + received_at: window.Signal.Util.incrementMessageCounter(), + received_at_ms: timestamp, + key_changed: keyChangedIdString, + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Unseen, + schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY, + // TODO: DESKTOP-722 + // this type does not fully implement the interface it is expected to + } as unknown as MessageAttributesType; + + const id = await window.Signal.Data.saveMessage(message, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + }); + const model = window.MessageController.register( id, - }) - ); + new window.Whisper.Message({ + ...message, + id, + }) + ); - const isUntrusted = await this.isUntrusted(); + const isUntrusted = await this.isUntrusted(); - this.trigger('newmessage', model); + this.trigger('newmessage', model); - const uuid = this.get('uuid'); - // Group calls are always with folks that have a UUID - if (isUntrusted && uuid) { - window.reduxActions.calling.keyChanged({ uuid }); - } + const uuid = this.get('uuid'); + // Group calls are always with folks that have a UUID + if (isUntrusted && uuid) { + window.reduxActions.calling.keyChanged({ uuid }); + } + + // Drop a member from sender key distribution list. + const senderKeyInfo = this.get('senderKeyInfo'); + if (senderKeyInfo) { + const updatedSenderKeyInfo = { + ...senderKeyInfo, + memberDevices: senderKeyInfo.memberDevices.filter( + ({ identifier }) => { + return identifier !== keyChangedIdString; + } + ), + }; + + this.set('senderKeyInfo', updatedSenderKeyInfo); + window.Signal.Data.updateConversation(this.attributes); + } + }); } async addVerifiedChange( diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 3257be7b8d2..4c4a2a28764 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -91,6 +91,22 @@ function toRecordVerified(verified: number): Proto.ContactRecord.IdentityState { } } +function fromRecordVerified( + verified: Proto.ContactRecord.IdentityState +): number { + const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus; + const STATE_ENUM = Proto.ContactRecord.IdentityState; + + switch (verified) { + case STATE_ENUM.VERIFIED: + return VERIFIED_ENUM.VERIFIED; + case STATE_ENUM.UNVERIFIED: + return VERIFIED_ENUM.UNVERIFIED; + default: + return VERIFIED_ENUM.DEFAULT; + } +} + function addUnknownFields( record: RecordClass, conversation: ConversationModel, @@ -991,35 +1007,34 @@ export async function mergeContactRecord( systemFamilyName: dropNull(contactRecord.systemFamilyName), }); + // https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt#L921-L936 if (contactRecord.identityKey) { const verified = await conversation.safeGetVerified(); - const storageServiceVerified = contactRecord.identityState || 0; - const verifiedOptions = { - key: contactRecord.identityKey, - viaStorageServiceSync: true, - }; - const STATE_ENUM = Proto.ContactRecord.IdentityState; + const newVerified = fromRecordVerified(contactRecord.identityState ?? 0); - if (verified !== storageServiceVerified) { - details.push(`updating verified state to=${verified}`); - } + const needsNotification = + await window.textsecure.storage.protocol.updateIdentityAfterSync( + new UUID(uuid), + newVerified, + contactRecord.identityKey + ); - // Update verified status unconditionally to make sure we will take the - // latest identity key from the manifest. - let keyChange: boolean; - switch (storageServiceVerified) { - case STATE_ENUM.VERIFIED: - keyChange = await conversation.setVerified(verifiedOptions); - break; - case STATE_ENUM.UNVERIFIED: - keyChange = await conversation.setUnverified(verifiedOptions); - break; - default: - keyChange = await conversation.setVerifiedDefault(verifiedOptions); + if (verified !== newVerified) { + details.push( + `updating verified state from=${verified} to=${newVerified}` + ); + + conversation.set({ verified: newVerified }); } - if (keyChange) { - details.push('key changed'); + const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus; + if (needsNotification) { + details.push('adding a verified notification'); + await conversation.addVerifiedChange( + conversation.id, + newVerified === VERIFIED_ENUM.VERIFIED, + { local: false } + ); } } diff --git a/ts/test-electron/SignalProtocolStore_test.ts b/ts/test-electron/SignalProtocolStore_test.ts index 1636590b2a0..b8e08470eca 100644 --- a/ts/test-electron/SignalProtocolStore_test.ts +++ b/ts/test-electron/SignalProtocolStore_test.ts @@ -641,11 +641,7 @@ describe('SignalProtocolStore', () => { describe('with the current public key', () => { before(saveRecordDefault); it('updates the verified status', async () => { - await store.setVerified( - theirUuid, - store.VerifiedStatus.VERIFIED, - testKey.pubKey - ); + await store.setVerified(theirUuid, store.VerifiedStatus.VERIFIED); const identity = await window.Signal.Data.getIdentityKeyById( theirUuid.toString() @@ -658,405 +654,111 @@ describe('SignalProtocolStore', () => { assert.isTrue(constantTimeEqual(identity.publicKey, testKey.pubKey)); }); }); - describe('with a mismatching public key', () => { - const newIdentity = getPublicKey(); - before(saveRecordDefault); - it('does not change the record.', async () => { - await store.setVerified( - theirUuid, - store.VerifiedStatus.VERIFIED, - newIdentity - ); - - const identity = await window.Signal.Data.getIdentityKeyById( - theirUuid.toString() - ); - if (!identity) { - throw new Error('Missing identity!'); - } - - assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); - assert.isTrue(constantTimeEqual(identity.publicKey, testKey.pubKey)); - }); - }); }); - describe('processVerifiedMessage', () => { + + describe('updateIdentityAfterSync', () => { const newIdentity = getPublicKey(); let keychangeTriggered: number; - beforeEach(() => { + beforeEach(async () => { keychangeTriggered = 0; store.on('keychange', () => { keychangeTriggered += 1; }); + + await window.Signal.Data.createOrUpdateIdentityKey({ + id: theirUuid.toString(), + publicKey: testKey.pubKey, + timestamp: Date.now() - 10 * 1000 * 60, + verified: store.VerifiedStatus.DEFAULT, + firstUse: false, + nonblockingApproval: false, + }); + await store.hydrateCaches(); }); + afterEach(() => { store.removeAllListeners('keychange'); }); - describe('when the new verified status is DEFAULT', () => { - describe('when there is no existing record', () => { - before(async () => { - await window.Signal.Data.removeIdentityKeyById(theirUuid.toString()); - await store.hydrateCaches(); - }); - - it('sets the identity key', async () => { - await store.processVerifiedMessage( - theirUuid, - store.VerifiedStatus.DEFAULT, - newIdentity - ); - - const identity = await window.Signal.Data.getIdentityKeyById( - theirUuid.toString() - ); - assert.isTrue( - identity?.publicKey && - constantTimeEqual(identity.publicKey, newIdentity) - ); - assert.strictEqual(keychangeTriggered, 0); - }); - }); - describe('when the record exists', () => { - describe('when the existing key is different', () => { - before(async () => { - await window.Signal.Data.createOrUpdateIdentityKey({ - id: theirUuid.toString(), - publicKey: testKey.pubKey, - firstUse: true, - timestamp: Date.now(), - verified: store.VerifiedStatus.VERIFIED, - nonblockingApproval: false, - }); - await store.hydrateCaches(); - }); - - it('updates the identity', async () => { - await store.processVerifiedMessage( - theirUuid, - store.VerifiedStatus.DEFAULT, - newIdentity - ); - - const identity = await window.Signal.Data.getIdentityKeyById( - theirUuid.toString() - ); - if (!identity) { - throw new Error('Missing identity!'); - } - - assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); - assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity)); - assert.strictEqual(keychangeTriggered, 1); - }); - }); - describe('when the existing key is the same but VERIFIED', () => { - before(async () => { - await window.Signal.Data.createOrUpdateIdentityKey({ - id: theirUuid.toString(), - publicKey: testKey.pubKey, - firstUse: true, - timestamp: Date.now(), - verified: store.VerifiedStatus.VERIFIED, - nonblockingApproval: false, - }); - await store.hydrateCaches(); - }); - - it('updates the verified status', async () => { - await store.processVerifiedMessage( - theirUuid, - store.VerifiedStatus.DEFAULT, - testKey.pubKey - ); + it('should create an identity and set verified to DEFAULT', async () => { + const newUuid = UUID.generate(); - const identity = await window.Signal.Data.getIdentityKeyById( - theirUuid.toString() - ); - if (!identity) { - throw new Error('Missing identity!'); - } - - assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); - assert.isTrue( - constantTimeEqual(identity.publicKey, testKey.pubKey) - ); - assert.strictEqual(keychangeTriggered, 0); - }); - }); - describe('when the existing key is the same and already DEFAULT', () => { - before(async () => { - await window.Signal.Data.createOrUpdateIdentityKey({ - id: theirUuid.toString(), - publicKey: testKey.pubKey, - firstUse: true, - timestamp: Date.now(), - verified: store.VerifiedStatus.DEFAULT, - nonblockingApproval: false, - }); - await store.hydrateCaches(); - }); - - it('does not hang', async () => { - await store.processVerifiedMessage( - theirUuid, - store.VerifiedStatus.DEFAULT, - testKey.pubKey - ); + const needsNotification = await store.updateIdentityAfterSync( + newUuid, + store.VerifiedStatus.DEFAULT, + newIdentity + ); + assert.isFalse(needsNotification); + assert.strictEqual(keychangeTriggered, 0); - assert.strictEqual(keychangeTriggered, 0); - }); - }); - }); + const identity = await window.Signal.Data.getIdentityKeyById( + newUuid.toString() + ); + if (!identity) { + throw new Error('Missing identity!'); + } + assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); + assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity)); }); - describe('when the new verified status is UNVERIFIED', () => { - describe('when there is no existing record', () => { - before(async () => { - await window.Signal.Data.removeIdentityKeyById(theirUuid.toString()); - await store.hydrateCaches(); - }); - it('saves the new identity and marks it UNVERIFIED', async () => { - await store.processVerifiedMessage( - theirUuid, - store.VerifiedStatus.UNVERIFIED, - newIdentity - ); + it('should create an identity and set verified to VERIFIED', async () => { + const newUuid = UUID.generate(); - const identity = await window.Signal.Data.getIdentityKeyById( - theirUuid.toString() - ); - if (!identity) { - throw new Error('Missing identity!'); - } - - assert.strictEqual( - identity.verified, - store.VerifiedStatus.UNVERIFIED - ); - assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity)); - assert.strictEqual(keychangeTriggered, 0); - }); - }); - describe('when the record exists', () => { - describe('when the existing key is different', () => { - before(async () => { - await window.Signal.Data.createOrUpdateIdentityKey({ - id: theirUuid.toString(), - publicKey: testKey.pubKey, - firstUse: true, - timestamp: Date.now(), - verified: store.VerifiedStatus.VERIFIED, - nonblockingApproval: false, - }); - await store.hydrateCaches(); - }); - - it('saves the new identity and marks it UNVERIFIED', async () => { - await store.processVerifiedMessage( - theirUuid, - store.VerifiedStatus.UNVERIFIED, - newIdentity - ); - - const identity = await window.Signal.Data.getIdentityKeyById( - theirUuid.toString() - ); - if (!identity) { - throw new Error('Missing identity!'); - } - - assert.strictEqual( - identity.verified, - store.VerifiedStatus.UNVERIFIED - ); - assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity)); - assert.strictEqual(keychangeTriggered, 1); - }); - }); - describe('when the key exists and is DEFAULT', () => { - before(async () => { - await window.Signal.Data.createOrUpdateIdentityKey({ - id: theirUuid.toString(), - publicKey: testKey.pubKey, - firstUse: true, - timestamp: Date.now(), - verified: store.VerifiedStatus.DEFAULT, - nonblockingApproval: false, - }); - await store.hydrateCaches(); - }); - - it('updates the verified status', async () => { - await store.processVerifiedMessage( - theirUuid, - store.VerifiedStatus.UNVERIFIED, - testKey.pubKey - ); - const identity = await window.Signal.Data.getIdentityKeyById( - theirUuid.toString() - ); - if (!identity) { - throw new Error('Missing identity!'); - } - - assert.strictEqual( - identity.verified, - store.VerifiedStatus.UNVERIFIED - ); - assert.isTrue( - constantTimeEqual(identity.publicKey, testKey.pubKey) - ); - assert.strictEqual(keychangeTriggered, 0); - }); - }); - describe('when the key exists and is already UNVERIFIED', () => { - before(async () => { - await window.Signal.Data.createOrUpdateIdentityKey({ - id: theirUuid.toString(), - publicKey: testKey.pubKey, - firstUse: true, - timestamp: Date.now(), - verified: store.VerifiedStatus.UNVERIFIED, - nonblockingApproval: false, - }); - await store.hydrateCaches(); - }); - - it('does not hang', async () => { - await store.processVerifiedMessage( - theirUuid, - store.VerifiedStatus.UNVERIFIED, - testKey.pubKey - ); + const needsNotification = await store.updateIdentityAfterSync( + newUuid, + store.VerifiedStatus.VERIFIED, + newIdentity + ); + assert.isTrue(needsNotification); + assert.strictEqual(keychangeTriggered, 0); - assert.strictEqual(keychangeTriggered, 0); - }); - }); - }); + const identity = await window.Signal.Data.getIdentityKeyById( + newUuid.toString() + ); + if (!identity) { + throw new Error('Missing identity!'); + } + assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED); + assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity)); }); - describe('when the new verified status is VERIFIED', () => { - describe('when there is no existing record', () => { - before(async () => { - await window.Signal.Data.removeIdentityKeyById(theirUuid.toString()); - await store.hydrateCaches(); - }); - it('saves the new identity and marks it verified', async () => { - await store.processVerifiedMessage( - theirUuid, - store.VerifiedStatus.VERIFIED, - newIdentity - ); - const identity = await window.Signal.Data.getIdentityKeyById( - theirUuid.toString() - ); - if (!identity) { - throw new Error('Missing identity!'); - } - - assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED); - assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity)); - assert.strictEqual(keychangeTriggered, 0); - }); - }); - describe('when the record exists', () => { - describe('when the existing key is different', () => { - before(async () => { - await window.Signal.Data.createOrUpdateIdentityKey({ - id: theirUuid.toString(), - publicKey: testKey.pubKey, - firstUse: true, - timestamp: Date.now(), - verified: store.VerifiedStatus.VERIFIED, - nonblockingApproval: false, - }); - await store.hydrateCaches(); - }); - - it('saves the new identity and marks it VERIFIED', async () => { - await store.processVerifiedMessage( - theirUuid, - store.VerifiedStatus.VERIFIED, - newIdentity - ); - - const identity = await window.Signal.Data.getIdentityKeyById( - theirUuid.toString() - ); - if (!identity) { - throw new Error('Missing identity!'); - } - - assert.strictEqual( - identity.verified, - store.VerifiedStatus.VERIFIED - ); - assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity)); - assert.strictEqual(keychangeTriggered, 1); - }); - }); - describe('when the existing key is the same but UNVERIFIED', () => { - before(async () => { - await window.Signal.Data.createOrUpdateIdentityKey({ - id: theirUuid.toString(), - publicKey: testKey.pubKey, - firstUse: true, - timestamp: Date.now(), - verified: store.VerifiedStatus.UNVERIFIED, - nonblockingApproval: false, - }); - await store.hydrateCaches(); - }); - - it('saves the identity and marks it verified', async () => { - await store.processVerifiedMessage( - theirUuid, - store.VerifiedStatus.VERIFIED, - testKey.pubKey - ); - const identity = await window.Signal.Data.getIdentityKeyById( - theirUuid.toString() - ); - if (!identity) { - throw new Error('Missing identity!'); - } + it('should update public key without verified change', async () => { + const needsNotification = await store.updateIdentityAfterSync( + theirUuid, + store.VerifiedStatus.DEFAULT, + newIdentity + ); + assert.isFalse(needsNotification); + assert.strictEqual(keychangeTriggered, 1); - assert.strictEqual( - identity.verified, - store.VerifiedStatus.VERIFIED - ); - assert.isTrue( - constantTimeEqual(identity.publicKey, testKey.pubKey) - ); - assert.strictEqual(keychangeTriggered, 0); - }); - }); - describe('when the existing key is the same and already VERIFIED', () => { - before(async () => { - await window.Signal.Data.createOrUpdateIdentityKey({ - id: theirUuid.toString(), - publicKey: testKey.pubKey, - firstUse: true, - timestamp: Date.now(), - verified: store.VerifiedStatus.VERIFIED, - nonblockingApproval: false, - }); - await store.hydrateCaches(); - }); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); + if (!identity) { + throw new Error('Missing identity!'); + } + assert.strictEqual(identity.verified, store.VerifiedStatus.DEFAULT); + assert.isTrue(constantTimeEqual(identity.publicKey, newIdentity)); + }); - it('does not hang', async () => { - await store.processVerifiedMessage( - theirUuid, - store.VerifiedStatus.VERIFIED, - testKey.pubKey - ); + it('should update verified without public key change', async () => { + const needsNotification = await store.updateIdentityAfterSync( + theirUuid, + store.VerifiedStatus.VERIFIED, + testKey.pubKey + ); + assert.isTrue(needsNotification); + assert.strictEqual(keychangeTriggered, 0); - assert.strictEqual(keychangeTriggered, 0); - }); - }); - }); + const identity = await window.Signal.Data.getIdentityKeyById( + theirUuid.toString() + ); + if (!identity) { + throw new Error('Missing identity!'); + } + assert.strictEqual(identity.verified, store.VerifiedStatus.VERIFIED); + assert.isTrue(constantTimeEqual(identity.publicKey, testKey.pubKey)); }); }); diff --git a/ts/textsecure/storage/User.ts b/ts/textsecure/storage/User.ts index 9888f138a91..a72bb4f3610 100644 --- a/ts/textsecure/storage/User.ts +++ b/ts/textsecure/storage/User.ts @@ -106,6 +106,10 @@ export class User { return UUIDKind.Unknown; } + public isOurUuid(uuid: UUID): boolean { + return this.getOurUuidKind(uuid) !== UUIDKind.Unknown; + } + public getDeviceId(): number | undefined { const value = this._getDeviceIdFromUuid() || this._getDeviceIdFromNumber(); if (value === undefined) {