Signal-Desktop/ts/textsecure/MessageReceiver.ts

1972 lines
61 KiB
TypeScript
Raw Normal View History

// tslint:disable no-bitwise no-default-export
import { isNumber, map, omit } from 'lodash';
import { w3cwebsocket as WebSocket } from 'websocket';
import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid';
import EventTarget from './EventTarget';
import { WebAPIType } from './WebAPI';
import { BatcherType, createBatcher } from '../util/batcher';
import utils from './Helpers';
import WebSocketResource, {
IncomingWebSocketRequest,
} from './WebsocketResources';
import Crypto from './Crypto';
import { SessionCipherClass, SignalProtocolAddressClass } from '../libsignal.d';
import { ContactBuffer, GroupBuffer } from './ContactsParser';
import { IncomingIdentityKeyError } from './Errors';
import {
AttachmentPointerClass,
2020-06-04 18:16:19 +00:00
CallingMessageClass,
DataMessageClass,
DownloadAttachmentType,
EnvelopeClass,
ReceiptMessageClass,
SyncMessageClass,
TypingMessageClass,
UnprocessedType,
VerifiedClass,
} from '../textsecure.d';
const RETRY_TIMEOUT = 2 * 60 * 1000;
declare global {
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;
messageRequestResponseType?: number;
proto?: any;
read?: any;
reason?: any;
sender?: any;
senderDevice?: any;
senderUuid?: any;
source?: any;
sourceUuid?: any;
stickerPacks?: any;
2020-05-27 21:37:06 +00:00
threadE164?: string;
threadUuid?: string;
storageServiceKey?: ArrayBuffer;
timestamp?: any;
typing?: any;
verified?: any;
}
interface Error {
reason?: any;
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;
deviceId: number;
hasConnected?: boolean;
incomingQueue: PQueue;
isEmptied?: boolean;
// tslint:disable-next-line variable-name
number_id: string | null;
password: string;
pendingQueue: PQueue;
retryCachedTimeout: any;
server: WebAPIType;
serverTrustRoot: ArrayBuffer;
signalingKey: ArrayBuffer;
socket?: WebSocket;
stoppingProcessing?: boolean;
username: string;
uuid: string;
// tslint:disable-next-line variable-name
uuid_id: string | null;
wsr?: WebSocketResource;
constructor(
oldUsername: string,
username: string,
password: string,
signalingKey: ArrayBuffer,
options: {
serverTrustRoot: string;
retryCached?: string;
}
) {
super();
this.count = 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] : null;
this.uuid_id = username ? utils.unencodeNumber(username)[0] : null;
this.deviceId = parseInt(
utils.unencodeNumber(username || oldUsername)[1],
10
);
this.incomingQueue = new PQueue({ concurrency: 1 });
this.pendingQueue = new PQueue({ concurrency: 1 });
this.appQueue = new PQueue({ concurrency: 1 });
this.cacheAddBatcher = createBatcher<CacheAddItemType>({
wait: 200,
maxSize: 30,
processBatch: this.cacheAndQueueBatch.bind(this),
});
this.cacheUpdateBatcher = createBatcher<CacheUpdateItemType>({
wait: 500,
maxSize: 30,
processBatch: this.cacheUpdateBatch.bind(this),
});
this.cacheRemoveBatcher = createBatcher<string>({
wait: 500,
maxSize: 30,
processBatch: this.cacheRemoveBatch.bind(this),
});
if (options.retryCached) {
// tslint:disable-next-line no-floating-promises
this.pendingQueue.add(async () => this.queueAllCached());
}
}
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;
}
this.count = 0;
if (this.hasConnected) {
const ev = new Event('reconnect');
this.dispatchEvent(ev);
}
this.isEmptied = false;
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) {
// @ts-ignore
this.socket.onclose = null;
// @ts-ignore
this.socket.onerror = null;
// @ts-ignore
this.socket.onopen = null;
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) {
// tslint:disable-next-line no-floating-promises
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') {
// tslint:disable-next-line no-floating-promises
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 = (envelope.serverGuid || getGuid()).replace(/-/g, '');
2018-11-09 01:23:07 +00:00
envelope.serverTimestamp = envelope.serverTimestamp
? envelope.serverTimestamp.toNumber()
: null;
// Calculate the message age (time on server).
envelope.messageAgeSec = this.calculateMessageAge(
headers,
envelope.serverTimestamp
);
2019-09-26 19:56:31 +00:00
this.cacheAndQueue(envelope, plaintext, request);
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);
}
};
// tslint:disable-next-line no-floating-promises
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;
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;
}
onEmpty() {
const emitEmpty = () => {
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
// tslint:disable-next-line no-floating-promises
2019-07-09 18:46:48 +00:00
this.appQueue.add(emitEmpty);
};
2019-07-09 18:46:48 +00:00
const waitForIncomingQueue = () => {
// tslint:disable-next-line no-floating-promises
2019-07-09 18:46:48 +00:00
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();
// tslint:disable-next-line no-floating-promises
2019-09-26 19:56:31 +00:00
this.incomingQueue.add(waitForIncomingQueue);
};
// tslint:disable-next-line no-floating-promises
2019-09-26 19:56:31 +00:00
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) {
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
);
2018-11-09 01:23:07 +00:00
envelope.id = envelope.serverGuid || item.id;
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!');
}
// tslint:disable-next-line no-floating-promises
this.queueDecryptedEnvelope(envelope, payloadPlaintext);
} else {
// tslint:disable-next-line no-floating-promises
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) {
if (envelope.sourceUuid || envelope.source) {
return `${envelope.sourceUuid || envelope.source}.${
envelope.sourceDevice
} ${envelope.timestamp.toNumber()} (${envelope.id})`;
}
return envelope.id;
}
clearRetryTimeout() {
if (this.retryCachedTimeout) {
clearInterval(this.retryCachedTimeout);
this.retryCachedTimeout = null;
}
}
maybeScheduleRetryTimeout() {
if (this.isEmptied) {
this.clearRetryTimeout();
this.retryCachedTimeout = setTimeout(() => {
// tslint:disable-next-line no-floating-promises
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>) {
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 => {
item.request.respond(200, 'OK');
// tslint:disable-next-line no-floating-promises
2019-09-26 19:56:31 +00:00
this.queueEnvelope(item.envelope);
});
this.maybeScheduleRetryTimeout();
} catch (error) {
items.forEach(item => {
item.request.respond(500, 'Failed to cache message');
});
window.log.error(
'cacheAndQueue error trying to add messages to cache:',
error && error.stack ? error.stack : error
);
}
}
cacheAndQueue(
envelope: EnvelopeClass,
plaintext: ArrayBuffer,
request: IncomingWebSocketRequest
) {
const { id } = envelope;
const data = {
id,
version: 2,
envelope: MessageReceiverInner.arrayBufferToStringBase64(plaintext),
timestamp: Date.now(),
attempts: 1,
};
2019-09-26 19:56:31 +00:00
this.cacheAddBatcher.add({
request,
envelope,
data,
});
}
async cacheUpdateBatch(items: Array<Partial<UnprocessedType>>) {
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);
const task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext);
const taskWithTimeout = window.textsecure.createTaskWithTimeout(
task,
`queueEncryptedEnvelope ${id}`
);
const promise = this.addToQueue(taskWithTimeout);
2018-05-02 16:51:22 +00:00
return promise.catch(error => {
window.log.error(
`queueDecryptedEnvelope error handling envelope ${id}:`,
error && error.stack ? error.stack : error
);
});
}
async queueEnvelope(envelope: EnvelopeClass) {
const id = this.getEnvelopeId(envelope);
window.log.info('queueing envelope', id);
const task = this.handleEnvelope.bind(this, envelope);
const taskWithTimeout = window.textsecure.createTaskWithTimeout(
2018-05-02 16:51:22 +00:00
task,
`queueEnvelope ${id}`
);
const promise = this.addToQueue(taskWithTimeout);
2018-05-02 16:51:22 +00:00
return promise.catch(error => {
2020-03-20 19:01:15 +00:00
const args = [
'queueEnvelope error handling envelope',
this.getEnvelopeId(envelope),
':',
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;
} else 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);
} else 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;
} else if (this.hasConnected) {
return WebSocket.CLOSED;
}
return -1;
}
async onDeliveryReceipt(envelope: EnvelopeClass) {
// tslint:disable-next-line promise-must-complete
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;
}
// tslint:disable-next-line max-func-body-length
async decrypt(
envelope: EnvelopeClass,
ciphertext: any
): Promise<ArrayBuffer> {
const { serverTrustRoot } = this;
let address: SignalProtocolAddressClass;
let promise;
const identifier = envelope.source || envelope.sourceUuid;
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
);
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
window.textsecure.storage.protocol
);
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(
window.Signal.Metadata.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
);
// 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.
2019-09-26 19:56:31 +00:00
this.updateCache(envelope, plaintext);
return plaintext;
})
.catch(async error => {
2018-05-02 16:51:22 +00:00
let errorToThrow = error;
if (error && error.message === 'Unknown identity key') {
2018-05-02 16:51:22 +00:00
// 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);
errorToThrow = new IncomingIdentityKeyError(
2018-05-02 16:51:22 +00:00
address.toString(),
buffer.toArrayBuffer(),
error.identityKey
);
}
const ev = new Event('error');
ev.error = errorToThrow;
ev.proto = envelope;
ev.confirm = this.removeFromCache.bind(this, envelope);
const returnError = async () => Promise.reject(errorToThrow);
2018-05-02 16:51:22 +00:00
return this.dispatchAndWait(ev).then(returnError, returnError);
});
}
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
) {
const {
destination,
timestamp,
message: msg,
expirationStartTimestamp,
unidentifiedStatus,
isRecipientUpdate,
} = sentContainer;
if (!msg) {
throw new Error('MessageReceiver.handleSentMessage: message was falsey!');
}
if (msg.groupV2) {
window.log.warn(
'MessageReceiver.handleSentMessage: Dropping GroupsV2 message'
);
this.removeFromCache(envelope);
return;
}
let p: Promise<any> = Promise.resolve();
// eslint-disable-next-line no-bitwise
if (
msg.flags &&
msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION
) {
if (!destination) {
throw new Error(
'MessageReceiver.handleSentMessage: Cannot end session with falsey destination'
);
}
p = this.handleEndSession(destination);
}
return p.then(async () =>
this.processDecrypted(envelope, msg).then(message => {
const groupId = message.group && message.group.id;
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(
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;
}
2018-05-02 16:51:22 +00:00
const ev = new Event('sent');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
destination,
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,
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) {
window.log.info('data message from', this.getEnvelopeId(envelope));
let p: Promise<any> = Promise.resolve();
// eslint-disable-next-line no-bitwise
const destination = envelope.source || envelope.sourceUuid;
if (!destination) {
throw new Error(
'MessageReceiver.handleDataMessage: source and sourceUuid were falsey'
);
}
if (msg.groupV2) {
window.log.warn(
'MessageReceiver.handleDataMessage: Dropping GroupsV2 message'
);
this.removeFromCache(envelope);
return;
}
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 => {
const groupId = message.group && message.group.id;
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(
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;
}
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,
};
return this.dispatchAndWait(ev);
})
);
}
async handleLegacyMessage(envelope: EnvelopeClass) {
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) {
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);
} else if (content.dataMessage) {
return this.handleDataMessage(envelope, content.dataMessage);
} else if (content.nullMessage) {
this.handleNullMessage(envelope);
return;
2020-06-04 18:16:19 +00:00
} else if (content.callingMessage) {
return this.handleCallingMessage(envelope, content.callingMessage);
} else if (content.receiptMessage) {
return this.handleReceiptMessage(envelope, content.receiptMessage);
2018-11-14 19:10:32 +00:00
} else if (content.typingMessage) {
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(),
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(),
reader: envelope.source || envelope.sourceUuid,
};
results.push(this.dispatchAndWait(ev));
}
}
return Promise.all(results);
}
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;
}
}
ev.sender = envelope.source;
ev.senderUuid = envelope.sourceUuid;
2018-11-14 19:10:32 +00:00
ev.senderDevice = envelope.sourceDevice;
ev.typing = {
typingMessage,
timestamp: typingMessage.timestamp
? typingMessage.timestamp.toNumber()
: Date.now(),
groupId: typingMessage.groupId
? typingMessage.groupId.toString('binary')
: null,
started:
typingMessage.action ===
window.textsecure.protobuf.TypingMessage.Action.STARTED,
2018-11-14 19:10:32 +00:00
stopped:
typingMessage.action ===
window.textsecure.protobuf.TypingMessage.Action.STOPPED,
2018-11-14 19:10:32 +00:00
};
return this.dispatchEvent(ev);
}
handleNullMessage(envelope: EnvelopeClass) {
window.log.info('null message from', this.getEnvelopeId(envelope));
this.removeFromCache(envelope);
}
// tslint:disable-next-line cyclomatic-complexity
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');
}
// tslint:disable-next-line triple-equals
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'
);
}
const to = sentMessage.message.group
? `group(${sentMessage.message.group.id.toBinary()})`
: sentMessage.destination;
window.log.info(
'sent message to',
to,
sentMessage.timestamp.toNumber(),
'from',
this.getEnvelopeId(envelope)
);
return this.handleSentMessage(envelope, sentMessage);
} else if (syncMessage.contacts) {
this.handleContacts(envelope, syncMessage.contacts);
return;
} else if (syncMessage.groups) {
this.handleGroups(envelope, syncMessage.groups);
return;
} else if (syncMessage.blocked) {
return this.handleBlocked(envelope, syncMessage.blocked);
} else if (syncMessage.request) {
window.log.info('Got SyncMessage Request');
this.removeFromCache(envelope);
return;
} else if (syncMessage.read && syncMessage.read.length) {
window.log.info('read messages from', this.getEnvelopeId(envelope));
return this.handleRead(envelope, syncMessage.read);
} else if (syncMessage.verified) {
return this.handleVerified(envelope, syncMessage.verified);
} else if (syncMessage.configuration) {
return this.handleConfiguration(envelope, syncMessage.configuration);
2019-06-26 19:33:13 +00:00
} else if (
syncMessage.stickerPackOperation &&
syncMessage.stickerPackOperation.length > 0
) {
return this.handleStickerPackOperation(
envelope,
syncMessage.stickerPackOperation
);
2019-08-05 20:53:15 +00:00
} else if (syncMessage.viewOnceOpen) {
return this.handleViewOnceOpen(envelope, syncMessage.viewOnceOpen);
2020-05-27 21:37:06 +00:00
} else if (syncMessage.messageRequestResponse) {
return this.handleMessageRequestResponse(
envelope,
syncMessage.messageRequestResponse
);
} else if (syncMessage.fetchLatest) {
return this.handleFetchLatest(envelope, syncMessage.fetchLatest);
} else 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.groupId = sync.groupId ? sync.groupId.toString('binary') : null;
ev.messageRequestResponseType = sync.type;
window.normalizeUuids(
ev,
['threadUuid'],
'MessageReceiver::handleMessageRequestResponse'
);
}
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;
}
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>
) {
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 = {
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.
// tslint:disable-next-line no-floating-promises
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.
// tslint:disable-next-line no-floating-promises
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);
return;
}
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,
};
}
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('deleting sessions for', address.toString());
2018-05-02 16:51:22 +00:00
return sessionCipher.deleteAllSessionsForDevice();
})
);
}
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
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);
} else 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 => {
const { image } = item;
if (!image) {
return item;
}
return {
...item,
image: this.cleanAttachment(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();
}
}
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.removeEventListener = inner.removeEventListener.bind(inner);
this.getStatus = inner.getStatus.bind(inner);
this.close = inner.close.bind(inner);
this.downloadAttachment = inner.downloadAttachment.bind(inner);
this.stopProcessing = inner.stopProcessing.bind(inner);
this.unregisterBatchers = inner.unregisterBatchers.bind(inner);
inner.connect();
}
addEventListener: (name: string, handler: Function) => void;
removeEventListener: (name: string, handler: Function) => void;
getStatus: () => number;
close: () => Promise<void>;
downloadAttachment: (
attachment: AttachmentPointerClass
) => Promise<DownloadAttachmentType>;
stopProcessing: () => Promise<void>;
unregisterBatchers: () => void;
static stringToArrayBuffer = MessageReceiverInner.stringToArrayBuffer;
static arrayBufferToString = MessageReceiverInner.arrayBufferToString;
static stringToArrayBufferBase64 =
MessageReceiverInner.stringToArrayBufferBase64;
static arrayBufferToStringBase64 =
MessageReceiverInner.arrayBufferToStringBase64;
}