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",
"@react-spring/web": "9.5.5",
"@signalapp/better-sqlite3": "8.4.3",
"@signalapp/libsignal-client": "0.22.0",
"@signalapp/libsignal-client": "0.27.0",
"@signalapp/ringrtc": "2.29.0",
"@types/fabric": "4.5.3",
"backbone": "1.4.0",
@ -189,7 +189,7 @@
"@electron/fuses": "1.5.0",
"@formatjs/intl": "2.6.7",
"@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-actions": "6.5.6",
"@storybook/addon-controls": "6.5.6",

View file

@ -587,9 +587,11 @@ message SyncMessage {
message PniChangeNumber {
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 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 {

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import * as client from '@signalapp/libsignal-client';
import type { KyberPreKeyRecord } from '@signalapp/libsignal-client';
import * as Bytes from './Bytes';
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 {
const privKey = client.PrivateKey.generate();
const pubKey = privKey.getPublicKey();

View file

@ -7,6 +7,7 @@ import { isNumber } from 'lodash';
import type {
Direction,
KyberPreKeyRecord,
PreKeyRecord,
ProtocolAddress,
SenderKeyRecord,
@ -16,6 +17,7 @@ import type {
} from '@signalapp/libsignal-client';
import {
IdentityKeyStore,
KyberPreKeyStore,
PreKeyStore,
PrivateKey,
PublicKey,
@ -23,7 +25,6 @@ import {
SessionStore,
SignedPreKeyStore,
} from '@signalapp/libsignal-client';
import { freezePreKey, freezeSignedPreKey } from './SignalProtocolStore';
import { Address } from './types/Address';
import { QualifiedAddress } from './types/QualifiedAddress';
import type { UUID } from './types/UUID';
@ -187,12 +188,8 @@ export class PreKeys extends PreKeyStore {
this.ourUuid = ourUuid;
}
async savePreKey(id: number, record: PreKeyRecord): Promise<void> {
await window.textsecure.storage.protocol.storePreKey(
this.ourUuid,
id,
freezePreKey(record)
);
async savePreKey(): Promise<void> {
throw new Error('savePreKey: Should not be called by libsignal!');
}
async getPreKey(id: number): Promise<PreKeyRecord> {
@ -209,7 +206,41 @@ export class PreKeys extends PreKeyStore {
}
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;
}
async saveSignedPreKey(
id: number,
record: SignedPreKeyRecord
): Promise<void> {
await window.textsecure.storage.protocol.storeSignedPreKey(
this.ourUuid,
id,
freezeSignedPreKey(record),
true
);
async saveSignedPreKey(): Promise<void> {
throw new Error('saveSignedPreKey: Should not be called by libsignal!');
}
async getSignedPreKey(id: number): Promise<SignedPreKeyRecord> {

View file

@ -9,6 +9,7 @@ import { EventEmitter } from 'events';
import {
Direction,
IdentityKeyPair,
KyberPreKeyRecord,
PreKeyRecord,
PrivateKey,
PublicKey,
@ -32,6 +33,7 @@ import type {
IdentityKeyType,
IdentityKeyIdType,
KeyPairType,
KyberPreKeyType,
OuterSignedPrekeyType,
PniKeyMaterialType,
PniSignatureMessageType,
@ -46,6 +48,7 @@ import type {
SignedPreKeyType,
UnprocessedType,
UnprocessedUpdateType,
CompatPreKeyType,
} from './textsecure/Types.d';
import type { RemoveAllConfiguration } from './types/RemoveAllConfiguration';
import type { UUIDStringType } from './types/UUID';
@ -57,8 +60,13 @@ import * as log from './logging/log';
import * as Errors from './types/errors';
import { MINUTE } from './util/durations';
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 LOW_KEYS_THRESHOLD = 25;
const VerifiedStatus = {
DEFAULT: 0,
@ -103,6 +111,7 @@ type CacheEntryType<DBType, HydratedType> =
| { hydrated: true; fromDB: DBType; item: HydratedType };
type MapFields =
| 'kyberPreKeys'
| 'identityKeys'
| 'preKeys'
| 'senderKeys'
@ -226,6 +235,11 @@ export class SignalProtocolStore extends EventEmitter {
CacheEntryType<IdentityKeyType, PublicKey>
>;
kyberPreKeys?: Map<
PreKeyIdType,
CacheEntryType<KyberPreKeyType, KyberPreKeyRecord>
>;
senderKeys?: Map<SenderKeyIdType, SenderKeyCacheEntry>;
sessions?: Map<SessionIdType, SessionCacheEntry>;
@ -290,6 +304,11 @@ export class SignalProtocolStore extends EventEmitter {
'identityKeys',
window.Signal.Data.getAllIdentityKeys()
),
_fillCaches<string, KyberPreKeyType, KyberPreKeyRecord>(
this,
'kyberPreKeys',
window.Signal.Data.getAllKyberPreKeys()
),
_fillCaches<string, SessionType, SessionRecord>(
this,
'sessions',
@ -321,6 +340,190 @@ export class SignalProtocolStore extends EventEmitter {
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
async loadPreKey(
@ -331,8 +534,7 @@ export class SignalProtocolStore extends EventEmitter {
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);
if (!entry) {
log.error('Failed to fetch prekey:', id);
@ -354,53 +556,76 @@ export class SignalProtocolStore extends EventEmitter {
return item;
}
async storePreKey(
ourUuid: UUID,
keyId: number,
keyPair: KeyPairType
): Promise<void> {
loadPreKeys(ourUuid: UUID): Array<PreKeyType> {
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!');
}
const id: PreKeyIdType = `${ourUuid.toString()}:${keyId}`;
if (this.preKeys.has(id)) {
throw new Error(`storePreKey: prekey ${id} already exists!`);
}
const now = Date.now();
const toSave: Array<PreKeyType> = [];
keys.forEach(key => {
const id: PreKeyIdType = this._getKeyId(ourUuid, key.keyId);
const fromDB = {
id,
keyId,
ourUuid: ourUuid.toString(),
publicKey: keyPair.pubKey,
privateKey: keyPair.privKey,
};
if (preKeyCache.has(id)) {
throw new Error(`storePreKeys: prekey ${id} already exists!`);
}
await window.Signal.Data.createOrUpdatePreKey(fromDB);
this.preKeys.set(id, {
hydrated: false,
fromDB,
const preKey = {
id,
keyId: key.keyId,
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> {
if (!this.preKeys) {
throw new Error('removePreKey: this.preKeys not yet cached!');
async removePreKeys(ourUuid: UUID, keyIds: Array<number>): Promise<void> {
const preKeyCache = this.preKeys;
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 {
this.emit('removePreKey', ourUuid);
} catch (error) {
log.error(
'removePreKey error triggering removePreKey:',
Errors.toLogFormat(error)
);
await window.Signal.Data.removePreKeyById(ids);
ids.forEach(id => {
preKeyCache.delete(id);
});
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> {
@ -443,9 +668,7 @@ export class SignalProtocolStore extends EventEmitter {
return item;
}
async loadSignedPreKeys(
ourUuid: UUID
): Promise<Array<OuterSignedPrekeyType>> {
loadSignedPreKeys(ourUuid: UUID): Array<OuterSignedPrekeyType> {
if (!this.signedPreKeys) {
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
// have indeed been accepted by the server.
async confirmSignedPreKey(ourUuid: UUID, keyId: number): Promise<void> {
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(
ourUuid: UUID,
keyId: number,
@ -482,7 +727,7 @@ export class SignalProtocolStore extends EventEmitter {
throw new Error('storeSignedPreKey: this.signedPreKeys not yet cached!');
}
const id: SignedPreKeyIdType = `${ourUuid.toString()}:${keyId}`;
const id: SignedPreKeyIdType = this._getKeyId(ourUuid, keyId);
const fromDB = {
id,
@ -501,14 +746,21 @@ export class SignalProtocolStore extends EventEmitter {
});
}
async removeSignedPreKey(ourUuid: UUID, keyId: number): Promise<void> {
if (!this.signedPreKeys) {
async removeSignedPreKeys(
ourUuid: UUID,
keyIds: Array<number>
): Promise<void> {
const signedPreKeyCache = this.signedPreKeys;
if (!signedPreKeyCache) {
throw new Error('removeSignedPreKey: this.signedPreKeys not yet cached!');
}
const id: SignedPreKeyIdType = `${ourUuid.toString()}:${keyId}`;
this.signedPreKeys.delete(id);
await window.Signal.Data.removeSignedPreKeyById(id);
const ids = keyIds.map(keyId => this._getKeyId(ourUuid, keyId));
await window.Signal.Data.removeSignedPreKeyById(ids);
ids.forEach(id => {
signedPreKeyCache.delete(id);
});
}
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
await Promise.all([
@ -2200,6 +2459,7 @@ export class SignalProtocolStore extends EventEmitter {
),
window.Signal.Data.removePreKeysByUuid(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,
{
identityKeyPair: identityBytes,
lastResortKyberPreKey: lastResortKyberPreKeyBytes,
signedPreKey: signedPreKeyBytes,
registrationId,
}: PniKeyMaterialType
): Promise<void> {
log.info(`SignalProtocolStore.updateOurPniKeyMaterial(${pni})`);
const logId = `SignalProtocolStore.updateOurPniKeyMaterial(${pni})`;
log.info(`${logId}: starting...`);
const identityKeyPair = IdentityKeyPair.deserialize(
Buffer.from(identityBytes)
@ -2219,6 +2481,9 @@ export class SignalProtocolStore extends EventEmitter {
const signedPreKey = SignedPreKeyRecord.deserialize(
Buffer.from(signedPreKeyBytes)
);
const lastResortKyberPreKey = lastResortKyberPreKeyBytes
? KyberPreKeyRecord.deserialize(Buffer.from(lastResortKyberPreKeyBytes))
: undefined;
const { storage } = window;
@ -2245,6 +2510,11 @@ export class SignalProtocolStore extends EventEmitter {
...(storage.get('registrationIdMap') || {}),
[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(
pni,
signedPreKey.id(),
@ -2255,6 +2525,26 @@ export class SignalProtocolStore extends EventEmitter {
true,
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());
}
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
//
public override on(
name: 'removePreKey',
name: 'lowKeys',
handler: (ourUuid: UUID) => unknown
): this;
@ -2378,7 +2679,7 @@ export class SignalProtocolStore extends EventEmitter {
return super.on(eventName, listener);
}
public override emit(name: 'removePreKey', ourUuid: UUID): boolean;
public override emit(name: 'lowKeys', ourUuid: UUID): boolean;
public override emit(
name: 'keychange',

View file

@ -107,7 +107,7 @@ import type {
} from './textsecure/messageReceiverEvents';
import type { WebAPIType } from './textsecure/WebAPI';
import * as KeyChangeListener from './textsecure/KeyChangeListener';
import { RotateSignedPreKeyListener } from './textsecure/RotateSignedPreKeyListener';
import { UpdateKeysListener } from './textsecure/UpdateKeysListener';
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff';
import { AppViewType } from './state/ducks/app';
@ -599,10 +599,17 @@ export async function startApp(): Promise<void> {
);
KeyChangeListener.init(window.textsecure.storage.protocol);
window.textsecure.storage.protocol.on('removePreKey', (ourUuid: UUID) => {
const uuidKind = window.textsecure.storage.user.getOurUuidKind(ourUuid);
void window.getAccountManager().refreshPreKeys(uuidKind);
});
window.textsecure.storage.protocol.on(
'lowKeys',
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.reduxActions.stories.removeAllStories();
@ -876,6 +883,15 @@ export async function startApp(): Promise<void> {
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')) {
const formattingWarningShown = window.storage.get(
'formattingWarningShown',
@ -2079,7 +2095,7 @@ export async function startApp(): Promise<void> {
window.ConversationController.onEmpty();
// Start listeners here, after we get through our queue.
RotateSignedPreKeyListener.init(window.Whisper.events, newVersion);
UpdateKeysListener.init(window.Whisper.events, newVersion);
profileKeyResponseQueue.start();
lightSessionResetQueue.start();

View file

@ -7,7 +7,12 @@ import { JobQueue } from './JobQueue';
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
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>;

View file

@ -1406,7 +1406,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SendMessageChallengeError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError'
);
}
@ -1472,7 +1471,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
e.name === 'OutgoingMessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SendMessageChallengeError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError')
);
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.
const errorsToSave: Array<CustomError> = [];
let hadSignedPreKeyRotationError = false;
errors.forEach(error => {
const conversation =
window.ConversationController.get(error.identifier) ||
@ -1616,9 +1613,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
let shouldSaveError = true;
switch (error.name) {
case 'SignedPreKeyRotationError':
hadSignedPreKeyRotationError = true;
break;
case 'OutgoingIdentityKeyError': {
if (conversation) {
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;
// Only update the expirationStartTimestamp if we don't already have one set
if (!this.get('expirationStartTimestamp')) {

View file

@ -52,6 +52,8 @@ import type {
SignedPreKeyIdType,
SignedPreKeyType,
StoredSignedPreKeyType,
KyberPreKeyType,
StoredKyberPreKeyType,
} from './Interface';
import { MINUTE } from '../util/durations';
import { getMessageIdForLogging } from '../util/idForLogging';
@ -73,6 +75,11 @@ const exclusiveInterface: ClientExclusiveInterface = {
bulkAddIdentityKeys,
getAllIdentityKeys,
createOrUpdateKyberPreKey,
getKyberPreKeyById,
bulkAddKyberPreKeys,
getAllKyberPreKeys,
createOrUpdatePreKey,
getPreKeyById,
bulkAddPreKeys,
@ -248,6 +255,37 @@ async function getAllIdentityKeys(): Promise<Array<IdentityKeyType>> {
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
async function createOrUpdatePreKey(data: PreKeyType): Promise<void> {

View file

@ -109,21 +109,35 @@ export type MessageType = MessageAttributesType;
export type MessageTypeUnhydrated = {
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 = {
id: `${UUIDStringType}:${number}`;
id: PreKeyIdType;
createdAt: number;
keyId: number;
ourUuid: UUIDStringType;
privateKey: Uint8Array;
publicKey: Uint8Array;
};
export type StoredPreKeyType = {
id: `${UUIDStringType}:${number}`;
keyId: number;
ourUuid: UUIDStringType;
export type StoredPreKeyType = PreKeyType & {
privateKey: string;
publicKey: string;
};
export type PreKeyIdType = PreKeyType['id'];
export type ServerSearchResultMessageType = {
json: string;
@ -410,16 +424,24 @@ export type DataInterface = {
removeIdentityKeyById: (id: IdentityKeyIdType) => 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>;
removeAllPreKeys: () => Promise<void>;
removeSignedPreKeyById: (id: SignedPreKeyIdType) => Promise<void>;
removeSignedPreKeyById: (
id: SignedPreKeyIdType | Array<SignedPreKeyIdType>
) => Promise<void>;
removeSignedPreKeysByUuid: (uuid: UUIDStringType) => Promise<void>;
removeAllSignedPreKeys: () => Promise<void>;
removeAllItems: () => Promise<void>;
removeItemById: (id: ItemKeyType) => Promise<void>;
removeItemById: (id: ItemKeyType | Array<ItemKeyType>) => Promise<void>;
createOrUpdateSenderKey: (key: SenderKeyType) => Promise<void>;
getSenderKeyById: (id: SenderKeyIdType) => Promise<SenderKeyType | undefined>;
@ -822,6 +844,13 @@ export type ServerInterface = DataInterface & {
bulkAddIdentityKeys: (array: Array<StoredIdentityKeyType>) => Promise<void>;
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>;
getPreKeyById: (id: PreKeyIdType) => Promise<StoredPreKeyType | undefined>;
bulkAddPreKeys: (array: Array<StoredPreKeyType>) => Promise<void>;
@ -901,6 +930,13 @@ export type ClientExclusiveInterface = {
bulkAddIdentityKeys: (array: Array<IdentityKeyType>) => Promise<void>;
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>;
getPreKeyById: (id: PreKeyIdType) => Promise<PreKeyType | undefined>;
bulkAddPreKeys: (array: Array<PreKeyType>) => Promise<void>;

View file

@ -133,6 +133,7 @@ import type {
UnprocessedType,
UnprocessedUpdateType,
GetNearbyMessageFromDeletedSetOptionsType,
StoredKyberPreKeyType,
} from './Interface';
import { SeenStatus } from '../MessageSeenStatus';
import {
@ -173,6 +174,14 @@ const dataInterface: ServerInterface = {
removeAllIdentityKeys,
getAllIdentityKeys,
createOrUpdateKyberPreKey,
getKyberPreKeyById,
bulkAddKyberPreKeys,
removeKyberPreKeyById,
removeKyberPreKeysByUuid,
removeAllKyberPreKeys,
getAllKyberPreKeys,
createOrUpdatePreKey,
getPreKeyById,
bulkAddPreKeys,
@ -655,6 +664,40 @@ async function getAllIdentityKeys(): Promise<Array<StoredIdentityKeyType>> {
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';
async function createOrUpdatePreKey(data: StoredPreKeyType): Promise<void> {
return createOrUpdate(getInstance(), PRE_KEYS_TABLE, data);
@ -667,7 +710,9 @@ async function getPreKeyById(
async function bulkAddPreKeys(array: Array<StoredPreKeyType>): Promise<void> {
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);
}
async function removePreKeysByUuid(uuid: UUIDStringType): Promise<void> {
@ -699,7 +744,9 @@ async function bulkAddSignedPreKeys(
): Promise<void> {
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);
}
async function removeSignedPreKeysByUuid(uuid: UUIDStringType): Promise<void> {
@ -755,7 +802,9 @@ async function getAllItems(): Promise<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);
}
async function removeAllItems(): Promise<void> {
@ -4989,6 +5038,7 @@ async function removeAll(): Promise<void> {
DELETE FROM identityKeys;
DELETE FROM items;
DELETE FROM jobs;
DELETE FROM kyberPreKeys;
DELETE FROM messages_fts;
DELETE FROM messages;
DELETE FROM preKeys;
@ -5024,6 +5074,7 @@ async function removeAllConfiguration(
`
DELETE FROM identityKeys;
DELETE FROM jobs;
DELETE FROM kyberPreKeys;
DELETE FROM preKeys;
DELETE FROM senderKeys;
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 updateToSchemaVersion83 from './83-mentions';
import updateToSchemaVersion84 from './84-all-mentions';
import updateToSchemaVersion85 from './85-add-kyber-keys';
function updateToSchemaVersion1(
currentVersion: number,
@ -1984,11 +1985,13 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion77,
updateToSchemaVersion78,
updateToSchemaVersion79,
updateToSchemaVersion80,
updateToSchemaVersion81,
updateToSchemaVersion82,
updateToSchemaVersion83,
updateToSchemaVersion84,
updateToSchemaVersion85,
];
export function updateSchema(db: Database, logger: LoggerType): void {

View file

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

View file

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

View file

@ -2,84 +2,98 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { range } from 'lodash';
import { getRandomBytes } from '../../Crypto';
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 { DAY } from '../../util/durations';
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('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(() => {
const server: any = {};
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', () => {
let originalGetIdentityKeyPair: any;
afterEach(() => {
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 originalRemoveSignedPreKey: any;
let originalGetUuid: any;
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 () => {
const ourUuid = UUID.generate();
originalGetUuid = window.textsecure.storage.user.getUuid;
originalGetIdentityKeyPair =
window.textsecure.storage.protocol.getIdentityKeyPair;
originalLoadSignedPreKeys =
window.textsecure.storage.protocol.loadSignedPreKeys;
originalRemoveSignedPreKey =
window.textsecure.storage.protocol.removeSignedPreKey;
window.textsecure.storage.protocol.removeSignedPreKeys;
window.textsecure.storage.user.getUuid = () => ourUuid;
window.textsecure.storage.protocol.getIdentityKeyPair = () => identityKey;
window.textsecure.storage.protocol.loadSignedPreKeys = async () =>
window.textsecure.storage.protocol.loadSignedPreKeys = () =>
signedPreKeys;
// removeSignedPreKeys is updated per-test, below
});
afterEach(() => {
window.textsecure.storage.user.getUuid = originalGetUuid;
window.textsecure.storage.protocol.getIdentityKeyPair =
originalGetIdentityKeyPair;
window.textsecure.storage.protocol.loadSignedPreKeys =
originalLoadSignedPreKeys;
window.textsecure.storage.protocol.removeSignedPreKey =
window.textsecure.storage.protocol.removeSignedPreKeys =
originalRemoveSignedPreKey;
});
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);
});
});
it('keeps three confirmed keys even if over a month old', () => {
it('keeps no keys if five or less, even if over a month old', () => {
const now = Date.now();
signedPreKeys = [
{
@ -103,10 +117,24 @@ describe('AccountManager', () => {
pubKey,
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
return accountManager.cleanSignedPreKeys(UUIDKind.ACI);
return accountManager._cleanSignedPreKeys(UUIDKind.ACI);
});
it('eliminates oldest keys, even if recent key is unconfirmed', async () => {
@ -157,60 +185,430 @@ describe('AccountManager', () => {
},
];
let count = 0;
window.textsecure.storage.protocol.removeSignedPreKey = async (
let removedKeys: Array<number> = [];
window.textsecure.storage.protocol.removeSignedPreKeys = async (
_,
keyId
keyIds
) => {
if (keyId !== 4) {
throw new Error(`Wrong keys were eliminated! ${keyId}`);
}
count += 1;
removedKeys = removedKeys.concat(keyIds);
};
await accountManager.cleanSignedPreKeys(UUIDKind.ACI);
assert.strictEqual(count, 1);
await accountManager._cleanSignedPreKeys(UUIDKind.ACI);
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();
signedPreKeys = [
kyberPreKeys = [
{
id: `${ourUuid.toString()}:1`,
createdAt: now - DAY * 32,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: true,
keyId: 1,
created_at: now - DAY * 32,
confirmed: true,
pubKey,
privKey,
ourUuid: ourUuid.toString(),
},
{
id: `${ourUuid.toString()}:2`,
createdAt: now - DAY * 34,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: true,
keyId: 2,
created_at: now - DAY * 44,
confirmed: true,
pubKey,
privKey,
ourUuid: ourUuid.toString(),
},
{
id: `${ourUuid.toString()}:3`,
createdAt: now - DAY * 38,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: true,
keyId: 3,
created_at: now - DAY * 36,
confirmed: false,
pubKey,
privKey,
ourUuid: ourUuid.toString(),
},
{
id: `${ourUuid.toString()}:4`,
createdAt: now - DAY * 39,
data: getRandomBytes(32),
isLastResort: true,
isConfirmed: false,
keyId: 4,
created_at: now - DAY * 20,
confirmed: false,
pubKey,
privKey,
ourUuid: ourUuid.toString(),
},
{
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 () => {
throw new Error('None should be removed!');
// should be no calls to store.removeKyberPreKey, would cause crash
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 { generateKeyPair } from '../../Curve';
import type { GeneratedKeysType } from '../../textsecure/AccountManager';
import type { UploadKeysType } from '../../textsecure/WebAPI';
import AccountManager from '../../textsecure/AccountManager';
import type { PreKeyType, SignedPreKeyType } from '../../textsecure/Types.d';
import { UUID, UUIDKind } from '../../types/UUID';
@ -19,6 +19,7 @@ const assertEqualBuffers = (a: Uint8Array, b: Uint8Array) => {
describe('Key generation', function thisNeeded() {
const count = 10;
const ourUuid = new UUID('aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee');
let result: UploadKeysType;
this.timeout(count * 2000);
function itStoresPreKey(keyId: number): void {
@ -30,6 +31,15 @@ describe('Key generation', function thisNeeded() {
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 {
it(`signed prekey ${keyId} is valid`, async () => {
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
@ -39,7 +49,8 @@ describe('Key generation', function thisNeeded() {
assert(keyPair, `SignedPreKey ${keyId} not found`);
});
}
async function validateResultKey(
async function validateResultPreKey(
resultKey: Pick<PreKeyType, 'keyId' | 'publicKey'>
): Promise<void> {
const keyPair = await textsecure.storage.protocol.loadPreKey(
@ -52,8 +63,11 @@ describe('Key generation', function thisNeeded() {
assertEqualBuffers(resultKey.publicKey, keyPair.publicKey().serialize());
}
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(
ourUuid,
resultSignedKey.keyId
@ -68,120 +82,166 @@ describe('Key generation', function thisNeeded() {
}
before(async () => {
await textsecure.storage.protocol.clearPreKeyStore();
await textsecure.storage.protocol.clearKyberPreKeyStore();
await textsecure.storage.protocol.clearSignedPreKeysStore();
const keyPair = generateKeyPair();
await textsecure.storage.put('identityKeyMap', {
[ourUuid.toString()]: keyPair,
});
await textsecure.storage.user.setUuidAndDeviceId(ourUuid.toString(), 1);
await textsecure.storage.protocol.hydrateCaches();
});
after(async () => {
await textsecure.storage.protocol.clearPreKeyStore();
await textsecure.storage.protocol.clearKyberPreKeyStore();
await textsecure.storage.protocol.clearSignedPreKeysStore();
});
describe('the first time', () => {
let result: GeneratedKeysType;
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-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) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
describe('generates the basics', () => {
for (let i = 1; i <= count; i += 1) {
itStoresPreKey(i);
}
for (let i = 1; i <= count + 1; i += 1) {
itStoresKyberPreKey(i);
}
itStoresSignedPreKey(1);
});
it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
const preKeys = result.preKeys || [];
assert.isArray(preKeys);
assert.lengthOf(preKeys, count);
for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]);
assert.isObject(preKeys[i]);
}
});
it('result contains the correct keyIds', () => {
const preKeys = result.preKeys || [];
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 () => {
await Promise.all(result.preKeys.map(validateResultKey));
const preKeys = result.preKeys || [];
await Promise.all(preKeys.map(validateResultPreKey));
});
it('returns a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 1);
assert.instanceOf(result.signedPreKey.signature, Uint8Array);
assert.strictEqual(result.signedPreKey?.keyId, 1);
assert.instanceOf(result.signedPreKey?.signature, Uint8Array);
return validateResultSignedKey(result.signedPreKey);
});
});
describe('the second time', () => {
let result: GeneratedKeysType;
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-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`, () => {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
const preKeys = result.preKeys || [];
assert.isArray(preKeys);
assert.lengthOf(preKeys, count);
for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]);
assert.isObject(preKeys[i]);
}
});
it('result contains the correct keyIds', () => {
const preKeys = result.preKeys || [];
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 () => {
await Promise.all(result.preKeys.map(validateResultKey));
const preKeys = result.preKeys || [];
await Promise.all(preKeys.map(validateResultPreKey));
});
it('returns a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 2);
assert.instanceOf(result.signedPreKey.signature, Uint8Array);
assert.strictEqual(result.signedPreKey?.keyId, 2);
assert.instanceOf(result.signedPreKey?.signature, Uint8Array);
return validateResultSignedKey(result.signedPreKey);
});
});
describe('the third time', () => {
let result: GeneratedKeysType;
describe('the third time, after keys are confirmed', () => {
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-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`, () => {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
const preKeys = result.preKeys || [];
assert.isArray(preKeys);
assert.lengthOf(preKeys, count);
for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]);
assert.isObject(preKeys[i]);
}
});
it('result contains the correct keyIds', () => {
const preKeys = result.preKeys || [];
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 () => {
await Promise.all(result.preKeys.map(validateResultKey));
const preKeys = result.preKeys || [];
await Promise.all(preKeys.map(validateResultPreKey));
});
it('result contains a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 3);
assert.instanceOf(result.signedPreKey.signature, Uint8Array);
return validateResultSignedKey(result.signedPreKey);
it('does not generate a third last resort prekey', async () => {
const keyId = 3 * count + 3;
const key = await textsecure.storage.protocol.loadKyberPreKey(
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 createDebug from 'debug';
import pTimeout from 'p-timeout';
import normalizePath from 'normalize-path';
import type { Device, PrimaryDevice } from '@signalapp/mock-server';
import { Server, UUIDKind, loadCertificates } from '@signalapp/mock-server';
@ -289,7 +290,20 @@ export class Bootstrap {
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;
if (!ARTIFACTS_DIR) {
// eslint-disable-next-line no-console
@ -299,7 +313,12 @@ export class Bootstrap {
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
console.error(`Saving logs to ${outDir}`);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -99,10 +99,7 @@ describe('storage service', function needsName() {
return;
}
if (this.currentTest?.state !== 'passed') {
await bootstrap.saveLogs(app);
}
await bootstrap.maybeSaveLogs(this.currentTest, app);
await app.close();
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 {
readonly httpError: HTTPError;

View file

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

View file

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

View file

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

View file

@ -1,13 +1,17 @@
// Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isNumber } from 'lodash';
import * as durations from '../util/durations';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import * as Registration from '../util/registration';
import { UUIDKind } from '../types/UUID';
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 = {
on(event: 'timetravel', callback: () => void): void;
@ -15,23 +19,20 @@ export type MinimalEventsType = {
let initComplete = false;
export class RotateSignedPreKeyListener {
export class UpdateKeysListener {
public timeout: NodeJS.Timeout | undefined;
protected scheduleRotationForNow(): void {
protected scheduleUpdateForNow(): void {
const now = Date.now();
void window.textsecure.storage.put('nextSignedKeyRotationTime', now);
void window.textsecure.storage.put(UPDATE_TIME_STORAGE_KEY, now);
}
protected setTimeoutForNextRun(): void {
const now = Date.now();
const time = window.textsecure.storage.get(
'nextSignedKeyRotationTime',
now
);
const time = window.textsecure.storage.get(UPDATE_TIME_STORAGE_KEY, now);
log.info(
'Next signed key rotation scheduled for',
'UpdateKeysListener: Next update scheduled for',
new Date(time).toISOString()
);
@ -44,31 +45,29 @@ export class RotateSignedPreKeyListener {
this.timeout = setTimeout(() => this.runWhenOnline(), waitTime);
}
private scheduleNextRotation(): void {
private scheduleNextUpdate(): void {
const now = Date.now();
const nextTime = now + ROTATION_INTERVAL;
void window.textsecure.storage.put('nextSignedKeyRotationTime', nextTime);
const nextTime = now + UPDATE_INTERVAL;
void window.textsecure.storage.put(UPDATE_TIME_STORAGE_KEY, nextTime);
}
private async run(): Promise<void> {
log.info('Rotating signed prekey...');
log.info('UpdateKeysListener: Updating keys...');
try {
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
// we're always in good shape
await Promise.all([
accountManager.refreshPreKeys(UUIDKind.ACI),
accountManager.refreshPreKeys(UUIDKind.PNI),
]);
this.scheduleNextRotation();
await accountManager.maybeUpdateKeys(UUIDKind.ACI);
await accountManager.maybeUpdateKeys(UUIDKind.PNI);
this.scheduleNextUpdate();
this.setTimeoutForNextRun();
} 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);
}
}
@ -77,7 +76,9 @@ export class RotateSignedPreKeyListener {
if (window.navigator.onLine) {
void this.run();
} 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 = () => {
window.removeEventListener('online', listener);
this.setTimeoutForNextRun();
@ -88,17 +89,15 @@ export class RotateSignedPreKeyListener {
public static init(events: MinimalEventsType, newVersion: boolean): void {
if (initComplete) {
window.SignalContext.log.info(
'Rotate signed prekey listener: Already initialized'
);
window.SignalContext.log.info('UpdateKeysListener: Already initialized');
return;
}
initComplete = true;
const listener = new RotateSignedPreKeyListener();
const listener = new UpdateKeysListener();
if (newVersion) {
listener.scheduleRotationForNow();
listener.scheduleUpdateForNow();
}
listener.setTimeoutForNextRun();

View file

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

View file

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

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

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

View file

@ -21,7 +21,7 @@ import {
} from '../textsecure/OutgoingMessage';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import { UUID, UUIDKind } from '../types/UUID';
import * as Errors from '../types/errors';
import { getValue, isEnabled } from '../RemoteConfig';
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({
contentHint,
contentMessage,
@ -180,6 +181,18 @@ export async function sendContentMessageToGroup({
urgent: boolean;
}): Promise<CallbackResultType> {
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(
window.textsecure.messaging,
'sendContentMessageToGroup: textsecure.messaging not available!'

View file

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