parent
817cf5ed03
commit
a7d78c0e9b
38 changed files with 2996 additions and 789 deletions
|
@ -123,6 +123,13 @@ function MessageReceiver(username, password, signalingKey, options = {}) {
|
|||
this.password = password;
|
||||
this.server = WebAPI.connect({ username, password });
|
||||
|
||||
if (!options.serverTrustRoot) {
|
||||
throw new Error('Server trust root is required!');
|
||||
}
|
||||
this.serverTrustRoot = window.Signal.Crypto.base64ToArrayBuffer(
|
||||
options.serverTrustRoot
|
||||
);
|
||||
|
||||
const address = libsignal.SignalProtocolAddress.fromString(username);
|
||||
this.number = address.getName();
|
||||
this.deviceId = address.getDeviceId();
|
||||
|
@ -279,6 +286,8 @@ MessageReceiver.prototype.extend({
|
|||
return request.respond(200, 'OK');
|
||||
}
|
||||
|
||||
envelope.id = envelope.serverGuid || window.getGuid();
|
||||
|
||||
return this.addToCache(envelope, plaintext).then(
|
||||
async () => {
|
||||
request.respond(200, 'OK');
|
||||
|
@ -437,9 +446,13 @@ MessageReceiver.prototype.extend({
|
|||
}
|
||||
},
|
||||
getEnvelopeId(envelope) {
|
||||
return `${envelope.source}.${
|
||||
envelope.sourceDevice
|
||||
} ${envelope.timestamp.toNumber()}`;
|
||||
if (envelope.source) {
|
||||
return `${envelope.source}.${
|
||||
envelope.sourceDevice
|
||||
} ${envelope.timestamp.toNumber()} (${envelope.id})`;
|
||||
}
|
||||
|
||||
return envelope.id;
|
||||
},
|
||||
async getAllFromCache() {
|
||||
window.log.info('getAllFromCache');
|
||||
|
@ -482,7 +495,7 @@ MessageReceiver.prototype.extend({
|
|||
);
|
||||
},
|
||||
async addToCache(envelope, plaintext) {
|
||||
const id = this.getEnvelopeId(envelope);
|
||||
const { id } = envelope;
|
||||
const data = {
|
||||
id,
|
||||
version: 2,
|
||||
|
@ -493,7 +506,7 @@ MessageReceiver.prototype.extend({
|
|||
return textsecure.storage.unprocessed.add(data);
|
||||
},
|
||||
async updateCache(envelope, plaintext) {
|
||||
const id = this.getEnvelopeId(envelope);
|
||||
const { id } = envelope;
|
||||
const item = await textsecure.storage.unprocessed.get(id);
|
||||
if (!item) {
|
||||
window.log.error(
|
||||
|
@ -517,11 +530,11 @@ MessageReceiver.prototype.extend({
|
|||
return textsecure.storage.unprocessed.save(item.attributes);
|
||||
},
|
||||
removeFromCache(envelope) {
|
||||
const id = this.getEnvelopeId(envelope);
|
||||
const { id } = envelope;
|
||||
return textsecure.storage.unprocessed.remove(id);
|
||||
},
|
||||
queueDecryptedEnvelope(envelope, plaintext) {
|
||||
const id = this.getEnvelopeId(envelope);
|
||||
const { id } = envelope;
|
||||
window.log.info('queueing decrypted envelope', id);
|
||||
|
||||
const task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext);
|
||||
|
@ -626,6 +639,8 @@ MessageReceiver.prototype.extend({
|
|||
return plaintext;
|
||||
},
|
||||
decrypt(envelope, ciphertext) {
|
||||
const { serverTrustRoot } = this;
|
||||
|
||||
let promise;
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
envelope.source,
|
||||
|
@ -646,6 +661,14 @@ MessageReceiver.prototype.extend({
|
|||
address,
|
||||
options
|
||||
);
|
||||
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
|
||||
textsecure.storage.protocol
|
||||
);
|
||||
|
||||
const me = {
|
||||
number: ourNumber,
|
||||
deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10),
|
||||
};
|
||||
|
||||
switch (envelope.type) {
|
||||
case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
|
||||
|
@ -662,13 +685,76 @@ MessageReceiver.prototype.extend({
|
|||
address
|
||||
);
|
||||
break;
|
||||
case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER:
|
||||
window.log.info('received unidentified sender message');
|
||||
promise = secretSessionCipher
|
||||
.decrypt(
|
||||
window.Signal.Metadata.createCertificateValidator(serverTrustRoot),
|
||||
ciphertext.toArrayBuffer(),
|
||||
Math.min(
|
||||
envelope.serverTimestamp
|
||||
? envelope.serverTimestamp.toNumber()
|
||||
: Date.now(),
|
||||
Date.now()
|
||||
),
|
||||
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 };
|
||||
}
|
||||
|
||||
// Here we take this sender information and attach it back to the envelope
|
||||
// to make the rest of the app work properly.
|
||||
|
||||
// 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 = true;
|
||||
|
||||
// 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) {
|
||||
// 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 = true;
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return this.removeFromCache().then(() => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
);
|
||||
break;
|
||||
default:
|
||||
promise = Promise.reject(new Error('Unknown message type'));
|
||||
}
|
||||
|
||||
return promise
|
||||
.then(plaintext =>
|
||||
this.updateCache(envelope, plaintext).then(
|
||||
.then(plaintext => {
|
||||
const { isMe } = plaintext || {};
|
||||
if (isMe) {
|
||||
return this.removeFromCache(envelope);
|
||||
}
|
||||
|
||||
return this.updateCache(envelope, plaintext).then(
|
||||
() => plaintext,
|
||||
error => {
|
||||
window.log.error(
|
||||
|
@ -677,8 +763,8 @@ MessageReceiver.prototype.extend({
|
|||
);
|
||||
return plaintext;
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
let errorToThrow = error;
|
||||
|
||||
|
@ -720,13 +806,14 @@ MessageReceiver.prototype.extend({
|
|||
throw e;
|
||||
}
|
||||
},
|
||||
handleSentMessage(
|
||||
envelope,
|
||||
destination,
|
||||
timestamp,
|
||||
msg,
|
||||
expirationStartTimestamp
|
||||
) {
|
||||
handleSentMessage(envelope, sentContainer, msg) {
|
||||
const {
|
||||
destination,
|
||||
timestamp,
|
||||
expirationStartTimestamp,
|
||||
unidentifiedStatus,
|
||||
} = sentContainer;
|
||||
|
||||
let p = Promise.resolve();
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
|
||||
|
@ -757,6 +844,7 @@ MessageReceiver.prototype.extend({
|
|||
destination,
|
||||
timestamp: timestamp.toNumber(),
|
||||
device: envelope.sourceDevice,
|
||||
unidentifiedStatus,
|
||||
message,
|
||||
};
|
||||
if (expirationStartTimestamp) {
|
||||
|
@ -799,6 +887,7 @@ MessageReceiver.prototype.extend({
|
|||
sourceDevice: envelope.sourceDevice,
|
||||
timestamp: envelope.timestamp.toNumber(),
|
||||
receivedAt: envelope.receivedAt,
|
||||
unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived,
|
||||
message,
|
||||
};
|
||||
return this.dispatchAndWait(ev);
|
||||
|
@ -806,18 +895,26 @@ MessageReceiver.prototype.extend({
|
|||
);
|
||||
},
|
||||
handleLegacyMessage(envelope) {
|
||||
return this.decrypt(envelope, envelope.legacyMessage).then(plaintext =>
|
||||
this.innerHandleLegacyMessage(envelope, plaintext)
|
||||
);
|
||||
return this.decrypt(envelope, envelope.legacyMessage).then(plaintext => {
|
||||
if (!plaintext) {
|
||||
window.log.warn('handleLegacyMessage: plaintext was falsey');
|
||||
return null;
|
||||
}
|
||||
return this.innerHandleLegacyMessage(envelope, plaintext);
|
||||
});
|
||||
},
|
||||
innerHandleLegacyMessage(envelope, plaintext) {
|
||||
const message = textsecure.protobuf.DataMessage.decode(plaintext);
|
||||
return this.handleDataMessage(envelope, message);
|
||||
},
|
||||
handleContentMessage(envelope) {
|
||||
return this.decrypt(envelope, envelope.content).then(plaintext =>
|
||||
this.innerHandleContentMessage(envelope, plaintext)
|
||||
);
|
||||
return this.decrypt(envelope, envelope.content).then(plaintext => {
|
||||
if (!plaintext) {
|
||||
window.log.warn('handleContentMessage: plaintext was falsey');
|
||||
return null;
|
||||
}
|
||||
return this.innerHandleContentMessage(envelope, plaintext);
|
||||
});
|
||||
},
|
||||
innerHandleContentMessage(envelope, plaintext) {
|
||||
const content = textsecure.protobuf.Content.decode(plaintext);
|
||||
|
@ -895,13 +992,7 @@ MessageReceiver.prototype.extend({
|
|||
'from',
|
||||
this.getEnvelopeId(envelope)
|
||||
);
|
||||
return this.handleSentMessage(
|
||||
envelope,
|
||||
sentMessage.destination,
|
||||
sentMessage.timestamp,
|
||||
sentMessage.message,
|
||||
sentMessage.expirationStartTimestamp
|
||||
);
|
||||
return this.handleSentMessage(envelope, sentMessage, sentMessage.message);
|
||||
} else if (syncMessage.contacts) {
|
||||
return this.handleContacts(envelope, syncMessage.contacts);
|
||||
} else if (syncMessage.groups) {
|
||||
|
@ -922,11 +1013,10 @@ MessageReceiver.prototype.extend({
|
|||
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 = {
|
||||
readReceipts: configuration.readReceipts,
|
||||
};
|
||||
ev.configuration = configuration;
|
||||
return this.dispatchAndWait(ev);
|
||||
},
|
||||
handleVerified(envelope, verified) {
|
||||
|
@ -1075,102 +1165,6 @@ MessageReceiver.prototype.extend({
|
|||
.then(decryptAttachment)
|
||||
.then(updateAttachment);
|
||||
},
|
||||
validateRetryContentMessage(content) {
|
||||
// Today this is only called for incoming identity key errors, so it can't be a sync
|
||||
// message.
|
||||
if (content.syncMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We want at least one field set, but not more than one
|
||||
let count = 0;
|
||||
count += content.dataMessage ? 1 : 0;
|
||||
count += content.callMessage ? 1 : 0;
|
||||
count += content.nullMessage ? 1 : 0;
|
||||
if (count !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// It's most likely that dataMessage will be populated, so we look at it in detail
|
||||
const data = content.dataMessage;
|
||||
if (
|
||||
data &&
|
||||
!data.attachments.length &&
|
||||
!data.body &&
|
||||
!data.expireTimer &&
|
||||
!data.flags &&
|
||||
!data.group
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
tryMessageAgain(from, ciphertext, message) {
|
||||
const address = libsignal.SignalProtocolAddress.fromString(from);
|
||||
const sentAt = message.sent_at || Date.now();
|
||||
const receivedAt = message.received_at || Date.now();
|
||||
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const number = address.getName();
|
||||
const device = address.getDeviceId();
|
||||
const options = {};
|
||||
|
||||
// No limit on message keys if we're communicating with our other devices
|
||||
if (ourNumber === number) {
|
||||
options.messageKeysLimit = false;
|
||||
}
|
||||
|
||||
const sessionCipher = new libsignal.SessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
address,
|
||||
options
|
||||
);
|
||||
window.log.info('retrying prekey whisper message');
|
||||
return this.decryptPreKeyWhisperMessage(
|
||||
ciphertext,
|
||||
sessionCipher,
|
||||
address
|
||||
).then(plaintext => {
|
||||
const envelope = {
|
||||
source: number,
|
||||
sourceDevice: device,
|
||||
receivedAt,
|
||||
timestamp: {
|
||||
toNumber() {
|
||||
return sentAt;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Before June, all incoming messages were still DataMessage:
|
||||
// - iOS: Michael Kirk says that they were sending Legacy messages until June
|
||||
// - Desktop: https://github.com/signalapp/Signal-Desktop/commit/e8548879db405d9bcd78b82a456ad8d655592c0f
|
||||
// - Android: https://github.com/signalapp/libsignal-service-java/commit/61a75d023fba950ff9b4c75a249d1a3408e12958
|
||||
//
|
||||
// var d = new Date('2017-06-01T07:00:00.000Z');
|
||||
// d.getTime();
|
||||
const startOfJune = 1496300400000;
|
||||
if (sentAt < startOfJune) {
|
||||
return this.innerHandleLegacyMessage(envelope, plaintext);
|
||||
}
|
||||
|
||||
// This is ugly. But we don't know what kind of proto we need to decode...
|
||||
try {
|
||||
// Simply decoding as a Content message may throw
|
||||
const content = textsecure.protobuf.Content.decode(plaintext);
|
||||
|
||||
// But it might also result in an invalid object, so we try to detect that
|
||||
if (this.validateRetryContentMessage(content)) {
|
||||
return this.innerHandleContentMessage(envelope, plaintext);
|
||||
}
|
||||
} catch (e) {
|
||||
return this.innerHandleLegacyMessage(envelope, plaintext);
|
||||
}
|
||||
|
||||
return this.innerHandleLegacyMessage(envelope, plaintext);
|
||||
});
|
||||
},
|
||||
async handleEndSession(number) {
|
||||
window.log.info('got end session');
|
||||
const deviceIds = await textsecure.storage.protocol.getDeviceIds(number);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue