Passive UUID support

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
Ken Powers 2020-03-05 13:14:58 -08:00 committed by Scott Nonnenberg
parent f64ca0ed21
commit a90246cbe5
49 changed files with 2226 additions and 776 deletions

View file

@ -24,14 +24,14 @@
this.pending = Promise.resolve();
}
function getNumber(numberId) {
if (!numberId || !numberId.length) {
return numberId;
function getIdentifier(id) {
if (!id || !id.length) {
return id;
}
const parts = numberId.split('.');
const parts = id.split('.');
if (!parts.length) {
return numberId;
return id;
}
return parts[0];
@ -136,7 +136,7 @@
.then(clearSessionsAndPreKeys)
.then(generateKeys)
.then(keys => registerKeys(keys).then(() => confirmKeys(keys)))
.then(() => registrationDone(number));
.then(() => registrationDone({ number }));
}
)
);
@ -212,7 +212,8 @@
provisionMessage.profileKey,
deviceName,
provisionMessage.userAgent,
provisionMessage.readReceipts
provisionMessage.readReceipts,
{ uuid: provisionMessage.uuid }
)
.then(clearSessionsAndPreKeys)
.then(generateKeys)
@ -221,9 +222,7 @@
confirmKeys(keys)
)
)
.then(() =>
registrationDone(provisionMessage.number)
);
.then(() => registrationDone(provisionMessage));
}
)
)
@ -414,7 +413,8 @@
password = password.substring(0, password.length - 2);
const registrationId = libsignal.KeyHelper.generateRegistrationId();
const previousNumber = getNumber(textsecure.storage.get('number_id'));
const previousNumber = getIdentifier(textsecure.storage.get('number_id'));
const previousUuid = getIdentifier(textsecure.storage.get('uuid_id'));
const encryptedDeviceName = await this.encryptDeviceName(
deviceName,
@ -437,10 +437,21 @@
{ accessKey }
);
if (previousNumber && previousNumber !== number) {
window.log.warn(
'New number is different from old number; deleting all previous data'
);
const numberChanged = previousNumber && previousNumber !== number;
const uuidChanged =
previousUuid && response.uuid && previousUuid !== response.uuid;
if (numberChanged || uuidChanged) {
if (numberChanged) {
window.log.warn(
'New number is different from old number; deleting all previous data'
);
}
if (uuidChanged) {
window.log.warn(
'New uuid is different from old uuid; deleting all previous data'
);
}
try {
await textsecure.storage.protocol.removeAllData();
@ -465,10 +476,29 @@
textsecure.storage.remove('read-receipts-setting'),
]);
// `setNumberAndDeviceId` and `setUuidAndDeviceId` need to be called
// before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes`
// indirectly calls `ConversationController.getConverationId()` which
// initializes the conversation for the given number (our number) which
// calls out to the user storage API to get the stored UUID and number
// information.
await textsecure.storage.user.setNumberAndDeviceId(
number,
response.deviceId || 1,
deviceName
);
const setUuid = response.uuid;
if (setUuid) {
await textsecure.storage.user.setUuidAndDeviceId(
setUuid,
response.deviceId || 1
);
}
// update our own identity key, which may have changed
// if we're relinking after a reinstall on the master device
await textsecure.storage.protocol.saveIdentityWithAttributes(number, {
id: number,
publicKey: identityKeyPair.pubKey,
firstUse: true,
timestamp: Date.now(),
@ -491,12 +521,6 @@
Boolean(readReceipts)
);
await textsecure.storage.user.setNumberAndDeviceId(
number,
response.deviceId || 1,
deviceName
);
const regionCode = libphonenumber.util.getRegionCodeForNumber(number);
await textsecure.storage.put('regionCode', regionCode);
await textsecure.storage.protocol.hydrateCaches();
@ -579,11 +603,16 @@
);
});
},
async registrationDone(number) {
async registrationDone({ uuid, number }) {
window.log.info('registration done');
// Ensure that we always have a conversation for ourself
await ConversationController.getOrCreateAndWait(number, 'private');
const conversation = await ConversationController.getOrCreateAndWait(
number || uuid,
'private'
);
conversation.updateE164(number);
conversation.updateUuid(uuid);
window.log.info('dispatching registration event');

View file

@ -36,6 +36,22 @@ ProtoParser.prototype = {
proto.profileKey = proto.profileKey.toArrayBuffer();
}
if (proto.uuid) {
window.normalizeUuids(
proto,
['uuid'],
'ProtoParser::next (proto.uuid)'
);
}
if (proto.members) {
window.normalizeUuids(
proto,
proto.members.map((_member, i) => `members.${i}.uuid`),
'ProtoParser::next (proto.members)'
);
}
return proto;
} catch (error) {
window.log.error(

View file

@ -36521,7 +36521,7 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ
})();
(function() {
var VERSION = 0;
var VERSION = shortToArrayBuffer(0);
function iterateHash(data, key, count) {
data = dcodeIO.ByteBuffer.concat([data, key]).toArrayBuffer();
@ -36551,10 +36551,21 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ
return s;
}
function decodeUuid(uuid) {
let i = 0;
let buf = new Uint8Array(16);
uuid.replace(/[0-9A-F]{2}/ig, oct => {
buf[i++] = parseInt(oct, 16);
});
return buf;
}
function getDisplayStringFor(identifier, key, iterations) {
var bytes = dcodeIO.ByteBuffer.concat([
shortToArrayBuffer(VERSION), key, identifier
]).toArrayBuffer();
var isUuid = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(identifier);
var encodedIdentifier = isUuid ? decodeUuid(identifier) : identifier;
var bytes = dcodeIO.ByteBuffer.concat([VERSION, key, encodedIdentifier]).toArrayBuffer();
return iterateHash(bytes, key, iterations).then(function(output) {
output = new Uint8Array(output);
return getEncodedChunk(output, 0) +

View file

@ -14,13 +14,20 @@
const RETRY_TIMEOUT = 2 * 60 * 1000;
function MessageReceiver(username, password, signalingKey, options = {}) {
function MessageReceiver(
oldUsername,
username,
password,
signalingKey,
options = {}
) {
this.count = 0;
this.signalingKey = signalingKey;
this.username = username;
this.username = oldUsername;
this.uuid = username;
this.password = password;
this.server = WebAPI.connect({ username, password });
this.server = WebAPI.connect({ username: username || oldUsername, password });
if (!options.serverTrustRoot) {
throw new Error('Server trust root is required!');
@ -29,9 +36,12 @@ function MessageReceiver(username, password, signalingKey, options = {}) {
options.serverTrustRoot
);
const address = libsignal.SignalProtocolAddress.fromString(username);
this.number = address.getName();
this.deviceId = address.getDeviceId();
this.number_id = oldUsername
? textsecure.utils.unencodeNumber(oldUsername)[0]
: null;
this.uuid_id = username ? textsecure.utils.unencodeNumber(username)[0] : null;
// eslint-disable-next-line prefer-destructuring
this.deviceId = textsecure.utils.unencodeNumber(username || oldUsername)[1];
this.incomingQueue = new window.PQueue({ concurrency: 1 });
this.pendingQueue = new window.PQueue({ concurrency: 1 });
@ -176,7 +186,7 @@ MessageReceiver.prototype.extend({
}
// possible 403 or network issue. Make an request to confirm
return this.server
.getDevices(this.number)
.getDevices(this.number_id || this.uuid_id)
.then(this.connect.bind(this)) // No HTTP error? Reconnect
.catch(e => {
const event = new Event('error');
@ -213,6 +223,11 @@ MessageReceiver.prototype.extend({
try {
const envelope = textsecure.protobuf.Envelope.decode(plaintext);
window.normalizeUuids(
envelope,
['sourceUuid'],
'message_receiver::handleRequest::job'
);
// 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
@ -222,6 +237,11 @@ MessageReceiver.prototype.extend({
return;
}
if (this.isUuidBlocked(envelope.sourceUuid)) {
request.respond(200, 'OK');
return;
}
envelope.id = envelope.serverGuid || window.getGuid();
envelope.serverTimestamp = envelope.serverTimestamp
? envelope.serverTimestamp.toNumber()
@ -333,6 +353,7 @@ MessageReceiver.prototype.extend({
const envelope = textsecure.protobuf.Envelope.decode(envelopePlaintext);
envelope.id = envelope.serverGuid || item.id;
envelope.source = envelope.source || item.source;
envelope.sourceUuid = envelope.sourceUuid || item.sourceUuid;
envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice;
envelope.serverTimestamp =
envelope.serverTimestamp || item.serverTimestamp;
@ -378,8 +399,8 @@ MessageReceiver.prototype.extend({
}
},
getEnvelopeId(envelope) {
if (envelope.source) {
return `${envelope.source}.${
if (envelope.sourceUuid || envelope.source) {
return `${envelope.sourceUuid || envelope.source}.${
envelope.sourceDevice
} ${envelope.timestamp.toNumber()} (${envelope.id})`;
}
@ -485,6 +506,7 @@ MessageReceiver.prototype.extend({
const { id } = envelope;
const data = {
source: envelope.source,
sourceUuid: envelope.sourceUuid,
sourceDevice: envelope.sourceDevice,
serverTimestamp: envelope.serverTimestamp,
decrypted: MessageReceiver.arrayBufferToStringBase64(plaintext),
@ -586,6 +608,7 @@ MessageReceiver.prototype.extend({
ev.deliveryReceipt = {
timestamp: envelope.timestamp.toNumber(),
source: envelope.source,
sourceUuid: envelope.sourceUuid,
sourceDevice: envelope.sourceDevice,
};
this.dispatchAndWait(ev).then(resolve, reject);
@ -613,16 +636,21 @@ MessageReceiver.prototype.extend({
let promise;
const address = new libsignal.SignalProtocolAddress(
envelope.source,
// Using source as opposed to sourceUuid allows us to get the existing
// session if we haven't yet harvested the incoming uuid
envelope.source || envelope.sourceUuid,
envelope.sourceDevice
);
const ourNumber = textsecure.storage.user.getNumber();
const number = address.toString().split('.')[0];
const ourUuid = textsecure.storage.user.getUuid();
const options = {};
// No limit on message keys if we're communicating with our other devices
if (ourNumber === number) {
if (
(envelope.source && ourNumber && ourNumber === envelope.source) ||
(envelope.sourceUuid && ourUuid && ourUuid === envelope.sourceUuid)
) {
options.messageKeysLimit = false;
}
@ -637,6 +665,7 @@ MessageReceiver.prototype.extend({
const me = {
number: ourNumber,
uuid: ourUuid,
deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10),
};
@ -666,7 +695,7 @@ MessageReceiver.prototype.extend({
)
.then(
result => {
const { isMe, sender, content } = result;
const { isMe, sender, senderUuid, content } = result;
// We need to drop incoming messages from ourself since server can't
// do it for us
@ -674,7 +703,10 @@ MessageReceiver.prototype.extend({
return { isMe: true };
}
if (this.isBlocked(sender.getName())) {
if (
(sender && this.isBlocked(sender.getName())) ||
(senderUuid && this.isUuidBlocked(senderUuid.getName()))
) {
window.log.info(
'Dropping blocked message after sealed sender decryption'
);
@ -685,25 +717,41 @@ MessageReceiver.prototype.extend({
// to make the rest of the app work properly.
const originalSource = envelope.source;
const originalSourceUuid = envelope.sourceUuid;
// eslint-disable-next-line no-param-reassign
envelope.source = sender.getName();
envelope.source = sender && sender.getName();
// eslint-disable-next-line no-param-reassign
envelope.sourceDevice = sender.getDeviceId();
envelope.sourceUuid = senderUuid && senderUuid.getName();
window.normalizeUuids(
envelope,
['sourceUuid'],
'message_receiver::decrypt::UNIDENTIFIED_SENDER'
);
// eslint-disable-next-line no-param-reassign
envelope.unidentifiedDeliveryReceived = !originalSource;
envelope.sourceDevice =
(sender && sender.getDeviceId()) ||
(senderUuid && senderUuid.getDeviceId());
// eslint-disable-next-line no-param-reassign
envelope.unidentifiedDeliveryReceived = !(
originalSource || originalSourceUuid
);
// Return just the content because that matches the signature of the other
// decrypt methods used above.
return this.unpad(content);
},
error => {
const { sender } = error || {};
const { sender, senderUuid } = error || {};
if (sender) {
if (sender || senderUuid) {
const originalSource = envelope.source;
const originalSourceUuid = envelope.sourceUuid;
if (this.isBlocked(sender.getName())) {
if (
(sender && this.isBlocked(sender.getName())) ||
(senderUuid && this.isUuidBlocked(senderUuid.getName()))
) {
window.log.info(
'Dropping blocked message with error after sealed sender decryption'
);
@ -711,11 +759,23 @@ MessageReceiver.prototype.extend({
}
// eslint-disable-next-line no-param-reassign
envelope.source = sender.getName();
envelope.source = sender && sender.getName();
// eslint-disable-next-line no-param-reassign
envelope.sourceDevice = sender.getDeviceId();
envelope.sourceUuid =
senderUuid && senderUuid.getName().toLowerCase();
window.normalizeUuids(
envelope,
['sourceUuid'],
'message_receiver::decrypt::UNIDENTIFIED_SENDER::error'
);
// eslint-disable-next-line no-param-reassign
envelope.unidentifiedDeliveryReceived = !originalSource;
envelope.sourceDevice =
(sender && sender.getDeviceId()) ||
(senderUuid && senderUuid.getDeviceId());
// eslint-disable-next-line no-param-reassign
envelope.unidentifiedDeliveryReceived = !(
originalSource || originalSourceUuid
);
throw error;
}
@ -803,7 +863,12 @@ MessageReceiver.prototype.extend({
this.processDecrypted(envelope, msg).then(message => {
const groupId = message.group && message.group.id;
const isBlocked = this.isGroupBlocked(groupId);
const isMe = envelope.source === textsecure.storage.user.getNumber();
const { source, sourceUuid } = envelope;
const ourE164 = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const isMe =
(source && ourE164 && source === ourE164) ||
(sourceUuid && ourUuid && sourceUuid === ourUuid);
const isLeavingGroup = Boolean(
message.group &&
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
@ -840,13 +905,18 @@ MessageReceiver.prototype.extend({
let p = Promise.resolve();
// eslint-disable-next-line no-bitwise
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
p = this.handleEndSession(envelope.source);
p = this.handleEndSession(envelope.source || envelope.sourceUuid);
}
return p.then(() =>
this.processDecrypted(envelope, msg).then(message => {
const groupId = message.group && message.group.id;
const isBlocked = this.isGroupBlocked(groupId);
const isMe = envelope.source === textsecure.storage.user.getNumber();
const { source, sourceUuid } = envelope;
const ourE164 = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const isMe =
(source && ourE164 && source === ourE164) ||
(sourceUuid && ourUuid && sourceUuid === ourUuid);
const isLeavingGroup = Boolean(
message.group &&
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
@ -865,6 +935,7 @@ MessageReceiver.prototype.extend({
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
source: envelope.source,
sourceUuid: envelope.sourceUuid,
sourceDevice: envelope.sourceDevice,
timestamp: envelope.timestamp.toNumber(),
receivedAt: envelope.receivedAt,
@ -930,6 +1001,7 @@ MessageReceiver.prototype.extend({
ev.deliveryReceipt = {
timestamp: receiptMessage.timestamp[i].toNumber(),
source: envelope.source,
sourceUuid: envelope.sourceUuid,
sourceDevice: envelope.sourceDevice,
};
results.push(this.dispatchAndWait(ev));
@ -943,7 +1015,7 @@ MessageReceiver.prototype.extend({
ev.timestamp = envelope.timestamp.toNumber();
ev.read = {
timestamp: receiptMessage.timestamp[i].toNumber(),
reader: envelope.source,
reader: envelope.source || envelope.sourceUuid,
};
results.push(this.dispatchAndWait(ev));
}
@ -968,6 +1040,7 @@ MessageReceiver.prototype.extend({
}
ev.sender = envelope.source;
ev.senderUuid = envelope.sourceUuid;
ev.senderDevice = envelope.sourceDevice;
ev.typing = {
typingMessage,
@ -992,7 +1065,24 @@ MessageReceiver.prototype.extend({
this.removeFromCache(envelope);
},
handleSyncMessage(envelope, syncMessage) {
if (envelope.source !== this.number) {
const unidentified = syncMessage.sent
? syncMessage.sent.unidentifiedStatus || []
: [];
window.normalizeUuids(
syncMessage,
[
'sent.destinationUuid',
...unidentified.map(
(_el, i) => `sent.unidentifiedStatus.${i}.destinationUuid`
),
],
'message_receiver::handleSyncMessage'
);
const fromSelfSource =
envelope.source && envelope.source === this.number_id;
const fromSelfSourceUuid =
envelope.sourceUuid && envelope.sourceUuid === this.uuid_id;
if (!fromSelfSource && !fromSelfSourceUuid) {
throw new Error('Received sync message from another number');
}
// eslint-disable-next-line eqeqeq
@ -1057,8 +1147,15 @@ MessageReceiver.prototype.extend({
const ev = new Event('viewSync');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.source = sync.sender;
ev.sourceUuid = sync.senderUuid;
ev.timestamp = sync.timestamp ? sync.timestamp.toNumber() : null;
window.normalizeUuids(
ev,
['sourceUuid'],
'message_receiver::handleViewOnceOpen'
);
return this.dispatchAndWait(ev);
},
handleStickerPackOperation(envelope, operations) {
@ -1080,8 +1177,14 @@ MessageReceiver.prototype.extend({
ev.verified = {
state: verified.state,
destination: verified.destination,
destinationUuid: verified.destinationUuid,
identityKey: verified.identityKey.toArrayBuffer(),
};
window.normalizeUuids(
ev,
['verified.destinationUuid'],
'message_receiver::handleVerified'
);
return this.dispatchAndWait(ev);
},
handleRead(envelope, read) {
@ -1093,7 +1196,13 @@ MessageReceiver.prototype.extend({
ev.read = {
timestamp: read[i].timestamp.toNumber(),
sender: read[i].sender,
senderUuid: read[i].senderUuid,
};
window.normalizeUuids(
ev,
['read.senderUuid'],
'message_receiver::handleRead'
);
results.push(this.dispatchAndWait(ev));
}
return Promise.all(results);
@ -1158,6 +1267,15 @@ MessageReceiver.prototype.extend({
handleBlocked(envelope, blocked) {
window.log.info('Setting these numbers as blocked:', blocked.numbers);
textsecure.storage.put('blocked', blocked.numbers);
if (blocked.uuids) {
window.normalizeUuids(
blocked,
blocked.uuids.map((_uuid, i) => `uuids.${i}`),
'message_receiver::handleBlocked'
);
window.log.info('Setting these uuids as blocked:', blocked.uuids);
textsecure.storage.put('blocked-uuids', blocked.uuids);
}
const groupIds = _.map(blocked.groupIds, groupId => groupId.toBinary());
window.log.info(
@ -1169,10 +1287,13 @@ MessageReceiver.prototype.extend({
return this.removeFromCache(envelope);
},
isBlocked(number) {
return textsecure.storage.get('blocked', []).indexOf(number) >= 0;
return textsecure.storage.get('blocked', []).includes(number);
},
isUuidBlocked(uuid) {
return textsecure.storage.get('blocked-uuids', []).includes(uuid);
},
isGroupBlocked(groupId) {
return textsecure.storage.get('blocked-groups', []).indexOf(groupId) >= 0;
return textsecure.storage.get('blocked-groups', []).includes(groupId);
},
cleanAttachment(attachment) {
return {
@ -1213,13 +1334,18 @@ MessageReceiver.prototype.extend({
const cleaned = this.cleanAttachment(attachment);
return this.downloadAttachment(cleaned);
},
async handleEndSession(number) {
async handleEndSession(identifier) {
window.log.info('got end session');
const deviceIds = await textsecure.storage.protocol.getDeviceIds(number);
const deviceIds = await textsecure.storage.protocol.getDeviceIds(
identifier
);
return Promise.all(
deviceIds.map(deviceId => {
const address = new libsignal.SignalProtocolAddress(number, deviceId);
const address = new libsignal.SignalProtocolAddress(
identifier,
deviceId
);
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
@ -1274,8 +1400,6 @@ MessageReceiver.prototype.extend({
throw new Error('Unknown flags in message');
}
const promises = [];
if (decrypted.group !== null) {
decrypted.group.id = decrypted.group.id.toBinary();
@ -1290,6 +1414,7 @@ MessageReceiver.prototype.extend({
break;
case textsecure.protobuf.GroupContext.Type.DELIVER:
decrypted.group.name = null;
decrypted.group.membersE164 = [];
decrypted.group.members = [];
decrypted.group.avatar = null;
break;
@ -1383,7 +1508,19 @@ MessageReceiver.prototype.extend({
}
}
return Promise.all(promises).then(() => decrypted);
const groupMembers = decrypted.group ? decrypted.group.members || [] : [];
window.normalizeUuids(
decrypted,
[
'quote.authorUuid',
'reaction.targetAuthorUuid',
...groupMembers.map((_member, i) => `group.members.${i}.uuid`),
],
'message_receiver::processDecrypted'
);
return Promise.resolve(decrypted);
/* eslint-enable no-bitwise, no-param-reassign */
},
});
@ -1392,12 +1529,14 @@ window.textsecure = window.textsecure || {};
textsecure.MessageReceiver = function MessageReceiverWrapper(
username,
uuid,
password,
signalingKey,
options
) {
const messageReceiver = new MessageReceiver(
username,
uuid,
password,
signalingKey,
options

View file

@ -5,7 +5,7 @@
function OutgoingMessage(
server,
timestamp,
numbers,
identifiers,
message,
silent,
callback,
@ -19,41 +19,43 @@ function OutgoingMessage(
}
this.server = server;
this.timestamp = timestamp;
this.numbers = numbers;
this.identifiers = identifiers;
this.message = message; // ContentMessage proto
this.callback = callback;
this.silent = silent;
this.numbersCompleted = 0;
this.identifiersCompleted = 0;
this.errors = [];
this.successfulNumbers = [];
this.failoverNumbers = [];
this.successfulIdentifiers = [];
this.failoverIdentifiers = [];
this.unidentifiedDeliveries = [];
const { numberInfo, senderCertificate, online } = options || {};
this.numberInfo = numberInfo;
const { sendMetadata, senderCertificate, senderCertificateWithUuid, online } =
options || {};
this.sendMetadata = sendMetadata;
this.senderCertificate = senderCertificate;
this.senderCertificateWithUuid = senderCertificateWithUuid;
this.online = online;
}
OutgoingMessage.prototype = {
constructor: OutgoingMessage,
numberCompleted() {
this.numbersCompleted += 1;
if (this.numbersCompleted >= this.numbers.length) {
this.identifiersCompleted += 1;
if (this.identifiersCompleted >= this.identifiers.length) {
this.callback({
successfulNumbers: this.successfulNumbers,
failoverNumbers: this.failoverNumbers,
successfulIdentifiers: this.successfulIdentifiers,
failoverIdentifiers: this.failoverIdentifiers,
errors: this.errors,
unidentifiedDeliveries: this.unidentifiedDeliveries,
});
}
},
registerError(number, reason, error) {
registerError(identifier, reason, error) {
if (!error || (error.name === 'HTTPError' && error.code !== 404)) {
// eslint-disable-next-line no-param-reassign
error = new textsecure.OutgoingMessageError(
number,
identifier,
this.message.toArrayBuffer(),
this.timestamp,
error
@ -61,27 +63,27 @@ OutgoingMessage.prototype = {
}
// eslint-disable-next-line no-param-reassign
error.number = number;
error.number = identifier;
// eslint-disable-next-line no-param-reassign
error.reason = reason;
this.errors[this.errors.length] = error;
this.numberCompleted();
},
reloadDevicesAndSend(number, recurse) {
reloadDevicesAndSend(identifier, recurse) {
return () =>
textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => {
textsecure.storage.protocol.getDeviceIds(identifier).then(deviceIds => {
if (deviceIds.length === 0) {
return this.registerError(
number,
identifier,
'Got empty device list when loading device keys',
null
);
}
return this.doSendMessage(number, deviceIds, recurse);
return this.doSendMessage(identifier, deviceIds, recurse);
});
},
getKeysForNumber(number, updateDevices) {
getKeysForIdentifier(identifier, updateDevices) {
const handleResult = response =>
Promise.all(
response.devices.map(device => {
@ -92,7 +94,7 @@ OutgoingMessage.prototype = {
updateDevices.indexOf(device.deviceId) > -1
) {
const address = new libsignal.SignalProtocolAddress(
number,
identifier,
device.deviceId
);
const builder = new libsignal.SessionBuilder(
@ -119,27 +121,30 @@ OutgoingMessage.prototype = {
})
);
const { numberInfo } = this;
const info = numberInfo && numberInfo[number] ? numberInfo[number] : {};
const { sendMetadata } = this;
const info =
sendMetadata && sendMetadata[identifier] ? sendMetadata[identifier] : {};
const { accessKey } = info || {};
if (updateDevices === undefined) {
if (accessKey) {
return this.server
.getKeysForNumberUnauth(number, '*', { accessKey })
.getKeysForIdentifierUnauth(identifier, '*', { accessKey })
.catch(error => {
if (error.code === 401 || error.code === 403) {
if (this.failoverNumbers.indexOf(number) === -1) {
this.failoverNumbers.push(number);
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
this.failoverIdentifiers.push(identifier);
}
return this.server.getKeysForNumber(number, '*');
return this.server.getKeysForIdentifier(identifier, '*');
}
throw error;
})
.then(handleResult);
}
return this.server.getKeysForNumber(number, '*').then(handleResult);
return this.server
.getKeysForIdentifier(identifier, '*')
.then(handleResult);
}
let promise = Promise.resolve();
@ -149,31 +154,31 @@ OutgoingMessage.prototype = {
if (accessKey) {
innerPromise = this.server
.getKeysForNumberUnauth(number, deviceId, { accessKey })
.getKeysForIdentifierUnauth(identifier, deviceId, { accessKey })
.then(handleResult)
.catch(error => {
if (error.code === 401 || error.code === 403) {
if (this.failoverNumbers.indexOf(number) === -1) {
this.failoverNumbers.push(number);
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
this.failoverIdentifiers.push(identifier);
}
return this.server
.getKeysForNumber(number, deviceId)
.getKeysForIdentifier(identifier, deviceId)
.then(handleResult);
}
throw error;
});
} else {
innerPromise = this.server
.getKeysForNumber(number, deviceId)
.getKeysForIdentifier(identifier, deviceId)
.then(handleResult);
}
return innerPromise.catch(e => {
if (e.name === 'HTTPError' && e.code === 404) {
if (deviceId !== 1) {
return this.removeDeviceIdsForNumber(number, [deviceId]);
return this.removeDeviceIdsForIdentifier(identifier, [deviceId]);
}
throw new textsecure.UnregisteredUserError(number, e);
throw new textsecure.UnregisteredUserError(identifier, e);
} else {
throw e;
}
@ -184,12 +189,12 @@ OutgoingMessage.prototype = {
return promise;
},
transmitMessage(number, jsonData, timestamp, { accessKey } = {}) {
transmitMessage(identifier, jsonData, timestamp, { accessKey } = {}) {
let promise;
if (accessKey) {
promise = this.server.sendMessagesUnauth(
number,
identifier,
jsonData,
timestamp,
this.silent,
@ -198,7 +203,7 @@ OutgoingMessage.prototype = {
);
} else {
promise = this.server.sendMessages(
number,
identifier,
jsonData,
timestamp,
this.silent,
@ -212,10 +217,10 @@ OutgoingMessage.prototype = {
// 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.UnregisteredUserError(identifier, e);
}
throw new textsecure.SendMessageNetworkError(
number,
identifier,
jsonData,
e,
timestamp
@ -248,13 +253,17 @@ OutgoingMessage.prototype = {
return this.plaintext;
},
doSendMessage(number, deviceIds, recurse) {
doSendMessage(identifier, deviceIds, recurse) {
const ciphers = {};
const plaintext = this.getPlaintext();
const { numberInfo, senderCertificate } = this;
const info = numberInfo && numberInfo[number] ? numberInfo[number] : {};
const { accessKey } = info || {};
const { sendMetadata } = this;
const info =
sendMetadata && sendMetadata[identifier] ? sendMetadata[identifier] : {};
const { accessKey, useUuidSenderCert } = info || {};
const senderCertificate = useUuidSenderCert
? this.senderCertificateWithUuid
: this.senderCertificate;
if (accessKey && !senderCertificate) {
window.log.warn(
@ -266,8 +275,9 @@ OutgoingMessage.prototype = {
// We don't send to ourselves if unless sealedSender is enabled
const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const ourDeviceId = textsecure.storage.user.getDeviceId();
if (number === ourNumber && !sealedSender) {
if ((identifier === ourNumber || identifier === ourUuid) && !sealedSender) {
// eslint-disable-next-line no-param-reassign
deviceIds = _.reject(
deviceIds,
@ -279,12 +289,15 @@ OutgoingMessage.prototype = {
return Promise.all(
deviceIds.map(async deviceId => {
const address = new libsignal.SignalProtocolAddress(number, deviceId);
const address = new libsignal.SignalProtocolAddress(
identifier,
deviceId
);
const options = {};
// No limit on message keys if we're communicating with our other devices
if (ourNumber === number) {
if (ourNumber === identifier || ourUuid === identifier) {
options.messageKeysLimit = false;
}
@ -299,6 +312,7 @@ OutgoingMessage.prototype = {
senderCertificate,
plaintext
);
return {
type: textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
destinationDeviceId: address.getDeviceId(),
@ -327,18 +341,18 @@ OutgoingMessage.prototype = {
)
.then(jsonData => {
if (sealedSender) {
return this.transmitMessage(number, jsonData, this.timestamp, {
return this.transmitMessage(identifier, jsonData, this.timestamp, {
accessKey,
}).then(
() => {
this.unidentifiedDeliveries.push(number);
this.successfulNumbers.push(number);
this.unidentifiedDeliveries.push(identifier);
this.successfulIdentifiers.push(identifier);
this.numberCompleted();
},
error => {
if (error.code === 401 || error.code === 403) {
if (this.failoverNumbers.indexOf(number) === -1) {
this.failoverNumbers.push(number);
if (this.failoverIdentifiers.indexOf(identifier) === -1) {
this.failoverIdentifiers.push(identifier);
}
if (info) {
info.accessKey = null;
@ -346,7 +360,7 @@ OutgoingMessage.prototype = {
// Set final parameter to true to ensure we don't hit this codepath a
// second time.
return this.doSendMessage(number, deviceIds, recurse, true);
return this.doSendMessage(identifier, deviceIds, recurse, true);
}
throw error;
@ -354,9 +368,9 @@ OutgoingMessage.prototype = {
);
}
return this.transmitMessage(number, jsonData, this.timestamp).then(
return this.transmitMessage(identifier, jsonData, this.timestamp).then(
() => {
this.successfulNumbers.push(number);
this.successfulIdentifiers.push(identifier);
this.numberCompleted();
}
);
@ -369,22 +383,22 @@ OutgoingMessage.prototype = {
) {
if (!recurse)
return this.registerError(
number,
identifier,
'Hit retry limit attempting to reload device list',
error
);
let p;
if (error.code === 409) {
p = this.removeDeviceIdsForNumber(
number,
p = this.removeDeviceIdsForIdentifier(
identifier,
error.response.extraDevices
);
} else {
p = Promise.all(
error.response.staleDevices.map(deviceId =>
ciphers[deviceId].closeOpenSessionForDevice(
new libsignal.SignalProtocolAddress(number, deviceId)
new libsignal.SignalProtocolAddress(identifier, deviceId)
)
)
);
@ -395,10 +409,10 @@ OutgoingMessage.prototype = {
error.code === 410
? error.response.staleDevices
: error.response.missingDevices;
return this.getKeysForNumber(number, resetDevices).then(
return this.getKeysForIdentifier(identifier, resetDevices).then(
// We continue to retry as long as the error code was 409; the assumption is
// that we'll request new device info and the next request will succeed.
this.reloadDevicesAndSend(number, error.code === 409)
this.reloadDevicesAndSend(identifier, error.code === 409)
);
});
} else if (error.message === 'Identity key changed') {
@ -408,13 +422,12 @@ OutgoingMessage.prototype = {
error.originalMessage = this.message.toArrayBuffer();
window.log.error(
'Got "key changed" error from encrypt - no identityKey for application layer',
number,
identifier,
deviceIds
);
const address = new libsignal.SignalProtocolAddress(number, 1);
const identifier = address.toString();
window.log.info('closing all sessions for', number);
window.log.info('closing all sessions for', identifier);
const address = new libsignal.SignalProtocolAddress(identifier, 1);
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
@ -425,7 +438,9 @@ OutgoingMessage.prototype = {
// Primary device
sessionCipher.closeOpenSessionForDevice(),
// The rest of their devices
textsecure.storage.protocol.archiveSiblingSessions(identifier),
textsecure.storage.protocol.archiveSiblingSessions(
address.toString()
),
]).then(
() => {
throw error;
@ -439,65 +454,76 @@ OutgoingMessage.prototype = {
);
}
this.registerError(number, 'Failed to create or send message', error);
this.registerError(
identifier,
'Failed to create or send message',
error
);
return null;
});
},
getStaleDeviceIdsForNumber(number) {
return textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => {
if (deviceIds.length === 0) {
return [1];
}
const updateDevices = [];
return Promise.all(
deviceIds.map(deviceId => {
const address = new libsignal.SignalProtocolAddress(number, deviceId);
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
return sessionCipher.hasOpenSession().then(hasSession => {
if (!hasSession) {
updateDevices.push(deviceId);
}
});
})
).then(() => updateDevices);
});
getStaleDeviceIdsForIdentifier(identifier) {
return textsecure.storage.protocol
.getDeviceIds(identifier)
.then(deviceIds => {
if (deviceIds.length === 0) {
return [1];
}
const updateDevices = [];
return Promise.all(
deviceIds.map(deviceId => {
const address = new libsignal.SignalProtocolAddress(
identifier,
deviceId
);
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
return sessionCipher.hasOpenSession().then(hasSession => {
if (!hasSession) {
updateDevices.push(deviceId);
}
});
})
).then(() => updateDevices);
});
},
removeDeviceIdsForNumber(number, deviceIdsToRemove) {
removeDeviceIdsForIdentifier(identifier, deviceIdsToRemove) {
let promise = Promise.resolve();
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const j in deviceIdsToRemove) {
promise = promise.then(() => {
const encodedNumber = `${number}.${deviceIdsToRemove[j]}`;
return textsecure.storage.protocol.removeSession(encodedNumber);
const encodedAddress = `${identifier}.${deviceIdsToRemove[j]}`;
return textsecure.storage.protocol.removeSession(encodedAddress);
});
}
return promise;
},
async sendToNumber(number) {
async sendToIdentifier(identifier) {
try {
const updateDevices = await this.getStaleDeviceIdsForNumber(number);
await this.getKeysForNumber(number, updateDevices);
await this.reloadDevicesAndSend(number, true)();
const updateDevices = await this.getStaleDeviceIdsForIdentifier(
identifier
);
await this.getKeysForIdentifier(identifier, updateDevices);
await this.reloadDevicesAndSend(identifier, true)();
} catch (error) {
if (error.message === 'Identity key changed') {
// eslint-disable-next-line no-param-reassign
const newError = new textsecure.OutgoingIdentityKeyError(
number,
identifier,
error.originalMessage,
error.timestamp,
error.identityKey
);
this.registerError(number, 'Identity key changed', newError);
this.registerError(identifier, 'Identity key changed', newError);
} else {
this.registerError(
number,
`Failed to retrieve new device keys for number ${number}`,
identifier,
`Failed to retrieve new device keys for number ${identifier}`,
error
);
}

View file

@ -1,4 +1,5 @@
/* global _, textsecure, WebAPI, libsignal, OutgoingMessage, window, dcodeIO */
// eslint-disable-next-line max-len
/* global _, textsecure, WebAPI, libsignal, OutgoingMessage, window, dcodeIO, ConversationController */
/* eslint-disable more/no-then, no-bitwise */
@ -246,18 +247,22 @@ MessageSender.prototype = {
return proto;
},
queueJobForNumber(number, runJob) {
this.pendingMessages[number] =
this.pendingMessages[number] || new window.PQueue({ concurrency: 1 });
async queueJobForIdentifier(identifier, runJob) {
const { id } = await ConversationController.getOrCreateAndWait(
identifier,
'private'
);
this.pendingMessages[id] =
this.pendingMessages[id] || new window.PQueue({ concurrency: 1 });
const queue = this.pendingMessages[number];
const queue = this.pendingMessages[id];
const taskWithTimeout = textsecure.createTaskWithTimeout(
runJob,
`queueJobForNumber ${number}`
`queueJobForIdentifier ${identifier} ${id}`
);
queue.add(taskWithTimeout);
return queue.add(taskWithTimeout);
},
uploadAttachments(message) {
@ -361,7 +366,7 @@ MessageSender.prototype = {
new Promise((resolve, reject) => {
this.sendMessageProto(
message.timestamp,
message.recipients,
message.recipients || [],
message.toProto(),
res => {
res.dataMessage = message.toArrayBuffer();
@ -379,8 +384,8 @@ MessageSender.prototype = {
},
sendMessageProto(
timestamp,
numbers,
message,
recipients,
messageProto,
callback,
silent,
options = {}
@ -388,8 +393,8 @@ MessageSender.prototype = {
const rejections = textsecure.storage.get('signedKeyRotationRejected', 0);
if (rejections > 5) {
throw new textsecure.SignedPreKeyRotationError(
numbers,
message.toArrayBuffer(),
recipients,
messageProto.toArrayBuffer(),
timestamp
);
}
@ -397,19 +402,27 @@ MessageSender.prototype = {
const outgoing = new OutgoingMessage(
this.server,
timestamp,
numbers,
message,
recipients,
messageProto,
silent,
callback,
options
);
numbers.forEach(number => {
this.queueJobForNumber(number, () => outgoing.sendToNumber(number));
recipients.forEach(identifier => {
this.queueJobForIdentifier(identifier, () =>
outgoing.sendToIdentifier(identifier)
);
});
},
sendMessageProtoAndWait(timestamp, numbers, message, silent, options = {}) {
sendMessageProtoAndWait(
timestamp,
identifiers,
messageProto,
silent,
options = {}
) {
return new Promise((resolve, reject) => {
const callback = result => {
if (result && result.errors && result.errors.length > 0) {
@ -421,8 +434,8 @@ MessageSender.prototype = {
this.sendMessageProto(
timestamp,
numbers,
message,
identifiers,
messageProto,
callback,
silent,
options
@ -430,7 +443,7 @@ MessageSender.prototype = {
});
},
sendIndividualProto(number, proto, timestamp, silent, options = {}) {
sendIndividualProto(identifier, proto, timestamp, silent, options = {}) {
return new Promise((resolve, reject) => {
const callback = res => {
if (res && res.errors && res.errors.length > 0) {
@ -441,7 +454,7 @@ MessageSender.prototype = {
};
this.sendMessageProto(
timestamp,
[number],
[identifier],
proto,
callback,
silent,
@ -467,6 +480,7 @@ MessageSender.prototype = {
encodedDataMessage,
timestamp,
destination,
destinationUuid,
expirationStartTimestamp,
sentTo = [],
unidentifiedDeliveries = [],
@ -474,7 +488,9 @@ MessageSender.prototype = {
options
) {
const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') {
return Promise.resolve();
}
@ -488,6 +504,9 @@ MessageSender.prototype = {
if (destination) {
sentMessage.destination = destination;
}
if (destinationUuid) {
sentMessage.destinationUuid = destinationUuid;
}
if (expirationStartTimestamp) {
sentMessage.expirationStartTimestamp = expirationStartTimestamp;
}
@ -508,10 +527,16 @@ MessageSender.prototype = {
// Though this field has 'unidenified' in the name, it should have entries for each
// number we sent to.
if (sentTo && sentTo.length) {
sentMessage.unidentifiedStatus = sentTo.map(number => {
sentMessage.unidentifiedStatus = sentTo.map(identifier => {
const status = new textsecure.protobuf.SyncMessage.Sent.UnidentifiedDeliveryStatus();
status.destination = number;
status.unidentified = Boolean(unidentifiedLookup[number]);
const conv = ConversationController.get(identifier);
if (conv && conv.get('e164')) {
status.destination = conv.get('e164');
}
if (conv && conv.get('uuid')) {
status.destinationUuid = conv.get('uuid');
}
status.unidentified = Boolean(unidentifiedLookup[identifier]);
return status;
});
}
@ -523,7 +548,7 @@ MessageSender.prototype = {
const silent = true;
return this.sendIndividualProto(
myNumber,
myUuid || myNumber,
contentMessage,
timestamp,
silent,
@ -552,6 +577,7 @@ MessageSender.prototype = {
sendRequestBlockSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const request = new textsecure.protobuf.SyncMessage.Request();
@ -563,7 +589,7 @@ MessageSender.prototype = {
const silent = true;
return this.sendIndividualProto(
myNumber,
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
@ -576,6 +602,7 @@ MessageSender.prototype = {
sendRequestConfigurationSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const request = new textsecure.protobuf.SyncMessage.Request();
@ -587,7 +614,7 @@ MessageSender.prototype = {
const silent = true;
return this.sendIndividualProto(
myNumber,
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
@ -600,6 +627,7 @@ MessageSender.prototype = {
sendRequestGroupSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const request = new textsecure.protobuf.SyncMessage.Request();
@ -611,7 +639,7 @@ MessageSender.prototype = {
const silent = true;
return this.sendIndividualProto(
myNumber,
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
@ -624,6 +652,8 @@ MessageSender.prototype = {
sendRequestContactSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const request = new textsecure.protobuf.SyncMessage.Request();
@ -635,7 +665,7 @@ MessageSender.prototype = {
const silent = true;
return this.sendIndividualProto(
myNumber,
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
@ -653,7 +683,8 @@ MessageSender.prototype = {
// We don't want to send typing messages to our other devices, but we will
// in the group case.
const myNumber = textsecure.storage.user.getNumber();
if (recipientId && myNumber === recipientId) {
const myUuid = textsecure.storage.user.getUuid();
if (recipientId && (myNumber === recipientId || myUuid === recipientId)) {
return null;
}
@ -662,7 +693,7 @@ MessageSender.prototype = {
}
const recipients = groupId
? _.without(groupNumbers, myNumber)
? _.without(groupNumbers, myNumber, myUuid)
: [recipientId];
const groupIdBuffer = groupId
? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(groupId)
@ -694,10 +725,14 @@ MessageSender.prototype = {
);
},
sendDeliveryReceipt(recipientId, timestamps, options) {
sendDeliveryReceipt(recipientE164, recipientUuid, timestamps, options) {
const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId();
if (myNumber === recipientId && (myDevice === 1 || myDevice === '1')) {
if (
(myNumber === recipientE164 || myUuid === recipientUuid) &&
(myDevice === 1 || myDevice === '1')
) {
return Promise.resolve();
}
@ -710,7 +745,7 @@ MessageSender.prototype = {
const silent = true;
return this.sendIndividualProto(
recipientId,
recipientUuid || recipientE164,
contentMessage,
Date.now(),
silent,
@ -718,7 +753,7 @@ MessageSender.prototype = {
);
},
sendReadReceipts(sender, timestamps, options) {
sendReadReceipts(senderE164, senderUuid, timestamps, options) {
const receiptMessage = new textsecure.protobuf.ReceiptMessage();
receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ;
receiptMessage.timestamp = timestamps;
@ -728,7 +763,7 @@ MessageSender.prototype = {
const silent = true;
return this.sendIndividualProto(
sender,
senderUuid || senderE164,
contentMessage,
Date.now(),
silent,
@ -737,6 +772,7 @@ MessageSender.prototype = {
},
syncReadMessages(reads, options) {
const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') {
const syncMessage = this.createSyncMessage();
@ -745,6 +781,7 @@ MessageSender.prototype = {
const read = new textsecure.protobuf.SyncMessage.Read();
read.timestamp = reads[i].timestamp;
read.sender = reads[i].sender;
syncMessage.read.push(read);
}
const contentMessage = new textsecure.protobuf.Content();
@ -752,7 +789,7 @@ MessageSender.prototype = {
const silent = true;
return this.sendIndividualProto(
myNumber,
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
@ -763,8 +800,9 @@ MessageSender.prototype = {
return Promise.resolve();
},
async syncViewOnceOpen(sender, timestamp, options) {
async syncViewOnceOpen(sender, senderUuid, timestamp, options) {
const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') {
return null;
@ -774,6 +812,7 @@ MessageSender.prototype = {
const viewOnceOpen = new textsecure.protobuf.SyncMessage.ViewOnceOpen();
viewOnceOpen.sender = sender;
viewOnceOpen.senderUuid = senderUuid;
viewOnceOpen.timestamp = timestamp;
syncMessage.viewOnceOpen = viewOnceOpen;
@ -782,7 +821,7 @@ MessageSender.prototype = {
const silent = true;
return this.sendIndividualProto(
myNumber,
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
@ -797,6 +836,7 @@ MessageSender.prototype = {
}
const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const ENUM = textsecure.protobuf.SyncMessage.StickerPackOperation.Type;
const packOperations = operations.map(item => {
@ -818,15 +858,23 @@ MessageSender.prototype = {
const silent = true;
return this.sendIndividualProto(
myNumber,
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
options
);
},
syncVerification(destination, state, identityKey, options) {
syncVerification(
destinationE164,
destinationUuid,
state,
identityKey,
options
) {
const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId();
const now = Date.now();
@ -850,7 +898,7 @@ MessageSender.prototype = {
// We want the NullMessage to look like a normal outgoing message; not silent
const silent = false;
const promise = this.sendIndividualProto(
destination,
destinationUuid || destinationE164,
contentMessage,
now,
silent,
@ -860,7 +908,12 @@ MessageSender.prototype = {
return promise.then(() => {
const verified = new textsecure.protobuf.Verified();
verified.state = state;
verified.destination = destination;
if (destinationE164) {
verified.destination = destinationE164;
}
if (destinationUuid) {
verified.destinationUuid = destinationUuid;
}
verified.identityKey = identityKey;
verified.nullMessage = nullMessage.padding;
@ -872,7 +925,7 @@ MessageSender.prototype = {
const innerSilent = true;
return this.sendIndividualProto(
myNumber,
myUuid || myNumber,
secondMessage,
now,
innerSilent,
@ -881,13 +934,22 @@ MessageSender.prototype = {
});
},
sendGroupProto(providedNumbers, proto, timestamp = Date.now(), options = {}) {
const me = textsecure.storage.user.getNumber();
const numbers = providedNumbers.filter(number => number !== me);
if (numbers.length === 0) {
sendGroupProto(
providedIdentifiers,
proto,
timestamp = Date.now(),
options = {}
) {
const myE164 = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const identifiers = providedIdentifiers.filter(
id => id !== myE164 && id !== myUuid
);
if (identifiers.length === 0) {
return Promise.resolve({
successfulNumbers: [],
failoverNumbers: [],
successfulIdentifiers: [],
failoverIdentifiers: [],
errors: [],
unidentifiedDeliveries: [],
dataMessage: proto.toArrayBuffer(),
@ -907,7 +969,7 @@ MessageSender.prototype = {
this.sendMessageProto(
timestamp,
numbers,
providedIdentifiers,
proto,
callback,
silent,
@ -917,7 +979,7 @@ MessageSender.prototype = {
},
async getMessageProto(
number,
destination,
body,
attachments,
quote,
@ -930,7 +992,8 @@ MessageSender.prototype = {
flags
) {
const attributes = {
recipients: [number],
recipients: [destination],
destination,
body,
timestamp,
attachments,
@ -958,8 +1021,8 @@ MessageSender.prototype = {
return message.toArrayBuffer();
},
sendMessageToNumber(
number,
sendMessageToIdentifier(
identifier,
messageText,
attachments,
quote,
@ -973,7 +1036,7 @@ MessageSender.prototype = {
) {
return this.sendMessage(
{
recipients: [number],
recipients: [identifier],
body: messageText,
timestamp,
attachments,
@ -988,7 +1051,7 @@ MessageSender.prototype = {
);
},
resetSession(number, timestamp, options) {
resetSession(identifier, timestamp, options) {
window.log.info('resetting secure session');
const silent = false;
const proto = new textsecure.protobuf.DataMessage();
@ -1017,14 +1080,14 @@ MessageSender.prototype = {
)
);
const sendToContactPromise = deleteAllSessions(number)
const sendToContactPromise = deleteAllSessions(identifier)
.catch(logError('resetSession/deleteAllSessions1 error:'))
.then(() => {
window.log.info(
'finished closing local sessions, now sending to contact'
);
return this.sendIndividualProto(
number,
identifier,
proto,
timestamp,
silent,
@ -1032,14 +1095,15 @@ MessageSender.prototype = {
).catch(logError('resetSession/sendToContact error:'));
})
.then(() =>
deleteAllSessions(number).catch(
deleteAllSessions(identifier).catch(
logError('resetSession/deleteAllSessions2 error:')
)
);
const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
// We already sent the reset session to our other devices in the code above!
if (number === myNumber) {
if (identifier === myNumber || identifier === myUuid) {
return sendToContactPromise;
}
@ -1047,7 +1111,7 @@ MessageSender.prototype = {
const sendSyncPromise = this.sendSyncMessage(
buffer,
timestamp,
number,
identifier,
null,
[],
[],
@ -1059,7 +1123,7 @@ MessageSender.prototype = {
async sendMessageToGroup(
groupId,
groupNumbers,
recipients,
messageText,
attachments,
quote,
@ -1071,10 +1135,10 @@ MessageSender.prototype = {
profileKey,
options
) {
const me = textsecure.storage.user.getNumber();
const numbers = groupNumbers.filter(number => number !== me);
const myE164 = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getNumber();
const attrs = {
recipients: numbers,
recipients: recipients.filter(r => r !== myE164 && r !== myUuid),
body: messageText,
timestamp,
attachments,
@ -1090,10 +1154,10 @@ MessageSender.prototype = {
},
};
if (numbers.length === 0) {
if (recipients.length === 0) {
return Promise.resolve({
successfulNumbers: [],
failoverNumbers: [],
successfulIdentifiers: [],
failoverIdentifiers: [],
errors: [],
unidentifiedDeliveries: [],
dataMessage: await this.getMessageProtoObj(attrs),
@ -1103,19 +1167,20 @@ MessageSender.prototype = {
return this.sendMessage(attrs, options);
},
createGroup(targetNumbers, id, name, avatar, options) {
createGroup(targetIdentifiers, id, name, avatar, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(id);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = targetNumbers;
// TODO
proto.group.members = targetIdentifiers;
proto.group.name = name;
return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = attachment;
return this.sendGroupProto(
targetNumbers,
targetIdentifiers,
proto,
Date.now(),
options
@ -1123,19 +1188,19 @@ MessageSender.prototype = {
});
},
updateGroup(groupId, name, avatar, targetNumbers, options) {
updateGroup(groupId, name, avatar, targetIdentifiers, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
proto.group.members = targetNumbers;
proto.group.members = targetIdentifiers;
return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = attachment;
return this.sendGroupProto(
targetNumbers,
targetIdentifiers,
proto,
Date.now(),
options
@ -1143,58 +1208,61 @@ MessageSender.prototype = {
});
},
addNumberToGroup(groupId, newNumbers, options) {
addIdentifierToGroup(groupId, newIdentifiers, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = newNumbers;
return this.sendGroupProto(newNumbers, proto, Date.now(), options);
proto.group.members = newIdentifiers;
return this.sendGroupProto(newIdentifiers, proto, Date.now(), options);
},
setGroupName(groupId, name, groupNumbers, options) {
setGroupName(groupId, name, groupIdentifiers, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name;
proto.group.members = groupNumbers;
proto.group.members = groupIdentifiers;
return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
return this.sendGroupProto(groupIdentifiers, proto, Date.now(), options);
},
setGroupAvatar(groupId, avatar, groupNumbers, options) {
setGroupAvatar(groupId, avatar, groupIdentifiers, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = groupNumbers;
proto.group.members = groupIdentifiers;
return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = attachment;
return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
return this.sendGroupProto(groupIdentifiers, proto, Date.now(), options);
});
},
leaveGroup(groupId, groupNumbers, options) {
leaveGroup(groupId, groupIdentifiers, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT;
return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
return this.sendGroupProto(groupIdentifiers, proto, Date.now(), options);
},
async sendExpirationTimerUpdateToGroup(
groupId,
groupNumbers,
groupIdentifiers,
expireTimer,
timestamp,
profileKey,
options
) {
const me = textsecure.storage.user.getNumber();
const numbers = groupNumbers.filter(number => number !== me);
const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const recipients = groupIdentifiers.filter(
identifier => identifier !== myNumber && identifier !== myUuid
);
const attrs = {
recipients: numbers,
recipients,
timestamp,
expireTimer,
profileKey,
@ -1205,10 +1273,10 @@ MessageSender.prototype = {
},
};
if (numbers.length === 0) {
if (recipients.length === 0) {
return Promise.resolve({
successfulNumbers: [],
failoverNumbers: [],
successfulIdentifiers: [],
failoverIdentifiers: [],
errors: [],
unidentifiedDeliveries: [],
dataMessage: await this.getMessageProtoObj(attrs),
@ -1217,8 +1285,8 @@ MessageSender.prototype = {
return this.sendMessage(attrs, options);
},
sendExpirationTimerUpdateToNumber(
number,
sendExpirationTimerUpdateToIdentifier(
identifier,
expireTimer,
timestamp,
profileKey,
@ -1226,7 +1294,7 @@ MessageSender.prototype = {
) {
return this.sendMessage(
{
recipients: [number],
recipients: [identifier],
timestamp,
expireTimer,
profileKey,
@ -1245,7 +1313,7 @@ window.textsecure = window.textsecure || {};
textsecure.MessageSender = function MessageSenderWrapper(username, password) {
const sender = new MessageSender(username, password);
this.sendExpirationTimerUpdateToNumber = sender.sendExpirationTimerUpdateToNumber.bind(
this.sendExpirationTimerUpdateToIdentifier = sender.sendExpirationTimerUpdateToIdentifier.bind(
sender
);
this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup.bind(
@ -1264,14 +1332,14 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
sender
);
this.sendMessageToNumber = sender.sendMessageToNumber.bind(sender);
this.sendMessageToIdentifier = sender.sendMessageToIdentifier.bind(sender);
this.sendMessage = sender.sendMessage.bind(sender);
this.resetSession = sender.resetSession.bind(sender);
this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender);
this.sendTypingMessage = sender.sendTypingMessage.bind(sender);
this.createGroup = sender.createGroup.bind(sender);
this.updateGroup = sender.updateGroup.bind(sender);
this.addNumberToGroup = sender.addNumberToGroup.bind(sender);
this.addIdentifierToGroup = sender.addIdentifierToGroup.bind(sender);
this.setGroupName = sender.setGroupName.bind(sender);
this.setGroupAvatar = sender.setGroupAvatar.bind(sender);
this.leaveGroup = sender.leaveGroup.bind(sender);

View file

@ -16,13 +16,33 @@
}
},
setUuidAndDeviceId(uuid, deviceId) {
textsecure.storage.put('uuid_id', `${uuid}.${deviceId}`);
},
getNumber() {
const numberId = textsecure.storage.get('number_id');
if (numberId === undefined) return undefined;
return textsecure.utils.unencodeNumber(numberId)[0];
},
getUuid() {
const uuid = textsecure.storage.get('uuid_id');
if (uuid === undefined) return undefined;
return textsecure.utils.unencodeNumber(uuid)[0];
},
getDeviceId() {
return this._getDeviceIdFromUuid() || this._getDeviceIdFromNumber();
},
_getDeviceIdFromUuid() {
const uuid = textsecure.storage.get('uuid_id');
if (uuid === undefined) return undefined;
return textsecure.utils.unencodeNumber(uuid)[1];
},
_getDeviceIdFromNumber() {
const numberId = textsecure.storage.get('number_id');
if (numberId === undefined) return undefined;
return textsecure.utils.unencodeNumber(numberId)[1];

View file

@ -13,6 +13,7 @@ describe('ContactBuffer', () => {
const contactInfo = new textsecure.protobuf.ContactDetails({
name: 'Zero Cool',
number: '+10000000000',
uuid: '7198E1BD-1293-452A-A098-F982FF201902',
avatar: { contentType: 'image/jpeg', length: avatarLen },
});
const contactInfoBuffer = contactInfo.encode().toArrayBuffer();
@ -37,6 +38,7 @@ describe('ContactBuffer', () => {
count += 1;
assert.strictEqual(contact.name, 'Zero Cool');
assert.strictEqual(contact.number, '+10000000000');
assert.strictEqual(contact.uuid, '7198e1bd-1293-452a-a098-f982ff201902');
assert.strictEqual(contact.avatar.contentType, 'image/jpeg');
assert.strictEqual(contact.avatar.length, 255);
assert.strictEqual(contact.avatar.data.byteLength, 255);
@ -63,7 +65,13 @@ describe('GroupBuffer', () => {
const groupInfo = new textsecure.protobuf.GroupDetails({
id: new Uint8Array([1, 3, 3, 7]).buffer,
name: 'Hackers',
members: ['cereal', 'burn', 'phreak', 'joey'],
membersE164: ['cereal', 'burn', 'phreak', 'joey'],
members: [
{ uuid: '3EA23646-92E8-4604-8833-6388861971C1', e164: 'cereal' },
{ uuid: 'B8414169-7149-4736-8E3B-477191931301', e164: 'burn' },
{ uuid: '64C97B95-A782-4E1E-BBCC-5A4ACE8d71f6', e164: 'phreak' },
{ uuid: 'CA334652-C35B-4FDC-9CC7-5F2060C771EE', e164: 'joey' },
],
avatar: { contentType: 'image/jpeg', length: avatarLen },
});
const groupInfoBuffer = groupInfo.encode().toArrayBuffer();
@ -91,7 +99,21 @@ describe('GroupBuffer', () => {
group.id.toArrayBuffer(),
new Uint8Array([1, 3, 3, 7]).buffer
);
assert.sameMembers(group.members, ['cereal', 'burn', 'phreak', 'joey']);
assert.sameMembers(group.membersE164, [
'cereal',
'burn',
'phreak',
'joey',
]);
assert.sameDeepMembers(
group.members.map(({ uuid, e164 }) => ({ uuid, e164 })),
[
{ uuid: '3ea23646-92e8-4604-8833-6388861971c1', e164: 'cereal' },
{ uuid: 'b8414169-7149-4736-8e3b-477191931301', e164: 'burn' },
{ uuid: '64c97b95-a782-4e1e-bbcc-5a4ace8d71f6', e164: 'phreak' },
{ uuid: 'ca334652-c35b-4fdc-9cc7-5f2060c771ee', e164: 'joey' },
]
);
assert.strictEqual(group.avatar.contentType, 'image/jpeg');
assert.strictEqual(group.avatar.length, 255);
assert.strictEqual(group.avatar.data.byteLength, 255);

View file

@ -1,6 +1,6 @@
window.setImmediate = window.nodeSetImmediate;
const getKeysForNumberMap = {};
const getKeysForIdentifierMap = {};
const messagesSentMap = {};
const fakeCall = () => Promise.resolve();
@ -10,7 +10,7 @@ const fakeAPI = {
getAttachment: fakeCall,
getAvatar: fakeCall,
getDevices: fakeCall,
// getKeysForNumber: fakeCall,
// getKeysForIdentifier : fakeCall,
getMessageSocket: fakeCall,
getMyKeys: fakeCall,
getProfile: fakeCall,
@ -22,13 +22,13 @@ const fakeAPI = {
// sendMessages: fakeCall,
setSignedPreKey: fakeCall,
getKeysForNumber(number) {
const res = getKeysForNumberMap[number];
getKeysForIdentifier(number) {
const res = getKeysForIdentifierMap[number];
if (res !== undefined) {
delete getKeysForNumberMap[number];
delete getKeysForIdentifierMap[number];
return Promise.resolve(res);
}
throw new Error('getKeysForNumber of unknown/used number');
throw new Error('getKeysForIdentfier of unknown/used number');
},
sendMessages(destination, messageArray) {

View file

@ -4,6 +4,12 @@ function SignalProtocolStore() {
SignalProtocolStore.prototype = {
Direction: { SENDING: 1, RECEIVING: 2 },
VerifiedStatus: {
DEFAULT: 0,
VERIFIED: 1,
UNVERIFIED: 2,
},
getIdentityKeyPair() {
return Promise.resolve(this.get('identityKey'));
},

View file

@ -23,7 +23,6 @@
<script type="text/javascript" src="../protobufs.js" data-cover></script>
<script type="text/javascript" src="../errors.js" data-cover></script>
<script type="text/javascript" src="../storage.js" data-cover></script>
<script type="text/javascript" src="../protocol_wrapper.js" data-cover></script>
<script type="text/javascript" src="../event_target.js" data-cover></script>
<script type="text/javascript" src="../websocket-resources.js" data-cover></script>
@ -34,6 +33,16 @@
<script type="text/javascript" src="../account_manager.js" data-cover></script>
<script type="text/javascript" src="../contacts_parser.js" data-cover></script>
<script type="text/javascript" src="../task_with_timeout.js" data-cover></script>
<script type="text/javascript" src="../storage/user.js" data-cover></script>
<script type="text/javascript" src="../protocol_wrapper.js" data-cover></script>
<script type="text/javascript" src="../../js/libphonenumber-util.js"></script>
<script type="text/javascript" src="../../js/components.js" data-cover></script>
<script type="text/javascript" src="../../js/signal_protocol_store.js" data-cover></script>
<script type="text/javascript" src="../../js/storage.js" data-cover></script>
<script type="text/javascript" src="../../js/models/messages.js" data-cover></script>
<script type="text/javascript" src="../../js/models/conversations.js" data-cover></script>
<script type="text/javascript" src="../../js/conversation_controller.js" data-cover></script>
<script type="text/javascript" src="errors_test.js"></script>
<script type="text/javascript" src="helpers_test.js"></script>

View file

@ -4,12 +4,14 @@ describe('MessageReceiver', () => {
textsecure.storage.impl = new SignalProtocolStore();
const { WebSocket } = window;
const number = '+19999999999';
const uuid = 'AAAAAAAA-BBBB-4CCC-9DDD-EEEEEEEEEEEE';
const deviceId = 1;
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
before(() => {
window.WebSocket = MockSocket;
textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name');
textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId);
textsecure.storage.put('password', 'password');
textsecure.storage.put('signaling_key', signalingKey);
});
@ -21,6 +23,7 @@ describe('MessageReceiver', () => {
const attrs = {
type: textsecure.protobuf.Envelope.Type.CIPHERTEXT,
source: number,
sourceUuid: uuid,
sourceDevice: deviceId,
timestamp: Date.now(),
};
@ -72,7 +75,7 @@ describe('MessageReceiver', () => {
it('connects', done => {
const mockServer = new MockServer(
`ws://localhost:8080/v1/websocket/?login=${encodeURIComponent(
number
uuid
)}.1&password=password`
);

View file

@ -1,9 +1,7 @@
/* global libsignal, textsecure */
/* global libsignal, textsecure, storage, ConversationController */
describe('SignalProtocolStore', () => {
before(() => {
localStorage.clear();
});
// debugger;
const store = textsecure.storage.protocol;
const identifier = '+5558675309';
const identityKey = {
@ -14,6 +12,14 @@ describe('SignalProtocolStore', () => {
pubKey: libsignal.crypto.getRandomBytes(33),
privKey: libsignal.crypto.getRandomBytes(32),
};
before(async () => {
localStorage.clear();
ConversationController.reset();
// store.hydrateCaches();
await storage.fetch();
await ConversationController.load();
await ConversationController.getOrCreateAndWait(identifier, 'private');
});
it('retrieves my registration id', async () => {
store.put('registrationId', 1337);