Signal-Desktop/ts/textsecure/MessageReceiver.ts

2732 lines
80 KiB
TypeScript
Raw Normal View History

// Copyright 2020-2021 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable no-bitwise */
/* eslint-disable class-methods-use-this */
/* eslint-disable more/no-then */
/* eslint-disable camelcase */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-classes-per-file */
/* eslint-disable no-restricted-syntax */
import { isNumber, map, omit, noop } from 'lodash';
import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid';
2021-05-28 19:11:19 +00:00
import { z } from 'zod';
import {
2021-05-28 19:11:19 +00:00
DecryptionErrorMessage,
groupDecrypt,
2021-05-28 19:11:19 +00:00
PlaintextContent,
PreKeySignalMessage,
processSenderKeyDistributionMessage,
ProtocolAddress,
PublicKey,
SealedSenderDecryptionResult,
sealedSenderDecryptMessage,
sealedSenderDecryptToUsmc,
SenderKeyDistributionMessage,
signalDecrypt,
signalDecryptPreKey,
SignalMessage,
UnidentifiedSenderMessageContent,
} from '@signalapp/signal-client';
import {
IdentityKeys,
PreKeys,
SenderKeys,
Sessions,
SignedPreKeys,
} from '../LibSignalStores';
import { BatcherType, createBatcher } from '../util/batcher';
import { parseIntOrThrow } from '../util/parseIntOrThrow';
2021-05-19 21:25:56 +00:00
import { Zone } from '../util/Zone';
import EventTarget from './EventTarget';
import { WebAPIType } from './WebAPI';
import utils from './Helpers';
import WebSocketResource, {
IncomingWebSocketRequest,
} from './WebsocketResources';
import Crypto from './Crypto';
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
import { ContactBuffer, GroupBuffer } from './ContactsParser';
import { isByteBufferEmpty } from '../util/isByteBufferEmpty';
import {
AttachmentPointerClass,
2020-06-04 18:16:19 +00:00
CallingMessageClass,
DataMessageClass,
DownloadAttachmentType,
EnvelopeClass,
ReceiptMessageClass,
SyncMessageClass,
TypingMessageClass,
UnprocessedType,
VerifiedClass,
} from '../textsecure.d';
import { ByteBufferClass } from '../window.d';
import { WebSocket } from './WebSocket';
2020-09-09 02:25:05 +00:00
import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups';
2020-11-20 17:30:45 +00:00
const GROUPV1_ID_LENGTH = 16;
const GROUPV2_ID_LENGTH = 32;
const RETRY_TIMEOUT = 2 * 60 * 1000;
2021-05-28 19:11:19 +00:00
const decryptionErrorTypeSchema = z
.object({
cipherTextBytes: z.instanceof(ArrayBuffer).optional(),
cipherTextType: z.number().optional(),
contentHint: z.number().optional(),
groupId: z.string().optional(),
receivedAtCounter: z.number(),
receivedAtDate: z.number(),
senderDevice: z.number(),
senderUuid: z.string(),
timestamp: z.number(),
})
.passthrough();
export type DecryptionErrorType = z.infer<typeof decryptionErrorTypeSchema>;
const retryRequestTypeSchema = z
.object({
2021-06-08 21:51:58 +00:00
groupId: z.string().optional(),
2021-05-28 19:11:19 +00:00
requesterUuid: z.string(),
requesterDevice: z.number(),
senderDevice: z.number(),
sentAt: z.number(),
})
.passthrough();
export type RetryRequestType = z.infer<typeof retryRequestTypeSchema>;
2021-02-18 16:40:26 +00:00
declare global {
// We want to extend `Event`, so we need an interface.
// eslint-disable-next-line no-restricted-syntax
interface Event {
code?: string | number;
configuration?: any;
confirm?: () => void;
contactDetails?: any;
count?: number;
data?: any;
deliveryReceipt?: any;
error?: any;
eventType?: string | number;
groupDetails?: any;
2020-05-27 21:37:06 +00:00
groupId?: string;
2020-11-20 17:30:45 +00:00
groupV2Id?: string;
2020-10-30 17:56:03 +00:00
messageRequestResponseType?: number | null;
proto?: any;
read?: any;
reason?: any;
sender?: any;
senderDevice?: any;
senderUuid?: any;
source?: any;
sourceUuid?: any;
stickerPacks?: any;
2020-10-30 17:56:03 +00:00
threadE164?: string | null;
threadUuid?: string | null;
storageServiceKey?: ArrayBuffer;
timestamp?: any;
typing?: any;
verified?: any;
2021-05-28 19:11:19 +00:00
retryRequest?: RetryRequestType;
decryptionError?: DecryptionErrorType;
}
// We want to extend `Error`, so we need an interface.
// eslint-disable-next-line no-restricted-syntax
interface Error {
reason?: any;
stackForLog?: string;
}
}
type CacheAddItemType = {
envelope: EnvelopeClass;
data: UnprocessedType;
2021-05-24 21:30:56 +00:00
request: Pick<IncomingWebSocketRequest, 'respond'>;
};
type DecryptedEnvelope = {
readonly plaintext: ArrayBuffer;
readonly data: UnprocessedType;
readonly envelope: EnvelopeClass;
};
type LockedStores = {
readonly sessionStore: Sessions;
readonly identityKeyStore: IdentityKeys;
readonly zone?: Zone;
};
enum TaskType {
Encrypted = 'Encrypted',
Decrypted = 'Decrypted',
}
class MessageReceiverInner extends EventTarget {
_onClose?: (ev: any) => Promise<void>;
appQueue: PQueue;
2021-05-24 21:30:56 +00:00
decryptAndCacheBatcher: BatcherType<CacheAddItemType>;
cacheRemoveBatcher: BatcherType<string>;
calledClose?: boolean;
count: number;
processedCount: number;
deviceId?: number;
hasConnected?: boolean;
incomingQueue: PQueue;
isEmptied?: boolean;
number_id?: string;
password: string;
encryptedQueue: PQueue;
decryptedQueue: PQueue;
retryCachedTimeout: any;
server: WebAPIType;
serverTrustRoot: ArrayBuffer;
signalingKey: ArrayBuffer;
socket?: WebSocket;
stoppingProcessing?: boolean;
username: string;
uuid: string;
uuid_id?: string;
wsr?: WebSocketResource;
constructor(
oldUsername: string,
username: string,
password: string,
signalingKey: ArrayBuffer,
options: {
serverTrustRoot: string;
}
) {
super();
this.count = 0;
this.processedCount = 0;
this.signalingKey = signalingKey;
this.username = oldUsername;
this.uuid = username;
this.password = password;
this.server = window.WebAPI.connect({
username: username || oldUsername,
password,
});
if (!options.serverTrustRoot) {
throw new Error('Server trust root is required!');
}
this.serverTrustRoot = MessageReceiverInner.stringToArrayBufferBase64(
options.serverTrustRoot
);
this.number_id = oldUsername
? utils.unencodeNumber(oldUsername)[0]
: undefined;
this.uuid_id = username ? utils.unencodeNumber(username)[0] : undefined;
this.deviceId =
username || oldUsername
? parseIntOrThrow(
utils.unencodeNumber(username || oldUsername)[1],
'MessageReceiver.constructor: username || oldUsername'
)
: undefined;
this.incomingQueue = new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 });
this.appQueue = new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 });
// All envelopes start in encryptedQueue and progress to decryptedQueue
this.encryptedQueue = new PQueue({
concurrency: 1,
timeout: 1000 * 60 * 2,
});
this.decryptedQueue = new PQueue({
concurrency: 1,
timeout: 1000 * 60 * 2,
});
2021-05-24 21:30:56 +00:00
this.decryptAndCacheBatcher = createBatcher<CacheAddItemType>({
name: 'MessageReceiver.decryptAndCacheBatcher',
wait: 75,
maxSize: 30,
2021-03-26 00:00:03 +00:00
processBatch: (items: Array<CacheAddItemType>) => {
// Not returning the promise here because we don't want to stall
// the batch.
2021-05-24 21:30:56 +00:00
this.decryptAndCacheBatch(items);
2021-03-26 00:00:03 +00:00
},
});
this.cacheRemoveBatcher = createBatcher<string>({
2021-03-26 00:00:03 +00:00
name: 'MessageReceiver.cacheRemoveBatcher',
wait: 75,
maxSize: 30,
processBatch: this.cacheRemoveBatch.bind(this),
});
}
static stringToArrayBuffer = (string: string): ArrayBuffer =>
window.dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();
static arrayBufferToString = (arrayBuffer: ArrayBuffer): string =>
window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');
static stringToArrayBufferBase64 = (string: string): ArrayBuffer =>
window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();
static arrayBufferToStringBase64 = (arrayBuffer: ArrayBuffer): string =>
window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
connect() {
if (this.calledClose) {
return;
}
2020-10-30 17:56:03 +00:00
// We always process our cache before processing a new websocket message
this.incomingQueue.add(async () => this.queueAllCached());
2020-10-30 17:56:03 +00:00
this.count = 0;
if (this.hasConnected) {
const ev = new Event('reconnect');
this.dispatchEvent(ev);
}
this.isEmptied = false;
2021-02-18 16:40:26 +00:00
this.hasConnected = true;
if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
this.socket.close();
if (this.wsr) {
this.wsr.close();
}
}
// initialize the socket and start listening for messages
this.socket = this.server.getMessageSocket();
this.socket.onclose = this.onclose.bind(this);
this.socket.onerror = this.onerror.bind(this);
this.socket.onopen = this.onopen.bind(this);
this.wsr = new WebSocketResource(this.socket, {
handleRequest: this.handleRequest.bind(this),
keepalive: {
path: '/v1/keepalive',
disconnect: true,
},
});
// Because sometimes the socket doesn't properly emit its close event
this._onClose = this.onclose.bind(this);
if (this._onClose) {
this.wsr.addEventListener('close', this._onClose);
}
}
async stopProcessing() {
window.log.info('MessageReceiver: stopProcessing requested');
this.stoppingProcessing = true;
return this.close();
}
2019-09-26 19:56:31 +00:00
unregisterBatchers() {
window.log.info('MessageReceiver: unregister batchers');
2021-05-24 21:30:56 +00:00
this.decryptAndCacheBatcher.unregister();
2019-09-26 19:56:31 +00:00
this.cacheRemoveBatcher.unregister();
}
shutdown() {
if (this.socket) {
this.socket.onclose = noop;
this.socket.onerror = noop;
this.socket.onopen = noop;
2020-11-20 17:30:45 +00:00
this.socket = undefined;
}
if (this.wsr) {
if (this._onClose) {
this.wsr.removeEventListener('close', this._onClose);
}
this.wsr = undefined;
}
}
async close() {
window.log.info('MessageReceiver.close()');
this.calledClose = true;
// Our WebSocketResource instance will close the socket and emit a 'close' event
// if the socket doesn't emit one quickly enough.
if (this.wsr) {
this.wsr.close(3000, 'called close');
}
this.clearRetryTimeout();
return this.drain();
}
onopen() {
window.log.info('websocket open');
2021-04-13 23:43:56 +00:00
window.logMessageReceiverConnect();
}
onerror() {
window.log.error('websocket error');
}
async dispatchAndWait(event: Event) {
this.appQueue.add(async () => Promise.all(this.dispatchEvent(event)));
return Promise.resolve();
}
async onclose(ev: any) {
window.log.info(
'websocket closed',
ev.code,
ev.reason || '',
'calledClose:',
this.calledClose
);
this.shutdown();
if (this.calledClose) {
return Promise.resolve();
}
if (ev.code === 3000) {
return Promise.resolve();
}
if (ev.code === 3001) {
this.onEmpty();
}
// possible 403 or network issue. Make an request to confirm
2018-05-02 16:51:22 +00:00
return this.server
.getDevices()
.then(this.connect.bind(this)) // No HTTP error? Reconnect
.catch(async e => {
const event = new Event('error');
event.error = e;
return this.dispatchAndWait(event);
});
}
handleRequest(request: IncomingWebSocketRequest) {
// We do the message decryption here, instead of in the ordered pending queue,
// to avoid exposing the time it took us to process messages through the time-to-ack.
if (request.path !== '/api/v1/message') {
window.log.info('got request', request.verb, request.path);
request.respond(200, 'OK');
if (request.verb === 'PUT' && request.path === '/api/v1/queue/empty') {
this.incomingQueue.add(() => {
this.onEmpty();
});
}
return;
}
2019-07-09 18:46:48 +00:00
const job = async () => {
let plaintext;
const headers = request.headers || [];
if (!request.body) {
throw new Error(
'MessageReceiver.handleRequest: request.body was falsey!'
);
}
2019-07-09 18:46:48 +00:00
if (headers.includes('X-Signal-Key: true')) {
plaintext = await Crypto.decryptWebsocketMessage(
2019-07-09 18:46:48 +00:00
request.body,
this.signalingKey
);
} else {
plaintext = request.body.toArrayBuffer();
}
try {
const envelope = window.textsecure.protobuf.Envelope.decode(plaintext);
window.normalizeUuids(
envelope,
['sourceUuid'],
'message_receiver::handleRequest::job'
);
2018-05-02 16:51:22 +00:00
// After this point, decoding errors are not the server's
// fault, and we should handle them gracefully and tell the
// user they received an invalid message
if (envelope.source && this.isBlocked(envelope.source)) {
2019-07-09 18:46:48 +00:00
request.respond(200, 'OK');
return;
2018-05-02 16:51:22 +00:00
}
if (envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid)) {
request.respond(200, 'OK');
return;
}
2020-03-20 19:01:15 +00:00
// Make non-private envelope IDs dashless so they don't get redacted
// from logs
envelope.id = getGuid().replace(/-/g, '');
2018-11-09 01:23:07 +00:00
envelope.serverTimestamp = envelope.serverTimestamp
? envelope.serverTimestamp.toNumber()
: null;
2021-03-04 21:44:57 +00:00
envelope.receivedAtCounter = window.Signal.Util.incrementMessageCounter();
envelope.receivedAtDate = Date.now();
// Calculate the message age (time on server).
envelope.messageAgeSec = this.calculateMessageAge(
headers,
envelope.serverTimestamp
);
2021-05-24 21:30:56 +00:00
this.decryptAndCache(envelope, plaintext, request);
2021-03-13 01:22:36 +00:00
this.processedCount += 1;
2019-07-09 18:46:48 +00:00
} catch (e) {
2018-05-02 16:51:22 +00:00
request.respond(500, 'Bad encrypted websocket message');
window.log.error(
2018-05-02 16:51:22 +00:00
'Error handling incoming message:',
e && e.stack ? e.stack : e
);
2018-05-02 16:51:22 +00:00
const ev = new Event('error');
ev.error = e;
2019-07-09 18:46:48 +00:00
await this.dispatchAndWait(ev);
}
};
2019-07-09 18:46:48 +00:00
this.incomingQueue.add(job);
}
calculateMessageAge(
headers: Array<string>,
serverTimestamp?: number
): number {
let messageAgeSec = 0; // Default to 0 in case of unreliable parameters.
if (serverTimestamp) {
// The 'X-Signal-Timestamp' is usually the last item, so start there.
let it = headers.length;
// eslint-disable-next-line no-plusplus
while (--it >= 0) {
const match = headers[it].match(/^X-Signal-Timestamp:\s*(\d+)\s*$/);
if (match && match.length === 2) {
const timestamp = Number(match[1]);
// One final sanity check, the timestamp when a message is pulled from
// the server should be later than when it was pushed.
if (timestamp > serverTimestamp) {
messageAgeSec = Math.floor((timestamp - serverTimestamp) / 1000);
}
break;
}
}
}
return messageAgeSec;
}
async addToQueue<T>(task: () => Promise<T>, taskType: TaskType): Promise<T> {
if (taskType === TaskType.Encrypted) {
this.count += 1;
}
const queue =
taskType === TaskType.Encrypted
? this.encryptedQueue
: this.decryptedQueue;
try {
return await queue.add(task);
} finally {
this.updateProgress(this.count);
}
}
2020-09-09 02:25:05 +00:00
hasEmptied(): boolean {
return Boolean(this.isEmptied);
}
onEmpty() {
2021-03-26 00:00:03 +00:00
const emitEmpty = async () => {
await Promise.all([
2021-05-24 21:30:56 +00:00
this.decryptAndCacheBatcher.flushAndWait(),
2021-03-26 00:00:03 +00:00
this.cacheRemoveBatcher.flushAndWait(),
]);
window.log.info("MessageReceiver: emitting 'empty' event");
const ev = new Event('empty');
this.dispatchEvent(ev);
this.isEmptied = true;
this.maybeScheduleRetryTimeout();
};
const waitForDecryptedQueue = async () => {
window.log.info(
"MessageReceiver: finished processing messages after 'empty', now waiting for application"
);
2019-07-09 18:46:48 +00:00
// We don't await here because we don't want this to gate future message processing
this.appQueue.add(emitEmpty);
};
const waitForEncryptedQueue = async () => {
this.addToQueue(waitForDecryptedQueue, TaskType.Decrypted);
};
2019-07-09 18:46:48 +00:00
const waitForIncomingQueue = () => {
this.addToQueue(waitForEncryptedQueue, TaskType.Encrypted);
2019-07-09 18:46:48 +00:00
// Note: this.count is used in addToQueue
// Resetting count so everything from the websocket after this starts at zero
this.count = 0;
};
2019-09-26 19:56:31 +00:00
const waitForCacheAddBatcher = async () => {
2021-05-24 21:30:56 +00:00
await this.decryptAndCacheBatcher.onIdle();
2019-09-26 19:56:31 +00:00
this.incomingQueue.add(waitForIncomingQueue);
};
waitForCacheAddBatcher();
}
async drain() {
const waitForEncryptedQueue = async () =>
this.addToQueue(async () => {
window.log.info('drained');
}, TaskType.Decrypted);
const waitForIncomingQueue = async () =>
this.addToQueue(waitForEncryptedQueue, TaskType.Encrypted);
2019-07-09 18:46:48 +00:00
return this.incomingQueue.add(waitForIncomingQueue);
}
updateProgress(count: number) {
// count by 10s
if (count % 10 !== 0) {
return;
}
const ev = new Event('progress');
ev.count = count;
this.dispatchEvent(ev);
}
async queueAllCached() {
const items = await this.getAllFromCache();
const max = items.length;
for (let i = 0; i < max; i += 1) {
// eslint-disable-next-line no-await-in-loop
await this.queueCached(items[i]);
}
}
async queueCached(item: UnprocessedType) {
window.log.info('MessageReceiver.queueCached', item.id);
try {
let envelopePlaintext: ArrayBuffer;
if (item.envelope && item.version === 2) {
envelopePlaintext = MessageReceiverInner.stringToArrayBufferBase64(
item.envelope
);
2021-05-24 21:30:56 +00:00
} else if (item.envelope && typeof item.envelope === 'string') {
envelopePlaintext = MessageReceiverInner.stringToArrayBuffer(
item.envelope
);
} else {
throw new Error(
'MessageReceiver.queueCached: item.envelope was malformed'
);
}
const envelope = window.textsecure.protobuf.Envelope.decode(
envelopePlaintext
);
envelope.id = item.id;
2021-03-04 21:44:57 +00:00
envelope.receivedAtCounter = item.timestamp;
envelope.receivedAtDate = Date.now();
2018-11-09 01:23:07 +00:00
envelope.source = envelope.source || item.source;
envelope.sourceUuid = envelope.sourceUuid || item.sourceUuid;
2018-11-09 01:23:07 +00:00
envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice;
envelope.serverTimestamp =
item.serverTimestamp || envelope.serverTimestamp;
if (envelope.serverTimestamp && envelope.serverTimestamp.toNumber) {
envelope.serverTimestamp = envelope.serverTimestamp.toNumber();
}
const { decrypted } = item;
if (decrypted) {
let payloadPlaintext: ArrayBuffer;
if (item.version === 2) {
payloadPlaintext = MessageReceiverInner.stringToArrayBufferBase64(
decrypted
);
} else if (typeof decrypted === 'string') {
payloadPlaintext = MessageReceiverInner.stringToArrayBuffer(
decrypted
);
} else {
throw new Error('Cached decrypted value was not a string!');
}
// Maintain invariant: encrypted queue => decrypted queue
this.addToQueue(async () => {
this.queueDecryptedEnvelope(envelope, payloadPlaintext);
}, TaskType.Encrypted);
} else {
2021-05-24 21:30:56 +00:00
this.queueCachedEnvelope(item, envelope);
}
} catch (error) {
window.log.error(
'queueCached error handling item',
item.id,
'removing it. Error:',
error && error.stack ? error.stack : error
);
try {
const { id } = item;
await window.textsecure.storage.unprocessed.remove(id);
} catch (deleteError) {
window.log.error(
'queueCached error deleting item',
item.id,
'Error:',
deleteError && deleteError.stack ? deleteError.stack : deleteError
);
}
}
}
getEnvelopeId(envelope: EnvelopeClass) {
const timestamp =
envelope && envelope.timestamp && envelope.timestamp.toNumber
? envelope.timestamp.toNumber()
: null;
if (envelope.sourceUuid || envelope.source) {
const sender = envelope.sourceUuid || envelope.source;
return `${sender}.${envelope.sourceDevice} ${timestamp} (${envelope.id})`;
}
return envelope.id;
}
clearRetryTimeout() {
if (this.retryCachedTimeout) {
clearInterval(this.retryCachedTimeout);
this.retryCachedTimeout = null;
}
}
maybeScheduleRetryTimeout() {
if (this.isEmptied) {
this.clearRetryTimeout();
this.retryCachedTimeout = setTimeout(() => {
this.incomingQueue.add(async () => this.queueAllCached());
}, RETRY_TIMEOUT);
}
}
async getAllFromCache() {
window.log.info('getAllFromCache');
const count = await window.textsecure.storage.unprocessed.getCount();
if (count > 1500) {
await window.textsecure.storage.unprocessed.removeAll();
window.log.warn(
`There were ${count} messages in cache. Deleted all instead of reprocessing`
);
return [];
}
const items = await window.textsecure.storage.unprocessed.getAll();
window.log.info('getAllFromCache loaded', items.length, 'saved envelopes');
return Promise.all(
map(items, async item => {
const attempts = 1 + (item.attempts || 0);
try {
if (attempts >= 3) {
window.log.warn(
'getAllFromCache final attempt for envelope',
item.id
);
await window.textsecure.storage.unprocessed.remove(item.id);
} else {
await window.textsecure.storage.unprocessed.updateAttempts(
item.id,
attempts
);
2018-05-02 16:51:22 +00:00
}
} catch (error) {
window.log.error(
'getAllFromCache error updating item after load:',
2018-05-02 16:51:22 +00:00
error && error.stack ? error.stack : error
);
}
return item;
})
);
}
2021-05-24 21:30:56 +00:00
async decryptAndCacheBatch(items: Array<CacheAddItemType>) {
window.log.info('MessageReceiver.decryptAndCacheBatch', items.length);
const decrypted: Array<DecryptedEnvelope> = [];
2021-05-19 21:25:56 +00:00
const storageProtocol = window.textsecure.storage.protocol;
2019-09-26 19:56:31 +00:00
try {
2021-05-24 21:30:56 +00:00
const zone = new Zone('decryptAndCacheBatch', {
2021-05-19 21:25:56 +00:00
pendingSessions: true,
pendingUnprocessed: true,
});
2021-05-19 21:25:56 +00:00
const sessionStore = new Sessions({ zone });
const identityKeyStore = new IdentityKeys({ zone });
const failed: Array<UnprocessedType> = [];
// Below we:
//
2021-05-19 21:25:56 +00:00
// 1. Enter zone
// 2. Decrypt all batched envelopes
// 3. Persist both decrypted envelopes and envelopes that we failed to
// decrypt (for future retries, see `attempts` field)
2021-05-19 21:25:56 +00:00
// 4. Leave zone and commit all pending sessions and unprocesseds
// 5. Acknowledge envelopes (can't fail)
// 6. Finally process decrypted envelopes
2021-05-19 21:25:56 +00:00
await storageProtocol.withZone(zone, 'MessageReceiver', async () => {
await Promise.all<void>(
items.map(async ({ data, envelope }) => {
try {
const plaintext = await this.queueEncryptedEnvelope(
{ sessionStore, identityKeyStore, zone },
envelope
);
if (plaintext) {
decrypted.push({ plaintext, data, envelope });
}
} catch (error) {
failed.push(data);
window.log.error(
2021-05-24 21:30:56 +00:00
'decryptAndCache error when processing the envelope',
error && error.stack ? error.stack : error
);
}
})
);
window.log.info(
2021-05-24 21:30:56 +00:00
'MessageReceiver.decryptAndCacheBatch storing ' +
`${decrypted.length} decrypted envelopes`
);
// Store both decrypted and failed unprocessed envelopes
const unprocesseds: Array<UnprocessedType> = decrypted.map(
({ envelope, data, plaintext }) => {
return {
...data,
source: envelope.source,
sourceUuid: envelope.sourceUuid,
sourceDevice: envelope.sourceDevice,
serverGuid: envelope.serverGuid,
serverTimestamp: envelope.serverTimestamp,
decrypted: MessageReceiverInner.arrayBufferToStringBase64(
plaintext
),
};
}
);
2021-05-19 21:25:56 +00:00
await storageProtocol.addMultipleUnprocessed(
unprocesseds.concat(failed),
{ zone }
);
});
window.log.info(
2021-05-24 21:30:56 +00:00
'MessageReceiver.decryptAndCacheBatch acknowledging receipt'
);
// Acknowledge all envelopes
for (const { request } of items) {
try {
request.respond(200, 'OK');
} catch (error) {
window.log.error(
2021-05-24 21:30:56 +00:00
'decryptAndCacheBatch: Failed to send 200 to server; still queuing envelope'
);
}
}
2019-09-26 19:56:31 +00:00
} catch (error) {
window.log.error(
2021-05-24 21:30:56 +00:00
'decryptAndCache error trying to add messages to cache:',
2019-09-26 19:56:31 +00:00
error && error.stack ? error.stack : error
);
items.forEach(item => {
item.request.respond(500, 'Failed to cache message');
});
return;
2019-09-26 19:56:31 +00:00
}
await Promise.all(
decrypted.map(async ({ envelope, plaintext }) => {
try {
await this.queueDecryptedEnvelope(envelope, plaintext);
} catch (error) {
window.log.error(
2021-05-24 21:30:56 +00:00
'decryptAndCache error when processing decrypted envelope',
error && error.stack ? error.stack : error
);
}
})
);
2021-05-24 21:30:56 +00:00
window.log.info('MessageReceiver.decryptAndCacheBatch fully processed');
this.maybeScheduleRetryTimeout();
}
2021-05-24 21:30:56 +00:00
decryptAndCache(
envelope: EnvelopeClass,
plaintext: ArrayBuffer,
request: IncomingWebSocketRequest
) {
const { id } = envelope;
const data = {
id,
version: 2,
envelope: MessageReceiverInner.arrayBufferToStringBase64(plaintext),
2021-03-04 21:44:57 +00:00
timestamp: envelope.receivedAtCounter,
attempts: 1,
};
2021-05-24 21:30:56 +00:00
this.decryptAndCacheBatcher.add({
2019-09-26 19:56:31 +00:00
request,
envelope,
data,
});
}
async cacheRemoveBatch(items: Array<string>) {
await window.textsecure.storage.unprocessed.remove(items);
}
removeFromCache(envelope: EnvelopeClass) {
const { id } = envelope;
2019-09-26 19:56:31 +00:00
this.cacheRemoveBatcher.add(id);
}
async queueDecryptedEnvelope(
envelope: EnvelopeClass,
plaintext: ArrayBuffer
) {
const id = this.getEnvelopeId(envelope);
window.log.info('queueing decrypted envelope', id);
2021-03-04 21:44:57 +00:00
const task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext);
const taskWithTimeout = window.textsecure.createTaskWithTimeout(
task,
2021-05-24 21:30:56 +00:00
`queueDecryptedEnvelope ${id}`
);
const promise = this.addToQueue(taskWithTimeout, TaskType.Decrypted);
2021-03-04 21:44:57 +00:00
return promise.catch(error => {
window.log.error(
`queueDecryptedEnvelope error handling envelope ${id}:`,
error && error.extra ? JSON.stringify(error.extra) : '',
error && error.stack ? error.stack : error
);
});
}
async queueEncryptedEnvelope(
stores: LockedStores,
envelope: EnvelopeClass
): Promise<ArrayBuffer | undefined> {
const id = this.getEnvelopeId(envelope);
window.log.info('queueing envelope', id);
2021-03-04 21:44:57 +00:00
const task = this.decryptEnvelope.bind(this, stores, envelope);
const taskWithTimeout = window.textsecure.createTaskWithTimeout(
task,
`queueEncryptedEnvelope ${id}`
);
2021-03-04 21:44:57 +00:00
try {
return await this.addToQueue(taskWithTimeout, TaskType.Encrypted);
} catch (error) {
2020-03-20 19:01:15 +00:00
const args = [
'queueEncryptedEnvelope error handling envelope',
this.getEnvelopeId(envelope),
':',
error && error.extra ? JSON.stringify(error.extra) : '',
2020-03-20 19:01:15 +00:00
error && error.stack ? error.stack : error,
];
if (error.warn) {
window.log.warn(...args);
} else {
window.log.error(...args);
}
return undefined;
}
}
2021-05-24 21:30:56 +00:00
async queueCachedEnvelope(
data: UnprocessedType,
envelope: EnvelopeClass
): Promise<void> {
this.decryptAndCacheBatcher.add({
request: {
respond(code, status) {
window.log.info(
'queueCachedEnvelope: fake response ' +
`with code ${code} and status ${status}`
);
},
},
2021-05-24 21:30:56 +00:00
envelope,
data,
});
}
// Called after `decryptEnvelope` decrypted the message.
async handleDecryptedEnvelope(
envelope: EnvelopeClass,
plaintext: ArrayBuffer
): Promise<void> {
if (this.stoppingProcessing) {
return;
}
// No decryption is required for delivery receipts, so the decrypted field of
// the Unprocessed model will never be set
if (envelope.content) {
await this.innerHandleContentMessage(envelope, plaintext);
return;
}
if (envelope.legacyMessage) {
await this.innerHandleLegacyMessage(envelope, plaintext);
return;
}
this.removeFromCache(envelope);
throw new Error('Received message with no content and no legacyMessage');
}
async decryptEnvelope(
stores: LockedStores,
envelope: EnvelopeClass
): Promise<ArrayBuffer | undefined> {
if (this.stoppingProcessing) {
return undefined;
}
if (envelope.type === window.textsecure.protobuf.Envelope.Type.RECEIPT) {
await this.onDeliveryReceipt(envelope);
return undefined;
}
if (envelope.content) {
return this.decryptContentMessage(stores, envelope);
}
if (envelope.legacyMessage) {
return this.decryptLegacyMessage(stores, envelope);
}
this.removeFromCache(envelope);
throw new Error('Received message with no content and no legacyMessage');
}
getStatus() {
if (this.socket) {
return this.socket.readyState;
}
if (this.hasConnected) {
return WebSocket.CLOSED;
}
return -1;
}
async onDeliveryReceipt(envelope: EnvelopeClass): Promise<void> {
return new Promise((resolve, reject) => {
const ev = new Event('delivery');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.deliveryReceipt = {
timestamp: envelope.timestamp.toNumber(),
source: envelope.source,
sourceUuid: envelope.sourceUuid,
sourceDevice: envelope.sourceDevice,
};
this.dispatchAndWait(ev).then(resolve as any, reject as any);
});
}
unpad(paddedData: ArrayBuffer) {
const paddedPlaintext = new Uint8Array(paddedData);
let plaintext;
for (let i = paddedPlaintext.length - 1; i >= 0; i -= 1) {
if (paddedPlaintext[i] === 0x80) {
plaintext = new Uint8Array(i);
plaintext.set(paddedPlaintext.subarray(0, i));
plaintext = plaintext.buffer;
break;
} else if (paddedPlaintext[i] !== 0x00) {
throw new Error('Invalid padding');
}
}
return plaintext;
}
async decrypt(
{ sessionStore, identityKeyStore, zone }: LockedStores,
envelope: EnvelopeClass,
ciphertext: ByteBufferClass
): Promise<ArrayBuffer | null> {
const logId = this.getEnvelopeId(envelope);
const { serverTrustRoot } = this;
const envelopeTypeEnum = window.textsecure.protobuf.Envelope.Type;
const unidentifiedSenderTypeEnum =
window.textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
const identifier = envelope.sourceUuid || envelope.source;
const { sourceDevice } = envelope;
const localE164 = window.textsecure.storage.user.getNumber();
const localUuid = window.textsecure.storage.user.getUuid();
const localDeviceId = parseIntOrThrow(
window.textsecure.storage.user.getDeviceId(),
'MessageReceiver.decrypt: localDeviceId'
);
if (!localUuid) {
throw new Error('MessageReceiver.decrypt: Failed to fetch local UUID');
}
const preKeyStore = new PreKeys();
const signedPreKeyStore = new SignedPreKeys();
let promise: Promise<
ArrayBuffer | { isMe: boolean } | { isBlocked: boolean } | undefined
>;
2021-05-28 19:11:19 +00:00
if (envelope.type === envelopeTypeEnum.PLAINTEXT_CONTENT) {
window.log.info(`decrypt/${logId}: plaintext message`);
2021-05-28 19:11:19 +00:00
const buffer = Buffer.from(ciphertext.toArrayBuffer());
const plaintextContent = PlaintextContent.deserialize(buffer);
promise = Promise.resolve(
this.unpad(typedArrayToArrayBuffer(plaintextContent.body()))
);
} else if (envelope.type === envelopeTypeEnum.CIPHERTEXT) {
window.log.info(`decrypt/${logId}: ciphertext message`);
if (!identifier) {
throw new Error(
'MessageReceiver.decrypt: No identifier for CIPHERTEXT message'
);
}
if (!sourceDevice) {
throw new Error(
'MessageReceiver.decrypt: No sourceDevice for CIPHERTEXT message'
);
}
const signalMessage = SignalMessage.deserialize(
Buffer.from(ciphertext.toArrayBuffer())
);
const address = `${identifier}.${sourceDevice}`;
promise = window.textsecure.storage.protocol.enqueueSessionJob(
address,
() =>
signalDecrypt(
signalMessage,
ProtocolAddress.new(identifier, sourceDevice),
sessionStore,
identityKeyStore
).then(plaintext => this.unpad(typedArrayToArrayBuffer(plaintext))),
zone
);
} else if (envelope.type === envelopeTypeEnum.PREKEY_BUNDLE) {
window.log.info(`decrypt/${logId}: prekey message`);
if (!identifier) {
throw new Error(
'MessageReceiver.decrypt: No identifier for PREKEY_BUNDLE message'
);
}
if (!sourceDevice) {
throw new Error(
'MessageReceiver.decrypt: No sourceDevice for PREKEY_BUNDLE message'
);
}
const preKeySignalMessage = PreKeySignalMessage.deserialize(
Buffer.from(ciphertext.toArrayBuffer())
);
const address = `${identifier}.${sourceDevice}`;
promise = window.textsecure.storage.protocol.enqueueSessionJob(
address,
() =>
signalDecryptPreKey(
preKeySignalMessage,
ProtocolAddress.new(identifier, sourceDevice),
sessionStore,
identityKeyStore,
preKeyStore,
signedPreKeyStore
).then(plaintext => this.unpad(typedArrayToArrayBuffer(plaintext))),
zone
);
} else if (envelope.type === envelopeTypeEnum.UNIDENTIFIED_SENDER) {
window.log.info(`decrypt/${logId}: unidentified message`);
const buffer = Buffer.from(ciphertext.toArrayBuffer());
const decryptSealedSender = async (): Promise<
SealedSenderDecryptionResult | Buffer | null | { isBlocked: true }
> => {
const messageContent: UnidentifiedSenderMessageContent = await sealedSenderDecryptToUsmc(
buffer,
identityKeyStore
);
// Here we take this sender information and attach it back to the envelope
// to make the rest of the app work properly.
const certificate = messageContent.senderCertificate();
const originalSource = envelope.source;
const originalSourceUuid = envelope.sourceUuid;
// eslint-disable-next-line no-param-reassign
envelope.source = certificate.senderE164() || undefined;
// eslint-disable-next-line no-param-reassign
envelope.sourceUuid = certificate.senderUuid();
window.normalizeUuids(
envelope,
['sourceUuid'],
'message_receiver::decrypt::UNIDENTIFIED_SENDER'
2018-05-02 16:51:22 +00:00
);
// eslint-disable-next-line no-param-reassign
envelope.sourceDevice = certificate.senderDeviceId();
// eslint-disable-next-line no-param-reassign
envelope.unidentifiedDeliveryReceived = !(
originalSource || originalSourceUuid
);
const unidentifiedLogId = this.getEnvelopeId(envelope);
2021-05-28 19:11:19 +00:00
// eslint-disable-next-line no-param-reassign
envelope.contentHint = messageContent.contentHint();
// eslint-disable-next-line no-param-reassign
envelope.groupId = messageContent.groupId()?.toString('base64');
// eslint-disable-next-line no-param-reassign
envelope.usmc = messageContent;
if (
(envelope.source && this.isBlocked(envelope.source)) ||
(envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid))
) {
window.log.info(
'MessageReceiver.decrypt: Dropping blocked message after partial sealed sender decryption'
);
return { isBlocked: true };
}
if (!envelope.serverTimestamp) {
throw new Error(
'MessageReceiver.decrypt: Sealed sender message was missing serverTimestamp'
);
}
2021-05-28 19:11:19 +00:00
if (
messageContent.msgType() ===
unidentifiedSenderTypeEnum.PLAINTEXT_CONTENT
) {
window.log.info(
`decrypt/${unidentifiedLogId}: unidentified message/plaintext contents`
);
2021-05-28 19:11:19 +00:00
const plaintextContent = PlaintextContent.deserialize(
messageContent.contents()
);
return plaintextContent.body();
}
if (
messageContent.msgType() ===
unidentifiedSenderTypeEnum.SENDERKEY_MESSAGE
) {
window.log.info(
`decrypt/${unidentifiedLogId}: unidentified message/sender key contents`
);
const sealedSenderIdentifier = certificate.senderUuid();
const sealedSenderSourceDevice = certificate.senderDeviceId();
const senderKeyStore = new SenderKeys();
const address = `${sealedSenderIdentifier}.${sealedSenderSourceDevice}`;
return window.textsecure.storage.protocol.enqueueSenderKeyJob(
address,
() =>
groupDecrypt(
ProtocolAddress.new(
sealedSenderIdentifier,
sealedSenderSourceDevice
),
senderKeyStore,
2021-05-24 18:59:45 +00:00
messageContent.contents()
),
zone
);
}
window.log.info(
`decrypt/${unidentifiedLogId}: unidentified message/passing to sealedSenderDecryptMessage`
);
const sealedSenderIdentifier = envelope.sourceUuid || envelope.source;
const address = `${sealedSenderIdentifier}.${envelope.sourceDevice}`;
return window.textsecure.storage.protocol.enqueueSessionJob(
address,
() =>
sealedSenderDecryptMessage(
buffer,
PublicKey.deserialize(Buffer.from(serverTrustRoot)),
envelope.serverTimestamp,
localE164,
localUuid,
localDeviceId,
sessionStore,
identityKeyStore,
preKeyStore,
signedPreKeyStore
),
zone
);
};
promise = decryptSealedSender().then(result => {
if (result === null) {
return { isMe: true };
}
if ('isBlocked' in result) {
return result;
}
if (result instanceof Buffer) {
return this.unpad(typedArrayToArrayBuffer(result));
}
const content = typedArrayToArrayBuffer(result.message());
if (!content) {
throw new Error(
'MessageReceiver.decrypt: Content returned was falsey!'
);
}
// Return just the content because that matches the signature of the other
// decrypt methods used above.
return this.unpad(content);
});
} else {
promise = Promise.reject(new Error('Unknown message type'));
}
return promise
.then(
(
plaintext:
| ArrayBuffer
| { isMe: boolean }
| { isBlocked: boolean }
| undefined
) => {
if (!plaintext || 'isMe' in plaintext || 'isBlocked' in plaintext) {
this.removeFromCache(envelope);
return null;
}
return plaintext;
}
)
.catch(async error => {
2021-02-18 16:40:26 +00:00
this.removeFromCache(envelope);
const uuid = envelope.sourceUuid;
const deviceId = envelope.sourceDevice;
// We don't do a light session reset if it's just a duplicated message
if (
error?.message?.includes &&
error.message.includes('message with old counter')
) {
2021-02-18 16:40:26 +00:00
throw error;
2018-05-02 16:51:22 +00:00
}
// We don't do a light session reset if it's an error with the sealed sender
// wrapper, since we don't trust the sender information.
if (
error?.message?.includes &&
error.message.includes('trust root validation failed')
) {
throw error;
}
2021-02-18 16:40:26 +00:00
if (uuid && deviceId) {
2021-05-28 19:11:19 +00:00
const event = new Event('decryption-error');
event.decryptionError = {
cipherTextBytes: envelope.usmc
? typedArrayToArrayBuffer(envelope.usmc.contents())
: undefined,
cipherTextType: envelope.usmc ? envelope.usmc.msgType() : undefined,
contentHint: envelope.contentHint,
groupId: envelope.groupId,
receivedAtCounter: envelope.receivedAtCounter,
receivedAtDate: envelope.receivedAtDate,
senderDevice: deviceId,
senderUuid: uuid,
timestamp: envelope.timestamp.toNumber(),
};
// Avoid deadlocks by scheduling processing on decrypted queue
this.addToQueue(
() => this.dispatchAndWait(event),
TaskType.Decrypted
);
2021-02-18 16:40:26 +00:00
} else {
const envelopeId = this.getEnvelopeId(envelope);
window.log.error(
`MessageReceiver.decrypt: Envelope ${envelopeId} missing uuid or deviceId`
);
}
2021-02-18 16:40:26 +00:00
throw error;
});
}
async handleSentMessage(
envelope: EnvelopeClass,
sentContainer: SyncMessageClass.Sent
) {
2021-03-04 21:44:57 +00:00
window.log.info(
'MessageReceiver.handleSentMessage',
this.getEnvelopeId(envelope)
);
const {
destination,
destinationUuid,
timestamp,
message: msg,
expirationStartTimestamp,
unidentifiedStatus,
isRecipientUpdate,
} = sentContainer;
if (!msg) {
throw new Error('MessageReceiver.handleSentMessage: message was falsey!');
}
let p: Promise<any> = Promise.resolve();
// eslint-disable-next-line no-bitwise
if (
msg.flags &&
msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION
) {
const identifier = destination || destinationUuid;
if (!identifier) {
throw new Error(
'MessageReceiver.handleSentMessage: Cannot end session with falsey destination'
);
}
p = this.handleEndSession(identifier);
}
return p.then(async () =>
this.processDecrypted(envelope, msg).then(message => {
2020-09-09 02:25:05 +00:00
// prettier-ignore
const groupId = this.getGroupId(message);
const isBlocked = this.isGroupBlocked(groupId);
const { source, sourceUuid } = envelope;
const ourE164 = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const isMe =
(source && ourE164 && source === ourE164) ||
(sourceUuid && ourUuid && sourceUuid === ourUuid);
const isLeavingGroup = Boolean(
2020-09-09 02:25:05 +00:00
!message.groupV2 &&
message.group &&
message.group.type ===
window.textsecure.protobuf.GroupContext.Type.QUIT
);
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
window.log.warn(
`Message ${this.getEnvelopeId(
envelope
)} ignored; destined for blocked group`
);
this.removeFromCache(envelope);
return undefined;
}
2018-05-02 16:51:22 +00:00
const ev = new Event('sent');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
destination,
destinationUuid,
2018-05-02 16:51:22 +00:00
timestamp: timestamp.toNumber(),
2020-04-29 21:24:12 +00:00
serverTimestamp: envelope.serverTimestamp,
2018-05-02 16:51:22 +00:00
device: envelope.sourceDevice,
unidentifiedStatus,
2018-05-02 16:51:22 +00:00
message,
isRecipientUpdate,
2021-03-04 21:44:57 +00:00
receivedAtCounter: envelope.receivedAtCounter,
receivedAtDate: envelope.receivedAtDate,
2018-05-02 16:51:22 +00:00
};
if (expirationStartTimestamp) {
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
}
return this.dispatchAndWait(ev);
})
);
}
async handleDataMessage(
envelope: EnvelopeClass,
msg: DataMessageClass
): Promise<void> {
2021-03-04 21:44:57 +00:00
window.log.info(
'MessageReceiver.handleDataMessage',
this.getEnvelopeId(envelope)
);
let p: Promise<any> = Promise.resolve();
// eslint-disable-next-line no-bitwise
const destination = envelope.sourceUuid || envelope.source;
if (!destination) {
throw new Error(
'MessageReceiver.handleDataMessage: source and sourceUuid were falsey'
);
}
2020-11-20 17:30:45 +00:00
if (this.isInvalidGroupData(msg, envelope)) {
this.removeFromCache(envelope);
return undefined;
}
await this.deriveGroupV1Data(msg);
2020-11-20 17:30:45 +00:00
this.deriveGroupV2Data(msg);
2020-09-09 02:25:05 +00:00
if (
msg.flags &&
msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION
) {
p = this.handleEndSession(destination);
}
2020-05-27 21:37:06 +00:00
if (
msg.flags &&
msg.flags &
window.textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE
) {
const ev = new Event('profileKeyUpdate');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
source: envelope.source,
sourceUuid: envelope.sourceUuid,
profileKey: msg.profileKey.toString('base64'),
};
return this.dispatchAndWait(ev);
}
return p.then(async () =>
this.processDecrypted(envelope, msg).then(message => {
2020-09-09 02:25:05 +00:00
// prettier-ignore
const groupId = this.getGroupId(message);
const isBlocked = this.isGroupBlocked(groupId);
const { source, sourceUuid } = envelope;
const ourE164 = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const isMe =
(source && ourE164 && source === ourE164) ||
(sourceUuid && ourUuid && sourceUuid === ourUuid);
const isLeavingGroup = Boolean(
2020-09-09 02:25:05 +00:00
!message.groupV2 &&
message.group &&
message.group.type ===
window.textsecure.protobuf.GroupContext.Type.QUIT
);
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
window.log.warn(
`Message ${this.getEnvelopeId(
envelope
)} ignored; destined for blocked group`
);
this.removeFromCache(envelope);
return undefined;
}
2018-05-02 16:51:22 +00:00
const ev = new Event('message');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
source: envelope.source,
sourceUuid: envelope.sourceUuid,
2018-05-02 16:51:22 +00:00
sourceDevice: envelope.sourceDevice,
timestamp: envelope.timestamp.toNumber(),
serverGuid: envelope.serverGuid,
2020-04-29 21:24:12 +00:00
serverTimestamp: envelope.serverTimestamp,
unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived,
2018-05-02 16:51:22 +00:00
message,
2021-03-04 21:44:57 +00:00
receivedAtCounter: envelope.receivedAtCounter,
receivedAtDate: envelope.receivedAtDate,
2018-05-02 16:51:22 +00:00
};
return this.dispatchAndWait(ev);
})
);
}
async decryptLegacyMessage(
stores: LockedStores,
envelope: EnvelopeClass
): Promise<ArrayBuffer | undefined> {
2021-03-04 21:44:57 +00:00
window.log.info(
'MessageReceiver.decryptLegacyMessage',
2021-03-04 21:44:57 +00:00
this.getEnvelopeId(envelope)
);
const plaintext = await this.decrypt(
stores,
envelope,
envelope.legacyMessage
);
if (!plaintext) {
window.log.warn('decryptLegacyMessage: plaintext was falsey');
return undefined;
}
return plaintext;
}
async innerHandleLegacyMessage(
envelope: EnvelopeClass,
plaintext: ArrayBuffer
) {
const message = window.textsecure.protobuf.DataMessage.decode(plaintext);
return this.handleDataMessage(envelope, message);
}
async decryptContentMessage(
stores: LockedStores,
envelope: EnvelopeClass
): Promise<ArrayBuffer | undefined> {
2021-03-04 21:44:57 +00:00
window.log.info(
'MessageReceiver.decryptContentMessage',
2021-03-04 21:44:57 +00:00
this.getEnvelopeId(envelope)
);
const plaintext = await this.decrypt(stores, envelope, envelope.content);
if (!plaintext) {
window.log.warn('decryptContentMessage: plaintext was falsey');
return undefined;
}
return plaintext;
}
async innerHandleContentMessage(
envelope: EnvelopeClass,
plaintext: ArrayBuffer
): Promise<void> {
const content = window.textsecure.protobuf.Content.decode(plaintext);
// Note: a distribution message can be tacked on to any other message, so we
// make sure to process it first. If that fails, we still try to process
// the rest of the message.
try {
2021-05-28 19:11:19 +00:00
if (
content.senderKeyDistributionMessage &&
!isByteBufferEmpty(content.senderKeyDistributionMessage)
) {
await this.handleSenderKeyDistributionMessage(
envelope,
content.senderKeyDistributionMessage
);
}
} catch (error) {
const errorString = error && error.stack ? error.stack : error;
window.log.error(
`innerHandleContentMessage: Failed to process sender key distribution message: ${errorString}`
);
}
2021-05-28 19:11:19 +00:00
if (
content.decryptionErrorMessage &&
!isByteBufferEmpty(content.decryptionErrorMessage)
) {
await this.handleDecryptionError(
envelope,
content.decryptionErrorMessage
);
return;
}
if (content.syncMessage) {
await this.handleSyncMessage(envelope, content.syncMessage);
return;
}
if (content.dataMessage) {
await this.handleDataMessage(envelope, content.dataMessage);
return;
}
if (content.nullMessage) {
await this.handleNullMessage(envelope);
return;
}
if (content.callingMessage) {
await this.handleCallingMessage(envelope, content.callingMessage);
return;
}
if (content.receiptMessage) {
await this.handleReceiptMessage(envelope, content.receiptMessage);
return;
}
if (content.typingMessage) {
await this.handleTypingMessage(envelope, content.typingMessage);
return;
}
this.removeFromCache(envelope);
if (isByteBufferEmpty(content.senderKeyDistributionMessage)) {
throw new Error('Unsupported content message');
}
}
2021-05-28 19:11:19 +00:00
async handleDecryptionError(
envelope: EnvelopeClass,
decryptionError: ByteBufferClass
) {
const logId = this.getEnvelopeId(envelope);
window.log.info(`handleDecryptionError: ${logId}`);
2021-05-28 19:11:19 +00:00
const buffer = Buffer.from(decryptionError.toArrayBuffer());
const request = DecryptionErrorMessage.deserialize(buffer);
this.removeFromCache(envelope);
const { sourceUuid, sourceDevice } = envelope;
if (!sourceUuid || !sourceDevice) {
window.log.error(
`handleDecryptionError/${logId}: Missing uuid or device!`
);
2021-05-28 19:11:19 +00:00
return;
}
const event = new Event('retry-request');
event.retryRequest = {
2021-06-08 21:51:58 +00:00
groupId: envelope.groupId,
2021-05-28 19:11:19 +00:00
requesterDevice: sourceDevice,
2021-06-08 21:51:58 +00:00
requesterUuid: sourceUuid,
2021-05-28 19:11:19 +00:00
senderDevice: request.deviceId(),
2021-06-08 21:51:58 +00:00
sentAt: request.timestamp(),
2021-05-28 19:11:19 +00:00
};
await this.dispatchAndWait(event);
}
async handleSenderKeyDistributionMessage(
envelope: EnvelopeClass,
distributionMessage: ByteBufferClass
): Promise<void> {
const envelopeId = this.getEnvelopeId(envelope);
window.log.info(`handleSenderKeyDistributionMessage/${envelopeId}`);
// Note: we don't call removeFromCache here because this message can be combined
// with a dataMessage, for example. That processing will dictate cache removal.
const identifier = envelope.sourceUuid || envelope.source;
const { sourceDevice } = envelope;
if (!identifier) {
throw new Error(
`handleSenderKeyDistributionMessage: No identifier for envelope ${envelopeId}`
);
}
if (!isNumber(sourceDevice)) {
throw new Error(
`handleSenderKeyDistributionMessage: Missing sourceDevice for envelope ${envelopeId}`
);
}
const sender = ProtocolAddress.new(identifier, sourceDevice);
const senderKeyDistributionMessage = SenderKeyDistributionMessage.deserialize(
Buffer.from(distributionMessage.toArrayBuffer())
);
const senderKeyStore = new SenderKeys();
const address = `${identifier}.${sourceDevice}`;
await window.textsecure.storage.protocol.enqueueSenderKeyJob(address, () =>
processSenderKeyDistributionMessage(
sender,
senderKeyDistributionMessage,
senderKeyStore
)
);
}
2020-06-04 18:16:19 +00:00
async handleCallingMessage(
envelope: EnvelopeClass,
callingMessage: CallingMessageClass
): Promise<void> {
this.removeFromCache(envelope);
2020-06-04 18:16:19 +00:00
await window.Signal.Services.calling.handleCallingMessage(
envelope,
callingMessage
);
}
async handleReceiptMessage(
envelope: EnvelopeClass,
receiptMessage: ReceiptMessageClass
): Promise<void> {
const results = [];
2018-05-02 16:51:22 +00:00
if (
receiptMessage.type ===
window.textsecure.protobuf.ReceiptMessage.Type.DELIVERY
2018-05-02 16:51:22 +00:00
) {
for (let i = 0; i < receiptMessage.timestamp.length; i += 1) {
const ev = new Event('delivery');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.deliveryReceipt = {
timestamp: receiptMessage.timestamp[i].toNumber(),
envelopeTimestamp: envelope.timestamp.toNumber(),
source: envelope.source,
sourceUuid: envelope.sourceUuid,
sourceDevice: envelope.sourceDevice,
};
results.push(this.dispatchAndWait(ev));
}
2018-05-02 16:51:22 +00:00
} else if (
receiptMessage.type ===
window.textsecure.protobuf.ReceiptMessage.Type.READ
2018-05-02 16:51:22 +00:00
) {
for (let i = 0; i < receiptMessage.timestamp.length; i += 1) {
const ev = new Event('read');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.timestamp = envelope.timestamp.toNumber();
ev.read = {
timestamp: receiptMessage.timestamp[i].toNumber(),
envelopeTimestamp: envelope.timestamp.toNumber(),
source: envelope.source,
sourceUuid: envelope.sourceUuid,
};
results.push(this.dispatchAndWait(ev));
}
}
await Promise.all(results);
}
2020-11-20 17:30:45 +00:00
async handleTypingMessage(
envelope: EnvelopeClass,
typingMessage: TypingMessageClass
): Promise<void> {
2018-11-14 19:10:32 +00:00
const ev = new Event('typing');
this.removeFromCache(envelope);
if (envelope.timestamp && typingMessage.timestamp) {
const envelopeTimestamp = envelope.timestamp.toNumber();
const typingTimestamp = typingMessage.timestamp.toNumber();
if (typingTimestamp !== envelopeTimestamp) {
window.log.warn(
`Typing message envelope timestamp (${envelopeTimestamp}) did not match typing timestamp (${typingTimestamp})`
);
return;
2018-11-14 19:10:32 +00:00
}
}
2020-09-09 02:25:05 +00:00
const { groupId, timestamp, action } = typingMessage;
2018-11-14 19:10:32 +00:00
ev.sender = envelope.source;
ev.senderUuid = envelope.sourceUuid;
2018-11-14 19:10:32 +00:00
ev.senderDevice = envelope.sourceDevice;
2020-09-10 20:06:26 +00:00
2018-11-14 19:10:32 +00:00
ev.typing = {
typingMessage,
2020-09-09 02:25:05 +00:00
timestamp: timestamp ? timestamp.toNumber() : Date.now(),
2018-11-14 19:10:32 +00:00
started:
2020-09-09 02:25:05 +00:00
action === window.textsecure.protobuf.TypingMessage.Action.STARTED,
2018-11-14 19:10:32 +00:00
stopped:
2020-09-09 02:25:05 +00:00
action === window.textsecure.protobuf.TypingMessage.Action.STOPPED,
2018-11-14 19:10:32 +00:00
};
2020-11-20 17:30:45 +00:00
const groupIdBuffer = groupId ? groupId.toArrayBuffer() : null;
if (groupIdBuffer && groupIdBuffer.byteLength > 0) {
if (groupIdBuffer.byteLength === GROUPV1_ID_LENGTH) {
ev.typing.groupId = groupId.toString('binary');
ev.typing.groupV2Id = await this.deriveGroupV2FromV1(groupIdBuffer);
} else if (groupIdBuffer.byteLength === GROUPV2_ID_LENGTH) {
ev.typing.groupV2Id = groupId.toString('base64');
} else {
window.log.error('handleTypingMessage: Received invalid groupId value');
this.removeFromCache(envelope);
}
}
await this.dispatchEvent(ev);
}
handleNullMessage(envelope: EnvelopeClass): void {
2021-03-04 21:44:57 +00:00
window.log.info(
'MessageReceiver.handleNullMessage',
this.getEnvelopeId(envelope)
);
this.removeFromCache(envelope);
}
2020-11-20 17:30:45 +00:00
isInvalidGroupData(
message: DataMessageClass,
envelope: EnvelopeClass
): boolean {
const { group, groupV2 } = message;
if (group) {
const id = group.id.toArrayBuffer();
const isInvalid = id.byteLength !== GROUPV1_ID_LENGTH;
if (isInvalid) {
window.log.info(
'isInvalidGroupData: invalid GroupV1 message from',
this.getEnvelopeId(envelope)
);
}
return isInvalid;
}
if (groupV2) {
const masterKey = groupV2.masterKey.toArrayBuffer();
const isInvalid = masterKey.byteLength !== MASTER_KEY_LENGTH;
if (isInvalid) {
window.log.info(
'isInvalidGroupData: invalid GroupV2 message from',
this.getEnvelopeId(envelope)
);
}
return isInvalid;
}
return false;
}
async deriveGroupV2FromV1(groupId: ArrayBuffer): Promise<string> {
if (groupId.byteLength !== GROUPV1_ID_LENGTH) {
throw new Error(
`deriveGroupV2FromV1: had id with wrong byteLength: ${groupId.byteLength}`
);
}
const masterKey = await deriveMasterKeyFromGroupV1(groupId);
const data = deriveGroupFields(masterKey);
const toBase64 = MessageReceiverInner.arrayBufferToStringBase64;
return toBase64(data.id);
}
async deriveGroupV1Data(message: DataMessageClass) {
const { group } = message;
if (!group) {
return;
}
if (!group.id) {
throw new Error('deriveGroupV1Data: had falsey id');
}
const id = group.id.toArrayBuffer();
if (id.byteLength !== GROUPV1_ID_LENGTH) {
throw new Error(
`deriveGroupV1Data: had id with wrong byteLength: ${id.byteLength}`
);
}
group.derivedGroupV2Id = await this.deriveGroupV2FromV1(id);
}
deriveGroupV2Data(message: DataMessageClass) {
2020-09-09 02:25:05 +00:00
const { groupV2 } = message;
if (!groupV2) {
return;
}
if (!isNumber(groupV2.revision)) {
2020-11-20 17:30:45 +00:00
throw new Error('deriveGroupV2Data: revision was not a number');
2020-09-09 02:25:05 +00:00
}
if (!groupV2.masterKey) {
2020-11-20 17:30:45 +00:00
throw new Error('deriveGroupV2Data: had falsey masterKey');
2020-09-09 02:25:05 +00:00
}
const toBase64 = MessageReceiverInner.arrayBufferToStringBase64;
const masterKey: ArrayBuffer = groupV2.masterKey.toArrayBuffer();
const length = masterKey.byteLength;
if (length !== MASTER_KEY_LENGTH) {
throw new Error(
2020-11-20 17:30:45 +00:00
`deriveGroupV2Data: masterKey had length ${length}, expected ${MASTER_KEY_LENGTH}`
2020-09-09 02:25:05 +00:00
);
}
const fields = deriveGroupFields(masterKey);
groupV2.masterKey = toBase64(masterKey);
groupV2.secretParams = toBase64(fields.secretParams);
groupV2.publicParams = toBase64(fields.publicParams);
groupV2.id = toBase64(fields.id);
if (groupV2.groupChange) {
groupV2.groupChange = groupV2.groupChange.toString('base64');
}
}
2020-09-09 02:25:05 +00:00
getGroupId(message: DataMessageClass) {
if (message.groupV2) {
return message.groupV2.id;
}
if (message.group) {
return message.group.id.toString('binary');
}
return null;
}
getDestination(sentMessage: SyncMessageClass.Sent) {
if (sentMessage.message && sentMessage.message.groupV2) {
return `groupv2(${sentMessage.message.groupV2.id})`;
}
if (sentMessage.message && sentMessage.message.group) {
2020-09-09 02:25:05 +00:00
return `group(${sentMessage.message.group.id.toBinary()})`;
}
return sentMessage.destination || sentMessage.destinationUuid;
2020-09-09 02:25:05 +00:00
}
async handleSyncMessage(
envelope: EnvelopeClass,
syncMessage: SyncMessageClass
): Promise<void> {
const unidentified = syncMessage.sent
? syncMessage.sent.unidentifiedStatus || []
: [];
window.normalizeUuids(
syncMessage,
[
'sent.destinationUuid',
...unidentified.map(
(_el, i) => `sent.unidentifiedStatus.${i}.destinationUuid`
),
],
'message_receiver::handleSyncMessage'
);
const fromSelfSource =
envelope.source && envelope.source === this.number_id;
const fromSelfSourceUuid =
envelope.sourceUuid && envelope.sourceUuid === this.uuid_id;
if (!fromSelfSource && !fromSelfSourceUuid) {
throw new Error('Received sync message from another number');
}
// eslint-disable-next-line eqeqeq
if (envelope.sourceDevice == this.deviceId) {
throw new Error('Received sync message from our own device');
}
if (syncMessage.sent) {
const sentMessage = syncMessage.sent;
if (!sentMessage || !sentMessage.message) {
throw new Error(
'MessageReceiver.handleSyncMessage: sync sent message was missing message'
);
}
2020-09-09 02:25:05 +00:00
2020-11-20 17:30:45 +00:00
if (this.isInvalidGroupData(sentMessage.message, envelope)) {
this.removeFromCache(envelope);
return undefined;
}
await this.deriveGroupV1Data(sentMessage.message);
2020-11-20 17:30:45 +00:00
this.deriveGroupV2Data(sentMessage.message);
window.log.info(
'sent message to',
2020-09-09 02:25:05 +00:00
this.getDestination(sentMessage),
sentMessage.timestamp.toNumber(),
'from',
this.getEnvelopeId(envelope)
);
return this.handleSentMessage(envelope, sentMessage);
}
if (syncMessage.contacts) {
this.handleContacts(envelope, syncMessage.contacts);
return undefined;
}
if (syncMessage.groups) {
this.handleGroups(envelope, syncMessage.groups);
return undefined;
}
if (syncMessage.blocked) {
return this.handleBlocked(envelope, syncMessage.blocked);
}
if (syncMessage.request) {
window.log.info('Got SyncMessage Request');
this.removeFromCache(envelope);
return undefined;
}
if (syncMessage.read && syncMessage.read.length) {
return this.handleRead(envelope, syncMessage.read);
}
if (syncMessage.verified) {
return this.handleVerified(envelope, syncMessage.verified);
}
if (syncMessage.configuration) {
return this.handleConfiguration(envelope, syncMessage.configuration);
}
if (
2019-06-26 19:33:13 +00:00
syncMessage.stickerPackOperation &&
syncMessage.stickerPackOperation.length > 0
) {
return this.handleStickerPackOperation(
envelope,
syncMessage.stickerPackOperation
);
}
if (syncMessage.viewOnceOpen) {
2019-08-05 20:53:15 +00:00
return this.handleViewOnceOpen(envelope, syncMessage.viewOnceOpen);
}
if (syncMessage.messageRequestResponse) {
2020-05-27 21:37:06 +00:00
return this.handleMessageRequestResponse(
envelope,
syncMessage.messageRequestResponse
);
}
if (syncMessage.fetchLatest) {
return this.handleFetchLatest(envelope, syncMessage.fetchLatest);
}
if (syncMessage.keys) {
return this.handleKeys(envelope, syncMessage.keys);
}
this.removeFromCache(envelope);
window.log.warn(
`handleSyncMessage/${this.getEnvelopeId(envelope)}: Got empty SyncMessage`
);
return Promise.resolve();
}
async handleConfiguration(
envelope: EnvelopeClass,
configuration: SyncMessageClass.Configuration
) {
window.log.info('got configuration sync message');
const ev = new Event('configuration');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.configuration = configuration;
return this.dispatchAndWait(ev);
}
async handleViewOnceOpen(
envelope: EnvelopeClass,
sync: SyncMessageClass.ViewOnceOpen
) {
2019-08-05 20:53:15 +00:00
window.log.info('got view once open sync message');
2019-06-26 19:33:13 +00:00
const ev = new Event('viewSync');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.source = sync.sender;
ev.sourceUuid = sync.senderUuid;
2019-06-26 19:33:13 +00:00
ev.timestamp = sync.timestamp ? sync.timestamp.toNumber() : null;
window.normalizeUuids(
ev,
['sourceUuid'],
'message_receiver::handleViewOnceOpen'
);
2019-06-26 19:33:13 +00:00
return this.dispatchAndWait(ev);
}
2020-05-27 21:37:06 +00:00
async handleMessageRequestResponse(
envelope: EnvelopeClass,
sync: SyncMessageClass.MessageRequestResponse
) {
window.log.info('got message request response sync message');
const ev = new Event('messageRequestResponse');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.threadE164 = sync.threadE164;
ev.threadUuid = sync.threadUuid;
ev.messageRequestResponseType = sync.type;
2020-11-20 17:30:45 +00:00
const idBuffer: ArrayBuffer = sync.groupId
? sync.groupId.toArrayBuffer()
: null;
if (idBuffer && idBuffer.byteLength > 0) {
if (idBuffer.byteLength === GROUPV1_ID_LENGTH) {
ev.groupId = sync.groupId.toString('binary');
ev.groupV2Id = await this.deriveGroupV2FromV1(idBuffer);
} else if (idBuffer.byteLength === GROUPV2_ID_LENGTH) {
ev.groupV2Id = sync.groupId.toString('base64');
} else {
this.removeFromCache(envelope);
window.log.error('Received message request with invalid groupId');
return undefined;
}
}
2020-05-27 21:37:06 +00:00
window.normalizeUuids(
ev,
['threadUuid'],
'MessageReceiver::handleMessageRequestResponse'
);
2020-11-20 17:30:45 +00:00
return this.dispatchAndWait(ev);
}
async handleFetchLatest(
envelope: EnvelopeClass,
sync: SyncMessageClass.FetchLatest
) {
window.log.info('got fetch latest sync message');
const ev = new Event('fetchLatest');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.eventType = sync.type;
return this.dispatchAndWait(ev);
}
async handleKeys(envelope: EnvelopeClass, sync: SyncMessageClass.Keys) {
window.log.info('got keys sync message');
if (!sync.storageService) {
return undefined;
}
const ev = new Event('keys');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.storageServiceKey = sync.storageService.toArrayBuffer();
2020-05-27 21:37:06 +00:00
return this.dispatchAndWait(ev);
}
async handleStickerPackOperation(
envelope: EnvelopeClass,
operations: Array<SyncMessageClass.StickerPackOperation>
) {
const ENUM =
window.textsecure.protobuf.SyncMessage.StickerPackOperation.Type;
window.log.info('got sticker pack operation sync message');
const ev = new Event('sticker-pack');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.stickerPacks = operations.map(operation => ({
id: operation.packId ? operation.packId.toString('hex') : null,
key: operation.packKey ? operation.packKey.toString('base64') : null,
isInstall: operation.type === ENUM.INSTALL,
isRemove: operation.type === ENUM.REMOVE,
}));
return this.dispatchAndWait(ev);
}
async handleVerified(envelope: EnvelopeClass, verified: VerifiedClass) {
const ev = new Event('verified');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.verified = {
state: verified.state,
destination: verified.destination,
destinationUuid: verified.destinationUuid,
identityKey: verified.identityKey.toArrayBuffer(),
};
window.normalizeUuids(
ev,
['verified.destinationUuid'],
'message_receiver::handleVerified'
);
return this.dispatchAndWait(ev);
}
async handleRead(
envelope: EnvelopeClass,
read: Array<SyncMessageClass.Read>
): Promise<void> {
2021-03-04 21:44:57 +00:00
window.log.info('MessageReceiver.handleRead', this.getEnvelopeId(envelope));
const results = [];
for (let i = 0; i < read.length; i += 1) {
const ev = new Event('readSync');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.timestamp = envelope.timestamp.toNumber();
ev.read = {
envelopeTimestamp: envelope.timestamp.toNumber(),
timestamp: read[i].timestamp.toNumber(),
sender: read[i].sender,
senderUuid: read[i].senderUuid,
};
window.normalizeUuids(
ev,
['read.senderUuid'],
'message_receiver::handleRead'
);
results.push(this.dispatchAndWait(ev));
}
await Promise.all(results);
}
handleContacts(envelope: EnvelopeClass, contacts: SyncMessageClass.Contacts) {
window.log.info('contact sync');
const { blob } = contacts;
if (!blob) {
throw new Error('MessageReceiver.handleContacts: blob field was missing');
}
this.removeFromCache(envelope);
// Note: we do not return here because we don't want to block the next message on
// this attachment download and a lot of processing of that attachment.
this.handleAttachment(blob).then(async attachmentPointer => {
const results = [];
const contactBuffer = new ContactBuffer(attachmentPointer.data);
let contactDetails = contactBuffer.next();
while (contactDetails !== undefined) {
const contactEvent = new Event('contact');
contactEvent.contactDetails = contactDetails;
2020-06-12 22:36:32 +00:00
window.normalizeUuids(
contactEvent,
['contactDetails.verified.destinationUuid'],
'message_receiver::handleContacts::handleAttachment'
);
results.push(this.dispatchAndWait(contactEvent));
contactDetails = contactBuffer.next();
}
const finalEvent = new Event('contactsync');
results.push(this.dispatchAndWait(finalEvent));
return Promise.all(results).then(() => {
window.log.info('handleContacts: finished');
});
});
}
handleGroups(envelope: EnvelopeClass, groups: SyncMessageClass.Groups) {
window.log.info('group sync');
const { blob } = groups;
this.removeFromCache(envelope);
if (!blob) {
throw new Error('MessageReceiver.handleGroups: blob field was missing');
}
// Note: we do not return here because we don't want to block the next message on
// this attachment download and a lot of processing of that attachment.
this.handleAttachment(blob).then(async attachmentPointer => {
const groupBuffer = new GroupBuffer(attachmentPointer.data);
let groupDetails = groupBuffer.next() as any;
const promises = [];
while (groupDetails) {
groupDetails.id = groupDetails.id.toBinary();
const ev = new Event('group');
ev.groupDetails = groupDetails;
const promise = this.dispatchAndWait(ev).catch(e => {
window.log.error('error processing group', e);
});
groupDetails = groupBuffer.next();
promises.push(promise);
}
return Promise.all(promises).then(async () => {
const ev = new Event('groupsync');
Feature: Blue check marks for read messages if opted in (#1489) * Refactor delivery receipt event handler * Rename the delivery receipt event For less ambiguity with read receipts. * Rename synced read event For less ambiguity with read receipts from other Signal users. * Add support for incoming receipt messages Handle ReceiptMessages, which may include encrypted delivery receipts or read receipts from recipients of our sent messages. // FREEBIE * Rename ReadReceipts to ReadSyncs * Render read messages with blue double checks * Send read receipts to senders of incoming messages // FREEBIE * Move ReadSyncs to their own file // FREEBIE * Fixup old comments on read receipts (now read syncs) And some variable renaming for extra clarity. // FREEBIE * Add global setting for read receipts Don't send read receipt messages unless the setting is enabled. Don't process read receipts if the setting is disabled. // FREEBIE * Sync read receipt setting from mobile Toggling this setting on your mobile device should sync it to Desktop. When linking, use the setting in the provisioning message. // FREEBIE * Send receipt messages silently Avoid generating phantom messages on ios // FREEBIE * Save recipients on the outgoing message models For accurate tracking and display of sent/delivered/read state, even if group membership changes later. // FREEBIE * Fix conversation type in profile key update handling // FREEBIE * Set recipients on synced sent messages * Render saved recipients in message detail if available For older messages, where we did not save the intended set of recipients at the time of sending, fall back to the current group membership. // FREEBIE * Record who has been successfully sent to // FREEBIE * Record who a message has been delivered to * Invert the not-clickable class * Fix readReceipt setting sync when linking * Render per recipient sent/delivered/read status In the message detail view for outgoing messages, render each recipient's individual sent/delivered/read status with respect to this message, as long as there are no errors associated with the recipient (ie, safety number changes, user not registered, etc...) since the error icon is displayed in that case. *Messages sent before this change may not have per-recipient status lists and will simply show no status icon. // FREEBIE * Add configuration sync request Send these requests in a one-off fashion when: 1. We have just setup from a chrome app import 2. We have just upgraded to read-receipt support // FREEBIE * Expose sendRequestConfigurationSyncMessage // FREEBIE * Fix handling of incoming delivery receipts - union with array FREEBIE
2017-10-04 22:28:43 +00:00
return this.dispatchAndWait(ev);
});
});
}
async handleBlocked(
envelope: EnvelopeClass,
blocked: SyncMessageClass.Blocked
) {
window.log.info('Setting these numbers as blocked:', blocked.numbers);
await window.textsecure.storage.put('blocked', blocked.numbers);
if (blocked.uuids) {
window.normalizeUuids(
blocked,
blocked.uuids.map((_uuid: string, i: number) => `uuids.${i}`),
'message_receiver::handleBlocked'
);
window.log.info('Setting these uuids as blocked:', blocked.uuids);
await window.textsecure.storage.put('blocked-uuids', blocked.uuids);
}
const groupIds = map(blocked.groupIds, groupId => groupId.toBinary());
window.log.info(
'Setting these groups as blocked:',
groupIds.map(groupId => `group(${groupId})`)
);
await window.textsecure.storage.put('blocked-groups', groupIds);
this.removeFromCache(envelope);
}
isBlocked(number: string) {
return window.textsecure.storage.get('blocked', []).includes(number);
}
isUuidBlocked(uuid: string) {
return window.textsecure.storage.get('blocked-uuids', []).includes(uuid);
}
isGroupBlocked(groupId: string) {
return window.textsecure.storage
.get('blocked-groups', [])
.includes(groupId);
}
cleanAttachment(attachment: AttachmentPointerClass) {
return {
...omit(attachment, 'thumbnail'),
cdnId: attachment.cdnId?.toString(),
key: attachment.key ? attachment.key.toString('base64') : null,
digest: attachment.digest ? attachment.digest.toString('base64') : null,
};
}
private isLinkPreviewDateValid(value: unknown): value is number {
return (
typeof value === 'number' &&
!Number.isNaN(value) &&
Number.isFinite(value) &&
value > 0
);
}
private cleanLinkPreviewDate(value: unknown): number | null {
if (this.isLinkPreviewDateValid(value)) {
return value;
}
if (!value) {
return null;
}
let result: unknown;
try {
result = (value as any).toNumber();
} catch (err) {
return null;
}
return this.isLinkPreviewDateValid(result) ? result : null;
}
async downloadAttachment(
attachment: AttachmentPointerClass
): Promise<DownloadAttachmentType> {
const encrypted = await this.server.getAttachment(
attachment.cdnId || attachment.cdnKey,
attachment.cdnNumber || 0
);
const { key, digest, size } = attachment;
if (!digest) {
throw new Error('Failure: Ask sender to update Signal and resend.');
}
const paddedData = await Crypto.decryptAttachment(
encrypted,
MessageReceiverInner.stringToArrayBufferBase64(key),
MessageReceiverInner.stringToArrayBufferBase64(digest)
);
if (!isNumber(size)) {
throw new Error(
`downloadAttachment: Size was not provided, actual size was ${paddedData.byteLength}`
);
}
const data = window.Signal.Crypto.getFirstBytes(paddedData, size);
return {
...omit(attachment, 'digest', 'key'),
data,
};
}
async handleAttachment(
attachment: AttachmentPointerClass
): Promise<DownloadAttachmentType> {
const cleaned = this.cleanAttachment(attachment);
return this.downloadAttachment(cleaned);
}
async handleEndSession(identifier: string) {
window.log.info(`handleEndSession: closing sessions for ${identifier}`);
await window.textsecure.storage.protocol.archiveAllSessions(identifier);
}
async processDecrypted(envelope: EnvelopeClass, decrypted: DataMessageClass) {
/* eslint-disable no-bitwise, no-param-reassign */
const FLAGS = window.textsecure.protobuf.DataMessage.Flags;
// Now that its decrypted, validate the message and clean it up for consumer
// processing
// Note that messages may (generally) only perform one action and we ignore remaining
// fields after the first action.
if (!envelope.timestamp || !decrypted.timestamp) {
throw new Error('Missing timestamp on dataMessage or envelope');
}
const envelopeTimestamp = envelope.timestamp.toNumber();
const decryptedTimestamp = decrypted.timestamp.toNumber();
if (envelopeTimestamp !== decryptedTimestamp) {
throw new Error(
`Timestamp ${decrypted.timestamp} in DataMessage did not match envelope timestamp ${envelope.timestamp}`
);
}
if (decrypted.flags == null) {
decrypted.flags = 0;
}
if (decrypted.expireTimer == null) {
decrypted.expireTimer = 0;
}
if (decrypted.flags & FLAGS.END_SESSION) {
decrypted.body = null;
decrypted.attachments = [];
decrypted.group = null;
return Promise.resolve(decrypted);
}
if (decrypted.flags & FLAGS.EXPIRATION_TIMER_UPDATE) {
decrypted.body = null;
decrypted.attachments = [];
} else if (decrypted.flags & FLAGS.PROFILE_KEY_UPDATE) {
decrypted.body = null;
decrypted.attachments = [];
} else if (decrypted.flags !== 0) {
throw new Error('Unknown flags in message');
}
if (decrypted.group) {
decrypted.group.id = decrypted.group.id.toBinary();
switch (decrypted.group.type) {
case window.textsecure.protobuf.GroupContext.Type.UPDATE:
decrypted.body = null;
decrypted.attachments = [];
break;
case window.textsecure.protobuf.GroupContext.Type.QUIT:
decrypted.body = null;
decrypted.attachments = [];
break;
case window.textsecure.protobuf.GroupContext.Type.DELIVER:
decrypted.group.name = null;
decrypted.group.membersE164 = [];
decrypted.group.avatar = null;
break;
2020-03-20 19:01:15 +00:00
default: {
this.removeFromCache(envelope);
2020-03-20 19:01:15 +00:00
const err = new Error('Unknown group message type');
err.warn = true;
throw err;
}
}
}
const attachmentCount = (decrypted.attachments || []).length;
const ATTACHMENT_MAX = 32;
if (attachmentCount > ATTACHMENT_MAX) {
throw new Error(
`Too many attachments: ${attachmentCount} included in one message, max is ${ATTACHMENT_MAX}`
);
}
// Here we go from binary to string/base64 in all AttachmentPointer digest/key fields
if (
decrypted.group &&
decrypted.group.type ===
window.textsecure.protobuf.GroupContext.Type.UPDATE
) {
if (decrypted.group.avatar) {
decrypted.group.avatar = this.cleanAttachment(decrypted.group.avatar);
2019-01-16 03:03:56 +00:00
}
}
decrypted.attachments = (decrypted.attachments || []).map(
this.cleanAttachment.bind(this)
);
decrypted.preview = (decrypted.preview || []).map(item => ({
...item,
date: this.cleanLinkPreviewDate(item.date),
2020-09-04 18:27:22 +00:00
...(item.image ? { image: this.cleanAttachment(item.image) } : {}),
}));
decrypted.contact = (decrypted.contact || []).map(item => {
const { avatar } = item;
if (!avatar || !avatar.avatar) {
return item;
}
return {
...item,
avatar: {
...item.avatar,
avatar: this.cleanAttachment(item.avatar.avatar),
},
};
});
if (decrypted.quote && decrypted.quote.id) {
decrypted.quote.id = decrypted.quote.id.toNumber();
}
if (decrypted.quote) {
decrypted.quote.attachments = (decrypted.quote.attachments || []).map(
item => {
if (!item.thumbnail) {
return item;
}
return {
...item,
thumbnail: this.cleanAttachment(item.thumbnail),
};
}
);
}
const { sticker } = decrypted;
if (sticker) {
if (sticker.packId) {
sticker.packId = sticker.packId.toString('hex');
}
if (sticker.packKey) {
sticker.packKey = sticker.packKey.toString('base64');
}
if (sticker.data) {
sticker.data = this.cleanAttachment(sticker.data);
}
}
2020-04-29 21:24:12 +00:00
const { delete: del } = decrypted;
if (del) {
if (del.targetSentTimestamp) {
del.targetSentTimestamp = del.targetSentTimestamp.toNumber();
}
}
2020-09-09 02:25:05 +00:00
const { reaction } = decrypted;
if (reaction) {
if (reaction.targetTimestamp) {
reaction.targetTimestamp = reaction.targetTimestamp.toNumber();
}
}
return Promise.resolve(decrypted);
/* eslint-enable no-bitwise, no-param-reassign */
}
}
export default class MessageReceiver {
constructor(
oldUsername: string,
username: string,
password: string,
signalingKey: ArrayBuffer,
options: {
serverTrustRoot: string;
retryCached?: string;
}
) {
const inner = new MessageReceiverInner(
oldUsername,
username,
password,
signalingKey,
options
);
this.addEventListener = inner.addEventListener.bind(inner);
this.close = inner.close.bind(inner);
this.downloadAttachment = inner.downloadAttachment.bind(inner);
2020-09-09 02:25:05 +00:00
this.getStatus = inner.getStatus.bind(inner);
this.hasEmptied = inner.hasEmptied.bind(inner);
this.removeEventListener = inner.removeEventListener.bind(inner);
this.stopProcessing = inner.stopProcessing.bind(inner);
this.unregisterBatchers = inner.unregisterBatchers.bind(inner);
inner.connect();
this.getProcessedCount = () => inner.processedCount;
}
addEventListener: (name: string, handler: Function) => void;
close: () => Promise<void>;
downloadAttachment: (
attachment: AttachmentPointerClass
) => Promise<DownloadAttachmentType>;
2020-09-09 02:25:05 +00:00
getStatus: () => number;
2020-09-09 02:25:05 +00:00
hasEmptied: () => boolean;
2020-09-09 02:25:05 +00:00
removeEventListener: (name: string, handler: Function) => void;
stopProcessing: () => Promise<void>;
unregisterBatchers: () => void;
getProcessedCount: () => number;
static stringToArrayBuffer = MessageReceiverInner.stringToArrayBuffer;
static arrayBufferToString = MessageReceiverInner.arrayBufferToString;
static stringToArrayBufferBase64 =
MessageReceiverInner.stringToArrayBufferBase64;
static arrayBufferToStringBase64 =
MessageReceiverInner.arrayBufferToStringBase64;
}