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