You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/libtextsecure/message_receiver.js

1489 lines
48 KiB
JavaScript

/* global window: false */
/* global callWorker: false */
/* global textsecure: false */
/* global StringView: false */
/* global libloki: false */
/* global libsignal: false */
/* global WebSocket: false */
/* global Event: false */
/* global dcodeIO: false */
/* global _: false */
/* global HttpResource: false */
/* global ContactBuffer: false */
/* global GroupBuffer: false */
/* global lokiPublicChatAPI: false */
/* global lokiMessageAPI: false */
6 years ago
/* global feeds: false */
/* global Whisper: false */
/* global WebAPI: false */
/* global ConversationController: false */
/* global log: false */
/* global libsession: false */
/* eslint-disable more/no-then */
/* eslint-disable no-unreachable */
let openGroupBound = false;
// TODO: remove this when no longer needed here
function getEnvelopeId(envelope) {
if (envelope.source) {
return `${envelope.source}.${
envelope.sourceDevice
} ${envelope.timestamp.toNumber()} (${envelope.id})`;
}
return envelope.id;
}
function MessageReceiver(username, password, signalingKey, options = {}) {
this.count = 0;
this.signalingKey = signalingKey;
this.username = username;
this.password = password;
this.server = WebAPI.connect();
const address = libsignal.SignalProtocolAddress.fromString(username);
this.number = address.getName();
this.deviceId = address.getDeviceId();
this.pending = Promise.resolve();
if (options.retryCached) {
this.pending = this.queueAllCached();
}
// only do this once to prevent duplicates
if (lokiPublicChatAPI) {
window.log.info('Binding open group events handler', openGroupBound);
if (!openGroupBound) {
// clear any previous binding
lokiPublicChatAPI.removeAllListeners('publicMessage');
// we only need one MR in the system handling these
// bind events
lokiPublicChatAPI.on(
'publicMessage',
this.handleUnencryptedMessage.bind(this)
);
openGroupBound = true;
}
} else {
window.log.warn('Can not handle open group data, API is not available');
}
}
MessageReceiver.stringToArrayBuffer = string =>
Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer());
MessageReceiver.arrayBufferToString = arrayBuffer =>
Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary'));
MessageReceiver.stringToArrayBufferBase64 = string =>
callWorker('stringToArrayBufferBase64', string);
MessageReceiver.arrayBufferToStringBase64 = arrayBuffer =>
callWorker('arrayBufferToStringBase64', arrayBuffer);
MessageReceiver.prototype = new textsecure.EventTarget();
MessageReceiver.prototype.extend({
constructor: MessageReceiver,
connect() {
if (this.calledClose) {
return;
}
this.count = 0;
if (this.hasConnected) {
const ev = new Event('reconnect');
this.dispatchEvent(ev);
}
this.hasConnected = true;
this.httpPollingResource = new HttpResource(lokiMessageAPI, {
handleRequest: this.handleRequest.bind(this),
});
this.httpPollingResource.pollServer();
// start polling all open group rooms you have registered
// if not registered yet, they'll get started when they're created
if (lokiPublicChatAPI) {
lokiPublicChatAPI.open();
}
6 years ago
// set up pollers for any RSS feeds
feeds.forEach(feed => {
feed.on('rssMessage', this.handleUnencryptedMessage.bind(this));
6 years ago
});
// Ensures that an immediate 'empty' event from the websocket will fire only after
// all cached envelopes are processed.
this.incoming = [this.pending];
},
async handleUnencryptedMessage({ message }) {
const isMe = message.source === textsecure.storage.user.getNumber();
if (!isMe && message.message.profile) {
const conversation = await window.ConversationController.getOrCreateAndWait(
message.source,
'private'
);
await window.NewReceiver.updateProfile(
conversation,
message.message.profile,
message.message.profileKey
);
}
const ourNumber = textsecure.storage.user.getNumber();
const primaryDevice = window.storage.get('primaryDevicePubKey');
const isOurDevice =
message.source &&
(message.source === ourNumber || message.source === primaryDevice);
const isPublicChatMessage =
message.message.group &&
message.message.group.id &&
!!message.message.group.id.match(/^publicChat:/);
let ev;
if (isPublicChatMessage && isOurDevice) {
// Public chat messages from ourselves should be outgoing
ev = new Event('sent');
} else {
ev = new Event('message');
}
ev.confirm = function confirmTerm() {};
ev.data = message;
this.dispatchAndWait(ev);
},
stopProcessing() {
window.log.info('MessageReceiver: stopProcessing requested');
this.stoppingProcessing = true;
return this.close();
},
shutdown() {},
async close() {
window.log.info('MessageReceiver.close()');
this.calledClose = true;
// stop polling all open group rooms
if (lokiPublicChatAPI) {
await lokiPublicChatAPI.close();
}
if (this.httpPollingResource) {
this.httpPollingResource.close();
}
return this.drain();
},
onopen() {
window.log.info('websocket open');
},
onerror() {
window.log.error('websocket error');
},
dispatchAndWait(event) {
const promise = this.appPromise || Promise.resolve();
const appJobPromise = Promise.all(this.dispatchEvent(event));
const job = () => appJobPromise;
this.appPromise = promise.then(job, job);
return Promise.resolve();
},
onclose(ev) {
window.log.info(
'websocket closed',
ev.code,
ev.reason || '',
'calledClose:',
this.calledClose
);
},
pollForAdditionalId(id) {
this.httpPollingResource.pollForAdditionalId(id);
},
handleRequest(request, options) {
this.incoming = this.incoming || [];
const lastPromise = _.last(this.incoming);
// 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 && request.path !== '/api/v1/message') {
window.log.info('got request', request, request.path);
request.respond(200, 'OK');
if (request.verb === 'PUT' && request.path === '/api/v1/queue/empty') {
this.onEmpty();
}
return;
}
const promise = Promise.resolve(request.body.toArrayBuffer()) // textsecure.crypto
.then(plaintextImmutable => {
let plaintext = plaintextImmutable;
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
// The message is for a medium size group
if (options.conversationId) {
const ourNumber = textsecure.storage.user.getNumber();
const senderIdentity = envelope.source;
if (senderIdentity === ourNumber) {
// Ignoring our own message
return request.respond(200, 'OK');
}
// Sender identity will be lost if we load from cache, because
// plaintext (and protobuf.Envelope) does not have that field...
envelope.source = options.conversationId;
plaintext = textsecure.protobuf.Envelope.encode(
envelope
).toArrayBuffer();
envelope.senderIdentity = senderIdentity;
}
if (this.isBlocked(envelope.source)) {
return request.respond(200, 'OK');
}
envelope.id = envelope.serverGuid || window.getGuid();
envelope.serverTimestamp = envelope.serverTimestamp
? envelope.serverTimestamp.toNumber()
: null;
return this.addToCache(envelope, plaintext).then(
async () => {
request.respond(200, 'OK');
// To ensure that we queue in the same order we receive messages
await lastPromise;
this.queueEnvelope(envelope);
},
error => {
request.respond(500, 'Failed to cache message');
window.log.error(
'handleRequest error trying to add message to cache:',
error && error.stack ? error.stack : error
);
}
);
})
.catch(e => {
request.respond(500, 'Bad encrypted websocket message');
window.log.error(
'Error handling incoming message:',
e && e.stack ? e.stack : e
);
const ev = new Event('error');
ev.error = e;
return this.dispatchAndWait(ev);
});
this.incoming.push(promise);
},
addToQueue(task) {
this.count += 1;
this.pending = this.pending.then(task, task);
const { count, pending } = this;
const cleanup = () => {
this.updateProgress(count);
// We want to clear out the promise chain whenever possible because it could
// lead to large memory usage over time:
// https://github.com/nodejs/node/issues/6673#issuecomment-244331609
if (this.pending === pending) {
this.pending = Promise.resolve();
}
};
pending.then(cleanup, cleanup);
return pending;
},
onEmpty() {
const { incoming } = this;
this.incoming = [];
const emitEmpty = () => {
window.log.info("MessageReceiver: emitting 'empty' event");
const ev = new Event('empty');
this.dispatchAndWait(ev);
};
const waitForApplication = async () => {
window.log.info(
"MessageReceiver: finished processing messages after 'empty', now waiting for application"
);
const promise = this.appPromise || Promise.resolve();
this.appPromise = Promise.resolve();
// We don't await here because we don't this to gate future message processing
promise.then(emitEmpty, emitEmpty);
};
const waitForEmptyQueue = () => {
// resetting count to zero so everything queued after this starts over again
this.count = 0;
this.addToQueue(waitForApplication);
};
// We first wait for all recently-received messages (this.incoming) to be queued,
// then we queue a task to wait for the application to finish its processing, then
// finally we emit the 'empty' event to the queue.
Promise.all(incoming).then(waitForEmptyQueue, waitForEmptyQueue);
},
drain() {
const { incoming } = this;
this.incoming = [];
const queueDispatch = () =>
this.addToQueue(() => {
window.log.info('drained');
});
// This promise will resolve when there are no more messages to be processed.
return Promise.all(incoming).then(queueDispatch, queueDispatch);
},
updateProgress(count) {
// 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();
for (let i = 0, max = items.length; i < max; i += 1) {
// eslint-disable-next-line no-await-in-loop
await this.queueCached(items[i]);
}
},
async queueCached(item) {
try {
let envelopePlaintext = item.envelope;
if (item.version === 2) {
envelopePlaintext = await MessageReceiver.stringToArrayBufferBase64(
envelopePlaintext
);
}
if (typeof envelopePlaintext === 'string') {
envelopePlaintext = await MessageReceiver.stringToArrayBuffer(
envelopePlaintext
);
}
const envelope = textsecure.protobuf.Envelope.decode(envelopePlaintext);
envelope.id = envelope.serverGuid || item.id;
envelope.source = envelope.source || item.source;
envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice;
envelope.senderIdentity = envelope.senderIdentity || item.senderIdentity;
envelope.serverTimestamp =
envelope.serverTimestamp || item.serverTimestamp;
6 years ago
envelope.preKeyBundleMessage =
envelope.preKeyBundleMessage || item.preKeyBundleMessage;
const { decrypted } = item;
if (decrypted) {
let payloadPlaintext = decrypted;
if (item.version === 2) {
payloadPlaintext = await MessageReceiver.stringToArrayBufferBase64(
payloadPlaintext
);
}
if (typeof payloadPlaintext === 'string') {
payloadPlaintext = await MessageReceiver.stringToArrayBuffer(
payloadPlaintext
);
}
// Convert preKeys to array buffer
if (typeof envelope.preKeyBundleMessage === 'string') {
envelope.preKeyBundleMessage = await MessageReceiver.stringToArrayBuffer(
envelope.preKeyBundleMessage
);
}
this.queueDecryptedEnvelope(envelope, payloadPlaintext);
} else {
this.queueEnvelope(envelope);
}
} catch (error) {
window.log.error(
'queueCached error handling item',
item.id,
'removing it. Error:',
error && error.stack ? error.stack : error
);
try {
const { id } = item;
await textsecure.storage.unprocessed.remove(id);
} catch (deleteError) {
window.log.error(
'queueCached error deleting item',
item.id,
'Error:',
deleteError && deleteError.stack ? deleteError.stack : deleteError
);
}
}
},
async getAllFromCache() {
window.log.info('getAllFromCache');
const count = await textsecure.storage.unprocessed.getCount();
if (count > 1500) {
await textsecure.storage.unprocessed.removeAll();
window.log.warn(
`There were ${count} messages in cache. Deleted all instead of reprocessing`
);
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);
try {
if (attempts >= 3) {
window.log.warn(
'getAllFromCache final attempt for envelope',
item.id
);
await textsecure.storage.unprocessed.remove(item.id);
} else {
await textsecure.storage.unprocessed.updateAttempts(
item.id,
attempts
);
}
} catch (error) {
window.log.error(
'getAllFromCache error updating item after load:',
error && error.stack ? error.stack : error
);
}
return item;
})
);
},
async addToCache(envelope, plaintext) {
const { id } = envelope;
const data = {
id,
version: 2,
envelope: await MessageReceiver.arrayBufferToStringBase64(plaintext),
timestamp: Date.now(),
attempts: 1,
};
if (envelope.senderIdentity) {
data.senderIdentity = envelope.senderIdentity;
}
return textsecure.storage.unprocessed.add(data);
},
async updateCache(envelope, plaintext) {
const { id } = envelope;
const item = await textsecure.storage.unprocessed.get(id);
if (!item) {
window.log.error(
`updateCache: Didn't find item ${id} in cache to update`
);
return null;
}
item.source = envelope.source;
item.sourceDevice = envelope.sourceDevice;
item.serverTimestamp = envelope.serverTimestamp;
// For medium-size closed groups
if (envelope.senderIdentity) {
item.senderIdentity = envelope.senderIdentity;
}
if (item.version === 2) {
item.decrypted = await MessageReceiver.arrayBufferToStringBase64(
plaintext
);
} else {
item.decrypted = await MessageReceiver.arrayBufferToString(plaintext);
}
return textsecure.storage.unprocessed.addDecryptedData(item.id, item);
},
removeFromCache(envelope) {
const { id } = envelope;
return textsecure.storage.unprocessed.remove(id);
},
queueDecryptedEnvelope(envelope, plaintext) {
const id = getEnvelopeId(envelope);
window.log.info('queueing decrypted envelope', id);
const task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext);
const taskWithTimeout = textsecure.createTaskWithTimeout(
task,
`queueEncryptedEnvelope ${id}`
);
const promise = this.addToQueue(taskWithTimeout);
return promise.catch(error => {
window.log.error(
`queueDecryptedEnvelope error handling envelope ${id}:`,
error && error.stack ? error.stack : error
);
});
},
queueEnvelope(envelope) {
const id = getEnvelopeId(envelope);
window.log.info('queueing envelope', id);
const task = this.handleEnvelope.bind(this, envelope);
const taskWithTimeout = textsecure.createTaskWithTimeout(
task,
`queueEnvelope ${id}`
);
const promise = this.addToQueue(taskWithTimeout);
return promise.catch(error => {
window.log.error(
'queueEnvelope error handling envelope',
id,
':',
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) {
if (this.stoppingProcessing) {
return Promise.resolve();
}
// 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) {
if (this.stoppingProcessing) {
return Promise.resolve();
}
if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) {
return this.onDeliveryReceipt(envelope);
}
if (envelope.content) {
return this.handleContentMessage(envelope);
}
this.removeFromCache(envelope);
throw new Error('Received message with no content and no legacyMessage');
},
getStatus() {
if (this.httpPollingResource) {
6 years ago
return this.httpPollingResource.isConnected()
? WebSocket.OPEN
: WebSocket.CLOSED;
}
if (this.hasConnected) {
return WebSocket.CLOSED;
}
return -1;
},
onDeliveryReceipt(envelope) {
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,
sourceDevice: envelope.sourceDevice,
};
this.dispatchAndWait(ev).then(resolve, reject);
});
},
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');
}
}
return plaintext;
},
async postDecrypt(envelope, plaintext) {
const { isMe, isBlocked } = plaintext || {};
if (isMe || isBlocked) {
this.removeFromCache(envelope);
return null;
}
this.updateCache(envelope, plaintext).catch(error => {
window.log.error(
'decrypt failed to save decrypted message contents to cache:',
error && error.stack ? error.stack : error
);
});
return plaintext;
},
async decryptForMediumGroup(envelope, ciphertextObj) {
const groupId = envelope.source;
const identity = await window.Signal.Data.getIdentityKeyById(groupId);
const secretKeyHex = identity.secretKey;
if (!secretKeyHex) {
throw new Error(`Secret key is empty for group ${groupId}!`);
}
const { senderIdentity } = envelope;
const {
ciphertext: outerCiphertext,
ephemeralKey,
} = textsecure.protobuf.MediumGroupContent.decode(ciphertextObj);
const ephemKey = ephemeralKey.toArrayBuffer();
const secretKey = dcodeIO.ByteBuffer.wrap(
secretKeyHex,
'hex'
).toArrayBuffer();
const mediumGroupCiphertext = await libloki.crypto.decryptForPubkey(
secretKey,
ephemKey,
outerCiphertext.toArrayBuffer()
);
const {
ciphertext,
keyIdx,
} = textsecure.protobuf.MediumGroupCiphertext.decode(mediumGroupCiphertext);
const plaintext = await window.SenderKeyAPI.decryptWithSenderKey(
ciphertext.toArrayBuffer(),
keyIdx,
groupId,
senderIdentity
);
return plaintext;
},
async decrypt(envelope, ciphertext) {
let promise;
const ourNumber = textsecure.storage.user.getNumber();
const me = {
number: ourNumber,
deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10),
};
// Envelope.source will be null on UNIDENTIFIED_SENDER
// Don't use it there!
const address = new libsignal.SignalProtocolAddress(
envelope.source,
envelope.sourceDevice
);
const lokiSessionCipher = new libloki.crypto.LokiSessionCipher(
textsecure.storage.protocol,
address
);
switch (envelope.type) {
case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
window.log.info('message from', getEnvelopeId(envelope));
promise = lokiSessionCipher
.decryptWhisperMessage(ciphertext)
.then(this.unpad);
break;
case textsecure.protobuf.Envelope.Type.MEDIUM_GROUP_CIPHERTEXT:
promise = this.decryptForMediumGroup(envelope, ciphertext);
break;
case textsecure.protobuf.Envelope.Type.SESSION_REQUEST: {
window.log.info('session-request message from ', envelope.source);
const fallBackSessionCipher = new libloki.crypto.FallBackSessionCipher(
address
);
6 years ago
promise = fallBackSessionCipher
.decrypt(ciphertext.toArrayBuffer())
.then(this.unpad);
break;
}
case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
window.log.info('prekey message from', getEnvelopeId(envelope));
promise = this.decryptPreKeyWhisperMessage(
ciphertext,
lokiSessionCipher,
address
);
break;
5 years ago
case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: {
window.log.info('received unidentified sender message');
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
textsecure.storage.protocol
);
promise = secretSessionCipher
.decrypt(ciphertext.toArrayBuffer(), me)
.then(
result => {
const { isMe, sender, content, type } = result;
// We need to drop incoming messages from ourself since server can't
// do it for us
if (isMe) {
return { isMe: true };
}
// We might have substituted the type based on decrypted content
if (type === textsecure.protobuf.Envelope.Type.SESSION_REQUEST) {
// eslint-disable-next-line no-param-reassign
envelope.type =
textsecure.protobuf.Envelope.Type.SESSION_REQUEST;
}
if (this.isBlocked(sender.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;
// 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
envelope.unidentifiedDeliveryReceived = !originalSource;
// 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) {
const originalSource = envelope.source;
if (this.isBlocked(sender.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.getName();
// eslint-disable-next-line no-param-reassign
envelope.sourceDevice = sender.getDeviceId();
// eslint-disable-next-line no-param-reassign
envelope.unidentifiedDeliveryReceived = !originalSource;
throw error;
}
return this.removeFromCache(envelope).then(() => {
throw error;
});
}
);
break;
5 years ago
}
default:
promise = Promise.reject(new Error('Unknown message type'));
}
return promise
.then(plaintext => this.postDecrypt(envelope, plaintext))
.catch(error => {
if (error && error instanceof textsecure.SenderKeyMissing) {
const groupId = envelope.source;
const { senderIdentity } = error;
log.info(
'Requesting missing key for identity: ',
senderIdentity,
'groupId: ',
groupId
);
textsecure.messaging.requestSenderKeys(senderIdentity, groupId);
return;
}
let errorToThrow = error;
const noSession =
error &&
(error.message.indexOf('No record for device') === 0 ||
5 years ago
error.message.indexOf('decryptWithSessionList: list is empty') ===
0);
if (error && error.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);
errorToThrow = new textsecure.IncomingIdentityKeyError(
address.toString(),
buffer.toArrayBuffer(),
error.identityKey
);
} else if (!noSession) {
// We want to handle "no-session" error, not re-throw it
throw error;
}
const ev = new Event('error');
ev.error = errorToThrow;
ev.proto = envelope;
ev.confirm = this.removeFromCache.bind(this, envelope);
const returnError = () => Promise.reject(errorToThrow);
this.dispatchAndWait(ev).then(returnError, returnError);
});
},
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;
}
},
// handle a SYNC message for a message
// sent by another device
async handleSentMessage(envelope, sentContainer, msg) {
const {
destination,
timestamp,
expirationStartTimestamp,
unidentifiedStatus,
} = sentContainer;
// eslint-disable-next-line no-bitwise
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
await window.NewReceiver.handleEndSession(destination);
}
if (msg.mediumGroupUpdate) {
await window.NewReceiver.handleMediumGroupUpdate(
envelope,
msg.mediumGroupUpdate
);
}
const message = await window.NewReceiver.processDecrypted(envelope, msg);
const primaryDevicePubKey = window.storage.get('primaryDevicePubKey');
// handle profileKey and avatar updates
if (envelope.source === primaryDevicePubKey) {
const { profileKey, profile } = message;
const primaryConversation = ConversationController.get(
primaryDevicePubKey
);
if (profile) {
window.NewReceiver.updateProfile(
primaryConversation,
profile,
profileKey
);
}
}
const ev = new Event('sent');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
destination,
timestamp: timestamp.toNumber(),
device: envelope.sourceDevice,
unidentifiedStatus,
message,
};
if (expirationStartTimestamp) {
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
}
this.dispatchAndWait(ev);
},
5 years ago
async handleLokiAddressMessage(envelope) {
window.log.warn('Ignoring a Loki address message');
return this.removeFromCache(envelope);
},
async handlePairingRequest(envelope, pairingRequest) {
const valid = await libloki.crypto.validateAuthorisation(pairingRequest);
if (valid) {
6 years ago
// Pairing dialog is open and is listening
if (Whisper.events.isListenedTo('devicePairingRequestReceived')) {
await window.libsession.Protocols.MultiDeviceProtocol.savePairingAuthorisation(
pairingRequest
);
6 years ago
Whisper.events.trigger(
'devicePairingRequestReceived',
pairingRequest.secondaryDevicePubKey
);
} else {
Whisper.events.trigger(
'devicePairingRequestReceivedNoListener',
pairingRequest.secondaryDevicePubKey
);
6 years ago
}
// Ignore requests if the dialog is closed
}
return this.removeFromCache(envelope);
},
async handleAuthorisationForSelf(
envelope,
pairingAuthorisation,
{ dataMessage, syncMessage }
) {
6 years ago
const valid = await libloki.crypto.validateAuthorisation(
pairingAuthorisation
);
const alreadySecondaryDevice = !!window.storage.get('isSecondaryDevice');
let removedFromCache = false;
if (alreadySecondaryDevice) {
window.log.warn(
'Received an unexpected pairing authorisation (device is already paired as secondary device). Ignoring.'
);
} else if (!valid) {
window.log.warn(
'Received invalid pairing authorisation for self. Could not verify signature. Ignoring.'
);
} else {
const { primaryDevicePubKey, grantSignature } = pairingAuthorisation;
if (grantSignature) {
// Authorisation received to become a secondary device
window.log.info(
`Received pairing authorisation from ${primaryDevicePubKey}`
);
// Set current device as secondary.
// This will ensure the authorisation is sent
// along with each session request.
window.storage.remove('secondaryDeviceStatus');
window.storage.put('isSecondaryDevice', true);
window.storage.put('primaryDevicePubKey', primaryDevicePubKey);
await window.libsession.Protocols.MultiDeviceProtocol.savePairingAuthorisation(
pairingAuthorisation
);
const primaryConversation = await ConversationController.getOrCreateAndWait(
primaryDevicePubKey,
'private'
);
primaryConversation.trigger('change');
Whisper.events.trigger('secondaryDeviceRegistration');
// Update profile
if (dataMessage) {
const { profile, profileKey } = dataMessage;
const ourNumber = window.storage.get('primaryDevicePubKey');
const me = window.ConversationController.get(ourNumber);
if (me) {
window.NewReceiver.updateProfile(me, profile, profileKey);
}
}
// Update contact list
if (syncMessage && syncMessage.contacts) {
// This call already removes the envelope from the cache
await this.handleContacts(envelope, syncMessage.contacts);
removedFromCache = true;
}
} else {
window.log.warn('Unimplemented pairing authorisation message type');
}
}
if (!removedFromCache) {
await this.removeFromCache(envelope);
}
},
async handlePairingAuthorisationMessage(envelope, content) {
const { pairingAuthorisation } = content;
const { secondaryDevicePubKey, grantSignature } = pairingAuthorisation;
const isGrant =
grantSignature &&
secondaryDevicePubKey === textsecure.storage.user.getNumber();
if (isGrant) {
return this.handleAuthorisationForSelf(
envelope,
pairingAuthorisation,
content
);
}
return this.handlePairingRequest(envelope, pairingAuthorisation);
},
async handleContentMessage(envelope) {
const plaintext = await this.decrypt(envelope, envelope.content);
if (!plaintext) {
window.log.warn('handleContentMessage: plaintext was falsey');
return null;
} else if (plaintext instanceof ArrayBuffer && plaintext.byteLength === 0) {
return null;
}
return this.innerHandleContentMessage(envelope, plaintext);
},
async handleSessionRequestMessage(envelope, content) {
const shouldProcessSessionRequest = await libsession.Protocols.SessionProtocol.shouldProcessSessionRequest(
envelope.source,
envelope.timestamp
);
window.console.log(
`Received SESSION_REQUEST from source: ${envelope.source}`
);
if (shouldProcessSessionRequest) {
try {
// device id are always 1 with Session
const deviceId = 1;
const pubkey = envelope.source;
const address = new libsignal.SignalProtocolAddress(
envelope.source,
deviceId
);
// we process the new prekeys and initiate a new session.
// The old sessions will get deleted once the correspondant
// has switch to the new session.
const [identityKey, preKey, signedKey, signature] = [
content.preKeyBundleMessage.identityKey,
content.preKeyBundleMessage.preKey,
content.preKeyBundleMessage.signedKey,
content.preKeyBundleMessage.signature,
].map(k => dcodeIO.ByteBuffer.wrap(k).toArrayBuffer());
const { preKeyId, signedKeyId } = content.preKeyBundleMessage;
if (pubkey !== StringView.arrayBufferToHex(identityKey)) {
throw new Error(
'Error in savePreKeyBundleMessage: envelope pubkey does not match pubkey in prekey bundle'
);
}
if (preKey === undefined || signedKey === undefined) {
window.console.warn(
"Couldn't process prekey bundle without preKey or signedKey"
);
return;
}
const signedPreKey = {
keyId: signedKeyId,
publicKey: signedKey,
signature,
};
const preKeyObject = {
publicKey: preKey,
keyId: preKeyId,
};
const device = {
identityKey,
deviceId,
preKey: preKeyObject,
signedPreKey,
registrationId: 0,
};
const builder = new libsignal.SessionBuilder(
textsecure.storage.protocol,
address
);
await builder.processPreKey(device);
await libsession.Protocols.SessionProtocol.onSessionRequestProcessed(
new libsession.Types.PubKey(envelope.source)
);
window.log.debug('sending session established to', envelope.source);
// We don't need to await the call below because we just want to send it off
const user = new libsession.Types.PubKey(envelope.source);
const sessionEstablished = new window.libsession.Messages.Outgoing.SessionEstablishedMessage(
{ timestamp: Date.now() }
);
await libsession.getMessageQueue().send(user, sessionEstablished);
} catch (e) {
window.log.warn('Failed to process session request', e);
// TODO how to handle a failed session request?
}
}
},
async innerHandleContentMessage(envelope, plaintext) {
const content = textsecure.protobuf.Content.decode(plaintext);
const { SESSION_REQUEST } = textsecure.protobuf.Envelope.Type;
await ConversationController.getOrCreateAndWait(envelope.source, 'private');
if (envelope.type === SESSION_REQUEST) {
await this.handleSessionRequestMessage(envelope, content);
} else {
const device = new libsession.Types.PubKey(envelope.source);
5 years ago
await libsession.Protocols.SessionProtocol.onSessionEstablished(device);
await libsession.getMessageQueue().processPending(device);
}
if (content.pairingAuthorisation) {
return this.handlePairingAuthorisationMessage(envelope, content);
}
if (content.syncMessage) {
return this.handleSyncMessage(envelope, content.syncMessage);
}
if (content.dataMessage) {
window.NewReceiver.handleDataMessage(envelope, content.dataMessage);
return undefined;
}
if (content.nullMessage) {
return this.handleNullMessage(envelope, content.nullMessage);
}
if (content.callMessage) {
return this.handleCallMessage(envelope, content.callMessage);
}
if (content.receiptMessage) {
return this.handleReceiptMessage(envelope, content.receiptMessage);
}
if (content.typingMessage) {
return this.handleTypingMessage(envelope, content.typingMessage);
}
return null;
},
handleCallMessage(envelope) {
window.log.info('call message from', getEnvelopeId(envelope));
this.removeFromCache(envelope);
},
handleReceiptMessage(envelope, receiptMessage) {
const results = [];
if (
receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY
) {
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));
}
} else if (
receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ
) {
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);
},
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);
},
handleNullMessage(envelope) {
window.log.info('null message from', getEnvelopeId(envelope));
this.removeFromCache(envelope);
},
async handleSyncMessage(envelope, syncMessage) {
// We should only accept sync messages from our devices
const ourNumber = textsecure.storage.user.getNumber();
const ourDevices = await libsession.Protocols.MultiDeviceProtocol.getAllDevices(
ourNumber
);
const validSyncSender = ourDevices.some(
device => device.key === envelope.source
);
if (!validSyncSender) {
throw new Error(
"Received sync message from a device we aren't paired with"
);
}
if (syncMessage.sent) {
const sentMessage = syncMessage.sent;
const to = sentMessage.message.group
? `group(${sentMessage.message.group.id.toBinary()})`
: sentMessage.destination;
window.log.info(
'sent message to',
to,
sentMessage.timestamp.toNumber(),
'from',
getEnvelopeId(envelope)
);
return this.handleSentMessage(envelope, sentMessage, sentMessage.message);
} else if (syncMessage.contacts) {
return this.handleContacts(envelope, syncMessage.contacts);
} else if (syncMessage.groups) {
return this.handleGroups(envelope, syncMessage.groups);
} else if (syncMessage.openGroups) {
return this.handleOpenGroups(envelope, syncMessage.openGroups);
} else if (syncMessage.blocked) {
return this.handleBlocked(envelope, syncMessage.blocked);
} else if (syncMessage.request) {
window.log.info('Got SyncMessage Request');
return this.removeFromCache(envelope);
} else if (syncMessage.read && syncMessage.read.length) {
window.log.info('read messages from', 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);
}
throw new Error('Got empty SyncMessage');
},
handleConfiguration(envelope, 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);
},
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) {
window.log.info('contact sync');
// const { blob } = contacts;
// 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(contacts).then(attachmentPointer => {
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));
return Promise.all(results).then(() => {
window.log.info('handleContacts: finished');
return this.removeFromCache(envelope);
});
});
},
handleGroups(envelope, groups) {
window.log.info('group sync');
// 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(groups).then(attachmentPointer => {
const groupBuffer = new GroupBuffer(attachmentPointer.data);
let groupDetails = groupBuffer.next();
const promises = [];
while (groupDetails !== undefined) {
groupDetails.id = groupDetails.id.toBinary();
const ev = new Event('group');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.groupDetails = groupDetails;
const promise = this.dispatchAndWait(ev).catch(e => {
window.log.error('error processing group', e);
});
groupDetails = groupBuffer.next();
promises.push(promise);
}
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
8 years ago
ev.confirm = this.removeFromCache.bind(this, envelope);
return this.dispatchAndWait(ev);
});
});
},
handleOpenGroups(envelope, openGroups) {
const groupsArray = openGroups.map(openGroup => openGroup.url);
libloki.api.debug.logGroupSync(
`Received GROUP_SYNC with open groups: [${groupsArray}]`
);
openGroups.forEach(({ url, channelId }) => {
window.attemptConnection(url, channelId);
});
return this.removeFromCache(envelope);
},
handleBlocked(envelope, blocked) {
window.log.info('Setting these numbers as blocked:', blocked.numbers);
textsecure.storage.put('blocked', blocked.numbers);
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);
},
isBlocked(number) {
return textsecure.storage.get('blocked', []).indexOf(number) >= 0;
},
handleAttachment(attachment) {
// window.log.info('Not handling attachments.');
return Promise.resolve({
...attachment,
data: dcodeIO.ByteBuffer.wrap(attachment.data).toArrayBuffer(), // ByteBuffer to ArrayBuffer
});
},
});
window.textsecure = window.textsecure || {};
textsecure.MessageReceiver = function MessageReceiverWrapper(
username,
password,
signalingKey,
options
) {
const messageReceiver = new MessageReceiver(
username,
password,
signalingKey,
options
);
this.addEventListener = messageReceiver.addEventListener.bind(
messageReceiver
);
this.removeEventListener = messageReceiver.removeEventListener.bind(
messageReceiver
);
this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
this.close = messageReceiver.close.bind(messageReceiver);
this.pollForAdditionalId = messageReceiver.pollForAdditionalId.bind(
messageReceiver
);
this.stopProcessing = messageReceiver.stopProcessing.bind(messageReceiver);
messageReceiver.connect();
};
textsecure.MessageReceiver.prototype = {
constructor: textsecure.MessageReceiver,
};
textsecure.MessageReceiver.stringToArrayBuffer =
MessageReceiver.stringToArrayBuffer;
textsecure.MessageReceiver.arrayBufferToString =
MessageReceiver.arrayBufferToString;
textsecure.MessageReceiver.stringToArrayBufferBase64 =
MessageReceiver.stringToArrayBufferBase64;
textsecure.MessageReceiver.arrayBufferToStringBase64 =
MessageReceiver.arrayBufferToStringBase64;