Send support for Sender Key

This commit is contained in:
Scott Nonnenberg 2021-05-25 15:40:04 -07:00 committed by GitHub
parent d8417e562b
commit e6f1ec2b6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 2290 additions and 911 deletions

View file

@ -10,19 +10,16 @@
import { reject } from 'lodash';
import * as z from 'zod';
import { z } from 'zod';
import {
CiphertextMessageType,
PreKeyBundle,
processPreKeyBundle,
ProtocolAddress,
PublicKey,
sealedSenderEncryptMessage,
SenderCertificate,
signalEncrypt,
} from '@signalapp/signal-client';
import { ServerKeysType, WebAPIType } from './WebAPI';
import { WebAPIType } from './WebAPI';
import { ContentClass, DataMessageClass } from '../textsecure.d';
import {
CallbackResultType,
@ -40,6 +37,7 @@ import {
import { isValidNumber } from '../types/PhoneNumber';
import { Sessions, IdentityKeys } from '../LibSignalStores';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
import { getKeysForIdentifier } from './getKeysForIdentifier';
export const enum SenderCertificateMode {
WithE164,
@ -80,6 +78,27 @@ function ciphertextMessageTypeToEnvelopeType(type: number) {
);
}
function getPaddedMessageLength(messageLength: number): number {
const messageLengthWithTerminator = messageLength + 1;
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
if (messageLengthWithTerminator % 160 !== 0) {
messagePartCount += 1;
}
return messagePartCount * 160;
}
export function padMessage(messageBuffer: ArrayBuffer): Uint8Array {
const plaintext = new Uint8Array(
getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
);
plaintext.set(new Uint8Array(messageBuffer));
plaintext[messageBuffer.byteLength] = 0x80;
return plaintext;
}
export default class OutgoingMessage {
server: WebAPIType;
@ -187,95 +206,26 @@ export default class OutgoingMessage {
identifier: string,
recurse?: boolean
): () => Promise<void> {
return async () =>
window.textsecure.storage.protocol
.getDeviceIds(identifier)
.then(async deviceIds => {
if (deviceIds.length === 0) {
this.registerError(
identifier,
'reloadDevicesAndSend: Got empty device list when loading device keys',
undefined
);
return undefined;
}
return this.doSendMessage(identifier, deviceIds, recurse);
});
return async () => {
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
identifier
);
if (deviceIds.length === 0) {
this.registerError(
identifier,
'reloadDevicesAndSend: Got empty device list when loading device keys',
undefined
);
return undefined;
}
return this.doSendMessage(identifier, deviceIds, recurse);
};
}
async getKeysForIdentifier(
identifier: string,
updateDevices: Array<number> | undefined
updateDevices?: Array<number>
): Promise<void | Array<void | null>> {
const handleResult = async (response: ServerKeysType) => {
const sessionStore = new Sessions();
const identityKeyStore = new IdentityKeys();
return Promise.all(
response.devices.map(async device => {
const { deviceId, registrationId, preKey, signedPreKey } = device;
if (
updateDevices === undefined ||
updateDevices.indexOf(deviceId) > -1
) {
if (device.registrationId === 0) {
window.log.info('device registrationId 0!');
}
if (!signedPreKey) {
throw new Error(
`getKeysForIdentifier/${identifier}: Missing signed prekey for deviceId ${deviceId}`
);
}
const protocolAddress = ProtocolAddress.new(identifier, deviceId);
const preKeyId = preKey?.keyId || null;
const preKeyObject = preKey
? PublicKey.deserialize(Buffer.from(preKey.publicKey))
: null;
const signedPreKeyObject = PublicKey.deserialize(
Buffer.from(signedPreKey.publicKey)
);
const identityKey = PublicKey.deserialize(
Buffer.from(response.identityKey)
);
const preKeyBundle = PreKeyBundle.new(
registrationId,
deviceId,
preKeyId,
preKeyObject,
signedPreKey.keyId,
signedPreKeyObject,
Buffer.from(signedPreKey.signature),
identityKey
);
const address = `${identifier}.${deviceId}`;
await window.textsecure.storage.protocol
.enqueueSessionJob(address, () =>
processPreKeyBundle(
preKeyBundle,
protocolAddress,
sessionStore,
identityKeyStore
)
)
.catch(error => {
if (
error?.message?.includes('untrusted identity for address')
) {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
error.identityKey = response.identityKey;
}
throw error;
});
}
return null;
})
);
};
const { sendMetadata } = this;
const info =
sendMetadata && sendMetadata[identifier]
@ -283,65 +233,23 @@ export default class OutgoingMessage {
: { accessKey: undefined };
const { accessKey } = info;
if (updateDevices === undefined) {
if (accessKey) {
return this.server
.getKeysForIdentifierUnauth(identifier, undefined, { accessKey })
.catch(async (error: Error) => {
if (error.code === 401 || error.code === 403) {
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
this.failoverIdentifiers.push(identifier);
}
return this.server.getKeysForIdentifier(identifier);
}
throw error;
})
.then(handleResult);
try {
const { accessKeyFailed } = await getKeysForIdentifier(
identifier,
this.server,
updateDevices,
accessKey
);
if (accessKeyFailed && !this.failoverIdentifiers.includes(identifier)) {
this.failoverIdentifiers.push(identifier);
}
return this.server.getKeysForIdentifier(identifier).then(handleResult);
} catch (error) {
if (error?.message?.includes('untrusted identity for address')) {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
}
throw error;
}
let promise: Promise<void | Array<void | null>> = Promise.resolve();
updateDevices.forEach(deviceId => {
promise = promise.then(async () => {
let innerPromise;
if (accessKey) {
innerPromise = this.server
.getKeysForIdentifierUnauth(identifier, deviceId, { accessKey })
.then(handleResult)
.catch(async error => {
if (error.code === 401 || error.code === 403) {
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
this.failoverIdentifiers.push(identifier);
}
return this.server
.getKeysForIdentifier(identifier, deviceId)
.then(handleResult);
}
throw error;
});
} else {
innerPromise = this.server
.getKeysForIdentifier(identifier, deviceId)
.then(handleResult);
}
return innerPromise.catch(async e => {
if (e.name === 'HTTPError' && e.code === 404) {
if (deviceId !== 1) {
return this.removeDeviceIdsForIdentifier(identifier, [deviceId]);
}
throw new UnregisteredUserError(identifier, e);
} else {
throw e;
}
});
});
});
return promise;
}
async transmitMessage(
@ -389,25 +297,9 @@ export default class OutgoingMessage {
});
}
getPaddedMessageLength(messageLength: number): number {
const messageLengthWithTerminator = messageLength + 1;
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
if (messageLengthWithTerminator % 160 !== 0) {
messagePartCount += 1;
}
return messagePartCount * 160;
}
getPlaintext(): ArrayBuffer {
if (!this.plaintext) {
const messageBuffer = this.message.toArrayBuffer();
this.plaintext = new Uint8Array(
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
);
this.plaintext.set(new Uint8Array(messageBuffer));
this.plaintext[messageBuffer.byteLength] = 0x80;
this.plaintext = padMessage(this.message.toArrayBuffer());
}
return this.plaintext;
}
@ -629,34 +521,6 @@ export default class OutgoingMessage {
});
}
async getStaleDeviceIdsForIdentifier(
identifier: string
): Promise<Array<number> | undefined> {
const sessionStore = new Sessions();
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
identifier
);
if (deviceIds.length === 0) {
return undefined;
}
const updateDevices: Array<number> = [];
await Promise.all(
deviceIds.map(async deviceId => {
const record = await sessionStore.getSession(
ProtocolAddress.new(identifier, deviceId)
);
if (!record || !record.hasCurrentState()) {
updateDevices.push(deviceId);
}
})
);
return updateDevices;
}
async removeDeviceIdsForIdentifier(
identifier: string,
deviceIdsToRemove: Array<number>
@ -713,10 +577,12 @@ export default class OutgoingMessage {
);
}
const updateDevices = await this.getStaleDeviceIdsForIdentifier(
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
identifier
);
await this.getKeysForIdentifier(identifier, updateDevices);
if (deviceIds.length === 0) {
await this.getKeysForIdentifier(identifier);
}
await this.reloadDevicesAndSend(identifier, true)();
} catch (error) {
if (error?.message?.includes('untrusted identity for address')) {

File diff suppressed because it is too large Load diff

View file

@ -11,6 +11,11 @@ export {
UnprocessedUpdateType,
} from '../sql/Interface';
export type DeviceType = {
id: number;
identifier: string;
};
// How the legacy APIs generate these types
export type CompatSignedPreKeyType = {

View file

@ -26,6 +26,7 @@ import { pki } from 'node-forge';
import is from '@sindresorhus/is';
import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid';
import { z } from 'zod';
import { Long } from '../window.d';
import { getUserAgent } from '../util/getUserAgent';
@ -351,6 +352,49 @@ type ArrayBufferWithDetailsType = {
response: Response;
};
export const multiRecipient200ResponseSchema = z
.object({
uuids404: z.array(z.string()).optional(),
needsSync: z.boolean().optional(),
})
.passthrough();
export type MultiRecipient200ResponseType = z.infer<
typeof multiRecipient200ResponseSchema
>;
export const multiRecipient409ResponseSchema = z.array(
z
.object({
uuid: z.string(),
devices: z
.object({
missingDevices: z.array(z.number()).optional(),
extraDevices: z.array(z.number()).optional(),
})
.passthrough(),
})
.passthrough()
);
export type MultiRecipient409ResponseType = z.infer<
typeof multiRecipient409ResponseSchema
>;
export const multiRecipient410ResponseSchema = z.array(
z
.object({
uuid: z.string(),
devices: z
.object({
staleDevices: z.array(z.number()).optional(),
})
.passthrough(),
})
.passthrough()
);
export type MultiRecipient410ResponseType = z.infer<
typeof multiRecipient410ResponseSchema
>;
function isSuccess(status: number): boolean {
return status >= 0 && status < 400;
}
@ -685,6 +729,7 @@ const URL_CALLS = {
groupToken: 'v1/groups/token',
keys: 'v2/keys',
messages: 'v1/messages',
multiRecipient: 'v1/messages/multi_recipient',
profile: 'v1/profile',
registerCapabilities: 'v1/devices/capabilities',
removeSignalingKey: 'v1/accounts/signaling_key',
@ -728,6 +773,7 @@ type AjaxOptionsType = {
call: keyof typeof URL_CALLS;
contentType?: string;
data?: ArrayBuffer | Buffer | string;
headers?: HeaderListType;
host?: string;
httpType: HTTPCodeType;
jsonData?: any;
@ -749,10 +795,12 @@ export type WebAPIConnectType = {
export type CapabilitiesType = {
gv2: boolean;
'gv1-migration': boolean;
senderKey: boolean;
};
export type CapabilitiesUploadType = {
'gv2-3': boolean;
'gv1-migration': boolean;
senderKey: boolean;
};
type StickerPackManifestType = any;
@ -895,6 +943,12 @@ export type WebAPIType = {
online?: boolean,
options?: { accessKey?: string }
) => Promise<void>;
sendWithSenderKey: (
payload: ArrayBuffer,
accessKeys: ArrayBuffer,
timestamp: number,
online?: boolean
) => Promise<MultiRecipient200ResponseType>;
setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>;
updateDeviceName: (deviceName: string) => Promise<void>;
uploadGroupAvatar: (
@ -1065,6 +1119,7 @@ export function initialize({
requestVerificationVoice,
sendMessages,
sendMessagesUnauth,
sendWithSenderKey,
setSignedPreKey,
updateDeviceName,
uploadGroupAvatar,
@ -1082,6 +1137,7 @@ export function initialize({
certificateAuthority,
contentType: param.contentType || 'application/json; charset=utf-8',
data: param.data || (param.jsonData && _jsonThing(param.jsonData)),
headers: param.headers,
host: param.host || url,
password: param.password || password,
path: URL_CALLS[param.call] + param.urlParameters,
@ -1375,6 +1431,7 @@ export function initialize({
const capabilities: CapabilitiesUploadType = {
'gv2-3': true,
'gv1-migration': true,
senderKey: false,
};
const { accessKey } = options;
@ -1661,6 +1718,25 @@ export function initialize({
});
}
async function sendWithSenderKey(
data: ArrayBuffer,
accessKeys: ArrayBuffer,
timestamp: number,
online?: boolean
): Promise<MultiRecipient200ResponseType> {
return _ajax({
call: 'multiRecipient',
httpType: 'PUT',
contentType: 'application/vnd.signal-messenger.mrm',
data,
urlParameters: `?ts=${timestamp}&online=${online ? 'true' : 'false'}`,
responseType: 'json',
headers: {
'Unidentified-Access-Key': arrayBufferToBase64(accessKeys),
},
});
}
function redactStickerUrl(stickerUrl: string) {
return stickerUrl.replace(
/(\/stickers\/)([^/]+)(\/)/,

View file

@ -0,0 +1,140 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import {
PreKeyBundle,
processPreKeyBundle,
ProtocolAddress,
PublicKey,
} from '@signalapp/signal-client';
import { UnregisteredUserError } from './Errors';
import { Sessions, IdentityKeys } from '../LibSignalStores';
import { ServerKeysType, WebAPIType } from './WebAPI';
export async function getKeysForIdentifier(
identifier: string,
server: WebAPIType,
devicesToUpdate?: Array<number>,
accessKey?: string
): Promise<{ accessKeyFailed?: boolean }> {
try {
const { keys, accessKeyFailed } = await getServerKeys(
identifier,
server,
accessKey
);
await handleServerKeys(identifier, keys, devicesToUpdate);
return {
accessKeyFailed,
};
} catch (error) {
if (error.name === 'HTTPError' && error.code === 404) {
await window.textsecure.storage.protocol.archiveAllSessions(identifier);
}
throw new UnregisteredUserError(identifier, error);
}
}
async function getServerKeys(
identifier: string,
server: WebAPIType,
accessKey?: string
): Promise<{ accessKeyFailed?: boolean; keys: ServerKeysType }> {
if (!accessKey) {
return {
keys: await server.getKeysForIdentifier(identifier),
};
}
try {
return {
keys: await server.getKeysForIdentifierUnauth(identifier, undefined, {
accessKey,
}),
};
} catch (error) {
if (error.code === 401 || error.code === 403) {
return {
accessKeyFailed: true,
keys: await server.getKeysForIdentifier(identifier),
};
}
throw error;
}
}
async function handleServerKeys(
identifier: string,
response: ServerKeysType,
devicesToUpdate?: Array<number>
): Promise<void> {
const sessionStore = new Sessions();
const identityKeyStore = new IdentityKeys();
await Promise.all(
response.devices.map(async device => {
const { deviceId, registrationId, preKey, signedPreKey } = device;
if (
devicesToUpdate !== undefined &&
!devicesToUpdate.includes(deviceId)
) {
return;
}
if (device.registrationId === 0) {
window.log.info(
`handleServerKeys/${identifier}: Got device registrationId zero!`
);
}
if (!signedPreKey) {
throw new Error(
`getKeysForIdentifier/${identifier}: Missing signed prekey for deviceId ${deviceId}`
);
}
const protocolAddress = ProtocolAddress.new(identifier, deviceId);
const preKeyId = preKey?.keyId || null;
const preKeyObject = preKey
? PublicKey.deserialize(Buffer.from(preKey.publicKey))
: null;
const signedPreKeyObject = PublicKey.deserialize(
Buffer.from(signedPreKey.publicKey)
);
const identityKey = PublicKey.deserialize(
Buffer.from(response.identityKey)
);
const preKeyBundle = PreKeyBundle.new(
registrationId,
deviceId,
preKeyId,
preKeyObject,
signedPreKey.keyId,
signedPreKeyObject,
Buffer.from(signedPreKey.signature),
identityKey
);
const address = `${identifier}.${deviceId}`;
await window.textsecure.storage.protocol
.enqueueSessionJob(address, () =>
processPreKeyBundle(
preKeyBundle,
protocolAddress,
sessionStore,
identityKeyStore
)
)
.catch(error => {
if (error?.message?.includes('untrusted identity for address')) {
// eslint-disable-next-line no-param-reassign
error.identityKey = response.identityKey;
}
throw error;
});
})
);
}