parent
817cf5ed03
commit
a7d78c0e9b
38 changed files with 2996 additions and 789 deletions
|
@ -1,4 +1,4 @@
|
|||
/* global textsecure, libsignal, window, btoa */
|
||||
/* global textsecure, libsignal, window, btoa, _ */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
|
@ -8,7 +8,8 @@ function OutgoingMessage(
|
|||
numbers,
|
||||
message,
|
||||
silent,
|
||||
callback
|
||||
callback,
|
||||
options = {}
|
||||
) {
|
||||
if (message instanceof textsecure.protobuf.DataMessage) {
|
||||
const content = new textsecure.protobuf.Content();
|
||||
|
@ -26,6 +27,12 @@ function OutgoingMessage(
|
|||
this.numbersCompleted = 0;
|
||||
this.errors = [];
|
||||
this.successfulNumbers = [];
|
||||
this.failoverNumbers = [];
|
||||
this.unidentifiedDeliveries = [];
|
||||
|
||||
const { numberInfo, senderCertificate } = options;
|
||||
this.numberInfo = numberInfo;
|
||||
this.senderCertificate = senderCertificate;
|
||||
}
|
||||
|
||||
OutgoingMessage.prototype = {
|
||||
|
@ -35,7 +42,9 @@ OutgoingMessage.prototype = {
|
|||
if (this.numbersCompleted >= this.numbers.length) {
|
||||
this.callback({
|
||||
successfulNumbers: this.successfulNumbers,
|
||||
failoverNumbers: this.failoverNumbers,
|
||||
errors: this.errors,
|
||||
unidentifiedDeliveries: this.unidentifiedDeliveries,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -57,7 +66,7 @@ OutgoingMessage.prototype = {
|
|||
this.errors[this.errors.length] = error;
|
||||
this.numberCompleted();
|
||||
},
|
||||
reloadDevicesAndSend(number, recurse) {
|
||||
reloadDevicesAndSend(number, recurse, failover) {
|
||||
return () =>
|
||||
textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => {
|
||||
if (deviceIds.length === 0) {
|
||||
|
@ -67,7 +76,7 @@ OutgoingMessage.prototype = {
|
|||
null
|
||||
);
|
||||
}
|
||||
return this.doSendMessage(number, deviceIds, recurse);
|
||||
return this.doSendMessage(number, deviceIds, recurse, failover);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -109,51 +118,108 @@ OutgoingMessage.prototype = {
|
|||
})
|
||||
);
|
||||
|
||||
const { numberInfo } = this;
|
||||
const info = numberInfo && numberInfo[number] ? numberInfo[number] : {};
|
||||
const { accessKey } = info || {};
|
||||
|
||||
if (updateDevices === undefined) {
|
||||
return this.server.getKeysForNumber(number).then(handleResult);
|
||||
}
|
||||
let promise = Promise.resolve();
|
||||
updateDevices.forEach(device => {
|
||||
promise = promise.then(() =>
|
||||
this.server
|
||||
.getKeysForNumber(number, device)
|
||||
.then(handleResult)
|
||||
.catch(e => {
|
||||
if (e.name === 'HTTPError' && e.code === 404) {
|
||||
if (device !== 1) {
|
||||
return this.removeDeviceIdsForNumber(number, [device]);
|
||||
if (accessKey) {
|
||||
return this.server
|
||||
.getKeysForNumberUnauth(number, '*', { accessKey })
|
||||
.catch(error => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
if (this.failoverNumbers.indexOf(number) === -1) {
|
||||
this.failoverNumbers.push(number);
|
||||
}
|
||||
throw new textsecure.UnregisteredUserError(number, e);
|
||||
} else {
|
||||
throw e;
|
||||
return this.server.getKeysForNumber(number, '*');
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
);
|
||||
.then(handleResult);
|
||||
}
|
||||
|
||||
return this.server.getKeysForNumber(number, '*').then(handleResult);
|
||||
}
|
||||
|
||||
let promise = Promise.resolve();
|
||||
updateDevices.forEach(deviceId => {
|
||||
promise = promise.then(() => {
|
||||
let innerPromise;
|
||||
|
||||
if (accessKey) {
|
||||
innerPromise = this.server
|
||||
.getKeysForNumberUnauth(number, deviceId, { accessKey })
|
||||
.then(handleResult)
|
||||
.catch(error => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
if (this.failoverNumbers.indexOf(number) === -1) {
|
||||
this.failoverNumbers.push(number);
|
||||
}
|
||||
return this.server
|
||||
.getKeysForNumber(number, deviceId)
|
||||
.then(handleResult);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
} else {
|
||||
innerPromise = this.server
|
||||
.getKeysForNumber(number, deviceId)
|
||||
.then(handleResult);
|
||||
}
|
||||
|
||||
return innerPromise.catch(e => {
|
||||
if (e.name === 'HTTPError' && e.code === 404) {
|
||||
if (deviceId !== 1) {
|
||||
return this.removeDeviceIdsForNumber(number, [deviceId]);
|
||||
}
|
||||
throw new textsecure.UnregisteredUserError(number, e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
transmitMessage(number, jsonData, timestamp) {
|
||||
return this.server
|
||||
.sendMessages(number, jsonData, timestamp, this.silent)
|
||||
.catch(e => {
|
||||
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
|
||||
// 409 and 410 should bubble and be handled by doSendMessage
|
||||
// 404 should throw UnregisteredUserError
|
||||
// all other network errors can be retried later.
|
||||
if (e.code === 404) {
|
||||
throw new textsecure.UnregisteredUserError(number, e);
|
||||
}
|
||||
throw new textsecure.SendMessageNetworkError(
|
||||
number,
|
||||
jsonData,
|
||||
e,
|
||||
timestamp
|
||||
);
|
||||
transmitMessage(number, jsonData, timestamp, { accessKey } = {}) {
|
||||
let promise;
|
||||
|
||||
if (accessKey) {
|
||||
promise = this.server.sendMessagesUnauth(
|
||||
number,
|
||||
jsonData,
|
||||
timestamp,
|
||||
this.silent,
|
||||
{ accessKey }
|
||||
);
|
||||
} else {
|
||||
promise = this.server.sendMessages(
|
||||
number,
|
||||
jsonData,
|
||||
timestamp,
|
||||
this.silent
|
||||
);
|
||||
}
|
||||
|
||||
return promise.catch(e => {
|
||||
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
|
||||
// 409 and 410 should bubble and be handled by doSendMessage
|
||||
// 404 should throw UnregisteredUserError
|
||||
// all other network errors can be retried later.
|
||||
if (e.code === 404) {
|
||||
throw new textsecure.UnregisteredUserError(number, e);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
throw new textsecure.SendMessageNetworkError(
|
||||
number,
|
||||
jsonData,
|
||||
e,
|
||||
timestamp
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
getPaddedMessageLength(messageLength) {
|
||||
|
@ -179,15 +245,42 @@ OutgoingMessage.prototype = {
|
|||
return this.plaintext;
|
||||
},
|
||||
|
||||
doSendMessage(number, deviceIds, recurse) {
|
||||
doSendMessage(number, deviceIds, recurse, failover) {
|
||||
const ciphers = {};
|
||||
const plaintext = this.getPlaintext();
|
||||
|
||||
const { numberInfo, senderCertificate } = this;
|
||||
const info = numberInfo && numberInfo[number] ? numberInfo[number] : {};
|
||||
const { accessKey } = info || {};
|
||||
|
||||
if (accessKey && !senderCertificate) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
'OutgoingMessage.doSendMessage: accessKey was provided, but senderCertificate was not'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// If failover is true, we don't send an unidentified sender message
|
||||
const sealedSender = Boolean(!failover && accessKey && senderCertificate);
|
||||
|
||||
// We don't send to ourselves if unless sealedSender is enabled
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const ourDeviceId = textsecure.storage.user.getDeviceId();
|
||||
if (number === ourNumber && !sealedSender) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
deviceIds = _.reject(
|
||||
deviceIds,
|
||||
deviceId =>
|
||||
// because we store our own device ID as a string at least sometimes
|
||||
deviceId === ourDeviceId || deviceId === parseInt(ourDeviceId, 10)
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
deviceIds.map(deviceId => {
|
||||
deviceIds.map(async deviceId => {
|
||||
const address = new libsignal.SignalProtocolAddress(number, deviceId);
|
||||
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const options = {};
|
||||
|
||||
// No limit on message keys if we're communicating with our other devices
|
||||
|
@ -195,26 +288,80 @@ OutgoingMessage.prototype = {
|
|||
options.messageKeysLimit = false;
|
||||
}
|
||||
|
||||
// If failover is true, we don't send an unidentified sender message
|
||||
if (sealedSender) {
|
||||
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
|
||||
textsecure.storage.protocol
|
||||
);
|
||||
ciphers[address.getDeviceId()] = secretSessionCipher;
|
||||
|
||||
const ciphertext = await secretSessionCipher.encrypt(
|
||||
address,
|
||||
senderCertificate,
|
||||
plaintext
|
||||
);
|
||||
return {
|
||||
type: textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
|
||||
destinationDeviceId: address.getDeviceId(),
|
||||
destinationRegistrationId: await secretSessionCipher.getRemoteRegistrationId(
|
||||
address
|
||||
),
|
||||
content: window.Signal.Crypto.arrayBufferToBase64(ciphertext),
|
||||
};
|
||||
}
|
||||
|
||||
const sessionCipher = new libsignal.SessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
address,
|
||||
options
|
||||
);
|
||||
ciphers[address.getDeviceId()] = sessionCipher;
|
||||
return sessionCipher.encrypt(plaintext).then(ciphertext => ({
|
||||
|
||||
const ciphertext = await sessionCipher.encrypt(plaintext);
|
||||
return {
|
||||
type: ciphertext.type,
|
||||
destinationDeviceId: address.getDeviceId(),
|
||||
destinationRegistrationId: ciphertext.registrationId,
|
||||
content: btoa(ciphertext.body),
|
||||
}));
|
||||
};
|
||||
})
|
||||
)
|
||||
.then(jsonData =>
|
||||
this.transmitMessage(number, jsonData, this.timestamp).then(() => {
|
||||
this.successfulNumbers[this.successfulNumbers.length] = number;
|
||||
this.numberCompleted();
|
||||
})
|
||||
)
|
||||
.then(jsonData => {
|
||||
if (sealedSender) {
|
||||
return this.transmitMessage(number, jsonData, this.timestamp, {
|
||||
accessKey,
|
||||
}).then(
|
||||
() => {
|
||||
this.unidentifiedDeliveries.push(number);
|
||||
this.successfulNumbers.push(number);
|
||||
this.numberCompleted();
|
||||
},
|
||||
error => {
|
||||
if (error.code === 401 || error.code === 403) {
|
||||
if (this.failoverNumbers.indexOf(number) === -1) {
|
||||
this.failoverNumbers.push(number);
|
||||
}
|
||||
if (info) {
|
||||
info.accessKey = null;
|
||||
}
|
||||
|
||||
// Set final parameter to true to ensure we don't hit this codepath a
|
||||
// second time.
|
||||
return this.doSendMessage(number, deviceIds, recurse, true);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return this.transmitMessage(number, jsonData, this.timestamp).then(
|
||||
() => {
|
||||
this.successfulNumbers.push(number);
|
||||
this.numberCompleted();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
|
@ -237,7 +384,9 @@ OutgoingMessage.prototype = {
|
|||
} else {
|
||||
p = Promise.all(
|
||||
error.response.staleDevices.map(deviceId =>
|
||||
ciphers[deviceId].closeOpenSessionForDevice()
|
||||
ciphers[deviceId].closeOpenSessionForDevice(
|
||||
new libsignal.SignalProtocolAddress(number, deviceId)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -248,7 +397,9 @@ OutgoingMessage.prototype = {
|
|||
? error.response.staleDevices
|
||||
: error.response.missingDevices;
|
||||
return this.getKeysForNumber(number, resetDevices).then(
|
||||
this.reloadDevicesAndSend(number, error.code === 409)
|
||||
// For now, we we won't retry unidentified delivery if we get here; new
|
||||
// devices could have been added which don't support it.
|
||||
this.reloadDevicesAndSend(number, error.code === 409, true)
|
||||
);
|
||||
});
|
||||
} else if (error.message === 'Identity key changed') {
|
||||
|
@ -305,28 +456,28 @@ OutgoingMessage.prototype = {
|
|||
return promise;
|
||||
},
|
||||
|
||||
sendToNumber(number) {
|
||||
return this.getStaleDeviceIdsForNumber(number).then(updateDevices =>
|
||||
this.getKeysForNumber(number, updateDevices)
|
||||
.then(this.reloadDevicesAndSend(number, true))
|
||||
.catch(error => {
|
||||
if (error.message === 'Identity key changed') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error = new textsecure.OutgoingIdentityKeyError(
|
||||
number,
|
||||
error.originalMessage,
|
||||
error.timestamp,
|
||||
error.identityKey
|
||||
);
|
||||
this.registerError(number, 'Identity key changed', error);
|
||||
} else {
|
||||
this.registerError(
|
||||
number,
|
||||
`Failed to retrieve new device keys for number ${number}`,
|
||||
error
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
async sendToNumber(number) {
|
||||
try {
|
||||
const updateDevices = await this.getStaleDeviceIdsForNumber(number);
|
||||
await this.getKeysForNumber(number, updateDevices);
|
||||
await this.reloadDevicesAndSend(number, true)();
|
||||
} catch (error) {
|
||||
if (error.message === 'Identity key changed') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
const newError = new textsecure.OutgoingIdentityKeyError(
|
||||
number,
|
||||
error.originalMessage,
|
||||
error.timestamp,
|
||||
error.identityKey
|
||||
);
|
||||
this.registerError(number, 'Identity key changed', newError);
|
||||
} else {
|
||||
this.registerError(
|
||||
number,
|
||||
`Failed to retrieve new device keys for number ${number}`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue