Support for loading, storing, and using kyber keys in decryption

This commit is contained in:
Scott Nonnenberg 2023-07-14 09:53:20 -07:00 committed by Fedor Indutnyy
parent c1580a5eb3
commit b6445a6af0
49 changed files with 2260 additions and 806 deletions

View file

@ -89,7 +89,7 @@
"@popperjs/core": "2.11.6", "@popperjs/core": "2.11.6",
"@react-spring/web": "9.5.5", "@react-spring/web": "9.5.5",
"@signalapp/better-sqlite3": "8.4.3", "@signalapp/better-sqlite3": "8.4.3",
"@signalapp/libsignal-client": "0.22.0", "@signalapp/libsignal-client": "0.27.0",
"@signalapp/ringrtc": "2.29.0", "@signalapp/ringrtc": "2.29.0",
"@types/fabric": "4.5.3", "@types/fabric": "4.5.3",
"backbone": "1.4.0", "backbone": "1.4.0",
@ -189,7 +189,7 @@
"@electron/fuses": "1.5.0", "@electron/fuses": "1.5.0",
"@formatjs/intl": "2.6.7", "@formatjs/intl": "2.6.7",
"@mixer/parallel-prettier": "2.0.3", "@mixer/parallel-prettier": "2.0.3",
"@signalapp/mock-server": "3.0.1", "@signalapp/mock-server": "3.1.0",
"@storybook/addon-a11y": "6.5.6", "@storybook/addon-a11y": "6.5.6",
"@storybook/addon-actions": "6.5.6", "@storybook/addon-actions": "6.5.6",
"@storybook/addon-controls": "6.5.6", "@storybook/addon-controls": "6.5.6",

View file

@ -587,9 +587,11 @@ message SyncMessage {
message PniChangeNumber { message PniChangeNumber {
optional bytes identityKeyPair = 1; // Serialized libsignal-client IdentityKeyPair optional bytes identityKeyPair = 1; // Serialized libsignal-client IdentityKeyPair
optional bytes signedPreKey = 2; // Serialized libsignal-client SignedPreKeyRecord optional bytes signedPreKey = 2; // Serialized libsignal-client SignedPreKeyRecord
optional bytes lastResortKyberPreKey = 5; // Serialized libsignal-client KyberPreKeyRecord
optional uint32 registrationId = 3; optional uint32 registrationId = 3;
optional string newE164 = 4; // The e164 we have changed our number to optional string newE164 = 4; // The e164 we have changed our number to
// Next ID: 6
} }
message CallEvent { message CallEvent {

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as client from '@signalapp/libsignal-client'; import * as client from '@signalapp/libsignal-client';
import type { KyberPreKeyRecord } from '@signalapp/libsignal-client';
import * as Bytes from './Bytes'; import * as Bytes from './Bytes';
import { constantTimeEqual } from './Crypto'; import { constantTimeEqual } from './Crypto';
@ -59,6 +60,40 @@ export function generatePreKey(keyId: number): CompatPreKeyType {
}; };
} }
export function generateKyberPreKey(
identityKeyPair: KeyPairType,
keyId: number
): KyberPreKeyRecord {
if (!isNonNegativeInteger(keyId)) {
throw new TypeError(
`generateKyberPreKey: Invalid argument for keyId: ${keyId}`
);
}
if (
!(identityKeyPair.privKey instanceof Uint8Array) ||
identityKeyPair.privKey.byteLength !== 32 ||
!(identityKeyPair.pubKey instanceof Uint8Array) ||
identityKeyPair.pubKey.byteLength !== 33
) {
throw new TypeError(
'generateKyberPreKey: Invalid argument for identityKeyPair'
);
}
const keyPair = client.KEMKeyPair.generate();
const signature = calculateSignature(
identityKeyPair.privKey,
keyPair.getPublicKey().serialize()
);
return client.KyberPreKeyRecord.new(
keyId,
Date.now(),
keyPair,
Buffer.from(signature)
);
}
export function generateKeyPair(): KeyPairType { export function generateKeyPair(): KeyPairType {
const privKey = client.PrivateKey.generate(); const privKey = client.PrivateKey.generate();
const pubKey = privKey.getPublicKey(); const pubKey = privKey.getPublicKey();

View file

@ -7,6 +7,7 @@ import { isNumber } from 'lodash';
import type { import type {
Direction, Direction,
KyberPreKeyRecord,
PreKeyRecord, PreKeyRecord,
ProtocolAddress, ProtocolAddress,
SenderKeyRecord, SenderKeyRecord,
@ -16,6 +17,7 @@ import type {
} from '@signalapp/libsignal-client'; } from '@signalapp/libsignal-client';
import { import {
IdentityKeyStore, IdentityKeyStore,
KyberPreKeyStore,
PreKeyStore, PreKeyStore,
PrivateKey, PrivateKey,
PublicKey, PublicKey,
@ -23,7 +25,6 @@ import {
SessionStore, SessionStore,
SignedPreKeyStore, SignedPreKeyStore,
} from '@signalapp/libsignal-client'; } from '@signalapp/libsignal-client';
import { freezePreKey, freezeSignedPreKey } from './SignalProtocolStore';
import { Address } from './types/Address'; import { Address } from './types/Address';
import { QualifiedAddress } from './types/QualifiedAddress'; import { QualifiedAddress } from './types/QualifiedAddress';
import type { UUID } from './types/UUID'; import type { UUID } from './types/UUID';
@ -187,12 +188,8 @@ export class PreKeys extends PreKeyStore {
this.ourUuid = ourUuid; this.ourUuid = ourUuid;
} }
async savePreKey(id: number, record: PreKeyRecord): Promise<void> { async savePreKey(): Promise<void> {
await window.textsecure.storage.protocol.storePreKey( throw new Error('savePreKey: Should not be called by libsignal!');
this.ourUuid,
id,
freezePreKey(record)
);
} }
async getPreKey(id: number): Promise<PreKeyRecord> { async getPreKey(id: number): Promise<PreKeyRecord> {
@ -209,7 +206,41 @@ export class PreKeys extends PreKeyStore {
} }
async removePreKey(id: number): Promise<void> { async removePreKey(id: number): Promise<void> {
await window.textsecure.storage.protocol.removePreKey(this.ourUuid, id); await window.textsecure.storage.protocol.removePreKeys(this.ourUuid, [id]);
}
}
export class KyberPreKeys extends KyberPreKeyStore {
private readonly ourUuid: UUID;
constructor({ ourUuid }: PreKeysOptions) {
super();
this.ourUuid = ourUuid;
}
async saveKyberPreKey(): Promise<void> {
throw new Error('saveKyberPreKey: Should not be called by libsignal!');
}
async getKyberPreKey(id: number): Promise<KyberPreKeyRecord> {
const kyberPreKey =
await window.textsecure.storage.protocol.loadKyberPreKey(
this.ourUuid,
id
);
if (kyberPreKey === undefined) {
throw new Error(`getKyberPreKey: KyberPreKey ${id} not found`);
}
return kyberPreKey;
}
async markKyberPreKeyUsed(id: number): Promise<void> {
await window.textsecure.storage.protocol.maybeRemoveKyberPreKey(
this.ourUuid,
id
);
} }
} }
@ -272,16 +303,8 @@ export class SignedPreKeys extends SignedPreKeyStore {
this.ourUuid = ourUuid; this.ourUuid = ourUuid;
} }
async saveSignedPreKey( async saveSignedPreKey(): Promise<void> {
id: number, throw new Error('saveSignedPreKey: Should not be called by libsignal!');
record: SignedPreKeyRecord
): Promise<void> {
await window.textsecure.storage.protocol.storeSignedPreKey(
this.ourUuid,
id,
freezeSignedPreKey(record),
true
);
} }
async getSignedPreKey(id: number): Promise<SignedPreKeyRecord> { async getSignedPreKey(id: number): Promise<SignedPreKeyRecord> {

View file

@ -9,6 +9,7 @@ import { EventEmitter } from 'events';
import { import {
Direction, Direction,
IdentityKeyPair, IdentityKeyPair,
KyberPreKeyRecord,
PreKeyRecord, PreKeyRecord,
PrivateKey, PrivateKey,
PublicKey, PublicKey,
@ -32,6 +33,7 @@ import type {
IdentityKeyType, IdentityKeyType,
IdentityKeyIdType, IdentityKeyIdType,
KeyPairType, KeyPairType,
KyberPreKeyType,
OuterSignedPrekeyType, OuterSignedPrekeyType,
PniKeyMaterialType, PniKeyMaterialType,
PniSignatureMessageType, PniSignatureMessageType,
@ -46,6 +48,7 @@ import type {
SignedPreKeyType, SignedPreKeyType,
UnprocessedType, UnprocessedType,
UnprocessedUpdateType, UnprocessedUpdateType,
CompatPreKeyType,
} from './textsecure/Types.d'; } from './textsecure/Types.d';
import type { RemoveAllConfiguration } from './types/RemoveAllConfiguration'; import type { RemoveAllConfiguration } from './types/RemoveAllConfiguration';
import type { UUIDStringType } from './types/UUID'; import type { UUIDStringType } from './types/UUID';
@ -57,8 +60,13 @@ import * as log from './logging/log';
import * as Errors from './types/errors'; import * as Errors from './types/errors';
import { MINUTE } from './util/durations'; import { MINUTE } from './util/durations';
import { conversationJobQueue } from './jobs/conversationJobQueue'; import { conversationJobQueue } from './jobs/conversationJobQueue';
import {
KYBER_KEY_ID_KEY,
SIGNED_PRE_KEY_ID_KEY,
} from './textsecure/AccountManager';
const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds
const LOW_KEYS_THRESHOLD = 25;
const VerifiedStatus = { const VerifiedStatus = {
DEFAULT: 0, DEFAULT: 0,
@ -103,6 +111,7 @@ type CacheEntryType<DBType, HydratedType> =
| { hydrated: true; fromDB: DBType; item: HydratedType }; | { hydrated: true; fromDB: DBType; item: HydratedType };
type MapFields = type MapFields =
| 'kyberPreKeys'
| 'identityKeys' | 'identityKeys'
| 'preKeys' | 'preKeys'
| 'senderKeys' | 'senderKeys'
@ -226,6 +235,11 @@ export class SignalProtocolStore extends EventEmitter {
CacheEntryType<IdentityKeyType, PublicKey> CacheEntryType<IdentityKeyType, PublicKey>
>; >;
kyberPreKeys?: Map<
PreKeyIdType,
CacheEntryType<KyberPreKeyType, KyberPreKeyRecord>
>;
senderKeys?: Map<SenderKeyIdType, SenderKeyCacheEntry>; senderKeys?: Map<SenderKeyIdType, SenderKeyCacheEntry>;
sessions?: Map<SessionIdType, SessionCacheEntry>; sessions?: Map<SessionIdType, SessionCacheEntry>;
@ -290,6 +304,11 @@ export class SignalProtocolStore extends EventEmitter {
'identityKeys', 'identityKeys',
window.Signal.Data.getAllIdentityKeys() window.Signal.Data.getAllIdentityKeys()
), ),
_fillCaches<string, KyberPreKeyType, KyberPreKeyRecord>(
this,
'kyberPreKeys',
window.Signal.Data.getAllKyberPreKeys()
),
_fillCaches<string, SessionType, SessionRecord>( _fillCaches<string, SessionType, SessionRecord>(
this, this,
'sessions', 'sessions',
@ -321,6 +340,190 @@ export class SignalProtocolStore extends EventEmitter {
return this.ourRegistrationIds.get(ourUuid.toString()); return this.ourRegistrationIds.get(ourUuid.toString());
} }
private _getKeyId(ourUuid: UUID, keyId: number): PreKeyIdType {
return `${ourUuid.toString()}:${keyId}`;
}
// KyberPreKeys
private _getKyberPreKeyEntry(
id: PreKeyIdType,
logContext: string
):
| { hydrated: true; fromDB: KyberPreKeyType; item: KyberPreKeyRecord }
| undefined {
if (!this.kyberPreKeys) {
throw new Error(`${logContext}: this.kyberPreKeys not yet cached!`);
}
const entry = this.kyberPreKeys.get(id);
if (!entry) {
log.error(`${logContext}: Failed to fetch kyber prekey: ${id}`);
return undefined;
}
if (entry.hydrated) {
log.info(
`${logContext}: Successfully fetched kyber prekey (cache hit): ${id}`
);
return entry;
}
const item = KyberPreKeyRecord.deserialize(Buffer.from(entry.fromDB.data));
const newEntry = {
hydrated: true as const,
fromDB: entry.fromDB,
item,
};
this.kyberPreKeys.set(id, newEntry);
log.info(
`${logContext}: Successfully fetched kyberPreKey (cache miss): ${id}`
);
return newEntry;
}
async loadKyberPreKey(
ourUuid: UUID,
keyId: number
): Promise<KyberPreKeyRecord | undefined> {
const id: PreKeyIdType = this._getKeyId(ourUuid, keyId);
const entry = this._getKyberPreKeyEntry(id, 'loadKyberPreKey');
return entry?.item;
}
loadKyberPreKeys(
ourUuid: UUID,
{ isLastResort }: { isLastResort: boolean }
): Array<KyberPreKeyType> {
if (!this.kyberPreKeys) {
throw new Error('loadKyberPreKeys: this.kyberPreKeys not yet cached!');
}
if (arguments.length > 2) {
throw new Error('loadKyberPreKeys takes two arguments');
}
const entries = Array.from(this.kyberPreKeys.values());
return entries
.map(item => item.fromDB)
.filter(
item =>
item.ourUuid === ourUuid.toString() &&
item.isLastResort === isLastResort
);
}
async confirmKyberPreKey(ourUuid: UUID, keyId: number): Promise<void> {
const kyberPreKeyCache = this.kyberPreKeys;
if (!kyberPreKeyCache) {
throw new Error('storeKyberPreKey: this.kyberPreKeys not yet cached!');
}
const id: PreKeyIdType = this._getKeyId(ourUuid, keyId);
const item = kyberPreKeyCache.get(id);
if (!item) {
throw new Error(`confirmKyberPreKey: missing kyber prekey ${id}!`);
}
const confirmedItem = {
...item,
fromDB: {
...item.fromDB,
isConfirmed: true,
},
};
await window.Signal.Data.createOrUpdateKyberPreKey(confirmedItem.fromDB);
kyberPreKeyCache.set(id, confirmedItem);
}
async storeKyberPreKeys(
ourUuid: UUID,
keys: Array<Omit<KyberPreKeyType, 'id'>>
): Promise<void> {
const kyberPreKeyCache = this.kyberPreKeys;
if (!kyberPreKeyCache) {
throw new Error('storeKyberPreKey: this.kyberPreKeys not yet cached!');
}
const toSave: Array<KyberPreKeyType> = [];
keys.forEach(key => {
const id: PreKeyIdType = this._getKeyId(ourUuid, key.keyId);
if (kyberPreKeyCache.has(id)) {
throw new Error(`storeKyberPreKey: kyber prekey ${id} already exists!`);
}
const kyberPreKey = {
id,
createdAt: key.createdAt,
data: key.data,
isConfirmed: key.isConfirmed,
isLastResort: key.isLastResort,
keyId: key.keyId,
ourUuid: ourUuid.toString(),
};
toSave.push(kyberPreKey);
});
await window.Signal.Data.bulkAddKyberPreKeys(toSave);
toSave.forEach(kyberPreKey => {
kyberPreKeyCache.set(kyberPreKey.id, {
hydrated: false,
fromDB: kyberPreKey,
});
});
}
async maybeRemoveKyberPreKey(ourUuid: UUID, keyId: number): Promise<void> {
const id: PreKeyIdType = this._getKeyId(ourUuid, keyId);
const entry = this._getKyberPreKeyEntry(id, 'maybeRemoveKyberPreKey');
if (!entry) {
return;
}
if (entry.fromDB.isLastResort) {
log.info(
`maybeRemoveKyberPreKey: Not removing kyber prekey ${id}; it's a last resort key`
);
return;
}
await this.removeKyberPreKeys(ourUuid, [keyId]);
}
async removeKyberPreKeys(
ourUuid: UUID,
keyIds: Array<number>
): Promise<void> {
const kyberPreKeyCache = this.kyberPreKeys;
if (!kyberPreKeyCache) {
throw new Error('removeKyberPreKeys: this.kyberPreKeys not yet cached!');
}
const ids = keyIds.map(keyId => this._getKeyId(ourUuid, keyId));
await window.Signal.Data.removeKyberPreKeyById(ids);
ids.forEach(id => {
kyberPreKeyCache.delete(id);
});
if (kyberPreKeyCache.size < LOW_KEYS_THRESHOLD) {
this.emitLowKeys(ourUuid, `removeKyberPreKeys@${kyberPreKeyCache.size}`);
}
}
async clearKyberPreKeyStore(): Promise<void> {
if (this.kyberPreKeys) {
this.kyberPreKeys.clear();
}
await window.Signal.Data.removeAllKyberPreKeys();
}
// PreKeys // PreKeys
async loadPreKey( async loadPreKey(
@ -331,8 +534,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('loadPreKey: this.preKeys not yet cached!'); throw new Error('loadPreKey: this.preKeys not yet cached!');
} }
const id: PreKeyIdType = `${ourUuid.toString()}:${keyId}`; const id: PreKeyIdType = this._getKeyId(ourUuid, keyId);
const entry = this.preKeys.get(id); const entry = this.preKeys.get(id);
if (!entry) { if (!entry) {
log.error('Failed to fetch prekey:', id); log.error('Failed to fetch prekey:', id);
@ -354,53 +556,76 @@ export class SignalProtocolStore extends EventEmitter {
return item; return item;
} }
async storePreKey( loadPreKeys(ourUuid: UUID): Array<PreKeyType> {
ourUuid: UUID,
keyId: number,
keyPair: KeyPairType
): Promise<void> {
if (!this.preKeys) { if (!this.preKeys) {
throw new Error('loadPreKeys: this.preKeys not yet cached!');
}
if (arguments.length > 1) {
throw new Error('loadPreKeys takes one argument');
}
const entries = Array.from(this.preKeys.values());
return entries
.map(item => item.fromDB)
.filter(item => item.ourUuid === ourUuid.toString());
}
async storePreKeys(
ourUuid: UUID,
keys: Array<CompatPreKeyType>
): Promise<void> {
const preKeyCache = this.preKeys;
if (!preKeyCache) {
throw new Error('storePreKey: this.preKeys not yet cached!'); throw new Error('storePreKey: this.preKeys not yet cached!');
} }
const id: PreKeyIdType = `${ourUuid.toString()}:${keyId}`; const now = Date.now();
if (this.preKeys.has(id)) { const toSave: Array<PreKeyType> = [];
throw new Error(`storePreKey: prekey ${id} already exists!`); keys.forEach(key => {
} const id: PreKeyIdType = this._getKeyId(ourUuid, key.keyId);
const fromDB = { if (preKeyCache.has(id)) {
id, throw new Error(`storePreKeys: prekey ${id} already exists!`);
keyId, }
ourUuid: ourUuid.toString(),
publicKey: keyPair.pubKey,
privateKey: keyPair.privKey,
};
await window.Signal.Data.createOrUpdatePreKey(fromDB); const preKey = {
this.preKeys.set(id, { id,
hydrated: false, keyId: key.keyId,
fromDB, ourUuid: ourUuid.toString(),
publicKey: key.keyPair.pubKey,
privateKey: key.keyPair.privKey,
createdAt: now,
};
toSave.push(preKey);
});
await window.Signal.Data.bulkAddPreKeys(toSave);
toSave.forEach(preKey => {
preKeyCache.set(preKey.id, {
hydrated: false,
fromDB: preKey,
});
}); });
} }
async removePreKey(ourUuid: UUID, keyId: number): Promise<void> { async removePreKeys(ourUuid: UUID, keyIds: Array<number>): Promise<void> {
if (!this.preKeys) { const preKeyCache = this.preKeys;
throw new Error('removePreKey: this.preKeys not yet cached!'); if (!preKeyCache) {
throw new Error('removePreKeys: this.preKeys not yet cached!');
} }
const id: PreKeyIdType = `${ourUuid.toString()}:${keyId}`; const ids = keyIds.map(keyId => this._getKeyId(ourUuid, keyId));
try { await window.Signal.Data.removePreKeyById(ids);
this.emit('removePreKey', ourUuid); ids.forEach(id => {
} catch (error) { preKeyCache.delete(id);
log.error( });
'removePreKey error triggering removePreKey:',
Errors.toLogFormat(error) if (preKeyCache.size < LOW_KEYS_THRESHOLD) {
); this.emitLowKeys(ourUuid, `removePreKeys@${preKeyCache.size}`);
} }
this.preKeys.delete(id);
await window.Signal.Data.removePreKeyById(id);
} }
async clearPreKeyStore(): Promise<void> { async clearPreKeyStore(): Promise<void> {
@ -443,9 +668,7 @@ export class SignalProtocolStore extends EventEmitter {
return item; return item;
} }
async loadSignedPreKeys( loadSignedPreKeys(ourUuid: UUID): Array<OuterSignedPrekeyType> {
ourUuid: UUID
): Promise<Array<OuterSignedPrekeyType>> {
if (!this.signedPreKeys) { if (!this.signedPreKeys) {
throw new Error('loadSignedPreKeys: this.signedPreKeys not yet cached!'); throw new Error('loadSignedPreKeys: this.signedPreKeys not yet cached!');
} }
@ -469,8 +692,30 @@ export class SignalProtocolStore extends EventEmitter {
}); });
} }
// Note that this is also called in update scenarios, for confirming that signed prekeys async confirmSignedPreKey(ourUuid: UUID, keyId: number): Promise<void> {
// have indeed been accepted by the server. const signedPreKeyCache = this.signedPreKeys;
if (!signedPreKeyCache) {
throw new Error('storeKyberPreKey: this.signedPreKeys not yet cached!');
}
const id: PreKeyIdType = this._getKeyId(ourUuid, keyId);
const item = signedPreKeyCache.get(id);
if (!item) {
throw new Error(`confirmSignedPreKey: missing prekey ${id}!`);
}
const confirmedItem = {
...item,
fromDB: {
...item.fromDB,
confirmed: true,
},
};
await window.Signal.Data.createOrUpdateSignedPreKey(confirmedItem.fromDB);
signedPreKeyCache.set(id, confirmedItem);
}
async storeSignedPreKey( async storeSignedPreKey(
ourUuid: UUID, ourUuid: UUID,
keyId: number, keyId: number,
@ -482,7 +727,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('storeSignedPreKey: this.signedPreKeys not yet cached!'); throw new Error('storeSignedPreKey: this.signedPreKeys not yet cached!');
} }
const id: SignedPreKeyIdType = `${ourUuid.toString()}:${keyId}`; const id: SignedPreKeyIdType = this._getKeyId(ourUuid, keyId);
const fromDB = { const fromDB = {
id, id,
@ -501,14 +746,21 @@ export class SignalProtocolStore extends EventEmitter {
}); });
} }
async removeSignedPreKey(ourUuid: UUID, keyId: number): Promise<void> { async removeSignedPreKeys(
if (!this.signedPreKeys) { ourUuid: UUID,
keyIds: Array<number>
): Promise<void> {
const signedPreKeyCache = this.signedPreKeys;
if (!signedPreKeyCache) {
throw new Error('removeSignedPreKey: this.signedPreKeys not yet cached!'); throw new Error('removeSignedPreKey: this.signedPreKeys not yet cached!');
} }
const id: SignedPreKeyIdType = `${ourUuid.toString()}:${keyId}`; const ids = keyIds.map(keyId => this._getKeyId(ourUuid, keyId));
this.signedPreKeys.delete(id);
await window.Signal.Data.removeSignedPreKeyById(id); await window.Signal.Data.removeSignedPreKeyById(ids);
ids.forEach(id => {
signedPreKeyCache.delete(id);
});
} }
async clearSignedPreKeysStore(): Promise<void> { async clearSignedPreKeysStore(): Promise<void> {
@ -2187,6 +2439,13 @@ export class SignalProtocolStore extends EventEmitter {
} }
} }
} }
if (this.kyberPreKeys) {
for (const key of this.kyberPreKeys.keys()) {
if (key.startsWith(preKeyPrefix)) {
this.kyberPreKeys.delete(key);
}
}
}
// Update database // Update database
await Promise.all([ await Promise.all([
@ -2200,6 +2459,7 @@ export class SignalProtocolStore extends EventEmitter {
), ),
window.Signal.Data.removePreKeysByUuid(oldPni.toString()), window.Signal.Data.removePreKeysByUuid(oldPni.toString()),
window.Signal.Data.removeSignedPreKeysByUuid(oldPni.toString()), window.Signal.Data.removeSignedPreKeysByUuid(oldPni.toString()),
window.Signal.Data.removeKyberPreKeysByUuid(oldPni.toString()),
]); ]);
} }
@ -2207,11 +2467,13 @@ export class SignalProtocolStore extends EventEmitter {
pni: UUID, pni: UUID,
{ {
identityKeyPair: identityBytes, identityKeyPair: identityBytes,
lastResortKyberPreKey: lastResortKyberPreKeyBytes,
signedPreKey: signedPreKeyBytes, signedPreKey: signedPreKeyBytes,
registrationId, registrationId,
}: PniKeyMaterialType }: PniKeyMaterialType
): Promise<void> { ): Promise<void> {
log.info(`SignalProtocolStore.updateOurPniKeyMaterial(${pni})`); const logId = `SignalProtocolStore.updateOurPniKeyMaterial(${pni})`;
log.info(`${logId}: starting...`);
const identityKeyPair = IdentityKeyPair.deserialize( const identityKeyPair = IdentityKeyPair.deserialize(
Buffer.from(identityBytes) Buffer.from(identityBytes)
@ -2219,6 +2481,9 @@ export class SignalProtocolStore extends EventEmitter {
const signedPreKey = SignedPreKeyRecord.deserialize( const signedPreKey = SignedPreKeyRecord.deserialize(
Buffer.from(signedPreKeyBytes) Buffer.from(signedPreKeyBytes)
); );
const lastResortKyberPreKey = lastResortKyberPreKeyBytes
? KyberPreKeyRecord.deserialize(Buffer.from(lastResortKyberPreKeyBytes))
: undefined;
const { storage } = window; const { storage } = window;
@ -2245,6 +2510,11 @@ export class SignalProtocolStore extends EventEmitter {
...(storage.get('registrationIdMap') || {}), ...(storage.get('registrationIdMap') || {}),
[pni.toString()]: registrationId, [pni.toString()]: registrationId,
}), }),
async () => {
const newId = signedPreKey.id() + 1;
log.warn(`${logId}: Updating next signed pre key id to ${newId}`);
await storage.put(SIGNED_PRE_KEY_ID_KEY[UUIDKind.PNI], newId);
},
this.storeSignedPreKey( this.storeSignedPreKey(
pni, pni,
signedPreKey.id(), signedPreKey.id(),
@ -2255,6 +2525,26 @@ export class SignalProtocolStore extends EventEmitter {
true, true,
signedPreKey.timestamp() signedPreKey.timestamp()
), ),
async () => {
if (!lastResortKyberPreKey) {
return;
}
const newId = lastResortKyberPreKey.id() + 1;
log.warn(`${logId}: Updating next kyber pre key id to ${newId}`);
await storage.put(KYBER_KEY_ID_KEY[UUIDKind.PNI], newId);
},
lastResortKyberPreKeyBytes && lastResortKyberPreKey
? this.storeKyberPreKeys(pni, [
{
createdAt: lastResortKyberPreKey.timestamp(),
data: lastResortKyberPreKeyBytes,
isConfirmed: true,
isLastResort: true,
keyId: lastResortKyberPreKey.id(),
ourUuid: pni.toString(),
},
])
: undefined,
]); ]);
} }
@ -2354,12 +2644,23 @@ export class SignalProtocolStore extends EventEmitter {
return Array.from(union.values()); return Array.from(union.values());
} }
private emitLowKeys(ourUuid: UUID, source: string) {
const logId = `SignalProtocolStore.emitLowKeys/${source}:`;
try {
log.info(`${logId}: Emitting event`);
this.emit('lowKeys', ourUuid);
} catch (error) {
log.error(`${logId}: Error thrown from emit`, Errors.toLogFormat(error));
}
}
// //
// EventEmitter types // EventEmitter types
// //
public override on( public override on(
name: 'removePreKey', name: 'lowKeys',
handler: (ourUuid: UUID) => unknown handler: (ourUuid: UUID) => unknown
): this; ): this;
@ -2378,7 +2679,7 @@ export class SignalProtocolStore extends EventEmitter {
return super.on(eventName, listener); return super.on(eventName, listener);
} }
public override emit(name: 'removePreKey', ourUuid: UUID): boolean; public override emit(name: 'lowKeys', ourUuid: UUID): boolean;
public override emit( public override emit(
name: 'keychange', name: 'keychange',

View file

@ -107,7 +107,7 @@ import type {
} from './textsecure/messageReceiverEvents'; } from './textsecure/messageReceiverEvents';
import type { WebAPIType } from './textsecure/WebAPI'; import type { WebAPIType } from './textsecure/WebAPI';
import * as KeyChangeListener from './textsecure/KeyChangeListener'; import * as KeyChangeListener from './textsecure/KeyChangeListener';
import { RotateSignedPreKeyListener } from './textsecure/RotateSignedPreKeyListener'; import { UpdateKeysListener } from './textsecure/UpdateKeysListener';
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation'; import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff'; import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff';
import { AppViewType } from './state/ducks/app'; import { AppViewType } from './state/ducks/app';
@ -599,10 +599,17 @@ export async function startApp(): Promise<void> {
); );
KeyChangeListener.init(window.textsecure.storage.protocol); KeyChangeListener.init(window.textsecure.storage.protocol);
window.textsecure.storage.protocol.on('removePreKey', (ourUuid: UUID) => { window.textsecure.storage.protocol.on(
const uuidKind = window.textsecure.storage.user.getOurUuidKind(ourUuid); 'lowKeys',
void window.getAccountManager().refreshPreKeys(uuidKind); throttle(
}); (ourUuid: UUID) => {
const uuidKind = window.textsecure.storage.user.getOurUuidKind(ourUuid);
drop(window.getAccountManager().maybeUpdateKeys(uuidKind));
},
durations.MINUTE,
{ trailing: true, leading: false }
)
);
window.textsecure.storage.protocol.on('removeAllData', () => { window.textsecure.storage.protocol.on('removeAllData', () => {
window.reduxActions.stories.removeAllStories(); window.reduxActions.stories.removeAllStories();
@ -876,6 +883,15 @@ export async function startApp(): Promise<void> {
await window.storage.remove('remoteBuildExpiration'); await window.storage.remove('remoteBuildExpiration');
} }
if (window.isBeforeVersion(lastVersion, '6.25.0-alpha')) {
await removeStorageKeyJobQueue.add({
key: 'nextSignedKeyRotationTime',
});
await removeStorageKeyJobQueue.add({
key: 'signedKeyRotationRejected',
});
}
if (window.isBeforeVersion(lastVersion, '6.22.0-alpha')) { if (window.isBeforeVersion(lastVersion, '6.22.0-alpha')) {
const formattingWarningShown = window.storage.get( const formattingWarningShown = window.storage.get(
'formattingWarningShown', 'formattingWarningShown',
@ -2079,7 +2095,7 @@ export async function startApp(): Promise<void> {
window.ConversationController.onEmpty(); window.ConversationController.onEmpty();
// Start listeners here, after we get through our queue. // Start listeners here, after we get through our queue.
RotateSignedPreKeyListener.init(window.Whisper.events, newVersion); UpdateKeysListener.init(window.Whisper.events, newVersion);
profileKeyResponseQueue.start(); profileKeyResponseQueue.start();
lightSessionResetQueue.start(); lightSessionResetQueue.start();

View file

@ -7,7 +7,12 @@ import { JobQueue } from './JobQueue';
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
const removeStorageKeyJobDataSchema = z.object({ const removeStorageKeyJobDataSchema = z.object({
key: z.enum(['senderCertificateWithUuid', 'challenge:retry-message-ids']), key: z.enum([
'challenge:retry-message-ids',
'nextSignedKeyRotationTime',
'senderCertificateWithUuid',
'signedKeyRotationRejected',
]),
}); });
type RemoveStorageKeyJobData = z.infer<typeof removeStorageKeyJobDataSchema>; type RemoveStorageKeyJobData = z.infer<typeof removeStorageKeyJobDataSchema>;

View file

@ -1406,7 +1406,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
e.name === 'OutgoingMessageError' || e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'SendMessageNetworkError' ||
e.name === 'SendMessageChallengeError' || e.name === 'SendMessageChallengeError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError' e.name === 'OutgoingIdentityKeyError'
); );
} }
@ -1472,7 +1471,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
e.name === 'OutgoingMessageError' || e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' || e.name === 'SendMessageNetworkError' ||
e.name === 'SendMessageChallengeError' || e.name === 'SendMessageChallengeError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError') e.name === 'OutgoingIdentityKeyError')
); );
this.set({ errors: errors[1] }); this.set({ errors: errors[1] });
@ -1591,7 +1589,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// screen will show that we didn't send to these unregistered users. // screen will show that we didn't send to these unregistered users.
const errorsToSave: Array<CustomError> = []; const errorsToSave: Array<CustomError> = [];
let hadSignedPreKeyRotationError = false;
errors.forEach(error => { errors.forEach(error => {
const conversation = const conversation =
window.ConversationController.get(error.identifier) || window.ConversationController.get(error.identifier) ||
@ -1616,9 +1613,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
let shouldSaveError = true; let shouldSaveError = true;
switch (error.name) { switch (error.name) {
case 'SignedPreKeyRotationError':
hadSignedPreKeyRotationError = true;
break;
case 'OutgoingIdentityKeyError': { case 'OutgoingIdentityKeyError': {
if (conversation) { if (conversation) {
promises.push(conversation.getProfiles()); promises.push(conversation.getProfiles());
@ -1648,12 +1642,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
}); });
if (hadSignedPreKeyRotationError) {
promises.push(
window.getAccountManager().rotateSignedPreKey(UUIDKind.ACI)
);
}
attributesToUpdate.sendStateByConversationId = sendStateByConversationId; attributesToUpdate.sendStateByConversationId = sendStateByConversationId;
// Only update the expirationStartTimestamp if we don't already have one set // Only update the expirationStartTimestamp if we don't already have one set
if (!this.get('expirationStartTimestamp')) { if (!this.get('expirationStartTimestamp')) {

View file

@ -52,6 +52,8 @@ import type {
SignedPreKeyIdType, SignedPreKeyIdType,
SignedPreKeyType, SignedPreKeyType,
StoredSignedPreKeyType, StoredSignedPreKeyType,
KyberPreKeyType,
StoredKyberPreKeyType,
} from './Interface'; } from './Interface';
import { MINUTE } from '../util/durations'; import { MINUTE } from '../util/durations';
import { getMessageIdForLogging } from '../util/idForLogging'; import { getMessageIdForLogging } from '../util/idForLogging';
@ -73,6 +75,11 @@ const exclusiveInterface: ClientExclusiveInterface = {
bulkAddIdentityKeys, bulkAddIdentityKeys,
getAllIdentityKeys, getAllIdentityKeys,
createOrUpdateKyberPreKey,
getKyberPreKeyById,
bulkAddKyberPreKeys,
getAllKyberPreKeys,
createOrUpdatePreKey, createOrUpdatePreKey,
getPreKeyById, getPreKeyById,
bulkAddPreKeys, bulkAddPreKeys,
@ -248,6 +255,37 @@ async function getAllIdentityKeys(): Promise<Array<IdentityKeyType>> {
return keys.map(key => specToBytes(IDENTITY_KEY_SPEC, key)); return keys.map(key => specToBytes(IDENTITY_KEY_SPEC, key));
} }
// Kyber Pre Keys
const KYBER_PRE_KEY_SPEC = ['data'];
async function createOrUpdateKyberPreKey(data: KyberPreKeyType): Promise<void> {
const updated: StoredKyberPreKeyType = specFromBytes(
KYBER_PRE_KEY_SPEC,
data
);
await channels.createOrUpdateKyberPreKey(updated);
}
async function getKyberPreKeyById(
id: PreKeyIdType
): Promise<KyberPreKeyType | undefined> {
const data = await channels.getPreKeyById(id);
return specToBytes(KYBER_PRE_KEY_SPEC, data);
}
async function bulkAddKyberPreKeys(
array: Array<KyberPreKeyType>
): Promise<void> {
const updated: Array<StoredKyberPreKeyType> = map(array, data =>
specFromBytes(KYBER_PRE_KEY_SPEC, data)
);
await channels.bulkAddKyberPreKeys(updated);
}
async function getAllKyberPreKeys(): Promise<Array<KyberPreKeyType>> {
const keys = await channels.getAllPreKeys();
return keys.map(key => specToBytes(KYBER_PRE_KEY_SPEC, key));
}
// Pre Keys // Pre Keys
async function createOrUpdatePreKey(data: PreKeyType): Promise<void> { async function createOrUpdatePreKey(data: PreKeyType): Promise<void> {

View file

@ -109,21 +109,35 @@ export type MessageType = MessageAttributesType;
export type MessageTypeUnhydrated = { export type MessageTypeUnhydrated = {
json: string; json: string;
}; };
export type PreKeyIdType = `${UUIDStringType}:${number}`;
export type KyberPreKeyType = {
id: PreKeyIdType;
createdAt: number;
data: Uint8Array;
isConfirmed: boolean;
isLastResort: boolean;
keyId: number;
ourUuid: UUIDStringType;
};
export type StoredKyberPreKeyType = KyberPreKeyType & {
data: string;
};
export type PreKeyType = { export type PreKeyType = {
id: `${UUIDStringType}:${number}`; id: PreKeyIdType;
createdAt: number;
keyId: number; keyId: number;
ourUuid: UUIDStringType; ourUuid: UUIDStringType;
privateKey: Uint8Array; privateKey: Uint8Array;
publicKey: Uint8Array; publicKey: Uint8Array;
}; };
export type StoredPreKeyType = {
id: `${UUIDStringType}:${number}`; export type StoredPreKeyType = PreKeyType & {
keyId: number;
ourUuid: UUIDStringType;
privateKey: string; privateKey: string;
publicKey: string; publicKey: string;
}; };
export type PreKeyIdType = PreKeyType['id'];
export type ServerSearchResultMessageType = { export type ServerSearchResultMessageType = {
json: string; json: string;
@ -410,16 +424,24 @@ export type DataInterface = {
removeIdentityKeyById: (id: IdentityKeyIdType) => Promise<void>; removeIdentityKeyById: (id: IdentityKeyIdType) => Promise<void>;
removeAllIdentityKeys: () => Promise<void>; removeAllIdentityKeys: () => Promise<void>;
removePreKeyById: (id: PreKeyIdType) => Promise<void>; removeKyberPreKeyById: (
id: PreKeyIdType | Array<PreKeyIdType>
) => Promise<void>;
removeKyberPreKeysByUuid: (uuid: UUIDStringType) => Promise<void>;
removeAllKyberPreKeys: () => Promise<void>;
removePreKeyById: (id: PreKeyIdType | Array<PreKeyIdType>) => Promise<void>;
removePreKeysByUuid: (uuid: UUIDStringType) => Promise<void>; removePreKeysByUuid: (uuid: UUIDStringType) => Promise<void>;
removeAllPreKeys: () => Promise<void>; removeAllPreKeys: () => Promise<void>;
removeSignedPreKeyById: (id: SignedPreKeyIdType) => Promise<void>; removeSignedPreKeyById: (
id: SignedPreKeyIdType | Array<SignedPreKeyIdType>
) => Promise<void>;
removeSignedPreKeysByUuid: (uuid: UUIDStringType) => Promise<void>; removeSignedPreKeysByUuid: (uuid: UUIDStringType) => Promise<void>;
removeAllSignedPreKeys: () => Promise<void>; removeAllSignedPreKeys: () => Promise<void>;
removeAllItems: () => Promise<void>; removeAllItems: () => Promise<void>;
removeItemById: (id: ItemKeyType) => Promise<void>; removeItemById: (id: ItemKeyType | Array<ItemKeyType>) => Promise<void>;
createOrUpdateSenderKey: (key: SenderKeyType) => Promise<void>; createOrUpdateSenderKey: (key: SenderKeyType) => Promise<void>;
getSenderKeyById: (id: SenderKeyIdType) => Promise<SenderKeyType | undefined>; getSenderKeyById: (id: SenderKeyIdType) => Promise<SenderKeyType | undefined>;
@ -822,6 +844,13 @@ export type ServerInterface = DataInterface & {
bulkAddIdentityKeys: (array: Array<StoredIdentityKeyType>) => Promise<void>; bulkAddIdentityKeys: (array: Array<StoredIdentityKeyType>) => Promise<void>;
getAllIdentityKeys: () => Promise<Array<StoredIdentityKeyType>>; getAllIdentityKeys: () => Promise<Array<StoredIdentityKeyType>>;
createOrUpdateKyberPreKey: (data: StoredKyberPreKeyType) => Promise<void>;
getKyberPreKeyById: (
id: PreKeyIdType
) => Promise<StoredKyberPreKeyType | undefined>;
bulkAddKyberPreKeys: (array: Array<StoredKyberPreKeyType>) => Promise<void>;
getAllKyberPreKeys: () => Promise<Array<StoredKyberPreKeyType>>;
createOrUpdatePreKey: (data: StoredPreKeyType) => Promise<void>; createOrUpdatePreKey: (data: StoredPreKeyType) => Promise<void>;
getPreKeyById: (id: PreKeyIdType) => Promise<StoredPreKeyType | undefined>; getPreKeyById: (id: PreKeyIdType) => Promise<StoredPreKeyType | undefined>;
bulkAddPreKeys: (array: Array<StoredPreKeyType>) => Promise<void>; bulkAddPreKeys: (array: Array<StoredPreKeyType>) => Promise<void>;
@ -901,6 +930,13 @@ export type ClientExclusiveInterface = {
bulkAddIdentityKeys: (array: Array<IdentityKeyType>) => Promise<void>; bulkAddIdentityKeys: (array: Array<IdentityKeyType>) => Promise<void>;
getAllIdentityKeys: () => Promise<Array<IdentityKeyType>>; getAllIdentityKeys: () => Promise<Array<IdentityKeyType>>;
createOrUpdateKyberPreKey: (data: KyberPreKeyType) => Promise<void>;
getKyberPreKeyById: (
id: PreKeyIdType
) => Promise<KyberPreKeyType | undefined>;
bulkAddKyberPreKeys: (array: Array<KyberPreKeyType>) => Promise<void>;
getAllKyberPreKeys: () => Promise<Array<KyberPreKeyType>>;
createOrUpdatePreKey: (data: PreKeyType) => Promise<void>; createOrUpdatePreKey: (data: PreKeyType) => Promise<void>;
getPreKeyById: (id: PreKeyIdType) => Promise<PreKeyType | undefined>; getPreKeyById: (id: PreKeyIdType) => Promise<PreKeyType | undefined>;
bulkAddPreKeys: (array: Array<PreKeyType>) => Promise<void>; bulkAddPreKeys: (array: Array<PreKeyType>) => Promise<void>;

View file

@ -133,6 +133,7 @@ import type {
UnprocessedType, UnprocessedType,
UnprocessedUpdateType, UnprocessedUpdateType,
GetNearbyMessageFromDeletedSetOptionsType, GetNearbyMessageFromDeletedSetOptionsType,
StoredKyberPreKeyType,
} from './Interface'; } from './Interface';
import { SeenStatus } from '../MessageSeenStatus'; import { SeenStatus } from '../MessageSeenStatus';
import { import {
@ -173,6 +174,14 @@ const dataInterface: ServerInterface = {
removeAllIdentityKeys, removeAllIdentityKeys,
getAllIdentityKeys, getAllIdentityKeys,
createOrUpdateKyberPreKey,
getKyberPreKeyById,
bulkAddKyberPreKeys,
removeKyberPreKeyById,
removeKyberPreKeysByUuid,
removeAllKyberPreKeys,
getAllKyberPreKeys,
createOrUpdatePreKey, createOrUpdatePreKey,
getPreKeyById, getPreKeyById,
bulkAddPreKeys, bulkAddPreKeys,
@ -655,6 +664,40 @@ async function getAllIdentityKeys(): Promise<Array<StoredIdentityKeyType>> {
return getAllFromTable(getInstance(), IDENTITY_KEYS_TABLE); return getAllFromTable(getInstance(), IDENTITY_KEYS_TABLE);
} }
const KYBER_PRE_KEYS_TABLE = 'kyberPreKeys';
async function createOrUpdateKyberPreKey(
data: StoredKyberPreKeyType
): Promise<void> {
return createOrUpdate(getInstance(), KYBER_PRE_KEYS_TABLE, data);
}
async function getKyberPreKeyById(
id: PreKeyIdType
): Promise<StoredKyberPreKeyType | undefined> {
return getById(getInstance(), KYBER_PRE_KEYS_TABLE, id);
}
async function bulkAddKyberPreKeys(
array: Array<StoredKyberPreKeyType>
): Promise<void> {
return bulkAdd(getInstance(), KYBER_PRE_KEYS_TABLE, array);
}
async function removeKyberPreKeyById(
id: PreKeyIdType | Array<PreKeyIdType>
): Promise<void> {
return removeById(getInstance(), KYBER_PRE_KEYS_TABLE, id);
}
async function removeKyberPreKeysByUuid(uuid: UUIDStringType): Promise<void> {
const db = getInstance();
db.prepare<Query>('DELETE FROM kyberPreKeys WHERE ourUuid IS $uuid;').run({
uuid,
});
}
async function removeAllKyberPreKeys(): Promise<void> {
return removeAllFromTable(getInstance(), KYBER_PRE_KEYS_TABLE);
}
async function getAllKyberPreKeys(): Promise<Array<StoredKyberPreKeyType>> {
return getAllFromTable(getInstance(), KYBER_PRE_KEYS_TABLE);
}
const PRE_KEYS_TABLE = 'preKeys'; const PRE_KEYS_TABLE = 'preKeys';
async function createOrUpdatePreKey(data: StoredPreKeyType): Promise<void> { async function createOrUpdatePreKey(data: StoredPreKeyType): Promise<void> {
return createOrUpdate(getInstance(), PRE_KEYS_TABLE, data); return createOrUpdate(getInstance(), PRE_KEYS_TABLE, data);
@ -667,7 +710,9 @@ async function getPreKeyById(
async function bulkAddPreKeys(array: Array<StoredPreKeyType>): Promise<void> { async function bulkAddPreKeys(array: Array<StoredPreKeyType>): Promise<void> {
return bulkAdd(getInstance(), PRE_KEYS_TABLE, array); return bulkAdd(getInstance(), PRE_KEYS_TABLE, array);
} }
async function removePreKeyById(id: PreKeyIdType): Promise<void> { async function removePreKeyById(
id: PreKeyIdType | Array<PreKeyIdType>
): Promise<void> {
return removeById(getInstance(), PRE_KEYS_TABLE, id); return removeById(getInstance(), PRE_KEYS_TABLE, id);
} }
async function removePreKeysByUuid(uuid: UUIDStringType): Promise<void> { async function removePreKeysByUuid(uuid: UUIDStringType): Promise<void> {
@ -699,7 +744,9 @@ async function bulkAddSignedPreKeys(
): Promise<void> { ): Promise<void> {
return bulkAdd(getInstance(), SIGNED_PRE_KEYS_TABLE, array); return bulkAdd(getInstance(), SIGNED_PRE_KEYS_TABLE, array);
} }
async function removeSignedPreKeyById(id: SignedPreKeyIdType): Promise<void> { async function removeSignedPreKeyById(
id: SignedPreKeyIdType | Array<SignedPreKeyIdType>
): Promise<void> {
return removeById(getInstance(), SIGNED_PRE_KEYS_TABLE, id); return removeById(getInstance(), SIGNED_PRE_KEYS_TABLE, id);
} }
async function removeSignedPreKeysByUuid(uuid: UUIDStringType): Promise<void> { async function removeSignedPreKeysByUuid(uuid: UUIDStringType): Promise<void> {
@ -755,7 +802,9 @@ async function getAllItems(): Promise<StoredAllItemsType> {
return result as unknown as StoredAllItemsType; return result as unknown as StoredAllItemsType;
} }
async function removeItemById(id: ItemKeyType): Promise<void> { async function removeItemById(
id: ItemKeyType | Array<ItemKeyType>
): Promise<void> {
return removeById(getInstance(), ITEMS_TABLE, id); return removeById(getInstance(), ITEMS_TABLE, id);
} }
async function removeAllItems(): Promise<void> { async function removeAllItems(): Promise<void> {
@ -4989,6 +5038,7 @@ async function removeAll(): Promise<void> {
DELETE FROM identityKeys; DELETE FROM identityKeys;
DELETE FROM items; DELETE FROM items;
DELETE FROM jobs; DELETE FROM jobs;
DELETE FROM kyberPreKeys;
DELETE FROM messages_fts; DELETE FROM messages_fts;
DELETE FROM messages; DELETE FROM messages;
DELETE FROM preKeys; DELETE FROM preKeys;
@ -5024,6 +5074,7 @@ async function removeAllConfiguration(
` `
DELETE FROM identityKeys; DELETE FROM identityKeys;
DELETE FROM jobs; DELETE FROM jobs;
DELETE FROM kyberPreKeys;
DELETE FROM preKeys; DELETE FROM preKeys;
DELETE FROM senderKeys; DELETE FROM senderKeys;
DELETE FROM sendLogMessageIds; DELETE FROM sendLogMessageIds;

View file

@ -0,0 +1,42 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from '@signalapp/better-sqlite3';
import type { LoggerType } from '../../types/Logging';
export default function updateToSchemaVersion85(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 85) {
return;
}
db.transaction(() => {
db.exec(
`CREATE TABLE kyberPreKeys(
id STRING PRIMARY KEY NOT NULL,
json TEXT NOT NULL,
ourUuid STRING
GENERATED ALWAYS AS (json_extract(json, '$.ourUuid'))
);`
);
// To manage our ACI or PNI keys quickly
db.exec('CREATE INDEX kyberPreKeys_ourUuid ON kyberPreKeys (ourUuid);');
// Add time to all existing preKeys to allow us to expire them
const now = Date.now();
db.exec(
`UPDATE preKeys SET
json = json_set(json, '$.createdAt', ${now});
`
);
db.pragma('user_version = 85');
})();
logger.info('updateToSchemaVersion85: success!');
}

View file

@ -60,6 +60,7 @@ import updateToSchemaVersion81 from './81-contact-removed-notification';
import updateToSchemaVersion82 from './82-edited-messages-read-index'; import updateToSchemaVersion82 from './82-edited-messages-read-index';
import updateToSchemaVersion83 from './83-mentions'; import updateToSchemaVersion83 from './83-mentions';
import updateToSchemaVersion84 from './84-all-mentions'; import updateToSchemaVersion84 from './84-all-mentions';
import updateToSchemaVersion85 from './85-add-kyber-keys';
function updateToSchemaVersion1( function updateToSchemaVersion1(
currentVersion: number, currentVersion: number,
@ -1984,11 +1985,13 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion77, updateToSchemaVersion77,
updateToSchemaVersion78, updateToSchemaVersion78,
updateToSchemaVersion79, updateToSchemaVersion79,
updateToSchemaVersion80, updateToSchemaVersion80,
updateToSchemaVersion81, updateToSchemaVersion81,
updateToSchemaVersion82, updateToSchemaVersion82,
updateToSchemaVersion83, updateToSchemaVersion83,
updateToSchemaVersion84, updateToSchemaVersion84,
updateToSchemaVersion85,
]; ];
export function updateSchema(db: Database, logger: LoggerType): void { export function updateSchema(db: Database, logger: LoggerType): void {

View file

@ -16,6 +16,7 @@ export type TableType =
| 'conversations' | 'conversations'
| 'identityKeys' | 'identityKeys'
| 'items' | 'items'
| 'kyberPreKeys'
| 'messages' | 'messages'
| 'preKeys' | 'preKeys'
| 'senderKeys' | 'senderKeys'

View file

@ -930,7 +930,7 @@ describe('SignalProtocolStore', () => {
}); });
describe('storePreKey', () => { describe('storePreKey', () => {
it('stores prekeys', async () => { it('stores prekeys', async () => {
await store.storePreKey(ourUuid, 1, testKey); await store.storePreKeys(ourUuid, [{ keyId: 1, keyPair: testKey }]);
const key = await store.loadPreKey(ourUuid, 1); const key = await store.loadPreKey(ourUuid, 1);
if (!key) { if (!key) {
throw new Error('Missing key!'); throw new Error('Missing key!');
@ -947,10 +947,10 @@ describe('SignalProtocolStore', () => {
}); });
describe('removePreKey', () => { describe('removePreKey', () => {
before(async () => { before(async () => {
await store.storePreKey(ourUuid, 2, testKey); await store.storePreKeys(ourUuid, [{ keyId: 2, keyPair: testKey }]);
}); });
it('deletes prekeys', async () => { it('deletes prekeys', async () => {
await store.removePreKey(ourUuid, 2); await store.removePreKeys(ourUuid, [2]);
const key = await store.loadPreKey(ourUuid, 2); const key = await store.loadPreKey(ourUuid, 2);
assert.isUndefined(key); assert.isUndefined(key);
@ -978,7 +978,7 @@ describe('SignalProtocolStore', () => {
await store.storeSignedPreKey(ourUuid, 4, testKey); await store.storeSignedPreKey(ourUuid, 4, testKey);
}); });
it('deletes signed prekeys', async () => { it('deletes signed prekeys', async () => {
await store.removeSignedPreKey(ourUuid, 4); await store.removeSignedPreKeys(ourUuid, [4]);
const key = await store.loadSignedPreKey(ourUuid, 4); const key = await store.loadSignedPreKey(ourUuid, 4);
assert.isUndefined(key); assert.isUndefined(key);
@ -1557,7 +1557,7 @@ describe('SignalProtocolStore', () => {
}); });
describe('removeOurOldPni/updateOurPniKeyMaterial', () => { describe('removeOurOldPni/updateOurPniKeyMaterial', () => {
beforeEach(async () => { beforeEach(async () => {
await store.storePreKey(ourUuid, 2, testKey); await store.storePreKeys(ourUuid, [{ keyId: 2, keyPair: testKey }]);
await store.storeSignedPreKey(ourUuid, 3, testKey); await store.storeSignedPreKey(ourUuid, 3, testKey);
}); });

View file

@ -2,84 +2,98 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai'; import { assert } from 'chai';
import { range } from 'lodash';
import { getRandomBytes } from '../../Crypto'; import { getRandomBytes } from '../../Crypto';
import AccountManager from '../../textsecure/AccountManager'; import AccountManager from '../../textsecure/AccountManager';
import type { OuterSignedPrekeyType } from '../../textsecure/Types.d'; import type {
KyberPreKeyType,
OuterSignedPrekeyType,
PreKeyType,
} from '../../textsecure/Types.d';
import { UUID, UUIDKind } from '../../types/UUID'; import { UUID, UUIDKind } from '../../types/UUID';
import { DAY } from '../../util/durations';
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
describe('AccountManager', () => { describe('AccountManager', () => {
let accountManager: AccountManager; let accountManager: AccountManager;
const ourUuid = UUID.generate();
const identityKey = window.Signal.Curve.generateKeyPair();
const pubKey = getRandomBytes(33);
const privKey = getRandomBytes(32);
let originalGetIdentityKeyPair: any;
let originalGetUuid: any;
let originalGetCheckedUuid: any;
beforeEach(() => { beforeEach(() => {
const server: any = {}; const server: any = {};
accountManager = new AccountManager(server); accountManager = new AccountManager(server);
originalGetIdentityKeyPair =
window.textsecure.storage.protocol.getIdentityKeyPair;
originalGetUuid = window.textsecure.storage.user.getUuid;
originalGetCheckedUuid = window.textsecure.storage.user.getCheckedUuid;
window.textsecure.storage.protocol.getIdentityKeyPair = () => identityKey;
window.textsecure.storage.user.getUuid = () => ourUuid;
window.textsecure.storage.user.getCheckedUuid = () => ourUuid;
}); });
describe('#cleanSignedPreKeys', () => { afterEach(() => {
let originalGetIdentityKeyPair: any; window.textsecure.storage.protocol.getIdentityKeyPair =
originalGetIdentityKeyPair;
window.textsecure.storage.user.getUuid = originalGetUuid;
window.textsecure.storage.user.getCheckedUuid = originalGetCheckedUuid;
});
describe('encrypted device name', () => {
it('roundtrips', async () => {
const deviceName = 'v2.5.0 on Ubunto 20.04';
const encrypted = accountManager.encryptDeviceName(
deviceName,
identityKey
);
if (!encrypted) {
throw new Error('failed to encrypt!');
}
assert.strictEqual(typeof encrypted, 'string');
const decrypted = await accountManager.decryptDeviceName(encrypted);
assert.strictEqual(decrypted, deviceName);
});
it('handles falsey deviceName', () => {
const encrypted = accountManager.encryptDeviceName('', identityKey);
assert.strictEqual(encrypted, null);
});
});
describe('#_cleanSignedPreKeys', () => {
let originalLoadSignedPreKeys: any; let originalLoadSignedPreKeys: any;
let originalRemoveSignedPreKey: any; let originalRemoveSignedPreKey: any;
let originalGetUuid: any;
let signedPreKeys: Array<OuterSignedPrekeyType>; let signedPreKeys: Array<OuterSignedPrekeyType>;
const DAY = 1000 * 60 * 60 * 24;
const pubKey = getRandomBytes(33);
const privKey = getRandomBytes(32);
const identityKey = window.Signal.Curve.generateKeyPair();
beforeEach(async () => { beforeEach(async () => {
const ourUuid = UUID.generate();
originalGetUuid = window.textsecure.storage.user.getUuid;
originalGetIdentityKeyPair =
window.textsecure.storage.protocol.getIdentityKeyPair;
originalLoadSignedPreKeys = originalLoadSignedPreKeys =
window.textsecure.storage.protocol.loadSignedPreKeys; window.textsecure.storage.protocol.loadSignedPreKeys;
originalRemoveSignedPreKey = originalRemoveSignedPreKey =
window.textsecure.storage.protocol.removeSignedPreKey; window.textsecure.storage.protocol.removeSignedPreKeys;
window.textsecure.storage.user.getUuid = () => ourUuid; window.textsecure.storage.protocol.loadSignedPreKeys = () =>
window.textsecure.storage.protocol.getIdentityKeyPair = () => identityKey;
window.textsecure.storage.protocol.loadSignedPreKeys = async () =>
signedPreKeys; signedPreKeys;
// removeSignedPreKeys is updated per-test, below
}); });
afterEach(() => { afterEach(() => {
window.textsecure.storage.user.getUuid = originalGetUuid;
window.textsecure.storage.protocol.getIdentityKeyPair =
originalGetIdentityKeyPair;
window.textsecure.storage.protocol.loadSignedPreKeys = window.textsecure.storage.protocol.loadSignedPreKeys =
originalLoadSignedPreKeys; originalLoadSignedPreKeys;
window.textsecure.storage.protocol.removeSignedPreKey = window.textsecure.storage.protocol.removeSignedPreKeys =
originalRemoveSignedPreKey; originalRemoveSignedPreKey;
}); });
describe('encrypted device name', () => { it('keeps no keys if five or less, even if over a month old', () => {
it('roundtrips', async () => {
const deviceName = 'v2.5.0 on Ubunto 20.04';
const encrypted = accountManager.encryptDeviceName(
deviceName,
identityKey
);
if (!encrypted) {
throw new Error('failed to encrypt!');
}
assert.strictEqual(typeof encrypted, 'string');
const decrypted = await accountManager.decryptDeviceName(encrypted);
assert.strictEqual(decrypted, deviceName);
});
it('handles falsey deviceName', () => {
const encrypted = accountManager.encryptDeviceName('', identityKey);
assert.strictEqual(encrypted, null);
});
});
it('keeps three confirmed keys even if over a month old', () => {
const now = Date.now(); const now = Date.now();
signedPreKeys = [ signedPreKeys = [
{ {
@ -103,10 +117,24 @@ describe('AccountManager', () => {
pubKey, pubKey,
privKey, privKey,
}, },
{
keyId: 4,
created_at: now - DAY * 39,
confirmed: true,
pubKey,
privKey,
},
{
keyId: 5,
created_at: now - DAY * 40,
confirmed: false,
pubKey,
privKey,
},
]; ];
// should be no calls to store.removeSignedPreKey, would cause crash // should be no calls to store.removeSignedPreKey, would cause crash
return accountManager.cleanSignedPreKeys(UUIDKind.ACI); return accountManager._cleanSignedPreKeys(UUIDKind.ACI);
}); });
it('eliminates oldest keys, even if recent key is unconfirmed', async () => { it('eliminates oldest keys, even if recent key is unconfirmed', async () => {
@ -157,60 +185,430 @@ describe('AccountManager', () => {
}, },
]; ];
let count = 0; let removedKeys: Array<number> = [];
window.textsecure.storage.protocol.removeSignedPreKey = async ( window.textsecure.storage.protocol.removeSignedPreKeys = async (
_, _,
keyId keyIds
) => { ) => {
if (keyId !== 4) { removedKeys = removedKeys.concat(keyIds);
throw new Error(`Wrong keys were eliminated! ${keyId}`);
}
count += 1;
}; };
await accountManager.cleanSignedPreKeys(UUIDKind.ACI); await accountManager._cleanSignedPreKeys(UUIDKind.ACI);
assert.strictEqual(count, 1); assert.deepEqual(removedKeys, [4]);
});
});
describe('#_cleanLastResortKeys', () => {
let originalLoadKyberPreKeys: any;
let originalRemoveKyberPreKey: any;
let kyberPreKeys: Array<KyberPreKeyType>;
beforeEach(async () => {
originalLoadKyberPreKeys =
window.textsecure.storage.protocol.loadKyberPreKeys;
originalRemoveKyberPreKey =
window.textsecure.storage.protocol.removeKyberPreKeys;
window.textsecure.storage.protocol.loadKyberPreKeys = () => kyberPreKeys;
// removeKyberPreKeys is updated per-test, below
});
afterEach(() => {
window.textsecure.storage.protocol.loadKyberPreKeys =
originalLoadKyberPreKeys;
window.textsecure.storage.protocol.removeKyberPreKeys =
originalRemoveKyberPreKey;
}); });
it('Removes no keys if less than five', async () => { it('keeps five keys even if over a month old', () => {
const now = Date.now(); const now = Date.now();
signedPreKeys = [ kyberPreKeys = [
{ {
id: `${ourUuid.toString()}:1`,
createdAt: now - DAY * 32,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: true,
keyId: 1, keyId: 1,
created_at: now - DAY * 32, ourUuid: ourUuid.toString(),
confirmed: true,
pubKey,
privKey,
}, },
{ {
id: `${ourUuid.toString()}:2`,
createdAt: now - DAY * 34,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: true,
keyId: 2, keyId: 2,
created_at: now - DAY * 44, ourUuid: ourUuid.toString(),
confirmed: true,
pubKey,
privKey,
}, },
{ {
id: `${ourUuid.toString()}:3`,
createdAt: now - DAY * 38,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: true,
keyId: 3, keyId: 3,
created_at: now - DAY * 36, ourUuid: ourUuid.toString(),
confirmed: false,
pubKey,
privKey,
}, },
{ {
id: `${ourUuid.toString()}:4`,
createdAt: now - DAY * 39,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: false,
keyId: 4, keyId: 4,
created_at: now - DAY * 20, ourUuid: ourUuid.toString(),
confirmed: false, },
pubKey, {
privKey, id: `${ourUuid.toString()}:5`,
createdAt: now - DAY * 40,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: false,
keyId: 5,
ourUuid: ourUuid.toString(),
}, },
]; ];
window.textsecure.storage.protocol.removeSignedPreKey = async () => { // should be no calls to store.removeKyberPreKey, would cause crash
throw new Error('None should be removed!'); return accountManager._cleanLastResortKeys(UUIDKind.ACI);
});
it('eliminates oldest keys, even if recent key is unconfirmed', async () => {
const now = Date.now();
kyberPreKeys = [
{
id: `${ourUuid.toString()}:1`,
createdAt: now - DAY * 32,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: true,
keyId: 1,
ourUuid: ourUuid.toString(),
},
{
id: `${ourUuid.toString()}:2`,
createdAt: now - DAY * 31,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: false,
keyId: 2,
ourUuid: ourUuid.toString(),
},
{
id: `${ourUuid.toString()}:3`,
createdAt: now - DAY * 24,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: true,
keyId: 3,
ourUuid: ourUuid.toString(),
},
{
// Oldest, should be dropped
id: `${ourUuid.toString()}:4`,
createdAt: now - DAY * 38,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: true,
keyId: 4,
ourUuid: ourUuid.toString(),
},
{
id: `${ourUuid.toString()}:5`,
createdAt: now - DAY * 5,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: true,
keyId: 5,
ourUuid: ourUuid.toString(),
},
{
id: `${ourUuid.toString()}:6`,
createdAt: now - DAY * 5,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: true,
keyId: 6,
ourUuid: ourUuid.toString(),
},
];
let removedKeys: Array<number> = [];
window.textsecure.storage.protocol.removeKyberPreKeys = async (
_,
keyIds
) => {
removedKeys = removedKeys.concat(keyIds);
}; };
await accountManager.cleanSignedPreKeys(UUIDKind.ACI); await accountManager._cleanLastResortKeys(UUIDKind.ACI);
assert.deepEqual(removedKeys, [4]);
});
});
describe('#_cleanPreKeys', () => {
let originalLoadPreKeys: any;
let originalRemovePreKeys: any;
let preKeys: Array<PreKeyType>;
beforeEach(async () => {
originalLoadPreKeys = window.textsecure.storage.protocol.loadPreKeys;
originalRemovePreKeys = window.textsecure.storage.protocol.removePreKeys;
window.textsecure.storage.protocol.loadPreKeys = () => preKeys;
// removePreKeys is updated per-test, below
});
afterEach(() => {
window.textsecure.storage.protocol.loadPreKeys = originalLoadPreKeys;
window.textsecure.storage.protocol.removePreKeys = originalRemovePreKeys;
});
it('keeps five keys even if over 90 days old, but all latest batch', () => {
const now = Date.now();
preKeys = [
{
id: `${ourUuid.toString()}:1`,
createdAt: now - DAY * 92,
keyId: 1,
ourUuid: ourUuid.toString(),
privateKey: privKey,
publicKey: pubKey,
},
{
id: `${ourUuid.toString()}:2`,
createdAt: now - DAY * 93,
keyId: 2,
ourUuid: ourUuid.toString(),
privateKey: privKey,
publicKey: pubKey,
},
{
id: `${ourUuid.toString()}:3`,
createdAt: now - DAY * 93,
keyId: 3,
ourUuid: ourUuid.toString(),
privateKey: privKey,
publicKey: pubKey,
},
{
id: `${ourUuid.toString()}:4`,
createdAt: now - DAY * 93,
keyId: 4,
ourUuid: ourUuid.toString(),
privateKey: privKey,
publicKey: pubKey,
},
{
id: `${ourUuid.toString()}:5`,
createdAt: now - DAY * 94,
keyId: 5,
ourUuid: ourUuid.toString(),
privateKey: privKey,
publicKey: pubKey,
},
];
// should be no calls to store.removeKyberPreKey, would cause crash
return accountManager._cleanPreKeys(UUIDKind.ACI);
});
it('eliminates keys not in the 200 newest, over 90 days old', async () => {
const now = Date.now();
preKeys = [
// The latest batch
...range(0, 100).map(
(id): PreKeyType => ({
id: `${ourUuid.toString()}:${id}`,
createdAt: now - DAY,
keyId: 1,
ourUuid: ourUuid.toString(),
privateKey: privKey,
publicKey: pubKey,
})
),
// Second-oldest batch, won't be dropped
...range(100, 200).map(
(id): PreKeyType => ({
id: `${ourUuid.toString()}:${id}`,
createdAt: now - DAY * 40,
keyId: 1,
ourUuid: ourUuid.toString(),
privateKey: privKey,
publicKey: pubKey,
})
),
// Oldest batch, will be dropped
{
id: `${ourUuid.toString()}:6`,
createdAt: now - DAY * 92,
keyId: 6,
ourUuid: ourUuid.toString(),
privateKey: privKey,
publicKey: pubKey,
},
];
let removedKeys: Array<number> = [];
window.textsecure.storage.protocol.removePreKeys = async (_, keyIds) => {
removedKeys = removedKeys.concat(keyIds);
};
await accountManager._cleanPreKeys(UUIDKind.ACI);
assert.deepEqual(removedKeys, [6]);
});
});
describe('#_cleanKyberPreKeys', () => {
let originalLoadKyberPreKeys: any;
let originalRemoveKyberPreKeys: any;
let kyberPreKeys: Array<KyberPreKeyType>;
beforeEach(async () => {
originalLoadKyberPreKeys =
window.textsecure.storage.protocol.loadKyberPreKeys;
originalRemoveKyberPreKeys =
window.textsecure.storage.protocol.removeKyberPreKeys;
window.textsecure.storage.protocol.loadKyberPreKeys = () => kyberPreKeys;
// removeKyberPreKeys is updated per-test, below
});
afterEach(() => {
window.textsecure.storage.protocol.loadKyberPreKeys =
originalLoadKyberPreKeys;
window.textsecure.storage.protocol.removeKyberPreKeys =
originalRemoveKyberPreKeys;
});
it('keeps five keys even if over 90 days old', () => {
const now = Date.now();
kyberPreKeys = [
{
id: `${ourUuid.toString()}:1`,
createdAt: now - DAY * 93,
data: getRandomBytes(32),
isConfirmed: false,
isLastResort: false,
keyId: 1,
ourUuid: ourUuid.toString(),
},
{
id: `${ourUuid.toString()}:2`,
createdAt: now - DAY * 93,
data: getRandomBytes(32),
isConfirmed: false,
isLastResort: false,
keyId: 2,
ourUuid: ourUuid.toString(),
},
{
id: `${ourUuid.toString()}:3`,
createdAt: now - DAY * 93,
data: getRandomBytes(32),
isConfirmed: false,
isLastResort: false,
keyId: 3,
ourUuid: ourUuid.toString(),
},
{
id: `${ourUuid.toString()}:4`,
createdAt: now - DAY * 93,
data: getRandomBytes(32),
isConfirmed: false,
isLastResort: false,
keyId: 4,
ourUuid: ourUuid.toString(),
},
{
id: `${ourUuid.toString()}:5`,
createdAt: now - DAY * 93,
data: getRandomBytes(32),
isConfirmed: false,
isLastResort: false,
keyId: 5,
ourUuid: ourUuid.toString(),
},
];
// should be no calls to store.removeKyberPreKey, would cause crash
return accountManager._cleanKyberPreKeys(UUIDKind.ACI);
});
it('eliminates keys not in the newest 200, over 90 days old', async () => {
const now = Date.now();
kyberPreKeys = [
// The latest batch
...range(0, 100).map(
(id): KyberPreKeyType => ({
id: `${ourUuid.toString()}:${id}`,
createdAt: now - DAY,
data: getRandomBytes(32),
isConfirmed: false,
isLastResort: false,
keyId: 1,
ourUuid: ourUuid.toString(),
})
),
// Second-oldest batch, won't be dropped
...range(100, 200).map(
(id): KyberPreKeyType => ({
id: `${ourUuid.toString()}:${id}`,
createdAt: now - DAY * 45,
data: getRandomBytes(32),
isConfirmed: false,
isLastResort: false,
keyId: 4,
ourUuid: ourUuid.toString(),
})
),
// Oldest batch, will be dropped
{
id: `${ourUuid.toString()}:6`,
createdAt: now - DAY * 93,
data: getRandomBytes(32),
isConfirmed: false,
isLastResort: false,
keyId: 6,
ourUuid: ourUuid.toString(),
},
];
let removedKeys: Array<number> = [];
window.textsecure.storage.protocol.removeKyberPreKeys = async (
_,
keyIds
) => {
removedKeys = removedKeys.concat(keyIds);
};
await accountManager._cleanKyberPreKeys(UUIDKind.ACI);
assert.deepEqual(removedKeys, [6]);
}); });
}); });
}); });

View file

@ -5,7 +5,7 @@ import { assert } from 'chai';
import { constantTimeEqual } from '../../Crypto'; import { constantTimeEqual } from '../../Crypto';
import { generateKeyPair } from '../../Curve'; import { generateKeyPair } from '../../Curve';
import type { GeneratedKeysType } from '../../textsecure/AccountManager'; import type { UploadKeysType } from '../../textsecure/WebAPI';
import AccountManager from '../../textsecure/AccountManager'; import AccountManager from '../../textsecure/AccountManager';
import type { PreKeyType, SignedPreKeyType } from '../../textsecure/Types.d'; import type { PreKeyType, SignedPreKeyType } from '../../textsecure/Types.d';
import { UUID, UUIDKind } from '../../types/UUID'; import { UUID, UUIDKind } from '../../types/UUID';
@ -19,6 +19,7 @@ const assertEqualBuffers = (a: Uint8Array, b: Uint8Array) => {
describe('Key generation', function thisNeeded() { describe('Key generation', function thisNeeded() {
const count = 10; const count = 10;
const ourUuid = new UUID('aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee'); const ourUuid = new UUID('aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee');
let result: UploadKeysType;
this.timeout(count * 2000); this.timeout(count * 2000);
function itStoresPreKey(keyId: number): void { function itStoresPreKey(keyId: number): void {
@ -30,6 +31,15 @@ describe('Key generation', function thisNeeded() {
assert(keyPair, `PreKey ${keyId} not found`); assert(keyPair, `PreKey ${keyId} not found`);
}); });
} }
function itStoresKyberPreKey(keyId: number): void {
it(`kyber pre key ${keyId} is valid`, async () => {
const key = await textsecure.storage.protocol.loadKyberPreKey(
ourUuid,
keyId
);
assert(key, `kyber pre key ${keyId} not found`);
});
}
function itStoresSignedPreKey(keyId: number): void { function itStoresSignedPreKey(keyId: number): void {
it(`signed prekey ${keyId} is valid`, async () => { it(`signed prekey ${keyId} is valid`, async () => {
const keyPair = await textsecure.storage.protocol.loadSignedPreKey( const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
@ -39,7 +49,8 @@ describe('Key generation', function thisNeeded() {
assert(keyPair, `SignedPreKey ${keyId} not found`); assert(keyPair, `SignedPreKey ${keyId} not found`);
}); });
} }
async function validateResultKey(
async function validateResultPreKey(
resultKey: Pick<PreKeyType, 'keyId' | 'publicKey'> resultKey: Pick<PreKeyType, 'keyId' | 'publicKey'>
): Promise<void> { ): Promise<void> {
const keyPair = await textsecure.storage.protocol.loadPreKey( const keyPair = await textsecure.storage.protocol.loadPreKey(
@ -52,8 +63,11 @@ describe('Key generation', function thisNeeded() {
assertEqualBuffers(resultKey.publicKey, keyPair.publicKey().serialize()); assertEqualBuffers(resultKey.publicKey, keyPair.publicKey().serialize());
} }
async function validateResultSignedKey( async function validateResultSignedKey(
resultSignedKey: Pick<SignedPreKeyType, 'keyId' | 'publicKey'> resultSignedKey?: Pick<SignedPreKeyType, 'keyId' | 'publicKey'>
) { ) {
if (!resultSignedKey) {
throw new Error('validateResultSignedKey: No signed prekey provided!');
}
const keyPair = await textsecure.storage.protocol.loadSignedPreKey( const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
ourUuid, ourUuid,
resultSignedKey.keyId resultSignedKey.keyId
@ -68,120 +82,166 @@ describe('Key generation', function thisNeeded() {
} }
before(async () => { before(async () => {
await textsecure.storage.protocol.clearPreKeyStore();
await textsecure.storage.protocol.clearKyberPreKeyStore();
await textsecure.storage.protocol.clearSignedPreKeysStore();
const keyPair = generateKeyPair(); const keyPair = generateKeyPair();
await textsecure.storage.put('identityKeyMap', { await textsecure.storage.put('identityKeyMap', {
[ourUuid.toString()]: keyPair, [ourUuid.toString()]: keyPair,
}); });
await textsecure.storage.user.setUuidAndDeviceId(ourUuid.toString(), 1); await textsecure.storage.user.setUuidAndDeviceId(ourUuid.toString(), 1);
await textsecure.storage.protocol.hydrateCaches(); await textsecure.storage.protocol.hydrateCaches();
}); });
after(async () => { after(async () => {
await textsecure.storage.protocol.clearPreKeyStore(); await textsecure.storage.protocol.clearPreKeyStore();
await textsecure.storage.protocol.clearKyberPreKeyStore();
await textsecure.storage.protocol.clearSignedPreKeysStore(); await textsecure.storage.protocol.clearSignedPreKeysStore();
}); });
describe('the first time', () => { describe('the first time', () => {
let result: GeneratedKeysType;
before(async () => { before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountManager = new AccountManager({} as any); const accountManager = new AccountManager({} as any);
result = await accountManager.generateKeys(count, UUIDKind.ACI); result = await accountManager._generateKeys(count, UUIDKind.ACI);
}); });
for (let i = 1; i <= count; i += 1) { describe('generates the basics', () => {
itStoresPreKey(i); for (let i = 1; i <= count; i += 1) {
} itStoresPreKey(i);
itStoresSignedPreKey(1); }
for (let i = 1; i <= count + 1; i += 1) {
itStoresKyberPreKey(i);
}
itStoresSignedPreKey(1);
});
it(`result contains ${count} preKeys`, () => { it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys); const preKeys = result.preKeys || [];
assert.lengthOf(result.preKeys, count); assert.isArray(preKeys);
assert.lengthOf(preKeys, count);
for (let i = 0; i < count; i += 1) { for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]); assert.isObject(preKeys[i]);
} }
}); });
it('result contains the correct keyIds', () => { it('result contains the correct keyIds', () => {
const preKeys = result.preKeys || [];
for (let i = 0; i < count; i += 1) { for (let i = 0; i < count; i += 1) {
assert.strictEqual(result.preKeys[i].keyId, i + 1); assert.strictEqual(preKeys[i].keyId, i + 1);
} }
}); });
it('result contains the correct public keys', async () => { it('result contains the correct public keys', async () => {
await Promise.all(result.preKeys.map(validateResultKey)); const preKeys = result.preKeys || [];
await Promise.all(preKeys.map(validateResultPreKey));
}); });
it('returns a signed prekey', () => { it('returns a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 1); assert.strictEqual(result.signedPreKey?.keyId, 1);
assert.instanceOf(result.signedPreKey.signature, Uint8Array); assert.instanceOf(result.signedPreKey?.signature, Uint8Array);
return validateResultSignedKey(result.signedPreKey); return validateResultSignedKey(result.signedPreKey);
}); });
}); });
describe('the second time', () => { describe('the second time', () => {
let result: GeneratedKeysType;
before(async () => { before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountManager = new AccountManager({} as any); const accountManager = new AccountManager({} as any);
result = await accountManager.generateKeys(count, UUIDKind.ACI); result = await accountManager._generateKeys(count, UUIDKind.ACI);
});
describe('generates the basics', () => {
for (let i = 1; i <= 2 * count; i += 1) {
itStoresPreKey(i);
}
for (let i = 1; i <= 2 * count + 2; i += 1) {
itStoresKyberPreKey(i);
}
itStoresSignedPreKey(1);
itStoresSignedPreKey(2);
}); });
for (let i = 1; i <= 2 * count; i += 1) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
itStoresSignedPreKey(2);
it(`result contains ${count} preKeys`, () => { it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys); const preKeys = result.preKeys || [];
assert.lengthOf(result.preKeys, count); assert.isArray(preKeys);
assert.lengthOf(preKeys, count);
for (let i = 0; i < count; i += 1) { for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]); assert.isObject(preKeys[i]);
} }
}); });
it('result contains the correct keyIds', () => { it('result contains the correct keyIds', () => {
const preKeys = result.preKeys || [];
for (let i = 1; i <= count; i += 1) { for (let i = 1; i <= count; i += 1) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + count); assert.strictEqual(preKeys[i - 1].keyId, i + count);
} }
}); });
it('result contains the correct public keys', async () => { it('result contains the correct public keys', async () => {
await Promise.all(result.preKeys.map(validateResultKey)); const preKeys = result.preKeys || [];
await Promise.all(preKeys.map(validateResultPreKey));
}); });
it('returns a signed prekey', () => { it('returns a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 2); assert.strictEqual(result.signedPreKey?.keyId, 2);
assert.instanceOf(result.signedPreKey.signature, Uint8Array); assert.instanceOf(result.signedPreKey?.signature, Uint8Array);
return validateResultSignedKey(result.signedPreKey); return validateResultSignedKey(result.signedPreKey);
}); });
}); });
describe('the third time', () => { describe('the third time, after keys are confirmed', () => {
let result: GeneratedKeysType;
before(async () => { before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountManager = new AccountManager({} as any); const accountManager = new AccountManager({} as any);
result = await accountManager.generateKeys(count, UUIDKind.ACI);
await accountManager._confirmKeys(result, UUIDKind.ACI);
result = await accountManager._generateKeys(count, UUIDKind.ACI);
});
describe('generates the basics', () => {
for (let i = 1; i <= 3 * count; i += 1) {
itStoresPreKey(i);
}
// Note: no new last resort kyber key generated
for (let i = 1; i <= 3 * count + 2; i += 1) {
itStoresKyberPreKey(i);
}
itStoresSignedPreKey(1);
itStoresSignedPreKey(2);
}); });
for (let i = 1; i <= 3 * count; i += 1) {
itStoresPreKey(i);
}
itStoresSignedPreKey(2);
itStoresSignedPreKey(3);
it(`result contains ${count} preKeys`, () => { it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys); const preKeys = result.preKeys || [];
assert.lengthOf(result.preKeys, count); assert.isArray(preKeys);
assert.lengthOf(preKeys, count);
for (let i = 0; i < count; i += 1) { for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]); assert.isObject(preKeys[i]);
} }
}); });
it('result contains the correct keyIds', () => { it('result contains the correct keyIds', () => {
const preKeys = result.preKeys || [];
for (let i = 1; i <= count; i += 1) { for (let i = 1; i <= count; i += 1) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + 2 * count); assert.strictEqual(preKeys[i - 1].keyId, i + 2 * count);
} }
}); });
it('result contains the correct public keys', async () => { it('result contains the correct public keys', async () => {
await Promise.all(result.preKeys.map(validateResultKey)); const preKeys = result.preKeys || [];
await Promise.all(preKeys.map(validateResultPreKey));
}); });
it('result contains a signed prekey', () => { it('does not generate a third last resort prekey', async () => {
assert.strictEqual(result.signedPreKey.keyId, 3); const keyId = 3 * count + 3;
assert.instanceOf(result.signedPreKey.signature, Uint8Array); const key = await textsecure.storage.protocol.loadKyberPreKey(
return validateResultSignedKey(result.signedPreKey); ourUuid,
keyId
);
assert.isUndefined(key, `kyber pre key ${keyId} was unexpectedly found`);
});
it('does not generate a third signed prekey', async () => {
const keyId = 3;
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
ourUuid,
keyId
);
assert.isUndefined(
keyPair,
`SignedPreKey ${keyId} was unexpectedly found`
);
}); });
}); });
}); });

View file

@ -7,6 +7,7 @@ import path from 'path';
import os from 'os'; import os from 'os';
import createDebug from 'debug'; import createDebug from 'debug';
import pTimeout from 'p-timeout'; import pTimeout from 'p-timeout';
import normalizePath from 'normalize-path';
import type { Device, PrimaryDevice } from '@signalapp/mock-server'; import type { Device, PrimaryDevice } from '@signalapp/mock-server';
import { Server, UUIDKind, loadCertificates } from '@signalapp/mock-server'; import { Server, UUIDKind, loadCertificates } from '@signalapp/mock-server';
@ -289,7 +290,20 @@ export class Bootstrap {
return result; return result;
} }
public async saveLogs(app: App | undefined = this.lastApp): Promise<void> { public async maybeSaveLogs(
test?: Mocha.Test,
app: App | undefined = this.lastApp
): Promise<void> {
const { FORCE_ARTIFACT_SAVE } = process.env;
if (test?.state !== 'passed' || FORCE_ARTIFACT_SAVE) {
await this.saveLogs(app, test?.fullTitle());
}
}
public async saveLogs(
app: App | undefined = this.lastApp,
pathPrefix?: string
): Promise<void> {
const { ARTIFACTS_DIR } = process.env; const { ARTIFACTS_DIR } = process.env;
if (!ARTIFACTS_DIR) { if (!ARTIFACTS_DIR) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -299,7 +313,12 @@ export class Bootstrap {
await fs.mkdir(ARTIFACTS_DIR, { recursive: true }); await fs.mkdir(ARTIFACTS_DIR, { recursive: true });
const outDir = await fs.mkdtemp(path.join(ARTIFACTS_DIR, 'logs-')); const normalizedPrefix = pathPrefix
? `-${normalizePath(pathPrefix.replace(/[ /]/g, '-'))}-`
: '';
const outDir = await fs.mkdtemp(
path.join(ARTIFACTS_DIR, `logs-${normalizedPrefix}`)
);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`Saving logs to ${outDir}`); console.error(`Saving logs to ${outDir}`);

View file

@ -67,10 +67,7 @@ describe('editing', function needsName() {
return; return;
} }
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -54,10 +54,7 @@ describe('senderKey', function needsName() {
return; return;
} }
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -118,10 +118,7 @@ describe('story/messaging', function unknownContacts() {
return; return;
} }
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -40,10 +40,7 @@ describe('unknown contacts', function unknownContacts() {
return; return;
} }
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -56,10 +56,7 @@ describe('pnp/accept gv2 invite', function needsName() {
}); });
afterEach(async function after() { afterEach(async function after() {
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -23,10 +23,7 @@ describe('pnp/change number', function needsName() {
}); });
afterEach(async function after() { afterEach(async function after() {
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -92,10 +92,7 @@ describe('pnp/merge', function needsName() {
}); });
afterEach(async function after() { afterEach(async function after() {
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -62,10 +62,7 @@ describe('pnp/PNI Change', function needsName() {
}); });
afterEach(async function after() { afterEach(async function after() {
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -86,10 +86,7 @@ describe('pnp/PNI Signature', function needsName() {
}); });
afterEach(async function after() { afterEach(async function after() {
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -85,10 +85,7 @@ describe('pnp/send gv2 invite', function needsName() {
}); });
afterEach(async function after() { afterEach(async function after() {
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -72,10 +72,7 @@ describe('pnp/username', function needsName() {
}); });
afterEach(async function after() { afterEach(async function after() {
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -57,10 +57,7 @@ describe('story/no-sender-key', function needsName() {
}); });
afterEach(async function after() { afterEach(async function after() {
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -64,10 +64,7 @@ describe('challenge/receipts', function challengeReceiptsTest() {
}); });
afterEach(async function after() { afterEach(async function after() {
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -22,10 +22,7 @@ describe('storage service', function needsName() {
return; return;
} }
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -25,10 +25,7 @@ describe('storage service', function needsName() {
return; return;
} }
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -27,10 +27,7 @@ describe('storage service', function needsName() {
return; return;
} }
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -22,10 +22,7 @@ describe('storage service', function needsName() {
return; return;
} }
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -26,10 +26,7 @@ describe('storage service', function needsName() {
return; return;
} }
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -99,10 +99,7 @@ describe('storage service', function needsName() {
return; return;
} }
if (this.currentTest?.state !== 'passed') { await bootstrap.maybeSaveLogs(this.currentTest, app);
await bootstrap.saveLogs(app);
}
await app.close(); await app.close();
await bootstrap.teardown(); await bootstrap.teardown();
}); });

View file

@ -3476,4 +3476,56 @@ describe('SQL migrations test', () => {
); );
}); });
}); });
describe('updateToSchemaVersion85', () => {
it('generates ourUuid field when JSON is inserted', () => {
updateToVersion(85);
const id = 'a1111:a2222';
const ourUuid = 'ab3333';
const value = {
ourUuid,
};
const json = JSON.stringify(value);
db.prepare(
`
INSERT INTO kyberPreKeys (id, json) VALUES
('${id}', '${json}');
`
).run();
const payload = db.prepare('SELECT * FROM kyberPreKeys LIMIT 1;').get();
assert.strictEqual(payload.id, id);
assert.strictEqual(payload.json, json);
assert.strictEqual(payload.ourUuid, ourUuid);
});
it('adds a createdAt to all existing prekeys', () => {
updateToVersion(84);
const id = 'a1111:a2222';
const ourUuid = 'ab3333';
const value = {
ourUuid,
};
const startingTime = Date.now();
const json = JSON.stringify(value);
db.prepare(
`
INSERT INTO preKeys (id, json) VALUES
('${id}', '${json}');
`
).run();
updateToVersion(85);
const payload = db.prepare('SELECT * FROM preKeys LIMIT 1;').get();
assert.strictEqual(payload.id, id);
const object = JSON.parse(payload.json);
assert.strictEqual(object.ourUuid, ourUuid);
assert.isAtLeast(object.createdAt, startingTime);
});
});
}); });

File diff suppressed because it is too large Load diff

View file

@ -251,15 +251,6 @@ export class SendMessageProtoError extends Error implements CallbackResultType {
} }
} }
export class SignedPreKeyRotationError extends ReplayableError {
constructor() {
super({
name: 'SignedPreKeyRotationError',
message: 'Too many signed prekey rotation failures',
});
}
}
export class MessageError extends ReplayableError { export class MessageError extends ReplayableError {
readonly httpError: HTTPError; readonly httpError: HTTPError;

View file

@ -32,6 +32,7 @@ import {
import { import {
IdentityKeys, IdentityKeys,
KyberPreKeys,
PreKeys, PreKeys,
SenderKeys, SenderKeys,
Sessions, Sessions,
@ -1758,6 +1759,7 @@ export default class MessageReceiver
const preKeyStore = new PreKeys({ ourUuid: destinationUuid }); const preKeyStore = new PreKeys({ ourUuid: destinationUuid });
const signedPreKeyStore = new SignedPreKeys({ ourUuid: destinationUuid }); const signedPreKeyStore = new SignedPreKeys({ ourUuid: destinationUuid });
const kyberPreKeyStore = new KyberPreKeys({ ourUuid: destinationUuid });
const sealedSenderIdentifier = envelope.sourceUuid; const sealedSenderIdentifier = envelope.sourceUuid;
strictAssert( strictAssert(
@ -1786,7 +1788,8 @@ export default class MessageReceiver
sessionStore, sessionStore,
identityKeyStore, identityKeyStore,
preKeyStore, preKeyStore,
signedPreKeyStore signedPreKeyStore,
kyberPreKeyStore
), ),
zone zone
); );
@ -1811,6 +1814,7 @@ export default class MessageReceiver
const { destinationUuid } = envelope; const { destinationUuid } = envelope;
const preKeyStore = new PreKeys({ ourUuid: destinationUuid }); const preKeyStore = new PreKeys({ ourUuid: destinationUuid });
const signedPreKeyStore = new SignedPreKeys({ ourUuid: destinationUuid }); const signedPreKeyStore = new SignedPreKeys({ ourUuid: destinationUuid });
const kyberPreKeyStore = new KyberPreKeys({ ourUuid: destinationUuid });
strictAssert(identifier !== undefined, 'Empty identifier'); strictAssert(identifier !== undefined, 'Empty identifier');
strictAssert(sourceDevice !== undefined, 'Empty source device'); strictAssert(sourceDevice !== undefined, 'Empty source device');
@ -1903,7 +1907,8 @@ export default class MessageReceiver
sessionStore, sessionStore,
identityKeyStore, identityKeyStore,
preKeyStore, preKeyStore,
signedPreKeyStore signedPreKeyStore,
kyberPreKeyStore
) )
), ),
zone zone
@ -2105,17 +2110,18 @@ export default class MessageReceiver
msg: Proto.IStoryMessage, msg: Proto.IStoryMessage,
sentMessage?: ProcessedSent sentMessage?: ProcessedSent
): Promise<void> { ): Promise<void> {
const logId = getEnvelopeId(envelope); const envelopeId = getEnvelopeId(envelope);
const logId = `MessageReceiver.handleStoryMessage(${envelopeId})`;
logUnexpectedUrgentValue(envelope, 'story'); logUnexpectedUrgentValue(envelope, 'story');
if (getStoriesBlocked()) { if (getStoriesBlocked()) {
log.info('MessageReceiver.handleStoryMessage: dropping', logId); log.info(`${logId}: dropping`);
this.removeFromCache(envelope); this.removeFromCache(envelope);
return; return;
} }
log.info('MessageReceiver.handleStoryMessage', logId); log.info(`${logId} starting`);
const attachments: Array<ProcessedAttachment> = []; const attachments: Array<ProcessedAttachment> = [];
let preview: ReadonlyArray<ProcessedPreview> | undefined; let preview: ReadonlyArray<ProcessedPreview> | undefined;
@ -2150,11 +2156,7 @@ export default class MessageReceiver
const groupV2 = msg.group ? processGroupV2Context(msg.group) : undefined; const groupV2 = msg.group ? processGroupV2Context(msg.group) : undefined;
if (groupV2 && this.isGroupBlocked(groupV2.id)) { if (groupV2 && this.isGroupBlocked(groupV2.id)) {
log.warn( log.warn(`${logId}: ignored; destined for blocked group`);
`MessageReceiver.handleStoryMessage: envelope ${getEnvelopeId(
envelope
)} ignored; destined for blocked group`
);
this.removeFromCache(envelope); this.removeFromCache(envelope);
return; return;
} }
@ -2165,10 +2167,7 @@ export default class MessageReceiver
); );
if (timeRemaining <= 0) { if (timeRemaining <= 0) {
log.info( log.info(`${logId}: story already expired`);
'MessageReceiver.handleStoryMessage: story already expired',
logId
);
this.removeFromCache(envelope); this.removeFromCache(envelope);
return; return;
} }
@ -2188,6 +2187,7 @@ export default class MessageReceiver
}; };
if (sentMessage && message.groupV2) { if (sentMessage && message.groupV2) {
log.warn(`${logId}: envelope is a sent group story`);
const ev = new SentEvent( const ev = new SentEvent(
{ {
destinationUuid: { destinationUuid: {
@ -2220,6 +2220,7 @@ export default class MessageReceiver
} }
if (sentMessage) { if (sentMessage) {
log.warn(`${logId}: envelope is a sent distribution list story`);
const { storyMessageRecipients } = sentMessage; const { storyMessageRecipients } = sentMessage;
const recipients = storyMessageRecipients ?? []; const recipients = storyMessageRecipients ?? [];
@ -2248,8 +2249,7 @@ export default class MessageReceiver
} else { } else {
assertDev( assertDev(
false, false,
`MessageReceiver.handleStoryMessage(${logId}): missing ` + `${logId}: missing distribution list id for: ${destinationUuid}`
`distribution list id for: ${destinationUuid}`
); );
} }
@ -2296,6 +2296,7 @@ export default class MessageReceiver
return; return;
} }
log.warn(`${logId}: envelope is a received story`);
const ev = new MessageEvent( const ev = new MessageEvent(
{ {
source: envelope.source, source: envelope.source,
@ -3241,6 +3242,7 @@ export default class MessageReceiver
{ {
identityKeyPair, identityKeyPair,
signedPreKey, signedPreKey,
lastResortKyberPreKey,
registrationId, registrationId,
newE164, newE164,
}: Proto.SyncMessage.IPniChangeNumber }: Proto.SyncMessage.IPniChangeNumber
@ -3255,6 +3257,7 @@ export default class MessageReceiver
return; return;
} }
// TDOO: DESKTOP-5652
if ( if (
!Bytes.isNotEmpty(identityKeyPair) || !Bytes.isNotEmpty(identityKeyPair) ||
!Bytes.isNotEmpty(signedPreKey) || !Bytes.isNotEmpty(signedPreKey) ||
@ -3268,6 +3271,7 @@ export default class MessageReceiver
const manager = window.getAccountManager(); const manager = window.getAccountManager();
await manager.setPni(updatedPni.toString(), { await manager.setPni(updatedPni.toString(), {
identityKeyPair, identityKeyPair,
lastResortKyberPreKey: dropNull(lastResortKyberPreKey),
signedPreKey, signedPreKey,
registrationId, registrationId,
}); });

View file

@ -25,7 +25,7 @@ import type {
TextAttachmentType, TextAttachmentType,
UploadedAttachmentType, UploadedAttachmentType,
} from '../types/Attachment'; } from '../types/Attachment';
import type { UUID, TaggedUUIDStringType } from '../types/UUID'; import { type UUID, type TaggedUUIDStringType, UUIDKind } from '../types/UUID';
import type { import type {
ChallengeType, ChallengeType,
GetGroupLogOptionsType, GetGroupLogOptionsType,
@ -53,7 +53,6 @@ import * as Bytes from '../Bytes';
import { getRandomBytes } from '../Crypto'; import { getRandomBytes } from '../Crypto';
import { import {
MessageError, MessageError,
SignedPreKeyRotationError,
SendMessageProtoError, SendMessageProtoError,
HTTPError, HTTPError,
NoSenderKeyError, NoSenderKeyError,
@ -79,6 +78,7 @@ import {
numberToAddressType, numberToAddressType,
} from '../types/EmbeddedContact'; } from '../types/EmbeddedContact';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import { drop } from '../util/drop';
export type SendMetadataType = { export type SendMetadataType = {
[identifier: string]: { [identifier: string]: {
@ -956,27 +956,31 @@ export default class MessageSender {
}); });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.sendMessageProto({ drop(
callback: (res: CallbackResultType) => { this.sendMessageProto({
if (res.errors && res.errors.length > 0) { callback: (res: CallbackResultType) => {
reject(new SendMessageProtoError(res)); if (res.errors && res.errors.length > 0) {
} else { reject(new SendMessageProtoError(res));
resolve(res); } else {
} resolve(res);
}, }
contentHint, },
groupId, contentHint,
options, groupId,
proto, options,
recipients: messageOptions.recipients || [], proto,
timestamp: messageOptions.timestamp, recipients: messageOptions.recipients || [],
urgent, timestamp: messageOptions.timestamp,
story, urgent,
}); story,
})
);
}); });
} }
sendMessageProto({ // Note: all the other low-level sends call this, so it is a chokepoint for 1:1 sends
// The chokepoint for group sends is sendContentMessageToGroup
async sendMessageProto({
callback, callback,
contentHint, contentHint,
groupId, groupId,
@ -998,13 +1002,26 @@ export default class MessageSender {
story?: boolean; story?: boolean;
timestamp: number; timestamp: number;
urgent: boolean; urgent: boolean;
}>): void { }>): Promise<void> {
const rejections = window.textsecure.storage.get( const accountManager = window.getAccountManager();
'signedKeyRotationRejected', try {
0 if (accountManager.areKeysOutOfDate(UUIDKind.ACI)) {
); log.warn(
if (rejections > 5) { `sendMessageProto/${timestamp}: Keys are out of date; updating before send`
throw new SignedPreKeyRotationError(); );
await accountManager.maybeUpdateKeys(UUIDKind.ACI);
if (accountManager.areKeysOutOfDate(UUIDKind.ACI)) {
throw new Error('Keys still out of date after update');
}
}
} catch (error) {
// TODO: DESKTOP-5642
callback({
dataMessage: undefined,
editMessage: undefined,
errors: [error],
});
return;
} }
const outgoing = new OutgoingMessage({ const outgoing = new OutgoingMessage({
@ -1022,8 +1039,10 @@ export default class MessageSender {
}); });
recipients.forEach(identifier => { recipients.forEach(identifier => {
void this.queueJobForIdentifier(identifier, async () => drop(
outgoing.sendToIdentifier(identifier) this.queueJobForIdentifier(identifier, async () =>
outgoing.sendToIdentifier(identifier)
)
); );
}); });
} }
@ -1056,17 +1075,19 @@ export default class MessageSender {
resolve(result); resolve(result);
}; };
this.sendMessageProto({ drop(
callback, this.sendMessageProto({
contentHint, callback,
groupId, contentHint,
options, groupId,
proto, options,
recipients, proto,
timestamp, recipients,
urgent, timestamp,
story, urgent,
}); story,
})
);
}); });
} }
@ -1096,16 +1117,18 @@ export default class MessageSender {
resolve(res); resolve(res);
} }
}; };
this.sendMessageProto({ drop(
callback, this.sendMessageProto({
contentHint, callback,
groupId, contentHint,
options, groupId,
proto, options,
recipients: [identifier], proto,
timestamp, recipients: [identifier],
urgent, timestamp,
}); urgent,
})
);
}); });
} }
@ -2036,18 +2059,20 @@ export default class MessageSender {
} }
}; };
this.sendMessageProto({ drop(
callback, this.sendMessageProto({
contentHint, callback,
groupId, contentHint,
options, groupId,
proto, options,
recipients: identifiers, proto,
sendLogCallback, recipients: identifiers,
story, sendLogCallback,
timestamp, story,
urgent, timestamp,
}); urgent,
})
);
}); });
} }

View file

@ -14,6 +14,7 @@ import type { RawBodyRange } from '../types/BodyRange';
export { export {
IdentityKeyType, IdentityKeyType,
IdentityKeyIdType, IdentityKeyIdType,
KyberPreKeyType,
PreKeyIdType, PreKeyIdType,
PreKeyType, PreKeyType,
SenderKeyIdType, SenderKeyIdType,
@ -286,6 +287,7 @@ export type IRequestHandler = {
export type PniKeyMaterialType = Readonly<{ export type PniKeyMaterialType = Readonly<{
identityKeyPair: Uint8Array; identityKeyPair: Uint8Array;
signedPreKey: Uint8Array; signedPreKey: Uint8Array;
lastResortKyberPreKey?: Uint8Array;
registrationId: number; registrationId: number;
}>; }>;

View file

@ -1,13 +1,17 @@
// Copyright 2017 Signal Messenger, LLC // Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { isNumber } from 'lodash';
import * as durations from '../util/durations'; import * as durations from '../util/durations';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary'; import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import * as Registration from '../util/registration'; import * as Registration from '../util/registration';
import { UUIDKind } from '../types/UUID'; import { UUIDKind } from '../types/UUID';
import * as log from '../logging/log'; import * as log from '../logging/log';
import * as Errors from '../types/errors';
const ROTATION_INTERVAL = 2 * durations.DAY; const UPDATE_INTERVAL = 2 * durations.DAY;
const UPDATE_TIME_STORAGE_KEY = 'nextScheduledUpdateKeyTime';
export type MinimalEventsType = { export type MinimalEventsType = {
on(event: 'timetravel', callback: () => void): void; on(event: 'timetravel', callback: () => void): void;
@ -15,23 +19,20 @@ export type MinimalEventsType = {
let initComplete = false; let initComplete = false;
export class RotateSignedPreKeyListener { export class UpdateKeysListener {
public timeout: NodeJS.Timeout | undefined; public timeout: NodeJS.Timeout | undefined;
protected scheduleRotationForNow(): void { protected scheduleUpdateForNow(): void {
const now = Date.now(); const now = Date.now();
void window.textsecure.storage.put('nextSignedKeyRotationTime', now); void window.textsecure.storage.put(UPDATE_TIME_STORAGE_KEY, now);
} }
protected setTimeoutForNextRun(): void { protected setTimeoutForNextRun(): void {
const now = Date.now(); const now = Date.now();
const time = window.textsecure.storage.get( const time = window.textsecure.storage.get(UPDATE_TIME_STORAGE_KEY, now);
'nextSignedKeyRotationTime',
now
);
log.info( log.info(
'Next signed key rotation scheduled for', 'UpdateKeysListener: Next update scheduled for',
new Date(time).toISOString() new Date(time).toISOString()
); );
@ -44,31 +45,29 @@ export class RotateSignedPreKeyListener {
this.timeout = setTimeout(() => this.runWhenOnline(), waitTime); this.timeout = setTimeout(() => this.runWhenOnline(), waitTime);
} }
private scheduleNextRotation(): void { private scheduleNextUpdate(): void {
const now = Date.now(); const now = Date.now();
const nextTime = now + ROTATION_INTERVAL; const nextTime = now + UPDATE_INTERVAL;
void window.textsecure.storage.put('nextSignedKeyRotationTime', nextTime); void window.textsecure.storage.put(UPDATE_TIME_STORAGE_KEY, nextTime);
} }
private async run(): Promise<void> { private async run(): Promise<void> {
log.info('Rotating signed prekey...'); log.info('UpdateKeysListener: Updating keys...');
try { try {
const accountManager = window.getAccountManager(); const accountManager = window.getAccountManager();
await Promise.all([
accountManager.rotateSignedPreKey(UUIDKind.ACI),
accountManager.rotateSignedPreKey(UUIDKind.PNI),
]);
// We try to update this whenever we remove a preKey; this is a fail-safe to ensure await accountManager.maybeUpdateKeys(UUIDKind.ACI);
// we're always in good shape await accountManager.maybeUpdateKeys(UUIDKind.PNI);
await Promise.all([
accountManager.refreshPreKeys(UUIDKind.ACI), this.scheduleNextUpdate();
accountManager.refreshPreKeys(UUIDKind.PNI),
]);
this.scheduleNextRotation();
this.setTimeoutForNextRun(); this.setTimeoutForNextRun();
} catch (error) { } catch (error) {
log.error('rotateSignedPrekey() failed. Trying again in five minutes'); const errorString = isNumber(error.code)
? error.code.toString()
: Errors.toLogFormat(error);
log.error(
`UpdateKeysListener.run failure - trying again in five minutes ${errorString}`
);
setTimeout(() => this.setTimeoutForNextRun(), 5 * durations.MINUTE); setTimeout(() => this.setTimeoutForNextRun(), 5 * durations.MINUTE);
} }
} }
@ -77,7 +76,9 @@ export class RotateSignedPreKeyListener {
if (window.navigator.onLine) { if (window.navigator.onLine) {
void this.run(); void this.run();
} else { } else {
log.info('We are offline; keys will be rotated when we are next online'); log.info(
'UpdateKeysListener: We are offline; will update keys when we are next online'
);
const listener = () => { const listener = () => {
window.removeEventListener('online', listener); window.removeEventListener('online', listener);
this.setTimeoutForNextRun(); this.setTimeoutForNextRun();
@ -88,17 +89,15 @@ export class RotateSignedPreKeyListener {
public static init(events: MinimalEventsType, newVersion: boolean): void { public static init(events: MinimalEventsType, newVersion: boolean): void {
if (initComplete) { if (initComplete) {
window.SignalContext.log.info( window.SignalContext.log.info('UpdateKeysListener: Already initialized');
'Rotate signed prekey listener: Already initialized'
);
return; return;
} }
initComplete = true; initComplete = true;
const listener = new RotateSignedPreKeyListener(); const listener = new UpdateKeysListener();
if (newVersion) { if (newVersion) {
listener.scheduleRotationForNow(); listener.scheduleUpdateForNow();
} }
listener.setTimeoutForNextRun(); listener.setTimeoutForNextRun();

View file

@ -870,6 +870,11 @@ const attachmentV3Response = z.object({
export type AttachmentV3ResponseType = z.infer<typeof attachmentV3Response>; export type AttachmentV3ResponseType = z.infer<typeof attachmentV3Response>;
export type ServerKeyCountType = {
count: number;
pqCount: number;
};
export type WebAPIType = { export type WebAPIType = {
startRegistration(): unknown; startRegistration(): unknown;
finishRegistration(baton: unknown): void; finishRegistration(baton: unknown): void;
@ -925,7 +930,7 @@ export type WebAPIType = {
deviceId?: number, deviceId?: number,
options?: { accessKey?: string } options?: { accessKey?: string }
) => Promise<ServerKeysType>; ) => Promise<ServerKeysType>;
getMyKeys: (uuidKind: UUIDKind) => Promise<number>; getMyKeyCounts: (uuidKind: UUIDKind) => Promise<ServerKeyCountType>;
getOnboardingStoryManifest: () => Promise<{ getOnboardingStoryManifest: () => Promise<{
version: string; version: string;
languages: Record<string, Array<string>>; languages: Record<string, Array<string>>;
@ -998,7 +1003,7 @@ export type WebAPIType = {
) => Promise<ReserveUsernameResultType>; ) => Promise<ReserveUsernameResultType>;
confirmUsername(options: ConfirmUsernameOptionsType): Promise<void>; confirmUsername(options: ConfirmUsernameOptionsType): Promise<void>;
registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>; registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>;
registerKeys: (genKeys: KeysType, uuidKind: UUIDKind) => Promise<void>; registerKeys: (genKeys: UploadKeysType, uuidKind: UUIDKind) => Promise<void>;
registerSupportForUnauthenticatedDelivery: () => Promise<void>; registerSupportForUnauthenticatedDelivery: () => Promise<void>;
reportMessage: (options: ReportMessageOptionsType) => Promise<void>; reportMessage: (options: ReportMessageOptionsType) => Promise<void>;
requestVerificationSMS: (number: string, token: string) => Promise<void>; requestVerificationSMS: (number: string, token: string) => Promise<void>;
@ -1032,10 +1037,6 @@ export type WebAPIType = {
} }
) => Promise<MultiRecipient200ResponseType>; ) => Promise<MultiRecipient200ResponseType>;
setPhoneNumberDiscoverability: (newValue: boolean) => Promise<void>; setPhoneNumberDiscoverability: (newValue: boolean) => Promise<void>;
setSignedPreKey: (
signedPreKey: SignedPreKeyType,
uuidKind: UUIDKind
) => Promise<void>;
updateDeviceName: (deviceName: string) => Promise<void>; updateDeviceName: (deviceName: string) => Promise<void>;
uploadAvatar: ( uploadAvatar: (
uploadAvatarRequestHeaders: UploadAvatarHeadersType, uploadAvatarRequestHeaders: UploadAvatarHeadersType,
@ -1060,33 +1061,46 @@ export type WebAPIType = {
reconnect: () => Promise<void>; reconnect: () => Promise<void>;
}; };
export type SignedPreKeyType = { export type UploadSignedPreKeyType = {
keyId: number; keyId: number;
publicKey: Uint8Array; publicKey: Uint8Array;
signature: Uint8Array; signature: Uint8Array;
}; };
export type UploadPreKeyType = {
keyId: number;
publicKey: Uint8Array;
};
export type UploadKyberPreKeyType = UploadSignedPreKeyType;
export type KeysType = { export type UploadKeysType = {
identityKey: Uint8Array; identityKey: Uint8Array;
signedPreKey: SignedPreKeyType;
preKeys: Array<{ // If a field is not provided, the server won't update its data.
keyId: number; preKeys?: Array<UploadPreKeyType>;
publicKey: Uint8Array; pqPreKeys?: Array<UploadSignedPreKeyType>;
}>; pqLastResortPreKey?: UploadSignedPreKeyType;
signedPreKey?: UploadSignedPreKeyType;
}; };
export type ServerKeysType = { export type ServerKeysType = {
devices: Array<{ devices: Array<{
deviceId: number; deviceId: number;
registrationId: number; registrationId: number;
signedPreKey: {
// We'll get a 404 if none of these keys are provided; we'll have at least one
preKey?: {
keyId: number;
publicKey: Uint8Array;
};
signedPreKey?: {
keyId: number; keyId: number;
publicKey: Uint8Array; publicKey: Uint8Array;
signature: Uint8Array; signature: Uint8Array;
}; };
preKey?: { pqPreKey?: {
keyId: number; keyId: number;
publicKey: Uint8Array; publicKey: Uint8Array;
signature: Uint8Array;
}; };
}>; }>;
identityKey: Uint8Array; identityKey: Uint8Array;
@ -1293,7 +1307,7 @@ export function initialize({
getIceServers, getIceServers,
getKeysForIdentifier, getKeysForIdentifier,
getKeysForIdentifierUnauth, getKeysForIdentifierUnauth,
getMyKeys, getMyKeyCounts,
getOnboardingStoryManifest, getOnboardingStoryManifest,
getProfile, getProfile,
getProfileUnauth, getProfileUnauth,
@ -1331,7 +1345,6 @@ export function initialize({
sendMessagesUnauth, sendMessagesUnauth,
sendWithSenderKey, sendWithSenderKey,
setPhoneNumberDiscoverability, setPhoneNumberDiscoverability,
setSignedPreKey,
startRegistration, startRegistration,
unregisterRequestHandler, unregisterRequestHandler,
updateDeviceName, updateDeviceName,
@ -2052,30 +2065,74 @@ export function initialize({
publicKey: string; publicKey: string;
signature: string; signature: string;
}; };
type JSONPreKeyType = {
keyId: number;
publicKey: string;
};
type JSONKyberPreKeyType = {
keyId: number;
publicKey: string;
signature: string;
};
type JSONKeysType = { type JSONKeysType = {
identityKey: string; identityKey: string;
signedPreKey: JSONSignedPreKeyType; preKeys?: Array<JSONPreKeyType>;
preKeys: Array<{ pqPreKeys?: Array<JSONKyberPreKeyType>;
keyId: number; pqLastResortPreKey?: JSONKyberPreKeyType;
publicKey: string; signedPreKey?: JSONSignedPreKeyType;
}>;
}; };
async function registerKeys(genKeys: KeysType, uuidKind: UUIDKind) { async function registerKeys(genKeys: UploadKeysType, uuidKind: UUIDKind) {
const preKeys = genKeys.preKeys.map(key => ({ const preKeys = genKeys.preKeys?.map(key => ({
keyId: key.keyId, keyId: key.keyId,
publicKey: Bytes.toBase64(key.publicKey), publicKey: Bytes.toBase64(key.publicKey),
})); }));
const pqPreKeys = genKeys.pqPreKeys?.map(key => ({
keyId: key.keyId,
publicKey: Bytes.toBase64(key.publicKey),
signature: Bytes.toBase64(key.signature),
}));
if (
!preKeys?.length &&
!pqPreKeys?.length &&
!genKeys.pqLastResortPreKey &&
!genKeys.signedPreKey
) {
throw new Error(
'registerKeys: None of the four potential key types were provided!'
);
}
if (preKeys && preKeys.length === 0) {
throw new Error('registerKeys: Attempting to upload zero preKeys!');
}
if (pqPreKeys && pqPreKeys.length === 0) {
throw new Error('registerKeys: Attempting to upload zero pqPreKeys!');
}
const keys: JSONKeysType = { const keys: JSONKeysType = {
identityKey: Bytes.toBase64(genKeys.identityKey), identityKey: Bytes.toBase64(genKeys.identityKey),
signedPreKey: {
keyId: genKeys.signedPreKey.keyId,
publicKey: Bytes.toBase64(genKeys.signedPreKey.publicKey),
signature: Bytes.toBase64(genKeys.signedPreKey.signature),
},
preKeys, preKeys,
pqPreKeys,
...(genKeys.pqLastResortPreKey
? {
pqLastResortPreKey: {
keyId: genKeys.pqLastResortPreKey.keyId,
publicKey: Bytes.toBase64(genKeys.pqLastResortPreKey.publicKey),
signature: Bytes.toBase64(genKeys.pqLastResortPreKey.signature),
},
}
: null),
...(genKeys.signedPreKey
? {
signedPreKey: {
keyId: genKeys.signedPreKey.keyId,
publicKey: Bytes.toBase64(genKeys.signedPreKey.publicKey),
signature: Bytes.toBase64(genKeys.signedPreKey.signature),
},
}
: null),
}; };
await _ajax({ await _ajax({
@ -2097,50 +2154,39 @@ export function initialize({
}); });
} }
async function setSignedPreKey( async function getMyKeyCounts(
signedPreKey: SignedPreKeyType,
uuidKind: UUIDKind uuidKind: UUIDKind
) { ): Promise<ServerKeyCountType> {
await _ajax({
call: 'signed',
urlParameters: `?${uuidKindToQuery(uuidKind)}`,
httpType: 'PUT',
jsonData: {
keyId: signedPreKey.keyId,
publicKey: Bytes.toBase64(signedPreKey.publicKey),
signature: Bytes.toBase64(signedPreKey.signature),
},
});
}
type ServerKeyCountType = {
count: number;
};
async function getMyKeys(uuidKind: UUIDKind): Promise<number> {
const result = (await _ajax({ const result = (await _ajax({
call: 'keys', call: 'keys',
urlParameters: `?${uuidKindToQuery(uuidKind)}`, urlParameters: `?${uuidKindToQuery(uuidKind)}`,
httpType: 'GET', httpType: 'GET',
responseType: 'json', responseType: 'json',
validateResponse: { count: 'number' }, validateResponse: { count: 'number', pqCount: 'number' },
})) as ServerKeyCountType; })) as ServerKeyCountType;
return result.count; return result;
} }
type ServerKeyResponseType = { type ServerKeyResponseType = {
devices: Array<{ devices: Array<{
deviceId: number; deviceId: number;
registrationId: number; registrationId: number;
signedPreKey: {
// We'll get a 404 if none of these keys are provided; we'll have at least one
preKey?: {
keyId: number;
publicKey: string;
};
signedPreKey?: {
keyId: number; keyId: number;
publicKey: string; publicKey: string;
signature: string; signature: string;
}; };
preKey?: { pqPreKey?: {
keyId: number; keyId: number;
publicKey: string; publicKey: string;
signature: string;
}; };
}>; }>;
identityKey: string; identityKey: string;
@ -2180,12 +2226,25 @@ export function initialize({
return { return {
deviceId: device.deviceId, deviceId: device.deviceId,
registrationId: device.registrationId, registrationId: device.registrationId,
preKey, ...(preKey ? { preKey } : null),
signedPreKey: { ...(device.signedPreKey
keyId: device.signedPreKey.keyId, ? {
publicKey: Bytes.fromBase64(device.signedPreKey.publicKey), signedPreKey: {
signature: Bytes.fromBase64(device.signedPreKey.signature), keyId: device.signedPreKey.keyId,
}, publicKey: Bytes.fromBase64(device.signedPreKey.publicKey),
signature: Bytes.fromBase64(device.signedPreKey.signature),
},
}
: null),
...(device.pqPreKey
? {
pqPreKey: {
keyId: device.pqPreKey.keyId,
publicKey: Bytes.fromBase64(device.pqPreKey.publicKey),
signature: Bytes.fromBase64(device.pqPreKey.signature),
},
}
: null),
}; };
}); });
@ -2199,7 +2258,7 @@ export function initialize({
const keys = (await _ajax({ const keys = (await _ajax({
call: 'keys', call: 'keys',
httpType: 'GET', httpType: 'GET',
urlParameters: `/${identifier}/${deviceId || '*'}`, urlParameters: `/${identifier}/${deviceId || '*'}?pq=true`,
responseType: 'json', responseType: 'json',
validateResponse: { identityKey: 'string', devices: 'object' }, validateResponse: { identityKey: 'string', devices: 'object' },
})) as ServerKeyResponseType; })) as ServerKeyResponseType;
@ -2214,7 +2273,7 @@ export function initialize({
const keys = (await _ajax({ const keys = (await _ajax({
call: 'keys', call: 'keys',
httpType: 'GET', httpType: 'GET',
urlParameters: `/${identifier}/${deviceId || '*'}`, urlParameters: `/${identifier}/${deviceId || '*'}?pq=true`,
responseType: 'json', responseType: 'json',
validateResponse: { identityKey: 'string', devices: 'object' }, validateResponse: { identityKey: 'string', devices: 'object' },
unauthenticated: true, unauthenticated: true,

View file

@ -3,6 +3,7 @@
import { import {
ErrorCode, ErrorCode,
KEMPublicKey,
LibSignalErrorBase, LibSignalErrorBase,
PreKeyBundle, PreKeyBundle,
processPreKeyBundle, processPreKeyBundle,
@ -101,7 +102,8 @@ async function handleServerKeys(
await Promise.all( await Promise.all(
response.devices.map(async device => { response.devices.map(async device => {
const { deviceId, registrationId, preKey, signedPreKey } = device; const { deviceId, registrationId, pqPreKey, preKey, signedPreKey } =
device;
if ( if (
devicesToUpdate !== undefined && devicesToUpdate !== undefined &&
!devicesToUpdate.includes(deviceId) !devicesToUpdate.includes(deviceId)
@ -135,6 +137,14 @@ async function handleServerKeys(
Buffer.from(response.identityKey) Buffer.from(response.identityKey)
); );
const pqPreKeyId = pqPreKey?.keyId || null;
const pqPreKeyPublic = pqPreKey
? KEMPublicKey.deserialize(Buffer.from(pqPreKey.publicKey))
: null;
const pqPreKeySignature = pqPreKey
? Buffer.from(pqPreKey.signature)
: null;
const preKeyBundle = PreKeyBundle.new( const preKeyBundle = PreKeyBundle.new(
registrationId, registrationId,
deviceId, deviceId,
@ -143,7 +153,10 @@ async function handleServerKeys(
signedPreKey.keyId, signedPreKey.keyId,
signedPreKeyObject, signedPreKeyObject,
Buffer.from(signedPreKey.signature), Buffer.from(signedPreKey.signature),
identityKey identityKey,
pqPreKeyId,
pqPreKeyPublic,
pqPreKeySignature
); );
const address = new QualifiedAddress( const address = new QualifiedAddress(

15
ts/types/Storage.d.ts vendored
View file

@ -82,7 +82,12 @@ export type StorageAccessType = {
lastHeartbeat: number; lastHeartbeat: number;
lastStartup: number; lastStartup: number;
lastAttemptedToRefreshProfilesAt: number; lastAttemptedToRefreshProfilesAt: number;
lastResortKeyUpdateTime: number;
lastResortKeyUpdateTimePNI: number;
maxPreKeyId: number; maxPreKeyId: number;
maxPreKeyIdPNI: number;
maxKyberPreKeyId: number;
maxKyberPreKeyIdPNI: number;
number_id: string; number_id: string;
password: string; password: string;
profileKey: Uint8Array; profileKey: Uint8Array;
@ -94,7 +99,9 @@ export type StorageAccessType = {
showStickerPickerHint: boolean; showStickerPickerHint: boolean;
showStickersIntroduction: boolean; showStickersIntroduction: boolean;
signedKeyId: number; signedKeyId: number;
signedKeyRotationRejected: number; signedKeyIdPNI: number;
signedKeyUpdateTime: number;
signedKeyUpdateTimePNI: number;
storageKey: string; storageKey: string;
synced_at: number; synced_at: number;
userAgent: string; userAgent: string;
@ -145,7 +152,7 @@ export type StorageAccessType = {
paymentAddress: string; paymentAddress: string;
zoomFactor: ZoomFactorType; zoomFactor: ZoomFactorType;
preferredLeftPaneWidth: number; preferredLeftPaneWidth: number;
nextSignedKeyRotationTime: number; nextScheduledUpdateKeyTime: number;
areWeASubscriber: boolean; areWeASubscriber: boolean;
subscriberId: Uint8Array; subscriberId: Uint8Array;
subscriberCurrencyCode: string; subscriberCurrencyCode: string;
@ -153,9 +160,11 @@ export type StorageAccessType = {
keepMutedChatsArchived: boolean; keepMutedChatsArchived: boolean;
// Deprecated // Deprecated
'challenge:retry-message-ids': never;
nextSignedKeyRotationTime: number;
senderCertificateWithUuid: never; senderCertificateWithUuid: never;
signaling_key: never; signaling_key: never;
'challenge:retry-message-ids': never; signedKeyRotationRejected: number;
}; };
/* eslint-enable camelcase */ /* eslint-enable camelcase */

View file

@ -21,7 +21,7 @@ import {
} from '../textsecure/OutgoingMessage'; } from '../textsecure/OutgoingMessage';
import { Address } from '../types/Address'; import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress'; import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID'; import { UUID, UUIDKind } from '../types/UUID';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import { getValue, isEnabled } from '../RemoteConfig'; import { getValue, isEnabled } from '../RemoteConfig';
import type { UUIDStringType } from '../types/UUID'; import type { UUIDStringType } from '../types/UUID';
@ -152,6 +152,7 @@ export async function sendToGroup({
}); });
} }
// Note: This is the group send chokepoint. The 1:1 send chokepoint is sendMessageProto.
export async function sendContentMessageToGroup({ export async function sendContentMessageToGroup({
contentHint, contentHint,
contentMessage, contentMessage,
@ -180,6 +181,18 @@ export async function sendContentMessageToGroup({
urgent: boolean; urgent: boolean;
}): Promise<CallbackResultType> { }): Promise<CallbackResultType> {
const logId = sendTarget.idForLogging(); const logId = sendTarget.idForLogging();
const accountManager = window.getAccountManager();
if (accountManager.areKeysOutOfDate(UUIDKind.ACI)) {
log.warn(
`sendToGroup/${logId}: Keys are out of date; updating before send`
);
await accountManager.maybeUpdateKeys(UUIDKind.ACI);
if (accountManager.areKeysOutOfDate(UUIDKind.ACI)) {
throw new Error('Keys still out of date after update');
}
}
strictAssert( strictAssert(
window.textsecure.messaging, window.textsecure.messaging,
'sendContentMessageToGroup: textsecure.messaging not available!' 'sendContentMessageToGroup: textsecure.messaging not available!'

View file

@ -2276,28 +2276,20 @@
bindings "^1.5.0" bindings "^1.5.0"
tar "^6.1.0" tar "^6.1.0"
"@signalapp/libsignal-client@0.22.0": "@signalapp/libsignal-client@0.27.0", "@signalapp/libsignal-client@^0.27.0":
version "0.22.0" version "0.27.0"
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.22.0.tgz#d57441612df46f90df68fc5d9ad45b857b9d2c44" resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.27.0.tgz#012e254d42e4dcd752979419c048af65a3f1eed1"
integrity sha512-f1PJuxpcbmhvHxzbf0BvSJhNA3sqXrwnTf2GtfFB2CQoqTEiGCRYfyFZjwUBByiFFI5mTWKER6WGAw5AvG/3+A== integrity sha512-XinrJ9R2veJM/u3CAaL/YN5Yid+ASfsSceQiL/Qr1vKCsMori0bWG6AzOBnDUx/Bnm6dcDBc15t8w31WkXOTVw==
dependencies: dependencies:
node-gyp-build "^4.2.3" node-gyp-build "^4.2.3"
uuid "^8.3.0" uuid "^8.3.0"
"@signalapp/libsignal-client@^0.24.0": "@signalapp/mock-server@3.1.0":
version "0.24.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.24.0.tgz#4c52194071f1b0f7e0ad27a3f091c881d624897a" resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-3.1.0.tgz#6f499cf1a396626901760b93e888bb5020983075"
integrity sha512-8IjvfD1wdkKcxwwM4KC1m8yWly5NJVAUaoiGMKiok9L+sD7HnY5DKpBXb/dpgkiSfSMdr3r4219+zFG1tq2UQQ== integrity sha512-u6zz9PWV7NLP+RIz2hg4zl0UY34Ufj2pjQsPmIY//oVnu3PfIiyuKMIzPU7jPMSYq29uFbUQPypLJYOYP2dOiA==
dependencies: dependencies:
node-gyp-build "^4.2.3" "@signalapp/libsignal-client" "^0.27.0"
uuid "^8.3.0"
"@signalapp/mock-server@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-3.0.1.tgz#9f5e7d8ed207af191eadc33667708013a332b522"
integrity sha512-TXATTeczq6O1apT3SNPYFYG8/Z9MMRzmRfwZgl5JyyfjJcR00+b2GSW0G5qVONeVr6r6wS5zbUBDhcZ8+b04OA==
dependencies:
"@signalapp/libsignal-client" "^0.24.0"
debug "^4.3.2" debug "^4.3.2"
long "^4.0.0" long "^4.0.0"
micro "^9.3.4" micro "^9.3.4"