Handle PniChangeNumber
This commit is contained in:
parent
412f07d2a2
commit
79b48115e6
|
@ -389,7 +389,6 @@ async function prepareUrl(
|
|||
directoryV3Url: config.get<string | null>('directoryV3Url') || undefined,
|
||||
directoryV3MRENCLAVE:
|
||||
config.get<string | null>('directoryV3MRENCLAVE') || undefined,
|
||||
directoryV3Root: config.get<string | null>('directoryV3Root') || undefined,
|
||||
});
|
||||
if (!directoryConfig.success) {
|
||||
throw new Error(
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
"directoryV2PublicKey": null,
|
||||
"directoryV2CodeHashes": null,
|
||||
"directoryV3Url": "https://cdsi.staging.signal.org",
|
||||
"directoryV3MRENCLAVE": "e5eaa62da3514e8b37ccabddb87e52e7f319ccf5120a13f9e1b42b87ec9dd3dd",
|
||||
"directoryV3Root": "-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n",
|
||||
"directoryV3MRENCLAVE": "7b75dd6e862decef9b37132d54be082441917a7790e82fe44f9cf653de03a75f",
|
||||
"cdn": {
|
||||
"0": "https://cdn-staging.signal.org",
|
||||
"2": "https://cdn2-staging.signal.org"
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
"@indutny/frameless-titlebar": "2.3.5",
|
||||
"@popperjs/core": "2.9.2",
|
||||
"@react-spring/web": "9.4.5",
|
||||
"@signalapp/libsignal-client": "0.18.1",
|
||||
"@signalapp/libsignal-client": "0.19.1",
|
||||
"@sindresorhus/is": "0.8.0",
|
||||
"@types/fabric": "4.5.3",
|
||||
"abort-controller": "3.0.0",
|
||||
|
@ -190,7 +190,7 @@
|
|||
"@babel/preset-typescript": "7.17.12",
|
||||
"@electron/fuses": "1.5.0",
|
||||
"@mixer/parallel-prettier": "2.0.1",
|
||||
"@signalapp/mock-server": "2.1.0",
|
||||
"@signalapp/mock-server": "2.3.0",
|
||||
"@storybook/addon-a11y": "6.5.6",
|
||||
"@storybook/addon-actions": "6.5.6",
|
||||
"@storybook/addon-controls": "6.5.6",
|
||||
|
|
|
@ -35,7 +35,8 @@ message Envelope {
|
|||
optional uint64 serverTimestamp = 10;
|
||||
optional bool ephemeral = 12; // indicates that the message should not be persisted if the recipient is offline
|
||||
optional bool urgent = 14 [default=true]; // indicates that the content is considered timely by the sender; defaults to true so senders have to opt-out to say something isn't time critical
|
||||
// next: 15
|
||||
optional string updated_pni = 15;
|
||||
// next: 16
|
||||
}
|
||||
|
||||
message Content {
|
||||
|
@ -509,6 +510,12 @@ message SyncMessage {
|
|||
optional Type type = 1;
|
||||
}
|
||||
|
||||
message PniChangeNumber {
|
||||
optional bytes identityKeyPair = 1; // Serialized libsignal-client IdentityKeyPair
|
||||
optional bytes signedPreKey = 2; // Serialized libsignal-client SignedPreKeyRecord
|
||||
optional uint32 registrationId = 3;
|
||||
}
|
||||
|
||||
optional Sent sent = 1;
|
||||
optional Contacts contacts = 2;
|
||||
optional Groups groups = 3;
|
||||
|
@ -526,6 +533,7 @@ message SyncMessage {
|
|||
reserved 15; // not yet added
|
||||
repeated Viewed viewed = 16;
|
||||
optional PniIdentity pniIdentity = 17;
|
||||
optional PniChangeNumber pniChangeNumber = 18;
|
||||
}
|
||||
|
||||
message AttachmentPointer {
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import PQueue from 'p-queue';
|
||||
import { isNumber } from 'lodash';
|
||||
import { isNumber, omit } from 'lodash';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
Direction,
|
||||
IdentityKeyPair,
|
||||
PreKeyRecord,
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
|
@ -31,6 +32,7 @@ import type {
|
|||
IdentityKeyIdType,
|
||||
KeyPairType,
|
||||
OuterSignedPrekeyType,
|
||||
PniKeyMaterialType,
|
||||
PreKeyIdType,
|
||||
PreKeyType,
|
||||
SenderKeyIdType,
|
||||
|
@ -255,8 +257,8 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
for (const key of Object.keys(map.value)) {
|
||||
const { privKey, pubKey } = map.value[key];
|
||||
this.ourIdentityKeys.set(new UUID(key).toString(), {
|
||||
privKey: Bytes.fromBase64(privKey),
|
||||
pubKey: Bytes.fromBase64(pubKey),
|
||||
privKey,
|
||||
pubKey,
|
||||
});
|
||||
}
|
||||
})(),
|
||||
|
@ -461,7 +463,8 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
ourUuid: UUID,
|
||||
keyId: number,
|
||||
keyPair: KeyPairType,
|
||||
confirmed?: boolean
|
||||
confirmed?: boolean,
|
||||
createdAt = Date.now()
|
||||
): Promise<void> {
|
||||
if (!this.signedPreKeys) {
|
||||
throw new Error('storeSignedPreKey: this.signedPreKeys not yet cached!');
|
||||
|
@ -475,7 +478,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
keyId,
|
||||
publicKey: keyPair.pubKey,
|
||||
privateKey: keyPair.privKey,
|
||||
created_at: Date.now(),
|
||||
created_at: createdAt,
|
||||
confirmed: Boolean(confirmed),
|
||||
};
|
||||
|
||||
|
@ -1935,6 +1938,101 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
});
|
||||
}
|
||||
|
||||
async removeOurOldPni(oldPni: UUID): Promise<void> {
|
||||
const { storage } = window;
|
||||
|
||||
log.info(`SignalProtocolStore.removeOurOldPni(${oldPni})`);
|
||||
|
||||
// Update caches
|
||||
this.ourIdentityKeys.delete(oldPni.toString());
|
||||
this.ourRegistrationIds.delete(oldPni.toString());
|
||||
|
||||
const preKeyPrefix = `${oldPni.toString()}:`;
|
||||
if (this.preKeys) {
|
||||
for (const key of this.preKeys.keys()) {
|
||||
if (key.startsWith(preKeyPrefix)) {
|
||||
this.preKeys.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.signedPreKeys) {
|
||||
for (const key of this.signedPreKeys.keys()) {
|
||||
if (key.startsWith(preKeyPrefix)) {
|
||||
this.signedPreKeys.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
await Promise.all([
|
||||
storage.put(
|
||||
'identityKeyMap',
|
||||
omit(storage.get('identityKeyMap') || {}, oldPni.toString())
|
||||
),
|
||||
storage.put(
|
||||
'registrationIdMap',
|
||||
omit(storage.get('registrationIdMap') || {}, oldPni.toString())
|
||||
),
|
||||
window.Signal.Data.removePreKeysByUuid(oldPni.toString()),
|
||||
window.Signal.Data.removeSignedPreKeysByUuid(oldPni.toString()),
|
||||
]);
|
||||
}
|
||||
|
||||
async updateOurPniKeyMaterial(
|
||||
pni: UUID,
|
||||
{
|
||||
identityKeyPair: identityBytes,
|
||||
signedPreKey: signedPreKeyBytes,
|
||||
registrationId,
|
||||
}: PniKeyMaterialType
|
||||
): Promise<void> {
|
||||
log.info(`SignalProtocolStore.updateOurPniKeyMaterial(${pni})`);
|
||||
|
||||
const identityKeyPair = IdentityKeyPair.deserialize(
|
||||
Buffer.from(identityBytes)
|
||||
);
|
||||
const signedPreKey = SignedPreKeyRecord.deserialize(
|
||||
Buffer.from(signedPreKeyBytes)
|
||||
);
|
||||
|
||||
const { storage } = window;
|
||||
|
||||
const pniPublicKey = identityKeyPair.publicKey.serialize();
|
||||
const pniPrivateKey = identityKeyPair.privateKey.serialize();
|
||||
|
||||
// Update caches
|
||||
this.ourIdentityKeys.set(pni.toString(), {
|
||||
pubKey: pniPublicKey,
|
||||
privKey: pniPrivateKey,
|
||||
});
|
||||
this.ourRegistrationIds.set(pni.toString(), registrationId);
|
||||
|
||||
// Update database
|
||||
await Promise.all([
|
||||
storage.put('identityKeyMap', {
|
||||
...(storage.get('identityKeyMap') || {}),
|
||||
[pni.toString()]: {
|
||||
pubKey: pniPublicKey,
|
||||
privKey: pniPrivateKey,
|
||||
},
|
||||
}),
|
||||
storage.put('registrationIdMap', {
|
||||
...(storage.get('registrationIdMap') || {}),
|
||||
[pni.toString()]: registrationId,
|
||||
}),
|
||||
this.storeSignedPreKey(
|
||||
pni,
|
||||
signedPreKey.id(),
|
||||
{
|
||||
privKey: signedPreKey.privateKey().serialize(),
|
||||
pubKey: signedPreKey.publicKey().serialize(),
|
||||
},
|
||||
true,
|
||||
signedPreKey.timestamp()
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
async removeAllData(): Promise<void> {
|
||||
await window.Signal.Data.removeAll();
|
||||
await this.hydrateCaches();
|
||||
|
|
|
@ -79,7 +79,6 @@ import type {
|
|||
FetchLatestEvent,
|
||||
GroupEvent,
|
||||
KeysEvent,
|
||||
PNIIdentityEvent,
|
||||
MessageEvent,
|
||||
MessageEventData,
|
||||
MessageRequestResponseEvent,
|
||||
|
@ -395,10 +394,6 @@ export async function startApp(): Promise<void> {
|
|||
queuedEventListener(onFetchLatestSync)
|
||||
);
|
||||
messageReceiver.addEventListener('keys', queuedEventListener(onKeysSync));
|
||||
messageReceiver.addEventListener(
|
||||
'pniIdentity',
|
||||
queuedEventListener(onPNIIdentitySync)
|
||||
);
|
||||
messageReceiver.addEventListener(
|
||||
'storyRecipientUpdate',
|
||||
queuedEventListener(onStoryRecipientUpdate, false)
|
||||
|
@ -3670,15 +3665,6 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
async function onPNIIdentitySync(ev: PNIIdentityEvent) {
|
||||
ev.confirm();
|
||||
|
||||
log.info('onPNIIdentitySync: updating PNI keys');
|
||||
const manager = window.getAccountManager();
|
||||
const { privateKey: privKey, publicKey: pubKey } = ev.data;
|
||||
await manager.updatePNIIdentity({ privKey, pubKey });
|
||||
}
|
||||
|
||||
async function onMessageRequestResponse(ev: MessageRequestResponseEvent) {
|
||||
ev.confirm();
|
||||
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
|
||||
import './wrap';
|
||||
|
||||
import { signalservice as SignalService } from './compiled';
|
||||
import { signalservice as SignalService, signal as Signal } from './compiled';
|
||||
|
||||
export { SignalService };
|
||||
export { SignalService, Signal };
|
||||
|
|
|
@ -17,7 +17,7 @@ import * as log from '../logging/log';
|
|||
|
||||
export const GROUP_CREDENTIALS_KEY = 'groupCredentials';
|
||||
|
||||
type CredentialsDataType = Array<GroupCredentialType>;
|
||||
type CredentialsDataType = ReadonlyArray<GroupCredentialType>;
|
||||
type RequestDatesType = {
|
||||
startDayInMs: number;
|
||||
endDayInMs: number;
|
||||
|
@ -145,33 +145,40 @@ export async function maybeFetchNewCredentials(): Promise<void> {
|
|||
`${logId}: fetching credentials for ${startDayInMs} through ${endDayInMs}`
|
||||
);
|
||||
|
||||
// TODO(indutny): In the future we'd like to avoid this extra call all the time
|
||||
const { pni } = await server.whoami();
|
||||
if (!pni) {
|
||||
log.info(`${logId}: no PNI, returning early`);
|
||||
return;
|
||||
}
|
||||
|
||||
const serverPublicParamsBase64 = window.getServerPublicParams();
|
||||
const clientZKAuthOperations = getClientZkAuthOperations(
|
||||
serverPublicParamsBase64
|
||||
);
|
||||
const newCredentials = sortCredentials(
|
||||
await server.getGroupCredentials({ startDayInMs, endDayInMs })
|
||||
).map((item: GroupCredentialType) => {
|
||||
const authCredential = clientZKAuthOperations.receiveAuthCredentialWithPni(
|
||||
aci,
|
||||
pni,
|
||||
item.redemptionTime,
|
||||
new AuthCredentialWithPniResponse(Buffer.from(item.credential, 'base64'))
|
||||
);
|
||||
const credential = authCredential.serialize().toString('base64');
|
||||
|
||||
return {
|
||||
redemptionTime: item.redemptionTime * durations.SECOND,
|
||||
credential,
|
||||
};
|
||||
});
|
||||
const { pni, credentials: rawCredentials } = await server.getGroupCredentials(
|
||||
{ startDayInMs, endDayInMs }
|
||||
);
|
||||
strictAssert(pni, 'Server must give pni along with group credentials');
|
||||
|
||||
const localPni = window.storage.user.getUuid(UUIDKind.PNI);
|
||||
if (pni !== localPni?.toString()) {
|
||||
log.error(`${logId}: local PNI ${localPni}, does not match remote ${pni}`);
|
||||
}
|
||||
|
||||
const newCredentials = sortCredentials(rawCredentials).map(
|
||||
(item: GroupCredentialType) => {
|
||||
const authCredential =
|
||||
clientZKAuthOperations.receiveAuthCredentialWithPni(
|
||||
aci,
|
||||
pni,
|
||||
item.redemptionTime,
|
||||
new AuthCredentialWithPniResponse(
|
||||
Buffer.from(item.credential, 'base64')
|
||||
)
|
||||
);
|
||||
const credential = authCredential.serialize().toString('base64');
|
||||
|
||||
return {
|
||||
redemptionTime: item.redemptionTime * durations.SECOND,
|
||||
credential,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const today = toDayMillis(Date.now());
|
||||
const previousCleaned = previous
|
||||
|
|
449
ts/sql/Client.ts
449
ts/sql/Client.ts
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,7 @@
|
|||
// Copyright 2020-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type {
|
||||
ConversationAttributesType,
|
||||
MessageAttributesType,
|
||||
|
@ -15,7 +13,7 @@ import type { ConversationColorType, CustomColorType } from '../types/Colors';
|
|||
import type { ProcessGroupCallRingRequestResult } from '../types/Calling';
|
||||
import type { StorageAccessType } from '../types/Storage.d';
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import type { BodyRangesType } from '../types/Util';
|
||||
import type { BodyRangesType, BytesToStrings } from '../types/Util';
|
||||
import type { QualifiedAddressStringType } from '../types/QualifiedAddress';
|
||||
import type { UUIDStringType } from '../types/UUID';
|
||||
import type { BadgeType } from '../badges/types';
|
||||
|
@ -66,14 +64,27 @@ export type IdentityKeyType = {
|
|||
timestamp: number;
|
||||
verified: number;
|
||||
};
|
||||
export type StoredIdentityKeyType = {
|
||||
firstUse: boolean;
|
||||
id: UUIDStringType | `conversation:${string}`;
|
||||
nonblockingApproval: boolean;
|
||||
publicKey: string;
|
||||
timestamp: number;
|
||||
verified: number;
|
||||
};
|
||||
export type IdentityKeyIdType = IdentityKeyType['id'];
|
||||
|
||||
export type ItemKeyType = keyof StorageAccessType;
|
||||
export type AllItemsType = Partial<StorageAccessType>;
|
||||
export type StoredAllItemsType = Partial<BytesToStrings<StorageAccessType>>;
|
||||
export type ItemType<K extends ItemKeyType> = {
|
||||
id: K;
|
||||
value: StorageAccessType[K];
|
||||
};
|
||||
export type StoredItemType<K extends ItemKeyType> = {
|
||||
id: K;
|
||||
value: BytesToStrings<StorageAccessType[K]>;
|
||||
};
|
||||
export type MessageType = MessageAttributesType;
|
||||
export type MessageTypeUnhydrated = {
|
||||
json: string;
|
||||
|
@ -85,6 +96,13 @@ export type PreKeyType = {
|
|||
privateKey: Uint8Array;
|
||||
publicKey: Uint8Array;
|
||||
};
|
||||
export type StoredPreKeyType = {
|
||||
id: `${UUIDStringType}:${number}`;
|
||||
keyId: number;
|
||||
ourUuid: UUIDStringType;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
};
|
||||
export type PreKeyIdType = PreKeyType['id'];
|
||||
export type ServerSearchResultMessageType = {
|
||||
json: string;
|
||||
|
@ -149,6 +167,15 @@ export type SignedPreKeyType = {
|
|||
privateKey: Uint8Array;
|
||||
publicKey: Uint8Array;
|
||||
};
|
||||
export type StoredSignedPreKeyType = {
|
||||
confirmed: boolean;
|
||||
created_at: number;
|
||||
ourUuid: UUIDStringType;
|
||||
id: `${UUIDStringType}:${number}`;
|
||||
keyId: number;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
};
|
||||
export type SignedPreKeyIdType = SignedPreKeyType['id'];
|
||||
|
||||
export type StickerType = Readonly<{
|
||||
|
@ -205,6 +232,7 @@ export type UnprocessedType = {
|
|||
sourceUuid?: UUIDStringType;
|
||||
sourceDevice?: number;
|
||||
destinationUuid?: string;
|
||||
updatedPni?: string;
|
||||
serverGuid?: string;
|
||||
serverTimestamp?: number;
|
||||
decrypted?: string;
|
||||
|
@ -262,41 +290,49 @@ export type StoryReadType = Readonly<{
|
|||
storyReadDate: number;
|
||||
}>;
|
||||
|
||||
export type ReactionResultType = Pick<
|
||||
ReactionType,
|
||||
'targetAuthorUuid' | 'targetTimestamp' | 'messageId'
|
||||
> & { rowid: number };
|
||||
|
||||
export type GetUnreadByConversationAndMarkReadResultType = Array<
|
||||
{ originalReadStatus: ReadStatus | undefined } & Pick<
|
||||
MessageType,
|
||||
| 'id'
|
||||
| 'source'
|
||||
| 'sourceUuid'
|
||||
| 'sent_at'
|
||||
| 'type'
|
||||
| 'readStatus'
|
||||
| 'seenStatus'
|
||||
>
|
||||
>;
|
||||
|
||||
export type GetConversationRangeCenteredOnMessageResultType<Message> =
|
||||
Readonly<{
|
||||
older: Array<Message>;
|
||||
newer: Array<Message>;
|
||||
metrics: ConversationMetricsType;
|
||||
}>;
|
||||
|
||||
export type DataInterface = {
|
||||
close: () => Promise<void>;
|
||||
removeDB: () => Promise<void>;
|
||||
removeIndexedDBFiles: () => Promise<void>;
|
||||
|
||||
createOrUpdateIdentityKey: (data: IdentityKeyType) => Promise<void>;
|
||||
getIdentityKeyById: (
|
||||
id: IdentityKeyIdType
|
||||
) => Promise<IdentityKeyType | undefined>;
|
||||
bulkAddIdentityKeys: (array: Array<IdentityKeyType>) => Promise<void>;
|
||||
removeIdentityKeyById: (id: IdentityKeyIdType) => Promise<void>;
|
||||
removeAllIdentityKeys: () => Promise<void>;
|
||||
getAllIdentityKeys: () => Promise<Array<IdentityKeyType>>;
|
||||
|
||||
createOrUpdatePreKey: (data: PreKeyType) => Promise<void>;
|
||||
getPreKeyById: (id: PreKeyIdType) => Promise<PreKeyType | undefined>;
|
||||
bulkAddPreKeys: (array: Array<PreKeyType>) => Promise<void>;
|
||||
removePreKeyById: (id: PreKeyIdType) => Promise<void>;
|
||||
removePreKeysByUuid: (uuid: UUIDStringType) => Promise<void>;
|
||||
removeAllPreKeys: () => Promise<void>;
|
||||
getAllPreKeys: () => Promise<Array<PreKeyType>>;
|
||||
|
||||
createOrUpdateSignedPreKey: (data: SignedPreKeyType) => Promise<void>;
|
||||
getSignedPreKeyById: (
|
||||
id: SignedPreKeyIdType
|
||||
) => Promise<SignedPreKeyType | undefined>;
|
||||
bulkAddSignedPreKeys: (array: Array<SignedPreKeyType>) => Promise<void>;
|
||||
removeSignedPreKeyById: (id: SignedPreKeyIdType) => Promise<void>;
|
||||
removeSignedPreKeysByUuid: (uuid: UUIDStringType) => Promise<void>;
|
||||
removeAllSignedPreKeys: () => Promise<void>;
|
||||
getAllSignedPreKeys: () => Promise<Array<SignedPreKeyType>>;
|
||||
|
||||
createOrUpdateItem<K extends ItemKeyType>(data: ItemType<K>): Promise<void>;
|
||||
getItemById<K extends ItemKeyType>(id: K): Promise<ItemType<K> | undefined>;
|
||||
removeItemById: (id: ItemKeyType) => Promise<void>;
|
||||
removeAllItems: () => Promise<void>;
|
||||
getAllItems: () => Promise<AllItemsType>;
|
||||
removeItemById: (id: ItemKeyType) => Promise<void>;
|
||||
|
||||
createOrUpdateSenderKey: (key: SenderKeyType) => Promise<void>;
|
||||
getSenderKeyById: (id: SenderKeyIdType) => Promise<SenderKeyType | undefined>;
|
||||
|
@ -402,29 +438,12 @@ export type DataInterface = {
|
|||
newestUnreadAt: number;
|
||||
readAt?: number;
|
||||
storyId?: UUIDStringType;
|
||||
}) => Promise<
|
||||
Array<
|
||||
{ originalReadStatus: ReadStatus | undefined } & Pick<
|
||||
MessageType,
|
||||
| 'id'
|
||||
| 'readStatus'
|
||||
| 'seenStatus'
|
||||
| 'sent_at'
|
||||
| 'source'
|
||||
| 'sourceUuid'
|
||||
| 'type'
|
||||
>
|
||||
>
|
||||
>;
|
||||
}) => Promise<GetUnreadByConversationAndMarkReadResultType>;
|
||||
getUnreadReactionsAndMarkRead: (options: {
|
||||
conversationId: string;
|
||||
newestUnreadAt: number;
|
||||
storyId?: UUIDStringType;
|
||||
}) => Promise<
|
||||
Array<
|
||||
Pick<ReactionType, 'targetAuthorUuid' | 'targetTimestamp' | 'messageId'>
|
||||
>
|
||||
>;
|
||||
}) => Promise<Array<ReactionResultType>>;
|
||||
markReactionAsRead: (
|
||||
targetAuthorUuid: string,
|
||||
targetTimestamp: number
|
||||
|
@ -671,11 +690,36 @@ export type ServerInterface = DataInterface & {
|
|||
receivedAt: number;
|
||||
sentAt?: number;
|
||||
storyId: UUIDStringType | undefined;
|
||||
}) => Promise<{
|
||||
older: Array<MessageTypeUnhydrated>;
|
||||
newer: Array<MessageTypeUnhydrated>;
|
||||
metrics: ConversationMetricsType;
|
||||
}>;
|
||||
}) => Promise<
|
||||
GetConversationRangeCenteredOnMessageResultType<MessageTypeUnhydrated>
|
||||
>;
|
||||
|
||||
createOrUpdateIdentityKey: (data: StoredIdentityKeyType) => Promise<void>;
|
||||
getIdentityKeyById: (
|
||||
id: IdentityKeyIdType
|
||||
) => Promise<StoredIdentityKeyType | undefined>;
|
||||
bulkAddIdentityKeys: (array: Array<StoredIdentityKeyType>) => Promise<void>;
|
||||
getAllIdentityKeys: () => Promise<Array<StoredIdentityKeyType>>;
|
||||
|
||||
createOrUpdatePreKey: (data: StoredPreKeyType) => Promise<void>;
|
||||
getPreKeyById: (id: PreKeyIdType) => Promise<StoredPreKeyType | undefined>;
|
||||
bulkAddPreKeys: (array: Array<StoredPreKeyType>) => Promise<void>;
|
||||
getAllPreKeys: () => Promise<Array<StoredPreKeyType>>;
|
||||
|
||||
createOrUpdateSignedPreKey: (data: StoredSignedPreKeyType) => Promise<void>;
|
||||
getSignedPreKeyById: (
|
||||
id: SignedPreKeyIdType
|
||||
) => Promise<StoredSignedPreKeyType | undefined>;
|
||||
bulkAddSignedPreKeys: (array: Array<StoredSignedPreKeyType>) => Promise<void>;
|
||||
getAllSignedPreKeys: () => Promise<Array<StoredSignedPreKeyType>>;
|
||||
|
||||
createOrUpdateItem<K extends ItemKeyType>(
|
||||
data: StoredItemType<K>
|
||||
): Promise<void>;
|
||||
getItemById<K extends ItemKeyType>(
|
||||
id: K
|
||||
): Promise<StoredItemType<K> | undefined>;
|
||||
getAllItems: () => Promise<StoredAllItemsType>;
|
||||
|
||||
// Server-only
|
||||
|
||||
|
@ -744,11 +788,30 @@ export type ClientInterface = DataInterface & {
|
|||
receivedAt: number;
|
||||
sentAt?: number;
|
||||
storyId: UUIDStringType | undefined;
|
||||
}) => Promise<{
|
||||
older: Array<MessageAttributesType>;
|
||||
newer: Array<MessageAttributesType>;
|
||||
metrics: ConversationMetricsType;
|
||||
}>;
|
||||
}) => Promise<GetConversationRangeCenteredOnMessageResultType<MessageType>>;
|
||||
|
||||
createOrUpdateIdentityKey: (data: IdentityKeyType) => Promise<void>;
|
||||
getIdentityKeyById: (
|
||||
id: IdentityKeyIdType
|
||||
) => Promise<IdentityKeyType | undefined>;
|
||||
bulkAddIdentityKeys: (array: Array<IdentityKeyType>) => Promise<void>;
|
||||
getAllIdentityKeys: () => Promise<Array<IdentityKeyType>>;
|
||||
|
||||
createOrUpdatePreKey: (data: PreKeyType) => Promise<void>;
|
||||
getPreKeyById: (id: PreKeyIdType) => Promise<PreKeyType | undefined>;
|
||||
bulkAddPreKeys: (array: Array<PreKeyType>) => Promise<void>;
|
||||
getAllPreKeys: () => Promise<Array<PreKeyType>>;
|
||||
|
||||
createOrUpdateSignedPreKey: (data: SignedPreKeyType) => Promise<void>;
|
||||
getSignedPreKeyById: (
|
||||
id: SignedPreKeyIdType
|
||||
) => Promise<SignedPreKeyType | undefined>;
|
||||
bulkAddSignedPreKeys: (array: Array<SignedPreKeyType>) => Promise<void>;
|
||||
getAllSignedPreKeys: () => Promise<Array<SignedPreKeyType>>;
|
||||
|
||||
createOrUpdateItem<K extends ItemKeyType>(data: ItemType<K>): Promise<void>;
|
||||
getItemById<K extends ItemKeyType>(id: K): Promise<ItemType<K> | undefined>;
|
||||
getAllItems: () => Promise<AllItemsType>;
|
||||
|
||||
// Client-side only
|
||||
|
||||
|
@ -774,10 +837,10 @@ export type ClientInterface = DataInterface & {
|
|||
export type ClientJobType = {
|
||||
fnName: string;
|
||||
start: number;
|
||||
resolve?: Function;
|
||||
reject?: Function;
|
||||
resolve?: (value: unknown) => void;
|
||||
reject?: (error: Error) => void;
|
||||
|
||||
// Only in DEBUG mode
|
||||
complete?: boolean;
|
||||
args?: Array<any>;
|
||||
args?: ReadonlyArray<unknown>;
|
||||
};
|
||||
|
|
|
@ -71,22 +71,25 @@ import {
|
|||
import { updateSchema } from './migrations';
|
||||
|
||||
import type {
|
||||
AllItemsType,
|
||||
StoredAllItemsType,
|
||||
AttachmentDownloadJobType,
|
||||
ConversationMetricsType,
|
||||
ConversationType,
|
||||
DeleteSentProtoRecipientOptionsType,
|
||||
EmojiType,
|
||||
GetConversationRangeCenteredOnMessageResultType,
|
||||
GetUnreadByConversationAndMarkReadResultType,
|
||||
IdentityKeyIdType,
|
||||
IdentityKeyType,
|
||||
StoredIdentityKeyType,
|
||||
ItemKeyType,
|
||||
ItemType,
|
||||
StoredItemType,
|
||||
ConversationMessageStatsType,
|
||||
MessageMetricsType,
|
||||
MessageType,
|
||||
MessageTypeUnhydrated,
|
||||
PreKeyIdType,
|
||||
PreKeyType,
|
||||
ReactionResultType,
|
||||
StoredPreKeyType,
|
||||
ServerSearchResultMessageType,
|
||||
SenderKeyIdType,
|
||||
SenderKeyType,
|
||||
|
@ -100,7 +103,7 @@ import type {
|
|||
SessionIdType,
|
||||
SessionType,
|
||||
SignedPreKeyIdType,
|
||||
SignedPreKeyType,
|
||||
StoredSignedPreKeyType,
|
||||
StickerPackStatusType,
|
||||
StickerPackType,
|
||||
StickerType,
|
||||
|
@ -149,6 +152,7 @@ const dataInterface: ServerInterface = {
|
|||
getPreKeyById,
|
||||
bulkAddPreKeys,
|
||||
removePreKeyById,
|
||||
removePreKeysByUuid,
|
||||
removeAllPreKeys,
|
||||
getAllPreKeys,
|
||||
|
||||
|
@ -156,6 +160,7 @@ const dataInterface: ServerInterface = {
|
|||
getSignedPreKeyById,
|
||||
bulkAddSignedPreKeys,
|
||||
removeSignedPreKeyById,
|
||||
removeSignedPreKeysByUuid,
|
||||
removeAllSignedPreKeys,
|
||||
getAllSignedPreKeys,
|
||||
|
||||
|
@ -634,16 +639,18 @@ function getInstance(): Database {
|
|||
}
|
||||
|
||||
const IDENTITY_KEYS_TABLE = 'identityKeys';
|
||||
async function createOrUpdateIdentityKey(data: IdentityKeyType): Promise<void> {
|
||||
async function createOrUpdateIdentityKey(
|
||||
data: StoredIdentityKeyType
|
||||
): Promise<void> {
|
||||
return createOrUpdate(getInstance(), IDENTITY_KEYS_TABLE, data);
|
||||
}
|
||||
async function getIdentityKeyById(
|
||||
id: IdentityKeyIdType
|
||||
): Promise<IdentityKeyType | undefined> {
|
||||
): Promise<StoredIdentityKeyType | undefined> {
|
||||
return getById(getInstance(), IDENTITY_KEYS_TABLE, id);
|
||||
}
|
||||
async function bulkAddIdentityKeys(
|
||||
array: Array<IdentityKeyType>
|
||||
array: Array<StoredIdentityKeyType>
|
||||
): Promise<void> {
|
||||
return bulkAdd(getInstance(), IDENTITY_KEYS_TABLE, array);
|
||||
}
|
||||
|
@ -653,55 +660,67 @@ async function removeIdentityKeyById(id: IdentityKeyIdType): Promise<void> {
|
|||
async function removeAllIdentityKeys(): Promise<void> {
|
||||
return removeAllFromTable(getInstance(), IDENTITY_KEYS_TABLE);
|
||||
}
|
||||
async function getAllIdentityKeys(): Promise<Array<IdentityKeyType>> {
|
||||
async function getAllIdentityKeys(): Promise<Array<StoredIdentityKeyType>> {
|
||||
return getAllFromTable(getInstance(), IDENTITY_KEYS_TABLE);
|
||||
}
|
||||
|
||||
const PRE_KEYS_TABLE = 'preKeys';
|
||||
async function createOrUpdatePreKey(data: PreKeyType): Promise<void> {
|
||||
async function createOrUpdatePreKey(data: StoredPreKeyType): Promise<void> {
|
||||
return createOrUpdate(getInstance(), PRE_KEYS_TABLE, data);
|
||||
}
|
||||
async function getPreKeyById(
|
||||
id: PreKeyIdType
|
||||
): Promise<PreKeyType | undefined> {
|
||||
): Promise<StoredPreKeyType | undefined> {
|
||||
return getById(getInstance(), PRE_KEYS_TABLE, id);
|
||||
}
|
||||
async function bulkAddPreKeys(array: Array<PreKeyType>): Promise<void> {
|
||||
async function bulkAddPreKeys(array: Array<StoredPreKeyType>): Promise<void> {
|
||||
return bulkAdd(getInstance(), PRE_KEYS_TABLE, array);
|
||||
}
|
||||
async function removePreKeyById(id: PreKeyIdType): Promise<void> {
|
||||
return removeById(getInstance(), PRE_KEYS_TABLE, id);
|
||||
}
|
||||
async function removePreKeysByUuid(uuid: UUIDStringType): Promise<void> {
|
||||
const db = getInstance();
|
||||
db.prepare<Query>('DELETE FROM preKeys WHERE ourUuid IS $uuid;').run({
|
||||
uuid,
|
||||
});
|
||||
}
|
||||
async function removeAllPreKeys(): Promise<void> {
|
||||
return removeAllFromTable(getInstance(), PRE_KEYS_TABLE);
|
||||
}
|
||||
async function getAllPreKeys(): Promise<Array<PreKeyType>> {
|
||||
async function getAllPreKeys(): Promise<Array<StoredPreKeyType>> {
|
||||
return getAllFromTable(getInstance(), PRE_KEYS_TABLE);
|
||||
}
|
||||
|
||||
const SIGNED_PRE_KEYS_TABLE = 'signedPreKeys';
|
||||
async function createOrUpdateSignedPreKey(
|
||||
data: SignedPreKeyType
|
||||
data: StoredSignedPreKeyType
|
||||
): Promise<void> {
|
||||
return createOrUpdate(getInstance(), SIGNED_PRE_KEYS_TABLE, data);
|
||||
}
|
||||
async function getSignedPreKeyById(
|
||||
id: SignedPreKeyIdType
|
||||
): Promise<SignedPreKeyType | undefined> {
|
||||
): Promise<StoredSignedPreKeyType | undefined> {
|
||||
return getById(getInstance(), SIGNED_PRE_KEYS_TABLE, id);
|
||||
}
|
||||
async function bulkAddSignedPreKeys(
|
||||
array: Array<SignedPreKeyType>
|
||||
array: Array<StoredSignedPreKeyType>
|
||||
): Promise<void> {
|
||||
return bulkAdd(getInstance(), SIGNED_PRE_KEYS_TABLE, array);
|
||||
}
|
||||
async function removeSignedPreKeyById(id: SignedPreKeyIdType): Promise<void> {
|
||||
return removeById(getInstance(), SIGNED_PRE_KEYS_TABLE, id);
|
||||
}
|
||||
async function removeSignedPreKeysByUuid(uuid: UUIDStringType): Promise<void> {
|
||||
const db = getInstance();
|
||||
db.prepare<Query>('DELETE FROM signedPreKeys WHERE ourUuid IS $uuid;').run({
|
||||
uuid,
|
||||
});
|
||||
}
|
||||
async function removeAllSignedPreKeys(): Promise<void> {
|
||||
return removeAllFromTable(getInstance(), SIGNED_PRE_KEYS_TABLE);
|
||||
}
|
||||
async function getAllSignedPreKeys(): Promise<Array<SignedPreKeyType>> {
|
||||
async function getAllSignedPreKeys(): Promise<Array<StoredSignedPreKeyType>> {
|
||||
const db = getInstance();
|
||||
const rows: JSONRows = db
|
||||
.prepare<EmptyQuery>(
|
||||
|
@ -718,16 +737,16 @@ async function getAllSignedPreKeys(): Promise<Array<SignedPreKeyType>> {
|
|||
|
||||
const ITEMS_TABLE = 'items';
|
||||
async function createOrUpdateItem<K extends ItemKeyType>(
|
||||
data: ItemType<K>
|
||||
data: StoredItemType<K>
|
||||
): Promise<void> {
|
||||
return createOrUpdate(getInstance(), ITEMS_TABLE, data);
|
||||
}
|
||||
async function getItemById<K extends ItemKeyType>(
|
||||
id: K
|
||||
): Promise<ItemType<K> | undefined> {
|
||||
): Promise<StoredItemType<K> | undefined> {
|
||||
return getById(getInstance(), ITEMS_TABLE, id);
|
||||
}
|
||||
async function getAllItems(): Promise<AllItemsType> {
|
||||
async function getAllItems(): Promise<StoredAllItemsType> {
|
||||
const db = getInstance();
|
||||
const rows: JSONRows = db
|
||||
.prepare<EmptyQuery>('SELECT json FROM items ORDER BY id ASC;')
|
||||
|
@ -743,7 +762,7 @@ async function getAllItems(): Promise<AllItemsType> {
|
|||
result[id] = value;
|
||||
}
|
||||
|
||||
return result as unknown as AllItemsType;
|
||||
return result as unknown as StoredAllItemsType;
|
||||
}
|
||||
async function removeItemById(id: ItemKeyType): Promise<void> {
|
||||
return removeById(getInstance(), ITEMS_TABLE, id);
|
||||
|
@ -2097,20 +2116,7 @@ async function getUnreadByConversationAndMarkRead({
|
|||
newestUnreadAt: number;
|
||||
storyId?: UUIDStringType;
|
||||
readAt?: number;
|
||||
}): Promise<
|
||||
Array<
|
||||
{ originalReadStatus: ReadStatus | undefined } & Pick<
|
||||
MessageType,
|
||||
| 'id'
|
||||
| 'source'
|
||||
| 'sourceUuid'
|
||||
| 'sent_at'
|
||||
| 'type'
|
||||
| 'readStatus'
|
||||
| 'seenStatus'
|
||||
>
|
||||
>
|
||||
> {
|
||||
}): Promise<GetUnreadByConversationAndMarkReadResultType> {
|
||||
const db = getInstance();
|
||||
return db.transaction(() => {
|
||||
const expirationStartTimestamp = Math.min(Date.now(), readAt ?? Infinity);
|
||||
|
@ -2203,10 +2209,6 @@ async function getUnreadByConversationAndMarkRead({
|
|||
})();
|
||||
}
|
||||
|
||||
type ReactionResultType = Pick<
|
||||
ReactionType,
|
||||
'targetAuthorUuid' | 'targetTimestamp' | 'messageId'
|
||||
> & { rowid: number };
|
||||
async function getUnreadReactionsAndMarkRead({
|
||||
conversationId,
|
||||
newestUnreadAt,
|
||||
|
@ -2869,11 +2871,9 @@ async function getConversationRangeCenteredOnMessage({
|
|||
receivedAt: number;
|
||||
sentAt?: number;
|
||||
storyId: UUIDStringType | undefined;
|
||||
}): Promise<{
|
||||
older: Array<MessageTypeUnhydrated>;
|
||||
newer: Array<MessageTypeUnhydrated>;
|
||||
metrics: ConversationMetricsType;
|
||||
}> {
|
||||
}): Promise<
|
||||
GetConversationRangeCenteredOnMessageResultType<MessageTypeUnhydrated>
|
||||
> {
|
||||
const db = getInstance();
|
||||
|
||||
return db.transaction(() => {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Database } from 'better-sqlite3';
|
||||
|
||||
import type { LoggerType } from '../../types/Logging';
|
||||
|
||||
export default function updateToSchemaVersion64(
|
||||
currentVersion: number,
|
||||
db: Database,
|
||||
logger: LoggerType
|
||||
): void {
|
||||
if (currentVersion >= 64) {
|
||||
return;
|
||||
}
|
||||
|
||||
db.transaction(() => {
|
||||
db.exec(
|
||||
`
|
||||
ALTER TABLE preKeys
|
||||
ADD COLUMN ourUuid STRING
|
||||
GENERATED ALWAYS AS (json_extract(json, '$.ourUuid'));
|
||||
|
||||
CREATE INDEX preKeys_ourUuid ON preKeys (ourUuid);
|
||||
|
||||
ALTER TABLE signedPreKeys
|
||||
ADD COLUMN ourUuid STRING
|
||||
GENERATED ALWAYS AS (json_extract(json, '$.ourUuid'));
|
||||
|
||||
CREATE INDEX signedPreKeys_ourUuid ON signedPreKeys (ourUuid);
|
||||
`
|
||||
);
|
||||
|
||||
db.pragma('user_version = 64');
|
||||
})();
|
||||
|
||||
logger.info('updateToSchemaVersion64: success!');
|
||||
}
|
|
@ -39,6 +39,7 @@ import updateToSchemaVersion60 from './60-update-expiring-index';
|
|||
import updateToSchemaVersion61 from './61-distribution-list-storage';
|
||||
import updateToSchemaVersion62 from './62-add-urgent-to-send-log';
|
||||
import updateToSchemaVersion63 from './63-add-urgent-to-unprocessed';
|
||||
import updateToSchemaVersion64 from './64-uuid-column-for-pre-keys';
|
||||
|
||||
function updateToSchemaVersion1(
|
||||
currentVersion: number,
|
||||
|
@ -1941,6 +1942,7 @@ export const SCHEMA_VERSIONS = [
|
|||
updateToSchemaVersion61,
|
||||
updateToSchemaVersion62,
|
||||
updateToSchemaVersion63,
|
||||
updateToSchemaVersion64,
|
||||
];
|
||||
|
||||
export function updateSchema(db: Database, logger: LoggerType): void {
|
||||
|
|
|
@ -7,8 +7,12 @@ import chai, { assert } from 'chai';
|
|||
import chaiAsPromised from 'chai-as-promised';
|
||||
import {
|
||||
Direction,
|
||||
IdentityKeyPair,
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
SenderKeyRecord,
|
||||
SessionRecord,
|
||||
SignedPreKeyRecord,
|
||||
} from '@signalapp/libsignal-client';
|
||||
|
||||
import { signal } from '../protobuf/compiled';
|
||||
|
@ -18,7 +22,11 @@ import { Zone } from '../util/Zone';
|
|||
|
||||
import * as Bytes from '../Bytes';
|
||||
import { getRandomBytes, constantTimeEqual } from '../Crypto';
|
||||
import { clampPrivateKey, setPublicKeyTypeByte } from '../Curve';
|
||||
import {
|
||||
clampPrivateKey,
|
||||
setPublicKeyTypeByte,
|
||||
generateSignedPreKey,
|
||||
} from '../Curve';
|
||||
import type { SignalProtocolStore } from '../SignalProtocolStore';
|
||||
import { GLOBAL_ZONE } from '../SignalProtocolStore';
|
||||
import { Address } from '../types/Address';
|
||||
|
@ -134,8 +142,8 @@ describe('SignalProtocolStore', () => {
|
|||
window.storage.put('registrationIdMap', { [ourUuid.toString()]: 1337 });
|
||||
window.storage.put('identityKeyMap', {
|
||||
[ourUuid.toString()]: {
|
||||
privKey: Bytes.toBase64(identityKey.privKey),
|
||||
pubKey: Bytes.toBase64(identityKey.pubKey),
|
||||
privKey: identityKey.privKey,
|
||||
pubKey: identityKey.pubKey,
|
||||
},
|
||||
});
|
||||
await window.storage.fetch();
|
||||
|
@ -1766,4 +1774,80 @@ describe('SignalProtocolStore', () => {
|
|||
assert.strictEqual(items.length, 0);
|
||||
});
|
||||
});
|
||||
describe('removeOurOldPni/updateOurPniKeyMaterial', () => {
|
||||
beforeEach(async () => {
|
||||
await store.storePreKey(ourUuid, 2, testKey);
|
||||
await store.storeSignedPreKey(ourUuid, 3, testKey);
|
||||
});
|
||||
|
||||
it('removes old data and sets new', async () => {
|
||||
const oldPni = ourUuid;
|
||||
const newPni = UUID.generate();
|
||||
|
||||
const newIdentity = IdentityKeyPair.generate();
|
||||
|
||||
const data = generateSignedPreKey(
|
||||
{
|
||||
pubKey: newIdentity.publicKey.serialize(),
|
||||
privKey: newIdentity.privateKey.serialize(),
|
||||
},
|
||||
8201
|
||||
);
|
||||
const createdAt = Date.now() - 1241;
|
||||
const signedPreKey = SignedPreKeyRecord.new(
|
||||
data.keyId,
|
||||
createdAt,
|
||||
PublicKey.deserialize(Buffer.from(data.keyPair.pubKey)),
|
||||
PrivateKey.deserialize(Buffer.from(data.keyPair.privKey)),
|
||||
Buffer.from(data.signature)
|
||||
);
|
||||
|
||||
await store.removeOurOldPni(oldPni);
|
||||
await store.updateOurPniKeyMaterial(newPni, {
|
||||
identityKeyPair: newIdentity.serialize(),
|
||||
signedPreKey: signedPreKey.serialize(),
|
||||
registrationId: 5231,
|
||||
});
|
||||
|
||||
// Old data has to be removed
|
||||
assert.isUndefined(await store.getIdentityKeyPair(oldPni));
|
||||
assert.isUndefined(await store.getLocalRegistrationId(oldPni));
|
||||
assert.isUndefined(await store.loadPreKey(oldPni, 2));
|
||||
assert.isUndefined(await store.loadSignedPreKey(oldPni, 3));
|
||||
|
||||
// New data has to be added
|
||||
const storedIdentity = await store.getIdentityKeyPair(newPni);
|
||||
if (!storedIdentity) {
|
||||
throw new Error('New identity not found');
|
||||
}
|
||||
assert.isTrue(
|
||||
Bytes.areEqual(
|
||||
storedIdentity.privKey,
|
||||
newIdentity.privateKey.serialize()
|
||||
)
|
||||
);
|
||||
assert.isTrue(
|
||||
Bytes.areEqual(storedIdentity.pubKey, newIdentity.publicKey.serialize())
|
||||
);
|
||||
|
||||
const storedSignedPreKey = await store.loadSignedPreKey(newPni, 8201);
|
||||
if (!storedSignedPreKey) {
|
||||
throw new Error('New signed pre key not found');
|
||||
}
|
||||
assert.isTrue(
|
||||
Bytes.areEqual(
|
||||
storedSignedPreKey.publicKey().serialize(),
|
||||
data.keyPair.pubKey
|
||||
)
|
||||
);
|
||||
assert.isTrue(
|
||||
Bytes.areEqual(
|
||||
storedSignedPreKey.privateKey().serialize(),
|
||||
data.keyPair.privKey
|
||||
)
|
||||
);
|
||||
assert.strictEqual(storedSignedPreKey.timestamp(), createdAt);
|
||||
// Note: signature is ignored.
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { toBase64 } from '../../Bytes';
|
||||
import { constantTimeEqual } from '../../Crypto';
|
||||
import { generateKeyPair } from '../../Curve';
|
||||
import type { GeneratedKeysType } from '../../textsecure/AccountManager';
|
||||
|
@ -71,10 +70,7 @@ describe('Key generation', function thisNeeded() {
|
|||
before(async () => {
|
||||
const keyPair = generateKeyPair();
|
||||
await textsecure.storage.put('identityKeyMap', {
|
||||
[ourUuid.toString()]: {
|
||||
privKey: toBase64(keyPair.privKey),
|
||||
pubKey: toBase64(keyPair.pubKey),
|
||||
},
|
||||
[ourUuid.toString()]: keyPair,
|
||||
});
|
||||
await textsecure.storage.user.setUuidAndDeviceId(ourUuid.toString(), 1);
|
||||
await textsecure.storage.protocol.hydrateCaches();
|
||||
|
|
|
@ -330,8 +330,6 @@ export class Bootstrap {
|
|||
directoryV3Url: url,
|
||||
directoryV3MRENCLAVE:
|
||||
'51133fecb3fa18aaf0c8f64cb763656d3272d9faaacdb26ae7df082e414fb142',
|
||||
directoryV3Root:
|
||||
'-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n',
|
||||
|
||||
...this.options.extraConfig,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { UUIDKind } from '@signalapp/mock-server';
|
||||
import createDebug from 'debug';
|
||||
|
||||
import * as durations from '../../util/durations';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
import type { App } from '../bootstrap';
|
||||
|
||||
export const debug = createDebug('mock:test:change-number');
|
||||
|
||||
describe('change number', function needsName() {
|
||||
this.timeout(durations.MINUTE);
|
||||
|
||||
let bootstrap: Bootstrap;
|
||||
let app: App;
|
||||
|
||||
beforeEach(async () => {
|
||||
bootstrap = new Bootstrap();
|
||||
await bootstrap.init();
|
||||
app = await bootstrap.link();
|
||||
});
|
||||
|
||||
afterEach(async function after() {
|
||||
if (this.currentTest?.state !== 'passed') {
|
||||
await bootstrap.saveLogs();
|
||||
}
|
||||
|
||||
await app.close();
|
||||
await bootstrap.teardown();
|
||||
});
|
||||
|
||||
it('should accept sync message and update keys', async () => {
|
||||
const { server, phone, desktop, contacts } = bootstrap;
|
||||
|
||||
const [first] = contacts;
|
||||
|
||||
const window = await app.getWindow();
|
||||
|
||||
const leftPane = window.locator('.left-pane-wrapper');
|
||||
|
||||
debug('prepare a message for original PNI');
|
||||
const messageBefore = await first.encryptText(desktop, 'Before', {
|
||||
uuidKind: UUIDKind.PNI,
|
||||
});
|
||||
|
||||
debug('preparing change number');
|
||||
const changeNumber = await phone.prepareChangeNumber();
|
||||
|
||||
const newKey = await desktop.popSingleUseKey(UUIDKind.PNI);
|
||||
await first.addSingleUseKey(desktop, newKey, UUIDKind.PNI);
|
||||
|
||||
debug('prepare a message for updated PNI');
|
||||
const messageAfter = await first.encryptText(desktop, 'After', {
|
||||
uuidKind: UUIDKind.PNI,
|
||||
});
|
||||
|
||||
debug('sending all messages');
|
||||
await Promise.all([
|
||||
server.send(desktop, messageBefore),
|
||||
phone.sendChangeNumber(changeNumber),
|
||||
server.send(desktop, messageAfter),
|
||||
]);
|
||||
|
||||
debug('opening conversation with the first contact');
|
||||
await leftPane
|
||||
.locator(
|
||||
'_react=ConversationListItem' +
|
||||
`[title = ${JSON.stringify(first.profileName)}] ` +
|
||||
' >> "After"'
|
||||
)
|
||||
.click();
|
||||
|
||||
debug('done');
|
||||
});
|
||||
});
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { assert } from 'chai';
|
||||
import type { PrimaryDevice, Group } from '@signalapp/mock-server';
|
||||
import { StorageState, Proto } from '@signalapp/mock-server';
|
||||
import { StorageState, Proto, UUIDKind } from '@signalapp/mock-server';
|
||||
import createDebug from 'debug';
|
||||
|
||||
import * as durations from '../../util/durations';
|
||||
|
@ -55,7 +55,7 @@ describe('gv2', function needsName() {
|
|||
identityState: Proto.ContactRecord.IdentityState.VERIFIED,
|
||||
whitelisted: true,
|
||||
|
||||
identityKey: pniContact.pniPublicKey.serialize(),
|
||||
identityKey: pniContact.getPublicKey(UUIDKind.PNI).serialize(),
|
||||
|
||||
// Give PNI as the uuid!
|
||||
serviceUuid: pniContact.device.pni,
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { mapObjectWithSpec } from '../../util/mapObjectWithSpec';
|
||||
|
||||
describe('mapObjectWithSpec', () => {
|
||||
const increment = (value: number) => value + 1;
|
||||
|
||||
it('maps a single key/value pair', () => {
|
||||
assert.deepStrictEqual(mapObjectWithSpec('a', { a: 1 }, increment), {
|
||||
a: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('maps a multiple key/value pairs', () => {
|
||||
assert.deepStrictEqual(
|
||||
mapObjectWithSpec(['a', 'b'], { a: 1, b: 2 }, increment),
|
||||
{ a: 2, b: 3 }
|
||||
);
|
||||
});
|
||||
|
||||
it('maps a key with a value spec', () => {
|
||||
assert.deepStrictEqual(
|
||||
mapObjectWithSpec(
|
||||
{
|
||||
key: 'a',
|
||||
valueSpec: ['b', 'c'],
|
||||
},
|
||||
{ a: { b: 1, c: 2 } },
|
||||
increment
|
||||
),
|
||||
{ a: { b: 2, c: 3 } }
|
||||
);
|
||||
});
|
||||
|
||||
it('maps a map with a value spec', () => {
|
||||
assert.deepStrictEqual(
|
||||
mapObjectWithSpec(
|
||||
{
|
||||
isMap: true,
|
||||
valueSpec: ['b', 'c'],
|
||||
},
|
||||
{
|
||||
key1: { b: 1, c: 2 },
|
||||
key2: { b: 3, c: 4 },
|
||||
},
|
||||
increment
|
||||
),
|
||||
{
|
||||
key1: { b: 2, c: 3 },
|
||||
key2: { b: 4, c: 5 },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('map undefined to undefined', () => {
|
||||
assert.deepStrictEqual(
|
||||
mapObjectWithSpec('a', undefined, increment),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
|
@ -7,7 +7,7 @@ import { omit } from 'lodash';
|
|||
import EventTarget from './EventTarget';
|
||||
import type { WebAPIType } from './WebAPI';
|
||||
import { HTTPError } from './Errors';
|
||||
import type { KeyPairType } from './Types.d';
|
||||
import type { KeyPairType, PniKeyMaterialType } from './Types.d';
|
||||
import ProvisioningCipher from './ProvisioningCipher';
|
||||
import type { IncomingWebSocketRequest } from './WebsocketResources';
|
||||
import createTaskWithTimeout from './TaskWithTimeout';
|
||||
|
@ -332,6 +332,7 @@ export default class AccountManager extends EventTarget {
|
|||
}
|
||||
const keys = await this.generateKeys(SIGNED_KEY_GEN_BATCH_SIZE, uuidKind);
|
||||
await this.server.registerKeys(keys, uuidKind);
|
||||
await this.confirmKeys(keys, uuidKind);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -649,16 +650,10 @@ export default class AccountManager extends EventTarget {
|
|||
|
||||
const identityKeyMap = {
|
||||
...(storage.get('identityKeyMap') || {}),
|
||||
[ourUuid]: {
|
||||
pubKey: Bytes.toBase64(aciKeyPair.pubKey),
|
||||
privKey: Bytes.toBase64(aciKeyPair.privKey),
|
||||
},
|
||||
[ourUuid]: aciKeyPair,
|
||||
...(pniKeyPair
|
||||
? {
|
||||
[ourPni]: {
|
||||
pubKey: Bytes.toBase64(pniKeyPair.pubKey),
|
||||
privKey: Bytes.toBase64(pniKeyPair.privKey),
|
||||
},
|
||||
[ourPni]: pniKeyPair,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
@ -702,30 +697,18 @@ export default class AccountManager extends EventTarget {
|
|||
|
||||
log.info('AccountManager.updatePNIIdentity: generating new keys');
|
||||
|
||||
return this.queueTask(async () => {
|
||||
const keys = await this.generateKeys(
|
||||
SIGNED_KEY_GEN_BATCH_SIZE,
|
||||
UUIDKind.PNI,
|
||||
identityKeyPair
|
||||
);
|
||||
await this.server.registerKeys(keys, UUIDKind.PNI);
|
||||
await this.confirmKeys(keys, UUIDKind.PNI);
|
||||
|
||||
await this.queueTask(async () => {
|
||||
// Server has accepted our keys which means we have the latest PNI identity
|
||||
// now that doesn't conflict the PNI identity of the primary device.
|
||||
log.info(
|
||||
'AccountManager.updatePNIIdentity: updating identity key ' +
|
||||
'and registration id'
|
||||
);
|
||||
const { pubKey, privKey } = identityKeyPair;
|
||||
|
||||
const pni = storage.user.getCheckedUuid(UUIDKind.PNI);
|
||||
const identityKeyMap = {
|
||||
...(storage.get('identityKeyMap') || {}),
|
||||
[pni.toString()]: {
|
||||
pubKey: Bytes.toBase64(pubKey),
|
||||
privKey: Bytes.toBase64(privKey),
|
||||
},
|
||||
[pni.toString()]: identityKeyPair,
|
||||
};
|
||||
|
||||
const aci = storage.user.getCheckedUuid(UUIDKind.ACI);
|
||||
|
@ -744,6 +727,26 @@ export default class AccountManager extends EventTarget {
|
|||
|
||||
await storage.protocol.hydrateCaches();
|
||||
});
|
||||
|
||||
// Intentionally not awaiting becase `updatePNIIdentity` runs on an
|
||||
// Encrypted queue of MessageReceiver and we don't want to await remote
|
||||
// endpoints and block message processing.
|
||||
this.queueTask(async () => {
|
||||
try {
|
||||
const keys = await this.generateKeys(
|
||||
SIGNED_KEY_GEN_BATCH_SIZE,
|
||||
UUIDKind.PNI,
|
||||
identityKeyPair
|
||||
);
|
||||
await this.server.registerKeys(keys, UUIDKind.PNI);
|
||||
await this.confirmKeys(keys, UUIDKind.PNI);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'updatePNIIdentity: Failed to upload PNI prekeys. Moving on',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Takes the same object returned by generateKeys
|
||||
|
@ -841,30 +844,50 @@ export default class AccountManager extends EventTarget {
|
|||
this.dispatchEvent(new Event('registration'));
|
||||
}
|
||||
|
||||
async setPni(pni: string): Promise<void> {
|
||||
async setPni(pni: string, keyMaterial?: PniKeyMaterialType): Promise<void> {
|
||||
const { storage } = window.textsecure;
|
||||
|
||||
const oldPni = storage.user.getUuid(UUIDKind.PNI)?.toString();
|
||||
if (oldPni === pni) {
|
||||
if (oldPni === pni && !keyMaterial) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`AccountManager.setPni(${pni}): updating from ${oldPni}`);
|
||||
|
||||
if (oldPni) {
|
||||
await Promise.all([
|
||||
storage.put(
|
||||
'identityKeyMap',
|
||||
omit(storage.get('identityKeyMap') || {}, oldPni)
|
||||
),
|
||||
storage.put(
|
||||
'registrationIdMap',
|
||||
omit(storage.get('registrationIdMap') || {}, oldPni)
|
||||
),
|
||||
]);
|
||||
await storage.protocol.removeOurOldPni(new UUID(oldPni));
|
||||
}
|
||||
|
||||
log.info(`AccountManager.setPni: updating pni from ${oldPni} to ${pni}`);
|
||||
await storage.user.setPni(pni);
|
||||
|
||||
await storage.protocol.hydrateCaches();
|
||||
if (keyMaterial) {
|
||||
await storage.protocol.updateOurPniKeyMaterial(
|
||||
new UUID(pni),
|
||||
keyMaterial
|
||||
);
|
||||
|
||||
// Intentionally not awaiting since this is processed on encrypted queue
|
||||
// of MessageReceiver.
|
||||
this.queueTask(async () => {
|
||||
try {
|
||||
const keys = await this.generateKeys(
|
||||
SIGNED_KEY_GEN_BATCH_SIZE,
|
||||
UUIDKind.PNI
|
||||
);
|
||||
await this.server.registerKeys(keys, UUIDKind.PNI);
|
||||
await this.confirmKeys(keys, UUIDKind.PNI);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'setPni: Failed to upload PNI prekeys. Moving on',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// PNI has changed and credentials are no longer valid
|
||||
await storage.put('groupCredentials', []);
|
||||
} else {
|
||||
log.warn(`AccountManager.setPni(${pni}): no key material`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,6 @@ import {
|
|||
MessageRequestResponseEvent,
|
||||
FetchLatestEvent,
|
||||
KeysEvent,
|
||||
PNIIdentityEvent,
|
||||
StickerPackEvent,
|
||||
ReadSyncEvent,
|
||||
ViewSyncEvent,
|
||||
|
@ -256,8 +255,6 @@ export default class MessageReceiver
|
|||
|
||||
private stoppingProcessing?: boolean;
|
||||
|
||||
private pendingPNIIdentityEvent?: PNIIdentityEvent;
|
||||
|
||||
constructor({ server, storage, serverTrustRoot }: MessageReceiverOptions) {
|
||||
super();
|
||||
|
||||
|
@ -376,6 +373,14 @@ export default class MessageReceiver
|
|||
)
|
||||
)
|
||||
: ourUuid,
|
||||
updatedPni: decoded.updatedPni
|
||||
? new UUID(
|
||||
normalizeUuid(
|
||||
decoded.updatedPni,
|
||||
'MessageReceiver.handleRequest.updatedPni'
|
||||
)
|
||||
)
|
||||
: undefined,
|
||||
timestamp: decoded.timestamp?.toNumber(),
|
||||
content: dropNull(decoded.content),
|
||||
serverGuid: decoded.serverGuid,
|
||||
|
@ -535,11 +540,6 @@ export default class MessageReceiver
|
|||
handler: (ev: KeysEvent) => void
|
||||
): void;
|
||||
|
||||
public override addEventListener(
|
||||
name: 'pniIdentity',
|
||||
handler: (ev: PNIIdentityEvent) => void
|
||||
): void;
|
||||
|
||||
public override addEventListener(
|
||||
name: 'sticker-pack',
|
||||
handler: (ev: StickerPackEvent) => void
|
||||
|
@ -671,13 +671,6 @@ export default class MessageReceiver
|
|||
this.isEmptied = true;
|
||||
|
||||
this.maybeScheduleRetryTimeout();
|
||||
|
||||
// Emit PNI identity event after processing the queue
|
||||
const { pendingPNIIdentityEvent } = this;
|
||||
this.pendingPNIIdentityEvent = undefined;
|
||||
if (pendingPNIIdentityEvent) {
|
||||
await this.dispatchAndWait(pendingPNIIdentityEvent);
|
||||
}
|
||||
};
|
||||
|
||||
const waitForDecryptedQueue = async () => {
|
||||
|
@ -772,6 +765,9 @@ export default class MessageReceiver
|
|||
destinationUuid: new UUID(
|
||||
decoded.destinationUuid || item.destinationUuid || ourUuid.toString()
|
||||
),
|
||||
updatedPni: decoded.updatedPni
|
||||
? new UUID(decoded.updatedPni)
|
||||
: undefined,
|
||||
timestamp: decoded.timestamp?.toNumber(),
|
||||
content: dropNull(decoded.content),
|
||||
serverGuid: decoded.serverGuid,
|
||||
|
@ -902,16 +898,6 @@ export default class MessageReceiver
|
|||
items.map(async ({ data, envelope }) => {
|
||||
try {
|
||||
const { destinationUuid } = envelope;
|
||||
const uuidKind =
|
||||
this.storage.user.getOurUuidKind(destinationUuid);
|
||||
if (uuidKind === UUIDKind.Unknown) {
|
||||
log.warn(
|
||||
'MessageReceiver.decryptAndCacheBatch: ' +
|
||||
`Rejecting envelope ${getEnvelopeId(envelope)}, ` +
|
||||
`unknown uuid: ${destinationUuid}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let stores = storesMap.get(destinationUuid.toString());
|
||||
if (!stores) {
|
||||
|
@ -935,8 +921,7 @@ export default class MessageReceiver
|
|||
|
||||
const result = await this.queueEncryptedEnvelope(
|
||||
stores,
|
||||
envelope,
|
||||
uuidKind
|
||||
envelope
|
||||
);
|
||||
if (result.plaintext) {
|
||||
decrypted.push({
|
||||
|
@ -972,6 +957,7 @@ export default class MessageReceiver
|
|||
sourceUuid: envelope.sourceUuid,
|
||||
sourceDevice: envelope.sourceDevice,
|
||||
destinationUuid: envelope.destinationUuid.toString(),
|
||||
updatedPni: envelope.updatedPni?.toString(),
|
||||
serverGuid: envelope.serverGuid,
|
||||
serverTimestamp: envelope.serverTimestamp,
|
||||
decrypted: Bytes.toBase64(plaintext),
|
||||
|
@ -1089,13 +1075,23 @@ export default class MessageReceiver
|
|||
|
||||
private async queueEncryptedEnvelope(
|
||||
stores: LockedStores,
|
||||
envelope: ProcessedEnvelope,
|
||||
uuidKind: UUIDKind
|
||||
envelope: ProcessedEnvelope
|
||||
): Promise<DecryptResult> {
|
||||
let logId = getEnvelopeId(envelope);
|
||||
log.info(`queueing ${uuidKind} envelope`, logId);
|
||||
log.info('queueing envelope', logId);
|
||||
|
||||
const task = async (): Promise<DecryptResult> => {
|
||||
const { destinationUuid } = envelope;
|
||||
const uuidKind = this.storage.user.getOurUuidKind(destinationUuid);
|
||||
if (uuidKind === UUIDKind.Unknown) {
|
||||
log.warn(
|
||||
'MessageReceiver.decryptAndCacheBatch: ' +
|
||||
`Rejecting envelope ${getEnvelopeId(envelope)}, ` +
|
||||
`unknown uuid: ${destinationUuid}`
|
||||
);
|
||||
return { plaintext: undefined, envelope };
|
||||
}
|
||||
|
||||
const unsealedEnvelope = await this.unsealEnvelope(
|
||||
stores,
|
||||
envelope,
|
||||
|
@ -1311,6 +1307,19 @@ export default class MessageReceiver
|
|||
content.senderKeyDistributionMessage
|
||||
);
|
||||
}
|
||||
|
||||
// Some sync messages have to be fully processed in the middle of
|
||||
// decryption queue since subsequent envelopes use their key material.
|
||||
const { syncMessage } = content;
|
||||
if (syncMessage?.pniIdentity) {
|
||||
await this.handlePNIIdentity(envelope, syncMessage.pniIdentity);
|
||||
return { plaintext: undefined, envelope };
|
||||
}
|
||||
|
||||
if (syncMessage?.pniChangeNumber) {
|
||||
await this.handlePNIChangeNumber(envelope, syncMessage.pniChangeNumber);
|
||||
return { plaintext: undefined, envelope };
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'MessageReceiver.decryptEnvelope: Failed to process sender ' +
|
||||
|
@ -2575,6 +2584,9 @@ export default class MessageReceiver
|
|||
if (envelope.sourceDevice == ourDeviceId) {
|
||||
throw new Error('Received sync message from our own device');
|
||||
}
|
||||
if (syncMessage.pniIdentity) {
|
||||
return;
|
||||
}
|
||||
if (syncMessage.sent) {
|
||||
const sentMessage = syncMessage.sent;
|
||||
|
||||
|
@ -2615,7 +2627,7 @@ export default class MessageReceiver
|
|||
|
||||
if (this.isInvalidGroupData(sentMessage.message, envelope)) {
|
||||
this.removeFromCache(envelope);
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.checkGroupV1Data(sentMessage.message);
|
||||
|
@ -2633,11 +2645,11 @@ export default class MessageReceiver
|
|||
}
|
||||
if (syncMessage.contacts) {
|
||||
this.handleContacts(envelope, syncMessage.contacts);
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
if (syncMessage.groups) {
|
||||
this.handleGroups(envelope, syncMessage.groups);
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
if (syncMessage.blocked) {
|
||||
return this.handleBlocked(envelope, syncMessage.blocked);
|
||||
|
@ -2645,7 +2657,7 @@ export default class MessageReceiver
|
|||
if (syncMessage.request) {
|
||||
log.info('Got SyncMessage Request');
|
||||
this.removeFromCache(envelope);
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
if (syncMessage.read && syncMessage.read.length) {
|
||||
return this.handleRead(envelope, syncMessage.read);
|
||||
|
@ -2653,7 +2665,7 @@ export default class MessageReceiver
|
|||
if (syncMessage.verified) {
|
||||
log.info('Got verified sync message, dropping');
|
||||
this.removeFromCache(envelope);
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
if (syncMessage.configuration) {
|
||||
return this.handleConfiguration(envelope, syncMessage.configuration);
|
||||
|
@ -2682,9 +2694,6 @@ export default class MessageReceiver
|
|||
if (syncMessage.keys) {
|
||||
return this.handleKeys(envelope, syncMessage.keys);
|
||||
}
|
||||
if (syncMessage.pniIdentity) {
|
||||
return this.handlePNIIdentity(envelope, syncMessage.pniIdentity);
|
||||
}
|
||||
if (syncMessage.viewed && syncMessage.viewed.length) {
|
||||
return this.handleViewed(envelope, syncMessage.viewed);
|
||||
}
|
||||
|
@ -2693,7 +2702,6 @@ export default class MessageReceiver
|
|||
log.warn(
|
||||
`handleSyncMessage/${getEnvelopeId(envelope)}: Got empty SyncMessage`
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private async handleConfiguration(
|
||||
|
@ -2813,6 +2821,7 @@ export default class MessageReceiver
|
|||
return this.dispatchAndWait(ev);
|
||||
}
|
||||
|
||||
// Runs on TaskType.Encrypted queue
|
||||
private async handlePNIIdentity(
|
||||
envelope: ProcessedEnvelope,
|
||||
{ publicKey, privateKey }: Proto.SyncMessage.IPniIdentity
|
||||
|
@ -2823,22 +2832,47 @@ export default class MessageReceiver
|
|||
|
||||
if (!publicKey || !privateKey) {
|
||||
log.warn('MessageReceiver: empty pni identity sync message');
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const ev = new PNIIdentityEvent(
|
||||
{ publicKey, privateKey },
|
||||
this.removeFromCache.bind(this, envelope)
|
||||
);
|
||||
const manager = window.getAccountManager();
|
||||
await manager.updatePNIIdentity({ privKey: privateKey, pubKey: publicKey });
|
||||
}
|
||||
|
||||
if (this.isEmptied) {
|
||||
log.info('MessageReceiver: emitting pni identity sync message');
|
||||
return this.dispatchAndWait(ev);
|
||||
// Runs on TaskType.Encrypted queue
|
||||
private async handlePNIChangeNumber(
|
||||
envelope: ProcessedEnvelope,
|
||||
{
|
||||
identityKeyPair,
|
||||
signedPreKey,
|
||||
registrationId,
|
||||
}: Proto.SyncMessage.IPniChangeNumber
|
||||
): Promise<void> {
|
||||
log.info('MessageReceiver: got pni change number sync message');
|
||||
|
||||
logUnexpectedUrgentValue(envelope, 'pniIdentitySync');
|
||||
|
||||
const { updatedPni } = envelope;
|
||||
if (!updatedPni) {
|
||||
log.warn('MessageReceiver: missing pni in change number sync message');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('MessageReceiver: scheduling pni identity sync message');
|
||||
this.pendingPNIIdentityEvent?.confirm();
|
||||
this.pendingPNIIdentityEvent = ev;
|
||||
if (
|
||||
!Bytes.isNotEmpty(identityKeyPair) ||
|
||||
!Bytes.isNotEmpty(signedPreKey) ||
|
||||
!isNumber(registrationId)
|
||||
) {
|
||||
log.warn('MessageReceiver: empty pni change number sync message');
|
||||
return;
|
||||
}
|
||||
|
||||
const manager = window.getAccountManager();
|
||||
await manager.setPni(updatedPni.toString(), {
|
||||
identityKeyPair,
|
||||
signedPreKey,
|
||||
registrationId,
|
||||
});
|
||||
}
|
||||
|
||||
private async handleStickerPackOperation(
|
||||
|
|
|
@ -87,6 +87,7 @@ export type ProcessedEnvelope = Readonly<{
|
|||
sourceUuid?: UUIDStringType;
|
||||
sourceDevice?: number;
|
||||
destinationUuid: UUID;
|
||||
updatedPni?: UUID;
|
||||
timestamp: number;
|
||||
content?: Uint8Array;
|
||||
serverGuid: string;
|
||||
|
@ -270,3 +271,9 @@ export interface CallbackResultType {
|
|||
export interface IRequestHandler {
|
||||
handleRequest(request: IncomingWebSocketRequest): void;
|
||||
}
|
||||
|
||||
export type PniKeyMaterialType = Readonly<{
|
||||
identityKeyPair: Uint8Array;
|
||||
signedPreKey: Uint8Array;
|
||||
registrationId: number;
|
||||
}>;
|
||||
|
|
|
@ -566,7 +566,6 @@ type DirectoryV3OptionsType = Readonly<{
|
|||
directoryVersion: 3;
|
||||
directoryV3Url: string;
|
||||
directoryV3MRENCLAVE: string;
|
||||
directoryV3Root: string;
|
||||
}>;
|
||||
|
||||
type OptionalDirectoryFieldsType = {
|
||||
|
@ -578,7 +577,6 @@ type OptionalDirectoryFieldsType = {
|
|||
directoryV2CodeHashes?: unknown;
|
||||
directoryV3Url?: unknown;
|
||||
directoryV3MRENCLAVE?: unknown;
|
||||
directoryV3Root?: unknown;
|
||||
};
|
||||
|
||||
type DirectoryOptionsType = OptionalDirectoryFieldsType &
|
||||
|
@ -803,6 +801,11 @@ export type GetGroupCredentialsOptionsType = Readonly<{
|
|||
endDayInMs: number;
|
||||
}>;
|
||||
|
||||
export type GetGroupCredentialsResultType = Readonly<{
|
||||
pni?: string | null;
|
||||
credentials: ReadonlyArray<GroupCredentialType>;
|
||||
}>;
|
||||
|
||||
export type WebAPIType = {
|
||||
startRegistration(): unknown;
|
||||
finishRegistration(baton: unknown): void;
|
||||
|
@ -831,7 +834,7 @@ export type WebAPIType = {
|
|||
getGroupAvatar: (key: string) => Promise<Uint8Array>;
|
||||
getGroupCredentials: (
|
||||
options: GetGroupCredentialsOptionsType
|
||||
) => Promise<Array<GroupCredentialType>>;
|
||||
) => Promise<GetGroupCredentialsResultType>;
|
||||
getGroupExternalCredential: (
|
||||
options: GroupCredentialsType
|
||||
) => Promise<Proto.GroupExternalCredential>;
|
||||
|
@ -1205,8 +1208,7 @@ export function initialize({
|
|||
},
|
||||
});
|
||||
} else if (directoryConfig.directoryVersion === 3) {
|
||||
const { directoryV3Url, directoryV3MRENCLAVE, directoryV3Root } =
|
||||
directoryConfig;
|
||||
const { directoryV3Url, directoryV3MRENCLAVE } = directoryConfig;
|
||||
|
||||
cds = new CDSI({
|
||||
logger: log,
|
||||
|
@ -1214,7 +1216,6 @@ export function initialize({
|
|||
|
||||
url: directoryV3Url,
|
||||
mrenclave: directoryV3MRENCLAVE,
|
||||
root: directoryV3Root,
|
||||
certificateAuthority,
|
||||
version,
|
||||
|
||||
|
@ -2510,7 +2511,7 @@ export function initialize({
|
|||
async function getGroupCredentials({
|
||||
startDayInMs,
|
||||
endDayInMs,
|
||||
}: GetGroupCredentialsOptionsType): Promise<Array<GroupCredentialType>> {
|
||||
}: GetGroupCredentialsOptionsType): Promise<GetGroupCredentialsResultType> {
|
||||
const startDayInSeconds = startDayInMs / durations.SECOND;
|
||||
const endDayInSeconds = endDayInMs / durations.SECOND;
|
||||
const response = (await _ajax({
|
||||
|
@ -2522,7 +2523,7 @@ export function initialize({
|
|||
responseType: 'json',
|
||||
})) as CredentialResponseType;
|
||||
|
||||
return response.credentials;
|
||||
return response;
|
||||
}
|
||||
|
||||
async function getGroupExternalCredential(
|
||||
|
|
|
@ -10,20 +10,16 @@ import { CDSSocketManagerBase } from './CDSSocketManagerBase';
|
|||
|
||||
export type CDSIOptionsType = Readonly<{
|
||||
mrenclave: string;
|
||||
root: string;
|
||||
}> &
|
||||
CDSSocketManagerBaseOptionsType;
|
||||
|
||||
export class CDSI extends CDSSocketManagerBase<CDSISocket, CDSIOptionsType> {
|
||||
private readonly mrenclave: Buffer;
|
||||
|
||||
private readonly trustedCaCert: Buffer;
|
||||
|
||||
constructor(options: CDSIOptionsType) {
|
||||
super(options);
|
||||
|
||||
this.mrenclave = Buffer.from(Bytes.fromHex(options.mrenclave));
|
||||
this.trustedCaCert = Buffer.from(options.root);
|
||||
}
|
||||
|
||||
protected override getSocketUrl(): string {
|
||||
|
@ -37,7 +33,6 @@ export class CDSI extends CDSSocketManagerBase<CDSISocket, CDSIOptionsType> {
|
|||
logger: this.logger,
|
||||
socket,
|
||||
mrenclave: this.mrenclave,
|
||||
trustedCaCert: this.trustedCaCert,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,12 @@
|
|||
import { Cds2Client } from '@signalapp/libsignal-client';
|
||||
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { DAY } from '../../util/durations';
|
||||
import { SignalService as Proto } from '../../protobuf';
|
||||
import { CDSSocketBase, CDSSocketState } from './CDSSocketBase';
|
||||
import type { CDSSocketBaseOptionsType } from './CDSSocketBase';
|
||||
|
||||
export type CDSISocketOptionsType = Readonly<{
|
||||
mrenclave: Buffer;
|
||||
trustedCaCert: Buffer;
|
||||
}> &
|
||||
CDSSocketBaseOptionsType;
|
||||
|
||||
|
@ -30,15 +28,14 @@ export class CDSISocket extends CDSSocketBase<CDSISocketOptionsType> {
|
|||
await this.socketIterator.next();
|
||||
strictAssert(!done, 'CDSI socket closed before handshake');
|
||||
|
||||
const earliestValidTimestamp = new Date(Date.now() - DAY);
|
||||
const earliestValidTimestamp = new Date();
|
||||
|
||||
strictAssert(
|
||||
this.privCdsClient === undefined,
|
||||
'CDSI handshake called twice'
|
||||
);
|
||||
this.privCdsClient = Cds2Client.new_NOT_FOR_PRODUCTION(
|
||||
this.privCdsClient = Cds2Client.new(
|
||||
this.options.mrenclave,
|
||||
this.options.trustedCaCert,
|
||||
attestationMessage,
|
||||
earliestValidTimestamp
|
||||
);
|
||||
|
|
|
@ -357,20 +357,6 @@ export class KeysEvent extends ConfirmableEvent {
|
|||
}
|
||||
}
|
||||
|
||||
export type PNIIdentityEventData = Readonly<{
|
||||
publicKey: Uint8Array;
|
||||
privateKey: Uint8Array;
|
||||
}>;
|
||||
|
||||
export class PNIIdentityEvent extends ConfirmableEvent {
|
||||
constructor(
|
||||
public readonly data: PNIIdentityEventData,
|
||||
confirm: ConfirmCallback
|
||||
) {
|
||||
super('pniIdentity', confirm);
|
||||
}
|
||||
}
|
||||
|
||||
export type StickerPackEventData = Readonly<{
|
||||
id?: string;
|
||||
key?: string;
|
||||
|
|
|
@ -36,7 +36,6 @@ const directoryV3ConfigSchema = z.object({
|
|||
directoryVersion: z.literal(3),
|
||||
directoryV3Url: configRequiredStringSchema,
|
||||
directoryV3MRENCLAVE: configRequiredStringSchema,
|
||||
directoryV3Root: configRequiredStringSchema,
|
||||
});
|
||||
|
||||
export const directoryConfigSchema = z
|
||||
|
@ -50,7 +49,6 @@ export const directoryConfigSchema = z
|
|||
directoryV2Url: configOptionalUnknownSchema,
|
||||
directoryV3Url: configOptionalUnknownSchema,
|
||||
directoryV3MRENCLAVE: configOptionalUnknownSchema,
|
||||
directoryV3Root: configOptionalUnknownSchema,
|
||||
})
|
||||
.and(
|
||||
directoryV1ConfigSchema
|
||||
|
|
|
@ -35,8 +35,8 @@ export type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
|
|||
export type IdentityKeyMap = Record<
|
||||
string,
|
||||
{
|
||||
privKey: string;
|
||||
pubKey: string;
|
||||
privKey: Uint8Array;
|
||||
pubKey: Uint8Array;
|
||||
}
|
||||
>;
|
||||
|
||||
|
|
|
@ -61,3 +61,7 @@ type InternalAssertProps<
|
|||
export type AssertProps<Result, Value> = InternalAssertProps<Result, Value>;
|
||||
|
||||
export type UnwrapPromise<Value> = Value extends Promise<infer T> ? T : Value;
|
||||
|
||||
export type BytesToStrings<Value> = Value extends Uint8Array
|
||||
? string
|
||||
: { [Key in keyof Value]: BytesToStrings<Value[Key]> };
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { cloneDeep, get, set } from 'lodash';
|
||||
|
||||
export type ObjectMappingSpecType =
|
||||
| string
|
||||
| ReadonlyArray<ObjectMappingSpecType>
|
||||
| Readonly<{
|
||||
key: string;
|
||||
valueSpec: ObjectMappingSpecType;
|
||||
}>
|
||||
| Readonly<{
|
||||
isMap: true;
|
||||
valueSpec: ObjectMappingSpecType;
|
||||
}>;
|
||||
|
||||
export function mapObjectWithSpec<Input, Output>(
|
||||
spec: ObjectMappingSpecType,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
data: any,
|
||||
map: (value: Input) => Output,
|
||||
target = cloneDeep(data)
|
||||
): any {
|
||||
if (!data) {
|
||||
return target;
|
||||
}
|
||||
|
||||
if (typeof spec === 'string') {
|
||||
const value = get(data, spec);
|
||||
|
||||
if (value) {
|
||||
set(target, spec, map(value));
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
if ('isMap' in spec) {
|
||||
for (const key of Object.keys(data)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
target[key] = mapObjectWithSpec(
|
||||
spec.valueSpec,
|
||||
data[key],
|
||||
map,
|
||||
target[key]
|
||||
);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
if ('key' in spec) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
target[spec.key] = mapObjectWithSpec(
|
||||
spec.valueSpec,
|
||||
data[spec.key],
|
||||
map,
|
||||
target[spec.key]
|
||||
);
|
||||
return target;
|
||||
}
|
||||
|
||||
for (const key of spec) {
|
||||
mapObjectWithSpec(key, data, map, target);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
|
@ -13,7 +13,7 @@ export async function updateOurUsernameAndPni(): Promise<void> {
|
|||
);
|
||||
|
||||
const me = window.ConversationController.getOurConversationOrThrow();
|
||||
const { username, pni } = await server.whoami();
|
||||
const { username } = await server.whoami();
|
||||
|
||||
me.set({ username: dropNull(username) });
|
||||
window.Signal.Data.updateConversation(me.attributes);
|
||||
|
@ -23,6 +23,4 @@ export async function updateOurUsernameAndPni(): Promise<void> {
|
|||
manager,
|
||||
'updateOurUsernameAndPni: AccountManager not available'
|
||||
);
|
||||
|
||||
await manager.setPni(pni);
|
||||
}
|
||||
|
|
18
yarn.lock
18
yarn.lock
|
@ -1745,20 +1745,20 @@
|
|||
"@react-spring/shared" "~9.4.5"
|
||||
"@react-spring/types" "~9.4.5"
|
||||
|
||||
"@signalapp/libsignal-client@0.18.1", "@signalapp/libsignal-client@^0.18.1":
|
||||
version "0.18.1"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.18.1.tgz#6b499cdcc952f1981c6367f68484cf3275be3b31"
|
||||
integrity sha512-43NcTYpahImlWHBDaNFmn7QaeXZHkFkTtb4m+ZWgzU0mkS1M8V+orGen2XuDvNiu+9HQmW4Lg7FV1deXhWtIRA==
|
||||
"@signalapp/libsignal-client@0.19.1", "@signalapp/libsignal-client@^0.19.1":
|
||||
version "0.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.19.1.tgz#ccc12f0f034fe522940ba176a4518b4a05162b6d"
|
||||
integrity sha512-x6qMjLxoq39oXnoUI8vA1Pd+fitEuYdA828LwBZIY0gxdBVv4D2DNB2kmyiGH2KtqHucnsRSz216gBOWbI2Q/g==
|
||||
dependencies:
|
||||
node-gyp-build "^4.2.3"
|
||||
uuid "^8.3.0"
|
||||
|
||||
"@signalapp/mock-server@2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.1.0.tgz#25e42aad9ec2bc76c92173e7894f1aec4c2bb719"
|
||||
integrity sha512-AoeCRw8hOv4F+YQ6um/ZZiskaS1SsAXoQPgSMK69/xfDcPURJnVU6KB5Fy3chU2ZF0SZyWzS8vF3QguFKsIFWA==
|
||||
"@signalapp/mock-server@2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.3.0.tgz#96e75fbc8d8b5f62a6deec89f9fc1bb1a4210c89"
|
||||
integrity sha512-BNvT9/FbEBOKBd+2T8ImqOfKygCDLHl8bzQImRDrh3Umy7fmqYhTiuZb7WslCojEpjwH6fvZ6KfXkZDzkahqjg==
|
||||
dependencies:
|
||||
"@signalapp/libsignal-client" "^0.18.1"
|
||||
"@signalapp/libsignal-client" "^0.19.1"
|
||||
debug "^4.3.2"
|
||||
long "^4.0.0"
|
||||
micro "^9.3.4"
|
||||
|
|
Loading…
Reference in New Issue