Support for loading, storing, and using kyber keys in decryption
This commit is contained in:

committed by
Fedor Indutnyy

parent
c1580a5eb3
commit
b6445a6af0
File diff suppressed because it is too large
Load Diff
@@ -251,15 +251,6 @@ export class SendMessageProtoError extends Error implements CallbackResultType {
|
||||
}
|
||||
}
|
||||
|
||||
export class SignedPreKeyRotationError extends ReplayableError {
|
||||
constructor() {
|
||||
super({
|
||||
name: 'SignedPreKeyRotationError',
|
||||
message: 'Too many signed prekey rotation failures',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageError extends ReplayableError {
|
||||
readonly httpError: HTTPError;
|
||||
|
||||
|
@@ -32,6 +32,7 @@ import {
|
||||
|
||||
import {
|
||||
IdentityKeys,
|
||||
KyberPreKeys,
|
||||
PreKeys,
|
||||
SenderKeys,
|
||||
Sessions,
|
||||
@@ -1758,6 +1759,7 @@ export default class MessageReceiver
|
||||
|
||||
const preKeyStore = new PreKeys({ ourUuid: destinationUuid });
|
||||
const signedPreKeyStore = new SignedPreKeys({ ourUuid: destinationUuid });
|
||||
const kyberPreKeyStore = new KyberPreKeys({ ourUuid: destinationUuid });
|
||||
|
||||
const sealedSenderIdentifier = envelope.sourceUuid;
|
||||
strictAssert(
|
||||
@@ -1786,7 +1788,8 @@ export default class MessageReceiver
|
||||
sessionStore,
|
||||
identityKeyStore,
|
||||
preKeyStore,
|
||||
signedPreKeyStore
|
||||
signedPreKeyStore,
|
||||
kyberPreKeyStore
|
||||
),
|
||||
zone
|
||||
);
|
||||
@@ -1811,6 +1814,7 @@ export default class MessageReceiver
|
||||
const { destinationUuid } = envelope;
|
||||
const preKeyStore = new PreKeys({ ourUuid: destinationUuid });
|
||||
const signedPreKeyStore = new SignedPreKeys({ ourUuid: destinationUuid });
|
||||
const kyberPreKeyStore = new KyberPreKeys({ ourUuid: destinationUuid });
|
||||
|
||||
strictAssert(identifier !== undefined, 'Empty identifier');
|
||||
strictAssert(sourceDevice !== undefined, 'Empty source device');
|
||||
@@ -1903,7 +1907,8 @@ export default class MessageReceiver
|
||||
sessionStore,
|
||||
identityKeyStore,
|
||||
preKeyStore,
|
||||
signedPreKeyStore
|
||||
signedPreKeyStore,
|
||||
kyberPreKeyStore
|
||||
)
|
||||
),
|
||||
zone
|
||||
@@ -2105,17 +2110,18 @@ export default class MessageReceiver
|
||||
msg: Proto.IStoryMessage,
|
||||
sentMessage?: ProcessedSent
|
||||
): Promise<void> {
|
||||
const logId = getEnvelopeId(envelope);
|
||||
const envelopeId = getEnvelopeId(envelope);
|
||||
const logId = `MessageReceiver.handleStoryMessage(${envelopeId})`;
|
||||
|
||||
logUnexpectedUrgentValue(envelope, 'story');
|
||||
|
||||
if (getStoriesBlocked()) {
|
||||
log.info('MessageReceiver.handleStoryMessage: dropping', logId);
|
||||
log.info(`${logId}: dropping`);
|
||||
this.removeFromCache(envelope);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('MessageReceiver.handleStoryMessage', logId);
|
||||
log.info(`${logId} starting`);
|
||||
|
||||
const attachments: Array<ProcessedAttachment> = [];
|
||||
let preview: ReadonlyArray<ProcessedPreview> | undefined;
|
||||
@@ -2150,11 +2156,7 @@ export default class MessageReceiver
|
||||
|
||||
const groupV2 = msg.group ? processGroupV2Context(msg.group) : undefined;
|
||||
if (groupV2 && this.isGroupBlocked(groupV2.id)) {
|
||||
log.warn(
|
||||
`MessageReceiver.handleStoryMessage: envelope ${getEnvelopeId(
|
||||
envelope
|
||||
)} ignored; destined for blocked group`
|
||||
);
|
||||
log.warn(`${logId}: ignored; destined for blocked group`);
|
||||
this.removeFromCache(envelope);
|
||||
return;
|
||||
}
|
||||
@@ -2165,10 +2167,7 @@ export default class MessageReceiver
|
||||
);
|
||||
|
||||
if (timeRemaining <= 0) {
|
||||
log.info(
|
||||
'MessageReceiver.handleStoryMessage: story already expired',
|
||||
logId
|
||||
);
|
||||
log.info(`${logId}: story already expired`);
|
||||
this.removeFromCache(envelope);
|
||||
return;
|
||||
}
|
||||
@@ -2188,6 +2187,7 @@ export default class MessageReceiver
|
||||
};
|
||||
|
||||
if (sentMessage && message.groupV2) {
|
||||
log.warn(`${logId}: envelope is a sent group story`);
|
||||
const ev = new SentEvent(
|
||||
{
|
||||
destinationUuid: {
|
||||
@@ -2220,6 +2220,7 @@ export default class MessageReceiver
|
||||
}
|
||||
|
||||
if (sentMessage) {
|
||||
log.warn(`${logId}: envelope is a sent distribution list story`);
|
||||
const { storyMessageRecipients } = sentMessage;
|
||||
const recipients = storyMessageRecipients ?? [];
|
||||
|
||||
@@ -2248,8 +2249,7 @@ export default class MessageReceiver
|
||||
} else {
|
||||
assertDev(
|
||||
false,
|
||||
`MessageReceiver.handleStoryMessage(${logId}): missing ` +
|
||||
`distribution list id for: ${destinationUuid}`
|
||||
`${logId}: missing distribution list id for: ${destinationUuid}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2296,6 +2296,7 @@ export default class MessageReceiver
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn(`${logId}: envelope is a received story`);
|
||||
const ev = new MessageEvent(
|
||||
{
|
||||
source: envelope.source,
|
||||
@@ -3241,6 +3242,7 @@ export default class MessageReceiver
|
||||
{
|
||||
identityKeyPair,
|
||||
signedPreKey,
|
||||
lastResortKyberPreKey,
|
||||
registrationId,
|
||||
newE164,
|
||||
}: Proto.SyncMessage.IPniChangeNumber
|
||||
@@ -3255,6 +3257,7 @@ export default class MessageReceiver
|
||||
return;
|
||||
}
|
||||
|
||||
// TDOO: DESKTOP-5652
|
||||
if (
|
||||
!Bytes.isNotEmpty(identityKeyPair) ||
|
||||
!Bytes.isNotEmpty(signedPreKey) ||
|
||||
@@ -3268,6 +3271,7 @@ export default class MessageReceiver
|
||||
const manager = window.getAccountManager();
|
||||
await manager.setPni(updatedPni.toString(), {
|
||||
identityKeyPair,
|
||||
lastResortKyberPreKey: dropNull(lastResortKyberPreKey),
|
||||
signedPreKey,
|
||||
registrationId,
|
||||
});
|
||||
|
@@ -25,7 +25,7 @@ import type {
|
||||
TextAttachmentType,
|
||||
UploadedAttachmentType,
|
||||
} from '../types/Attachment';
|
||||
import type { UUID, TaggedUUIDStringType } from '../types/UUID';
|
||||
import { type UUID, type TaggedUUIDStringType, UUIDKind } from '../types/UUID';
|
||||
import type {
|
||||
ChallengeType,
|
||||
GetGroupLogOptionsType,
|
||||
@@ -53,7 +53,6 @@ import * as Bytes from '../Bytes';
|
||||
import { getRandomBytes } from '../Crypto';
|
||||
import {
|
||||
MessageError,
|
||||
SignedPreKeyRotationError,
|
||||
SendMessageProtoError,
|
||||
HTTPError,
|
||||
NoSenderKeyError,
|
||||
@@ -79,6 +78,7 @@ import {
|
||||
numberToAddressType,
|
||||
} from '../types/EmbeddedContact';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { drop } from '../util/drop';
|
||||
|
||||
export type SendMetadataType = {
|
||||
[identifier: string]: {
|
||||
@@ -956,27 +956,31 @@ export default class MessageSender {
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sendMessageProto({
|
||||
callback: (res: CallbackResultType) => {
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
reject(new SendMessageProtoError(res));
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
},
|
||||
contentHint,
|
||||
groupId,
|
||||
options,
|
||||
proto,
|
||||
recipients: messageOptions.recipients || [],
|
||||
timestamp: messageOptions.timestamp,
|
||||
urgent,
|
||||
story,
|
||||
});
|
||||
drop(
|
||||
this.sendMessageProto({
|
||||
callback: (res: CallbackResultType) => {
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
reject(new SendMessageProtoError(res));
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
},
|
||||
contentHint,
|
||||
groupId,
|
||||
options,
|
||||
proto,
|
||||
recipients: messageOptions.recipients || [],
|
||||
timestamp: messageOptions.timestamp,
|
||||
urgent,
|
||||
story,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
sendMessageProto({
|
||||
// Note: all the other low-level sends call this, so it is a chokepoint for 1:1 sends
|
||||
// The chokepoint for group sends is sendContentMessageToGroup
|
||||
async sendMessageProto({
|
||||
callback,
|
||||
contentHint,
|
||||
groupId,
|
||||
@@ -998,13 +1002,26 @@ export default class MessageSender {
|
||||
story?: boolean;
|
||||
timestamp: number;
|
||||
urgent: boolean;
|
||||
}>): void {
|
||||
const rejections = window.textsecure.storage.get(
|
||||
'signedKeyRotationRejected',
|
||||
0
|
||||
);
|
||||
if (rejections > 5) {
|
||||
throw new SignedPreKeyRotationError();
|
||||
}>): Promise<void> {
|
||||
const accountManager = window.getAccountManager();
|
||||
try {
|
||||
if (accountManager.areKeysOutOfDate(UUIDKind.ACI)) {
|
||||
log.warn(
|
||||
`sendMessageProto/${timestamp}: Keys are out of date; updating before send`
|
||||
);
|
||||
await accountManager.maybeUpdateKeys(UUIDKind.ACI);
|
||||
if (accountManager.areKeysOutOfDate(UUIDKind.ACI)) {
|
||||
throw new Error('Keys still out of date after update');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: DESKTOP-5642
|
||||
callback({
|
||||
dataMessage: undefined,
|
||||
editMessage: undefined,
|
||||
errors: [error],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const outgoing = new OutgoingMessage({
|
||||
@@ -1022,8 +1039,10 @@ export default class MessageSender {
|
||||
});
|
||||
|
||||
recipients.forEach(identifier => {
|
||||
void this.queueJobForIdentifier(identifier, async () =>
|
||||
outgoing.sendToIdentifier(identifier)
|
||||
drop(
|
||||
this.queueJobForIdentifier(identifier, async () =>
|
||||
outgoing.sendToIdentifier(identifier)
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1056,17 +1075,19 @@ export default class MessageSender {
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
this.sendMessageProto({
|
||||
callback,
|
||||
contentHint,
|
||||
groupId,
|
||||
options,
|
||||
proto,
|
||||
recipients,
|
||||
timestamp,
|
||||
urgent,
|
||||
story,
|
||||
});
|
||||
drop(
|
||||
this.sendMessageProto({
|
||||
callback,
|
||||
contentHint,
|
||||
groupId,
|
||||
options,
|
||||
proto,
|
||||
recipients,
|
||||
timestamp,
|
||||
urgent,
|
||||
story,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1096,16 +1117,18 @@ export default class MessageSender {
|
||||
resolve(res);
|
||||
}
|
||||
};
|
||||
this.sendMessageProto({
|
||||
callback,
|
||||
contentHint,
|
||||
groupId,
|
||||
options,
|
||||
proto,
|
||||
recipients: [identifier],
|
||||
timestamp,
|
||||
urgent,
|
||||
});
|
||||
drop(
|
||||
this.sendMessageProto({
|
||||
callback,
|
||||
contentHint,
|
||||
groupId,
|
||||
options,
|
||||
proto,
|
||||
recipients: [identifier],
|
||||
timestamp,
|
||||
urgent,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2036,18 +2059,20 @@ export default class MessageSender {
|
||||
}
|
||||
};
|
||||
|
||||
this.sendMessageProto({
|
||||
callback,
|
||||
contentHint,
|
||||
groupId,
|
||||
options,
|
||||
proto,
|
||||
recipients: identifiers,
|
||||
sendLogCallback,
|
||||
story,
|
||||
timestamp,
|
||||
urgent,
|
||||
});
|
||||
drop(
|
||||
this.sendMessageProto({
|
||||
callback,
|
||||
contentHint,
|
||||
groupId,
|
||||
options,
|
||||
proto,
|
||||
recipients: identifiers,
|
||||
sendLogCallback,
|
||||
story,
|
||||
timestamp,
|
||||
urgent,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
2
ts/textsecure/Types.d.ts
vendored
2
ts/textsecure/Types.d.ts
vendored
@@ -14,6 +14,7 @@ import type { RawBodyRange } from '../types/BodyRange';
|
||||
export {
|
||||
IdentityKeyType,
|
||||
IdentityKeyIdType,
|
||||
KyberPreKeyType,
|
||||
PreKeyIdType,
|
||||
PreKeyType,
|
||||
SenderKeyIdType,
|
||||
@@ -286,6 +287,7 @@ export type IRequestHandler = {
|
||||
export type PniKeyMaterialType = Readonly<{
|
||||
identityKeyPair: Uint8Array;
|
||||
signedPreKey: Uint8Array;
|
||||
lastResortKyberPreKey?: Uint8Array;
|
||||
registrationId: number;
|
||||
}>;
|
||||
|
||||
|
@@ -1,13 +1,17 @@
|
||||
// Copyright 2017 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
import * as durations from '../util/durations';
|
||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||
import * as Registration from '../util/registration';
|
||||
import { UUIDKind } from '../types/UUID';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
|
||||
const ROTATION_INTERVAL = 2 * durations.DAY;
|
||||
const UPDATE_INTERVAL = 2 * durations.DAY;
|
||||
const UPDATE_TIME_STORAGE_KEY = 'nextScheduledUpdateKeyTime';
|
||||
|
||||
export type MinimalEventsType = {
|
||||
on(event: 'timetravel', callback: () => void): void;
|
||||
@@ -15,23 +19,20 @@ export type MinimalEventsType = {
|
||||
|
||||
let initComplete = false;
|
||||
|
||||
export class RotateSignedPreKeyListener {
|
||||
export class UpdateKeysListener {
|
||||
public timeout: NodeJS.Timeout | undefined;
|
||||
|
||||
protected scheduleRotationForNow(): void {
|
||||
protected scheduleUpdateForNow(): void {
|
||||
const now = Date.now();
|
||||
void window.textsecure.storage.put('nextSignedKeyRotationTime', now);
|
||||
void window.textsecure.storage.put(UPDATE_TIME_STORAGE_KEY, now);
|
||||
}
|
||||
|
||||
protected setTimeoutForNextRun(): void {
|
||||
const now = Date.now();
|
||||
const time = window.textsecure.storage.get(
|
||||
'nextSignedKeyRotationTime',
|
||||
now
|
||||
);
|
||||
const time = window.textsecure.storage.get(UPDATE_TIME_STORAGE_KEY, now);
|
||||
|
||||
log.info(
|
||||
'Next signed key rotation scheduled for',
|
||||
'UpdateKeysListener: Next update scheduled for',
|
||||
new Date(time).toISOString()
|
||||
);
|
||||
|
||||
@@ -44,31 +45,29 @@ export class RotateSignedPreKeyListener {
|
||||
this.timeout = setTimeout(() => this.runWhenOnline(), waitTime);
|
||||
}
|
||||
|
||||
private scheduleNextRotation(): void {
|
||||
private scheduleNextUpdate(): void {
|
||||
const now = Date.now();
|
||||
const nextTime = now + ROTATION_INTERVAL;
|
||||
void window.textsecure.storage.put('nextSignedKeyRotationTime', nextTime);
|
||||
const nextTime = now + UPDATE_INTERVAL;
|
||||
void window.textsecure.storage.put(UPDATE_TIME_STORAGE_KEY, nextTime);
|
||||
}
|
||||
|
||||
private async run(): Promise<void> {
|
||||
log.info('Rotating signed prekey...');
|
||||
log.info('UpdateKeysListener: Updating keys...');
|
||||
try {
|
||||
const accountManager = window.getAccountManager();
|
||||
await Promise.all([
|
||||
accountManager.rotateSignedPreKey(UUIDKind.ACI),
|
||||
accountManager.rotateSignedPreKey(UUIDKind.PNI),
|
||||
]);
|
||||
|
||||
// We try to update this whenever we remove a preKey; this is a fail-safe to ensure
|
||||
// we're always in good shape
|
||||
await Promise.all([
|
||||
accountManager.refreshPreKeys(UUIDKind.ACI),
|
||||
accountManager.refreshPreKeys(UUIDKind.PNI),
|
||||
]);
|
||||
this.scheduleNextRotation();
|
||||
await accountManager.maybeUpdateKeys(UUIDKind.ACI);
|
||||
await accountManager.maybeUpdateKeys(UUIDKind.PNI);
|
||||
|
||||
this.scheduleNextUpdate();
|
||||
this.setTimeoutForNextRun();
|
||||
} catch (error) {
|
||||
log.error('rotateSignedPrekey() failed. Trying again in five minutes');
|
||||
const errorString = isNumber(error.code)
|
||||
? error.code.toString()
|
||||
: Errors.toLogFormat(error);
|
||||
log.error(
|
||||
`UpdateKeysListener.run failure - trying again in five minutes ${errorString}`
|
||||
);
|
||||
setTimeout(() => this.setTimeoutForNextRun(), 5 * durations.MINUTE);
|
||||
}
|
||||
}
|
||||
@@ -77,7 +76,9 @@ export class RotateSignedPreKeyListener {
|
||||
if (window.navigator.onLine) {
|
||||
void this.run();
|
||||
} else {
|
||||
log.info('We are offline; keys will be rotated when we are next online');
|
||||
log.info(
|
||||
'UpdateKeysListener: We are offline; will update keys when we are next online'
|
||||
);
|
||||
const listener = () => {
|
||||
window.removeEventListener('online', listener);
|
||||
this.setTimeoutForNextRun();
|
||||
@@ -88,17 +89,15 @@ export class RotateSignedPreKeyListener {
|
||||
|
||||
public static init(events: MinimalEventsType, newVersion: boolean): void {
|
||||
if (initComplete) {
|
||||
window.SignalContext.log.info(
|
||||
'Rotate signed prekey listener: Already initialized'
|
||||
);
|
||||
window.SignalContext.log.info('UpdateKeysListener: Already initialized');
|
||||
return;
|
||||
}
|
||||
initComplete = true;
|
||||
|
||||
const listener = new RotateSignedPreKeyListener();
|
||||
const listener = new UpdateKeysListener();
|
||||
|
||||
if (newVersion) {
|
||||
listener.scheduleRotationForNow();
|
||||
listener.scheduleUpdateForNow();
|
||||
}
|
||||
listener.setTimeoutForNextRun();
|
||||
|
@@ -870,6 +870,11 @@ const attachmentV3Response = z.object({
|
||||
|
||||
export type AttachmentV3ResponseType = z.infer<typeof attachmentV3Response>;
|
||||
|
||||
export type ServerKeyCountType = {
|
||||
count: number;
|
||||
pqCount: number;
|
||||
};
|
||||
|
||||
export type WebAPIType = {
|
||||
startRegistration(): unknown;
|
||||
finishRegistration(baton: unknown): void;
|
||||
@@ -925,7 +930,7 @@ export type WebAPIType = {
|
||||
deviceId?: number,
|
||||
options?: { accessKey?: string }
|
||||
) => Promise<ServerKeysType>;
|
||||
getMyKeys: (uuidKind: UUIDKind) => Promise<number>;
|
||||
getMyKeyCounts: (uuidKind: UUIDKind) => Promise<ServerKeyCountType>;
|
||||
getOnboardingStoryManifest: () => Promise<{
|
||||
version: string;
|
||||
languages: Record<string, Array<string>>;
|
||||
@@ -998,7 +1003,7 @@ export type WebAPIType = {
|
||||
) => Promise<ReserveUsernameResultType>;
|
||||
confirmUsername(options: ConfirmUsernameOptionsType): Promise<void>;
|
||||
registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>;
|
||||
registerKeys: (genKeys: KeysType, uuidKind: UUIDKind) => Promise<void>;
|
||||
registerKeys: (genKeys: UploadKeysType, uuidKind: UUIDKind) => Promise<void>;
|
||||
registerSupportForUnauthenticatedDelivery: () => Promise<void>;
|
||||
reportMessage: (options: ReportMessageOptionsType) => Promise<void>;
|
||||
requestVerificationSMS: (number: string, token: string) => Promise<void>;
|
||||
@@ -1032,10 +1037,6 @@ export type WebAPIType = {
|
||||
}
|
||||
) => Promise<MultiRecipient200ResponseType>;
|
||||
setPhoneNumberDiscoverability: (newValue: boolean) => Promise<void>;
|
||||
setSignedPreKey: (
|
||||
signedPreKey: SignedPreKeyType,
|
||||
uuidKind: UUIDKind
|
||||
) => Promise<void>;
|
||||
updateDeviceName: (deviceName: string) => Promise<void>;
|
||||
uploadAvatar: (
|
||||
uploadAvatarRequestHeaders: UploadAvatarHeadersType,
|
||||
@@ -1060,33 +1061,46 @@ export type WebAPIType = {
|
||||
reconnect: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type SignedPreKeyType = {
|
||||
export type UploadSignedPreKeyType = {
|
||||
keyId: number;
|
||||
publicKey: Uint8Array;
|
||||
signature: Uint8Array;
|
||||
};
|
||||
export type UploadPreKeyType = {
|
||||
keyId: number;
|
||||
publicKey: Uint8Array;
|
||||
};
|
||||
export type UploadKyberPreKeyType = UploadSignedPreKeyType;
|
||||
|
||||
export type KeysType = {
|
||||
export type UploadKeysType = {
|
||||
identityKey: Uint8Array;
|
||||
signedPreKey: SignedPreKeyType;
|
||||
preKeys: Array<{
|
||||
keyId: number;
|
||||
publicKey: Uint8Array;
|
||||
}>;
|
||||
|
||||
// If a field is not provided, the server won't update its data.
|
||||
preKeys?: Array<UploadPreKeyType>;
|
||||
pqPreKeys?: Array<UploadSignedPreKeyType>;
|
||||
pqLastResortPreKey?: UploadSignedPreKeyType;
|
||||
signedPreKey?: UploadSignedPreKeyType;
|
||||
};
|
||||
|
||||
export type ServerKeysType = {
|
||||
devices: Array<{
|
||||
deviceId: number;
|
||||
registrationId: number;
|
||||
signedPreKey: {
|
||||
|
||||
// We'll get a 404 if none of these keys are provided; we'll have at least one
|
||||
preKey?: {
|
||||
keyId: number;
|
||||
publicKey: Uint8Array;
|
||||
};
|
||||
signedPreKey?: {
|
||||
keyId: number;
|
||||
publicKey: Uint8Array;
|
||||
signature: Uint8Array;
|
||||
};
|
||||
preKey?: {
|
||||
pqPreKey?: {
|
||||
keyId: number;
|
||||
publicKey: Uint8Array;
|
||||
signature: Uint8Array;
|
||||
};
|
||||
}>;
|
||||
identityKey: Uint8Array;
|
||||
@@ -1293,7 +1307,7 @@ export function initialize({
|
||||
getIceServers,
|
||||
getKeysForIdentifier,
|
||||
getKeysForIdentifierUnauth,
|
||||
getMyKeys,
|
||||
getMyKeyCounts,
|
||||
getOnboardingStoryManifest,
|
||||
getProfile,
|
||||
getProfileUnauth,
|
||||
@@ -1331,7 +1345,6 @@ export function initialize({
|
||||
sendMessagesUnauth,
|
||||
sendWithSenderKey,
|
||||
setPhoneNumberDiscoverability,
|
||||
setSignedPreKey,
|
||||
startRegistration,
|
||||
unregisterRequestHandler,
|
||||
updateDeviceName,
|
||||
@@ -2052,30 +2065,74 @@ export function initialize({
|
||||
publicKey: string;
|
||||
signature: string;
|
||||
};
|
||||
type JSONPreKeyType = {
|
||||
keyId: number;
|
||||
publicKey: string;
|
||||
};
|
||||
type JSONKyberPreKeyType = {
|
||||
keyId: number;
|
||||
publicKey: string;
|
||||
signature: string;
|
||||
};
|
||||
|
||||
type JSONKeysType = {
|
||||
identityKey: string;
|
||||
signedPreKey: JSONSignedPreKeyType;
|
||||
preKeys: Array<{
|
||||
keyId: number;
|
||||
publicKey: string;
|
||||
}>;
|
||||
preKeys?: Array<JSONPreKeyType>;
|
||||
pqPreKeys?: Array<JSONKyberPreKeyType>;
|
||||
pqLastResortPreKey?: JSONKyberPreKeyType;
|
||||
signedPreKey?: JSONSignedPreKeyType;
|
||||
};
|
||||
|
||||
async function registerKeys(genKeys: KeysType, uuidKind: UUIDKind) {
|
||||
const preKeys = genKeys.preKeys.map(key => ({
|
||||
async function registerKeys(genKeys: UploadKeysType, uuidKind: UUIDKind) {
|
||||
const preKeys = genKeys.preKeys?.map(key => ({
|
||||
keyId: key.keyId,
|
||||
publicKey: Bytes.toBase64(key.publicKey),
|
||||
}));
|
||||
const pqPreKeys = genKeys.pqPreKeys?.map(key => ({
|
||||
keyId: key.keyId,
|
||||
publicKey: Bytes.toBase64(key.publicKey),
|
||||
signature: Bytes.toBase64(key.signature),
|
||||
}));
|
||||
|
||||
if (
|
||||
!preKeys?.length &&
|
||||
!pqPreKeys?.length &&
|
||||
!genKeys.pqLastResortPreKey &&
|
||||
!genKeys.signedPreKey
|
||||
) {
|
||||
throw new Error(
|
||||
'registerKeys: None of the four potential key types were provided!'
|
||||
);
|
||||
}
|
||||
if (preKeys && preKeys.length === 0) {
|
||||
throw new Error('registerKeys: Attempting to upload zero preKeys!');
|
||||
}
|
||||
if (pqPreKeys && pqPreKeys.length === 0) {
|
||||
throw new Error('registerKeys: Attempting to upload zero pqPreKeys!');
|
||||
}
|
||||
|
||||
const keys: JSONKeysType = {
|
||||
identityKey: Bytes.toBase64(genKeys.identityKey),
|
||||
signedPreKey: {
|
||||
keyId: genKeys.signedPreKey.keyId,
|
||||
publicKey: Bytes.toBase64(genKeys.signedPreKey.publicKey),
|
||||
signature: Bytes.toBase64(genKeys.signedPreKey.signature),
|
||||
},
|
||||
preKeys,
|
||||
pqPreKeys,
|
||||
...(genKeys.pqLastResortPreKey
|
||||
? {
|
||||
pqLastResortPreKey: {
|
||||
keyId: genKeys.pqLastResortPreKey.keyId,
|
||||
publicKey: Bytes.toBase64(genKeys.pqLastResortPreKey.publicKey),
|
||||
signature: Bytes.toBase64(genKeys.pqLastResortPreKey.signature),
|
||||
},
|
||||
}
|
||||
: null),
|
||||
...(genKeys.signedPreKey
|
||||
? {
|
||||
signedPreKey: {
|
||||
keyId: genKeys.signedPreKey.keyId,
|
||||
publicKey: Bytes.toBase64(genKeys.signedPreKey.publicKey),
|
||||
signature: Bytes.toBase64(genKeys.signedPreKey.signature),
|
||||
},
|
||||
}
|
||||
: null),
|
||||
};
|
||||
|
||||
await _ajax({
|
||||
@@ -2097,50 +2154,39 @@ export function initialize({
|
||||
});
|
||||
}
|
||||
|
||||
async function setSignedPreKey(
|
||||
signedPreKey: SignedPreKeyType,
|
||||
async function getMyKeyCounts(
|
||||
uuidKind: UUIDKind
|
||||
) {
|
||||
await _ajax({
|
||||
call: 'signed',
|
||||
urlParameters: `?${uuidKindToQuery(uuidKind)}`,
|
||||
httpType: 'PUT',
|
||||
jsonData: {
|
||||
keyId: signedPreKey.keyId,
|
||||
publicKey: Bytes.toBase64(signedPreKey.publicKey),
|
||||
signature: Bytes.toBase64(signedPreKey.signature),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type ServerKeyCountType = {
|
||||
count: number;
|
||||
};
|
||||
|
||||
async function getMyKeys(uuidKind: UUIDKind): Promise<number> {
|
||||
): Promise<ServerKeyCountType> {
|
||||
const result = (await _ajax({
|
||||
call: 'keys',
|
||||
urlParameters: `?${uuidKindToQuery(uuidKind)}`,
|
||||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
validateResponse: { count: 'number' },
|
||||
validateResponse: { count: 'number', pqCount: 'number' },
|
||||
})) as ServerKeyCountType;
|
||||
|
||||
return result.count;
|
||||
return result;
|
||||
}
|
||||
|
||||
type ServerKeyResponseType = {
|
||||
devices: Array<{
|
||||
deviceId: number;
|
||||
registrationId: number;
|
||||
signedPreKey: {
|
||||
|
||||
// We'll get a 404 if none of these keys are provided; we'll have at least one
|
||||
preKey?: {
|
||||
keyId: number;
|
||||
publicKey: string;
|
||||
};
|
||||
signedPreKey?: {
|
||||
keyId: number;
|
||||
publicKey: string;
|
||||
signature: string;
|
||||
};
|
||||
preKey?: {
|
||||
pqPreKey?: {
|
||||
keyId: number;
|
||||
publicKey: string;
|
||||
signature: string;
|
||||
};
|
||||
}>;
|
||||
identityKey: string;
|
||||
@@ -2180,12 +2226,25 @@ export function initialize({
|
||||
return {
|
||||
deviceId: device.deviceId,
|
||||
registrationId: device.registrationId,
|
||||
preKey,
|
||||
signedPreKey: {
|
||||
keyId: device.signedPreKey.keyId,
|
||||
publicKey: Bytes.fromBase64(device.signedPreKey.publicKey),
|
||||
signature: Bytes.fromBase64(device.signedPreKey.signature),
|
||||
},
|
||||
...(preKey ? { preKey } : null),
|
||||
...(device.signedPreKey
|
||||
? {
|
||||
signedPreKey: {
|
||||
keyId: device.signedPreKey.keyId,
|
||||
publicKey: Bytes.fromBase64(device.signedPreKey.publicKey),
|
||||
signature: Bytes.fromBase64(device.signedPreKey.signature),
|
||||
},
|
||||
}
|
||||
: null),
|
||||
...(device.pqPreKey
|
||||
? {
|
||||
pqPreKey: {
|
||||
keyId: device.pqPreKey.keyId,
|
||||
publicKey: Bytes.fromBase64(device.pqPreKey.publicKey),
|
||||
signature: Bytes.fromBase64(device.pqPreKey.signature),
|
||||
},
|
||||
}
|
||||
: null),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2199,7 +2258,7 @@ export function initialize({
|
||||
const keys = (await _ajax({
|
||||
call: 'keys',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${identifier}/${deviceId || '*'}`,
|
||||
urlParameters: `/${identifier}/${deviceId || '*'}?pq=true`,
|
||||
responseType: 'json',
|
||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||
})) as ServerKeyResponseType;
|
||||
@@ -2214,7 +2273,7 @@ export function initialize({
|
||||
const keys = (await _ajax({
|
||||
call: 'keys',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${identifier}/${deviceId || '*'}`,
|
||||
urlParameters: `/${identifier}/${deviceId || '*'}?pq=true`,
|
||||
responseType: 'json',
|
||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||
unauthenticated: true,
|
||||
|
@@ -3,6 +3,7 @@
|
||||
|
||||
import {
|
||||
ErrorCode,
|
||||
KEMPublicKey,
|
||||
LibSignalErrorBase,
|
||||
PreKeyBundle,
|
||||
processPreKeyBundle,
|
||||
@@ -101,7 +102,8 @@ async function handleServerKeys(
|
||||
|
||||
await Promise.all(
|
||||
response.devices.map(async device => {
|
||||
const { deviceId, registrationId, preKey, signedPreKey } = device;
|
||||
const { deviceId, registrationId, pqPreKey, preKey, signedPreKey } =
|
||||
device;
|
||||
if (
|
||||
devicesToUpdate !== undefined &&
|
||||
!devicesToUpdate.includes(deviceId)
|
||||
@@ -135,6 +137,14 @@ async function handleServerKeys(
|
||||
Buffer.from(response.identityKey)
|
||||
);
|
||||
|
||||
const pqPreKeyId = pqPreKey?.keyId || null;
|
||||
const pqPreKeyPublic = pqPreKey
|
||||
? KEMPublicKey.deserialize(Buffer.from(pqPreKey.publicKey))
|
||||
: null;
|
||||
const pqPreKeySignature = pqPreKey
|
||||
? Buffer.from(pqPreKey.signature)
|
||||
: null;
|
||||
|
||||
const preKeyBundle = PreKeyBundle.new(
|
||||
registrationId,
|
||||
deviceId,
|
||||
@@ -143,7 +153,10 @@ async function handleServerKeys(
|
||||
signedPreKey.keyId,
|
||||
signedPreKeyObject,
|
||||
Buffer.from(signedPreKey.signature),
|
||||
identityKey
|
||||
identityKey,
|
||||
pqPreKeyId,
|
||||
pqPreKeyPublic,
|
||||
pqPreKeySignature
|
||||
);
|
||||
|
||||
const address = new QualifiedAddress(
|
||||
|
Reference in New Issue
Block a user