Support for loading, storing, and using kyber keys in decryption
This commit is contained in:
parent
c1580a5eb3
commit
b6445a6af0
49 changed files with 2260 additions and 806 deletions
|
@ -89,7 +89,7 @@
|
||||||
"@popperjs/core": "2.11.6",
|
"@popperjs/core": "2.11.6",
|
||||||
"@react-spring/web": "9.5.5",
|
"@react-spring/web": "9.5.5",
|
||||||
"@signalapp/better-sqlite3": "8.4.3",
|
"@signalapp/better-sqlite3": "8.4.3",
|
||||||
"@signalapp/libsignal-client": "0.22.0",
|
"@signalapp/libsignal-client": "0.27.0",
|
||||||
"@signalapp/ringrtc": "2.29.0",
|
"@signalapp/ringrtc": "2.29.0",
|
||||||
"@types/fabric": "4.5.3",
|
"@types/fabric": "4.5.3",
|
||||||
"backbone": "1.4.0",
|
"backbone": "1.4.0",
|
||||||
|
@ -189,7 +189,7 @@
|
||||||
"@electron/fuses": "1.5.0",
|
"@electron/fuses": "1.5.0",
|
||||||
"@formatjs/intl": "2.6.7",
|
"@formatjs/intl": "2.6.7",
|
||||||
"@mixer/parallel-prettier": "2.0.3",
|
"@mixer/parallel-prettier": "2.0.3",
|
||||||
"@signalapp/mock-server": "3.0.1",
|
"@signalapp/mock-server": "3.1.0",
|
||||||
"@storybook/addon-a11y": "6.5.6",
|
"@storybook/addon-a11y": "6.5.6",
|
||||||
"@storybook/addon-actions": "6.5.6",
|
"@storybook/addon-actions": "6.5.6",
|
||||||
"@storybook/addon-controls": "6.5.6",
|
"@storybook/addon-controls": "6.5.6",
|
||||||
|
|
|
@ -587,9 +587,11 @@ message SyncMessage {
|
||||||
|
|
||||||
message PniChangeNumber {
|
message PniChangeNumber {
|
||||||
optional bytes identityKeyPair = 1; // Serialized libsignal-client IdentityKeyPair
|
optional bytes identityKeyPair = 1; // Serialized libsignal-client IdentityKeyPair
|
||||||
optional bytes signedPreKey = 2; // Serialized libsignal-client SignedPreKeyRecord
|
optional bytes signedPreKey = 2; // Serialized libsignal-client SignedPreKeyRecord
|
||||||
|
optional bytes lastResortKyberPreKey = 5; // Serialized libsignal-client KyberPreKeyRecord
|
||||||
optional uint32 registrationId = 3;
|
optional uint32 registrationId = 3;
|
||||||
optional string newE164 = 4; // The e164 we have changed our number to
|
optional string newE164 = 4; // The e164 we have changed our number to
|
||||||
|
// Next ID: 6
|
||||||
}
|
}
|
||||||
|
|
||||||
message CallEvent {
|
message CallEvent {
|
||||||
|
|
35
ts/Curve.ts
35
ts/Curve.ts
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as client from '@signalapp/libsignal-client';
|
import * as client from '@signalapp/libsignal-client';
|
||||||
|
import type { KyberPreKeyRecord } from '@signalapp/libsignal-client';
|
||||||
|
|
||||||
import * as Bytes from './Bytes';
|
import * as Bytes from './Bytes';
|
||||||
import { constantTimeEqual } from './Crypto';
|
import { constantTimeEqual } from './Crypto';
|
||||||
|
@ -59,6 +60,40 @@ export function generatePreKey(keyId: number): CompatPreKeyType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateKyberPreKey(
|
||||||
|
identityKeyPair: KeyPairType,
|
||||||
|
keyId: number
|
||||||
|
): KyberPreKeyRecord {
|
||||||
|
if (!isNonNegativeInteger(keyId)) {
|
||||||
|
throw new TypeError(
|
||||||
|
`generateKyberPreKey: Invalid argument for keyId: ${keyId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(identityKeyPair.privKey instanceof Uint8Array) ||
|
||||||
|
identityKeyPair.privKey.byteLength !== 32 ||
|
||||||
|
!(identityKeyPair.pubKey instanceof Uint8Array) ||
|
||||||
|
identityKeyPair.pubKey.byteLength !== 33
|
||||||
|
) {
|
||||||
|
throw new TypeError(
|
||||||
|
'generateKyberPreKey: Invalid argument for identityKeyPair'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyPair = client.KEMKeyPair.generate();
|
||||||
|
const signature = calculateSignature(
|
||||||
|
identityKeyPair.privKey,
|
||||||
|
keyPair.getPublicKey().serialize()
|
||||||
|
);
|
||||||
|
return client.KyberPreKeyRecord.new(
|
||||||
|
keyId,
|
||||||
|
Date.now(),
|
||||||
|
keyPair,
|
||||||
|
Buffer.from(signature)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function generateKeyPair(): KeyPairType {
|
export function generateKeyPair(): KeyPairType {
|
||||||
const privKey = client.PrivateKey.generate();
|
const privKey = client.PrivateKey.generate();
|
||||||
const pubKey = privKey.getPublicKey();
|
const pubKey = privKey.getPublicKey();
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { isNumber } from 'lodash';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Direction,
|
Direction,
|
||||||
|
KyberPreKeyRecord,
|
||||||
PreKeyRecord,
|
PreKeyRecord,
|
||||||
ProtocolAddress,
|
ProtocolAddress,
|
||||||
SenderKeyRecord,
|
SenderKeyRecord,
|
||||||
|
@ -16,6 +17,7 @@ import type {
|
||||||
} from '@signalapp/libsignal-client';
|
} from '@signalapp/libsignal-client';
|
||||||
import {
|
import {
|
||||||
IdentityKeyStore,
|
IdentityKeyStore,
|
||||||
|
KyberPreKeyStore,
|
||||||
PreKeyStore,
|
PreKeyStore,
|
||||||
PrivateKey,
|
PrivateKey,
|
||||||
PublicKey,
|
PublicKey,
|
||||||
|
@ -23,7 +25,6 @@ import {
|
||||||
SessionStore,
|
SessionStore,
|
||||||
SignedPreKeyStore,
|
SignedPreKeyStore,
|
||||||
} from '@signalapp/libsignal-client';
|
} from '@signalapp/libsignal-client';
|
||||||
import { freezePreKey, freezeSignedPreKey } from './SignalProtocolStore';
|
|
||||||
import { Address } from './types/Address';
|
import { Address } from './types/Address';
|
||||||
import { QualifiedAddress } from './types/QualifiedAddress';
|
import { QualifiedAddress } from './types/QualifiedAddress';
|
||||||
import type { UUID } from './types/UUID';
|
import type { UUID } from './types/UUID';
|
||||||
|
@ -187,12 +188,8 @@ export class PreKeys extends PreKeyStore {
|
||||||
this.ourUuid = ourUuid;
|
this.ourUuid = ourUuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
async savePreKey(id: number, record: PreKeyRecord): Promise<void> {
|
async savePreKey(): Promise<void> {
|
||||||
await window.textsecure.storage.protocol.storePreKey(
|
throw new Error('savePreKey: Should not be called by libsignal!');
|
||||||
this.ourUuid,
|
|
||||||
id,
|
|
||||||
freezePreKey(record)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPreKey(id: number): Promise<PreKeyRecord> {
|
async getPreKey(id: number): Promise<PreKeyRecord> {
|
||||||
|
@ -209,7 +206,41 @@ export class PreKeys extends PreKeyStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
async removePreKey(id: number): Promise<void> {
|
async removePreKey(id: number): Promise<void> {
|
||||||
await window.textsecure.storage.protocol.removePreKey(this.ourUuid, id);
|
await window.textsecure.storage.protocol.removePreKeys(this.ourUuid, [id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KyberPreKeys extends KyberPreKeyStore {
|
||||||
|
private readonly ourUuid: UUID;
|
||||||
|
|
||||||
|
constructor({ ourUuid }: PreKeysOptions) {
|
||||||
|
super();
|
||||||
|
this.ourUuid = ourUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveKyberPreKey(): Promise<void> {
|
||||||
|
throw new Error('saveKyberPreKey: Should not be called by libsignal!');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getKyberPreKey(id: number): Promise<KyberPreKeyRecord> {
|
||||||
|
const kyberPreKey =
|
||||||
|
await window.textsecure.storage.protocol.loadKyberPreKey(
|
||||||
|
this.ourUuid,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (kyberPreKey === undefined) {
|
||||||
|
throw new Error(`getKyberPreKey: KyberPreKey ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return kyberPreKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async markKyberPreKeyUsed(id: number): Promise<void> {
|
||||||
|
await window.textsecure.storage.protocol.maybeRemoveKyberPreKey(
|
||||||
|
this.ourUuid,
|
||||||
|
id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,16 +303,8 @@ export class SignedPreKeys extends SignedPreKeyStore {
|
||||||
this.ourUuid = ourUuid;
|
this.ourUuid = ourUuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSignedPreKey(
|
async saveSignedPreKey(): Promise<void> {
|
||||||
id: number,
|
throw new Error('saveSignedPreKey: Should not be called by libsignal!');
|
||||||
record: SignedPreKeyRecord
|
|
||||||
): Promise<void> {
|
|
||||||
await window.textsecure.storage.protocol.storeSignedPreKey(
|
|
||||||
this.ourUuid,
|
|
||||||
id,
|
|
||||||
freezeSignedPreKey(record),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSignedPreKey(id: number): Promise<SignedPreKeyRecord> {
|
async getSignedPreKey(id: number): Promise<SignedPreKeyRecord> {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { EventEmitter } from 'events';
|
||||||
import {
|
import {
|
||||||
Direction,
|
Direction,
|
||||||
IdentityKeyPair,
|
IdentityKeyPair,
|
||||||
|
KyberPreKeyRecord,
|
||||||
PreKeyRecord,
|
PreKeyRecord,
|
||||||
PrivateKey,
|
PrivateKey,
|
||||||
PublicKey,
|
PublicKey,
|
||||||
|
@ -32,6 +33,7 @@ import type {
|
||||||
IdentityKeyType,
|
IdentityKeyType,
|
||||||
IdentityKeyIdType,
|
IdentityKeyIdType,
|
||||||
KeyPairType,
|
KeyPairType,
|
||||||
|
KyberPreKeyType,
|
||||||
OuterSignedPrekeyType,
|
OuterSignedPrekeyType,
|
||||||
PniKeyMaterialType,
|
PniKeyMaterialType,
|
||||||
PniSignatureMessageType,
|
PniSignatureMessageType,
|
||||||
|
@ -46,6 +48,7 @@ import type {
|
||||||
SignedPreKeyType,
|
SignedPreKeyType,
|
||||||
UnprocessedType,
|
UnprocessedType,
|
||||||
UnprocessedUpdateType,
|
UnprocessedUpdateType,
|
||||||
|
CompatPreKeyType,
|
||||||
} from './textsecure/Types.d';
|
} from './textsecure/Types.d';
|
||||||
import type { RemoveAllConfiguration } from './types/RemoveAllConfiguration';
|
import type { RemoveAllConfiguration } from './types/RemoveAllConfiguration';
|
||||||
import type { UUIDStringType } from './types/UUID';
|
import type { UUIDStringType } from './types/UUID';
|
||||||
|
@ -57,8 +60,13 @@ import * as log from './logging/log';
|
||||||
import * as Errors from './types/errors';
|
import * as Errors from './types/errors';
|
||||||
import { MINUTE } from './util/durations';
|
import { MINUTE } from './util/durations';
|
||||||
import { conversationJobQueue } from './jobs/conversationJobQueue';
|
import { conversationJobQueue } from './jobs/conversationJobQueue';
|
||||||
|
import {
|
||||||
|
KYBER_KEY_ID_KEY,
|
||||||
|
SIGNED_PRE_KEY_ID_KEY,
|
||||||
|
} from './textsecure/AccountManager';
|
||||||
|
|
||||||
const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds
|
const TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds
|
||||||
|
const LOW_KEYS_THRESHOLD = 25;
|
||||||
|
|
||||||
const VerifiedStatus = {
|
const VerifiedStatus = {
|
||||||
DEFAULT: 0,
|
DEFAULT: 0,
|
||||||
|
@ -103,6 +111,7 @@ type CacheEntryType<DBType, HydratedType> =
|
||||||
| { hydrated: true; fromDB: DBType; item: HydratedType };
|
| { hydrated: true; fromDB: DBType; item: HydratedType };
|
||||||
|
|
||||||
type MapFields =
|
type MapFields =
|
||||||
|
| 'kyberPreKeys'
|
||||||
| 'identityKeys'
|
| 'identityKeys'
|
||||||
| 'preKeys'
|
| 'preKeys'
|
||||||
| 'senderKeys'
|
| 'senderKeys'
|
||||||
|
@ -226,6 +235,11 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
CacheEntryType<IdentityKeyType, PublicKey>
|
CacheEntryType<IdentityKeyType, PublicKey>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
kyberPreKeys?: Map<
|
||||||
|
PreKeyIdType,
|
||||||
|
CacheEntryType<KyberPreKeyType, KyberPreKeyRecord>
|
||||||
|
>;
|
||||||
|
|
||||||
senderKeys?: Map<SenderKeyIdType, SenderKeyCacheEntry>;
|
senderKeys?: Map<SenderKeyIdType, SenderKeyCacheEntry>;
|
||||||
|
|
||||||
sessions?: Map<SessionIdType, SessionCacheEntry>;
|
sessions?: Map<SessionIdType, SessionCacheEntry>;
|
||||||
|
@ -290,6 +304,11 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
'identityKeys',
|
'identityKeys',
|
||||||
window.Signal.Data.getAllIdentityKeys()
|
window.Signal.Data.getAllIdentityKeys()
|
||||||
),
|
),
|
||||||
|
_fillCaches<string, KyberPreKeyType, KyberPreKeyRecord>(
|
||||||
|
this,
|
||||||
|
'kyberPreKeys',
|
||||||
|
window.Signal.Data.getAllKyberPreKeys()
|
||||||
|
),
|
||||||
_fillCaches<string, SessionType, SessionRecord>(
|
_fillCaches<string, SessionType, SessionRecord>(
|
||||||
this,
|
this,
|
||||||
'sessions',
|
'sessions',
|
||||||
|
@ -321,6 +340,190 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
return this.ourRegistrationIds.get(ourUuid.toString());
|
return this.ourRegistrationIds.get(ourUuid.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getKeyId(ourUuid: UUID, keyId: number): PreKeyIdType {
|
||||||
|
return `${ourUuid.toString()}:${keyId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// KyberPreKeys
|
||||||
|
|
||||||
|
private _getKyberPreKeyEntry(
|
||||||
|
id: PreKeyIdType,
|
||||||
|
logContext: string
|
||||||
|
):
|
||||||
|
| { hydrated: true; fromDB: KyberPreKeyType; item: KyberPreKeyRecord }
|
||||||
|
| undefined {
|
||||||
|
if (!this.kyberPreKeys) {
|
||||||
|
throw new Error(`${logContext}: this.kyberPreKeys not yet cached!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = this.kyberPreKeys.get(id);
|
||||||
|
if (!entry) {
|
||||||
|
log.error(`${logContext}: Failed to fetch kyber prekey: ${id}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.hydrated) {
|
||||||
|
log.info(
|
||||||
|
`${logContext}: Successfully fetched kyber prekey (cache hit): ${id}`
|
||||||
|
);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = KyberPreKeyRecord.deserialize(Buffer.from(entry.fromDB.data));
|
||||||
|
const newEntry = {
|
||||||
|
hydrated: true as const,
|
||||||
|
fromDB: entry.fromDB,
|
||||||
|
item,
|
||||||
|
};
|
||||||
|
this.kyberPreKeys.set(id, newEntry);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`${logContext}: Successfully fetched kyberPreKey (cache miss): ${id}`
|
||||||
|
);
|
||||||
|
return newEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadKyberPreKey(
|
||||||
|
ourUuid: UUID,
|
||||||
|
keyId: number
|
||||||
|
): Promise<KyberPreKeyRecord | undefined> {
|
||||||
|
const id: PreKeyIdType = this._getKeyId(ourUuid, keyId);
|
||||||
|
const entry = this._getKyberPreKeyEntry(id, 'loadKyberPreKey');
|
||||||
|
|
||||||
|
return entry?.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadKyberPreKeys(
|
||||||
|
ourUuid: UUID,
|
||||||
|
{ isLastResort }: { isLastResort: boolean }
|
||||||
|
): Array<KyberPreKeyType> {
|
||||||
|
if (!this.kyberPreKeys) {
|
||||||
|
throw new Error('loadKyberPreKeys: this.kyberPreKeys not yet cached!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arguments.length > 2) {
|
||||||
|
throw new Error('loadKyberPreKeys takes two arguments');
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Array.from(this.kyberPreKeys.values());
|
||||||
|
return entries
|
||||||
|
.map(item => item.fromDB)
|
||||||
|
.filter(
|
||||||
|
item =>
|
||||||
|
item.ourUuid === ourUuid.toString() &&
|
||||||
|
item.isLastResort === isLastResort
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmKyberPreKey(ourUuid: UUID, keyId: number): Promise<void> {
|
||||||
|
const kyberPreKeyCache = this.kyberPreKeys;
|
||||||
|
if (!kyberPreKeyCache) {
|
||||||
|
throw new Error('storeKyberPreKey: this.kyberPreKeys not yet cached!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const id: PreKeyIdType = this._getKeyId(ourUuid, keyId);
|
||||||
|
const item = kyberPreKeyCache.get(id);
|
||||||
|
if (!item) {
|
||||||
|
throw new Error(`confirmKyberPreKey: missing kyber prekey ${id}!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmedItem = {
|
||||||
|
...item,
|
||||||
|
fromDB: {
|
||||||
|
...item.fromDB,
|
||||||
|
isConfirmed: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await window.Signal.Data.createOrUpdateKyberPreKey(confirmedItem.fromDB);
|
||||||
|
kyberPreKeyCache.set(id, confirmedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeKyberPreKeys(
|
||||||
|
ourUuid: UUID,
|
||||||
|
keys: Array<Omit<KyberPreKeyType, 'id'>>
|
||||||
|
): Promise<void> {
|
||||||
|
const kyberPreKeyCache = this.kyberPreKeys;
|
||||||
|
if (!kyberPreKeyCache) {
|
||||||
|
throw new Error('storeKyberPreKey: this.kyberPreKeys not yet cached!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const toSave: Array<KyberPreKeyType> = [];
|
||||||
|
|
||||||
|
keys.forEach(key => {
|
||||||
|
const id: PreKeyIdType = this._getKeyId(ourUuid, key.keyId);
|
||||||
|
if (kyberPreKeyCache.has(id)) {
|
||||||
|
throw new Error(`storeKyberPreKey: kyber prekey ${id} already exists!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const kyberPreKey = {
|
||||||
|
id,
|
||||||
|
|
||||||
|
createdAt: key.createdAt,
|
||||||
|
data: key.data,
|
||||||
|
isConfirmed: key.isConfirmed,
|
||||||
|
isLastResort: key.isLastResort,
|
||||||
|
keyId: key.keyId,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
toSave.push(kyberPreKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
await window.Signal.Data.bulkAddKyberPreKeys(toSave);
|
||||||
|
toSave.forEach(kyberPreKey => {
|
||||||
|
kyberPreKeyCache.set(kyberPreKey.id, {
|
||||||
|
hydrated: false,
|
||||||
|
fromDB: kyberPreKey,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async maybeRemoveKyberPreKey(ourUuid: UUID, keyId: number): Promise<void> {
|
||||||
|
const id: PreKeyIdType = this._getKeyId(ourUuid, keyId);
|
||||||
|
const entry = this._getKyberPreKeyEntry(id, 'maybeRemoveKyberPreKey');
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entry.fromDB.isLastResort) {
|
||||||
|
log.info(
|
||||||
|
`maybeRemoveKyberPreKey: Not removing kyber prekey ${id}; it's a last resort key`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.removeKyberPreKeys(ourUuid, [keyId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeKyberPreKeys(
|
||||||
|
ourUuid: UUID,
|
||||||
|
keyIds: Array<number>
|
||||||
|
): Promise<void> {
|
||||||
|
const kyberPreKeyCache = this.kyberPreKeys;
|
||||||
|
if (!kyberPreKeyCache) {
|
||||||
|
throw new Error('removeKyberPreKeys: this.kyberPreKeys not yet cached!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = keyIds.map(keyId => this._getKeyId(ourUuid, keyId));
|
||||||
|
|
||||||
|
await window.Signal.Data.removeKyberPreKeyById(ids);
|
||||||
|
ids.forEach(id => {
|
||||||
|
kyberPreKeyCache.delete(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (kyberPreKeyCache.size < LOW_KEYS_THRESHOLD) {
|
||||||
|
this.emitLowKeys(ourUuid, `removeKyberPreKeys@${kyberPreKeyCache.size}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearKyberPreKeyStore(): Promise<void> {
|
||||||
|
if (this.kyberPreKeys) {
|
||||||
|
this.kyberPreKeys.clear();
|
||||||
|
}
|
||||||
|
await window.Signal.Data.removeAllKyberPreKeys();
|
||||||
|
}
|
||||||
|
|
||||||
// PreKeys
|
// PreKeys
|
||||||
|
|
||||||
async loadPreKey(
|
async loadPreKey(
|
||||||
|
@ -331,8 +534,7 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
throw new Error('loadPreKey: this.preKeys not yet cached!');
|
throw new Error('loadPreKey: this.preKeys not yet cached!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const id: PreKeyIdType = `${ourUuid.toString()}:${keyId}`;
|
const id: PreKeyIdType = this._getKeyId(ourUuid, keyId);
|
||||||
|
|
||||||
const entry = this.preKeys.get(id);
|
const entry = this.preKeys.get(id);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
log.error('Failed to fetch prekey:', id);
|
log.error('Failed to fetch prekey:', id);
|
||||||
|
@ -354,53 +556,76 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
async storePreKey(
|
loadPreKeys(ourUuid: UUID): Array<PreKeyType> {
|
||||||
ourUuid: UUID,
|
|
||||||
keyId: number,
|
|
||||||
keyPair: KeyPairType
|
|
||||||
): Promise<void> {
|
|
||||||
if (!this.preKeys) {
|
if (!this.preKeys) {
|
||||||
|
throw new Error('loadPreKeys: this.preKeys not yet cached!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arguments.length > 1) {
|
||||||
|
throw new Error('loadPreKeys takes one argument');
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Array.from(this.preKeys.values());
|
||||||
|
return entries
|
||||||
|
.map(item => item.fromDB)
|
||||||
|
.filter(item => item.ourUuid === ourUuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
async storePreKeys(
|
||||||
|
ourUuid: UUID,
|
||||||
|
keys: Array<CompatPreKeyType>
|
||||||
|
): Promise<void> {
|
||||||
|
const preKeyCache = this.preKeys;
|
||||||
|
if (!preKeyCache) {
|
||||||
throw new Error('storePreKey: this.preKeys not yet cached!');
|
throw new Error('storePreKey: this.preKeys not yet cached!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const id: PreKeyIdType = `${ourUuid.toString()}:${keyId}`;
|
const now = Date.now();
|
||||||
if (this.preKeys.has(id)) {
|
const toSave: Array<PreKeyType> = [];
|
||||||
throw new Error(`storePreKey: prekey ${id} already exists!`);
|
keys.forEach(key => {
|
||||||
}
|
const id: PreKeyIdType = this._getKeyId(ourUuid, key.keyId);
|
||||||
|
|
||||||
const fromDB = {
|
if (preKeyCache.has(id)) {
|
||||||
id,
|
throw new Error(`storePreKeys: prekey ${id} already exists!`);
|
||||||
keyId,
|
}
|
||||||
ourUuid: ourUuid.toString(),
|
|
||||||
publicKey: keyPair.pubKey,
|
|
||||||
privateKey: keyPair.privKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
await window.Signal.Data.createOrUpdatePreKey(fromDB);
|
const preKey = {
|
||||||
this.preKeys.set(id, {
|
id,
|
||||||
hydrated: false,
|
keyId: key.keyId,
|
||||||
fromDB,
|
ourUuid: ourUuid.toString(),
|
||||||
|
publicKey: key.keyPair.pubKey,
|
||||||
|
privateKey: key.keyPair.privKey,
|
||||||
|
createdAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
toSave.push(preKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
await window.Signal.Data.bulkAddPreKeys(toSave);
|
||||||
|
toSave.forEach(preKey => {
|
||||||
|
preKeyCache.set(preKey.id, {
|
||||||
|
hydrated: false,
|
||||||
|
fromDB: preKey,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async removePreKey(ourUuid: UUID, keyId: number): Promise<void> {
|
async removePreKeys(ourUuid: UUID, keyIds: Array<number>): Promise<void> {
|
||||||
if (!this.preKeys) {
|
const preKeyCache = this.preKeys;
|
||||||
throw new Error('removePreKey: this.preKeys not yet cached!');
|
if (!preKeyCache) {
|
||||||
|
throw new Error('removePreKeys: this.preKeys not yet cached!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const id: PreKeyIdType = `${ourUuid.toString()}:${keyId}`;
|
const ids = keyIds.map(keyId => this._getKeyId(ourUuid, keyId));
|
||||||
|
|
||||||
try {
|
await window.Signal.Data.removePreKeyById(ids);
|
||||||
this.emit('removePreKey', ourUuid);
|
ids.forEach(id => {
|
||||||
} catch (error) {
|
preKeyCache.delete(id);
|
||||||
log.error(
|
});
|
||||||
'removePreKey error triggering removePreKey:',
|
|
||||||
Errors.toLogFormat(error)
|
if (preKeyCache.size < LOW_KEYS_THRESHOLD) {
|
||||||
);
|
this.emitLowKeys(ourUuid, `removePreKeys@${preKeyCache.size}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.preKeys.delete(id);
|
|
||||||
await window.Signal.Data.removePreKeyById(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearPreKeyStore(): Promise<void> {
|
async clearPreKeyStore(): Promise<void> {
|
||||||
|
@ -443,9 +668,7 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSignedPreKeys(
|
loadSignedPreKeys(ourUuid: UUID): Array<OuterSignedPrekeyType> {
|
||||||
ourUuid: UUID
|
|
||||||
): Promise<Array<OuterSignedPrekeyType>> {
|
|
||||||
if (!this.signedPreKeys) {
|
if (!this.signedPreKeys) {
|
||||||
throw new Error('loadSignedPreKeys: this.signedPreKeys not yet cached!');
|
throw new Error('loadSignedPreKeys: this.signedPreKeys not yet cached!');
|
||||||
}
|
}
|
||||||
|
@ -469,8 +692,30 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that this is also called in update scenarios, for confirming that signed prekeys
|
async confirmSignedPreKey(ourUuid: UUID, keyId: number): Promise<void> {
|
||||||
// have indeed been accepted by the server.
|
const signedPreKeyCache = this.signedPreKeys;
|
||||||
|
if (!signedPreKeyCache) {
|
||||||
|
throw new Error('storeKyberPreKey: this.signedPreKeys not yet cached!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const id: PreKeyIdType = this._getKeyId(ourUuid, keyId);
|
||||||
|
const item = signedPreKeyCache.get(id);
|
||||||
|
if (!item) {
|
||||||
|
throw new Error(`confirmSignedPreKey: missing prekey ${id}!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmedItem = {
|
||||||
|
...item,
|
||||||
|
fromDB: {
|
||||||
|
...item.fromDB,
|
||||||
|
confirmed: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await window.Signal.Data.createOrUpdateSignedPreKey(confirmedItem.fromDB);
|
||||||
|
signedPreKeyCache.set(id, confirmedItem);
|
||||||
|
}
|
||||||
|
|
||||||
async storeSignedPreKey(
|
async storeSignedPreKey(
|
||||||
ourUuid: UUID,
|
ourUuid: UUID,
|
||||||
keyId: number,
|
keyId: number,
|
||||||
|
@ -482,7 +727,7 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
throw new Error('storeSignedPreKey: this.signedPreKeys not yet cached!');
|
throw new Error('storeSignedPreKey: this.signedPreKeys not yet cached!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const id: SignedPreKeyIdType = `${ourUuid.toString()}:${keyId}`;
|
const id: SignedPreKeyIdType = this._getKeyId(ourUuid, keyId);
|
||||||
|
|
||||||
const fromDB = {
|
const fromDB = {
|
||||||
id,
|
id,
|
||||||
|
@ -501,14 +746,21 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeSignedPreKey(ourUuid: UUID, keyId: number): Promise<void> {
|
async removeSignedPreKeys(
|
||||||
if (!this.signedPreKeys) {
|
ourUuid: UUID,
|
||||||
|
keyIds: Array<number>
|
||||||
|
): Promise<void> {
|
||||||
|
const signedPreKeyCache = this.signedPreKeys;
|
||||||
|
if (!signedPreKeyCache) {
|
||||||
throw new Error('removeSignedPreKey: this.signedPreKeys not yet cached!');
|
throw new Error('removeSignedPreKey: this.signedPreKeys not yet cached!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const id: SignedPreKeyIdType = `${ourUuid.toString()}:${keyId}`;
|
const ids = keyIds.map(keyId => this._getKeyId(ourUuid, keyId));
|
||||||
this.signedPreKeys.delete(id);
|
|
||||||
await window.Signal.Data.removeSignedPreKeyById(id);
|
await window.Signal.Data.removeSignedPreKeyById(ids);
|
||||||
|
ids.forEach(id => {
|
||||||
|
signedPreKeyCache.delete(id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearSignedPreKeysStore(): Promise<void> {
|
async clearSignedPreKeysStore(): Promise<void> {
|
||||||
|
@ -2187,6 +2439,13 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.kyberPreKeys) {
|
||||||
|
for (const key of this.kyberPreKeys.keys()) {
|
||||||
|
if (key.startsWith(preKeyPrefix)) {
|
||||||
|
this.kyberPreKeys.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update database
|
// Update database
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
@ -2200,6 +2459,7 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
),
|
),
|
||||||
window.Signal.Data.removePreKeysByUuid(oldPni.toString()),
|
window.Signal.Data.removePreKeysByUuid(oldPni.toString()),
|
||||||
window.Signal.Data.removeSignedPreKeysByUuid(oldPni.toString()),
|
window.Signal.Data.removeSignedPreKeysByUuid(oldPni.toString()),
|
||||||
|
window.Signal.Data.removeKyberPreKeysByUuid(oldPni.toString()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2207,11 +2467,13 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
pni: UUID,
|
pni: UUID,
|
||||||
{
|
{
|
||||||
identityKeyPair: identityBytes,
|
identityKeyPair: identityBytes,
|
||||||
|
lastResortKyberPreKey: lastResortKyberPreKeyBytes,
|
||||||
signedPreKey: signedPreKeyBytes,
|
signedPreKey: signedPreKeyBytes,
|
||||||
registrationId,
|
registrationId,
|
||||||
}: PniKeyMaterialType
|
}: PniKeyMaterialType
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
log.info(`SignalProtocolStore.updateOurPniKeyMaterial(${pni})`);
|
const logId = `SignalProtocolStore.updateOurPniKeyMaterial(${pni})`;
|
||||||
|
log.info(`${logId}: starting...`);
|
||||||
|
|
||||||
const identityKeyPair = IdentityKeyPair.deserialize(
|
const identityKeyPair = IdentityKeyPair.deserialize(
|
||||||
Buffer.from(identityBytes)
|
Buffer.from(identityBytes)
|
||||||
|
@ -2219,6 +2481,9 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
const signedPreKey = SignedPreKeyRecord.deserialize(
|
const signedPreKey = SignedPreKeyRecord.deserialize(
|
||||||
Buffer.from(signedPreKeyBytes)
|
Buffer.from(signedPreKeyBytes)
|
||||||
);
|
);
|
||||||
|
const lastResortKyberPreKey = lastResortKyberPreKeyBytes
|
||||||
|
? KyberPreKeyRecord.deserialize(Buffer.from(lastResortKyberPreKeyBytes))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const { storage } = window;
|
const { storage } = window;
|
||||||
|
|
||||||
|
@ -2245,6 +2510,11 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
...(storage.get('registrationIdMap') || {}),
|
...(storage.get('registrationIdMap') || {}),
|
||||||
[pni.toString()]: registrationId,
|
[pni.toString()]: registrationId,
|
||||||
}),
|
}),
|
||||||
|
async () => {
|
||||||
|
const newId = signedPreKey.id() + 1;
|
||||||
|
log.warn(`${logId}: Updating next signed pre key id to ${newId}`);
|
||||||
|
await storage.put(SIGNED_PRE_KEY_ID_KEY[UUIDKind.PNI], newId);
|
||||||
|
},
|
||||||
this.storeSignedPreKey(
|
this.storeSignedPreKey(
|
||||||
pni,
|
pni,
|
||||||
signedPreKey.id(),
|
signedPreKey.id(),
|
||||||
|
@ -2255,6 +2525,26 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
true,
|
true,
|
||||||
signedPreKey.timestamp()
|
signedPreKey.timestamp()
|
||||||
),
|
),
|
||||||
|
async () => {
|
||||||
|
if (!lastResortKyberPreKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newId = lastResortKyberPreKey.id() + 1;
|
||||||
|
log.warn(`${logId}: Updating next kyber pre key id to ${newId}`);
|
||||||
|
await storage.put(KYBER_KEY_ID_KEY[UUIDKind.PNI], newId);
|
||||||
|
},
|
||||||
|
lastResortKyberPreKeyBytes && lastResortKyberPreKey
|
||||||
|
? this.storeKyberPreKeys(pni, [
|
||||||
|
{
|
||||||
|
createdAt: lastResortKyberPreKey.timestamp(),
|
||||||
|
data: lastResortKyberPreKeyBytes,
|
||||||
|
isConfirmed: true,
|
||||||
|
isLastResort: true,
|
||||||
|
keyId: lastResortKyberPreKey.id(),
|
||||||
|
ourUuid: pni.toString(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
: undefined,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2354,12 +2644,23 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
|
|
||||||
return Array.from(union.values());
|
return Array.from(union.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private emitLowKeys(ourUuid: UUID, source: string) {
|
||||||
|
const logId = `SignalProtocolStore.emitLowKeys/${source}:`;
|
||||||
|
try {
|
||||||
|
log.info(`${logId}: Emitting event`);
|
||||||
|
this.emit('lowKeys', ourUuid);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`${logId}: Error thrown from emit`, Errors.toLogFormat(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// EventEmitter types
|
// EventEmitter types
|
||||||
//
|
//
|
||||||
|
|
||||||
public override on(
|
public override on(
|
||||||
name: 'removePreKey',
|
name: 'lowKeys',
|
||||||
handler: (ourUuid: UUID) => unknown
|
handler: (ourUuid: UUID) => unknown
|
||||||
): this;
|
): this;
|
||||||
|
|
||||||
|
@ -2378,7 +2679,7 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
return super.on(eventName, listener);
|
return super.on(eventName, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override emit(name: 'removePreKey', ourUuid: UUID): boolean;
|
public override emit(name: 'lowKeys', ourUuid: UUID): boolean;
|
||||||
|
|
||||||
public override emit(
|
public override emit(
|
||||||
name: 'keychange',
|
name: 'keychange',
|
||||||
|
|
|
@ -107,7 +107,7 @@ import type {
|
||||||
} from './textsecure/messageReceiverEvents';
|
} from './textsecure/messageReceiverEvents';
|
||||||
import type { WebAPIType } from './textsecure/WebAPI';
|
import type { WebAPIType } from './textsecure/WebAPI';
|
||||||
import * as KeyChangeListener from './textsecure/KeyChangeListener';
|
import * as KeyChangeListener from './textsecure/KeyChangeListener';
|
||||||
import { RotateSignedPreKeyListener } from './textsecure/RotateSignedPreKeyListener';
|
import { UpdateKeysListener } from './textsecure/UpdateKeysListener';
|
||||||
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
|
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
|
||||||
import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff';
|
import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff';
|
||||||
import { AppViewType } from './state/ducks/app';
|
import { AppViewType } from './state/ducks/app';
|
||||||
|
@ -599,10 +599,17 @@ export async function startApp(): Promise<void> {
|
||||||
);
|
);
|
||||||
|
|
||||||
KeyChangeListener.init(window.textsecure.storage.protocol);
|
KeyChangeListener.init(window.textsecure.storage.protocol);
|
||||||
window.textsecure.storage.protocol.on('removePreKey', (ourUuid: UUID) => {
|
window.textsecure.storage.protocol.on(
|
||||||
const uuidKind = window.textsecure.storage.user.getOurUuidKind(ourUuid);
|
'lowKeys',
|
||||||
void window.getAccountManager().refreshPreKeys(uuidKind);
|
throttle(
|
||||||
});
|
(ourUuid: UUID) => {
|
||||||
|
const uuidKind = window.textsecure.storage.user.getOurUuidKind(ourUuid);
|
||||||
|
drop(window.getAccountManager().maybeUpdateKeys(uuidKind));
|
||||||
|
},
|
||||||
|
durations.MINUTE,
|
||||||
|
{ trailing: true, leading: false }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
window.textsecure.storage.protocol.on('removeAllData', () => {
|
window.textsecure.storage.protocol.on('removeAllData', () => {
|
||||||
window.reduxActions.stories.removeAllStories();
|
window.reduxActions.stories.removeAllStories();
|
||||||
|
@ -876,6 +883,15 @@ export async function startApp(): Promise<void> {
|
||||||
await window.storage.remove('remoteBuildExpiration');
|
await window.storage.remove('remoteBuildExpiration');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window.isBeforeVersion(lastVersion, '6.25.0-alpha')) {
|
||||||
|
await removeStorageKeyJobQueue.add({
|
||||||
|
key: 'nextSignedKeyRotationTime',
|
||||||
|
});
|
||||||
|
await removeStorageKeyJobQueue.add({
|
||||||
|
key: 'signedKeyRotationRejected',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (window.isBeforeVersion(lastVersion, '6.22.0-alpha')) {
|
if (window.isBeforeVersion(lastVersion, '6.22.0-alpha')) {
|
||||||
const formattingWarningShown = window.storage.get(
|
const formattingWarningShown = window.storage.get(
|
||||||
'formattingWarningShown',
|
'formattingWarningShown',
|
||||||
|
@ -2079,7 +2095,7 @@ export async function startApp(): Promise<void> {
|
||||||
window.ConversationController.onEmpty();
|
window.ConversationController.onEmpty();
|
||||||
|
|
||||||
// Start listeners here, after we get through our queue.
|
// Start listeners here, after we get through our queue.
|
||||||
RotateSignedPreKeyListener.init(window.Whisper.events, newVersion);
|
UpdateKeysListener.init(window.Whisper.events, newVersion);
|
||||||
|
|
||||||
profileKeyResponseQueue.start();
|
profileKeyResponseQueue.start();
|
||||||
lightSessionResetQueue.start();
|
lightSessionResetQueue.start();
|
||||||
|
|
|
@ -7,7 +7,12 @@ import { JobQueue } from './JobQueue';
|
||||||
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
|
||||||
|
|
||||||
const removeStorageKeyJobDataSchema = z.object({
|
const removeStorageKeyJobDataSchema = z.object({
|
||||||
key: z.enum(['senderCertificateWithUuid', 'challenge:retry-message-ids']),
|
key: z.enum([
|
||||||
|
'challenge:retry-message-ids',
|
||||||
|
'nextSignedKeyRotationTime',
|
||||||
|
'senderCertificateWithUuid',
|
||||||
|
'signedKeyRotationRejected',
|
||||||
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
type RemoveStorageKeyJobData = z.infer<typeof removeStorageKeyJobDataSchema>;
|
type RemoveStorageKeyJobData = z.infer<typeof removeStorageKeyJobDataSchema>;
|
||||||
|
|
|
@ -1406,7 +1406,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
e.name === 'OutgoingMessageError' ||
|
e.name === 'OutgoingMessageError' ||
|
||||||
e.name === 'SendMessageNetworkError' ||
|
e.name === 'SendMessageNetworkError' ||
|
||||||
e.name === 'SendMessageChallengeError' ||
|
e.name === 'SendMessageChallengeError' ||
|
||||||
e.name === 'SignedPreKeyRotationError' ||
|
|
||||||
e.name === 'OutgoingIdentityKeyError'
|
e.name === 'OutgoingIdentityKeyError'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1472,7 +1471,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
e.name === 'OutgoingMessageError' ||
|
e.name === 'OutgoingMessageError' ||
|
||||||
e.name === 'SendMessageNetworkError' ||
|
e.name === 'SendMessageNetworkError' ||
|
||||||
e.name === 'SendMessageChallengeError' ||
|
e.name === 'SendMessageChallengeError' ||
|
||||||
e.name === 'SignedPreKeyRotationError' ||
|
|
||||||
e.name === 'OutgoingIdentityKeyError')
|
e.name === 'OutgoingIdentityKeyError')
|
||||||
);
|
);
|
||||||
this.set({ errors: errors[1] });
|
this.set({ errors: errors[1] });
|
||||||
|
@ -1591,7 +1589,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
// screen will show that we didn't send to these unregistered users.
|
// screen will show that we didn't send to these unregistered users.
|
||||||
const errorsToSave: Array<CustomError> = [];
|
const errorsToSave: Array<CustomError> = [];
|
||||||
|
|
||||||
let hadSignedPreKeyRotationError = false;
|
|
||||||
errors.forEach(error => {
|
errors.forEach(error => {
|
||||||
const conversation =
|
const conversation =
|
||||||
window.ConversationController.get(error.identifier) ||
|
window.ConversationController.get(error.identifier) ||
|
||||||
|
@ -1616,9 +1613,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
let shouldSaveError = true;
|
let shouldSaveError = true;
|
||||||
switch (error.name) {
|
switch (error.name) {
|
||||||
case 'SignedPreKeyRotationError':
|
|
||||||
hadSignedPreKeyRotationError = true;
|
|
||||||
break;
|
|
||||||
case 'OutgoingIdentityKeyError': {
|
case 'OutgoingIdentityKeyError': {
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
promises.push(conversation.getProfiles());
|
promises.push(conversation.getProfiles());
|
||||||
|
@ -1648,12 +1642,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hadSignedPreKeyRotationError) {
|
|
||||||
promises.push(
|
|
||||||
window.getAccountManager().rotateSignedPreKey(UUIDKind.ACI)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
attributesToUpdate.sendStateByConversationId = sendStateByConversationId;
|
attributesToUpdate.sendStateByConversationId = sendStateByConversationId;
|
||||||
// Only update the expirationStartTimestamp if we don't already have one set
|
// Only update the expirationStartTimestamp if we don't already have one set
|
||||||
if (!this.get('expirationStartTimestamp')) {
|
if (!this.get('expirationStartTimestamp')) {
|
||||||
|
|
|
@ -52,6 +52,8 @@ import type {
|
||||||
SignedPreKeyIdType,
|
SignedPreKeyIdType,
|
||||||
SignedPreKeyType,
|
SignedPreKeyType,
|
||||||
StoredSignedPreKeyType,
|
StoredSignedPreKeyType,
|
||||||
|
KyberPreKeyType,
|
||||||
|
StoredKyberPreKeyType,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
import { MINUTE } from '../util/durations';
|
import { MINUTE } from '../util/durations';
|
||||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
import { getMessageIdForLogging } from '../util/idForLogging';
|
||||||
|
@ -73,6 +75,11 @@ const exclusiveInterface: ClientExclusiveInterface = {
|
||||||
bulkAddIdentityKeys,
|
bulkAddIdentityKeys,
|
||||||
getAllIdentityKeys,
|
getAllIdentityKeys,
|
||||||
|
|
||||||
|
createOrUpdateKyberPreKey,
|
||||||
|
getKyberPreKeyById,
|
||||||
|
bulkAddKyberPreKeys,
|
||||||
|
getAllKyberPreKeys,
|
||||||
|
|
||||||
createOrUpdatePreKey,
|
createOrUpdatePreKey,
|
||||||
getPreKeyById,
|
getPreKeyById,
|
||||||
bulkAddPreKeys,
|
bulkAddPreKeys,
|
||||||
|
@ -248,6 +255,37 @@ async function getAllIdentityKeys(): Promise<Array<IdentityKeyType>> {
|
||||||
return keys.map(key => specToBytes(IDENTITY_KEY_SPEC, key));
|
return keys.map(key => specToBytes(IDENTITY_KEY_SPEC, key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kyber Pre Keys
|
||||||
|
|
||||||
|
const KYBER_PRE_KEY_SPEC = ['data'];
|
||||||
|
async function createOrUpdateKyberPreKey(data: KyberPreKeyType): Promise<void> {
|
||||||
|
const updated: StoredKyberPreKeyType = specFromBytes(
|
||||||
|
KYBER_PRE_KEY_SPEC,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
await channels.createOrUpdateKyberPreKey(updated);
|
||||||
|
}
|
||||||
|
async function getKyberPreKeyById(
|
||||||
|
id: PreKeyIdType
|
||||||
|
): Promise<KyberPreKeyType | undefined> {
|
||||||
|
const data = await channels.getPreKeyById(id);
|
||||||
|
|
||||||
|
return specToBytes(KYBER_PRE_KEY_SPEC, data);
|
||||||
|
}
|
||||||
|
async function bulkAddKyberPreKeys(
|
||||||
|
array: Array<KyberPreKeyType>
|
||||||
|
): Promise<void> {
|
||||||
|
const updated: Array<StoredKyberPreKeyType> = map(array, data =>
|
||||||
|
specFromBytes(KYBER_PRE_KEY_SPEC, data)
|
||||||
|
);
|
||||||
|
await channels.bulkAddKyberPreKeys(updated);
|
||||||
|
}
|
||||||
|
async function getAllKyberPreKeys(): Promise<Array<KyberPreKeyType>> {
|
||||||
|
const keys = await channels.getAllPreKeys();
|
||||||
|
|
||||||
|
return keys.map(key => specToBytes(KYBER_PRE_KEY_SPEC, key));
|
||||||
|
}
|
||||||
|
|
||||||
// Pre Keys
|
// Pre Keys
|
||||||
|
|
||||||
async function createOrUpdatePreKey(data: PreKeyType): Promise<void> {
|
async function createOrUpdatePreKey(data: PreKeyType): Promise<void> {
|
||||||
|
|
|
@ -109,21 +109,35 @@ export type MessageType = MessageAttributesType;
|
||||||
export type MessageTypeUnhydrated = {
|
export type MessageTypeUnhydrated = {
|
||||||
json: string;
|
json: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PreKeyIdType = `${UUIDStringType}:${number}`;
|
||||||
|
export type KyberPreKeyType = {
|
||||||
|
id: PreKeyIdType;
|
||||||
|
|
||||||
|
createdAt: number;
|
||||||
|
data: Uint8Array;
|
||||||
|
isConfirmed: boolean;
|
||||||
|
isLastResort: boolean;
|
||||||
|
keyId: number;
|
||||||
|
ourUuid: UUIDStringType;
|
||||||
|
};
|
||||||
|
export type StoredKyberPreKeyType = KyberPreKeyType & {
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
export type PreKeyType = {
|
export type PreKeyType = {
|
||||||
id: `${UUIDStringType}:${number}`;
|
id: PreKeyIdType;
|
||||||
|
|
||||||
|
createdAt: number;
|
||||||
keyId: number;
|
keyId: number;
|
||||||
ourUuid: UUIDStringType;
|
ourUuid: UUIDStringType;
|
||||||
privateKey: Uint8Array;
|
privateKey: Uint8Array;
|
||||||
publicKey: Uint8Array;
|
publicKey: Uint8Array;
|
||||||
};
|
};
|
||||||
export type StoredPreKeyType = {
|
|
||||||
id: `${UUIDStringType}:${number}`;
|
export type StoredPreKeyType = PreKeyType & {
|
||||||
keyId: number;
|
|
||||||
ourUuid: UUIDStringType;
|
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
};
|
};
|
||||||
export type PreKeyIdType = PreKeyType['id'];
|
|
||||||
export type ServerSearchResultMessageType = {
|
export type ServerSearchResultMessageType = {
|
||||||
json: string;
|
json: string;
|
||||||
|
|
||||||
|
@ -410,16 +424,24 @@ export type DataInterface = {
|
||||||
removeIdentityKeyById: (id: IdentityKeyIdType) => Promise<void>;
|
removeIdentityKeyById: (id: IdentityKeyIdType) => Promise<void>;
|
||||||
removeAllIdentityKeys: () => Promise<void>;
|
removeAllIdentityKeys: () => Promise<void>;
|
||||||
|
|
||||||
removePreKeyById: (id: PreKeyIdType) => Promise<void>;
|
removeKyberPreKeyById: (
|
||||||
|
id: PreKeyIdType | Array<PreKeyIdType>
|
||||||
|
) => Promise<void>;
|
||||||
|
removeKyberPreKeysByUuid: (uuid: UUIDStringType) => Promise<void>;
|
||||||
|
removeAllKyberPreKeys: () => Promise<void>;
|
||||||
|
|
||||||
|
removePreKeyById: (id: PreKeyIdType | Array<PreKeyIdType>) => Promise<void>;
|
||||||
removePreKeysByUuid: (uuid: UUIDStringType) => Promise<void>;
|
removePreKeysByUuid: (uuid: UUIDStringType) => Promise<void>;
|
||||||
removeAllPreKeys: () => Promise<void>;
|
removeAllPreKeys: () => Promise<void>;
|
||||||
|
|
||||||
removeSignedPreKeyById: (id: SignedPreKeyIdType) => Promise<void>;
|
removeSignedPreKeyById: (
|
||||||
|
id: SignedPreKeyIdType | Array<SignedPreKeyIdType>
|
||||||
|
) => Promise<void>;
|
||||||
removeSignedPreKeysByUuid: (uuid: UUIDStringType) => Promise<void>;
|
removeSignedPreKeysByUuid: (uuid: UUIDStringType) => Promise<void>;
|
||||||
removeAllSignedPreKeys: () => Promise<void>;
|
removeAllSignedPreKeys: () => Promise<void>;
|
||||||
|
|
||||||
removeAllItems: () => Promise<void>;
|
removeAllItems: () => Promise<void>;
|
||||||
removeItemById: (id: ItemKeyType) => Promise<void>;
|
removeItemById: (id: ItemKeyType | Array<ItemKeyType>) => Promise<void>;
|
||||||
|
|
||||||
createOrUpdateSenderKey: (key: SenderKeyType) => Promise<void>;
|
createOrUpdateSenderKey: (key: SenderKeyType) => Promise<void>;
|
||||||
getSenderKeyById: (id: SenderKeyIdType) => Promise<SenderKeyType | undefined>;
|
getSenderKeyById: (id: SenderKeyIdType) => Promise<SenderKeyType | undefined>;
|
||||||
|
@ -822,6 +844,13 @@ export type ServerInterface = DataInterface & {
|
||||||
bulkAddIdentityKeys: (array: Array<StoredIdentityKeyType>) => Promise<void>;
|
bulkAddIdentityKeys: (array: Array<StoredIdentityKeyType>) => Promise<void>;
|
||||||
getAllIdentityKeys: () => Promise<Array<StoredIdentityKeyType>>;
|
getAllIdentityKeys: () => Promise<Array<StoredIdentityKeyType>>;
|
||||||
|
|
||||||
|
createOrUpdateKyberPreKey: (data: StoredKyberPreKeyType) => Promise<void>;
|
||||||
|
getKyberPreKeyById: (
|
||||||
|
id: PreKeyIdType
|
||||||
|
) => Promise<StoredKyberPreKeyType | undefined>;
|
||||||
|
bulkAddKyberPreKeys: (array: Array<StoredKyberPreKeyType>) => Promise<void>;
|
||||||
|
getAllKyberPreKeys: () => Promise<Array<StoredKyberPreKeyType>>;
|
||||||
|
|
||||||
createOrUpdatePreKey: (data: StoredPreKeyType) => Promise<void>;
|
createOrUpdatePreKey: (data: StoredPreKeyType) => Promise<void>;
|
||||||
getPreKeyById: (id: PreKeyIdType) => Promise<StoredPreKeyType | undefined>;
|
getPreKeyById: (id: PreKeyIdType) => Promise<StoredPreKeyType | undefined>;
|
||||||
bulkAddPreKeys: (array: Array<StoredPreKeyType>) => Promise<void>;
|
bulkAddPreKeys: (array: Array<StoredPreKeyType>) => Promise<void>;
|
||||||
|
@ -901,6 +930,13 @@ export type ClientExclusiveInterface = {
|
||||||
bulkAddIdentityKeys: (array: Array<IdentityKeyType>) => Promise<void>;
|
bulkAddIdentityKeys: (array: Array<IdentityKeyType>) => Promise<void>;
|
||||||
getAllIdentityKeys: () => Promise<Array<IdentityKeyType>>;
|
getAllIdentityKeys: () => Promise<Array<IdentityKeyType>>;
|
||||||
|
|
||||||
|
createOrUpdateKyberPreKey: (data: KyberPreKeyType) => Promise<void>;
|
||||||
|
getKyberPreKeyById: (
|
||||||
|
id: PreKeyIdType
|
||||||
|
) => Promise<KyberPreKeyType | undefined>;
|
||||||
|
bulkAddKyberPreKeys: (array: Array<KyberPreKeyType>) => Promise<void>;
|
||||||
|
getAllKyberPreKeys: () => Promise<Array<KyberPreKeyType>>;
|
||||||
|
|
||||||
createOrUpdatePreKey: (data: PreKeyType) => Promise<void>;
|
createOrUpdatePreKey: (data: PreKeyType) => Promise<void>;
|
||||||
getPreKeyById: (id: PreKeyIdType) => Promise<PreKeyType | undefined>;
|
getPreKeyById: (id: PreKeyIdType) => Promise<PreKeyType | undefined>;
|
||||||
bulkAddPreKeys: (array: Array<PreKeyType>) => Promise<void>;
|
bulkAddPreKeys: (array: Array<PreKeyType>) => Promise<void>;
|
||||||
|
|
|
@ -133,6 +133,7 @@ import type {
|
||||||
UnprocessedType,
|
UnprocessedType,
|
||||||
UnprocessedUpdateType,
|
UnprocessedUpdateType,
|
||||||
GetNearbyMessageFromDeletedSetOptionsType,
|
GetNearbyMessageFromDeletedSetOptionsType,
|
||||||
|
StoredKyberPreKeyType,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
import { SeenStatus } from '../MessageSeenStatus';
|
import { SeenStatus } from '../MessageSeenStatus';
|
||||||
import {
|
import {
|
||||||
|
@ -173,6 +174,14 @@ const dataInterface: ServerInterface = {
|
||||||
removeAllIdentityKeys,
|
removeAllIdentityKeys,
|
||||||
getAllIdentityKeys,
|
getAllIdentityKeys,
|
||||||
|
|
||||||
|
createOrUpdateKyberPreKey,
|
||||||
|
getKyberPreKeyById,
|
||||||
|
bulkAddKyberPreKeys,
|
||||||
|
removeKyberPreKeyById,
|
||||||
|
removeKyberPreKeysByUuid,
|
||||||
|
removeAllKyberPreKeys,
|
||||||
|
getAllKyberPreKeys,
|
||||||
|
|
||||||
createOrUpdatePreKey,
|
createOrUpdatePreKey,
|
||||||
getPreKeyById,
|
getPreKeyById,
|
||||||
bulkAddPreKeys,
|
bulkAddPreKeys,
|
||||||
|
@ -655,6 +664,40 @@ async function getAllIdentityKeys(): Promise<Array<StoredIdentityKeyType>> {
|
||||||
return getAllFromTable(getInstance(), IDENTITY_KEYS_TABLE);
|
return getAllFromTable(getInstance(), IDENTITY_KEYS_TABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const KYBER_PRE_KEYS_TABLE = 'kyberPreKeys';
|
||||||
|
async function createOrUpdateKyberPreKey(
|
||||||
|
data: StoredKyberPreKeyType
|
||||||
|
): Promise<void> {
|
||||||
|
return createOrUpdate(getInstance(), KYBER_PRE_KEYS_TABLE, data);
|
||||||
|
}
|
||||||
|
async function getKyberPreKeyById(
|
||||||
|
id: PreKeyIdType
|
||||||
|
): Promise<StoredKyberPreKeyType | undefined> {
|
||||||
|
return getById(getInstance(), KYBER_PRE_KEYS_TABLE, id);
|
||||||
|
}
|
||||||
|
async function bulkAddKyberPreKeys(
|
||||||
|
array: Array<StoredKyberPreKeyType>
|
||||||
|
): Promise<void> {
|
||||||
|
return bulkAdd(getInstance(), KYBER_PRE_KEYS_TABLE, array);
|
||||||
|
}
|
||||||
|
async function removeKyberPreKeyById(
|
||||||
|
id: PreKeyIdType | Array<PreKeyIdType>
|
||||||
|
): Promise<void> {
|
||||||
|
return removeById(getInstance(), KYBER_PRE_KEYS_TABLE, id);
|
||||||
|
}
|
||||||
|
async function removeKyberPreKeysByUuid(uuid: UUIDStringType): Promise<void> {
|
||||||
|
const db = getInstance();
|
||||||
|
db.prepare<Query>('DELETE FROM kyberPreKeys WHERE ourUuid IS $uuid;').run({
|
||||||
|
uuid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function removeAllKyberPreKeys(): Promise<void> {
|
||||||
|
return removeAllFromTable(getInstance(), KYBER_PRE_KEYS_TABLE);
|
||||||
|
}
|
||||||
|
async function getAllKyberPreKeys(): Promise<Array<StoredKyberPreKeyType>> {
|
||||||
|
return getAllFromTable(getInstance(), KYBER_PRE_KEYS_TABLE);
|
||||||
|
}
|
||||||
|
|
||||||
const PRE_KEYS_TABLE = 'preKeys';
|
const PRE_KEYS_TABLE = 'preKeys';
|
||||||
async function createOrUpdatePreKey(data: StoredPreKeyType): Promise<void> {
|
async function createOrUpdatePreKey(data: StoredPreKeyType): Promise<void> {
|
||||||
return createOrUpdate(getInstance(), PRE_KEYS_TABLE, data);
|
return createOrUpdate(getInstance(), PRE_KEYS_TABLE, data);
|
||||||
|
@ -667,7 +710,9 @@ async function getPreKeyById(
|
||||||
async function bulkAddPreKeys(array: Array<StoredPreKeyType>): Promise<void> {
|
async function bulkAddPreKeys(array: Array<StoredPreKeyType>): Promise<void> {
|
||||||
return bulkAdd(getInstance(), PRE_KEYS_TABLE, array);
|
return bulkAdd(getInstance(), PRE_KEYS_TABLE, array);
|
||||||
}
|
}
|
||||||
async function removePreKeyById(id: PreKeyIdType): Promise<void> {
|
async function removePreKeyById(
|
||||||
|
id: PreKeyIdType | Array<PreKeyIdType>
|
||||||
|
): Promise<void> {
|
||||||
return removeById(getInstance(), PRE_KEYS_TABLE, id);
|
return removeById(getInstance(), PRE_KEYS_TABLE, id);
|
||||||
}
|
}
|
||||||
async function removePreKeysByUuid(uuid: UUIDStringType): Promise<void> {
|
async function removePreKeysByUuid(uuid: UUIDStringType): Promise<void> {
|
||||||
|
@ -699,7 +744,9 @@ async function bulkAddSignedPreKeys(
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return bulkAdd(getInstance(), SIGNED_PRE_KEYS_TABLE, array);
|
return bulkAdd(getInstance(), SIGNED_PRE_KEYS_TABLE, array);
|
||||||
}
|
}
|
||||||
async function removeSignedPreKeyById(id: SignedPreKeyIdType): Promise<void> {
|
async function removeSignedPreKeyById(
|
||||||
|
id: SignedPreKeyIdType | Array<SignedPreKeyIdType>
|
||||||
|
): Promise<void> {
|
||||||
return removeById(getInstance(), SIGNED_PRE_KEYS_TABLE, id);
|
return removeById(getInstance(), SIGNED_PRE_KEYS_TABLE, id);
|
||||||
}
|
}
|
||||||
async function removeSignedPreKeysByUuid(uuid: UUIDStringType): Promise<void> {
|
async function removeSignedPreKeysByUuid(uuid: UUIDStringType): Promise<void> {
|
||||||
|
@ -755,7 +802,9 @@ async function getAllItems(): Promise<StoredAllItemsType> {
|
||||||
|
|
||||||
return result as unknown as StoredAllItemsType;
|
return result as unknown as StoredAllItemsType;
|
||||||
}
|
}
|
||||||
async function removeItemById(id: ItemKeyType): Promise<void> {
|
async function removeItemById(
|
||||||
|
id: ItemKeyType | Array<ItemKeyType>
|
||||||
|
): Promise<void> {
|
||||||
return removeById(getInstance(), ITEMS_TABLE, id);
|
return removeById(getInstance(), ITEMS_TABLE, id);
|
||||||
}
|
}
|
||||||
async function removeAllItems(): Promise<void> {
|
async function removeAllItems(): Promise<void> {
|
||||||
|
@ -4989,6 +5038,7 @@ async function removeAll(): Promise<void> {
|
||||||
DELETE FROM identityKeys;
|
DELETE FROM identityKeys;
|
||||||
DELETE FROM items;
|
DELETE FROM items;
|
||||||
DELETE FROM jobs;
|
DELETE FROM jobs;
|
||||||
|
DELETE FROM kyberPreKeys;
|
||||||
DELETE FROM messages_fts;
|
DELETE FROM messages_fts;
|
||||||
DELETE FROM messages;
|
DELETE FROM messages;
|
||||||
DELETE FROM preKeys;
|
DELETE FROM preKeys;
|
||||||
|
@ -5024,6 +5074,7 @@ async function removeAllConfiguration(
|
||||||
`
|
`
|
||||||
DELETE FROM identityKeys;
|
DELETE FROM identityKeys;
|
||||||
DELETE FROM jobs;
|
DELETE FROM jobs;
|
||||||
|
DELETE FROM kyberPreKeys;
|
||||||
DELETE FROM preKeys;
|
DELETE FROM preKeys;
|
||||||
DELETE FROM senderKeys;
|
DELETE FROM senderKeys;
|
||||||
DELETE FROM sendLogMessageIds;
|
DELETE FROM sendLogMessageIds;
|
||||||
|
|
42
ts/sql/migrations/85-add-kyber-keys.ts
Normal file
42
ts/sql/migrations/85-add-kyber-keys.ts
Normal 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!');
|
||||||
|
}
|
|
@ -60,6 +60,7 @@ import updateToSchemaVersion81 from './81-contact-removed-notification';
|
||||||
import updateToSchemaVersion82 from './82-edited-messages-read-index';
|
import updateToSchemaVersion82 from './82-edited-messages-read-index';
|
||||||
import updateToSchemaVersion83 from './83-mentions';
|
import updateToSchemaVersion83 from './83-mentions';
|
||||||
import updateToSchemaVersion84 from './84-all-mentions';
|
import updateToSchemaVersion84 from './84-all-mentions';
|
||||||
|
import updateToSchemaVersion85 from './85-add-kyber-keys';
|
||||||
|
|
||||||
function updateToSchemaVersion1(
|
function updateToSchemaVersion1(
|
||||||
currentVersion: number,
|
currentVersion: number,
|
||||||
|
@ -1984,11 +1985,13 @@ export const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion77,
|
updateToSchemaVersion77,
|
||||||
updateToSchemaVersion78,
|
updateToSchemaVersion78,
|
||||||
updateToSchemaVersion79,
|
updateToSchemaVersion79,
|
||||||
|
|
||||||
updateToSchemaVersion80,
|
updateToSchemaVersion80,
|
||||||
updateToSchemaVersion81,
|
updateToSchemaVersion81,
|
||||||
updateToSchemaVersion82,
|
updateToSchemaVersion82,
|
||||||
updateToSchemaVersion83,
|
updateToSchemaVersion83,
|
||||||
updateToSchemaVersion84,
|
updateToSchemaVersion84,
|
||||||
|
updateToSchemaVersion85,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function updateSchema(db: Database, logger: LoggerType): void {
|
export function updateSchema(db: Database, logger: LoggerType): void {
|
||||||
|
|
|
@ -16,6 +16,7 @@ export type TableType =
|
||||||
| 'conversations'
|
| 'conversations'
|
||||||
| 'identityKeys'
|
| 'identityKeys'
|
||||||
| 'items'
|
| 'items'
|
||||||
|
| 'kyberPreKeys'
|
||||||
| 'messages'
|
| 'messages'
|
||||||
| 'preKeys'
|
| 'preKeys'
|
||||||
| 'senderKeys'
|
| 'senderKeys'
|
||||||
|
|
|
@ -930,7 +930,7 @@ describe('SignalProtocolStore', () => {
|
||||||
});
|
});
|
||||||
describe('storePreKey', () => {
|
describe('storePreKey', () => {
|
||||||
it('stores prekeys', async () => {
|
it('stores prekeys', async () => {
|
||||||
await store.storePreKey(ourUuid, 1, testKey);
|
await store.storePreKeys(ourUuid, [{ keyId: 1, keyPair: testKey }]);
|
||||||
const key = await store.loadPreKey(ourUuid, 1);
|
const key = await store.loadPreKey(ourUuid, 1);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw new Error('Missing key!');
|
throw new Error('Missing key!');
|
||||||
|
@ -947,10 +947,10 @@ describe('SignalProtocolStore', () => {
|
||||||
});
|
});
|
||||||
describe('removePreKey', () => {
|
describe('removePreKey', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await store.storePreKey(ourUuid, 2, testKey);
|
await store.storePreKeys(ourUuid, [{ keyId: 2, keyPair: testKey }]);
|
||||||
});
|
});
|
||||||
it('deletes prekeys', async () => {
|
it('deletes prekeys', async () => {
|
||||||
await store.removePreKey(ourUuid, 2);
|
await store.removePreKeys(ourUuid, [2]);
|
||||||
|
|
||||||
const key = await store.loadPreKey(ourUuid, 2);
|
const key = await store.loadPreKey(ourUuid, 2);
|
||||||
assert.isUndefined(key);
|
assert.isUndefined(key);
|
||||||
|
@ -978,7 +978,7 @@ describe('SignalProtocolStore', () => {
|
||||||
await store.storeSignedPreKey(ourUuid, 4, testKey);
|
await store.storeSignedPreKey(ourUuid, 4, testKey);
|
||||||
});
|
});
|
||||||
it('deletes signed prekeys', async () => {
|
it('deletes signed prekeys', async () => {
|
||||||
await store.removeSignedPreKey(ourUuid, 4);
|
await store.removeSignedPreKeys(ourUuid, [4]);
|
||||||
|
|
||||||
const key = await store.loadSignedPreKey(ourUuid, 4);
|
const key = await store.loadSignedPreKey(ourUuid, 4);
|
||||||
assert.isUndefined(key);
|
assert.isUndefined(key);
|
||||||
|
@ -1557,7 +1557,7 @@ describe('SignalProtocolStore', () => {
|
||||||
});
|
});
|
||||||
describe('removeOurOldPni/updateOurPniKeyMaterial', () => {
|
describe('removeOurOldPni/updateOurPniKeyMaterial', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await store.storePreKey(ourUuid, 2, testKey);
|
await store.storePreKeys(ourUuid, [{ keyId: 2, keyPair: testKey }]);
|
||||||
await store.storeSignedPreKey(ourUuid, 3, testKey);
|
await store.storeSignedPreKey(ourUuid, 3, testKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,84 +2,98 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
import { range } from 'lodash';
|
||||||
|
|
||||||
import { getRandomBytes } from '../../Crypto';
|
import { getRandomBytes } from '../../Crypto';
|
||||||
import AccountManager from '../../textsecure/AccountManager';
|
import AccountManager from '../../textsecure/AccountManager';
|
||||||
import type { OuterSignedPrekeyType } from '../../textsecure/Types.d';
|
import type {
|
||||||
|
KyberPreKeyType,
|
||||||
|
OuterSignedPrekeyType,
|
||||||
|
PreKeyType,
|
||||||
|
} from '../../textsecure/Types.d';
|
||||||
import { UUID, UUIDKind } from '../../types/UUID';
|
import { UUID, UUIDKind } from '../../types/UUID';
|
||||||
|
import { DAY } from '../../util/durations';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
describe('AccountManager', () => {
|
describe('AccountManager', () => {
|
||||||
let accountManager: AccountManager;
|
let accountManager: AccountManager;
|
||||||
|
|
||||||
|
const ourUuid = UUID.generate();
|
||||||
|
const identityKey = window.Signal.Curve.generateKeyPair();
|
||||||
|
const pubKey = getRandomBytes(33);
|
||||||
|
const privKey = getRandomBytes(32);
|
||||||
|
|
||||||
|
let originalGetIdentityKeyPair: any;
|
||||||
|
let originalGetUuid: any;
|
||||||
|
let originalGetCheckedUuid: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const server: any = {};
|
const server: any = {};
|
||||||
accountManager = new AccountManager(server);
|
accountManager = new AccountManager(server);
|
||||||
|
|
||||||
|
originalGetIdentityKeyPair =
|
||||||
|
window.textsecure.storage.protocol.getIdentityKeyPair;
|
||||||
|
originalGetUuid = window.textsecure.storage.user.getUuid;
|
||||||
|
originalGetCheckedUuid = window.textsecure.storage.user.getCheckedUuid;
|
||||||
|
|
||||||
|
window.textsecure.storage.protocol.getIdentityKeyPair = () => identityKey;
|
||||||
|
window.textsecure.storage.user.getUuid = () => ourUuid;
|
||||||
|
window.textsecure.storage.user.getCheckedUuid = () => ourUuid;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#cleanSignedPreKeys', () => {
|
afterEach(() => {
|
||||||
let originalGetIdentityKeyPair: any;
|
window.textsecure.storage.protocol.getIdentityKeyPair =
|
||||||
|
originalGetIdentityKeyPair;
|
||||||
|
window.textsecure.storage.user.getUuid = originalGetUuid;
|
||||||
|
window.textsecure.storage.user.getCheckedUuid = originalGetCheckedUuid;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('encrypted device name', () => {
|
||||||
|
it('roundtrips', async () => {
|
||||||
|
const deviceName = 'v2.5.0 on Ubunto 20.04';
|
||||||
|
const encrypted = accountManager.encryptDeviceName(
|
||||||
|
deviceName,
|
||||||
|
identityKey
|
||||||
|
);
|
||||||
|
if (!encrypted) {
|
||||||
|
throw new Error('failed to encrypt!');
|
||||||
|
}
|
||||||
|
assert.strictEqual(typeof encrypted, 'string');
|
||||||
|
const decrypted = await accountManager.decryptDeviceName(encrypted);
|
||||||
|
|
||||||
|
assert.strictEqual(decrypted, deviceName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles falsey deviceName', () => {
|
||||||
|
const encrypted = accountManager.encryptDeviceName('', identityKey);
|
||||||
|
assert.strictEqual(encrypted, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#_cleanSignedPreKeys', () => {
|
||||||
let originalLoadSignedPreKeys: any;
|
let originalLoadSignedPreKeys: any;
|
||||||
let originalRemoveSignedPreKey: any;
|
let originalRemoveSignedPreKey: any;
|
||||||
let originalGetUuid: any;
|
|
||||||
let signedPreKeys: Array<OuterSignedPrekeyType>;
|
let signedPreKeys: Array<OuterSignedPrekeyType>;
|
||||||
const DAY = 1000 * 60 * 60 * 24;
|
|
||||||
|
|
||||||
const pubKey = getRandomBytes(33);
|
|
||||||
const privKey = getRandomBytes(32);
|
|
||||||
const identityKey = window.Signal.Curve.generateKeyPair();
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const ourUuid = UUID.generate();
|
|
||||||
|
|
||||||
originalGetUuid = window.textsecure.storage.user.getUuid;
|
|
||||||
originalGetIdentityKeyPair =
|
|
||||||
window.textsecure.storage.protocol.getIdentityKeyPair;
|
|
||||||
originalLoadSignedPreKeys =
|
originalLoadSignedPreKeys =
|
||||||
window.textsecure.storage.protocol.loadSignedPreKeys;
|
window.textsecure.storage.protocol.loadSignedPreKeys;
|
||||||
originalRemoveSignedPreKey =
|
originalRemoveSignedPreKey =
|
||||||
window.textsecure.storage.protocol.removeSignedPreKey;
|
window.textsecure.storage.protocol.removeSignedPreKeys;
|
||||||
|
|
||||||
window.textsecure.storage.user.getUuid = () => ourUuid;
|
window.textsecure.storage.protocol.loadSignedPreKeys = () =>
|
||||||
|
|
||||||
window.textsecure.storage.protocol.getIdentityKeyPair = () => identityKey;
|
|
||||||
window.textsecure.storage.protocol.loadSignedPreKeys = async () =>
|
|
||||||
signedPreKeys;
|
signedPreKeys;
|
||||||
|
// removeSignedPreKeys is updated per-test, below
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
window.textsecure.storage.user.getUuid = originalGetUuid;
|
|
||||||
window.textsecure.storage.protocol.getIdentityKeyPair =
|
|
||||||
originalGetIdentityKeyPair;
|
|
||||||
window.textsecure.storage.protocol.loadSignedPreKeys =
|
window.textsecure.storage.protocol.loadSignedPreKeys =
|
||||||
originalLoadSignedPreKeys;
|
originalLoadSignedPreKeys;
|
||||||
window.textsecure.storage.protocol.removeSignedPreKey =
|
window.textsecure.storage.protocol.removeSignedPreKeys =
|
||||||
originalRemoveSignedPreKey;
|
originalRemoveSignedPreKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('encrypted device name', () => {
|
it('keeps no keys if five or less, even if over a month old', () => {
|
||||||
it('roundtrips', async () => {
|
|
||||||
const deviceName = 'v2.5.0 on Ubunto 20.04';
|
|
||||||
const encrypted = accountManager.encryptDeviceName(
|
|
||||||
deviceName,
|
|
||||||
identityKey
|
|
||||||
);
|
|
||||||
if (!encrypted) {
|
|
||||||
throw new Error('failed to encrypt!');
|
|
||||||
}
|
|
||||||
assert.strictEqual(typeof encrypted, 'string');
|
|
||||||
const decrypted = await accountManager.decryptDeviceName(encrypted);
|
|
||||||
|
|
||||||
assert.strictEqual(decrypted, deviceName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles falsey deviceName', () => {
|
|
||||||
const encrypted = accountManager.encryptDeviceName('', identityKey);
|
|
||||||
assert.strictEqual(encrypted, null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps three confirmed keys even if over a month old', () => {
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
signedPreKeys = [
|
signedPreKeys = [
|
||||||
{
|
{
|
||||||
|
@ -103,10 +117,24 @@ describe('AccountManager', () => {
|
||||||
pubKey,
|
pubKey,
|
||||||
privKey,
|
privKey,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
keyId: 4,
|
||||||
|
created_at: now - DAY * 39,
|
||||||
|
confirmed: true,
|
||||||
|
pubKey,
|
||||||
|
privKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keyId: 5,
|
||||||
|
created_at: now - DAY * 40,
|
||||||
|
confirmed: false,
|
||||||
|
pubKey,
|
||||||
|
privKey,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// should be no calls to store.removeSignedPreKey, would cause crash
|
// should be no calls to store.removeSignedPreKey, would cause crash
|
||||||
return accountManager.cleanSignedPreKeys(UUIDKind.ACI);
|
return accountManager._cleanSignedPreKeys(UUIDKind.ACI);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('eliminates oldest keys, even if recent key is unconfirmed', async () => {
|
it('eliminates oldest keys, even if recent key is unconfirmed', async () => {
|
||||||
|
@ -157,60 +185,430 @@ describe('AccountManager', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let count = 0;
|
let removedKeys: Array<number> = [];
|
||||||
window.textsecure.storage.protocol.removeSignedPreKey = async (
|
window.textsecure.storage.protocol.removeSignedPreKeys = async (
|
||||||
_,
|
_,
|
||||||
keyId
|
keyIds
|
||||||
) => {
|
) => {
|
||||||
if (keyId !== 4) {
|
removedKeys = removedKeys.concat(keyIds);
|
||||||
throw new Error(`Wrong keys were eliminated! ${keyId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
count += 1;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await accountManager.cleanSignedPreKeys(UUIDKind.ACI);
|
await accountManager._cleanSignedPreKeys(UUIDKind.ACI);
|
||||||
assert.strictEqual(count, 1);
|
assert.deepEqual(removedKeys, [4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#_cleanLastResortKeys', () => {
|
||||||
|
let originalLoadKyberPreKeys: any;
|
||||||
|
let originalRemoveKyberPreKey: any;
|
||||||
|
let kyberPreKeys: Array<KyberPreKeyType>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
originalLoadKyberPreKeys =
|
||||||
|
window.textsecure.storage.protocol.loadKyberPreKeys;
|
||||||
|
originalRemoveKyberPreKey =
|
||||||
|
window.textsecure.storage.protocol.removeKyberPreKeys;
|
||||||
|
|
||||||
|
window.textsecure.storage.protocol.loadKyberPreKeys = () => kyberPreKeys;
|
||||||
|
// removeKyberPreKeys is updated per-test, below
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
window.textsecure.storage.protocol.loadKyberPreKeys =
|
||||||
|
originalLoadKyberPreKeys;
|
||||||
|
window.textsecure.storage.protocol.removeKyberPreKeys =
|
||||||
|
originalRemoveKyberPreKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Removes no keys if less than five', async () => {
|
it('keeps five keys even if over a month old', () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
signedPreKeys = [
|
kyberPreKeys = [
|
||||||
{
|
{
|
||||||
|
id: `${ourUuid.toString()}:1`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 32,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isLastResort: true,
|
||||||
|
isConfirmed: true,
|
||||||
keyId: 1,
|
keyId: 1,
|
||||||
created_at: now - DAY * 32,
|
ourUuid: ourUuid.toString(),
|
||||||
confirmed: true,
|
|
||||||
pubKey,
|
|
||||||
privKey,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: `${ourUuid.toString()}:2`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 34,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isLastResort: true,
|
||||||
|
isConfirmed: true,
|
||||||
keyId: 2,
|
keyId: 2,
|
||||||
created_at: now - DAY * 44,
|
ourUuid: ourUuid.toString(),
|
||||||
confirmed: true,
|
|
||||||
pubKey,
|
|
||||||
privKey,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: `${ourUuid.toString()}:3`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 38,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isLastResort: true,
|
||||||
|
isConfirmed: true,
|
||||||
keyId: 3,
|
keyId: 3,
|
||||||
created_at: now - DAY * 36,
|
ourUuid: ourUuid.toString(),
|
||||||
confirmed: false,
|
|
||||||
pubKey,
|
|
||||||
privKey,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: `${ourUuid.toString()}:4`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 39,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isLastResort: true,
|
||||||
|
isConfirmed: false,
|
||||||
keyId: 4,
|
keyId: 4,
|
||||||
created_at: now - DAY * 20,
|
ourUuid: ourUuid.toString(),
|
||||||
confirmed: false,
|
},
|
||||||
pubKey,
|
{
|
||||||
privKey,
|
id: `${ourUuid.toString()}:5`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 40,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isLastResort: true,
|
||||||
|
isConfirmed: false,
|
||||||
|
keyId: 5,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
window.textsecure.storage.protocol.removeSignedPreKey = async () => {
|
// should be no calls to store.removeKyberPreKey, would cause crash
|
||||||
throw new Error('None should be removed!');
|
return accountManager._cleanLastResortKeys(UUIDKind.ACI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('eliminates oldest keys, even if recent key is unconfirmed', async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
kyberPreKeys = [
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:1`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 32,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isLastResort: true,
|
||||||
|
isConfirmed: true,
|
||||||
|
keyId: 1,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:2`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 31,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isLastResort: true,
|
||||||
|
isConfirmed: false,
|
||||||
|
keyId: 2,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:3`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 24,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isLastResort: true,
|
||||||
|
isConfirmed: true,
|
||||||
|
keyId: 3,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Oldest, should be dropped
|
||||||
|
id: `${ourUuid.toString()}:4`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 38,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isLastResort: true,
|
||||||
|
isConfirmed: true,
|
||||||
|
keyId: 4,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:5`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 5,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isLastResort: true,
|
||||||
|
isConfirmed: true,
|
||||||
|
keyId: 5,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:6`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 5,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isLastResort: true,
|
||||||
|
isConfirmed: true,
|
||||||
|
keyId: 6,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let removedKeys: Array<number> = [];
|
||||||
|
window.textsecure.storage.protocol.removeKyberPreKeys = async (
|
||||||
|
_,
|
||||||
|
keyIds
|
||||||
|
) => {
|
||||||
|
removedKeys = removedKeys.concat(keyIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
await accountManager.cleanSignedPreKeys(UUIDKind.ACI);
|
await accountManager._cleanLastResortKeys(UUIDKind.ACI);
|
||||||
|
assert.deepEqual(removedKeys, [4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#_cleanPreKeys', () => {
|
||||||
|
let originalLoadPreKeys: any;
|
||||||
|
let originalRemovePreKeys: any;
|
||||||
|
let preKeys: Array<PreKeyType>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
originalLoadPreKeys = window.textsecure.storage.protocol.loadPreKeys;
|
||||||
|
originalRemovePreKeys = window.textsecure.storage.protocol.removePreKeys;
|
||||||
|
|
||||||
|
window.textsecure.storage.protocol.loadPreKeys = () => preKeys;
|
||||||
|
// removePreKeys is updated per-test, below
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
window.textsecure.storage.protocol.loadPreKeys = originalLoadPreKeys;
|
||||||
|
window.textsecure.storage.protocol.removePreKeys = originalRemovePreKeys;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps five keys even if over 90 days old, but all latest batch', () => {
|
||||||
|
const now = Date.now();
|
||||||
|
preKeys = [
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:1`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 92,
|
||||||
|
keyId: 1,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
privateKey: privKey,
|
||||||
|
publicKey: pubKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:2`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 93,
|
||||||
|
keyId: 2,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
privateKey: privKey,
|
||||||
|
publicKey: pubKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:3`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 93,
|
||||||
|
keyId: 3,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
privateKey: privKey,
|
||||||
|
publicKey: pubKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:4`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 93,
|
||||||
|
keyId: 4,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
privateKey: privKey,
|
||||||
|
publicKey: pubKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:5`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 94,
|
||||||
|
keyId: 5,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
privateKey: privKey,
|
||||||
|
publicKey: pubKey,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// should be no calls to store.removeKyberPreKey, would cause crash
|
||||||
|
return accountManager._cleanPreKeys(UUIDKind.ACI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('eliminates keys not in the 200 newest, over 90 days old', async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
preKeys = [
|
||||||
|
// The latest batch
|
||||||
|
...range(0, 100).map(
|
||||||
|
(id): PreKeyType => ({
|
||||||
|
id: `${ourUuid.toString()}:${id}`,
|
||||||
|
|
||||||
|
createdAt: now - DAY,
|
||||||
|
keyId: 1,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
privateKey: privKey,
|
||||||
|
publicKey: pubKey,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// Second-oldest batch, won't be dropped
|
||||||
|
...range(100, 200).map(
|
||||||
|
(id): PreKeyType => ({
|
||||||
|
id: `${ourUuid.toString()}:${id}`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 40,
|
||||||
|
keyId: 1,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
privateKey: privKey,
|
||||||
|
publicKey: pubKey,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// Oldest batch, will be dropped
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:6`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 92,
|
||||||
|
keyId: 6,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
privateKey: privKey,
|
||||||
|
publicKey: pubKey,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let removedKeys: Array<number> = [];
|
||||||
|
window.textsecure.storage.protocol.removePreKeys = async (_, keyIds) => {
|
||||||
|
removedKeys = removedKeys.concat(keyIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
await accountManager._cleanPreKeys(UUIDKind.ACI);
|
||||||
|
assert.deepEqual(removedKeys, [6]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#_cleanKyberPreKeys', () => {
|
||||||
|
let originalLoadKyberPreKeys: any;
|
||||||
|
let originalRemoveKyberPreKeys: any;
|
||||||
|
let kyberPreKeys: Array<KyberPreKeyType>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
originalLoadKyberPreKeys =
|
||||||
|
window.textsecure.storage.protocol.loadKyberPreKeys;
|
||||||
|
originalRemoveKyberPreKeys =
|
||||||
|
window.textsecure.storage.protocol.removeKyberPreKeys;
|
||||||
|
|
||||||
|
window.textsecure.storage.protocol.loadKyberPreKeys = () => kyberPreKeys;
|
||||||
|
// removeKyberPreKeys is updated per-test, below
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
window.textsecure.storage.protocol.loadKyberPreKeys =
|
||||||
|
originalLoadKyberPreKeys;
|
||||||
|
window.textsecure.storage.protocol.removeKyberPreKeys =
|
||||||
|
originalRemoveKyberPreKeys;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps five keys even if over 90 days old', () => {
|
||||||
|
const now = Date.now();
|
||||||
|
kyberPreKeys = [
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:1`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 93,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isConfirmed: false,
|
||||||
|
isLastResort: false,
|
||||||
|
keyId: 1,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:2`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 93,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isConfirmed: false,
|
||||||
|
isLastResort: false,
|
||||||
|
keyId: 2,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:3`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 93,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isConfirmed: false,
|
||||||
|
isLastResort: false,
|
||||||
|
keyId: 3,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:4`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 93,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isConfirmed: false,
|
||||||
|
isLastResort: false,
|
||||||
|
keyId: 4,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:5`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 93,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isConfirmed: false,
|
||||||
|
isLastResort: false,
|
||||||
|
keyId: 5,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// should be no calls to store.removeKyberPreKey, would cause crash
|
||||||
|
return accountManager._cleanKyberPreKeys(UUIDKind.ACI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('eliminates keys not in the newest 200, over 90 days old', async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
kyberPreKeys = [
|
||||||
|
// The latest batch
|
||||||
|
...range(0, 100).map(
|
||||||
|
(id): KyberPreKeyType => ({
|
||||||
|
id: `${ourUuid.toString()}:${id}`,
|
||||||
|
|
||||||
|
createdAt: now - DAY,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isConfirmed: false,
|
||||||
|
isLastResort: false,
|
||||||
|
keyId: 1,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// Second-oldest batch, won't be dropped
|
||||||
|
...range(100, 200).map(
|
||||||
|
(id): KyberPreKeyType => ({
|
||||||
|
id: `${ourUuid.toString()}:${id}`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 45,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isConfirmed: false,
|
||||||
|
isLastResort: false,
|
||||||
|
keyId: 4,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// Oldest batch, will be dropped
|
||||||
|
{
|
||||||
|
id: `${ourUuid.toString()}:6`,
|
||||||
|
|
||||||
|
createdAt: now - DAY * 93,
|
||||||
|
data: getRandomBytes(32),
|
||||||
|
isConfirmed: false,
|
||||||
|
isLastResort: false,
|
||||||
|
keyId: 6,
|
||||||
|
ourUuid: ourUuid.toString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let removedKeys: Array<number> = [];
|
||||||
|
window.textsecure.storage.protocol.removeKyberPreKeys = async (
|
||||||
|
_,
|
||||||
|
keyIds
|
||||||
|
) => {
|
||||||
|
removedKeys = removedKeys.concat(keyIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
await accountManager._cleanKyberPreKeys(UUIDKind.ACI);
|
||||||
|
assert.deepEqual(removedKeys, [6]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { assert } from 'chai';
|
||||||
|
|
||||||
import { constantTimeEqual } from '../../Crypto';
|
import { constantTimeEqual } from '../../Crypto';
|
||||||
import { generateKeyPair } from '../../Curve';
|
import { generateKeyPair } from '../../Curve';
|
||||||
import type { GeneratedKeysType } from '../../textsecure/AccountManager';
|
import type { UploadKeysType } from '../../textsecure/WebAPI';
|
||||||
import AccountManager from '../../textsecure/AccountManager';
|
import AccountManager from '../../textsecure/AccountManager';
|
||||||
import type { PreKeyType, SignedPreKeyType } from '../../textsecure/Types.d';
|
import type { PreKeyType, SignedPreKeyType } from '../../textsecure/Types.d';
|
||||||
import { UUID, UUIDKind } from '../../types/UUID';
|
import { UUID, UUIDKind } from '../../types/UUID';
|
||||||
|
@ -19,6 +19,7 @@ const assertEqualBuffers = (a: Uint8Array, b: Uint8Array) => {
|
||||||
describe('Key generation', function thisNeeded() {
|
describe('Key generation', function thisNeeded() {
|
||||||
const count = 10;
|
const count = 10;
|
||||||
const ourUuid = new UUID('aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee');
|
const ourUuid = new UUID('aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee');
|
||||||
|
let result: UploadKeysType;
|
||||||
this.timeout(count * 2000);
|
this.timeout(count * 2000);
|
||||||
|
|
||||||
function itStoresPreKey(keyId: number): void {
|
function itStoresPreKey(keyId: number): void {
|
||||||
|
@ -30,6 +31,15 @@ describe('Key generation', function thisNeeded() {
|
||||||
assert(keyPair, `PreKey ${keyId} not found`);
|
assert(keyPair, `PreKey ${keyId} not found`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function itStoresKyberPreKey(keyId: number): void {
|
||||||
|
it(`kyber pre key ${keyId} is valid`, async () => {
|
||||||
|
const key = await textsecure.storage.protocol.loadKyberPreKey(
|
||||||
|
ourUuid,
|
||||||
|
keyId
|
||||||
|
);
|
||||||
|
assert(key, `kyber pre key ${keyId} not found`);
|
||||||
|
});
|
||||||
|
}
|
||||||
function itStoresSignedPreKey(keyId: number): void {
|
function itStoresSignedPreKey(keyId: number): void {
|
||||||
it(`signed prekey ${keyId} is valid`, async () => {
|
it(`signed prekey ${keyId} is valid`, async () => {
|
||||||
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
|
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
|
||||||
|
@ -39,7 +49,8 @@ describe('Key generation', function thisNeeded() {
|
||||||
assert(keyPair, `SignedPreKey ${keyId} not found`);
|
assert(keyPair, `SignedPreKey ${keyId} not found`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function validateResultKey(
|
|
||||||
|
async function validateResultPreKey(
|
||||||
resultKey: Pick<PreKeyType, 'keyId' | 'publicKey'>
|
resultKey: Pick<PreKeyType, 'keyId' | 'publicKey'>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const keyPair = await textsecure.storage.protocol.loadPreKey(
|
const keyPair = await textsecure.storage.protocol.loadPreKey(
|
||||||
|
@ -52,8 +63,11 @@ describe('Key generation', function thisNeeded() {
|
||||||
assertEqualBuffers(resultKey.publicKey, keyPair.publicKey().serialize());
|
assertEqualBuffers(resultKey.publicKey, keyPair.publicKey().serialize());
|
||||||
}
|
}
|
||||||
async function validateResultSignedKey(
|
async function validateResultSignedKey(
|
||||||
resultSignedKey: Pick<SignedPreKeyType, 'keyId' | 'publicKey'>
|
resultSignedKey?: Pick<SignedPreKeyType, 'keyId' | 'publicKey'>
|
||||||
) {
|
) {
|
||||||
|
if (!resultSignedKey) {
|
||||||
|
throw new Error('validateResultSignedKey: No signed prekey provided!');
|
||||||
|
}
|
||||||
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
|
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
|
||||||
ourUuid,
|
ourUuid,
|
||||||
resultSignedKey.keyId
|
resultSignedKey.keyId
|
||||||
|
@ -68,120 +82,166 @@ describe('Key generation', function thisNeeded() {
|
||||||
}
|
}
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
await textsecure.storage.protocol.clearPreKeyStore();
|
||||||
|
await textsecure.storage.protocol.clearKyberPreKeyStore();
|
||||||
|
await textsecure.storage.protocol.clearSignedPreKeysStore();
|
||||||
|
|
||||||
const keyPair = generateKeyPair();
|
const keyPair = generateKeyPair();
|
||||||
await textsecure.storage.put('identityKeyMap', {
|
await textsecure.storage.put('identityKeyMap', {
|
||||||
[ourUuid.toString()]: keyPair,
|
[ourUuid.toString()]: keyPair,
|
||||||
});
|
});
|
||||||
await textsecure.storage.user.setUuidAndDeviceId(ourUuid.toString(), 1);
|
await textsecure.storage.user.setUuidAndDeviceId(ourUuid.toString(), 1);
|
||||||
|
|
||||||
await textsecure.storage.protocol.hydrateCaches();
|
await textsecure.storage.protocol.hydrateCaches();
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
await textsecure.storage.protocol.clearPreKeyStore();
|
await textsecure.storage.protocol.clearPreKeyStore();
|
||||||
|
await textsecure.storage.protocol.clearKyberPreKeyStore();
|
||||||
await textsecure.storage.protocol.clearSignedPreKeysStore();
|
await textsecure.storage.protocol.clearSignedPreKeysStore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the first time', () => {
|
describe('the first time', () => {
|
||||||
let result: GeneratedKeysType;
|
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const accountManager = new AccountManager({} as any);
|
const accountManager = new AccountManager({} as any);
|
||||||
result = await accountManager.generateKeys(count, UUIDKind.ACI);
|
result = await accountManager._generateKeys(count, UUIDKind.ACI);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 1; i <= count; i += 1) {
|
describe('generates the basics', () => {
|
||||||
itStoresPreKey(i);
|
for (let i = 1; i <= count; i += 1) {
|
||||||
}
|
itStoresPreKey(i);
|
||||||
itStoresSignedPreKey(1);
|
}
|
||||||
|
for (let i = 1; i <= count + 1; i += 1) {
|
||||||
|
itStoresKyberPreKey(i);
|
||||||
|
}
|
||||||
|
itStoresSignedPreKey(1);
|
||||||
|
});
|
||||||
|
|
||||||
it(`result contains ${count} preKeys`, () => {
|
it(`result contains ${count} preKeys`, () => {
|
||||||
assert.isArray(result.preKeys);
|
const preKeys = result.preKeys || [];
|
||||||
assert.lengthOf(result.preKeys, count);
|
assert.isArray(preKeys);
|
||||||
|
assert.lengthOf(preKeys, count);
|
||||||
for (let i = 0; i < count; i += 1) {
|
for (let i = 0; i < count; i += 1) {
|
||||||
assert.isObject(result.preKeys[i]);
|
assert.isObject(preKeys[i]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('result contains the correct keyIds', () => {
|
it('result contains the correct keyIds', () => {
|
||||||
|
const preKeys = result.preKeys || [];
|
||||||
for (let i = 0; i < count; i += 1) {
|
for (let i = 0; i < count; i += 1) {
|
||||||
assert.strictEqual(result.preKeys[i].keyId, i + 1);
|
assert.strictEqual(preKeys[i].keyId, i + 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('result contains the correct public keys', async () => {
|
it('result contains the correct public keys', async () => {
|
||||||
await Promise.all(result.preKeys.map(validateResultKey));
|
const preKeys = result.preKeys || [];
|
||||||
|
await Promise.all(preKeys.map(validateResultPreKey));
|
||||||
});
|
});
|
||||||
it('returns a signed prekey', () => {
|
it('returns a signed prekey', () => {
|
||||||
assert.strictEqual(result.signedPreKey.keyId, 1);
|
assert.strictEqual(result.signedPreKey?.keyId, 1);
|
||||||
assert.instanceOf(result.signedPreKey.signature, Uint8Array);
|
assert.instanceOf(result.signedPreKey?.signature, Uint8Array);
|
||||||
return validateResultSignedKey(result.signedPreKey);
|
return validateResultSignedKey(result.signedPreKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('the second time', () => {
|
describe('the second time', () => {
|
||||||
let result: GeneratedKeysType;
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const accountManager = new AccountManager({} as any);
|
const accountManager = new AccountManager({} as any);
|
||||||
result = await accountManager.generateKeys(count, UUIDKind.ACI);
|
result = await accountManager._generateKeys(count, UUIDKind.ACI);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generates the basics', () => {
|
||||||
|
for (let i = 1; i <= 2 * count; i += 1) {
|
||||||
|
itStoresPreKey(i);
|
||||||
|
}
|
||||||
|
for (let i = 1; i <= 2 * count + 2; i += 1) {
|
||||||
|
itStoresKyberPreKey(i);
|
||||||
|
}
|
||||||
|
itStoresSignedPreKey(1);
|
||||||
|
itStoresSignedPreKey(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 1; i <= 2 * count; i += 1) {
|
|
||||||
itStoresPreKey(i);
|
|
||||||
}
|
|
||||||
itStoresSignedPreKey(1);
|
|
||||||
itStoresSignedPreKey(2);
|
|
||||||
it(`result contains ${count} preKeys`, () => {
|
it(`result contains ${count} preKeys`, () => {
|
||||||
assert.isArray(result.preKeys);
|
const preKeys = result.preKeys || [];
|
||||||
assert.lengthOf(result.preKeys, count);
|
assert.isArray(preKeys);
|
||||||
|
assert.lengthOf(preKeys, count);
|
||||||
for (let i = 0; i < count; i += 1) {
|
for (let i = 0; i < count; i += 1) {
|
||||||
assert.isObject(result.preKeys[i]);
|
assert.isObject(preKeys[i]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('result contains the correct keyIds', () => {
|
it('result contains the correct keyIds', () => {
|
||||||
|
const preKeys = result.preKeys || [];
|
||||||
for (let i = 1; i <= count; i += 1) {
|
for (let i = 1; i <= count; i += 1) {
|
||||||
assert.strictEqual(result.preKeys[i - 1].keyId, i + count);
|
assert.strictEqual(preKeys[i - 1].keyId, i + count);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('result contains the correct public keys', async () => {
|
it('result contains the correct public keys', async () => {
|
||||||
await Promise.all(result.preKeys.map(validateResultKey));
|
const preKeys = result.preKeys || [];
|
||||||
|
await Promise.all(preKeys.map(validateResultPreKey));
|
||||||
});
|
});
|
||||||
it('returns a signed prekey', () => {
|
it('returns a signed prekey', () => {
|
||||||
assert.strictEqual(result.signedPreKey.keyId, 2);
|
assert.strictEqual(result.signedPreKey?.keyId, 2);
|
||||||
assert.instanceOf(result.signedPreKey.signature, Uint8Array);
|
assert.instanceOf(result.signedPreKey?.signature, Uint8Array);
|
||||||
return validateResultSignedKey(result.signedPreKey);
|
return validateResultSignedKey(result.signedPreKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('the third time', () => {
|
describe('the third time, after keys are confirmed', () => {
|
||||||
let result: GeneratedKeysType;
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const accountManager = new AccountManager({} as any);
|
const accountManager = new AccountManager({} as any);
|
||||||
result = await accountManager.generateKeys(count, UUIDKind.ACI);
|
|
||||||
|
await accountManager._confirmKeys(result, UUIDKind.ACI);
|
||||||
|
|
||||||
|
result = await accountManager._generateKeys(count, UUIDKind.ACI);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generates the basics', () => {
|
||||||
|
for (let i = 1; i <= 3 * count; i += 1) {
|
||||||
|
itStoresPreKey(i);
|
||||||
|
}
|
||||||
|
// Note: no new last resort kyber key generated
|
||||||
|
for (let i = 1; i <= 3 * count + 2; i += 1) {
|
||||||
|
itStoresKyberPreKey(i);
|
||||||
|
}
|
||||||
|
itStoresSignedPreKey(1);
|
||||||
|
itStoresSignedPreKey(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 1; i <= 3 * count; i += 1) {
|
|
||||||
itStoresPreKey(i);
|
|
||||||
}
|
|
||||||
itStoresSignedPreKey(2);
|
|
||||||
itStoresSignedPreKey(3);
|
|
||||||
it(`result contains ${count} preKeys`, () => {
|
it(`result contains ${count} preKeys`, () => {
|
||||||
assert.isArray(result.preKeys);
|
const preKeys = result.preKeys || [];
|
||||||
assert.lengthOf(result.preKeys, count);
|
assert.isArray(preKeys);
|
||||||
|
assert.lengthOf(preKeys, count);
|
||||||
for (let i = 0; i < count; i += 1) {
|
for (let i = 0; i < count; i += 1) {
|
||||||
assert.isObject(result.preKeys[i]);
|
assert.isObject(preKeys[i]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('result contains the correct keyIds', () => {
|
it('result contains the correct keyIds', () => {
|
||||||
|
const preKeys = result.preKeys || [];
|
||||||
for (let i = 1; i <= count; i += 1) {
|
for (let i = 1; i <= count; i += 1) {
|
||||||
assert.strictEqual(result.preKeys[i - 1].keyId, i + 2 * count);
|
assert.strictEqual(preKeys[i - 1].keyId, i + 2 * count);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('result contains the correct public keys', async () => {
|
it('result contains the correct public keys', async () => {
|
||||||
await Promise.all(result.preKeys.map(validateResultKey));
|
const preKeys = result.preKeys || [];
|
||||||
|
await Promise.all(preKeys.map(validateResultPreKey));
|
||||||
});
|
});
|
||||||
it('result contains a signed prekey', () => {
|
it('does not generate a third last resort prekey', async () => {
|
||||||
assert.strictEqual(result.signedPreKey.keyId, 3);
|
const keyId = 3 * count + 3;
|
||||||
assert.instanceOf(result.signedPreKey.signature, Uint8Array);
|
const key = await textsecure.storage.protocol.loadKyberPreKey(
|
||||||
return validateResultSignedKey(result.signedPreKey);
|
ourUuid,
|
||||||
|
keyId
|
||||||
|
);
|
||||||
|
assert.isUndefined(key, `kyber pre key ${keyId} was unexpectedly found`);
|
||||||
|
});
|
||||||
|
it('does not generate a third signed prekey', async () => {
|
||||||
|
const keyId = 3;
|
||||||
|
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
|
||||||
|
ourUuid,
|
||||||
|
keyId
|
||||||
|
);
|
||||||
|
assert.isUndefined(
|
||||||
|
keyPair,
|
||||||
|
`SignedPreKey ${keyId} was unexpectedly found`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,7 @@ import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import createDebug from 'debug';
|
import createDebug from 'debug';
|
||||||
import pTimeout from 'p-timeout';
|
import pTimeout from 'p-timeout';
|
||||||
|
import normalizePath from 'normalize-path';
|
||||||
|
|
||||||
import type { Device, PrimaryDevice } from '@signalapp/mock-server';
|
import type { Device, PrimaryDevice } from '@signalapp/mock-server';
|
||||||
import { Server, UUIDKind, loadCertificates } from '@signalapp/mock-server';
|
import { Server, UUIDKind, loadCertificates } from '@signalapp/mock-server';
|
||||||
|
@ -289,7 +290,20 @@ export class Bootstrap {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async saveLogs(app: App | undefined = this.lastApp): Promise<void> {
|
public async maybeSaveLogs(
|
||||||
|
test?: Mocha.Test,
|
||||||
|
app: App | undefined = this.lastApp
|
||||||
|
): Promise<void> {
|
||||||
|
const { FORCE_ARTIFACT_SAVE } = process.env;
|
||||||
|
if (test?.state !== 'passed' || FORCE_ARTIFACT_SAVE) {
|
||||||
|
await this.saveLogs(app, test?.fullTitle());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async saveLogs(
|
||||||
|
app: App | undefined = this.lastApp,
|
||||||
|
pathPrefix?: string
|
||||||
|
): Promise<void> {
|
||||||
const { ARTIFACTS_DIR } = process.env;
|
const { ARTIFACTS_DIR } = process.env;
|
||||||
if (!ARTIFACTS_DIR) {
|
if (!ARTIFACTS_DIR) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
@ -299,7 +313,12 @@ export class Bootstrap {
|
||||||
|
|
||||||
await fs.mkdir(ARTIFACTS_DIR, { recursive: true });
|
await fs.mkdir(ARTIFACTS_DIR, { recursive: true });
|
||||||
|
|
||||||
const outDir = await fs.mkdtemp(path.join(ARTIFACTS_DIR, 'logs-'));
|
const normalizedPrefix = pathPrefix
|
||||||
|
? `-${normalizePath(pathPrefix.replace(/[ /]/g, '-'))}-`
|
||||||
|
: '';
|
||||||
|
const outDir = await fs.mkdtemp(
|
||||||
|
path.join(ARTIFACTS_DIR, `logs-${normalizedPrefix}`)
|
||||||
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(`Saving logs to ${outDir}`);
|
console.error(`Saving logs to ${outDir}`);
|
||||||
|
|
|
@ -67,10 +67,7 @@ describe('editing', function needsName() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,10 +54,7 @@ describe('senderKey', function needsName() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -118,10 +118,7 @@ describe('story/messaging', function unknownContacts() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,10 +40,7 @@ describe('unknown contacts', function unknownContacts() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,10 +56,7 @@ describe('pnp/accept gv2 invite', function needsName() {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function after() {
|
afterEach(async function after() {
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,10 +23,7 @@ describe('pnp/change number', function needsName() {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function after() {
|
afterEach(async function after() {
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -92,10 +92,7 @@ describe('pnp/merge', function needsName() {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function after() {
|
afterEach(async function after() {
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -62,10 +62,7 @@ describe('pnp/PNI Change', function needsName() {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function after() {
|
afterEach(async function after() {
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -86,10 +86,7 @@ describe('pnp/PNI Signature', function needsName() {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function after() {
|
afterEach(async function after() {
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -85,10 +85,7 @@ describe('pnp/send gv2 invite', function needsName() {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function after() {
|
afterEach(async function after() {
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -72,10 +72,7 @@ describe('pnp/username', function needsName() {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function after() {
|
afterEach(async function after() {
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -57,10 +57,7 @@ describe('story/no-sender-key', function needsName() {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function after() {
|
afterEach(async function after() {
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -64,10 +64,7 @@ describe('challenge/receipts', function challengeReceiptsTest() {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function after() {
|
afterEach(async function after() {
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,10 +22,7 @@ describe('storage service', function needsName() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,10 +25,7 @@ describe('storage service', function needsName() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,10 +27,7 @@ describe('storage service', function needsName() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,10 +22,7 @@ describe('storage service', function needsName() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,10 +26,7 @@ describe('storage service', function needsName() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -99,10 +99,7 @@ describe('storage service', function needsName() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentTest?.state !== 'passed') {
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||||
await bootstrap.saveLogs(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.close();
|
await app.close();
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
@ -251,15 +251,6 @@ export class SendMessageProtoError extends Error implements CallbackResultType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SignedPreKeyRotationError extends ReplayableError {
|
|
||||||
constructor() {
|
|
||||||
super({
|
|
||||||
name: 'SignedPreKeyRotationError',
|
|
||||||
message: 'Too many signed prekey rotation failures',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MessageError extends ReplayableError {
|
export class MessageError extends ReplayableError {
|
||||||
readonly httpError: HTTPError;
|
readonly httpError: HTTPError;
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IdentityKeys,
|
IdentityKeys,
|
||||||
|
KyberPreKeys,
|
||||||
PreKeys,
|
PreKeys,
|
||||||
SenderKeys,
|
SenderKeys,
|
||||||
Sessions,
|
Sessions,
|
||||||
|
@ -1758,6 +1759,7 @@ export default class MessageReceiver
|
||||||
|
|
||||||
const preKeyStore = new PreKeys({ ourUuid: destinationUuid });
|
const preKeyStore = new PreKeys({ ourUuid: destinationUuid });
|
||||||
const signedPreKeyStore = new SignedPreKeys({ ourUuid: destinationUuid });
|
const signedPreKeyStore = new SignedPreKeys({ ourUuid: destinationUuid });
|
||||||
|
const kyberPreKeyStore = new KyberPreKeys({ ourUuid: destinationUuid });
|
||||||
|
|
||||||
const sealedSenderIdentifier = envelope.sourceUuid;
|
const sealedSenderIdentifier = envelope.sourceUuid;
|
||||||
strictAssert(
|
strictAssert(
|
||||||
|
@ -1786,7 +1788,8 @@ export default class MessageReceiver
|
||||||
sessionStore,
|
sessionStore,
|
||||||
identityKeyStore,
|
identityKeyStore,
|
||||||
preKeyStore,
|
preKeyStore,
|
||||||
signedPreKeyStore
|
signedPreKeyStore,
|
||||||
|
kyberPreKeyStore
|
||||||
),
|
),
|
||||||
zone
|
zone
|
||||||
);
|
);
|
||||||
|
@ -1811,6 +1814,7 @@ export default class MessageReceiver
|
||||||
const { destinationUuid } = envelope;
|
const { destinationUuid } = envelope;
|
||||||
const preKeyStore = new PreKeys({ ourUuid: destinationUuid });
|
const preKeyStore = new PreKeys({ ourUuid: destinationUuid });
|
||||||
const signedPreKeyStore = new SignedPreKeys({ ourUuid: destinationUuid });
|
const signedPreKeyStore = new SignedPreKeys({ ourUuid: destinationUuid });
|
||||||
|
const kyberPreKeyStore = new KyberPreKeys({ ourUuid: destinationUuid });
|
||||||
|
|
||||||
strictAssert(identifier !== undefined, 'Empty identifier');
|
strictAssert(identifier !== undefined, 'Empty identifier');
|
||||||
strictAssert(sourceDevice !== undefined, 'Empty source device');
|
strictAssert(sourceDevice !== undefined, 'Empty source device');
|
||||||
|
@ -1903,7 +1907,8 @@ export default class MessageReceiver
|
||||||
sessionStore,
|
sessionStore,
|
||||||
identityKeyStore,
|
identityKeyStore,
|
||||||
preKeyStore,
|
preKeyStore,
|
||||||
signedPreKeyStore
|
signedPreKeyStore,
|
||||||
|
kyberPreKeyStore
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
zone
|
zone
|
||||||
|
@ -2105,17 +2110,18 @@ export default class MessageReceiver
|
||||||
msg: Proto.IStoryMessage,
|
msg: Proto.IStoryMessage,
|
||||||
sentMessage?: ProcessedSent
|
sentMessage?: ProcessedSent
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const logId = getEnvelopeId(envelope);
|
const envelopeId = getEnvelopeId(envelope);
|
||||||
|
const logId = `MessageReceiver.handleStoryMessage(${envelopeId})`;
|
||||||
|
|
||||||
logUnexpectedUrgentValue(envelope, 'story');
|
logUnexpectedUrgentValue(envelope, 'story');
|
||||||
|
|
||||||
if (getStoriesBlocked()) {
|
if (getStoriesBlocked()) {
|
||||||
log.info('MessageReceiver.handleStoryMessage: dropping', logId);
|
log.info(`${logId}: dropping`);
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('MessageReceiver.handleStoryMessage', logId);
|
log.info(`${logId} starting`);
|
||||||
|
|
||||||
const attachments: Array<ProcessedAttachment> = [];
|
const attachments: Array<ProcessedAttachment> = [];
|
||||||
let preview: ReadonlyArray<ProcessedPreview> | undefined;
|
let preview: ReadonlyArray<ProcessedPreview> | undefined;
|
||||||
|
@ -2150,11 +2156,7 @@ export default class MessageReceiver
|
||||||
|
|
||||||
const groupV2 = msg.group ? processGroupV2Context(msg.group) : undefined;
|
const groupV2 = msg.group ? processGroupV2Context(msg.group) : undefined;
|
||||||
if (groupV2 && this.isGroupBlocked(groupV2.id)) {
|
if (groupV2 && this.isGroupBlocked(groupV2.id)) {
|
||||||
log.warn(
|
log.warn(`${logId}: ignored; destined for blocked group`);
|
||||||
`MessageReceiver.handleStoryMessage: envelope ${getEnvelopeId(
|
|
||||||
envelope
|
|
||||||
)} ignored; destined for blocked group`
|
|
||||||
);
|
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2165,10 +2167,7 @@ export default class MessageReceiver
|
||||||
);
|
);
|
||||||
|
|
||||||
if (timeRemaining <= 0) {
|
if (timeRemaining <= 0) {
|
||||||
log.info(
|
log.info(`${logId}: story already expired`);
|
||||||
'MessageReceiver.handleStoryMessage: story already expired',
|
|
||||||
logId
|
|
||||||
);
|
|
||||||
this.removeFromCache(envelope);
|
this.removeFromCache(envelope);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2188,6 +2187,7 @@ export default class MessageReceiver
|
||||||
};
|
};
|
||||||
|
|
||||||
if (sentMessage && message.groupV2) {
|
if (sentMessage && message.groupV2) {
|
||||||
|
log.warn(`${logId}: envelope is a sent group story`);
|
||||||
const ev = new SentEvent(
|
const ev = new SentEvent(
|
||||||
{
|
{
|
||||||
destinationUuid: {
|
destinationUuid: {
|
||||||
|
@ -2220,6 +2220,7 @@ export default class MessageReceiver
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sentMessage) {
|
if (sentMessage) {
|
||||||
|
log.warn(`${logId}: envelope is a sent distribution list story`);
|
||||||
const { storyMessageRecipients } = sentMessage;
|
const { storyMessageRecipients } = sentMessage;
|
||||||
const recipients = storyMessageRecipients ?? [];
|
const recipients = storyMessageRecipients ?? [];
|
||||||
|
|
||||||
|
@ -2248,8 +2249,7 @@ export default class MessageReceiver
|
||||||
} else {
|
} else {
|
||||||
assertDev(
|
assertDev(
|
||||||
false,
|
false,
|
||||||
`MessageReceiver.handleStoryMessage(${logId}): missing ` +
|
`${logId}: missing distribution list id for: ${destinationUuid}`
|
||||||
`distribution list id for: ${destinationUuid}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2296,6 +2296,7 @@ export default class MessageReceiver
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.warn(`${logId}: envelope is a received story`);
|
||||||
const ev = new MessageEvent(
|
const ev = new MessageEvent(
|
||||||
{
|
{
|
||||||
source: envelope.source,
|
source: envelope.source,
|
||||||
|
@ -3241,6 +3242,7 @@ export default class MessageReceiver
|
||||||
{
|
{
|
||||||
identityKeyPair,
|
identityKeyPair,
|
||||||
signedPreKey,
|
signedPreKey,
|
||||||
|
lastResortKyberPreKey,
|
||||||
registrationId,
|
registrationId,
|
||||||
newE164,
|
newE164,
|
||||||
}: Proto.SyncMessage.IPniChangeNumber
|
}: Proto.SyncMessage.IPniChangeNumber
|
||||||
|
@ -3255,6 +3257,7 @@ export default class MessageReceiver
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TDOO: DESKTOP-5652
|
||||||
if (
|
if (
|
||||||
!Bytes.isNotEmpty(identityKeyPair) ||
|
!Bytes.isNotEmpty(identityKeyPair) ||
|
||||||
!Bytes.isNotEmpty(signedPreKey) ||
|
!Bytes.isNotEmpty(signedPreKey) ||
|
||||||
|
@ -3268,6 +3271,7 @@ export default class MessageReceiver
|
||||||
const manager = window.getAccountManager();
|
const manager = window.getAccountManager();
|
||||||
await manager.setPni(updatedPni.toString(), {
|
await manager.setPni(updatedPni.toString(), {
|
||||||
identityKeyPair,
|
identityKeyPair,
|
||||||
|
lastResortKyberPreKey: dropNull(lastResortKyberPreKey),
|
||||||
signedPreKey,
|
signedPreKey,
|
||||||
registrationId,
|
registrationId,
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,7 +25,7 @@ import type {
|
||||||
TextAttachmentType,
|
TextAttachmentType,
|
||||||
UploadedAttachmentType,
|
UploadedAttachmentType,
|
||||||
} from '../types/Attachment';
|
} from '../types/Attachment';
|
||||||
import type { UUID, TaggedUUIDStringType } from '../types/UUID';
|
import { type UUID, type TaggedUUIDStringType, UUIDKind } from '../types/UUID';
|
||||||
import type {
|
import type {
|
||||||
ChallengeType,
|
ChallengeType,
|
||||||
GetGroupLogOptionsType,
|
GetGroupLogOptionsType,
|
||||||
|
@ -53,7 +53,6 @@ import * as Bytes from '../Bytes';
|
||||||
import { getRandomBytes } from '../Crypto';
|
import { getRandomBytes } from '../Crypto';
|
||||||
import {
|
import {
|
||||||
MessageError,
|
MessageError,
|
||||||
SignedPreKeyRotationError,
|
|
||||||
SendMessageProtoError,
|
SendMessageProtoError,
|
||||||
HTTPError,
|
HTTPError,
|
||||||
NoSenderKeyError,
|
NoSenderKeyError,
|
||||||
|
@ -79,6 +78,7 @@ import {
|
||||||
numberToAddressType,
|
numberToAddressType,
|
||||||
} from '../types/EmbeddedContact';
|
} from '../types/EmbeddedContact';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
|
import { drop } from '../util/drop';
|
||||||
|
|
||||||
export type SendMetadataType = {
|
export type SendMetadataType = {
|
||||||
[identifier: string]: {
|
[identifier: string]: {
|
||||||
|
@ -956,27 +956,31 @@ export default class MessageSender {
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.sendMessageProto({
|
drop(
|
||||||
callback: (res: CallbackResultType) => {
|
this.sendMessageProto({
|
||||||
if (res.errors && res.errors.length > 0) {
|
callback: (res: CallbackResultType) => {
|
||||||
reject(new SendMessageProtoError(res));
|
if (res.errors && res.errors.length > 0) {
|
||||||
} else {
|
reject(new SendMessageProtoError(res));
|
||||||
resolve(res);
|
} else {
|
||||||
}
|
resolve(res);
|
||||||
},
|
}
|
||||||
contentHint,
|
},
|
||||||
groupId,
|
contentHint,
|
||||||
options,
|
groupId,
|
||||||
proto,
|
options,
|
||||||
recipients: messageOptions.recipients || [],
|
proto,
|
||||||
timestamp: messageOptions.timestamp,
|
recipients: messageOptions.recipients || [],
|
||||||
urgent,
|
timestamp: messageOptions.timestamp,
|
||||||
story,
|
urgent,
|
||||||
});
|
story,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessageProto({
|
// Note: all the other low-level sends call this, so it is a chokepoint for 1:1 sends
|
||||||
|
// The chokepoint for group sends is sendContentMessageToGroup
|
||||||
|
async sendMessageProto({
|
||||||
callback,
|
callback,
|
||||||
contentHint,
|
contentHint,
|
||||||
groupId,
|
groupId,
|
||||||
|
@ -998,13 +1002,26 @@ export default class MessageSender {
|
||||||
story?: boolean;
|
story?: boolean;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
urgent: boolean;
|
urgent: boolean;
|
||||||
}>): void {
|
}>): Promise<void> {
|
||||||
const rejections = window.textsecure.storage.get(
|
const accountManager = window.getAccountManager();
|
||||||
'signedKeyRotationRejected',
|
try {
|
||||||
0
|
if (accountManager.areKeysOutOfDate(UUIDKind.ACI)) {
|
||||||
);
|
log.warn(
|
||||||
if (rejections > 5) {
|
`sendMessageProto/${timestamp}: Keys are out of date; updating before send`
|
||||||
throw new SignedPreKeyRotationError();
|
);
|
||||||
|
await accountManager.maybeUpdateKeys(UUIDKind.ACI);
|
||||||
|
if (accountManager.areKeysOutOfDate(UUIDKind.ACI)) {
|
||||||
|
throw new Error('Keys still out of date after update');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// TODO: DESKTOP-5642
|
||||||
|
callback({
|
||||||
|
dataMessage: undefined,
|
||||||
|
editMessage: undefined,
|
||||||
|
errors: [error],
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const outgoing = new OutgoingMessage({
|
const outgoing = new OutgoingMessage({
|
||||||
|
@ -1022,8 +1039,10 @@ export default class MessageSender {
|
||||||
});
|
});
|
||||||
|
|
||||||
recipients.forEach(identifier => {
|
recipients.forEach(identifier => {
|
||||||
void this.queueJobForIdentifier(identifier, async () =>
|
drop(
|
||||||
outgoing.sendToIdentifier(identifier)
|
this.queueJobForIdentifier(identifier, async () =>
|
||||||
|
outgoing.sendToIdentifier(identifier)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1056,17 +1075,19 @@ export default class MessageSender {
|
||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sendMessageProto({
|
drop(
|
||||||
callback,
|
this.sendMessageProto({
|
||||||
contentHint,
|
callback,
|
||||||
groupId,
|
contentHint,
|
||||||
options,
|
groupId,
|
||||||
proto,
|
options,
|
||||||
recipients,
|
proto,
|
||||||
timestamp,
|
recipients,
|
||||||
urgent,
|
timestamp,
|
||||||
story,
|
urgent,
|
||||||
});
|
story,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1096,16 +1117,18 @@ export default class MessageSender {
|
||||||
resolve(res);
|
resolve(res);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.sendMessageProto({
|
drop(
|
||||||
callback,
|
this.sendMessageProto({
|
||||||
contentHint,
|
callback,
|
||||||
groupId,
|
contentHint,
|
||||||
options,
|
groupId,
|
||||||
proto,
|
options,
|
||||||
recipients: [identifier],
|
proto,
|
||||||
timestamp,
|
recipients: [identifier],
|
||||||
urgent,
|
timestamp,
|
||||||
});
|
urgent,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2036,18 +2059,20 @@ export default class MessageSender {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sendMessageProto({
|
drop(
|
||||||
callback,
|
this.sendMessageProto({
|
||||||
contentHint,
|
callback,
|
||||||
groupId,
|
contentHint,
|
||||||
options,
|
groupId,
|
||||||
proto,
|
options,
|
||||||
recipients: identifiers,
|
proto,
|
||||||
sendLogCallback,
|
recipients: identifiers,
|
||||||
story,
|
sendLogCallback,
|
||||||
timestamp,
|
story,
|
||||||
urgent,
|
timestamp,
|
||||||
});
|
urgent,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
ts/textsecure/Types.d.ts
vendored
2
ts/textsecure/Types.d.ts
vendored
|
@ -14,6 +14,7 @@ import type { RawBodyRange } from '../types/BodyRange';
|
||||||
export {
|
export {
|
||||||
IdentityKeyType,
|
IdentityKeyType,
|
||||||
IdentityKeyIdType,
|
IdentityKeyIdType,
|
||||||
|
KyberPreKeyType,
|
||||||
PreKeyIdType,
|
PreKeyIdType,
|
||||||
PreKeyType,
|
PreKeyType,
|
||||||
SenderKeyIdType,
|
SenderKeyIdType,
|
||||||
|
@ -286,6 +287,7 @@ export type IRequestHandler = {
|
||||||
export type PniKeyMaterialType = Readonly<{
|
export type PniKeyMaterialType = Readonly<{
|
||||||
identityKeyPair: Uint8Array;
|
identityKeyPair: Uint8Array;
|
||||||
signedPreKey: Uint8Array;
|
signedPreKey: Uint8Array;
|
||||||
|
lastResortKyberPreKey?: Uint8Array;
|
||||||
registrationId: number;
|
registrationId: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
// Copyright 2017 Signal Messenger, LLC
|
// Copyright 2017 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
import * as durations from '../util/durations';
|
import * as durations from '../util/durations';
|
||||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||||
import * as Registration from '../util/registration';
|
import * as Registration from '../util/registration';
|
||||||
import { UUIDKind } from '../types/UUID';
|
import { UUIDKind } from '../types/UUID';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
import * as Errors from '../types/errors';
|
||||||
|
|
||||||
const ROTATION_INTERVAL = 2 * durations.DAY;
|
const UPDATE_INTERVAL = 2 * durations.DAY;
|
||||||
|
const UPDATE_TIME_STORAGE_KEY = 'nextScheduledUpdateKeyTime';
|
||||||
|
|
||||||
export type MinimalEventsType = {
|
export type MinimalEventsType = {
|
||||||
on(event: 'timetravel', callback: () => void): void;
|
on(event: 'timetravel', callback: () => void): void;
|
||||||
|
@ -15,23 +19,20 @@ export type MinimalEventsType = {
|
||||||
|
|
||||||
let initComplete = false;
|
let initComplete = false;
|
||||||
|
|
||||||
export class RotateSignedPreKeyListener {
|
export class UpdateKeysListener {
|
||||||
public timeout: NodeJS.Timeout | undefined;
|
public timeout: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
protected scheduleRotationForNow(): void {
|
protected scheduleUpdateForNow(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
void window.textsecure.storage.put('nextSignedKeyRotationTime', now);
|
void window.textsecure.storage.put(UPDATE_TIME_STORAGE_KEY, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setTimeoutForNextRun(): void {
|
protected setTimeoutForNextRun(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const time = window.textsecure.storage.get(
|
const time = window.textsecure.storage.get(UPDATE_TIME_STORAGE_KEY, now);
|
||||||
'nextSignedKeyRotationTime',
|
|
||||||
now
|
|
||||||
);
|
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
'Next signed key rotation scheduled for',
|
'UpdateKeysListener: Next update scheduled for',
|
||||||
new Date(time).toISOString()
|
new Date(time).toISOString()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -44,31 +45,29 @@ export class RotateSignedPreKeyListener {
|
||||||
this.timeout = setTimeout(() => this.runWhenOnline(), waitTime);
|
this.timeout = setTimeout(() => this.runWhenOnline(), waitTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleNextRotation(): void {
|
private scheduleNextUpdate(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const nextTime = now + ROTATION_INTERVAL;
|
const nextTime = now + UPDATE_INTERVAL;
|
||||||
void window.textsecure.storage.put('nextSignedKeyRotationTime', nextTime);
|
void window.textsecure.storage.put(UPDATE_TIME_STORAGE_KEY, nextTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async run(): Promise<void> {
|
private async run(): Promise<void> {
|
||||||
log.info('Rotating signed prekey...');
|
log.info('UpdateKeysListener: Updating keys...');
|
||||||
try {
|
try {
|
||||||
const accountManager = window.getAccountManager();
|
const accountManager = window.getAccountManager();
|
||||||
await Promise.all([
|
|
||||||
accountManager.rotateSignedPreKey(UUIDKind.ACI),
|
|
||||||
accountManager.rotateSignedPreKey(UUIDKind.PNI),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// We try to update this whenever we remove a preKey; this is a fail-safe to ensure
|
await accountManager.maybeUpdateKeys(UUIDKind.ACI);
|
||||||
// we're always in good shape
|
await accountManager.maybeUpdateKeys(UUIDKind.PNI);
|
||||||
await Promise.all([
|
|
||||||
accountManager.refreshPreKeys(UUIDKind.ACI),
|
this.scheduleNextUpdate();
|
||||||
accountManager.refreshPreKeys(UUIDKind.PNI),
|
|
||||||
]);
|
|
||||||
this.scheduleNextRotation();
|
|
||||||
this.setTimeoutForNextRun();
|
this.setTimeoutForNextRun();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('rotateSignedPrekey() failed. Trying again in five minutes');
|
const errorString = isNumber(error.code)
|
||||||
|
? error.code.toString()
|
||||||
|
: Errors.toLogFormat(error);
|
||||||
|
log.error(
|
||||||
|
`UpdateKeysListener.run failure - trying again in five minutes ${errorString}`
|
||||||
|
);
|
||||||
setTimeout(() => this.setTimeoutForNextRun(), 5 * durations.MINUTE);
|
setTimeout(() => this.setTimeoutForNextRun(), 5 * durations.MINUTE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +76,9 @@ export class RotateSignedPreKeyListener {
|
||||||
if (window.navigator.onLine) {
|
if (window.navigator.onLine) {
|
||||||
void this.run();
|
void this.run();
|
||||||
} else {
|
} else {
|
||||||
log.info('We are offline; keys will be rotated when we are next online');
|
log.info(
|
||||||
|
'UpdateKeysListener: We are offline; will update keys when we are next online'
|
||||||
|
);
|
||||||
const listener = () => {
|
const listener = () => {
|
||||||
window.removeEventListener('online', listener);
|
window.removeEventListener('online', listener);
|
||||||
this.setTimeoutForNextRun();
|
this.setTimeoutForNextRun();
|
||||||
|
@ -88,17 +89,15 @@ export class RotateSignedPreKeyListener {
|
||||||
|
|
||||||
public static init(events: MinimalEventsType, newVersion: boolean): void {
|
public static init(events: MinimalEventsType, newVersion: boolean): void {
|
||||||
if (initComplete) {
|
if (initComplete) {
|
||||||
window.SignalContext.log.info(
|
window.SignalContext.log.info('UpdateKeysListener: Already initialized');
|
||||||
'Rotate signed prekey listener: Already initialized'
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
initComplete = true;
|
initComplete = true;
|
||||||
|
|
||||||
const listener = new RotateSignedPreKeyListener();
|
const listener = new UpdateKeysListener();
|
||||||
|
|
||||||
if (newVersion) {
|
if (newVersion) {
|
||||||
listener.scheduleRotationForNow();
|
listener.scheduleUpdateForNow();
|
||||||
}
|
}
|
||||||
listener.setTimeoutForNextRun();
|
listener.setTimeoutForNextRun();
|
||||||
|
|
|
@ -870,6 +870,11 @@ const attachmentV3Response = z.object({
|
||||||
|
|
||||||
export type AttachmentV3ResponseType = z.infer<typeof attachmentV3Response>;
|
export type AttachmentV3ResponseType = z.infer<typeof attachmentV3Response>;
|
||||||
|
|
||||||
|
export type ServerKeyCountType = {
|
||||||
|
count: number;
|
||||||
|
pqCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type WebAPIType = {
|
export type WebAPIType = {
|
||||||
startRegistration(): unknown;
|
startRegistration(): unknown;
|
||||||
finishRegistration(baton: unknown): void;
|
finishRegistration(baton: unknown): void;
|
||||||
|
@ -925,7 +930,7 @@ export type WebAPIType = {
|
||||||
deviceId?: number,
|
deviceId?: number,
|
||||||
options?: { accessKey?: string }
|
options?: { accessKey?: string }
|
||||||
) => Promise<ServerKeysType>;
|
) => Promise<ServerKeysType>;
|
||||||
getMyKeys: (uuidKind: UUIDKind) => Promise<number>;
|
getMyKeyCounts: (uuidKind: UUIDKind) => Promise<ServerKeyCountType>;
|
||||||
getOnboardingStoryManifest: () => Promise<{
|
getOnboardingStoryManifest: () => Promise<{
|
||||||
version: string;
|
version: string;
|
||||||
languages: Record<string, Array<string>>;
|
languages: Record<string, Array<string>>;
|
||||||
|
@ -998,7 +1003,7 @@ export type WebAPIType = {
|
||||||
) => Promise<ReserveUsernameResultType>;
|
) => Promise<ReserveUsernameResultType>;
|
||||||
confirmUsername(options: ConfirmUsernameOptionsType): Promise<void>;
|
confirmUsername(options: ConfirmUsernameOptionsType): Promise<void>;
|
||||||
registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>;
|
registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>;
|
||||||
registerKeys: (genKeys: KeysType, uuidKind: UUIDKind) => Promise<void>;
|
registerKeys: (genKeys: UploadKeysType, uuidKind: UUIDKind) => Promise<void>;
|
||||||
registerSupportForUnauthenticatedDelivery: () => Promise<void>;
|
registerSupportForUnauthenticatedDelivery: () => Promise<void>;
|
||||||
reportMessage: (options: ReportMessageOptionsType) => Promise<void>;
|
reportMessage: (options: ReportMessageOptionsType) => Promise<void>;
|
||||||
requestVerificationSMS: (number: string, token: string) => Promise<void>;
|
requestVerificationSMS: (number: string, token: string) => Promise<void>;
|
||||||
|
@ -1032,10 +1037,6 @@ export type WebAPIType = {
|
||||||
}
|
}
|
||||||
) => Promise<MultiRecipient200ResponseType>;
|
) => Promise<MultiRecipient200ResponseType>;
|
||||||
setPhoneNumberDiscoverability: (newValue: boolean) => Promise<void>;
|
setPhoneNumberDiscoverability: (newValue: boolean) => Promise<void>;
|
||||||
setSignedPreKey: (
|
|
||||||
signedPreKey: SignedPreKeyType,
|
|
||||||
uuidKind: UUIDKind
|
|
||||||
) => Promise<void>;
|
|
||||||
updateDeviceName: (deviceName: string) => Promise<void>;
|
updateDeviceName: (deviceName: string) => Promise<void>;
|
||||||
uploadAvatar: (
|
uploadAvatar: (
|
||||||
uploadAvatarRequestHeaders: UploadAvatarHeadersType,
|
uploadAvatarRequestHeaders: UploadAvatarHeadersType,
|
||||||
|
@ -1060,33 +1061,46 @@ export type WebAPIType = {
|
||||||
reconnect: () => Promise<void>;
|
reconnect: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SignedPreKeyType = {
|
export type UploadSignedPreKeyType = {
|
||||||
keyId: number;
|
keyId: number;
|
||||||
publicKey: Uint8Array;
|
publicKey: Uint8Array;
|
||||||
signature: Uint8Array;
|
signature: Uint8Array;
|
||||||
};
|
};
|
||||||
|
export type UploadPreKeyType = {
|
||||||
|
keyId: number;
|
||||||
|
publicKey: Uint8Array;
|
||||||
|
};
|
||||||
|
export type UploadKyberPreKeyType = UploadSignedPreKeyType;
|
||||||
|
|
||||||
export type KeysType = {
|
export type UploadKeysType = {
|
||||||
identityKey: Uint8Array;
|
identityKey: Uint8Array;
|
||||||
signedPreKey: SignedPreKeyType;
|
|
||||||
preKeys: Array<{
|
// If a field is not provided, the server won't update its data.
|
||||||
keyId: number;
|
preKeys?: Array<UploadPreKeyType>;
|
||||||
publicKey: Uint8Array;
|
pqPreKeys?: Array<UploadSignedPreKeyType>;
|
||||||
}>;
|
pqLastResortPreKey?: UploadSignedPreKeyType;
|
||||||
|
signedPreKey?: UploadSignedPreKeyType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServerKeysType = {
|
export type ServerKeysType = {
|
||||||
devices: Array<{
|
devices: Array<{
|
||||||
deviceId: number;
|
deviceId: number;
|
||||||
registrationId: number;
|
registrationId: number;
|
||||||
signedPreKey: {
|
|
||||||
|
// We'll get a 404 if none of these keys are provided; we'll have at least one
|
||||||
|
preKey?: {
|
||||||
|
keyId: number;
|
||||||
|
publicKey: Uint8Array;
|
||||||
|
};
|
||||||
|
signedPreKey?: {
|
||||||
keyId: number;
|
keyId: number;
|
||||||
publicKey: Uint8Array;
|
publicKey: Uint8Array;
|
||||||
signature: Uint8Array;
|
signature: Uint8Array;
|
||||||
};
|
};
|
||||||
preKey?: {
|
pqPreKey?: {
|
||||||
keyId: number;
|
keyId: number;
|
||||||
publicKey: Uint8Array;
|
publicKey: Uint8Array;
|
||||||
|
signature: Uint8Array;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
identityKey: Uint8Array;
|
identityKey: Uint8Array;
|
||||||
|
@ -1293,7 +1307,7 @@ export function initialize({
|
||||||
getIceServers,
|
getIceServers,
|
||||||
getKeysForIdentifier,
|
getKeysForIdentifier,
|
||||||
getKeysForIdentifierUnauth,
|
getKeysForIdentifierUnauth,
|
||||||
getMyKeys,
|
getMyKeyCounts,
|
||||||
getOnboardingStoryManifest,
|
getOnboardingStoryManifest,
|
||||||
getProfile,
|
getProfile,
|
||||||
getProfileUnauth,
|
getProfileUnauth,
|
||||||
|
@ -1331,7 +1345,6 @@ export function initialize({
|
||||||
sendMessagesUnauth,
|
sendMessagesUnauth,
|
||||||
sendWithSenderKey,
|
sendWithSenderKey,
|
||||||
setPhoneNumberDiscoverability,
|
setPhoneNumberDiscoverability,
|
||||||
setSignedPreKey,
|
|
||||||
startRegistration,
|
startRegistration,
|
||||||
unregisterRequestHandler,
|
unregisterRequestHandler,
|
||||||
updateDeviceName,
|
updateDeviceName,
|
||||||
|
@ -2052,30 +2065,74 @@ export function initialize({
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
signature: string;
|
signature: string;
|
||||||
};
|
};
|
||||||
|
type JSONPreKeyType = {
|
||||||
|
keyId: number;
|
||||||
|
publicKey: string;
|
||||||
|
};
|
||||||
|
type JSONKyberPreKeyType = {
|
||||||
|
keyId: number;
|
||||||
|
publicKey: string;
|
||||||
|
signature: string;
|
||||||
|
};
|
||||||
|
|
||||||
type JSONKeysType = {
|
type JSONKeysType = {
|
||||||
identityKey: string;
|
identityKey: string;
|
||||||
signedPreKey: JSONSignedPreKeyType;
|
preKeys?: Array<JSONPreKeyType>;
|
||||||
preKeys: Array<{
|
pqPreKeys?: Array<JSONKyberPreKeyType>;
|
||||||
keyId: number;
|
pqLastResortPreKey?: JSONKyberPreKeyType;
|
||||||
publicKey: string;
|
signedPreKey?: JSONSignedPreKeyType;
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function registerKeys(genKeys: KeysType, uuidKind: UUIDKind) {
|
async function registerKeys(genKeys: UploadKeysType, uuidKind: UUIDKind) {
|
||||||
const preKeys = genKeys.preKeys.map(key => ({
|
const preKeys = genKeys.preKeys?.map(key => ({
|
||||||
keyId: key.keyId,
|
keyId: key.keyId,
|
||||||
publicKey: Bytes.toBase64(key.publicKey),
|
publicKey: Bytes.toBase64(key.publicKey),
|
||||||
}));
|
}));
|
||||||
|
const pqPreKeys = genKeys.pqPreKeys?.map(key => ({
|
||||||
|
keyId: key.keyId,
|
||||||
|
publicKey: Bytes.toBase64(key.publicKey),
|
||||||
|
signature: Bytes.toBase64(key.signature),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (
|
||||||
|
!preKeys?.length &&
|
||||||
|
!pqPreKeys?.length &&
|
||||||
|
!genKeys.pqLastResortPreKey &&
|
||||||
|
!genKeys.signedPreKey
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'registerKeys: None of the four potential key types were provided!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (preKeys && preKeys.length === 0) {
|
||||||
|
throw new Error('registerKeys: Attempting to upload zero preKeys!');
|
||||||
|
}
|
||||||
|
if (pqPreKeys && pqPreKeys.length === 0) {
|
||||||
|
throw new Error('registerKeys: Attempting to upload zero pqPreKeys!');
|
||||||
|
}
|
||||||
|
|
||||||
const keys: JSONKeysType = {
|
const keys: JSONKeysType = {
|
||||||
identityKey: Bytes.toBase64(genKeys.identityKey),
|
identityKey: Bytes.toBase64(genKeys.identityKey),
|
||||||
signedPreKey: {
|
|
||||||
keyId: genKeys.signedPreKey.keyId,
|
|
||||||
publicKey: Bytes.toBase64(genKeys.signedPreKey.publicKey),
|
|
||||||
signature: Bytes.toBase64(genKeys.signedPreKey.signature),
|
|
||||||
},
|
|
||||||
preKeys,
|
preKeys,
|
||||||
|
pqPreKeys,
|
||||||
|
...(genKeys.pqLastResortPreKey
|
||||||
|
? {
|
||||||
|
pqLastResortPreKey: {
|
||||||
|
keyId: genKeys.pqLastResortPreKey.keyId,
|
||||||
|
publicKey: Bytes.toBase64(genKeys.pqLastResortPreKey.publicKey),
|
||||||
|
signature: Bytes.toBase64(genKeys.pqLastResortPreKey.signature),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null),
|
||||||
|
...(genKeys.signedPreKey
|
||||||
|
? {
|
||||||
|
signedPreKey: {
|
||||||
|
keyId: genKeys.signedPreKey.keyId,
|
||||||
|
publicKey: Bytes.toBase64(genKeys.signedPreKey.publicKey),
|
||||||
|
signature: Bytes.toBase64(genKeys.signedPreKey.signature),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null),
|
||||||
};
|
};
|
||||||
|
|
||||||
await _ajax({
|
await _ajax({
|
||||||
|
@ -2097,50 +2154,39 @@ export function initialize({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setSignedPreKey(
|
async function getMyKeyCounts(
|
||||||
signedPreKey: SignedPreKeyType,
|
|
||||||
uuidKind: UUIDKind
|
uuidKind: UUIDKind
|
||||||
) {
|
): Promise<ServerKeyCountType> {
|
||||||
await _ajax({
|
|
||||||
call: 'signed',
|
|
||||||
urlParameters: `?${uuidKindToQuery(uuidKind)}`,
|
|
||||||
httpType: 'PUT',
|
|
||||||
jsonData: {
|
|
||||||
keyId: signedPreKey.keyId,
|
|
||||||
publicKey: Bytes.toBase64(signedPreKey.publicKey),
|
|
||||||
signature: Bytes.toBase64(signedPreKey.signature),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerKeyCountType = {
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getMyKeys(uuidKind: UUIDKind): Promise<number> {
|
|
||||||
const result = (await _ajax({
|
const result = (await _ajax({
|
||||||
call: 'keys',
|
call: 'keys',
|
||||||
urlParameters: `?${uuidKindToQuery(uuidKind)}`,
|
urlParameters: `?${uuidKindToQuery(uuidKind)}`,
|
||||||
httpType: 'GET',
|
httpType: 'GET',
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
validateResponse: { count: 'number' },
|
validateResponse: { count: 'number', pqCount: 'number' },
|
||||||
})) as ServerKeyCountType;
|
})) as ServerKeyCountType;
|
||||||
|
|
||||||
return result.count;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerKeyResponseType = {
|
type ServerKeyResponseType = {
|
||||||
devices: Array<{
|
devices: Array<{
|
||||||
deviceId: number;
|
deviceId: number;
|
||||||
registrationId: number;
|
registrationId: number;
|
||||||
signedPreKey: {
|
|
||||||
|
// We'll get a 404 if none of these keys are provided; we'll have at least one
|
||||||
|
preKey?: {
|
||||||
|
keyId: number;
|
||||||
|
publicKey: string;
|
||||||
|
};
|
||||||
|
signedPreKey?: {
|
||||||
keyId: number;
|
keyId: number;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
signature: string;
|
signature: string;
|
||||||
};
|
};
|
||||||
preKey?: {
|
pqPreKey?: {
|
||||||
keyId: number;
|
keyId: number;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
|
signature: string;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
identityKey: string;
|
identityKey: string;
|
||||||
|
@ -2180,12 +2226,25 @@ export function initialize({
|
||||||
return {
|
return {
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
registrationId: device.registrationId,
|
registrationId: device.registrationId,
|
||||||
preKey,
|
...(preKey ? { preKey } : null),
|
||||||
signedPreKey: {
|
...(device.signedPreKey
|
||||||
keyId: device.signedPreKey.keyId,
|
? {
|
||||||
publicKey: Bytes.fromBase64(device.signedPreKey.publicKey),
|
signedPreKey: {
|
||||||
signature: Bytes.fromBase64(device.signedPreKey.signature),
|
keyId: device.signedPreKey.keyId,
|
||||||
},
|
publicKey: Bytes.fromBase64(device.signedPreKey.publicKey),
|
||||||
|
signature: Bytes.fromBase64(device.signedPreKey.signature),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null),
|
||||||
|
...(device.pqPreKey
|
||||||
|
? {
|
||||||
|
pqPreKey: {
|
||||||
|
keyId: device.pqPreKey.keyId,
|
||||||
|
publicKey: Bytes.fromBase64(device.pqPreKey.publicKey),
|
||||||
|
signature: Bytes.fromBase64(device.pqPreKey.signature),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2199,7 +2258,7 @@ export function initialize({
|
||||||
const keys = (await _ajax({
|
const keys = (await _ajax({
|
||||||
call: 'keys',
|
call: 'keys',
|
||||||
httpType: 'GET',
|
httpType: 'GET',
|
||||||
urlParameters: `/${identifier}/${deviceId || '*'}`,
|
urlParameters: `/${identifier}/${deviceId || '*'}?pq=true`,
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||||
})) as ServerKeyResponseType;
|
})) as ServerKeyResponseType;
|
||||||
|
@ -2214,7 +2273,7 @@ export function initialize({
|
||||||
const keys = (await _ajax({
|
const keys = (await _ajax({
|
||||||
call: 'keys',
|
call: 'keys',
|
||||||
httpType: 'GET',
|
httpType: 'GET',
|
||||||
urlParameters: `/${identifier}/${deviceId || '*'}`,
|
urlParameters: `/${identifier}/${deviceId || '*'}?pq=true`,
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||||
unauthenticated: true,
|
unauthenticated: true,
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ErrorCode,
|
ErrorCode,
|
||||||
|
KEMPublicKey,
|
||||||
LibSignalErrorBase,
|
LibSignalErrorBase,
|
||||||
PreKeyBundle,
|
PreKeyBundle,
|
||||||
processPreKeyBundle,
|
processPreKeyBundle,
|
||||||
|
@ -101,7 +102,8 @@ async function handleServerKeys(
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
response.devices.map(async device => {
|
response.devices.map(async device => {
|
||||||
const { deviceId, registrationId, preKey, signedPreKey } = device;
|
const { deviceId, registrationId, pqPreKey, preKey, signedPreKey } =
|
||||||
|
device;
|
||||||
if (
|
if (
|
||||||
devicesToUpdate !== undefined &&
|
devicesToUpdate !== undefined &&
|
||||||
!devicesToUpdate.includes(deviceId)
|
!devicesToUpdate.includes(deviceId)
|
||||||
|
@ -135,6 +137,14 @@ async function handleServerKeys(
|
||||||
Buffer.from(response.identityKey)
|
Buffer.from(response.identityKey)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pqPreKeyId = pqPreKey?.keyId || null;
|
||||||
|
const pqPreKeyPublic = pqPreKey
|
||||||
|
? KEMPublicKey.deserialize(Buffer.from(pqPreKey.publicKey))
|
||||||
|
: null;
|
||||||
|
const pqPreKeySignature = pqPreKey
|
||||||
|
? Buffer.from(pqPreKey.signature)
|
||||||
|
: null;
|
||||||
|
|
||||||
const preKeyBundle = PreKeyBundle.new(
|
const preKeyBundle = PreKeyBundle.new(
|
||||||
registrationId,
|
registrationId,
|
||||||
deviceId,
|
deviceId,
|
||||||
|
@ -143,7 +153,10 @@ async function handleServerKeys(
|
||||||
signedPreKey.keyId,
|
signedPreKey.keyId,
|
||||||
signedPreKeyObject,
|
signedPreKeyObject,
|
||||||
Buffer.from(signedPreKey.signature),
|
Buffer.from(signedPreKey.signature),
|
||||||
identityKey
|
identityKey,
|
||||||
|
pqPreKeyId,
|
||||||
|
pqPreKeyPublic,
|
||||||
|
pqPreKeySignature
|
||||||
);
|
);
|
||||||
|
|
||||||
const address = new QualifiedAddress(
|
const address = new QualifiedAddress(
|
||||||
|
|
15
ts/types/Storage.d.ts
vendored
15
ts/types/Storage.d.ts
vendored
|
@ -82,7 +82,12 @@ export type StorageAccessType = {
|
||||||
lastHeartbeat: number;
|
lastHeartbeat: number;
|
||||||
lastStartup: number;
|
lastStartup: number;
|
||||||
lastAttemptedToRefreshProfilesAt: number;
|
lastAttemptedToRefreshProfilesAt: number;
|
||||||
|
lastResortKeyUpdateTime: number;
|
||||||
|
lastResortKeyUpdateTimePNI: number;
|
||||||
maxPreKeyId: number;
|
maxPreKeyId: number;
|
||||||
|
maxPreKeyIdPNI: number;
|
||||||
|
maxKyberPreKeyId: number;
|
||||||
|
maxKyberPreKeyIdPNI: number;
|
||||||
number_id: string;
|
number_id: string;
|
||||||
password: string;
|
password: string;
|
||||||
profileKey: Uint8Array;
|
profileKey: Uint8Array;
|
||||||
|
@ -94,7 +99,9 @@ export type StorageAccessType = {
|
||||||
showStickerPickerHint: boolean;
|
showStickerPickerHint: boolean;
|
||||||
showStickersIntroduction: boolean;
|
showStickersIntroduction: boolean;
|
||||||
signedKeyId: number;
|
signedKeyId: number;
|
||||||
signedKeyRotationRejected: number;
|
signedKeyIdPNI: number;
|
||||||
|
signedKeyUpdateTime: number;
|
||||||
|
signedKeyUpdateTimePNI: number;
|
||||||
storageKey: string;
|
storageKey: string;
|
||||||
synced_at: number;
|
synced_at: number;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
|
@ -145,7 +152,7 @@ export type StorageAccessType = {
|
||||||
paymentAddress: string;
|
paymentAddress: string;
|
||||||
zoomFactor: ZoomFactorType;
|
zoomFactor: ZoomFactorType;
|
||||||
preferredLeftPaneWidth: number;
|
preferredLeftPaneWidth: number;
|
||||||
nextSignedKeyRotationTime: number;
|
nextScheduledUpdateKeyTime: number;
|
||||||
areWeASubscriber: boolean;
|
areWeASubscriber: boolean;
|
||||||
subscriberId: Uint8Array;
|
subscriberId: Uint8Array;
|
||||||
subscriberCurrencyCode: string;
|
subscriberCurrencyCode: string;
|
||||||
|
@ -153,9 +160,11 @@ export type StorageAccessType = {
|
||||||
keepMutedChatsArchived: boolean;
|
keepMutedChatsArchived: boolean;
|
||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
|
'challenge:retry-message-ids': never;
|
||||||
|
nextSignedKeyRotationTime: number;
|
||||||
senderCertificateWithUuid: never;
|
senderCertificateWithUuid: never;
|
||||||
signaling_key: never;
|
signaling_key: never;
|
||||||
'challenge:retry-message-ids': never;
|
signedKeyRotationRejected: number;
|
||||||
};
|
};
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
} from '../textsecure/OutgoingMessage';
|
} from '../textsecure/OutgoingMessage';
|
||||||
import { Address } from '../types/Address';
|
import { Address } from '../types/Address';
|
||||||
import { QualifiedAddress } from '../types/QualifiedAddress';
|
import { QualifiedAddress } from '../types/QualifiedAddress';
|
||||||
import { UUID } from '../types/UUID';
|
import { UUID, UUIDKind } from '../types/UUID';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
import { getValue, isEnabled } from '../RemoteConfig';
|
import { getValue, isEnabled } from '../RemoteConfig';
|
||||||
import type { UUIDStringType } from '../types/UUID';
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
|
@ -152,6 +152,7 @@ export async function sendToGroup({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: This is the group send chokepoint. The 1:1 send chokepoint is sendMessageProto.
|
||||||
export async function sendContentMessageToGroup({
|
export async function sendContentMessageToGroup({
|
||||||
contentHint,
|
contentHint,
|
||||||
contentMessage,
|
contentMessage,
|
||||||
|
@ -180,6 +181,18 @@ export async function sendContentMessageToGroup({
|
||||||
urgent: boolean;
|
urgent: boolean;
|
||||||
}): Promise<CallbackResultType> {
|
}): Promise<CallbackResultType> {
|
||||||
const logId = sendTarget.idForLogging();
|
const logId = sendTarget.idForLogging();
|
||||||
|
|
||||||
|
const accountManager = window.getAccountManager();
|
||||||
|
if (accountManager.areKeysOutOfDate(UUIDKind.ACI)) {
|
||||||
|
log.warn(
|
||||||
|
`sendToGroup/${logId}: Keys are out of date; updating before send`
|
||||||
|
);
|
||||||
|
await accountManager.maybeUpdateKeys(UUIDKind.ACI);
|
||||||
|
if (accountManager.areKeysOutOfDate(UUIDKind.ACI)) {
|
||||||
|
throw new Error('Keys still out of date after update');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
strictAssert(
|
strictAssert(
|
||||||
window.textsecure.messaging,
|
window.textsecure.messaging,
|
||||||
'sendContentMessageToGroup: textsecure.messaging not available!'
|
'sendContentMessageToGroup: textsecure.messaging not available!'
|
||||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -2276,28 +2276,20 @@
|
||||||
bindings "^1.5.0"
|
bindings "^1.5.0"
|
||||||
tar "^6.1.0"
|
tar "^6.1.0"
|
||||||
|
|
||||||
"@signalapp/libsignal-client@0.22.0":
|
"@signalapp/libsignal-client@0.27.0", "@signalapp/libsignal-client@^0.27.0":
|
||||||
version "0.22.0"
|
version "0.27.0"
|
||||||
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.22.0.tgz#d57441612df46f90df68fc5d9ad45b857b9d2c44"
|
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.27.0.tgz#012e254d42e4dcd752979419c048af65a3f1eed1"
|
||||||
integrity sha512-f1PJuxpcbmhvHxzbf0BvSJhNA3sqXrwnTf2GtfFB2CQoqTEiGCRYfyFZjwUBByiFFI5mTWKER6WGAw5AvG/3+A==
|
integrity sha512-XinrJ9R2veJM/u3CAaL/YN5Yid+ASfsSceQiL/Qr1vKCsMori0bWG6AzOBnDUx/Bnm6dcDBc15t8w31WkXOTVw==
|
||||||
dependencies:
|
dependencies:
|
||||||
node-gyp-build "^4.2.3"
|
node-gyp-build "^4.2.3"
|
||||||
uuid "^8.3.0"
|
uuid "^8.3.0"
|
||||||
|
|
||||||
"@signalapp/libsignal-client@^0.24.0":
|
"@signalapp/mock-server@3.1.0":
|
||||||
version "0.24.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.24.0.tgz#4c52194071f1b0f7e0ad27a3f091c881d624897a"
|
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-3.1.0.tgz#6f499cf1a396626901760b93e888bb5020983075"
|
||||||
integrity sha512-8IjvfD1wdkKcxwwM4KC1m8yWly5NJVAUaoiGMKiok9L+sD7HnY5DKpBXb/dpgkiSfSMdr3r4219+zFG1tq2UQQ==
|
integrity sha512-u6zz9PWV7NLP+RIz2hg4zl0UY34Ufj2pjQsPmIY//oVnu3PfIiyuKMIzPU7jPMSYq29uFbUQPypLJYOYP2dOiA==
|
||||||
dependencies:
|
dependencies:
|
||||||
node-gyp-build "^4.2.3"
|
"@signalapp/libsignal-client" "^0.27.0"
|
||||||
uuid "^8.3.0"
|
|
||||||
|
|
||||||
"@signalapp/mock-server@3.0.1":
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-3.0.1.tgz#9f5e7d8ed207af191eadc33667708013a332b522"
|
|
||||||
integrity sha512-TXATTeczq6O1apT3SNPYFYG8/Z9MMRzmRfwZgl5JyyfjJcR00+b2GSW0G5qVONeVr6r6wS5zbUBDhcZ8+b04OA==
|
|
||||||
dependencies:
|
|
||||||
"@signalapp/libsignal-client" "^0.24.0"
|
|
||||||
debug "^4.3.2"
|
debug "^4.3.2"
|
||||||
long "^4.0.0"
|
long "^4.0.0"
|
||||||
micro "^9.3.4"
|
micro "^9.3.4"
|
||||||
|
|
Loading…
Reference in a new issue