2021-04-28 18:36:10 +00:00
|
|
|
// Copyright 2020-2021 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2020-09-24 21:53:21 +00:00
|
|
|
/* eslint-disable guard-for-in */
|
|
|
|
/* eslint-disable no-restricted-syntax */
|
|
|
|
/* eslint-disable class-methods-use-this */
|
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
|
|
/* eslint-disable more/no-then */
|
|
|
|
/* eslint-disable no-param-reassign */
|
2020-04-13 17:37:29 +00:00
|
|
|
|
|
|
|
import { reject } from 'lodash';
|
2021-04-16 23:13:13 +00:00
|
|
|
|
|
|
|
import * as z from 'zod';
|
|
|
|
import {
|
|
|
|
CiphertextMessageType,
|
|
|
|
PreKeyBundle,
|
|
|
|
processPreKeyBundle,
|
|
|
|
ProtocolAddress,
|
|
|
|
PublicKey,
|
|
|
|
sealedSenderEncryptMessage,
|
|
|
|
SenderCertificate,
|
|
|
|
signalEncrypt,
|
2021-05-14 01:18:43 +00:00
|
|
|
} from '@signalapp/signal-client';
|
2021-04-16 23:13:13 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
import { ServerKeysType, WebAPIType } from './WebAPI';
|
|
|
|
import { ContentClass, DataMessageClass } from '../textsecure.d';
|
|
|
|
import {
|
|
|
|
CallbackResultType,
|
|
|
|
SendMetadataType,
|
|
|
|
SendOptionsType,
|
2020-10-24 15:11:18 +00:00
|
|
|
CustomError,
|
2020-04-13 17:37:29 +00:00
|
|
|
} from './SendMessage';
|
|
|
|
import {
|
|
|
|
OutgoingIdentityKeyError,
|
|
|
|
OutgoingMessageError,
|
|
|
|
SendMessageNetworkError,
|
2021-05-06 00:09:29 +00:00
|
|
|
SendMessageChallengeError,
|
2020-04-13 17:37:29 +00:00
|
|
|
UnregisteredUserError,
|
|
|
|
} from './Errors';
|
2020-09-04 01:25:19 +00:00
|
|
|
import { isValidNumber } from '../types/PhoneNumber';
|
2021-04-16 23:13:13 +00:00
|
|
|
import { Sessions, IdentityKeys } from '../LibSignalStores';
|
2021-04-28 18:36:10 +00:00
|
|
|
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
|
2021-04-16 23:13:13 +00:00
|
|
|
|
|
|
|
export const enum SenderCertificateMode {
|
|
|
|
WithE164,
|
|
|
|
WithoutE164,
|
|
|
|
}
|
|
|
|
|
2021-05-05 01:03:03 +00:00
|
|
|
type SendMetadata = {
|
|
|
|
type: number;
|
|
|
|
destinationDeviceId: number;
|
|
|
|
destinationRegistrationId: number;
|
|
|
|
content: string;
|
|
|
|
};
|
|
|
|
|
2021-04-16 23:13:13 +00:00
|
|
|
export const serializedCertificateSchema = z
|
|
|
|
.object({
|
|
|
|
expires: z.number().optional(),
|
|
|
|
serialized: z.instanceof(ArrayBuffer),
|
|
|
|
})
|
|
|
|
.nonstrict();
|
|
|
|
|
|
|
|
export type SerializedCertificateType = z.infer<
|
|
|
|
typeof serializedCertificateSchema
|
|
|
|
>;
|
2020-04-13 17:37:29 +00:00
|
|
|
|
|
|
|
type OutgoingMessageOptionsType = SendOptionsType & {
|
|
|
|
online?: boolean;
|
|
|
|
};
|
2015-10-20 23:30:39 +00:00
|
|
|
|
2021-04-16 23:13:13 +00:00
|
|
|
function ciphertextMessageTypeToEnvelopeType(type: number) {
|
|
|
|
if (type === CiphertextMessageType.PreKey) {
|
|
|
|
return window.textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE;
|
|
|
|
}
|
|
|
|
if (type === CiphertextMessageType.Whisper) {
|
|
|
|
return window.textsecure.protobuf.Envelope.Type.CIPHERTEXT;
|
|
|
|
}
|
|
|
|
throw new Error(
|
|
|
|
`ciphertextMessageTypeToEnvelopeType: Unrecognized type ${type}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
export default class OutgoingMessage {
|
|
|
|
server: WebAPIType;
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
timestamp: number;
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
identifiers: Array<string>;
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
message: ContentClass;
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
callback: (result: CallbackResultType) => void;
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
silent?: boolean;
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
plaintext?: Uint8Array;
|
|
|
|
|
|
|
|
identifiersCompleted: number;
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-10-24 15:11:18 +00:00
|
|
|
errors: Array<CustomError>;
|
2020-09-24 21:53:21 +00:00
|
|
|
|
|
|
|
successfulIdentifiers: Array<unknown>;
|
|
|
|
|
|
|
|
failoverIdentifiers: Array<unknown>;
|
|
|
|
|
|
|
|
unidentifiedDeliveries: Array<unknown>;
|
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
sendMetadata?: SendMetadataType;
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
online?: boolean;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
server: WebAPIType,
|
|
|
|
timestamp: number,
|
|
|
|
identifiers: Array<string>,
|
|
|
|
message: ContentClass | DataMessageClass,
|
|
|
|
silent: boolean | undefined,
|
|
|
|
callback: (result: CallbackResultType) => void,
|
|
|
|
options: OutgoingMessageOptionsType = {}
|
|
|
|
) {
|
|
|
|
if (message instanceof window.textsecure.protobuf.DataMessage) {
|
|
|
|
const content = new window.textsecure.protobuf.Content();
|
|
|
|
content.dataMessage = message;
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
this.message = content;
|
|
|
|
} else {
|
|
|
|
this.message = message;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.server = server;
|
|
|
|
this.timestamp = timestamp;
|
|
|
|
this.identifiers = identifiers;
|
|
|
|
this.callback = callback;
|
|
|
|
this.silent = silent;
|
|
|
|
|
|
|
|
this.identifiersCompleted = 0;
|
|
|
|
this.errors = [];
|
|
|
|
this.successfulIdentifiers = [];
|
|
|
|
this.failoverIdentifiers = [];
|
|
|
|
this.unidentifiedDeliveries = [];
|
|
|
|
|
2021-04-05 20:38:36 +00:00
|
|
|
const { sendMetadata, online } = options;
|
2020-04-13 17:37:29 +00:00
|
|
|
this.sendMetadata = sendMetadata;
|
|
|
|
this.online = online;
|
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
|
|
|
numberCompleted(): void {
|
2020-03-05 21:14:58 +00:00
|
|
|
this.identifiersCompleted += 1;
|
|
|
|
if (this.identifiersCompleted >= this.identifiers.length) {
|
2018-05-02 16:51:22 +00:00
|
|
|
this.callback({
|
2020-03-05 21:14:58 +00:00
|
|
|
successfulIdentifiers: this.successfulIdentifiers,
|
|
|
|
failoverIdentifiers: this.failoverIdentifiers,
|
2018-05-02 16:51:22 +00:00
|
|
|
errors: this.errors,
|
2018-10-18 01:01:21 +00:00
|
|
|
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
2018-05-02 16:51:22 +00:00
|
|
|
});
|
|
|
|
}
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-10-22 22:30:28 +00:00
|
|
|
registerError(
|
|
|
|
identifier: string,
|
|
|
|
reason: string,
|
|
|
|
providedError?: Error
|
|
|
|
): void {
|
|
|
|
let error = providedError;
|
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
if (!error || (error.name === 'HTTPError' && error.code !== 404)) {
|
2021-05-06 00:09:29 +00:00
|
|
|
if (error && error.code === 428) {
|
|
|
|
error = new SendMessageChallengeError(identifier, error);
|
|
|
|
} else {
|
|
|
|
error = new OutgoingMessageError(
|
|
|
|
identifier,
|
|
|
|
this.message.toArrayBuffer(),
|
|
|
|
this.timestamp,
|
|
|
|
error
|
|
|
|
);
|
|
|
|
}
|
2018-05-02 16:51:22 +00:00
|
|
|
}
|
2015-10-20 23:30:39 +00:00
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
error.reason = reason;
|
2020-10-22 22:30:28 +00:00
|
|
|
error.stackForLog = providedError ? providedError.stack : undefined;
|
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
this.errors[this.errors.length] = error;
|
|
|
|
this.numberCompleted();
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
reloadDevicesAndSend(
|
|
|
|
identifier: string,
|
|
|
|
recurse?: boolean
|
|
|
|
): () => Promise<void> {
|
|
|
|
return async () =>
|
|
|
|
window.textsecure.storage.protocol
|
|
|
|
.getDeviceIds(identifier)
|
|
|
|
.then(async deviceIds => {
|
|
|
|
if (deviceIds.length === 0) {
|
|
|
|
this.registerError(
|
|
|
|
identifier,
|
2021-04-16 23:13:13 +00:00
|
|
|
'reloadDevicesAndSend: Got empty device list when loading device keys',
|
2020-04-13 17:37:29 +00:00
|
|
|
undefined
|
|
|
|
);
|
2020-09-24 21:53:21 +00:00
|
|
|
return undefined;
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
|
|
|
return this.doSendMessage(identifier, deviceIds, recurse);
|
|
|
|
});
|
|
|
|
}
|
2018-07-21 21:51:20 +00:00
|
|
|
|
2020-09-24 21:53:21 +00:00
|
|
|
async getKeysForIdentifier(
|
|
|
|
identifier: string,
|
2021-04-16 23:13:13 +00:00
|
|
|
updateDevices: Array<number> | undefined
|
2020-09-24 21:53:21 +00:00
|
|
|
): Promise<void | Array<void | null>> {
|
2021-04-16 23:13:13 +00:00
|
|
|
const handleResult = async (response: ServerKeysType) => {
|
|
|
|
const sessionStore = new Sessions();
|
|
|
|
const identityKeyStore = new IdentityKeys();
|
|
|
|
|
|
|
|
return Promise.all(
|
2020-04-13 17:37:29 +00:00
|
|
|
response.devices.map(async device => {
|
2021-04-16 23:13:13 +00:00
|
|
|
const { deviceId, registrationId, preKey, signedPreKey } = device;
|
2018-07-21 21:51:20 +00:00
|
|
|
if (
|
|
|
|
updateDevices === undefined ||
|
2021-04-16 23:13:13 +00:00
|
|
|
updateDevices.indexOf(deviceId) > -1
|
2018-07-21 21:51:20 +00:00
|
|
|
) {
|
|
|
|
if (device.registrationId === 0) {
|
|
|
|
window.log.info('device registrationId 0!');
|
|
|
|
}
|
2021-04-16 23:13:13 +00:00
|
|
|
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)
|
|
|
|
);
|
2020-04-13 17:37:29 +00:00
|
|
|
|
2021-04-16 23:13:13 +00:00
|
|
|
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;
|
|
|
|
});
|
2018-05-02 16:51:22 +00:00
|
|
|
}
|
|
|
|
|
2018-07-21 21:51:20 +00:00
|
|
|
return null;
|
|
|
|
})
|
2018-05-02 16:51:22 +00:00
|
|
|
);
|
2021-04-16 23:13:13 +00:00
|
|
|
};
|
2015-11-17 20:00:41 +00:00
|
|
|
|
2020-03-05 21:14:58 +00:00
|
|
|
const { sendMetadata } = this;
|
|
|
|
const info =
|
2020-04-13 17:37:29 +00:00
|
|
|
sendMetadata && sendMetadata[identifier]
|
|
|
|
? sendMetadata[identifier]
|
|
|
|
: { accessKey: undefined };
|
|
|
|
const { accessKey } = info;
|
2018-10-18 01:01:21 +00:00
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
if (updateDevices === undefined) {
|
2018-10-18 01:01:21 +00:00
|
|
|
if (accessKey) {
|
|
|
|
return this.server
|
2020-04-13 17:37:29 +00:00
|
|
|
.getKeysForIdentifierUnauth(identifier, undefined, { accessKey })
|
|
|
|
.catch(async (error: Error) => {
|
2018-10-18 01:01:21 +00:00
|
|
|
if (error.code === 401 || error.code === 403) {
|
2020-03-05 21:14:58 +00:00
|
|
|
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
|
|
|
this.failoverIdentifiers.push(identifier);
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
2020-04-13 17:37:29 +00:00
|
|
|
return this.server.getKeysForIdentifier(identifier);
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
|
|
|
throw error;
|
|
|
|
})
|
|
|
|
.then(handleResult);
|
|
|
|
}
|
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
return this.server.getKeysForIdentifier(identifier).then(handleResult);
|
2018-07-21 21:51:20 +00:00
|
|
|
}
|
2018-10-18 01:01:21 +00:00
|
|
|
|
2020-09-24 21:53:21 +00:00
|
|
|
let promise: Promise<void | Array<void | null>> = Promise.resolve();
|
2018-10-18 01:01:21 +00:00
|
|
|
updateDevices.forEach(deviceId => {
|
2020-04-13 17:37:29 +00:00
|
|
|
promise = promise.then(async () => {
|
2018-10-18 01:01:21 +00:00
|
|
|
let innerPromise;
|
|
|
|
|
|
|
|
if (accessKey) {
|
|
|
|
innerPromise = this.server
|
2020-03-05 21:14:58 +00:00
|
|
|
.getKeysForIdentifierUnauth(identifier, deviceId, { accessKey })
|
2018-10-18 01:01:21 +00:00
|
|
|
.then(handleResult)
|
2020-04-13 17:37:29 +00:00
|
|
|
.catch(async error => {
|
2018-10-18 01:01:21 +00:00
|
|
|
if (error.code === 401 || error.code === 403) {
|
2020-03-05 21:14:58 +00:00
|
|
|
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
|
|
|
this.failoverIdentifiers.push(identifier);
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
|
|
|
return this.server
|
2020-03-05 21:14:58 +00:00
|
|
|
.getKeysForIdentifier(identifier, deviceId)
|
2018-10-18 01:01:21 +00:00
|
|
|
.then(handleResult);
|
2018-07-21 21:51:20 +00:00
|
|
|
}
|
2018-10-18 01:01:21 +00:00
|
|
|
throw error;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
innerPromise = this.server
|
2020-03-05 21:14:58 +00:00
|
|
|
.getKeysForIdentifier(identifier, deviceId)
|
2018-10-18 01:01:21 +00:00
|
|
|
.then(handleResult);
|
|
|
|
}
|
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
return innerPromise.catch(async e => {
|
2018-10-18 01:01:21 +00:00
|
|
|
if (e.name === 'HTTPError' && e.code === 404) {
|
|
|
|
if (deviceId !== 1) {
|
2020-03-05 21:14:58 +00:00
|
|
|
return this.removeDeviceIdsForIdentifier(identifier, [deviceId]);
|
2018-07-21 21:51:20 +00:00
|
|
|
}
|
2020-04-13 17:37:29 +00:00
|
|
|
throw new UnregisteredUserError(identifier, e);
|
2018-10-18 01:01:21 +00:00
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
2018-07-21 21:51:20 +00:00
|
|
|
});
|
2018-05-02 16:51:22 +00:00
|
|
|
|
2018-07-21 21:51:20 +00:00
|
|
|
return promise;
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2016-06-15 02:30:18 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async transmitMessage(
|
|
|
|
identifier: string,
|
2020-09-24 21:53:21 +00:00
|
|
|
jsonData: Array<unknown>,
|
2020-04-13 17:37:29 +00:00
|
|
|
timestamp: number,
|
|
|
|
{ accessKey }: { accessKey?: string } = {}
|
2020-09-24 21:53:21 +00:00
|
|
|
): Promise<void> {
|
2018-10-18 01:01:21 +00:00
|
|
|
let promise;
|
|
|
|
|
|
|
|
if (accessKey) {
|
|
|
|
promise = this.server.sendMessagesUnauth(
|
2020-03-05 21:14:58 +00:00
|
|
|
identifier,
|
2018-10-18 01:01:21 +00:00
|
|
|
jsonData,
|
|
|
|
timestamp,
|
|
|
|
this.silent,
|
2018-11-14 19:10:32 +00:00
|
|
|
this.online,
|
2018-10-18 01:01:21 +00:00
|
|
|
{ accessKey }
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
promise = this.server.sendMessages(
|
2020-03-05 21:14:58 +00:00
|
|
|
identifier,
|
2018-10-18 01:01:21 +00:00
|
|
|
jsonData,
|
|
|
|
timestamp,
|
2018-11-14 19:10:32 +00:00
|
|
|
this.silent,
|
|
|
|
this.online
|
2018-10-18 01:01:21 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return promise.catch(e => {
|
2020-01-08 17:44:54 +00:00
|
|
|
if (e.name === 'HTTPError' && e.code !== 409 && e.code !== 410) {
|
2018-10-18 01:01:21 +00:00
|
|
|
// 409 and 410 should bubble and be handled by doSendMessage
|
|
|
|
// 404 should throw UnregisteredUserError
|
2021-05-06 00:09:29 +00:00
|
|
|
// 428 should throw SendMessageChallengeError
|
2018-10-18 01:01:21 +00:00
|
|
|
// all other network errors can be retried later.
|
|
|
|
if (e.code === 404) {
|
2020-04-13 17:37:29 +00:00
|
|
|
throw new UnregisteredUserError(identifier, e);
|
2016-06-15 02:30:18 +00:00
|
|
|
}
|
2021-05-06 00:09:29 +00:00
|
|
|
if (e.code === 428) {
|
|
|
|
throw new SendMessageChallengeError(identifier, e);
|
|
|
|
}
|
2020-04-13 17:37:29 +00:00
|
|
|
throw new SendMessageNetworkError(identifier, jsonData, e);
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
|
|
|
throw e;
|
|
|
|
});
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2016-06-15 02:30:18 +00:00
|
|
|
|
2020-09-24 21:53:21 +00:00
|
|
|
getPaddedMessageLength(messageLength: number): number {
|
2018-07-21 21:51:20 +00:00
|
|
|
const messageLengthWithTerminator = messageLength + 1;
|
|
|
|
let messagePartCount = Math.floor(messageLengthWithTerminator / 160);
|
2016-06-15 02:30:18 +00:00
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
if (messageLengthWithTerminator % 160 !== 0) {
|
2018-07-21 21:51:20 +00:00
|
|
|
messagePartCount += 1;
|
2018-05-02 16:51:22 +00:00
|
|
|
}
|
2017-06-21 00:21:55 +00:00
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
return messagePartCount * 160;
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2016-06-15 02:30:18 +00:00
|
|
|
|
2020-09-24 21:53:21 +00:00
|
|
|
getPlaintext(): ArrayBuffer {
|
2018-05-02 16:51:22 +00:00
|
|
|
if (!this.plaintext) {
|
2018-07-21 21:51:20 +00:00
|
|
|
const messageBuffer = this.message.toArrayBuffer();
|
2018-05-02 16:51:22 +00:00
|
|
|
this.plaintext = new Uint8Array(
|
|
|
|
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
|
|
|
|
);
|
|
|
|
this.plaintext.set(new Uint8Array(messageBuffer));
|
|
|
|
this.plaintext[messageBuffer.byteLength] = 0x80;
|
|
|
|
}
|
|
|
|
return this.plaintext;
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2017-08-04 19:25:30 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async doSendMessage(
|
|
|
|
identifier: string,
|
|
|
|
deviceIds: Array<number>,
|
|
|
|
recurse?: boolean
|
|
|
|
): Promise<void> {
|
2018-07-21 21:51:20 +00:00
|
|
|
const plaintext = this.getPlaintext();
|
2017-08-04 19:25:30 +00:00
|
|
|
|
2021-04-05 20:38:36 +00:00
|
|
|
const { sendMetadata } = this;
|
|
|
|
const { accessKey, senderCertificate } = sendMetadata?.[identifier] || {};
|
2018-10-18 01:01:21 +00:00
|
|
|
|
|
|
|
if (accessKey && !senderCertificate) {
|
2020-01-16 23:57:37 +00:00
|
|
|
window.log.warn(
|
|
|
|
'OutgoingMessage.doSendMessage: accessKey was provided, but senderCertificate was not'
|
2018-10-18 01:01:21 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-10-31 23:58:14 +00:00
|
|
|
const sealedSender = Boolean(accessKey && senderCertificate);
|
2018-10-18 01:01:21 +00:00
|
|
|
|
2021-05-17 17:57:47 +00:00
|
|
|
// We don't send to ourselves unless sealedSender is enabled
|
2020-04-13 17:37:29 +00:00
|
|
|
const ourNumber = window.textsecure.storage.user.getNumber();
|
|
|
|
const ourUuid = window.textsecure.storage.user.getUuid();
|
|
|
|
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
|
2020-03-05 21:14:58 +00:00
|
|
|
if ((identifier === ourNumber || identifier === ourUuid) && !sealedSender) {
|
2020-04-13 17:37:29 +00:00
|
|
|
deviceIds = reject(
|
2018-10-18 01:01:21 +00:00
|
|
|
deviceIds,
|
|
|
|
deviceId =>
|
|
|
|
// because we store our own device ID as a string at least sometimes
|
2020-04-13 17:37:29 +00:00
|
|
|
deviceId === ourDeviceId ||
|
|
|
|
(typeof ourDeviceId === 'string' &&
|
|
|
|
deviceId === parseInt(ourDeviceId, 10))
|
2018-10-18 01:01:21 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-04-16 23:13:13 +00:00
|
|
|
const sessionStore = new Sessions();
|
|
|
|
const identityKeyStore = new IdentityKeys();
|
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
return Promise.all(
|
2021-04-16 23:13:13 +00:00
|
|
|
deviceIds.map(async destinationDeviceId => {
|
2021-05-05 01:03:03 +00:00
|
|
|
const address = `${identifier}.${destinationDeviceId}`;
|
2017-08-04 19:25:30 +00:00
|
|
|
|
2021-05-05 01:03:03 +00:00
|
|
|
return window.textsecure.storage.protocol.enqueueSessionJob<SendMetadata>(
|
|
|
|
address,
|
|
|
|
async () => {
|
|
|
|
const protocolAddress = ProtocolAddress.new(
|
|
|
|
identifier,
|
|
|
|
destinationDeviceId
|
|
|
|
);
|
2018-05-02 16:51:22 +00:00
|
|
|
|
2021-05-05 01:03:03 +00:00
|
|
|
const activeSession = await sessionStore.getSession(
|
|
|
|
protocolAddress
|
|
|
|
);
|
|
|
|
if (!activeSession) {
|
|
|
|
throw new Error(
|
|
|
|
'OutgoingMessage.doSendMessage: No active sesssion!'
|
|
|
|
);
|
|
|
|
}
|
2021-04-16 23:13:13 +00:00
|
|
|
|
2021-05-05 01:03:03 +00:00
|
|
|
const destinationRegistrationId = activeSession.remoteRegistrationId();
|
|
|
|
|
|
|
|
if (sealedSender && senderCertificate) {
|
|
|
|
const certificate = SenderCertificate.deserialize(
|
|
|
|
Buffer.from(senderCertificate.serialized)
|
|
|
|
);
|
|
|
|
|
|
|
|
const buffer = await sealedSenderEncryptMessage(
|
|
|
|
Buffer.from(plaintext),
|
|
|
|
protocolAddress,
|
|
|
|
certificate,
|
|
|
|
sessionStore,
|
|
|
|
identityKeyStore
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
type:
|
|
|
|
window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
|
|
|
|
destinationDeviceId,
|
|
|
|
destinationRegistrationId,
|
|
|
|
content: buffer.toString('base64'),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const ciphertextMessage = await signalEncrypt(
|
|
|
|
Buffer.from(plaintext),
|
|
|
|
protocolAddress,
|
|
|
|
sessionStore,
|
|
|
|
identityKeyStore
|
|
|
|
);
|
|
|
|
const type = ciphertextMessageTypeToEnvelopeType(
|
|
|
|
ciphertextMessage.type()
|
|
|
|
);
|
2020-09-24 21:53:21 +00:00
|
|
|
|
2021-05-05 01:03:03 +00:00
|
|
|
return {
|
|
|
|
type,
|
|
|
|
destinationDeviceId,
|
|
|
|
destinationRegistrationId,
|
|
|
|
content: ciphertextMessage.serialize().toString('base64'),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
);
|
2018-07-21 21:51:20 +00:00
|
|
|
})
|
2018-05-02 16:51:22 +00:00
|
|
|
)
|
2021-05-05 01:03:03 +00:00
|
|
|
.then(async (jsonData: Array<SendMetadata>) => {
|
2018-10-18 01:01:21 +00:00
|
|
|
if (sealedSender) {
|
2020-03-05 21:14:58 +00:00
|
|
|
return this.transmitMessage(identifier, jsonData, this.timestamp, {
|
2018-10-18 01:01:21 +00:00
|
|
|
accessKey,
|
|
|
|
}).then(
|
|
|
|
() => {
|
2020-03-05 21:14:58 +00:00
|
|
|
this.unidentifiedDeliveries.push(identifier);
|
|
|
|
this.successfulIdentifiers.push(identifier);
|
2018-10-18 01:01:21 +00:00
|
|
|
this.numberCompleted();
|
|
|
|
},
|
2020-04-13 17:37:29 +00:00
|
|
|
async (error: Error) => {
|
2018-10-18 01:01:21 +00:00
|
|
|
if (error.code === 401 || error.code === 403) {
|
2020-03-05 21:14:58 +00:00
|
|
|
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
|
|
|
|
this.failoverIdentifiers.push(identifier);
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
2020-04-13 17:37:29 +00:00
|
|
|
|
|
|
|
// This ensures that we don't hit this codepath the next time through
|
2021-04-05 20:38:36 +00:00
|
|
|
if (sendMetadata) {
|
|
|
|
delete sendMetadata[identifier];
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
return this.doSendMessage(identifier, deviceIds, recurse);
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-03-05 21:14:58 +00:00
|
|
|
return this.transmitMessage(identifier, jsonData, this.timestamp).then(
|
2018-10-18 01:01:21 +00:00
|
|
|
() => {
|
2020-03-05 21:14:58 +00:00
|
|
|
this.successfulIdentifiers.push(identifier);
|
2018-10-18 01:01:21 +00:00
|
|
|
this.numberCompleted();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
})
|
2020-04-13 17:37:29 +00:00
|
|
|
.catch(async error => {
|
2018-07-21 21:51:20 +00:00
|
|
|
if (
|
|
|
|
error instanceof Error &&
|
|
|
|
error.name === 'HTTPError' &&
|
|
|
|
(error.code === 410 || error.code === 409)
|
|
|
|
) {
|
2020-04-13 17:37:29 +00:00
|
|
|
if (!recurse) {
|
|
|
|
this.registerError(
|
2020-03-05 21:14:58 +00:00
|
|
|
identifier,
|
2018-07-21 21:51:20 +00:00
|
|
|
'Hit retry limit attempting to reload device list',
|
|
|
|
error
|
2018-05-02 16:51:22 +00:00
|
|
|
);
|
2020-09-24 21:53:21 +00:00
|
|
|
return undefined;
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2018-07-21 21:51:20 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
let p: Promise<any> = Promise.resolve();
|
2018-07-21 21:51:20 +00:00
|
|
|
if (error.code === 409) {
|
2020-03-05 21:14:58 +00:00
|
|
|
p = this.removeDeviceIdsForIdentifier(
|
|
|
|
identifier,
|
2021-01-29 22:16:48 +00:00
|
|
|
error.response.extraDevices || []
|
2018-05-02 16:51:22 +00:00
|
|
|
);
|
|
|
|
} else {
|
2018-07-21 21:51:20 +00:00
|
|
|
p = Promise.all(
|
2021-04-16 23:13:13 +00:00
|
|
|
error.response.staleDevices.map(async (deviceId: number) => {
|
|
|
|
await window.textsecure.storage.protocol.archiveSession(
|
|
|
|
`${identifier}.${deviceId}`
|
|
|
|
);
|
|
|
|
})
|
2018-05-02 16:51:22 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
return p.then(async () => {
|
2018-07-21 21:51:20 +00:00
|
|
|
const resetDevices =
|
|
|
|
error.code === 410
|
|
|
|
? error.response.staleDevices
|
|
|
|
: error.response.missingDevices;
|
2020-03-05 21:14:58 +00:00
|
|
|
return this.getKeysForIdentifier(identifier, resetDevices).then(
|
2018-10-31 23:58:14 +00:00
|
|
|
// We continue to retry as long as the error code was 409; the assumption is
|
|
|
|
// that we'll request new device info and the next request will succeed.
|
2020-03-05 21:14:58 +00:00
|
|
|
this.reloadDevicesAndSend(identifier, error.code === 409)
|
2018-05-02 16:51:22 +00:00
|
|
|
);
|
2018-07-21 21:51:20 +00:00
|
|
|
});
|
2020-09-24 21:53:21 +00:00
|
|
|
}
|
2021-04-16 23:13:13 +00:00
|
|
|
if (error?.message?.includes('untrusted identity for address')) {
|
2018-07-21 21:51:20 +00:00
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
error.timestamp = this.timestamp;
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
error.originalMessage = this.message.toArrayBuffer();
|
|
|
|
window.log.error(
|
|
|
|
'Got "key changed" error from encrypt - no identityKey for application layer',
|
2020-03-05 21:14:58 +00:00
|
|
|
identifier,
|
2018-07-21 21:51:20 +00:00
|
|
|
deviceIds
|
|
|
|
);
|
2020-01-10 16:10:43 +00:00
|
|
|
|
2020-03-05 21:14:58 +00:00
|
|
|
window.log.info('closing all sessions for', identifier);
|
2021-04-16 23:13:13 +00:00
|
|
|
window.textsecure.storage.protocol
|
|
|
|
.archiveAllSessions(identifier)
|
|
|
|
.then(
|
|
|
|
() => {
|
|
|
|
throw error;
|
|
|
|
},
|
|
|
|
innerError => {
|
|
|
|
window.log.error(
|
|
|
|
`doSendMessage: Error closing sessions: ${innerError.stack}`
|
|
|
|
);
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
);
|
2018-07-21 21:51:20 +00:00
|
|
|
}
|
|
|
|
|
2020-03-05 21:14:58 +00:00
|
|
|
this.registerError(
|
|
|
|
identifier,
|
|
|
|
'Failed to create or send message',
|
|
|
|
error
|
|
|
|
);
|
2020-09-24 21:53:21 +00:00
|
|
|
|
|
|
|
return undefined;
|
2018-05-02 16:51:22 +00:00
|
|
|
});
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2018-05-02 16:51:22 +00:00
|
|
|
|
2020-09-24 21:53:21 +00:00
|
|
|
async getStaleDeviceIdsForIdentifier(
|
|
|
|
identifier: string
|
2021-04-16 23:13:13 +00:00
|
|
|
): 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);
|
2020-03-05 21:14:58 +00:00
|
|
|
}
|
2021-04-16 23:13:13 +00:00
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
return updateDevices;
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2018-07-21 21:51:20 +00:00
|
|
|
|
2020-04-13 17:37:29 +00:00
|
|
|
async removeDeviceIdsForIdentifier(
|
|
|
|
identifier: string,
|
|
|
|
deviceIdsToRemove: Array<number>
|
2020-09-24 21:53:21 +00:00
|
|
|
): Promise<void> {
|
2020-12-09 22:05:11 +00:00
|
|
|
await Promise.all(
|
|
|
|
deviceIdsToRemove.map(async deviceId => {
|
2021-04-16 23:13:13 +00:00
|
|
|
await window.textsecure.storage.protocol.archiveSession(
|
|
|
|
`${identifier}.${deviceId}`
|
2020-12-09 22:05:11 +00:00
|
|
|
);
|
|
|
|
})
|
|
|
|
);
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
2018-05-02 16:51:22 +00:00
|
|
|
|
2020-09-24 21:53:21 +00:00
|
|
|
async sendToIdentifier(providedIdentifier: string): Promise<void> {
|
2020-09-04 01:25:19 +00:00
|
|
|
let identifier = providedIdentifier;
|
2018-10-18 01:01:21 +00:00
|
|
|
try {
|
2021-04-26 18:15:00 +00:00
|
|
|
if (window.isValidGuid(identifier)) {
|
|
|
|
// We're good!
|
|
|
|
} else if (isValidNumber(identifier)) {
|
|
|
|
if (!window.textsecure.messaging) {
|
|
|
|
throw new Error(
|
|
|
|
'sendToIdentifier: window.textsecure.messaging is not available!'
|
|
|
|
);
|
|
|
|
}
|
2021-04-28 18:36:10 +00:00
|
|
|
|
2021-04-26 18:15:00 +00:00
|
|
|
try {
|
2021-04-28 18:36:10 +00:00
|
|
|
await updateConversationsWithUuidLookup({
|
|
|
|
conversationController: window.ConversationController,
|
|
|
|
conversations: [
|
|
|
|
window.ConversationController.getOrCreate(identifier, 'private'),
|
|
|
|
],
|
|
|
|
messaging: window.textsecure.messaging,
|
|
|
|
});
|
2021-04-26 18:15:00 +00:00
|
|
|
|
2021-04-28 18:36:10 +00:00
|
|
|
const uuid = window.ConversationController.get(identifier)?.get(
|
|
|
|
'uuid'
|
|
|
|
);
|
|
|
|
if (!uuid) {
|
2021-04-26 18:15:00 +00:00
|
|
|
throw new UnregisteredUserError(
|
|
|
|
identifier,
|
|
|
|
new Error('User is not registered')
|
2020-09-09 02:25:05 +00:00
|
|
|
);
|
|
|
|
}
|
2021-04-28 18:36:10 +00:00
|
|
|
identifier = uuid;
|
2021-04-26 18:15:00 +00:00
|
|
|
} catch (error) {
|
|
|
|
window.log.error(
|
2021-05-04 16:44:17 +00:00
|
|
|
`sendToIdentifier: Failed to fetch UUID for identifier ${identifier}`,
|
2021-04-26 18:15:00 +00:00
|
|
|
error && error.stack ? error.stack : error
|
2020-09-04 01:25:19 +00:00
|
|
|
);
|
|
|
|
}
|
2021-04-26 18:15:00 +00:00
|
|
|
} else {
|
|
|
|
throw new Error(
|
|
|
|
`sendToIdentifier: identifier ${identifier} was neither a UUID or E164`
|
|
|
|
);
|
2020-09-04 01:25:19 +00:00
|
|
|
}
|
|
|
|
|
2020-03-05 21:14:58 +00:00
|
|
|
const updateDevices = await this.getStaleDeviceIdsForIdentifier(
|
|
|
|
identifier
|
|
|
|
);
|
|
|
|
await this.getKeysForIdentifier(identifier, updateDevices);
|
|
|
|
await this.reloadDevicesAndSend(identifier, true)();
|
2018-10-18 01:01:21 +00:00
|
|
|
} catch (error) {
|
2021-04-16 23:13:13 +00:00
|
|
|
if (error?.message?.includes('untrusted identity for address')) {
|
2020-04-13 17:37:29 +00:00
|
|
|
const newError = new OutgoingIdentityKeyError(
|
2020-03-05 21:14:58 +00:00
|
|
|
identifier,
|
2018-10-18 01:01:21 +00:00
|
|
|
error.originalMessage,
|
|
|
|
error.timestamp,
|
|
|
|
error.identityKey
|
|
|
|
);
|
2021-04-16 23:13:13 +00:00
|
|
|
this.registerError(identifier, 'Untrusted identity', newError);
|
2018-10-18 01:01:21 +00:00
|
|
|
} else {
|
|
|
|
this.registerError(
|
2020-03-05 21:14:58 +00:00
|
|
|
identifier,
|
2020-09-04 01:25:19 +00:00
|
|
|
`Failed to retrieve new device keys for identifier ${identifier}`,
|
2018-10-18 01:01:21 +00:00
|
|
|
error
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2020-04-13 17:37:29 +00:00
|
|
|
}
|
|
|
|
}
|