Handle PniChangeNumber

This commit is contained in:
Fedor Indutny 2022-07-28 09:35:29 -07:00 committed by Josh Perez
parent 412f07d2a2
commit 79b48115e6
32 changed files with 1086 additions and 485 deletions

View File

@ -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(

View File

@ -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"

View File

@ -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",

View File

@ -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 {

View File

@ -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();

View File

@ -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();

View File

@ -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 };

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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>;
};

View File

@ -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(() => {

View File

@ -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!');
}

View File

@ -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 {

View File

@ -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.
});
});
});

View File

@ -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();

View File

@ -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,
});

View File

@ -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');
});
});

View File

@ -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,

View File

@ -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
);
});
});

View File

@ -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`);
}
}
}

View File

@ -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(

View File

@ -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;
}>;

View File

@ -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(

View File

@ -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,
});
}
}

View File

@ -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
);

View File

@ -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;

View File

@ -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

View File

@ -35,8 +35,8 @@ export type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
export type IdentityKeyMap = Record<
string,
{
privKey: string;
pubKey: string;
privKey: Uint8Array;
pubKey: Uint8Array;
}
>;

View File

@ -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]> };

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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"