signal-desktop/ts/textsecure/MessageReceiver.ts

2475 lines
72 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 */
import { isNumber, map, omit, noop } from 'lodash';
import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid';
import { SessionCipherClass, SignalProtocolAddressClass } from '../libsignal.d';
import { BatcherType, createBatcher } from '../util/batcher';
2021-02-18 16:40:26 +00:00
import { assert } from '../util/assert';
import EventTarget from './EventTarget';
import { WebAPIType } from './WebAPI';
import utils from './Helpers';
import WebSocketResource, {
IncomingWebSocketRequest,
} from './WebsocketResources';
import Crypto from './Crypto';
2020-11-20 17:30:45 +00:00
import { deriveMasterKeyFromGroupV1 } from '../Crypto';
import { ContactBuffer, GroupBuffer } from './ContactsParser';
import { IncomingIdentityKeyError } from './Errors';
2021-02-27 00:00:37 +00:00
import {
createCertificateValidator,
SecretSessionCipher,
} from '../metadata/SecretSessionCipher';
import {
AttachmentPointerClass,
2020-06-04 18:16:19 +00:00
CallingMessageClass,
DataMessageClass,
DownloadAttachmentType,
EnvelopeClass,
ReceiptMessageClass,
SyncMessageClass,
TypingMessageClass,
UnprocessedType,
VerifiedClass,
} from '../textsecure.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-02-18 16:40:26 +00:00
type SessionResetsType = Record<string, number>;
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;
}
// We want to extend `Error`, so we need an interface.
// eslint-disable-next-line no-restricted-syntax
interface Error {
reason?: any;
stackForLog?: string;
sender?: SignalProtocolAddressClass;
senderUuid?: SignalProtocolAddressClass;
}
}
type CacheAddItemType = {
envelope: EnvelopeClass;
data: UnprocessedType;
request: IncomingWebSocketRequest;
};
type CacheUpdateItemType = {
id: string;
data: Partial<UnprocessedType>;
};
class MessageReceiverInner extends EventTarget {
_onClose?: (ev: any) => Promise<void>;
appQueue: PQueue;
cacheAddBatcher: BatcherType<CacheAddItemType>;
cacheRemoveBatcher: BatcherType<string>;
cacheUpdateBatcher: BatcherType<CacheUpdateItemType>;
calledClose?: boolean;
count: number;
processedCount: number;
deviceId?: number;
hasConnected?: boolean;
incomingQueue: PQueue;
isEmptied?: boolean;
number_id?: string;
password: string;
pendingQueue: 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
? parseInt(utils.unencodeNumber(username || oldUsername)[1], 10)
: undefined;
this.incomingQueue = new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 });
this.pendingQueue = new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 });
this.appQueue = new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 });
this.cacheAddBatcher = createBatcher<CacheAddItemType>({
2021-03-26 00:00:03 +00:00
name: 'MessageReceiver.cacheAddBatcher',
wait: 200,
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.
this.cacheAndQueueBatch(items);
},
});
this.cacheUpdateBatcher = createBatcher<CacheUpdateItemType>({
2021-03-26 00:00:03 +00:00
name: 'MessageReceiver.cacheUpdateBatcher',
wait: 500,
maxSize: 30,
processBatch: this.cacheUpdateBatch.bind(this),
});
this.cacheRemoveBatcher = createBatcher<string>({
2021-03-26 00:00:03 +00:00
name: 'MessageReceiver.cacheRemoveBatcher',
wait: 500,
maxSize: 30,
processBatch: this.cacheRemoveBatch.bind(this),
});
2021-02-18 16:40:26 +00:00
this.cleanupSessionResets();
}
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.pendingQueue.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');
this.cacheAddBatcher.unregister();
this.cacheUpdateBatcher.unregister();
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');
}
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
);
this.cacheAndQueue(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(task: () => Promise<void>) {
this.count += 1;
2019-07-09 18:46:48 +00:00
const promise = this.pendingQueue.add(task);
const { count } = this;
2019-07-09 18:46:48 +00:00
const update = () => {
this.updateProgress(count);
};
2019-07-09 18:46:48 +00:00
promise.then(update, update);
2019-07-09 18:46:48 +00:00
return promise;
}
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([
this.cacheAddBatcher.flushAndWait(),
this.cacheUpdateBatcher.flushAndWait(),
this.cacheRemoveBatcher.flushAndWait(),
]);
window.log.info("MessageReceiver: emitting 'empty' event");
const ev = new Event('empty');
this.dispatchEvent(ev);
this.isEmptied = true;
this.maybeScheduleRetryTimeout();
};
const waitForPendingQueue = 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);
};
2019-07-09 18:46:48 +00:00
const waitForIncomingQueue = () => {
this.addToQueue(waitForPendingQueue);
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 () => {
await this.cacheAddBatcher.onIdle();
this.incomingQueue.add(waitForIncomingQueue);
};
waitForCacheAddBatcher();
}
async drain() {
const waitForIncomingQueue = async () =>
this.addToQueue(async () => {
window.log.info('drained');
2018-05-02 16:51:22 +00:00
});
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
);
} 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 =
envelope.serverTimestamp || item.serverTimestamp;
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!');
}
this.queueDecryptedEnvelope(envelope, payloadPlaintext);
} else {
this.queueEnvelope(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.pendingQueue.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;
})
);
}
async cacheAndQueueBatch(items: Array<CacheAddItemType>) {
window.log.info('MessageReceiver.cacheAndQueueBatch', items.length);
2019-09-26 19:56:31 +00:00
const dataArray = items.map(item => item.data);
try {
await window.textsecure.storage.unprocessed.batchAdd(dataArray);
2019-09-26 19:56:31 +00:00
items.forEach(item => {
try {
item.request.respond(200, 'OK');
} catch (error) {
window.log.error(
'cacheAndQueueBatch: Failed to send 200 to server; still queuing envelope'
);
}
this.queueEnvelope(item.envelope);
2019-09-26 19:56:31 +00:00
});
this.maybeScheduleRetryTimeout();
} catch (error) {
window.log.error(
'cacheAndQueue 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');
});
2019-09-26 19:56:31 +00:00
}
}
cacheAndQueue(
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,
};
2019-09-26 19:56:31 +00:00
this.cacheAddBatcher.add({
request,
envelope,
data,
});
}
async cacheUpdateBatch(items: Array<Partial<UnprocessedType>>) {
2021-03-04 21:44:57 +00:00
window.log.info('MessageReceiver.cacheUpdateBatch', items.length);
await window.textsecure.storage.unprocessed.addDecryptedDataToList(items);
}
updateCache(envelope: EnvelopeClass, plaintext: ArrayBuffer) {
const { id } = envelope;
2019-09-26 19:56:31 +00:00
const data = {
source: envelope.source,
sourceUuid: envelope.sourceUuid,
2019-09-26 19:56:31 +00:00
sourceDevice: envelope.sourceDevice,
serverTimestamp: envelope.serverTimestamp,
decrypted: MessageReceiverInner.arrayBufferToStringBase64(plaintext),
2019-09-26 19:56:31 +00:00
};
this.cacheUpdateBatcher.add({ id, 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,
`queueEncryptedEnvelope ${id}`
);
const promise = this.addToQueue(taskWithTimeout);
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 queueEnvelope(envelope: EnvelopeClass) {
const id = this.getEnvelopeId(envelope);
window.log.info('queueing envelope', id);
2021-03-04 21:44:57 +00:00
const task = this.handleEnvelope.bind(this, envelope);
const taskWithTimeout = window.textsecure.createTaskWithTimeout(
task,
`queueEnvelope ${id}`
);
const promise = this.addToQueue(taskWithTimeout);
2021-03-04 21:44:57 +00:00
return promise.catch(error => {
2020-03-20 19:01:15 +00:00
const args = [
'queueEnvelope 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);
}
});
}
// Same as handleEnvelope, just without the decryption step. Necessary for handling
// messages which were successfully decrypted, but application logic didn't finish
// processing.
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 handleEnvelope(envelope: EnvelopeClass) {
if (this.stoppingProcessing) {
return Promise.resolve();
}
if (envelope.type === window.textsecure.protobuf.Envelope.Type.RECEIPT) {
return this.onDeliveryReceipt(envelope);
}
if (envelope.content) {
return this.handleContentMessage(envelope);
}
if (envelope.legacyMessage) {
return this.handleLegacyMessage(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) {
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(
envelope: EnvelopeClass,
ciphertext: any
): Promise<ArrayBuffer> {
const { serverTrustRoot } = this;
let promise;
const identifier = envelope.sourceUuid || envelope.source;
const address = new window.libsignal.SignalProtocolAddress(
// Using source as opposed to sourceUuid allows us to get the existing
// session if we haven't yet harvested the incoming uuid
identifier as any,
envelope.sourceDevice as any
);
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const options: any = {};
// No limit on message keys if we're communicating with our other devices
if (
(envelope.source && ourNumber && ourNumber === envelope.source) ||
(envelope.sourceUuid && ourUuid && ourUuid === envelope.sourceUuid)
) {
options.messageKeysLimit = false;
}
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address,
options
);
2021-02-27 00:00:37 +00:00
const secretSessionCipher = new SecretSessionCipher(
window.textsecure.storage.protocol,
options
);
const me = {
number: ourNumber,
uuid: ourUuid,
deviceId: parseInt(
window.textsecure.storage.user.getDeviceId() as string,
10
),
};
switch (envelope.type) {
case window.textsecure.protobuf.Envelope.Type.CIPHERTEXT:
window.log.info('message from', this.getEnvelopeId(envelope));
2018-05-02 16:51:22 +00:00
promise = sessionCipher
.decryptWhisperMessage(ciphertext)
.then(this.unpad);
break;
case window.textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
window.log.info('prekey message from', this.getEnvelopeId(envelope));
2018-05-02 16:51:22 +00:00
promise = this.decryptPreKeyWhisperMessage(
ciphertext,
sessionCipher,
address
);
break;
case window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER:
window.log.info('received unidentified sender message');
promise = secretSessionCipher
.decrypt(
2021-02-27 00:00:37 +00:00
createCertificateValidator(serverTrustRoot),
ciphertext.toArrayBuffer(),
2018-11-09 01:23:07 +00:00
Math.min(envelope.serverTimestamp || Date.now(), Date.now()),
me
)
.then(
result => {
const { isMe, sender, senderUuid, content } = result;
// We need to drop incoming messages from ourself since server can't
// do it for us
if (isMe) {
return { isMe: true };
}
if (
(sender && this.isBlocked(sender.getName())) ||
(senderUuid && this.isUuidBlocked(senderUuid.getName()))
) {
window.log.info(
'Dropping blocked message after sealed sender decryption'
);
return { isBlocked: true };
}
// Here we take this sender information and attach it back to the envelope
// to make the rest of the app work properly.
const originalSource = envelope.source;
const originalSourceUuid = envelope.sourceUuid;
// eslint-disable-next-line no-param-reassign
envelope.source = sender && sender.getName();
// eslint-disable-next-line no-param-reassign
envelope.sourceUuid = senderUuid && senderUuid.getName();
window.normalizeUuids(
envelope,
['sourceUuid'],
'message_receiver::decrypt::UNIDENTIFIED_SENDER'
);
// eslint-disable-next-line no-param-reassign
envelope.sourceDevice =
(sender && sender.getDeviceId()) ||
(senderUuid && senderUuid.getDeviceId());
// eslint-disable-next-line no-param-reassign
envelope.unidentifiedDeliveryReceived = !(
originalSource || originalSourceUuid
);
2021-02-27 00:00:37 +00:00
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);
},
(error: Error) => {
const { sender, senderUuid } = error || {};
if (sender || senderUuid) {
const originalSource = envelope.source;
const originalSourceUuid = envelope.sourceUuid;
if (
(sender && this.isBlocked(sender.getName())) ||
(senderUuid && this.isUuidBlocked(senderUuid.getName()))
) {
window.log.info(
'Dropping blocked message with error after sealed sender decryption'
);
return { isBlocked: true };
}
// eslint-disable-next-line no-param-reassign
envelope.source = sender && sender.getName();
// eslint-disable-next-line no-param-reassign
envelope.sourceUuid =
senderUuid && senderUuid.getName().toLowerCase();
window.normalizeUuids(
envelope,
['sourceUuid'],
'message_receiver::decrypt::UNIDENTIFIED_SENDER::error'
);
// eslint-disable-next-line no-param-reassign
envelope.sourceDevice =
(sender && sender.getDeviceId()) ||
(senderUuid && senderUuid.getDeviceId());
// eslint-disable-next-line no-param-reassign
envelope.unidentifiedDeliveryReceived = !(
originalSource || originalSourceUuid
);
throw error;
}
this.removeFromCache(envelope);
throw error;
}
);
break;
default:
promise = Promise.reject(new Error('Unknown message type'));
}
2018-05-02 16:51:22 +00:00
return promise
.then((plaintext: any) => {
const { isMe, isBlocked } = plaintext || {};
if (isMe || isBlocked) {
this.removeFromCache(envelope);
return null;
}
// Note: this is an out of band update; there are cases where the item in the
// cache has already been deleted by the time this runs. That's okay.
try {
this.updateCache(envelope, plaintext);
} catch (error) {
const errorString = error && error.stack ? error.stack : error;
window.log.error(`decrypt: updateCache failed: ${errorString}`);
}
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 && error.name === 'MessageCounterError') {
throw error;
2018-05-02 16:51:22 +00:00
}
2021-02-18 16:40:26 +00:00
if (uuid && deviceId) {
await this.maybeLightSessionReset(uuid, deviceId);
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;
});
}
isOverHourIntoPast(timestamp: number): boolean {
const HOUR = 1000 * 60 * 60;
const now = Date.now();
const oneHourIntoPast = now - HOUR;
return isNumber(timestamp) && timestamp <= oneHourIntoPast;
}
// We don't lose anything if we delete keys over an hour into the past, because we only
// change our behavior if the timestamps stored are less than an hour ago.
cleanupSessionResets(): void {
const sessionResets = window.storage.get(
'sessionResets',
{}
) as SessionResetsType;
const keys = Object.keys(sessionResets);
keys.forEach(key => {
const timestamp = sessionResets[key];
if (!timestamp || this.isOverHourIntoPast(timestamp)) {
delete sessionResets[key];
}
});
window.storage.put('sessionResets', sessionResets);
}
async maybeLightSessionReset(uuid: string, deviceId: number): Promise<void> {
2021-02-18 16:40:26 +00:00
const id = `${uuid}.${deviceId}`;
try {
const sessionResets = window.storage.get(
'sessionResets',
{}
) as SessionResetsType;
const lastReset = sessionResets[id];
// We emit this event every time we encounter an error, not just when we reset the
// session. This is because a message might have been lost with every decryption
// failure.
const event = new Event('light-session-reset');
event.senderUuid = uuid;
this.dispatchAndWait(event);
2021-02-18 16:40:26 +00:00
if (lastReset && !this.isOverHourIntoPast(lastReset)) {
window.log.warn(
`maybeLightSessionReset/${id}: Skipping session reset, last reset at ${lastReset}`
2021-02-18 16:40:26 +00:00
);
return;
}
2021-02-18 16:40:26 +00:00
sessionResets[id] = Date.now();
window.storage.put('sessionResets', sessionResets);
await this.lightSessionReset(uuid, deviceId);
2021-02-18 16:40:26 +00:00
} catch (error) {
// If we failed to do the session reset, then we'll allow another attempt sooner
// than one hour from now.
2021-02-18 16:40:26 +00:00
const sessionResets = window.storage.get(
'sessionResets',
{}
) as SessionResetsType;
delete sessionResets[id];
window.storage.put('sessionResets', sessionResets);
const errorString = error && error.stack ? error.stack : error;
window.log.error(
`maybeLightSessionReset/${id}: Enountered error`,
errorString
);
2021-02-18 16:40:26 +00:00
}
}
async lightSessionReset(uuid: string, deviceId: number): Promise<void> {
const id = `${uuid}.${deviceId}`;
// First, fetch this conversation
const conversationId = window.ConversationController.ensureContactIds({
uuid,
});
assert(conversationId, `lightSessionReset/${id}: missing conversationId`);
const conversation = window.ConversationController.get(conversationId);
assert(conversation, `lightSessionReset/${id}: missing conversation`);
window.log.warn(`lightSessionReset/${id}: Resetting session`);
// Archive open session with this device
const address = new window.libsignal.SignalProtocolAddress(uuid, deviceId);
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
await sessionCipher.closeOpenSessionForDevice();
// Send a null message with newly-created session
const sendOptions = conversation.getSendOptions();
await window.textsecure.messaging.sendNullMessage({ uuid }, sendOptions);
}
async decryptPreKeyWhisperMessage(
ciphertext: ArrayBuffer,
sessionCipher: SessionCipherClass,
address: SignalProtocolAddressClass
) {
const padded = await sessionCipher.decryptPreKeyWhisperMessage(ciphertext);
try {
return this.unpad(padded);
} catch (e) {
if (e.message === 'Unknown identity key') {
// create an error that the UI will pick up and ask the
// user if they want to re-negotiate
const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext);
throw new IncomingIdentityKeyError(
address.toString(),
buffer.toArrayBuffer(),
e.identityKey
);
}
throw e;
}
}
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) {
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(),
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 handleLegacyMessage(envelope: EnvelopeClass) {
2021-03-04 21:44:57 +00:00
window.log.info(
'MessageReceiver.handleLegacyMessage',
this.getEnvelopeId(envelope)
);
return this.decrypt(envelope, envelope.legacyMessage).then(plaintext => {
if (!plaintext) {
window.log.warn('handleLegacyMessage: plaintext was falsey');
return null;
}
return this.innerHandleLegacyMessage(envelope, plaintext);
});
}
async innerHandleLegacyMessage(
envelope: EnvelopeClass,
plaintext: ArrayBuffer
) {
const message = window.textsecure.protobuf.DataMessage.decode(plaintext);
return this.handleDataMessage(envelope, message);
}
async handleContentMessage(envelope: EnvelopeClass) {
2021-03-04 21:44:57 +00:00
window.log.info(
'MessageReceiver.handleContentMessage',
this.getEnvelopeId(envelope)
);
return this.decrypt(envelope, envelope.content).then(plaintext => {
if (!plaintext) {
window.log.warn('handleContentMessage: plaintext was falsey');
return null;
}
return this.innerHandleContentMessage(envelope, plaintext);
});
}
async innerHandleContentMessage(
envelope: EnvelopeClass,
plaintext: ArrayBuffer
) {
const content = window.textsecure.protobuf.Content.decode(plaintext);
if (content.syncMessage) {
return this.handleSyncMessage(envelope, content.syncMessage);
}
if (content.dataMessage) {
return this.handleDataMessage(envelope, content.dataMessage);
}
if (content.nullMessage) {
this.handleNullMessage(envelope);
return undefined;
}
if (content.callingMessage) {
2020-06-04 18:16:19 +00:00
return this.handleCallingMessage(envelope, content.callingMessage);
}
if (content.receiptMessage) {
return this.handleReceiptMessage(envelope, content.receiptMessage);
}
if (content.typingMessage) {
2018-11-14 19:10:32 +00:00
return this.handleTypingMessage(envelope, content.typingMessage);
}
this.removeFromCache(envelope);
throw new Error('Unsupported content message');
}
2020-06-04 18:16:19 +00:00
async handleCallingMessage(
envelope: EnvelopeClass,
callingMessage: CallingMessageClass
) {
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
) {
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));
}
}
return Promise.all(results);
}
2020-11-20 17:30:45 +00:00
async handleTypingMessage(
envelope: EnvelopeClass,
typingMessage: TypingMessageClass
) {
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 null;
}
}
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);
}
}
2018-11-14 19:10:32 +00:00
return this.dispatchEvent(ev);
}
handleNullMessage(envelope: EnvelopeClass) {
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
) {
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);
throw new Error('Got empty SyncMessage');
}
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>
) {
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));
}
return 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('got end session');
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
identifier
);
2018-05-02 16:51:22 +00:00
return Promise.all(
deviceIds.map(async deviceId => {
const address = new window.libsignal.SignalProtocolAddress(
identifier,
deviceId
);
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
2018-05-02 16:51:22 +00:00
address
);
window.log.info(
'handleEndSession: closing sessions for',
address.toString()
);
return sessionCipher.closeOpenSessionForDevice();
2018-05-02 16:51:22 +00:00
})
);
}
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);
2021-02-18 16:40:26 +00:00
// For tests
this.isOverHourIntoPast = inner.isOverHourIntoPast.bind(inner);
this.cleanupSessionResets = inner.cleanupSessionResets.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;
2021-02-18 16:40:26 +00:00
isOverHourIntoPast: (timestamp: number) => boolean;
cleanupSessionResets: () => void;
getProcessedCount: () => number;
static stringToArrayBuffer = MessageReceiverInner.stringToArrayBuffer;
static arrayBufferToString = MessageReceiverInner.arrayBufferToString;
static stringToArrayBufferBase64 =
MessageReceiverInner.stringToArrayBufferBase64;
static arrayBufferToStringBase64 =
MessageReceiverInner.arrayBufferToStringBase64;
}