Sealed Sender support

https://signal.org/blog/sealed-sender/
This commit is contained in:
Scott Nonnenberg 2018-10-17 18:01:21 -07:00
parent 817cf5ed03
commit a7d78c0e9b
38 changed files with 2996 additions and 789 deletions

View file

@ -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);