2018-04-10 17:23:09 +00:00
|
|
|
/* global window: false */
|
|
|
|
/* global textsecure: false */
|
2018-05-26 01:01:56 +00:00
|
|
|
/* global WebAPI: false */
|
2018-04-10 17:23:09 +00:00
|
|
|
/* global libsignal: false */
|
|
|
|
/* global WebSocketResource: false */
|
|
|
|
/* global WebSocket: false */
|
|
|
|
/* global Event: false */
|
|
|
|
/* global dcodeIO: false */
|
|
|
|
/* global _: false */
|
|
|
|
/* global ContactBuffer: false */
|
|
|
|
/* global GroupBuffer: false */
|
|
|
|
|
|
|
|
/* eslint-disable more/no-then */
|
|
|
|
|
2019-08-16 16:15:39 +00:00
|
|
|
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
2018-08-16 17:45:09 +00:00
|
|
|
|
2018-06-02 00:55:35 +00:00
|
|
|
function MessageReceiver(username, password, signalingKey, options = {}) {
|
2018-04-10 17:23:09 +00:00
|
|
|
this.count = 0;
|
|
|
|
|
|
|
|
this.signalingKey = signalingKey;
|
|
|
|
this.username = username;
|
|
|
|
this.password = password;
|
2018-05-26 01:01:56 +00:00
|
|
|
this.server = WebAPI.connect({ username, password });
|
2018-04-10 17:23:09 +00:00
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
if (!options.serverTrustRoot) {
|
|
|
|
throw new Error('Server trust root is required!');
|
|
|
|
}
|
|
|
|
this.serverTrustRoot = window.Signal.Crypto.base64ToArrayBuffer(
|
|
|
|
options.serverTrustRoot
|
|
|
|
);
|
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
const address = libsignal.SignalProtocolAddress.fromString(username);
|
|
|
|
this.number = address.getName();
|
|
|
|
this.deviceId = address.getDeviceId();
|
|
|
|
|
2019-07-09 18:46:48 +00:00
|
|
|
this.incomingQueue = new window.PQueue({ concurrency: 1 });
|
2019-09-26 19:56:31 +00:00
|
|
|
this.pendingQueue = new window.PQueue({ concurrency: 1 });
|
2019-07-09 18:46:48 +00:00
|
|
|
this.appQueue = new window.PQueue({ concurrency: 1 });
|
2018-04-10 17:23:09 +00:00
|
|
|
|
2019-09-26 19:56:31 +00:00
|
|
|
this.cacheAddBatcher = window.Signal.Util.createBatcher({
|
|
|
|
wait: 200,
|
|
|
|
maxSize: 30,
|
|
|
|
processBatch: this.cacheAndQueueBatch.bind(this),
|
|
|
|
});
|
|
|
|
this.cacheUpdateBatcher = window.Signal.Util.createBatcher({
|
|
|
|
wait: 500,
|
|
|
|
maxSize: 30,
|
|
|
|
processBatch: this.cacheUpdateBatch.bind(this),
|
|
|
|
});
|
|
|
|
this.cacheRemoveBatcher = window.Signal.Util.createBatcher({
|
|
|
|
wait: 500,
|
|
|
|
maxSize: 30,
|
|
|
|
processBatch: this.cacheRemoveBatch.bind(this),
|
|
|
|
});
|
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
if (options.retryCached) {
|
2019-07-09 18:46:48 +00:00
|
|
|
this.pendingQueue.add(() => this.queueAllCached());
|
2018-04-10 17:23:09 +00:00
|
|
|
}
|
2015-09-01 23:59:06 +00:00
|
|
|
}
|
2016-01-14 22:07:00 +00:00
|
|
|
|
2018-08-06 19:11:02 +00:00
|
|
|
MessageReceiver.stringToArrayBuffer = string =>
|
2019-09-26 19:56:31 +00:00
|
|
|
dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();
|
2018-08-06 19:11:02 +00:00
|
|
|
MessageReceiver.arrayBufferToString = arrayBuffer =>
|
2019-09-26 19:56:31 +00:00
|
|
|
dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');
|
2018-08-10 00:28:51 +00:00
|
|
|
MessageReceiver.stringToArrayBufferBase64 = string =>
|
2019-09-26 19:56:31 +00:00
|
|
|
dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();
|
2018-08-10 00:28:51 +00:00
|
|
|
MessageReceiver.arrayBufferToStringBase64 = arrayBuffer =>
|
2019-09-26 19:56:31 +00:00
|
|
|
dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
|
2018-08-06 19:11:02 +00:00
|
|
|
|
2016-01-14 22:07:00 +00:00
|
|
|
MessageReceiver.prototype = new textsecure.EventTarget();
|
|
|
|
MessageReceiver.prototype.extend({
|
2018-04-10 17:23:09 +00:00
|
|
|
constructor: MessageReceiver,
|
|
|
|
connect() {
|
|
|
|
if (this.calledClose) {
|
|
|
|
return;
|
|
|
|
}
|
2017-10-23 21:38:43 +00:00
|
|
|
|
2018-07-03 21:56:30 +00:00
|
|
|
this.count = 0;
|
|
|
|
if (this.hasConnected) {
|
|
|
|
const ev = new Event('reconnect');
|
|
|
|
this.dispatchEvent(ev);
|
|
|
|
}
|
|
|
|
|
2019-08-16 16:15:39 +00:00
|
|
|
this.isEmptied = false;
|
2018-04-10 17:23:09 +00:00
|
|
|
this.hasConnected = true;
|
2017-10-06 23:28:13 +00:00
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
|
|
|
|
this.socket.close();
|
|
|
|
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);
|
|
|
|
this.wsr.addEventListener('close', this._onClose);
|
|
|
|
},
|
2019-02-19 20:10:26 +00:00
|
|
|
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();
|
|
|
|
},
|
2018-04-10 17:23:09 +00:00
|
|
|
shutdown() {
|
|
|
|
if (this.socket) {
|
|
|
|
this.socket.onclose = null;
|
|
|
|
this.socket.onerror = null;
|
|
|
|
this.socket.onopen = null;
|
|
|
|
this.socket = null;
|
|
|
|
}
|
2017-07-17 22:46:00 +00:00
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
if (this.wsr) {
|
|
|
|
this.wsr.removeEventListener('close', this._onClose);
|
|
|
|
this.wsr = null;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
close() {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('MessageReceiver.close()');
|
2018-04-10 17:23:09 +00:00
|
|
|
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');
|
|
|
|
}
|
2017-10-06 23:28:13 +00:00
|
|
|
|
2019-08-16 16:15:39 +00:00
|
|
|
this.clearRetryTimeout();
|
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
return this.drain();
|
|
|
|
},
|
|
|
|
onopen() {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('websocket open');
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
onerror() {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.error('websocket error');
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
dispatchAndWait(event) {
|
2019-07-09 18:46:48 +00:00
|
|
|
this.appQueue.add(() => Promise.all(this.dispatchEvent(event)));
|
2019-02-11 23:59:21 +00:00
|
|
|
|
|
|
|
return Promise.resolve();
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
onclose(ev) {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info(
|
2018-04-10 17:23:09 +00:00
|
|
|
'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(this.number)
|
2018-04-10 17:23:09 +00:00
|
|
|
.then(this.connect.bind(this)) // No HTTP error? Reconnect
|
2018-05-02 16:51:22 +00:00
|
|
|
.catch(e => {
|
2018-04-10 17:23:09 +00:00
|
|
|
const event = new Event('error');
|
|
|
|
event.error = e;
|
|
|
|
return this.dispatchAndWait(event);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
handleRequest(request) {
|
|
|
|
// 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') {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('got request', request.verb, request.path);
|
2018-04-10 17:23:09 +00:00
|
|
|
request.respond(200, 'OK');
|
|
|
|
|
|
|
|
if (request.verb === 'PUT' && request.path === '/api/v1/queue/empty') {
|
2019-07-09 18:46:48 +00:00
|
|
|
this.incomingQueue.add(() => this.onEmpty());
|
2018-04-10 17:23:09 +00:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2017-10-06 23:28:13 +00:00
|
|
|
|
2019-07-09 18:46:48 +00:00
|
|
|
const job = async () => {
|
|
|
|
let plaintext;
|
|
|
|
const headers = request.headers || [];
|
2019-01-11 16:53:35 +00:00
|
|
|
|
2019-07-09 18:46:48 +00:00
|
|
|
if (headers.includes('X-Signal-Key: true')) {
|
|
|
|
plaintext = await textsecure.crypto.decryptWebsocketMessage(
|
|
|
|
request.body,
|
|
|
|
this.signalingKey
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
plaintext = request.body.toArrayBuffer();
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2018-05-02 16:51:22 +00:00
|
|
|
const envelope = textsecure.protobuf.Envelope.decode(plaintext);
|
|
|
|
// 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
|
2018-04-10 17:23:09 +00:00
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
if (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
|
|
|
}
|
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
envelope.id = envelope.serverGuid || window.getGuid();
|
2018-11-09 01:23:07 +00:00
|
|
|
envelope.serverTimestamp = envelope.serverTimestamp
|
|
|
|
? envelope.serverTimestamp.toNumber()
|
|
|
|
: null;
|
2018-10-18 01:01:21 +00:00
|
|
|
|
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');
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.error(
|
2018-05-02 16:51:22 +00:00
|
|
|
'Error handling incoming message:',
|
|
|
|
e && e.stack ? e.stack : e
|
2017-09-28 20:12:43 +00:00
|
|
|
);
|
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);
|
|
|
|
}
|
|
|
|
};
|
2018-04-10 17:23:09 +00:00
|
|
|
|
2019-07-09 18:46:48 +00:00
|
|
|
this.incomingQueue.add(job);
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
addToQueue(task) {
|
|
|
|
this.count += 1;
|
|
|
|
|
2019-07-09 18:46:48 +00:00
|
|
|
const promise = this.pendingQueue.add(task);
|
|
|
|
|
|
|
|
const { count } = this;
|
2018-04-10 17:23:09 +00:00
|
|
|
|
2019-07-09 18:46:48 +00:00
|
|
|
const update = () => {
|
2018-04-10 17:23:09 +00:00
|
|
|
this.updateProgress(count);
|
|
|
|
};
|
|
|
|
|
2019-07-09 18:46:48 +00:00
|
|
|
promise.then(update, update);
|
2018-04-10 17:23:09 +00:00
|
|
|
|
2019-07-09 18:46:48 +00:00
|
|
|
return promise;
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
onEmpty() {
|
2019-02-05 18:04:38 +00:00
|
|
|
const emitEmpty = () => {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info("MessageReceiver: emitting 'empty' event");
|
2018-04-10 17:23:09 +00:00
|
|
|
const ev = new Event('empty');
|
2019-02-05 18:04:38 +00:00
|
|
|
this.dispatchAndWait(ev);
|
2019-08-16 16:15:39 +00:00
|
|
|
this.isEmptied = true;
|
2019-10-22 01:54:40 +00:00
|
|
|
|
2019-08-16 16:15:39 +00:00
|
|
|
this.maybeScheduleRetryTimeout();
|
2019-02-05 18:04:38 +00:00
|
|
|
};
|
|
|
|
|
2019-07-09 18:46:48 +00:00
|
|
|
const waitForPendingQueue = () => {
|
2019-02-05 18:04:38 +00:00
|
|
|
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);
|
2018-04-10 17:23:09 +00:00
|
|
|
};
|
|
|
|
|
2019-07-09 18:46:48 +00:00
|
|
|
const waitForIncomingQueue = () => {
|
|
|
|
this.addToQueue(waitForPendingQueue);
|
2018-04-10 17:23:09 +00:00
|
|
|
|
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;
|
2018-04-10 17:23:09 +00:00
|
|
|
};
|
|
|
|
|
2019-09-26 19:56:31 +00:00
|
|
|
const waitForCacheAddBatcher = async () => {
|
|
|
|
await this.cacheAddBatcher.onIdle();
|
|
|
|
this.incomingQueue.add(waitForIncomingQueue);
|
|
|
|
};
|
|
|
|
|
|
|
|
waitForCacheAddBatcher();
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
drain() {
|
2019-07-09 18:46:48 +00:00
|
|
|
const waitForIncomingQueue = () =>
|
2018-05-02 16:51:22 +00:00
|
|
|
this.addToQueue(() => {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('drained');
|
2018-05-02 16:51:22 +00:00
|
|
|
});
|
2018-04-10 17:23:09 +00:00
|
|
|
|
2019-07-09 18:46:48 +00:00
|
|
|
return this.incomingQueue.add(waitForIncomingQueue);
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
updateProgress(count) {
|
|
|
|
// count by 10s
|
|
|
|
if (count % 10 !== 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const ev = new Event('progress');
|
|
|
|
ev.count = count;
|
|
|
|
this.dispatchEvent(ev);
|
|
|
|
},
|
2018-08-15 22:14:12 +00:00
|
|
|
async queueAllCached() {
|
|
|
|
const items = await this.getAllFromCache();
|
|
|
|
for (let i = 0, max = items.length; i < max; i += 1) {
|
2018-08-16 16:50:56 +00:00
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
await this.queueCached(items[i]);
|
2018-08-15 22:14:12 +00:00
|
|
|
}
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
2018-08-03 18:55:43 +00:00
|
|
|
async queueCached(item) {
|
2018-04-10 17:23:09 +00:00
|
|
|
try {
|
|
|
|
let envelopePlaintext = item.envelope;
|
|
|
|
|
2018-08-10 00:28:51 +00:00
|
|
|
if (item.version === 2) {
|
2019-09-26 19:56:31 +00:00
|
|
|
envelopePlaintext = MessageReceiver.stringToArrayBufferBase64(
|
2018-08-10 00:28:51 +00:00
|
|
|
envelopePlaintext
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
if (typeof envelopePlaintext === 'string') {
|
2019-09-26 19:56:31 +00:00
|
|
|
envelopePlaintext = MessageReceiver.stringToArrayBuffer(
|
2018-08-06 19:11:02 +00:00
|
|
|
envelopePlaintext
|
|
|
|
);
|
2018-04-10 17:23:09 +00:00
|
|
|
}
|
|
|
|
const envelope = 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.sourceDevice = envelope.sourceDevice || item.sourceDevice;
|
|
|
|
envelope.serverTimestamp =
|
|
|
|
envelope.serverTimestamp || item.serverTimestamp;
|
2018-04-10 17:23:09 +00:00
|
|
|
|
|
|
|
const { decrypted } = item;
|
|
|
|
if (decrypted) {
|
|
|
|
let payloadPlaintext = decrypted;
|
2018-08-10 00:28:51 +00:00
|
|
|
|
|
|
|
if (item.version === 2) {
|
2019-09-26 19:56:31 +00:00
|
|
|
payloadPlaintext = MessageReceiver.stringToArrayBufferBase64(
|
2018-08-10 00:28:51 +00:00
|
|
|
payloadPlaintext
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
if (typeof payloadPlaintext === 'string') {
|
2019-09-26 19:56:31 +00:00
|
|
|
payloadPlaintext = MessageReceiver.stringToArrayBuffer(
|
2018-08-06 19:11:02 +00:00
|
|
|
payloadPlaintext
|
|
|
|
);
|
2017-09-13 18:25:51 +00:00
|
|
|
}
|
2018-04-10 17:23:09 +00:00
|
|
|
this.queueDecryptedEnvelope(envelope, payloadPlaintext);
|
|
|
|
} else {
|
|
|
|
this.queueEnvelope(envelope);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
2018-08-03 18:55:43 +00:00
|
|
|
window.log.error(
|
|
|
|
'queueCached error handling item',
|
|
|
|
item.id,
|
|
|
|
'removing it. Error:',
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const { id } = item;
|
|
|
|
await textsecure.storage.unprocessed.remove(id);
|
|
|
|
} catch (deleteError) {
|
|
|
|
window.log.error(
|
|
|
|
'queueCached error deleting item',
|
|
|
|
item.id,
|
|
|
|
'Error:',
|
|
|
|
deleteError && deleteError.stack ? deleteError.stack : deleteError
|
|
|
|
);
|
|
|
|
}
|
2018-04-10 17:23:09 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
getEnvelopeId(envelope) {
|
2018-10-18 01:01:21 +00:00
|
|
|
if (envelope.source) {
|
|
|
|
return `${envelope.source}.${
|
|
|
|
envelope.sourceDevice
|
|
|
|
} ${envelope.timestamp.toNumber()} (${envelope.id})`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return envelope.id;
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
2019-08-16 16:15:39 +00:00
|
|
|
clearRetryTimeout() {
|
|
|
|
if (this.retryCachedTimeout) {
|
|
|
|
clearInterval(this.retryCachedTimeout);
|
|
|
|
this.retryCachedTimeout = null;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
maybeScheduleRetryTimeout() {
|
|
|
|
if (this.isEmptied) {
|
2019-10-22 01:54:40 +00:00
|
|
|
this.clearRetryTimeout();
|
2019-08-16 16:15:39 +00:00
|
|
|
this.retryCachedTimeout = setTimeout(() => {
|
|
|
|
this.pendingQueue.add(() => this.queueAllCached());
|
|
|
|
}, RETRY_TIMEOUT);
|
|
|
|
}
|
|
|
|
},
|
2018-09-28 22:51:26 +00:00
|
|
|
async getAllFromCache() {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('getAllFromCache');
|
2018-09-28 22:51:26 +00:00
|
|
|
const count = await textsecure.storage.unprocessed.getCount();
|
|
|
|
|
2018-11-29 22:13:54 +00:00
|
|
|
if (count > 1500) {
|
2018-09-28 22:51:26 +00:00
|
|
|
await textsecure.storage.unprocessed.removeAll();
|
|
|
|
window.log.warn(
|
|
|
|
`There were ${count} messages in cache. Deleted all instead of reprocessing`
|
2018-07-21 19:00:08 +00:00
|
|
|
);
|
2018-09-28 22:51:26 +00:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const items = await 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);
|
2018-04-10 17:23:09 +00:00
|
|
|
|
2018-09-28 22:51:26 +00:00
|
|
|
try {
|
|
|
|
if (attempts >= 3) {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.warn(
|
|
|
|
'getAllFromCache final attempt for envelope',
|
|
|
|
item.id
|
|
|
|
);
|
2018-09-28 22:51:26 +00:00
|
|
|
await textsecure.storage.unprocessed.remove(item.id);
|
|
|
|
} else {
|
2019-02-05 01:23:50 +00:00
|
|
|
await textsecure.storage.unprocessed.updateAttempts(
|
|
|
|
item.id,
|
|
|
|
attempts
|
|
|
|
);
|
2018-05-02 16:51:22 +00:00
|
|
|
}
|
2018-09-28 22:51:26 +00:00
|
|
|
} catch (error) {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.error(
|
2018-09-28 22:51:26 +00:00
|
|
|
'getAllFromCache error updating item after load:',
|
2018-05-02 16:51:22 +00:00
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
);
|
2017-04-07 19:48:59 +00:00
|
|
|
}
|
2018-09-28 22:51:26 +00:00
|
|
|
|
|
|
|
return item;
|
|
|
|
})
|
|
|
|
);
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
2019-09-26 19:56:31 +00:00
|
|
|
async cacheAndQueueBatch(items) {
|
|
|
|
const dataArray = items.map(item => item.data);
|
|
|
|
try {
|
|
|
|
await textsecure.storage.unprocessed.batchAdd(dataArray);
|
|
|
|
items.forEach(item => {
|
|
|
|
item.request.respond(200, 'OK');
|
|
|
|
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, plaintext, request) {
|
2018-10-18 01:01:21 +00:00
|
|
|
const { id } = envelope;
|
2018-04-10 17:23:09 +00:00
|
|
|
const data = {
|
|
|
|
id,
|
2018-08-10 00:28:51 +00:00
|
|
|
version: 2,
|
2019-09-26 19:56:31 +00:00
|
|
|
envelope: MessageReceiver.arrayBufferToStringBase64(plaintext),
|
2018-04-10 17:23:09 +00:00
|
|
|
timestamp: Date.now(),
|
|
|
|
attempts: 1,
|
|
|
|
};
|
2019-09-26 19:56:31 +00:00
|
|
|
this.cacheAddBatcher.add({
|
|
|
|
request,
|
|
|
|
envelope,
|
|
|
|
data,
|
|
|
|
});
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
2019-09-26 19:56:31 +00:00
|
|
|
async cacheUpdateBatch(items) {
|
|
|
|
await textsecure.storage.unprocessed.addDecryptedDataToList(items);
|
|
|
|
},
|
|
|
|
updateCache(envelope, plaintext) {
|
2018-10-18 01:01:21 +00:00
|
|
|
const { id } = envelope;
|
2019-09-26 19:56:31 +00:00
|
|
|
const data = {
|
|
|
|
source: envelope.source,
|
|
|
|
sourceDevice: envelope.sourceDevice,
|
|
|
|
serverTimestamp: envelope.serverTimestamp,
|
|
|
|
decrypted: MessageReceiver.arrayBufferToStringBase64(plaintext),
|
|
|
|
};
|
|
|
|
this.cacheUpdateBatcher.add({ id, data });
|
|
|
|
},
|
|
|
|
async cacheRemoveBatch(items) {
|
|
|
|
await textsecure.storage.unprocessed.remove(items);
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
removeFromCache(envelope) {
|
2018-10-18 01:01:21 +00:00
|
|
|
const { id } = envelope;
|
2019-09-26 19:56:31 +00:00
|
|
|
this.cacheRemoveBatcher.add(id);
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
queueDecryptedEnvelope(envelope, plaintext) {
|
2019-02-11 22:53:57 +00:00
|
|
|
const id = this.getEnvelopeId(envelope);
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('queueing decrypted envelope', id);
|
2018-04-10 17:23:09 +00:00
|
|
|
|
|
|
|
const task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext);
|
|
|
|
const taskWithTimeout = textsecure.createTaskWithTimeout(
|
|
|
|
task,
|
|
|
|
`queueEncryptedEnvelope ${id}`
|
|
|
|
);
|
|
|
|
const promise = this.addToQueue(taskWithTimeout);
|
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
return promise.catch(error => {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.error(
|
2019-02-19 23:15:43 +00:00
|
|
|
`queueDecryptedEnvelope error handling envelope ${id}:`,
|
2018-04-10 17:23:09 +00:00
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
queueEnvelope(envelope) {
|
|
|
|
const id = this.getEnvelopeId(envelope);
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('queueing envelope', id);
|
2018-04-10 17:23:09 +00:00
|
|
|
|
|
|
|
const task = this.handleEnvelope.bind(this, envelope);
|
2018-05-02 16:51:22 +00:00
|
|
|
const taskWithTimeout = textsecure.createTaskWithTimeout(
|
|
|
|
task,
|
|
|
|
`queueEnvelope ${id}`
|
|
|
|
);
|
2018-04-10 17:23:09 +00:00
|
|
|
const promise = this.addToQueue(taskWithTimeout);
|
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
return promise.catch(error => {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.error(
|
2018-04-10 17:23:09 +00:00
|
|
|
'queueEnvelope error handling envelope',
|
2019-07-03 18:56:49 +00:00
|
|
|
this.getEnvelopeId(envelope),
|
2018-04-10 17:23:09 +00:00
|
|
|
':',
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
// Same as handleEnvelope, just without the decryption step. Necessary for handling
|
|
|
|
// messages which were successfully decrypted, but application logic didn't finish
|
|
|
|
// processing.
|
|
|
|
handleDecryptedEnvelope(envelope, plaintext) {
|
2019-02-19 20:10:26 +00:00
|
|
|
if (this.stoppingProcessing) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
2018-04-10 17:23:09 +00:00
|
|
|
// No decryption is required for delivery receipts, so the decrypted field of
|
|
|
|
// the Unprocessed model will never be set
|
|
|
|
|
|
|
|
if (envelope.content) {
|
|
|
|
return this.innerHandleContentMessage(envelope, plaintext);
|
|
|
|
} else if (envelope.legacyMessage) {
|
|
|
|
return this.innerHandleLegacyMessage(envelope, plaintext);
|
|
|
|
}
|
|
|
|
this.removeFromCache(envelope);
|
|
|
|
throw new Error('Received message with no content and no legacyMessage');
|
|
|
|
},
|
|
|
|
handleEnvelope(envelope) {
|
2019-02-19 20:10:26 +00:00
|
|
|
if (this.stoppingProcessing) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) {
|
|
|
|
return this.onDeliveryReceipt(envelope);
|
|
|
|
}
|
2017-04-07 19:48:59 +00:00
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
onDeliveryReceipt(envelope) {
|
2018-04-11 00:00:30 +00:00
|
|
|
return new Promise((resolve, reject) => {
|
2018-04-10 17:23:09 +00:00
|
|
|
const ev = new Event('delivery');
|
|
|
|
ev.confirm = this.removeFromCache.bind(this, envelope);
|
|
|
|
ev.deliveryReceipt = {
|
|
|
|
timestamp: envelope.timestamp.toNumber(),
|
|
|
|
source: envelope.source,
|
|
|
|
sourceDevice: envelope.sourceDevice,
|
|
|
|
};
|
|
|
|
this.dispatchAndWait(ev).then(resolve, reject);
|
2018-04-11 00:00:30 +00:00
|
|
|
});
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
unpad(paddedData) {
|
|
|
|
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');
|
|
|
|
}
|
|
|
|
}
|
2016-09-07 00:12:45 +00:00
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
return plaintext;
|
|
|
|
},
|
|
|
|
decrypt(envelope, ciphertext) {
|
2018-10-18 01:01:21 +00:00
|
|
|
const { serverTrustRoot } = this;
|
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
let promise;
|
|
|
|
const address = new libsignal.SignalProtocolAddress(
|
|
|
|
envelope.source,
|
|
|
|
envelope.sourceDevice
|
|
|
|
);
|
|
|
|
|
|
|
|
const ourNumber = textsecure.storage.user.getNumber();
|
|
|
|
const number = address.toString().split('.')[0];
|
|
|
|
const options = {};
|
|
|
|
|
|
|
|
// No limit on message keys if we're communicating with our other devices
|
|
|
|
if (ourNumber === number) {
|
|
|
|
options.messageKeysLimit = false;
|
|
|
|
}
|
2015-09-01 23:59:06 +00:00
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
const sessionCipher = new libsignal.SessionCipher(
|
|
|
|
textsecure.storage.protocol,
|
|
|
|
address,
|
|
|
|
options
|
|
|
|
);
|
2018-10-18 01:01:21 +00:00
|
|
|
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
|
|
|
|
textsecure.storage.protocol
|
|
|
|
);
|
|
|
|
|
|
|
|
const me = {
|
|
|
|
number: ourNumber,
|
|
|
|
deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10),
|
|
|
|
};
|
2018-04-10 17:23:09 +00:00
|
|
|
|
|
|
|
switch (envelope.type) {
|
|
|
|
case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('message from', this.getEnvelopeId(envelope));
|
2018-05-02 16:51:22 +00:00
|
|
|
promise = sessionCipher
|
|
|
|
.decryptWhisperMessage(ciphertext)
|
|
|
|
.then(this.unpad);
|
2018-04-10 17:23:09 +00:00
|
|
|
break;
|
|
|
|
case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('prekey message from', this.getEnvelopeId(envelope));
|
2018-05-02 16:51:22 +00:00
|
|
|
promise = this.decryptPreKeyWhisperMessage(
|
|
|
|
ciphertext,
|
|
|
|
sessionCipher,
|
|
|
|
address
|
|
|
|
);
|
2018-04-10 17:23:09 +00:00
|
|
|
break;
|
2018-10-18 01:01:21 +00:00
|
|
|
case 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()),
|
2018-10-18 01:01:21 +00:00
|
|
|
me
|
|
|
|
)
|
|
|
|
.then(
|
|
|
|
result => {
|
|
|
|
const { isMe, sender, content } = result;
|
|
|
|
|
|
|
|
// We need to drop incoming messages from ourself since server can't
|
|
|
|
// do it for us
|
|
|
|
if (isMe) {
|
|
|
|
return { isMe: true };
|
|
|
|
}
|
|
|
|
|
2019-01-22 22:49:36 +00:00
|
|
|
if (this.isBlocked(sender.getName())) {
|
|
|
|
window.log.info(
|
|
|
|
'Dropping blocked message after sealed sender decryption'
|
|
|
|
);
|
|
|
|
return { isBlocked: true };
|
|
|
|
}
|
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
// Here we take this sender information and attach it back to the envelope
|
|
|
|
// to make the rest of the app work properly.
|
|
|
|
|
2018-11-29 22:13:54 +00:00
|
|
|
const originalSource = envelope.source;
|
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
envelope.source = sender.getName();
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
envelope.sourceDevice = sender.getDeviceId();
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
2018-11-29 22:13:54 +00:00
|
|
|
envelope.unidentifiedDeliveryReceived = !originalSource;
|
2018-10-18 01:01:21 +00:00
|
|
|
|
|
|
|
// Return just the content because that matches the signature of the other
|
|
|
|
// decrypt methods used above.
|
|
|
|
return this.unpad(content);
|
|
|
|
},
|
|
|
|
error => {
|
|
|
|
const { sender } = error || {};
|
|
|
|
|
|
|
|
if (sender) {
|
2018-11-29 22:13:54 +00:00
|
|
|
const originalSource = envelope.source;
|
|
|
|
|
2019-01-22 22:49:36 +00:00
|
|
|
if (this.isBlocked(sender.getName())) {
|
|
|
|
window.log.info(
|
|
|
|
'Dropping blocked message with error after sealed sender decryption'
|
|
|
|
);
|
|
|
|
return { isBlocked: true };
|
|
|
|
}
|
|
|
|
|
2018-10-18 01:01:21 +00:00
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
envelope.source = sender.getName();
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
envelope.sourceDevice = sender.getDeviceId();
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
2018-11-29 22:13:54 +00:00
|
|
|
envelope.unidentifiedDeliveryReceived = !originalSource;
|
2018-10-18 01:01:21 +00:00
|
|
|
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
2019-10-14 15:35:29 +00:00
|
|
|
this.removeFromCache(envelope);
|
|
|
|
throw error;
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
break;
|
2018-04-10 17:23:09 +00:00
|
|
|
default:
|
|
|
|
promise = Promise.reject(new Error('Unknown message type'));
|
|
|
|
}
|
2017-07-17 22:46:00 +00:00
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
return promise
|
2018-10-18 01:01:21 +00:00
|
|
|
.then(plaintext => {
|
2019-01-22 22:49:36 +00:00
|
|
|
const { isMe, isBlocked } = plaintext || {};
|
|
|
|
if (isMe || isBlocked) {
|
2019-02-13 20:04:45 +00:00
|
|
|
this.removeFromCache(envelope);
|
|
|
|
return null;
|
2018-10-18 01:01:21 +00:00
|
|
|
}
|
|
|
|
|
2019-09-18 18:44:32 +00:00
|
|
|
// 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);
|
2019-02-13 20:04:45 +00:00
|
|
|
|
|
|
|
return plaintext;
|
2018-10-18 01:01:21 +00:00
|
|
|
})
|
2018-05-02 16:51:22 +00:00
|
|
|
.catch(error => {
|
|
|
|
let errorToThrow = error;
|
|
|
|
|
2018-07-13 15:59:18 +00:00
|
|
|
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 = dcodeIO.ByteBuffer.wrap(ciphertext);
|
|
|
|
errorToThrow = new textsecure.IncomingIdentityKeyError(
|
|
|
|
address.toString(),
|
|
|
|
buffer.toArrayBuffer(),
|
|
|
|
error.identityKey
|
|
|
|
);
|
|
|
|
}
|
|
|
|
const ev = new Event('error');
|
|
|
|
ev.error = errorToThrow;
|
|
|
|
ev.proto = envelope;
|
|
|
|
ev.confirm = this.removeFromCache.bind(this, envelope);
|
2018-04-10 17:23:09 +00:00
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
const returnError = () => Promise.reject(errorToThrow);
|
|
|
|
return this.dispatchAndWait(ev).then(returnError, returnError);
|
|
|
|
});
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
async decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address) {
|
|
|
|
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 = dcodeIO.ByteBuffer.wrap(ciphertext);
|
|
|
|
throw new textsecure.IncomingIdentityKeyError(
|
|
|
|
address.toString(),
|
|
|
|
buffer.toArrayBuffer(),
|
|
|
|
e.identityKey
|
|
|
|
);
|
|
|
|
}
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
},
|
2019-05-09 15:38:05 +00:00
|
|
|
handleSentMessage(envelope, sentContainer) {
|
2018-10-18 01:01:21 +00:00
|
|
|
const {
|
|
|
|
destination,
|
|
|
|
timestamp,
|
2019-05-09 15:38:05 +00:00
|
|
|
message: msg,
|
2018-10-18 01:01:21 +00:00
|
|
|
expirationStartTimestamp,
|
|
|
|
unidentifiedStatus,
|
2019-05-09 15:38:05 +00:00
|
|
|
isRecipientUpdate,
|
2018-10-18 01:01:21 +00:00
|
|
|
} = sentContainer;
|
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
let p = Promise.resolve();
|
|
|
|
// eslint-disable-next-line no-bitwise
|
|
|
|
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
|
|
|
|
p = this.handleEndSession(destination);
|
|
|
|
}
|
2018-05-02 16:51:22 +00:00
|
|
|
return p.then(() =>
|
2019-02-11 23:59:21 +00:00
|
|
|
this.processDecrypted(envelope, msg).then(message => {
|
2018-09-13 19:57:07 +00:00
|
|
|
const groupId = message.group && message.group.id;
|
2018-09-17 19:18:18 +00:00
|
|
|
const isBlocked = this.isGroupBlocked(groupId);
|
|
|
|
const isMe = envelope.source === textsecure.storage.user.getNumber();
|
2018-09-17 22:58:27 +00:00
|
|
|
const isLeavingGroup = Boolean(
|
|
|
|
message.group &&
|
|
|
|
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
|
|
|
|
);
|
2018-09-17 19:18:18 +00:00
|
|
|
|
2018-09-17 22:58:27 +00:00
|
|
|
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
|
2018-09-13 19:57:07 +00:00
|
|
|
window.log.warn(
|
|
|
|
`Message ${this.getEnvelopeId(
|
|
|
|
envelope
|
|
|
|
)} ignored; destined for blocked group`
|
|
|
|
);
|
|
|
|
return this.removeFromCache(envelope);
|
|
|
|
}
|
|
|
|
|
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(),
|
|
|
|
device: envelope.sourceDevice,
|
2018-10-18 01:01:21 +00:00
|
|
|
unidentifiedStatus,
|
2018-05-02 16:51:22 +00:00
|
|
|
message,
|
2019-05-09 15:38:05 +00:00
|
|
|
isRecipientUpdate,
|
2018-05-02 16:51:22 +00:00
|
|
|
};
|
|
|
|
if (expirationStartTimestamp) {
|
|
|
|
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
|
|
|
|
}
|
|
|
|
return this.dispatchAndWait(ev);
|
|
|
|
})
|
|
|
|
);
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
handleDataMessage(envelope, msg) {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('data message from', this.getEnvelopeId(envelope));
|
2018-04-10 17:23:09 +00:00
|
|
|
let p = Promise.resolve();
|
|
|
|
// eslint-disable-next-line no-bitwise
|
|
|
|
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
|
|
|
|
p = this.handleEndSession(envelope.source);
|
|
|
|
}
|
2018-05-02 16:51:22 +00:00
|
|
|
return p.then(() =>
|
2019-02-11 23:59:21 +00:00
|
|
|
this.processDecrypted(envelope, msg).then(message => {
|
2018-09-13 19:57:07 +00:00
|
|
|
const groupId = message.group && message.group.id;
|
2018-09-17 19:18:18 +00:00
|
|
|
const isBlocked = this.isGroupBlocked(groupId);
|
|
|
|
const isMe = envelope.source === textsecure.storage.user.getNumber();
|
2018-09-17 22:58:27 +00:00
|
|
|
const isLeavingGroup = Boolean(
|
|
|
|
message.group &&
|
|
|
|
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
|
|
|
|
);
|
2018-09-17 19:18:18 +00:00
|
|
|
|
2018-09-17 22:58:27 +00:00
|
|
|
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
|
2018-09-13 19:57:07 +00:00
|
|
|
window.log.warn(
|
|
|
|
`Message ${this.getEnvelopeId(
|
|
|
|
envelope
|
|
|
|
)} ignored; destined for blocked group`
|
|
|
|
);
|
|
|
|
return this.removeFromCache(envelope);
|
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
sourceDevice: envelope.sourceDevice,
|
|
|
|
timestamp: envelope.timestamp.toNumber(),
|
|
|
|
receivedAt: envelope.receivedAt,
|
2018-10-18 01:01:21 +00:00
|
|
|
unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived,
|
2018-05-02 16:51:22 +00:00
|
|
|
message,
|
|
|
|
};
|
|
|
|
return this.dispatchAndWait(ev);
|
|
|
|
})
|
|
|
|
);
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
handleLegacyMessage(envelope) {
|
2018-10-18 01:01:21 +00:00
|
|
|
return this.decrypt(envelope, envelope.legacyMessage).then(plaintext => {
|
|
|
|
if (!plaintext) {
|
|
|
|
window.log.warn('handleLegacyMessage: plaintext was falsey');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return this.innerHandleLegacyMessage(envelope, plaintext);
|
|
|
|
});
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
innerHandleLegacyMessage(envelope, plaintext) {
|
|
|
|
const message = textsecure.protobuf.DataMessage.decode(plaintext);
|
|
|
|
return this.handleDataMessage(envelope, message);
|
|
|
|
},
|
|
|
|
handleContentMessage(envelope) {
|
2018-10-18 01:01:21 +00:00
|
|
|
return this.decrypt(envelope, envelope.content).then(plaintext => {
|
|
|
|
if (!plaintext) {
|
|
|
|
window.log.warn('handleContentMessage: plaintext was falsey');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return this.innerHandleContentMessage(envelope, plaintext);
|
|
|
|
});
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
innerHandleContentMessage(envelope, plaintext) {
|
|
|
|
const content = 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) {
|
|
|
|
return this.handleNullMessage(envelope, content.nullMessage);
|
|
|
|
} else if (content.callMessage) {
|
|
|
|
return this.handleCallMessage(envelope, content.callMessage);
|
|
|
|
} 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);
|
2018-04-10 17:23:09 +00:00
|
|
|
}
|
|
|
|
this.removeFromCache(envelope);
|
|
|
|
throw new Error('Unsupported content message');
|
|
|
|
},
|
|
|
|
handleCallMessage(envelope) {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('call message from', this.getEnvelopeId(envelope));
|
2018-04-10 17:23:09 +00:00
|
|
|
this.removeFromCache(envelope);
|
|
|
|
},
|
|
|
|
handleReceiptMessage(envelope, receiptMessage) {
|
|
|
|
const results = [];
|
2018-05-02 16:51:22 +00:00
|
|
|
if (
|
|
|
|
receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY
|
|
|
|
) {
|
2018-04-10 17:23:09 +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,
|
|
|
|
sourceDevice: envelope.sourceDevice,
|
|
|
|
};
|
|
|
|
results.push(this.dispatchAndWait(ev));
|
|
|
|
}
|
2018-05-02 16:51:22 +00:00
|
|
|
} else if (
|
|
|
|
receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ
|
|
|
|
) {
|
2018-04-10 17:23:09 +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,
|
|
|
|
};
|
|
|
|
results.push(this.dispatchAndWait(ev));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Promise.all(results);
|
|
|
|
},
|
2018-11-14 19:10:32 +00:00
|
|
|
handleTypingMessage(envelope, typingMessage) {
|
|
|
|
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.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 ===
|
|
|
|
textsecure.protobuf.TypingMessage.Action.STARTED,
|
|
|
|
stopped:
|
|
|
|
typingMessage.action ===
|
|
|
|
textsecure.protobuf.TypingMessage.Action.STOPPED,
|
|
|
|
};
|
|
|
|
|
|
|
|
return this.dispatchEvent(ev);
|
|
|
|
},
|
2018-04-10 17:23:09 +00:00
|
|
|
handleNullMessage(envelope) {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('null message from', this.getEnvelopeId(envelope));
|
2018-04-10 17:23:09 +00:00
|
|
|
this.removeFromCache(envelope);
|
|
|
|
},
|
|
|
|
handleSyncMessage(envelope, syncMessage) {
|
|
|
|
if (envelope.source !== this.number) {
|
|
|
|
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;
|
|
|
|
const to = sentMessage.message.group
|
|
|
|
? `group(${sentMessage.message.group.id.toBinary()})`
|
|
|
|
: sentMessage.destination;
|
|
|
|
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info(
|
2018-04-10 17:23:09 +00:00
|
|
|
'sent message to',
|
|
|
|
to,
|
|
|
|
sentMessage.timestamp.toNumber(),
|
|
|
|
'from',
|
|
|
|
this.getEnvelopeId(envelope)
|
|
|
|
);
|
2019-05-09 15:38:05 +00:00
|
|
|
return this.handleSentMessage(envelope, sentMessage);
|
2018-04-10 17:23:09 +00:00
|
|
|
} else if (syncMessage.contacts) {
|
|
|
|
return this.handleContacts(envelope, syncMessage.contacts);
|
|
|
|
} else if (syncMessage.groups) {
|
|
|
|
return this.handleGroups(envelope, syncMessage.groups);
|
|
|
|
} else if (syncMessage.blocked) {
|
|
|
|
return this.handleBlocked(envelope, syncMessage.blocked);
|
|
|
|
} else if (syncMessage.request) {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('Got SyncMessage Request');
|
2018-04-10 17:23:09 +00:00
|
|
|
return this.removeFromCache(envelope);
|
|
|
|
} else if (syncMessage.read && syncMessage.read.length) {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('read messages from', this.getEnvelopeId(envelope));
|
2018-04-10 17:23:09 +00:00
|
|
|
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
|
|
|
|
) {
|
2019-05-16 22:32:11 +00:00
|
|
|
return this.handleStickerPackOperation(
|
|
|
|
envelope,
|
|
|
|
syncMessage.stickerPackOperation
|
|
|
|
);
|
2019-08-05 20:53:15 +00:00
|
|
|
} else if (syncMessage.viewOnceOpen) {
|
|
|
|
return this.handleViewOnceOpen(envelope, syncMessage.viewOnceOpen);
|
2018-04-10 17:23:09 +00:00
|
|
|
}
|
2019-10-04 17:30:43 +00:00
|
|
|
|
|
|
|
this.removeFromCache(envelope);
|
2018-04-10 17:23:09 +00:00
|
|
|
throw new Error('Got empty SyncMessage');
|
|
|
|
},
|
|
|
|
handleConfiguration(envelope, configuration) {
|
2018-10-18 01:01:21 +00:00
|
|
|
window.log.info('got configuration sync message');
|
2018-04-10 17:23:09 +00:00
|
|
|
const ev = new Event('configuration');
|
|
|
|
ev.confirm = this.removeFromCache.bind(this, envelope);
|
2018-10-18 01:01:21 +00:00
|
|
|
ev.configuration = configuration;
|
2018-04-10 17:23:09 +00:00
|
|
|
return this.dispatchAndWait(ev);
|
|
|
|
},
|
2019-08-05 20:53:15 +00:00
|
|
|
handleViewOnceOpen(envelope, sync) {
|
|
|
|
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.timestamp = sync.timestamp ? sync.timestamp.toNumber() : null;
|
|
|
|
|
|
|
|
return this.dispatchAndWait(ev);
|
|
|
|
},
|
2019-05-16 22:32:11 +00:00
|
|
|
handleStickerPackOperation(envelope, operations) {
|
|
|
|
const ENUM = 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);
|
|
|
|
},
|
2018-04-10 17:23:09 +00:00
|
|
|
handleVerified(envelope, verified) {
|
|
|
|
const ev = new Event('verified');
|
|
|
|
ev.confirm = this.removeFromCache.bind(this, envelope);
|
|
|
|
ev.verified = {
|
|
|
|
state: verified.state,
|
|
|
|
destination: verified.destination,
|
|
|
|
identityKey: verified.identityKey.toArrayBuffer(),
|
|
|
|
};
|
|
|
|
return this.dispatchAndWait(ev);
|
|
|
|
},
|
|
|
|
handleRead(envelope, 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,
|
|
|
|
};
|
|
|
|
results.push(this.dispatchAndWait(ev));
|
|
|
|
}
|
|
|
|
return Promise.all(results);
|
|
|
|
},
|
|
|
|
handleContacts(envelope, contacts) {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('contact sync');
|
2019-01-30 20:15:07 +00:00
|
|
|
const { blob } = contacts;
|
|
|
|
|
2019-10-04 17:30:43 +00:00
|
|
|
this.removeFromCache(envelope);
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
// 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(attachmentPointer => {
|
2018-04-10 17:23:09 +00:00
|
|
|
const results = [];
|
|
|
|
const contactBuffer = new ContactBuffer(attachmentPointer.data);
|
|
|
|
let contactDetails = contactBuffer.next();
|
|
|
|
while (contactDetails !== undefined) {
|
|
|
|
const ev = new Event('contact');
|
|
|
|
ev.contactDetails = contactDetails;
|
|
|
|
results.push(this.dispatchAndWait(ev));
|
|
|
|
|
|
|
|
contactDetails = contactBuffer.next();
|
|
|
|
}
|
|
|
|
|
|
|
|
const ev = new Event('contactsync');
|
|
|
|
results.push(this.dispatchAndWait(ev));
|
|
|
|
|
2018-08-15 18:20:53 +00:00
|
|
|
return Promise.all(results).then(() => {
|
|
|
|
window.log.info('handleContacts: finished');
|
|
|
|
});
|
2018-04-10 17:23:09 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
handleGroups(envelope, groups) {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('group sync');
|
2019-01-30 20:15:07 +00:00
|
|
|
const { blob } = groups;
|
|
|
|
|
2019-10-04 17:30:43 +00:00
|
|
|
this.removeFromCache(envelope);
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
// 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(attachmentPointer => {
|
2018-04-10 17:23:09 +00:00
|
|
|
const groupBuffer = new GroupBuffer(attachmentPointer.data);
|
|
|
|
let groupDetails = groupBuffer.next();
|
|
|
|
const promises = [];
|
|
|
|
while (groupDetails !== undefined) {
|
2019-02-11 23:59:21 +00:00
|
|
|
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);
|
|
|
|
});
|
2018-04-10 17:23:09 +00:00
|
|
|
groupDetails = groupBuffer.next();
|
|
|
|
promises.push(promise);
|
|
|
|
}
|
2016-07-18 22:48:09 +00:00
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
Promise.all(promises).then(() => {
|
|
|
|
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);
|
2018-04-10 17:23:09 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
handleBlocked(envelope, blocked) {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('Setting these numbers as blocked:', blocked.numbers);
|
2018-04-10 17:23:09 +00:00
|
|
|
textsecure.storage.put('blocked', blocked.numbers);
|
2018-09-13 19:57:07 +00:00
|
|
|
|
|
|
|
const groupIds = _.map(blocked.groupIds, groupId => groupId.toBinary());
|
|
|
|
window.log.info(
|
|
|
|
'Setting these groups as blocked:',
|
|
|
|
groupIds.map(groupId => `group(${groupId})`)
|
|
|
|
);
|
|
|
|
textsecure.storage.put('blocked-groups', groupIds);
|
|
|
|
|
|
|
|
return this.removeFromCache(envelope);
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
isBlocked(number) {
|
|
|
|
return textsecure.storage.get('blocked', []).indexOf(number) >= 0;
|
|
|
|
},
|
2018-09-13 19:57:07 +00:00
|
|
|
isGroupBlocked(groupId) {
|
|
|
|
return textsecure.storage.get('blocked-groups', []).indexOf(groupId) >= 0;
|
|
|
|
},
|
2019-01-30 20:15:07 +00:00
|
|
|
cleanAttachment(attachment) {
|
|
|
|
return {
|
|
|
|
..._.omit(attachment, 'thumbnail'),
|
|
|
|
id: attachment.id.toString(),
|
|
|
|
key: attachment.key ? attachment.key.toString('base64') : null,
|
|
|
|
digest: attachment.digest ? attachment.digest.toString('base64') : null,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
async downloadAttachment(attachment) {
|
|
|
|
const encrypted = await this.server.getAttachment(attachment.id);
|
2019-02-15 21:43:22 +00:00
|
|
|
const { key, digest, size } = attachment;
|
2019-01-30 20:15:07 +00:00
|
|
|
|
2019-05-16 22:32:11 +00:00
|
|
|
if (!digest) {
|
|
|
|
throw new Error('Failure: Ask sender to update Signal and resend.');
|
|
|
|
}
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
const data = await textsecure.crypto.decryptAttachment(
|
|
|
|
encrypted,
|
|
|
|
window.Signal.Crypto.base64ToArrayBuffer(key),
|
|
|
|
window.Signal.Crypto.base64ToArrayBuffer(digest)
|
|
|
|
);
|
2017-07-17 22:46:00 +00:00
|
|
|
|
2019-07-24 16:14:59 +00:00
|
|
|
if (!_.isNumber(size)) {
|
2019-02-15 21:43:22 +00:00
|
|
|
throw new Error(
|
2019-05-08 20:14:52 +00:00
|
|
|
`downloadAttachment: Size was not provided, actual size was ${
|
2019-02-15 21:43:22 +00:00
|
|
|
data.byteLength
|
|
|
|
}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-05-08 20:14:52 +00:00
|
|
|
const typedArray = window.Signal.Crypto.getFirstBytes(data, size);
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
return {
|
|
|
|
..._.omit(attachment, 'digest', 'key'),
|
2019-05-08 20:14:52 +00:00
|
|
|
data: window.Signal.Crypto.typedArrayToArrayBuffer(typedArray),
|
2019-01-30 20:15:07 +00:00
|
|
|
};
|
|
|
|
},
|
|
|
|
handleAttachment(attachment) {
|
|
|
|
const cleaned = this.cleanAttachment(attachment);
|
|
|
|
return this.downloadAttachment(cleaned);
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
|
|
|
async handleEndSession(number) {
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('got end session');
|
2018-04-10 17:23:09 +00:00
|
|
|
const deviceIds = await textsecure.storage.protocol.getDeviceIds(number);
|
|
|
|
|
2018-05-02 16:51:22 +00:00
|
|
|
return Promise.all(
|
|
|
|
deviceIds.map(deviceId => {
|
|
|
|
const address = new libsignal.SignalProtocolAddress(number, deviceId);
|
|
|
|
const sessionCipher = new libsignal.SessionCipher(
|
|
|
|
textsecure.storage.protocol,
|
|
|
|
address
|
|
|
|
);
|
2018-04-10 17:23:09 +00:00
|
|
|
|
2018-07-21 19:00:08 +00:00
|
|
|
window.log.info('deleting sessions for', address.toString());
|
2018-05-02 16:51:22 +00:00
|
|
|
return sessionCipher.deleteAllSessionsForDevice();
|
|
|
|
})
|
|
|
|
);
|
2018-04-10 17:23:09 +00:00
|
|
|
},
|
2019-02-11 23:59:21 +00:00
|
|
|
processDecrypted(envelope, decrypted) {
|
2018-04-10 17:23:09 +00:00
|
|
|
/* eslint-disable no-bitwise, no-param-reassign */
|
|
|
|
const FLAGS = 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.
|
|
|
|
|
2019-05-16 22:14:06 +00:00
|
|
|
if (window.TIMESTAMP_VALIDATION) {
|
2019-05-29 21:33:50 +00:00
|
|
|
if (!envelope.timestamp || !decrypted.timestamp) {
|
|
|
|
throw new Error('Missing timestamp on dataMessage or envelope');
|
|
|
|
}
|
|
|
|
|
2019-05-16 22:14:06 +00:00
|
|
|
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
|
|
|
|
}`
|
|
|
|
);
|
|
|
|
}
|
2019-05-14 19:14:29 +00:00
|
|
|
}
|
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
if (decrypted.flags == null) {
|
|
|
|
decrypted.flags = 0;
|
|
|
|
}
|
|
|
|
if (decrypted.expireTimer == null) {
|
|
|
|
decrypted.expireTimer = 0;
|
|
|
|
}
|
2017-08-12 00:16:22 +00:00
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
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');
|
|
|
|
}
|
2015-09-26 00:25:25 +00:00
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
const promises = [];
|
2017-08-12 00:16:22 +00:00
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
if (decrypted.group !== null) {
|
|
|
|
decrypted.group.id = decrypted.group.id.toBinary();
|
2017-08-12 00:16:22 +00:00
|
|
|
|
2019-02-11 23:59:21 +00:00
|
|
|
switch (decrypted.group.type) {
|
|
|
|
case textsecure.protobuf.GroupContext.Type.UPDATE:
|
|
|
|
decrypted.body = null;
|
|
|
|
decrypted.attachments = [];
|
|
|
|
break;
|
|
|
|
case textsecure.protobuf.GroupContext.Type.QUIT:
|
|
|
|
decrypted.body = null;
|
|
|
|
decrypted.attachments = [];
|
|
|
|
break;
|
|
|
|
case textsecure.protobuf.GroupContext.Type.DELIVER:
|
|
|
|
decrypted.group.name = null;
|
|
|
|
decrypted.group.members = [];
|
|
|
|
decrypted.group.avatar = null;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
this.removeFromCache(envelope);
|
|
|
|
throw new Error('Unknown group message type');
|
|
|
|
}
|
2018-04-10 17:23:09 +00:00
|
|
|
}
|
2015-09-01 23:59:06 +00:00
|
|
|
|
2018-11-14 18:47:19 +00:00
|
|
|
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}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
// Here we go from binary to string/base64 in all AttachmentPointer digest/key fields
|
2018-04-10 22:09:29 +00:00
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
if (
|
|
|
|
decrypted.group &&
|
|
|
|
decrypted.group.type === textsecure.protobuf.GroupContext.Type.UPDATE
|
|
|
|
) {
|
|
|
|
if (decrypted.group.avatar !== null) {
|
|
|
|
decrypted.group.avatar = this.cleanAttachment(decrypted.group.avatar);
|
2019-01-16 03:03:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
decrypted.attachments = (decrypted.attachments || []).map(
|
|
|
|
this.cleanAttachment.bind(this)
|
|
|
|
);
|
|
|
|
decrypted.preview = (decrypted.preview || []).map(item => {
|
|
|
|
const { image } = item;
|
|
|
|
|
|
|
|
if (!image) {
|
|
|
|
return item;
|
2018-05-08 23:50:57 +00:00
|
|
|
}
|
2019-01-30 20:15:07 +00:00
|
|
|
|
|
|
|
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),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
});
|
2018-04-27 16:32:31 +00:00
|
|
|
|
2018-04-12 23:58:21 +00:00
|
|
|
if (decrypted.quote && decrypted.quote.id) {
|
|
|
|
decrypted.quote.id = decrypted.quote.id.toNumber();
|
|
|
|
}
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
if (decrypted.quote) {
|
|
|
|
decrypted.quote.attachments = (decrypted.quote.attachments || []).map(
|
|
|
|
item => {
|
|
|
|
const { thumbnail } = item;
|
|
|
|
|
|
|
|
if (!thumbnail) {
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...item,
|
|
|
|
thumbnail: this.cleanAttachment(item.thumbnail),
|
|
|
|
};
|
2018-04-10 22:09:29 +00:00
|
|
|
}
|
2019-01-30 20:15:07 +00:00
|
|
|
);
|
2018-04-10 22:09:29 +00:00
|
|
|
}
|
|
|
|
|
2019-05-16 22:32:11 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
return Promise.all(promises).then(() => decrypted);
|
|
|
|
/* eslint-enable no-bitwise, no-param-reassign */
|
|
|
|
},
|
2016-01-14 22:07:00 +00:00
|
|
|
});
|
2015-04-29 18:56:40 +00:00
|
|
|
|
2015-09-01 23:59:06 +00:00
|
|
|
window.textsecure = window.textsecure || {};
|
2015-09-01 19:13:38 +00:00
|
|
|
|
2018-04-10 23:53:01 +00:00
|
|
|
textsecure.MessageReceiver = function MessageReceiverWrapper(
|
|
|
|
username,
|
|
|
|
password,
|
|
|
|
signalingKey,
|
|
|
|
options
|
|
|
|
) {
|
2018-04-10 17:23:09 +00:00
|
|
|
const messageReceiver = new MessageReceiver(
|
|
|
|
username,
|
|
|
|
password,
|
|
|
|
signalingKey,
|
|
|
|
options
|
|
|
|
);
|
2018-05-02 16:51:22 +00:00
|
|
|
this.addEventListener = messageReceiver.addEventListener.bind(
|
|
|
|
messageReceiver
|
|
|
|
);
|
|
|
|
this.removeEventListener = messageReceiver.removeEventListener.bind(
|
|
|
|
messageReceiver
|
|
|
|
);
|
2018-04-10 17:23:09 +00:00
|
|
|
this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
|
|
|
|
this.close = messageReceiver.close.bind(messageReceiver);
|
2019-01-30 20:15:07 +00:00
|
|
|
|
|
|
|
this.downloadAttachment = messageReceiver.downloadAttachment.bind(
|
|
|
|
messageReceiver
|
|
|
|
);
|
2019-02-19 20:10:26 +00:00
|
|
|
this.stopProcessing = messageReceiver.stopProcessing.bind(messageReceiver);
|
2019-09-26 19:56:31 +00:00
|
|
|
this.unregisterBatchers = messageReceiver.unregisterBatchers.bind(
|
|
|
|
messageReceiver
|
|
|
|
);
|
2019-01-30 20:15:07 +00:00
|
|
|
|
2018-04-10 17:23:09 +00:00
|
|
|
messageReceiver.connect();
|
2015-09-01 23:59:06 +00:00
|
|
|
};
|
2015-04-29 18:56:40 +00:00
|
|
|
|
2015-09-01 23:59:06 +00:00
|
|
|
textsecure.MessageReceiver.prototype = {
|
2018-04-10 17:23:09 +00:00
|
|
|
constructor: textsecure.MessageReceiver,
|
2015-09-01 23:59:06 +00:00
|
|
|
};
|
2018-08-07 15:17:51 +00:00
|
|
|
|
|
|
|
textsecure.MessageReceiver.stringToArrayBuffer =
|
|
|
|
MessageReceiver.stringToArrayBuffer;
|
|
|
|
textsecure.MessageReceiver.arrayBufferToString =
|
|
|
|
MessageReceiver.arrayBufferToString;
|
2018-08-10 00:28:51 +00:00
|
|
|
textsecure.MessageReceiver.stringToArrayBufferBase64 =
|
|
|
|
MessageReceiver.stringToArrayBufferBase64;
|
|
|
|
textsecure.MessageReceiver.arrayBufferToStringBase64 =
|
|
|
|
MessageReceiver.arrayBufferToStringBase64;
|