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

@ -1,6 +1,7 @@
const { join } = require('path'); const { join } = require('path');
const mkdirp = require('mkdirp'); const mkdirp = require('mkdirp');
const rimraf = require('rimraf'); const rimraf = require('rimraf');
const Queue = require('p-queue').default;
const sql = require('@journeyapps/sqlcipher'); const sql = require('@journeyapps/sqlcipher');
const { app, dialog, clipboard } = require('electron'); const { app, dialog, clipboard } = require('electron');
const { redactAll } = require('../js/modules/privacy'); const { redactAll } = require('../js/modules/privacy');
@ -15,6 +16,7 @@ const {
isNumber, isNumber,
isObject, isObject,
isString, isString,
keyBy,
last, last,
map, map,
pick, pick,
@ -57,10 +59,10 @@ module.exports = {
createOrUpdateSession, createOrUpdateSession,
createOrUpdateSessions, createOrUpdateSessions,
getSessionById, getSessionById,
getSessionsByNumber, getSessionsById,
bulkAddSessions, bulkAddSessions,
removeSessionById, removeSessionById,
removeSessionsByNumber, removeSessionsById,
removeAllSessions, removeAllSessions,
getAllSessions, getAllSessions,
@ -1181,6 +1183,7 @@ async function updateToSchemaVersion18(currentVersion, instance) {
throw error; throw error;
} }
} }
async function updateToSchemaVersion19(currentVersion, instance) { async function updateToSchemaVersion19(currentVersion, instance) {
if (currentVersion >= 19) { if (currentVersion >= 19) {
return; return;
@ -1210,6 +1213,230 @@ async function updateToSchemaVersion19(currentVersion, instance) {
throw error; throw error;
} }
} }
async function updateToSchemaVersion20(currentVersion, instance) {
if (currentVersion >= 20) {
return;
}
console.log('updateToSchemaVersion20: starting...');
await instance.run('BEGIN TRANSACTION;');
try {
const migrationJobQueue = new Queue({ concurrency: 10 });
// The triggers on the messages table slow down this migration
// significantly, so we drop them and recreate them later.
// Drop triggers
const triggers = await instance.all(
'SELECT * FROM sqlite_master WHERE type = "trigger" AND tbl_name = "messages"'
);
// eslint-disable-next-line no-restricted-syntax
for (const trigger of triggers) {
// eslint-disable-next-line no-await-in-loop
await instance.run(`DROP TRIGGER ${trigger.name}`);
}
// Create new columns and indices
await instance.run('ALTER TABLE conversations ADD COLUMN e164 TEXT;');
await instance.run('ALTER TABLE conversations ADD COLUMN uuid TEXT;');
await instance.run('ALTER TABLE conversations ADD COLUMN groupId TEXT;');
await instance.run('ALTER TABLE messages ADD COLUMN sourceUuid TEXT;');
await instance.run(
'ALTER TABLE sessions RENAME COLUMN number TO conversationId;'
);
await instance.run(
'CREATE INDEX conversations_e164 ON conversations(e164);'
);
await instance.run(
'CREATE INDEX conversations_uuid ON conversations(uuid);'
);
await instance.run(
'CREATE INDEX conversations_groupId ON conversations(groupId);'
);
await instance.run(
'CREATE INDEX messages_sourceUuid on messages(sourceUuid);'
);
// Migrate existing IDs
await instance.run(
"UPDATE conversations SET e164 = '+' || id WHERE type = 'private';"
);
await instance.run(
"UPDATE conversations SET groupId = id WHERE type = 'group';"
);
// Drop invalid groups and any associated messages
const maybeInvalidGroups = await instance.all(
"SELECT * FROM conversations WHERE type = 'group' AND members IS NULL;"
);
// eslint-disable-next-line no-restricted-syntax
for (const group of maybeInvalidGroups) {
const json = JSON.parse(group.json);
if (!json.members || !json.members.length) {
// eslint-disable-next-line no-await-in-loop
await instance.run('DELETE FROM conversations WHERE id = $id;', {
$id: json.id,
});
// eslint-disable-next-line no-await-in-loop
await instance.run('DELETE FROM messages WHERE conversationId = $id;', {
$id: json.id,
});
// eslint-disable-next-line no-await-in-loop
// await instance.run('DELETE FROM sessions WHERE conversationId = $id;', {
// $id: json.id,
// });
}
}
// Generate new IDs and alter data
const allConversations = await instance.all('SELECT * FROM conversations;');
const allConversationsByOldId = keyBy(allConversations, 'id');
// eslint-disable-next-line no-restricted-syntax
for (const row of allConversations) {
const oldId = row.id;
const newId = generateUUID();
allConversationsByOldId[oldId].id = newId;
const patchObj = { id: newId };
if (row.type === 'private') {
patchObj.e164 = `+${oldId}`;
} else if (row.type === 'group') {
patchObj.groupId = oldId;
}
const patch = JSON.stringify(patchObj);
// eslint-disable-next-line no-await-in-loop
await instance.run(
'UPDATE conversations SET id = $newId, json = JSON_PATCH(json, $patch) WHERE id = $oldId',
{
$newId: newId,
$oldId: oldId,
$patch: patch,
}
);
const messagePatch = JSON.stringify({ conversationId: newId });
// eslint-disable-next-line no-await-in-loop
await instance.run(
'UPDATE messages SET conversationId = $newId, json = JSON_PATCH(json, $patch) WHERE conversationId = $oldId',
{ $newId: newId, $oldId: oldId, $patch: messagePatch }
);
}
const groupConverations = await instance.all(
"SELECT * FROM conversations WHERE type = 'group';"
);
// Update group conversations, point members at new conversation ids
migrationJobQueue.addAll(
groupConverations.map(groupRow => async () => {
const members = groupRow.members.split(/\s?\+/).filter(Boolean);
const newMembers = [];
// eslint-disable-next-line no-restricted-syntax
for (const m of members) {
const memberRow = allConversationsByOldId[m];
if (memberRow) {
newMembers.push(memberRow.id);
} else {
// We didn't previously have a private conversation for this member,
// we need to create one
const id = generateUUID();
// eslint-disable-next-line no-await-in-loop
await saveConversation(
{
id,
e164: m,
type: 'private',
version: 2,
unreadCount: 0,
verified: 0,
},
instance
);
newMembers.push(id);
}
}
const json = { ...jsonToObject(groupRow.json), members: newMembers };
const newMembersValue = newMembers.join(' ');
await instance.run(
'UPDATE conversations SET members = $newMembersValue, json = $newJsonValue WHERE id = $id',
{
$id: groupRow.id,
$newMembersValue: newMembersValue,
$newJsonValue: objectToJSON(json),
}
);
})
);
// Wait for group conversation updates to finish
await migrationJobQueue.onEmpty();
// Update sessions to stable IDs
const allSessions = await instance.all('SELECT * FROM sessions;');
// eslint-disable-next-line no-restricted-syntax
for (const session of allSessions) {
// Not using patch here so we can explicitly delete a property rather than
// implicitly delete via null
const newJson = JSON.parse(session.json);
const conversation = allConversationsByOldId[newJson.number.substr(1)];
if (conversation) {
newJson.conversationId = conversation.id;
newJson.id = `${newJson.conversationId}.${newJson.deviceId}`;
}
delete newJson.number;
// eslint-disable-next-line no-await-in-loop
await instance.run(
`
UPDATE sessions
SET id = $newId, json = $newJson, conversationId = $newConversationId
WHERE id = $oldId
`,
{
$newId: newJson.id,
$newJson: objectToJSON(newJson),
$oldId: session.id,
$newConversationId: newJson.conversationId,
}
);
}
// Update identity keys to stable IDs
const allIdentityKeys = await instance.all('SELECT * FROM identityKeys;');
// eslint-disable-next-line no-restricted-syntax
for (const identityKey of allIdentityKeys) {
const newJson = JSON.parse(identityKey.json);
newJson.id = allConversationsByOldId[newJson.id];
// eslint-disable-next-line no-await-in-loop
await instance.run(
`
UPDATE identityKeys
SET id = $newId, json = $newJson
WHERE id = $oldId
`,
{
$newId: newJson.id,
$newJson: objectToJSON(newJson),
$oldId: identityKey.id,
}
);
}
// Recreate triggers
// eslint-disable-next-line no-restricted-syntax
for (const trigger of triggers) {
// eslint-disable-next-line no-await-in-loop
await instance.run(trigger.sql);
}
await instance.run('PRAGMA user_version = 20;');
await instance.run('COMMIT TRANSACTION;');
} catch (error) {
await instance.run('ROLLBACK;');
throw error;
}
}
const SCHEMA_VERSIONS = [ const SCHEMA_VERSIONS = [
updateToSchemaVersion1, updateToSchemaVersion1,
updateToSchemaVersion2, updateToSchemaVersion2,
@ -1230,6 +1457,7 @@ const SCHEMA_VERSIONS = [
updateToSchemaVersion17, updateToSchemaVersion17,
updateToSchemaVersion18, updateToSchemaVersion18,
updateToSchemaVersion19, updateToSchemaVersion19,
updateToSchemaVersion20,
]; ];
async function updateSchema(instance) { async function updateSchema(instance) {
@ -1479,31 +1707,31 @@ async function removeAllItems() {
const SESSIONS_TABLE = 'sessions'; const SESSIONS_TABLE = 'sessions';
async function createOrUpdateSession(data) { async function createOrUpdateSession(data) {
const { id, number } = data; const { id, conversationId } = data;
if (!id) { if (!id) {
throw new Error( throw new Error(
'createOrUpdateSession: Provided data did not have a truthy id' 'createOrUpdateSession: Provided data did not have a truthy id'
); );
} }
if (!number) { if (!conversationId) {
throw new Error( throw new Error(
'createOrUpdateSession: Provided data did not have a truthy number' 'createOrUpdateSession: Provided data did not have a truthy conversationId'
); );
} }
await db.run( await db.run(
`INSERT OR REPLACE INTO sessions ( `INSERT OR REPLACE INTO sessions (
id, id,
number, conversationId,
json json
) values ( ) values (
$id, $id,
$number, $conversationId,
$json $json
)`, )`,
{ {
$id: id, $id: id,
$number: number, $conversationId: conversationId,
$json: objectToJSON(data), $json: objectToJSON(data),
} }
); );
@ -1524,10 +1752,13 @@ createOrUpdateSessions.needsSerial = true;
async function getSessionById(id) { async function getSessionById(id) {
return getById(SESSIONS_TABLE, id); return getById(SESSIONS_TABLE, id);
} }
async function getSessionsByNumber(number) { async function getSessionsById(id) {
const rows = await db.all('SELECT * FROM sessions WHERE number = $number;', { const rows = await db.all(
$number: number, 'SELECT * FROM sessions WHERE conversationId = $id;',
}); {
$id: id,
}
);
return map(rows, row => jsonToObject(row.json)); return map(rows, row => jsonToObject(row.json));
} }
async function bulkAddSessions(array) { async function bulkAddSessions(array) {
@ -1536,9 +1767,9 @@ async function bulkAddSessions(array) {
async function removeSessionById(id) { async function removeSessionById(id) {
return removeById(SESSIONS_TABLE, id); return removeById(SESSIONS_TABLE, id);
} }
async function removeSessionsByNumber(number) { async function removeSessionsById(id) {
await db.run('DELETE FROM sessions WHERE number = $number;', { await db.run('DELETE FROM sessions WHERE conversationId = $id;', {
$number: number, $id: id,
}); });
} }
async function removeAllSessions() { async function removeAllSessions() {
@ -1634,23 +1865,30 @@ async function getConversationCount() {
return row['count(*)']; return row['count(*)'];
} }
async function saveConversation(data) { async function saveConversation(data, instance = db) {
const { const {
id,
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
active_at, active_at,
type, e164,
groupId,
id,
members, members,
name, name,
profileName,
profileFamilyName, profileFamilyName,
profileName,
type,
uuid,
} = data; } = data;
await db.run( await instance.run(
`INSERT INTO conversations ( `INSERT INTO conversations (
id, id,
json, json,
e164,
uuid,
groupId,
active_at, active_at,
type, type,
members, members,
@ -1662,6 +1900,10 @@ async function saveConversation(data) {
$id, $id,
$json, $json,
$e164,
$uuid,
$groupId,
$active_at, $active_at,
$type, $type,
$members, $members,
@ -1674,6 +1916,10 @@ async function saveConversation(data) {
$id: id, $id: id,
$json: objectToJSON(data), $json: objectToJSON(data),
$e164: e164,
$uuid: uuid,
$groupId: groupId,
$active_at: active_at, $active_at: active_at,
$type: type, $type: type,
$members: members ? members.join(' ') : null, $members: members ? members.join(' ') : null,
@ -1713,12 +1959,17 @@ async function updateConversation(data) {
name, name,
profileName, profileName,
profileFamilyName, profileFamilyName,
e164,
uuid,
} = data; } = data;
await db.run( await db.run(
`UPDATE conversations SET `UPDATE conversations SET
json = $json, json = $json,
e164 = $e164,
uuid = $uuid,
active_at = $active_at, active_at = $active_at,
type = $type, type = $type,
members = $members, members = $members,
@ -1731,6 +1982,9 @@ async function updateConversation(data) {
$id: id, $id: id,
$json: objectToJSON(data), $json: objectToJSON(data),
$e164: e164,
$uuid: uuid,
$active_at: active_at, $active_at: active_at,
$type: type, $type: type,
$members: members ? members.join(' ') : null, $members: members ? members.join(' ') : null,
@ -1920,6 +2174,7 @@ async function saveMessage(data, { forceSave } = {}) {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
sent_at, sent_at,
source, source,
sourceUuid,
sourceDevice, sourceDevice,
type, type,
unread, unread,
@ -1945,6 +2200,7 @@ async function saveMessage(data, { forceSave } = {}) {
$schemaVersion: schemaVersion, $schemaVersion: schemaVersion,
$sent_at: sent_at, $sent_at: sent_at,
$source: source, $source: source,
$sourceUuid: sourceUuid,
$sourceDevice: sourceDevice, $sourceDevice: sourceDevice,
$type: type, $type: type,
$unread: unread, $unread: unread,
@ -1970,6 +2226,7 @@ async function saveMessage(data, { forceSave } = {}) {
schemaVersion = $schemaVersion, schemaVersion = $schemaVersion,
sent_at = $sent_at, sent_at = $sent_at,
source = $source, source = $source,
sourceUuid = $sourceUuid,
sourceDevice = $sourceDevice, sourceDevice = $sourceDevice,
type = $type, type = $type,
unread = $unread unread = $unread
@ -2004,6 +2261,7 @@ async function saveMessage(data, { forceSave } = {}) {
schemaVersion, schemaVersion,
sent_at, sent_at,
source, source,
sourceUuid,
sourceDevice, sourceDevice,
type, type,
unread unread
@ -2025,6 +2283,7 @@ async function saveMessage(data, { forceSave } = {}) {
$schemaVersion, $schemaVersion,
$sent_at, $sent_at,
$source, $source,
$sourceUuid,
$sourceDevice, $sourceDevice,
$type, $type,
$unread $unread
@ -2095,14 +2354,21 @@ async function getAllMessageIds() {
} }
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
async function getMessageBySender({ source, sourceDevice, sent_at }) { async function getMessageBySender({
source,
sourceUuid,
sourceDevice,
// eslint-disable-next-line camelcase
sent_at,
}) {
const rows = await db.all( const rows = await db.all(
`SELECT json FROM messages WHERE `SELECT json FROM messages WHERE
source = $source AND (source = $source OR sourceUuid = $sourceUuid) AND
sourceDevice = $sourceDevice AND sourceDevice = $sourceDevice AND
sent_at = $sent_at;`, sent_at = $sent_at;`,
{ {
$source: source, $source: source,
$sourceUuid: sourceUuid,
$sourceDevice: sourceDevice, $sourceDevice: sourceDevice,
$sent_at: sent_at, $sent_at: sent_at,
} }

View file

@ -25,7 +25,7 @@
wait: 500, wait: 500,
maxSize: 500, maxSize: 500,
processBatch: async items => { processBatch: async items => {
const bySource = _.groupBy(items, item => item.source); const bySource = _.groupBy(items, item => item.source || item.sourceUuid);
const sources = Object.keys(bySource); const sources = Object.keys(bySource);
for (let i = 0, max = sources.length; i < max; i += 1) { for (let i = 0, max = sources.length; i < max; i += 1) {
@ -33,13 +33,15 @@
const timestamps = bySource[source].map(item => item.timestamp); const timestamps = bySource[source].map(item => item.timestamp);
try { try {
const c = ConversationController.get(source);
const { wrap, sendOptions } = ConversationController.prepareForSend( const { wrap, sendOptions } = ConversationController.prepareForSend(
source c.get('id')
); );
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await wrap( await wrap(
textsecure.messaging.sendDeliveryReceipt( textsecure.messaging.sendDeliveryReceipt(
source, c.get('e164'),
c.get('uuid'),
timestamps, timestamps,
sendOptions sendOptions
) )
@ -234,13 +236,23 @@
let accountManager; let accountManager;
window.getAccountManager = () => { window.getAccountManager = () => {
if (!accountManager) { if (!accountManager) {
const USERNAME = storage.get('number_id'); const OLD_USERNAME = storage.get('number_id');
const USERNAME = storage.get('uuid_id');
const PASSWORD = storage.get('password'); const PASSWORD = storage.get('password');
accountManager = new textsecure.AccountManager(USERNAME, PASSWORD); accountManager = new textsecure.AccountManager(
USERNAME || OLD_USERNAME,
PASSWORD
);
accountManager.addEventListener('registration', () => { accountManager.addEventListener('registration', () => {
const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const user = { const user = {
regionCode: window.storage.get('regionCode'), regionCode: window.storage.get('regionCode'),
ourNumber: textsecure.storage.user.getNumber(), ourNumber,
ourUuid,
ourConversationId: ConversationController.getConversationId(
ourNumber || ourUuid
),
}; };
Whisper.events.trigger('userChanged', user); Whisper.events.trigger('userChanged', user);
@ -580,6 +592,11 @@
const conversations = convoCollection.map( const conversations = convoCollection.map(
conversation => conversation.cachedProps conversation => conversation.cachedProps
); );
const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const ourConversationId = ConversationController.getConversationId(
ourNumber || ourUuid
);
const initialState = { const initialState = {
conversations: { conversations: {
conversationLookup: Signal.Util.makeLookup(conversations, 'id'), conversationLookup: Signal.Util.makeLookup(conversations, 'id'),
@ -598,7 +615,9 @@
stickersPath: window.baseStickersPath, stickersPath: window.baseStickersPath,
tempPath: window.baseTempPath, tempPath: window.baseTempPath,
regionCode: window.storage.get('regionCode'), regionCode: window.storage.get('regionCode'),
ourNumber: textsecure.storage.user.getNumber(), ourConversationId,
ourNumber,
ourUuid,
platform: window.platform, platform: window.platform,
i18n: window.i18n, i18n: window.i18n,
interactionMode: window.getInteractionMode(), interactionMode: window.getInteractionMode(),
@ -1508,7 +1527,8 @@
messageReceiver = null; messageReceiver = null;
} }
const USERNAME = storage.get('number_id'); const OLD_USERNAME = storage.get('number_id');
const USERNAME = storage.get('uuid_id');
const PASSWORD = storage.get('password'); const PASSWORD = storage.get('password');
const mySignalingKey = storage.get('signaling_key'); const mySignalingKey = storage.get('signaling_key');
@ -1524,6 +1544,7 @@
// initialize the socket and start listening for messages // initialize the socket and start listening for messages
window.log.info('Initializing socket and listening for messages'); window.log.info('Initializing socket and listening for messages');
messageReceiver = new textsecure.MessageReceiver( messageReceiver = new textsecure.MessageReceiver(
OLD_USERNAME,
USERNAME, USERNAME,
PASSWORD, PASSWORD,
mySignalingKey, mySignalingKey,
@ -1569,7 +1590,7 @@
}); });
window.textsecure.messaging = new textsecure.MessageSender( window.textsecure.messaging = new textsecure.MessageSender(
USERNAME, USERNAME || OLD_USERNAME,
PASSWORD PASSWORD
); );
@ -1605,7 +1626,10 @@
const udSupportKey = 'hasRegisterSupportForUnauthenticatedDelivery'; const udSupportKey = 'hasRegisterSupportForUnauthenticatedDelivery';
if (!storage.get(udSupportKey)) { if (!storage.get(udSupportKey)) {
const server = WebAPI.connect({ username: USERNAME, password: PASSWORD }); const server = WebAPI.connect({
username: USERNAME || OLD_USERNAME,
password: PASSWORD,
});
try { try {
await server.registerSupportForUnauthenticatedDelivery(); await server.registerSupportForUnauthenticatedDelivery();
storage.put(udSupportKey, true); storage.put(udSupportKey, true);
@ -1617,7 +1641,50 @@
} }
} }
const hasRegisteredUuidSupportKey = 'hasRegisteredUuidSupport';
if (
!storage.get(hasRegisteredUuidSupportKey) &&
textsecure.storage.user.getUuid()
) {
const server = WebAPI.connect({
username: USERNAME || OLD_USERNAME,
password: PASSWORD,
});
try {
await server.registerCapabilities({ uuid: true });
storage.put(hasRegisteredUuidSupportKey, true);
} catch (error) {
window.log.error(
'Error: Unable to register support for UUID messages.',
error && error.stack ? error.stack : error
);
}
}
const deviceId = textsecure.storage.user.getDeviceId(); const deviceId = textsecure.storage.user.getDeviceId();
if (!textsecure.storage.user.getUuid()) {
const server = WebAPI.connect({
username: OLD_USERNAME,
password: PASSWORD,
});
try {
const { uuid } = await server.whoami();
textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId);
const ourNumber = textsecure.storage.user.getNumber();
const me = await ConversationController.getOrCreateAndWait(
ourNumber,
'private'
);
me.updateUuid(uuid);
} catch (error) {
window.log.error(
'Error: Unable to retrieve UUID from service.',
error && error.stack ? error.stack : error
);
}
}
if (firstRun === true && deviceId !== '1') { if (firstRun === true && deviceId !== '1') {
const hasThemeSetting = Boolean(storage.get('theme-setting')); const hasThemeSetting = Boolean(storage.get('theme-setting'));
if (!hasThemeSetting && textsecure.storage.get('userAgent') === 'OWI') { if (!hasThemeSetting && textsecure.storage.get('userAgent') === 'OWI') {
@ -1639,9 +1706,10 @@
Whisper.events.trigger('contactsync'); Whisper.events.trigger('contactsync');
}); });
const ourUuid = textsecure.storage.user.getUuid();
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const { wrap, sendOptions } = ConversationController.prepareForSend( const { wrap, sendOptions } = ConversationController.prepareForSend(
ourNumber, ourNumber || ourUuid,
{ {
syncMessage: true, syncMessage: true,
} }
@ -1766,7 +1834,7 @@
function onTyping(ev) { function onTyping(ev) {
// Note: this type of message is automatically removed from cache in MessageReceiver // Note: this type of message is automatically removed from cache in MessageReceiver
const { typing, sender, senderDevice } = ev; const { typing, sender, senderUuid, senderDevice } = ev;
const { groupId, started } = typing || {}; const { groupId, started } = typing || {};
// We don't do anything with incoming typing messages if the setting is disabled // We don't do anything with incoming typing messages if the setting is disabled
@ -1774,12 +1842,18 @@
return; return;
} }
const conversation = ConversationController.get(groupId || sender); const conversation = ConversationController.get(
groupId || sender || senderUuid
);
const ourUuid = textsecure.storage.user.getUuid();
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
if (conversation) { if (conversation) {
// We drop typing notifications in groups we're not a part of // We drop typing notifications in groups we're not a part of
if (!conversation.isPrivate() && !conversation.hasMember(ourNumber)) { if (
!conversation.isPrivate() &&
!conversation.hasMember(ourNumber || ourUuid)
) {
window.log.warn( window.log.warn(
`Received typing indicator for group ${conversation.idForLogging()}, which we're not a part of. Dropping.` `Received typing indicator for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
); );
@ -1789,6 +1863,7 @@
conversation.notifyTyping({ conversation.notifyTyping({
isTyping: started, isTyping: started,
sender, sender,
senderUuid,
senderDevice, senderDevice,
}); });
} }
@ -1833,9 +1908,10 @@
async function onContactReceived(ev) { async function onContactReceived(ev) {
const details = ev.contactDetails; const details = ev.contactDetails;
const id = details.number; if (
details.number === textsecure.storage.user.getNumber() ||
if (id === textsecure.storage.user.getNumber()) { details.uuid === textsecure.storage.user.getUuid()
) {
// special case for syncing details about ourselves // special case for syncing details about ourselves
if (details.profileKey) { if (details.profileKey) {
window.log.info('Got sync message with our own profile key'); window.log.info('Got sync message with our own profile key');
@ -1844,9 +1920,11 @@
} }
const c = new Whisper.Conversation({ const c = new Whisper.Conversation({
id, e164: details.number,
uuid: details.uuid,
type: 'private',
}); });
const validationError = c.validateNumber(); const validationError = c.validate();
if (validationError) { if (validationError) {
window.log.error( window.log.error(
'Invalid contact received:', 'Invalid contact received:',
@ -1857,7 +1935,7 @@
try { try {
const conversation = await ConversationController.getOrCreateAndWait( const conversation = await ConversationController.getOrCreateAndWait(
id, details.number || details.uuid,
'private' 'private'
); );
let activeAt = conversation.get('active_at'); let activeAt = conversation.get('active_at');
@ -1878,10 +1956,18 @@
} }
if (typeof details.blocked !== 'undefined') { if (typeof details.blocked !== 'undefined') {
if (details.blocked) { const e164 = conversation.get('e164');
storage.addBlockedNumber(id); if (details.blocked && e164) {
storage.addBlockedNumber(e164);
} else { } else {
storage.removeBlockedNumber(id); storage.removeBlockedNumber(e164);
}
const uuid = conversation.get('uuid');
if (details.blocked && uuid) {
storage.addBlockedUuid(uuid);
} else {
storage.removeBlockedUuid(uuid);
} }
} }
@ -1912,17 +1998,21 @@
conversation.set({ avatar: null }); conversation.set({ avatar: null });
} }
window.Signal.Data.updateConversation(id, conversation.attributes); window.Signal.Data.updateConversation(
details.number || details.uuid,
conversation.attributes
);
const { expireTimer } = details; const { expireTimer } = details;
const isValidExpireTimer = typeof expireTimer === 'number'; const isValidExpireTimer = typeof expireTimer === 'number';
if (isValidExpireTimer) { if (isValidExpireTimer) {
const source = textsecure.storage.user.getNumber(); const sourceE164 = textsecure.storage.user.getNumber();
const sourceUuid = textsecure.storage.user.getUuid();
const receivedAt = Date.now(); const receivedAt = Date.now();
await conversation.updateExpirationTimer( await conversation.updateExpirationTimer(
expireTimer, expireTimer,
source, sourceE164 || sourceUuid,
receivedAt, receivedAt,
{ fromSync: true } { fromSync: true }
); );
@ -1934,6 +2024,7 @@
verifiedEvent.verified = { verifiedEvent.verified = {
state: verified.state, state: verified.state,
destination: verified.destination, destination: verified.destination,
destinationUuid: verified.destinationUuid,
identityKey: verified.identityKey.toArrayBuffer(), identityKey: verified.identityKey.toArrayBuffer(),
}; };
verifiedEvent.viaContactSync = true; verifiedEvent.viaContactSync = true;
@ -1953,9 +2044,23 @@
'group' 'group'
); );
const memberConversations = await Promise.all(
(details.members || details.membersE164).map(member => {
if (member.e164 || member.uuid) {
return ConversationController.getOrCreateAndWait(
member.e164 || member.uuid,
'private'
);
}
return ConversationController.getOrCreateAndWait(member, 'private');
})
);
const members = memberConversations.map(c => c.get('id'));
const updates = { const updates = {
name: details.name, name: details.name,
members: details.members, members,
color: details.color, color: details.color,
type: 'group', type: 'group',
}; };
@ -2004,11 +2109,17 @@
return; return;
} }
const source = textsecure.storage.user.getNumber(); const sourceE164 = textsecure.storage.user.getNumber();
const sourceUuid = textsecure.storage.user.getUuid();
const receivedAt = Date.now(); const receivedAt = Date.now();
await conversation.updateExpirationTimer(expireTimer, source, receivedAt, { await conversation.updateExpirationTimer(
fromSync: true, expireTimer,
}); sourceE164 || sourceUuid,
receivedAt,
{
fromSync: true,
}
);
} }
// Descriptors // Descriptors
@ -2024,10 +2135,10 @@
: { type: Message.PRIVATE, id: destination }; : { type: Message.PRIVATE, id: destination };
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`: // Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
const getDescriptorForReceived = ({ message, source }) => const getDescriptorForReceived = ({ message, source, sourceUuid }) =>
message.group message.group
? getGroupDescriptor(message.group) ? getGroupDescriptor(message.group)
: { type: Message.PRIVATE, id: source }; : { type: Message.PRIVATE, id: source || sourceUuid };
// Received: // Received:
async function handleMessageReceivedProfileUpdate({ async function handleMessageReceivedProfileUpdate({
@ -2069,11 +2180,18 @@
const message = await initIncomingMessage(data); const message = await initIncomingMessage(data);
await ConversationController.getOrCreateAndWait( const result = await ConversationController.getOrCreateAndWait(
messageDescriptor.id, messageDescriptor.id,
messageDescriptor.type messageDescriptor.type
); );
if (messageDescriptor.type === 'private') {
result.updateE164(data.source);
if (data.sourceUuid) {
result.updateUuid(data.sourceUuid);
}
}
if (data.message.reaction) { if (data.message.reaction) {
const { reaction } = data.message; const { reaction } = data.message;
const reactionModel = Whisper.Reactions.add({ const reactionModel = Whisper.Reactions.add({
@ -2083,7 +2201,7 @@
targetAuthorUuid: reaction.targetAuthorUuid, targetAuthorUuid: reaction.targetAuthorUuid,
targetTimestamp: reaction.targetTimestamp.toNumber(), targetTimestamp: reaction.targetTimestamp.toNumber(),
timestamp: Date.now(), timestamp: Date.now(),
fromId: data.source, fromId: data.source || data.sourceUuid,
}); });
// Note: We do not wait for completion here // Note: We do not wait for completion here
Whisper.Reactions.onReaction(reactionModel); Whisper.Reactions.onReaction(reactionModel);
@ -2112,8 +2230,12 @@
// Then we update our own profileKey if it's different from what we have // Then we update our own profileKey if it's different from what we have
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const profileKey = data.message.profileKey.toString('base64'); const profileKey = data.message.profileKey.toString('base64');
const me = await ConversationController.getOrCreate(ourNumber, 'private'); const me = await ConversationController.getOrCreate(
ourNumber || ourUuid,
'private'
);
// Will do the save for us if needed // Will do the save for us if needed
await me.setProfileKey(profileKey); await me.setProfileKey(profileKey);
@ -2136,6 +2258,7 @@
return new Whisper.Message({ return new Whisper.Message({
source: textsecure.storage.user.getNumber(), source: textsecure.storage.user.getNumber(),
sourceUuid: textsecure.storage.user.getUuid(),
sourceDevice: data.device, sourceDevice: data.device,
sent_at: data.timestamp, sent_at: data.timestamp,
sent_to: sentTo, sent_to: sentTo,
@ -2176,6 +2299,7 @@
if (data.message.reaction) { if (data.message.reaction) {
const { reaction } = data.message; const { reaction } = data.message;
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const reactionModel = Whisper.Reactions.add({ const reactionModel = Whisper.Reactions.add({
emoji: reaction.emoji, emoji: reaction.emoji,
remove: reaction.remove, remove: reaction.remove,
@ -2183,7 +2307,7 @@
targetAuthorUuid: reaction.targetAuthorUuid, targetAuthorUuid: reaction.targetAuthorUuid,
targetTimestamp: reaction.targetTimestamp.toNumber(), targetTimestamp: reaction.targetTimestamp.toNumber(),
timestamp: Date.now(), timestamp: Date.now(),
fromId: ourNumber, fromId: ourNumber || ourUuid,
fromSync: true, fromSync: true,
}); });
// Note: We do not wait for completion here // Note: We do not wait for completion here
@ -2197,20 +2321,25 @@
messageDescriptor.id, messageDescriptor.id,
messageDescriptor.type messageDescriptor.type
); );
// Don't wait for handleDataMessage, as it has its own per-conversation queueing // Don't wait for handleDataMessage, as it has its own per-conversation queueing
message.handleDataMessage(data.message, event.confirm, { message.handleDataMessage(data.message, event.confirm, {
data, data,
}); });
} }
async function initIncomingMessage(data) { async function initIncomingMessage(data) {
const targetId = data.source || data.sourceUuid;
const conversation = ConversationController.get(targetId);
const conversationId = conversation ? conversation.id : targetId;
return new Whisper.Message({ return new Whisper.Message({
source: data.source, source: data.source,
sourceUuid: data.sourceUuid,
sourceDevice: data.sourceDevice, sourceDevice: data.sourceDevice,
sent_at: data.timestamp, sent_at: data.timestamp,
received_at: data.receivedAt || Date.now(), received_at: data.receivedAt || Date.now(),
conversationId: data.source, conversationId,
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived, unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
type: 'incoming', type: 'incoming',
unread: 1, unread: 1,
@ -2384,11 +2513,16 @@
async function onViewSync(ev) { async function onViewSync(ev) {
ev.confirm(); ev.confirm();
const { source, timestamp } = ev; const { source, sourceUuid, timestamp } = ev;
window.log.info(`view sync ${source} ${timestamp}`); window.log.info(`view sync ${source} ${timestamp}`);
const conversationId = ConversationController.getConversationId(
source || sourceUuid
);
const sync = Whisper.ViewSyncs.add({ const sync = Whisper.ViewSyncs.add({
source, source,
sourceUuid,
conversationId,
timestamp, timestamp,
}); });
@ -2398,12 +2532,12 @@
function onReadReceipt(ev) { function onReadReceipt(ev) {
const readAt = ev.timestamp; const readAt = ev.timestamp;
const { timestamp } = ev.read; const { timestamp } = ev.read;
const { reader } = ev.read; const reader = ConversationController.getConversationId(ev.read.reader);
window.log.info('read receipt', reader, timestamp); window.log.info('read receipt', reader, timestamp);
ev.confirm(); ev.confirm();
if (!storage.get('read-receipt-setting')) { if (!storage.get('read-receipt-setting') || !reader) {
return; return;
} }
@ -2420,11 +2554,12 @@
function onReadSync(ev) { function onReadSync(ev) {
const readAt = ev.timestamp; const readAt = ev.timestamp;
const { timestamp } = ev.read; const { timestamp } = ev.read;
const { sender } = ev.read; const { sender, senderUuid } = ev.read;
window.log.info('read sync', sender, timestamp); window.log.info('read sync', sender, senderUuid, timestamp);
const receipt = Whisper.ReadSyncs.add({ const receipt = Whisper.ReadSyncs.add({
sender, sender,
senderUuid,
timestamp, timestamp,
read_at: readAt, read_at: readAt,
}); });
@ -2437,7 +2572,8 @@
} }
async function onVerified(ev) { async function onVerified(ev) {
const number = ev.verified.destination; const e164 = ev.verified.destination;
const uuid = ev.verified.destinationUuid;
const key = ev.verified.identityKey; const key = ev.verified.identityKey;
let state; let state;
@ -2446,12 +2582,16 @@
} }
const c = new Whisper.Conversation({ const c = new Whisper.Conversation({
id: number, e164,
uuid,
type: 'private',
}); });
const error = c.validateNumber(); const error = c.validate();
if (error) { if (error) {
window.log.error( window.log.error(
'Invalid verified sync received:', 'Invalid verified sync received:',
e164,
uuid,
Errors.toLogFormat(error) Errors.toLogFormat(error)
); );
return; return;
@ -2473,13 +2613,14 @@
window.log.info( window.log.info(
'got verified sync for', 'got verified sync for',
number, e164,
uuid,
state, state,
ev.viaContactSync ? 'via contact sync' : '' ev.viaContactSync ? 'via contact sync' : ''
); );
const contact = await ConversationController.getOrCreateAndWait( const contact = await ConversationController.getOrCreateAndWait(
number, e164 || uuid,
'private' 'private'
); );
const options = { const options = {
@ -2499,17 +2640,27 @@
function onDeliveryReceipt(ev) { function onDeliveryReceipt(ev) {
const { deliveryReceipt } = ev; const { deliveryReceipt } = ev;
const { sourceUuid, source } = deliveryReceipt;
const identifier = source || sourceUuid;
window.log.info( window.log.info(
'delivery receipt from', 'delivery receipt from',
`${deliveryReceipt.source}.${deliveryReceipt.sourceDevice}`, `${identifier}.${deliveryReceipt.sourceDevice}`,
deliveryReceipt.timestamp deliveryReceipt.timestamp
); );
ev.confirm(); ev.confirm();
const deliveredTo = ConversationController.getConversationId(identifier);
if (!deliveredTo) {
window.log.info('no conversation for identifier', identifier);
return;
}
const receipt = Whisper.DeliveryReceipts.add({ const receipt = Whisper.DeliveryReceipts.add({
timestamp: deliveryReceipt.timestamp, timestamp: deliveryReceipt.timestamp,
source: deliveryReceipt.source, deliveredTo,
}); });
// Note: We don't wait for completion here // Note: We don't wait for completion here

View file

@ -1,4 +1,4 @@
/* global _, Whisper, Backbone, storage */ /* global _, Whisper, Backbone, storage, textsecure */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
@ -67,8 +67,8 @@
dangerouslyCreateAndAdd(attributes) { dangerouslyCreateAndAdd(attributes) {
return conversations.add(attributes); return conversations.add(attributes);
}, },
getOrCreate(id, type) { getOrCreate(identifier, type) {
if (typeof id !== 'string') { if (typeof identifier !== 'string') {
throw new TypeError("'id' must be a string"); throw new TypeError("'id' must be a string");
} }
@ -84,16 +84,41 @@
); );
} }
let conversation = conversations.get(id); let conversation = conversations.get(identifier);
if (conversation) { if (conversation) {
return conversation; return conversation;
} }
conversation = conversations.add({ const id = window.getGuid();
id,
type, if (type === 'group') {
version: 2, conversation = conversations.add({
}); id,
uuid: null,
e164: null,
groupId: identifier,
type,
version: 2,
});
} else if (window.isValidGuid(identifier)) {
conversation = conversations.add({
id,
uuid: identifier,
e164: null,
groupId: null,
type,
version: 2,
});
} else {
conversation = conversations.add({
id,
uuid: null,
e164: identifier,
groupId: null,
type,
version: 2,
});
}
const create = async () => { const create = async () => {
if (!conversation.isValid()) { if (!conversation.isValid()) {
@ -114,7 +139,7 @@
} catch (error) { } catch (error) {
window.log.error( window.log.error(
'Conversation save failed! ', 'Conversation save failed! ',
id, identifier,
type, type,
'Error:', 'Error:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
@ -142,8 +167,22 @@
); );
}); });
}, },
getConversationId(address) {
if (!address) {
return null;
}
const [id] = textsecure.utils.unencodeNumber(address);
const conv = this.get(id);
if (conv) {
return conv.get('id');
}
return null;
},
prepareForSend(id, options) { prepareForSend(id, options) {
// id is either a group id or an individual user's id // id is any valid conversation identifier
const conversation = this.get(id); const conversation = this.get(id);
const sendOptions = conversation const sendOptions = conversation
? conversation.getSendOptions(options) ? conversation.getSendOptions(options)

View file

@ -25,7 +25,7 @@
const receipts = this.filter( const receipts = this.filter(
receipt => receipt =>
receipt.get('timestamp') === message.get('sent_at') && receipt.get('timestamp') === message.get('sent_at') &&
recipients.indexOf(receipt.get('source')) > -1 recipients.indexOf(receipt.get('deliveredTo')) > -1
); );
this.remove(receipts); this.remove(receipts);
return receipts; return receipts;
@ -34,19 +34,23 @@
if (messages.length === 0) { if (messages.length === 0) {
return null; return null;
} }
const sourceId = ConversationController.getConversationId(source);
const message = messages.find( const message = messages.find(
item => !item.isIncoming() && source === item.get('conversationId') item => !item.isIncoming() && sourceId === item.get('conversationId')
); );
if (message) { if (message) {
return MessageController.register(message.id, message); return MessageController.register(message.id, message);
} }
const groups = await window.Signal.Data.getAllGroupsInvolvingId(source, { const groups = await window.Signal.Data.getAllGroupsInvolvingId(
ConversationCollection: Whisper.ConversationCollection, sourceId,
}); {
ConversationCollection: Whisper.ConversationCollection,
}
);
const ids = groups.pluck('id'); const ids = groups.pluck('id');
ids.push(source); ids.push(sourceId);
const target = messages.find( const target = messages.find(
item => item =>
@ -68,25 +72,25 @@
); );
const message = await this.getTargetMessage( const message = await this.getTargetMessage(
receipt.get('source'), receipt.get('deliveredTo'),
messages messages
); );
if (!message) { if (!message) {
window.log.info( window.log.info(
'No message for delivery receipt', 'No message for delivery receipt',
receipt.get('source'), receipt.get('deliveredTo'),
receipt.get('timestamp') receipt.get('timestamp')
); );
return; return;
} }
const deliveries = message.get('delivered') || 0; const deliveries = message.get('delivered') || 0;
const deliveredTo = message.get('delivered_to') || []; const deliveredTo = message.get('deliveredTo') || [];
const expirationStartTimestamp = message.get( const expirationStartTimestamp = message.get(
'expirationStartTimestamp' 'expirationStartTimestamp'
); );
message.set({ message.set({
delivered_to: _.union(deliveredTo, [receipt.get('source')]), delivered_to: _.union(deliveredTo, [receipt.get('deliveredTo')]),
delivered: deliveries + 1, delivered: deliveries + 1,
expirationStartTimestamp: expirationStartTimestamp || Date.now(), expirationStartTimestamp: expirationStartTimestamp || Date.now(),
sent: true, sent: true,

View file

@ -5,6 +5,7 @@
'use strict'; 'use strict';
const BLOCKED_NUMBERS_ID = 'blocked'; const BLOCKED_NUMBERS_ID = 'blocked';
const BLOCKED_UUIDS_ID = 'blocked-uuids';
const BLOCKED_GROUPS_ID = 'blocked-groups'; const BLOCKED_GROUPS_ID = 'blocked-groups';
storage.isBlocked = number => { storage.isBlocked = number => {
@ -31,6 +32,30 @@
storage.put(BLOCKED_NUMBERS_ID, _.without(numbers, number)); storage.put(BLOCKED_NUMBERS_ID, _.without(numbers, number));
}; };
storage.isUuidBlocked = uuid => {
const uuids = storage.get(BLOCKED_UUIDS_ID, []);
return _.include(uuids, uuid);
};
storage.addBlockedUuid = uuid => {
const uuids = storage.get(BLOCKED_UUIDS_ID, []);
if (_.include(uuids, uuid)) {
return;
}
window.log.info('adding', uuid, 'to blocked list');
storage.put(BLOCKED_UUIDS_ID, uuids.concat(uuid));
};
storage.removeBlockedUuid = uuid => {
const numbers = storage.get(BLOCKED_UUIDS_ID, []);
if (!_.include(numbers, uuid)) {
return;
}
window.log.info('removing', uuid, 'from blocked list');
storage.put(BLOCKED_NUMBERS_ID, _.without(numbers, uuid));
};
storage.isGroupBlocked = groupId => { storage.isGroupBlocked = groupId => {
const groupIds = storage.get(BLOCKED_GROUPS_ID, []); const groupIds = storage.get(BLOCKED_GROUPS_ID, []);

View file

@ -27,7 +27,7 @@
}; };
const { Util } = window.Signal; const { Util } = window.Signal;
const { Conversation, Contact, Message, PhoneNumber } = window.Signal.Types; const { Conversation, Contact, Message } = window.Signal.Types;
const { const {
deleteAttachmentData, deleteAttachmentData,
doesAttachmentExist, doesAttachmentExist,
@ -85,8 +85,13 @@
return collection; return collection;
}, },
initialize() { initialize(attributes) {
if (window.isValidE164(attributes.id)) {
this.set({ id: window.getGuid(), e164: attributes.id });
}
this.ourNumber = textsecure.storage.user.getNumber(); this.ourNumber = textsecure.storage.user.getNumber();
this.ourUuid = textsecure.storage.user.getUuid();
this.verifiedEnum = textsecure.storage.protocol.VerifiedStatus; this.verifiedEnum = textsecure.storage.protocol.VerifiedStatus;
// This may be overridden by ConversationController.getOrCreate, and signify // This may be overridden by ConversationController.getOrCreate, and signify
@ -148,7 +153,11 @@
}, },
isMe() { isMe() {
return this.id === this.ourNumber; const e164 = this.get('e164');
const uuid = this.get('uuid');
return (
(e164 && e164 === this.ourNumber) || (uuid && uuid === this.ourUuid)
);
}, },
hasDraft() { hasDraft() {
@ -241,11 +250,17 @@
}, },
sendTypingMessage(isTyping) { sendTypingMessage(isTyping) {
const groupId = !this.isPrivate() ? this.id : null; if (!textsecure.messaging) {
const recipientId = this.isPrivate() ? this.id : null; return;
}
const groupId = !this.isPrivate() ? this.get('groupId') : null;
const maybeRecipientId = this.get('uuid') || this.get('e164');
const recipientId = this.isPrivate() ? maybeRecipientId : null;
const groupNumbers = this.getRecipients(); const groupNumbers = this.getRecipients();
const sendOptions = this.getSendOptions(); const sendOptions = this.getSendOptions();
this.wrapSend( this.wrapSend(
textsecure.messaging.sendTypingMessage( textsecure.messaging.sendTypingMessage(
{ {
@ -356,8 +371,6 @@
return this.cachedProps; return this.cachedProps;
}, },
getProps() { getProps() {
const { format } = PhoneNumber;
const regionCode = storage.get('regionCode');
const color = this.getColor(); const color = this.getColor();
const typingValues = _.values(this.contactTypingTimers || {}); const typingValues = _.values(this.contactTypingTimers || {});
@ -394,9 +407,7 @@
draftPreview, draftPreview,
draftText, draftText,
phoneNumber: format(this.id, { phoneNumber: this.getNumber(),
ourRegionCode: regionCode,
}),
lastMessage: { lastMessage: {
status: this.get('lastMessageStatus'), status: this.get('lastMessageStatus'),
text: this.get('lastMessage'), text: this.get('lastMessage'),
@ -406,6 +417,31 @@
return result; return result;
}, },
updateE164(e164) {
const oldValue = this.get('e164');
if (e164 !== oldValue) {
this.set('e164', e164);
window.Signal.Data.updateConversation(this.id, this.attributes);
this.trigger('idUpdated', this, 'e164', oldValue);
}
},
updateUuid(uuid) {
const oldValue = this.get('uuid');
if (uuid !== oldValue) {
this.set('uuid', uuid);
window.Signal.Data.updateConversation(this.id, this.attributes);
this.trigger('idUpdated', this, 'uuid', oldValue);
}
},
updateGroupId(groupId) {
const oldValue = this.get('groupId');
if (groupId !== oldValue) {
this.set('groupId', groupId);
window.Signal.Data.updateConversation(this.id, this.attributes);
this.trigger('idUpdated', this, 'groupId', oldValue);
}
},
onMessageError() { onMessageError() {
this.updateVerified(); this.updateVerified();
}, },
@ -506,24 +542,28 @@
}); });
} }
if (!options.viaSyncMessage) { if (!options.viaSyncMessage) {
await this.sendVerifySyncMessage(this.id, verified); await this.sendVerifySyncMessage(
this.get('e164'),
this.get('uuid'),
verified
);
} }
}, },
sendVerifySyncMessage(number, state) { sendVerifySyncMessage(e164, uuid, state) {
// Because syncVerification sends a (null) message to the target of the verify and // Because syncVerification sends a (null) message to the target of the verify and
// a sync message to our own devices, we need to send the accessKeys down for both // a sync message to our own devices, we need to send the accessKeys down for both
// contacts. So we merge their sendOptions. // contacts. So we merge their sendOptions.
const { sendOptions } = ConversationController.prepareForSend( const { sendOptions } = ConversationController.prepareForSend(
this.ourNumber, this.ourNumber || this.ourUuid,
{ syncMessage: true } { syncMessage: true }
); );
const contactSendOptions = this.getSendOptions(); const contactSendOptions = this.getSendOptions();
const options = Object.assign({}, sendOptions, contactSendOptions); const options = Object.assign({}, sendOptions, contactSendOptions);
const promise = textsecure.storage.protocol.loadIdentityKey(number); const promise = textsecure.storage.protocol.loadIdentityKey(e164);
return promise.then(key => return promise.then(key =>
this.wrapSend( this.wrapSend(
textsecure.messaging.syncVerification(number, state, key, options) textsecure.messaging.syncVerification(e164, uuid, state, key, options)
) )
); );
}, },
@ -764,8 +804,8 @@
}); });
}, },
validate(attributes) { validate(attributes = this.attributes) {
const required = ['id', 'type']; const required = ['type'];
const missing = _.filter(required, attr => !attributes[attr]); const missing = _.filter(required, attr => !attributes[attr]);
if (missing.length) { if (missing.length) {
return `Conversation must have ${missing}`; return `Conversation must have ${missing}`;
@ -775,7 +815,16 @@
return `Invalid conversation type: ${attributes.type}`; return `Invalid conversation type: ${attributes.type}`;
} }
const error = this.validateNumber(); const atLeastOneOf = ['e164', 'uuid', 'groupId'];
const hasAtLeastOneOf =
_.filter(atLeastOneOf, attr => attributes[attr]).length > 0;
if (!hasAtLeastOneOf) {
return 'Missing one of e164, uuid, or groupId';
}
const error = this.validateNumber() || this.validateUuid();
if (error) { if (error) {
return error; return error;
} }
@ -784,11 +833,14 @@
}, },
validateNumber() { validateNumber() {
if (this.isPrivate()) { if (this.isPrivate() && this.get('e164')) {
const regionCode = storage.get('regionCode'); const regionCode = storage.get('regionCode');
const number = libphonenumber.util.parseNumber(this.id, regionCode); const number = libphonenumber.util.parseNumber(
this.get('e164'),
regionCode
);
if (number.isValidNumber) { if (number.isValidNumber) {
this.set({ id: number.e164 }); this.set({ e164: number.e164 });
return null; return null;
} }
@ -798,6 +850,18 @@
return null; return null;
}, },
validateUuid() {
if (this.isPrivate() && this.get('uuid')) {
if (window.isValidGuid(this.get('uuid'))) {
return null;
}
return 'Invalid UUID';
}
return null;
},
queueJob(callback) { queueJob(callback) {
this.jobQueue = this.jobQueue || new window.PQueue({ concurrency: 1 }); this.jobQueue = this.jobQueue || new window.PQueue({ concurrency: 1 });
@ -811,10 +875,15 @@
getRecipients() { getRecipients() {
if (this.isPrivate()) { if (this.isPrivate()) {
return [this.id]; return [this.get('uuid') || this.get('e164')];
} }
const me = textsecure.storage.user.getNumber(); const me = ConversationController.getConversationId(
return _.without(this.get('members'), me); textsecure.storage.user.getUuid() || textsecure.storage.user.getNumber()
);
return _.without(this.get('members'), me).map(memberId => {
const c = ConversationController.get(memberId);
return c.get('uuid') || c.get('e164');
});
}, },
async getQuoteAttachment(attachments, preview, sticker) { async getQuoteAttachment(attachments, preview, sticker) {
@ -908,7 +977,8 @@
: ''; : '';
return { return {
author: contact.id, author: contact.get('e164'),
authorUuid: contact.get('uuid'),
id: quotedMessage.get('sent_at'), id: quotedMessage.get('sent_at'),
text: body || embeddedContactName, text: body || embeddedContactName,
attachments: quotedMessage.isTapToView() attachments: quotedMessage.isTapToView()
@ -955,7 +1025,9 @@
* @param {boolean} [reaction.remove] - Set to `true` if we are removing a * @param {boolean} [reaction.remove] - Set to `true` if we are removing a
* reaction with the given emoji * reaction with the given emoji
* @param {object} target - The target of the reaction * @param {object} target - The target of the reaction
* @param {string} target.targetAuthorE164 - The E164 address of the target * @param {string} [target.targetAuthorE164] - The E164 address of the target
* message's author
* @param {string} [target.targetAuthorUuid] - The UUID address of the target
* message's author * message's author
* @param {number} target.targetTimestamp - The sent_at timestamp of the * @param {number} target.targetTimestamp - The sent_at timestamp of the
* target message * target message
@ -965,13 +1037,17 @@
const outgoingReaction = { ...reaction, ...target }; const outgoingReaction = { ...reaction, ...target };
const reactionModel = Whisper.Reactions.add({ const reactionModel = Whisper.Reactions.add({
...outgoingReaction, ...outgoingReaction,
fromId: this.ourNumber || textsecure.storage.user.getNumber(), fromId:
this.ourNumber ||
this.ourUuid ||
textsecure.storage.user.getNumber() ||
textsecure.storage.user.getUuid(),
timestamp, timestamp,
fromSync: true, fromSync: true,
}); });
Whisper.Reactions.onReaction(reactionModel); Whisper.Reactions.onReaction(reactionModel);
const destination = this.id; const destination = this.get('e164');
const recipients = this.getRecipients(); const recipients = this.getRecipients();
let profileKey; let profileKey;
@ -987,11 +1063,10 @@
timestamp timestamp
); );
// Here we move attachments to disk
const attributes = { const attributes = {
id: window.getGuid(), id: window.getGuid(),
type: 'outgoing', type: 'outgoing',
conversationId: destination, conversationId: this.get('id'),
sent_at: timestamp, sent_at: timestamp,
received_at: timestamp, received_at: timestamp,
recipients, recipients,
@ -1029,11 +1104,10 @@
} }
const options = this.getSendOptions(); const options = this.getSendOptions();
const groupNumbers = this.getRecipients();
const promise = (() => { const promise = (() => {
if (this.isPrivate()) { if (this.isPrivate()) {
return textsecure.messaging.sendMessageToNumber( return textsecure.messaging.sendMessageToIdentifier(
destination, destination,
null, null,
null, null,
@ -1049,8 +1123,8 @@
} }
return textsecure.messaging.sendMessageToGroup( return textsecure.messaging.sendMessageToGroup(
destination, this.get('groupId'),
groupNumbers, this.getRecipients(),
null, null,
null, null,
null, null,
@ -1082,7 +1156,7 @@
const { clearUnreadMetrics } = window.reduxActions.conversations; const { clearUnreadMetrics } = window.reduxActions.conversations;
clearUnreadMetrics(this.id); clearUnreadMetrics(this.id);
const destination = this.id; const destination = this.get('uuid') || this.get('e164');
const expireTimer = this.get('expireTimer'); const expireTimer = this.get('expireTimer');
const recipients = this.getRecipients(); const recipients = this.getRecipients();
@ -1105,7 +1179,7 @@
const messageWithSchema = await upgradeMessageSchema({ const messageWithSchema = await upgradeMessageSchema({
type: 'outgoing', type: 'outgoing',
body, body,
conversationId: destination, conversationId: this.id,
quote, quote,
preview, preview,
attachments, attachments,
@ -1147,10 +1221,13 @@
// We're offline! // We're offline!
if (!textsecure.messaging) { if (!textsecure.messaging) {
const errors = this.contactCollection.map(contact => { const errors = (this.contactCollection.length
? this.contactCollection
: [this]
).map(contact => {
const error = new Error('Network is not available'); const error = new Error('Network is not available');
error.name = 'SendMessageNetworkError'; error.name = 'SendMessageNetworkError';
error.number = contact.id; error.number = contact.get('uuid') || contact.get('e164');
return error; return error;
}); });
await message.saveErrors(errors); await message.saveErrors(errors);
@ -1189,12 +1266,11 @@
const conversationType = this.get('type'); const conversationType = this.get('type');
const options = this.getSendOptions(); const options = this.getSendOptions();
const groupNumbers = this.getRecipients();
const promise = (() => { const promise = (() => {
switch (conversationType) { switch (conversationType) {
case Message.PRIVATE: case Message.PRIVATE:
return textsecure.messaging.sendMessageToNumber( return textsecure.messaging.sendMessageToIdentifier(
destination, destination,
messageBody, messageBody,
finalAttachments, finalAttachments,
@ -1209,8 +1285,8 @@
); );
case Message.GROUP: case Message.GROUP:
return textsecure.messaging.sendMessageToGroup( return textsecure.messaging.sendMessageToGroup(
destination, this.get('groupId'),
groupNumbers, this.getRecipients(),
messageBody, messageBody,
finalAttachments, finalAttachments,
quote, quote,
@ -1239,7 +1315,7 @@
// success // success
if (result) { if (result) {
await this.handleMessageSendResult( await this.handleMessageSendResult(
result.failoverNumbers, result.failoverIdentifiers,
result.unidentifiedDeliveries result.unidentifiedDeliveries
); );
} }
@ -1249,7 +1325,7 @@
// failure // failure
if (result) { if (result) {
await this.handleMessageSendResult( await this.handleMessageSendResult(
result.failoverNumbers, result.failoverIdentifiers,
result.unidentifiedDeliveries result.unidentifiedDeliveries
); );
} }
@ -1258,9 +1334,9 @@
); );
}, },
async handleMessageSendResult(failoverNumbers, unidentifiedDeliveries) { async handleMessageSendResult(failoverIdentifiers, unidentifiedDeliveries) {
await Promise.all( await Promise.all(
(failoverNumbers || []).map(async number => { (failoverIdentifiers || []).map(async number => {
const conversation = ConversationController.get(number); const conversation = ConversationController.get(number);
if ( if (
@ -1315,25 +1391,36 @@
getSendOptions(options = {}) { getSendOptions(options = {}) {
const senderCertificate = storage.get('senderCertificate'); const senderCertificate = storage.get('senderCertificate');
const numberInfo = this.getNumberInfo(options); const senderCertificateWithUuid = storage.get(
'senderCertificateWithUuid'
);
const sendMetadata = this.getSendMetadata(options);
return { return {
senderCertificate, senderCertificate,
numberInfo, senderCertificateWithUuid,
sendMetadata,
}; };
}, },
getNumberInfo(options = {}) { getUuidCapable() {
return Boolean(_.property('uuid')(this.get('capabilities')));
},
getSendMetadata(options = {}) {
const { syncMessage, disableMeCheck } = options; const { syncMessage, disableMeCheck } = options;
if (!this.ourNumber) { if (!this.ourNumber && !this.ourUuid) {
return null; return null;
} }
// START: this code has an Expiration date of ~2018/11/21 // START: this code has an Expiration date of ~2018/11/21
// We don't want to enable unidentified delivery for send unless it is // We don't want to enable unidentified delivery for send unless it is
// also enabled for our own account. // also enabled for our own account.
const me = ConversationController.getOrCreate(this.ourNumber, 'private'); const me = ConversationController.getOrCreate(
this.ourNumber || this.ourUuid,
'private'
);
if ( if (
!disableMeCheck && !disableMeCheck &&
me.get('sealedSender') === SEALED_SENDER.DISABLED me.get('sealedSender') === SEALED_SENDER.DISABLED
@ -1344,29 +1431,36 @@
if (!this.isPrivate()) { if (!this.isPrivate()) {
const infoArray = this.contactCollection.map(conversation => const infoArray = this.contactCollection.map(conversation =>
conversation.getNumberInfo(options) conversation.getSendMetadata(options)
); );
return Object.assign({}, ...infoArray); return Object.assign({}, ...infoArray);
} }
const accessKey = this.get('accessKey'); const accessKey = this.get('accessKey');
const sealedSender = this.get('sealedSender'); const sealedSender = this.get('sealedSender');
const uuidCapable = this.getUuidCapable();
// We never send sync messages as sealed sender // We never send sync messages as sealed sender
if (syncMessage && this.id === this.ourNumber) { if (syncMessage && this.isMe()) {
return null; return null;
} }
const e164 = this.get('e164');
const uuid = this.get('uuid');
// If we've never fetched user's profile, we default to what we have // If we've never fetched user's profile, we default to what we have
if (sealedSender === SEALED_SENDER.UNKNOWN) { if (sealedSender === SEALED_SENDER.UNKNOWN) {
const info = {
accessKey:
accessKey ||
window.Signal.Crypto.arrayBufferToBase64(
window.Signal.Crypto.getRandomBytes(16)
),
useUuidSenderCert: uuidCapable,
};
return { return {
[this.id]: { ...(e164 ? { [e164]: info } : {}),
accessKey: ...(uuid ? { [uuid]: info } : {}),
accessKey ||
window.Signal.Crypto.arrayBufferToBase64(
window.Signal.Crypto.getRandomBytes(16)
),
},
}; };
} }
@ -1374,15 +1468,19 @@
return null; return null;
} }
const info = {
accessKey:
accessKey && sealedSender === SEALED_SENDER.ENABLED
? accessKey
: window.Signal.Crypto.arrayBufferToBase64(
window.Signal.Crypto.getRandomBytes(16)
),
useUuidSenderCert: uuidCapable,
};
return { return {
[this.id]: { ...(e164 ? { [e164]: info } : {}),
accessKey: ...(uuid ? { [uuid]: info } : {}),
accessKey && sealedSender === SEALED_SENDER.ENABLED
? accessKey
: window.Signal.Crypto.arrayBufferToBase64(
window.Signal.Crypto.getRandomBytes(16)
),
},
}; };
}, },
@ -1465,7 +1563,10 @@
source, source,
}); });
source = source || textsecure.storage.user.getNumber(); source =
source ||
textsecure.storage.user.getNumber() ||
textsecure.storage.user.getUuid();
// When we add a disappearing messages notification to the conversation, we want it // When we add a disappearing messages notification to the conversation, we want it
// to be above the message that initiated that change, hence the subtraction. // to be above the message that initiated that change, hence the subtraction.
@ -1492,7 +1593,7 @@
}); });
if (this.isPrivate()) { if (this.isPrivate()) {
model.set({ destination: this.id }); model.set({ destination: this.get('uuid') || this.get('e164') });
} }
if (model.isOutgoing()) { if (model.isOutgoing()) {
model.set({ recipients: this.getRecipients() }); model.set({ recipients: this.getRecipients() });
@ -1522,7 +1623,7 @@
const flags = const flags =
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
const dataMessage = await textsecure.messaging.getMessageProto( const dataMessage = await textsecure.messaging.getMessageProto(
this.get('id'), this.get('uuid') || this.get('e164'),
null, null,
[], [],
null, null,
@ -1538,8 +1639,8 @@
} }
if (this.get('type') === 'private') { if (this.get('type') === 'private') {
promise = textsecure.messaging.sendExpirationTimerUpdateToNumber( promise = textsecure.messaging.sendExpirationTimerUpdateToIdentifier(
this.get('id'), this.get('uuid') || this.get('e164'),
expireTimer, expireTimer,
message.get('sent_at'), message.get('sent_at'),
profileKey, profileKey,
@ -1547,7 +1648,7 @@
); );
} else { } else {
promise = textsecure.messaging.sendExpirationTimerUpdateToGroup( promise = textsecure.messaging.sendExpirationTimerUpdateToGroup(
this.get('id'), this.get('groupId'),
this.getRecipients(), this.getRecipients(),
expireTimer, expireTimer,
message.get('sent_at'), message.get('sent_at'),
@ -1573,7 +1674,8 @@
type: 'outgoing', type: 'outgoing',
sent_at: now, sent_at: now,
received_at: now, received_at: now,
destination: this.id, destination: this.get('e164'),
destinationUuid: this.get('uuid'),
recipients: this.getRecipients(), recipients: this.getRecipients(),
flags: textsecure.protobuf.DataMessage.Flags.END_SESSION, flags: textsecure.protobuf.DataMessage.Flags.END_SESSION,
}); });
@ -1589,7 +1691,11 @@
const options = this.getSendOptions(); const options = this.getSendOptions();
message.send( message.send(
this.wrapSend( this.wrapSend(
textsecure.messaging.resetSession(this.id, now, options) textsecure.messaging.resetSession(
this.get('uuid') || this.get('e164'),
now,
options
)
) )
); );
} }
@ -1720,7 +1826,7 @@
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes // Because syncReadMessages sends to our other devices, and sendReadReceipts goes
// to a contact, we need accessKeys for both. // to a contact, we need accessKeys for both.
const { sendOptions } = ConversationController.prepareForSend( const { sendOptions } = ConversationController.prepareForSend(
this.ourNumber, this.ourUuid || this.ourNumber,
{ syncMessage: true } { syncMessage: true }
); );
await this.wrapSend( await this.wrapSend(
@ -1731,11 +1837,13 @@
const convoSendOptions = this.getSendOptions(); const convoSendOptions = this.getSendOptions();
await Promise.all( await Promise.all(
_.map(_.groupBy(read, 'sender'), async (receipts, sender) => { _.map(_.groupBy(read, 'sender'), async (receipts, identifier) => {
const timestamps = _.map(receipts, 'timestamp'); const timestamps = _.map(receipts, 'timestamp');
const c = ConversationController.get(identifier);
await this.wrapSend( await this.wrapSend(
textsecure.messaging.sendReadReceipts( textsecure.messaging.sendReadReceipts(
sender, c.get('e164'),
c.get('uuid'),
timestamps, timestamps,
convoSendOptions convoSendOptions
) )
@ -1756,9 +1864,14 @@
// request all conversation members' keys // request all conversation members' keys
let ids = []; let ids = [];
if (this.isPrivate()) { if (this.isPrivate()) {
ids = [this.id]; ids = [this.get('uuid') || this.get('e164')];
} else { } else {
ids = this.get('members'); ids = this.get('members')
.map(id => {
const c = ConversationController.get(id);
return c ? c.get('uuid') || c.get('e164') : null;
})
.filter(Boolean);
} }
return Promise.all(_.map(ids, this.getProfile)); return Promise.all(_.map(ids, this.getProfile));
}, },
@ -1780,8 +1893,8 @@
try { try {
await c.deriveAccessKeyIfNeeded(); await c.deriveAccessKeyIfNeeded();
const numberInfo = c.getNumberInfo({ disableMeCheck: true }) || {}; const sendMetadata = c.getSendMetadata({ disableMeCheck: true }) || {};
const getInfo = numberInfo[c.id] || {}; const getInfo = sendMetadata[c.id] || {};
if (getInfo.accessKey) { if (getInfo.accessKey) {
try { try {
@ -1863,6 +1976,10 @@
sealedSender: SEALED_SENDER.DISABLED, sealedSender: SEALED_SENDER.DISABLED,
}); });
} }
if (profile.capabilities) {
c.set({ capabilities: profile.capabilities });
}
} catch (error) { } catch (error) {
if (error.code !== 403 && error.code !== 404) { if (error.code !== 403 && error.code !== 404) {
window.log.error( window.log.error(
@ -2027,8 +2144,9 @@
this.set({ accessKey }); this.set({ accessKey });
}, },
hasMember(number) { hasMember(identifier) {
return _.contains(this.get('members'), number); const cid = ConversationController.getConversationId(identifier);
return cid && _.contains(this.get('members'), cid);
}, },
fetchContacts() { fetchContacts() {
if (this.isPrivate()) { if (this.isPrivate()) {
@ -2114,7 +2232,7 @@
if (!this.isPrivate()) { if (!this.isPrivate()) {
return ''; return '';
} }
const number = this.id; const number = this.get('e164');
try { try {
const parsedNumber = libphonenumber.parse(number); const parsedNumber = libphonenumber.parse(number);
const regionCode = libphonenumber.getRegionCodeForNumber(parsedNumber); const regionCode = libphonenumber.getRegionCodeForNumber(parsedNumber);
@ -2230,10 +2348,10 @@
}, },
notifyTyping(options = {}) { notifyTyping(options = {}) {
const { isTyping, sender, senderDevice } = options; const { isTyping, sender, senderUuid, senderDevice } = options;
// We don't do anything with typing messages from our other devices // We don't do anything with typing messages from our other devices
if (sender === this.ourNumber) { if (sender === this.ourNumber || senderUuid === this.ourUuid) {
return; return;
} }
@ -2289,6 +2407,83 @@
Whisper.ConversationCollection = Backbone.Collection.extend({ Whisper.ConversationCollection = Backbone.Collection.extend({
model: Whisper.Conversation, model: Whisper.Conversation,
/**
* Backbone defines a `_byId` field. Here we set up additional `_byE164`,
* `_byUuid`, and `_byGroupId` fields so we can track conversations by more
* than just their id.
*/
initialize() {
this._byE164 = {};
this._byUuid = {};
this._byGroupId = {};
this.on('idUpdated', (model, idProp, oldValue) => {
if (oldValue) {
if (idProp === 'e164') {
delete this._byE164[oldValue];
}
if (idProp === 'uuid') {
delete this._byUuid[oldValue];
}
if (idProp === 'groupId') {
delete this._byGroupid[oldValue];
}
}
if (model.get('e164')) {
this._byE164[model.get('e164')] = model;
}
if (model.get('uuid')) {
this._byUuid[model.get('uuid')] = model;
}
if (model.get('groupId')) {
this._byGroupid[model.get('groupId')] = model;
}
});
},
reset(...args) {
Backbone.Collection.prototype.reset.apply(this, args);
this._byE164 = {};
this._byUuid = {};
this._byGroupId = {};
},
add(...models) {
const res = Backbone.Collection.prototype.add.apply(this, models);
[].concat(res).forEach(model => {
const e164 = model.get('e164');
if (e164) {
this._byE164[e164] = model;
}
const uuid = model.get('uuid');
if (uuid) {
this._byUuid[uuid] = model;
}
const groupId = model.get('groupId');
if (groupId) {
this._byGroupId[groupId] = model;
}
});
return res;
},
/**
* Backbone collections have a `_byId` field that `get` defers to. Here, we
* override `get` to first access our custom `_byE164`, `_byUuid`, and
* `_byGroupId` functions, followed by falling back to the original
* Backbone implementation.
*/
get(id) {
return (
this._byE164[id] ||
this._byE164[`+${id}`] ||
this._byUuid[id] ||
this._byGroupId[id] ||
Backbone.Collection.prototype.get.call(this, id)
);
},
comparator(m) { comparator(m) {
return -m.get('timestamp'); return -m.get('timestamp');
}, },

View file

@ -78,6 +78,9 @@
window.AccountCache[number] !== undefined; window.AccountCache[number] !== undefined;
window.hasSignalAccount = number => window.AccountCache[number]; window.hasSignalAccount = number => window.AccountCache[number];
const includesAny = (haystack, ...needles) =>
needles.some(needle => haystack.includes(needle));
window.Whisper.Message = Backbone.Model.extend({ window.Whisper.Message = Backbone.Model.extend({
initialize(attributes) { initialize(attributes) {
if (_.isObject(attributes)) { if (_.isObject(attributes)) {
@ -94,6 +97,7 @@
this.INITIAL_PROTOCOL_VERSION = this.INITIAL_PROTOCOL_VERSION =
textsecure.protobuf.DataMessage.ProtocolVersion.INITIAL; textsecure.protobuf.DataMessage.ProtocolVersion.INITIAL;
this.OUR_NUMBER = textsecure.storage.user.getNumber(); this.OUR_NUMBER = textsecure.storage.user.getNumber();
this.OUR_UUID = textsecure.storage.user.getUuid();
this.on('destroy', this.onDestroy); this.on('destroy', this.onDestroy);
this.on('change:expirationStartTimestamp', this.setToExpire); this.on('change:expirationStartTimestamp', this.setToExpire);
@ -178,24 +182,32 @@
// Other top-level prop-generation // Other top-level prop-generation
getPropsForSearchResult() { getPropsForSearchResult() {
const fromNumber = this.getSource(); const sourceE164 = this.getSource();
const from = this.findAndFormatContact(fromNumber); const sourceUuid = this.getSourceUuid();
if (fromNumber === this.OUR_NUMBER) { const fromContact = this.findAndFormatContact(sourceE164 || sourceUuid);
from.isMe = true;
if (
(sourceE164 && sourceE164 === this.OUR_NUMBER) ||
(sourceUuid && sourceUuid === this.OUR_UUID)
) {
fromContact.isMe = true;
} }
const toNumber = this.get('conversationId'); const conversation = this.getConversation();
let to = this.findAndFormatContact(toNumber); let to = this.findAndFormatContact(conversation.get('id'));
if (toNumber === this.OUR_NUMBER) { if (conversation.isMe()) {
to.isMe = true; to.isMe = true;
} else if (fromNumber === toNumber) { } else if (
sourceE164 === conversation.get('e164') ||
sourceUuid === conversation.get('uuid')
) {
to = { to = {
isMe: true, isMe: true,
}; };
} }
return { return {
from, from: fromContact,
to, to,
isSelected: this.isSelected, isSelected: this.isSelected,
@ -221,11 +233,15 @@
// We include numbers we didn't successfully send to so we can display errors. // We include numbers we didn't successfully send to so we can display errors.
// Older messages don't have the recipients included on the message, so we fall // Older messages don't have the recipients included on the message, so we fall
// back to the conversation's current recipients // back to the conversation's current recipients
const phoneNumbers = this.isIncoming() const conversationIds = this.isIncoming()
? [this.get('source')] ? [this.getConversation().get('id')]
: _.union( : _.union(
this.get('sent_to') || [], (this.get('sent_to') || []).map(id =>
this.get('recipients') || this.getConversation().getRecipients() ConversationController.getConversationId(id)
),
(
this.get('recipients') || this.getConversation().getRecipients()
).map(id => ConversationController.getConversationId(id))
); );
// This will make the error message for outgoing key errors a bit nicer // This will make the error message for outgoing key errors a bit nicer
@ -242,7 +258,7 @@
// that contact. Otherwise, it will be a standalone entry. // that contact. Otherwise, it will be a standalone entry.
const errors = _.reject(allErrors, error => Boolean(error.number)); const errors = _.reject(allErrors, error => Boolean(error.number));
const errorsGroupedById = _.groupBy(allErrors, 'number'); const errorsGroupedById = _.groupBy(allErrors, 'number');
const finalContacts = (phoneNumbers || []).map(id => { const finalContacts = (conversationIds || []).map(id => {
const errorsForContact = errorsGroupedById[id]; const errorsForContact = errorsGroupedById[id];
const isOutgoingKeyError = Boolean( const isOutgoingKeyError = Boolean(
_.find(errorsForContact, error => error.name === OUTGOING_KEY_ERROR) _.find(errorsForContact, error => error.name === OUTGOING_KEY_ERROR)
@ -353,7 +369,7 @@
return null; return null;
} }
const { expireTimer, fromSync, source } = timerUpdate; const { expireTimer, fromSync, source, sourceUuid } = timerUpdate;
const timespan = Whisper.ExpirationTimerOptions.getName(expireTimer || 0); const timespan = Whisper.ExpirationTimerOptions.getName(expireTimer || 0);
const disabled = !expireTimer; const disabled = !expireTimer;
@ -369,7 +385,7 @@
...basicProps, ...basicProps,
type: 'fromSync', type: 'fromSync',
}; };
} else if (source === this.OUR_NUMBER) { } else if (source === this.OUR_NUMBER || sourceUuid === this.OUR_UUID) {
return { return {
...basicProps, ...basicProps,
type: 'fromMe', type: 'fromMe',
@ -477,9 +493,10 @@
.map(attachment => this.getPropsForAttachment(attachment)); .map(attachment => this.getPropsForAttachment(attachment));
}, },
getPropsForMessage() { getPropsForMessage() {
const phoneNumber = this.getSource(); const sourceE164 = this.getSource();
const contact = this.findAndFormatContact(phoneNumber); const sourceUuid = this.getSourceUuid();
const contactModel = this.findContact(phoneNumber); const contact = this.findAndFormatContact(sourceE164 || sourceUuid);
const contactModel = this.findContact(sourceE164 || sourceUuid);
const authorColor = contactModel ? contactModel.getColor() : null; const authorColor = contactModel ? contactModel.getColor() : null;
const authorAvatarPath = contactModel const authorAvatarPath = contactModel
@ -558,8 +575,8 @@
}, },
// Dependencies of prop-generation functions // Dependencies of prop-generation functions
findAndFormatContact(phoneNumber) { findAndFormatContact(identifier) {
const contactModel = this.findContact(phoneNumber); const contactModel = this.findContact(identifier);
if (contactModel) { if (contactModel) {
return contactModel.format(); return contactModel.format();
} }
@ -567,13 +584,13 @@
const { format } = PhoneNumber; const { format } = PhoneNumber;
const regionCode = storage.get('regionCode'); const regionCode = storage.get('regionCode');
return { return {
phoneNumber: format(phoneNumber, { phoneNumber: format(identifier, {
ourRegionCode: regionCode, ourRegionCode: regionCode,
}), }),
}; };
}, },
findContact(phoneNumber) { findContact(identifier) {
return ConversationController.get(phoneNumber); return ConversationController.get(identifier);
}, },
getConversation() { getConversation() {
// This needs to be an unsafe call, because this method is called during // This needs to be an unsafe call, because this method is called during
@ -700,8 +717,14 @@
const { format } = PhoneNumber; const { format } = PhoneNumber;
const regionCode = storage.get('regionCode'); const regionCode = storage.get('regionCode');
const { author, id: sentAt, referencedMessageNotFound } = quote; const {
const contact = author && ConversationController.get(author); author,
authorUuid,
id: sentAt,
referencedMessageNotFound,
} = quote;
const contact =
author && ConversationController.get(author || authorUuid);
const authorColor = contact ? contact.getColor() : 'grey'; const authorColor = contact ? contact.getColor() : 'grey';
const authorPhoneNumber = format(author, { const authorPhoneNumber = format(author, {
@ -709,7 +732,7 @@
}); });
const authorProfileName = contact ? contact.getProfileName() : null; const authorProfileName = contact ? contact.getProfileName() : null;
const authorName = contact ? contact.getName() : null; const authorName = contact ? contact.getName() : null;
const isFromMe = contact ? contact.id === this.OUR_NUMBER : false; const isFromMe = contact ? contact.isMe() : false;
const firstAttachment = quote.attachments && quote.attachments[0]; const firstAttachment = quote.attachments && quote.attachments[0];
return { return {
@ -728,17 +751,26 @@
onClick: () => this.trigger('scroll-to-message'), onClick: () => this.trigger('scroll-to-message'),
}; };
}, },
getStatus(number) { getStatus(identifier) {
const conversation = ConversationController.get(identifier);
if (!conversation) {
return null;
}
const e164 = conversation.get('e164');
const uuid = conversation.get('uuid');
const readBy = this.get('read_by') || []; const readBy = this.get('read_by') || [];
if (readBy.indexOf(number) >= 0) { if (includesAny(readBy, identifier, e164, uuid)) {
return 'read'; return 'read';
} }
const deliveredTo = this.get('delivered_to') || []; const deliveredTo = this.get('delivered_to') || [];
if (deliveredTo.indexOf(number) >= 0) { if (includesAny(deliveredTo, identifier, e164, uuid)) {
return 'delivered'; return 'delivered';
} }
const sentTo = this.get('sent_to') || []; const sentTo = this.get('sent_to') || [];
if (sentTo.indexOf(number) >= 0) { if (includesAny(sentTo, identifier, e164, uuid)) {
return 'sent'; return 'sent';
} }
@ -982,17 +1014,24 @@
if (!fromSync) { if (!fromSync) {
const sender = this.getSource(); const sender = this.getSource();
const senderUuid = this.getSourceUuid();
const timestamp = this.get('sent_at'); const timestamp = this.get('sent_at');
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const { wrap, sendOptions } = ConversationController.prepareForSend( const { wrap, sendOptions } = ConversationController.prepareForSend(
ourNumber, ourNumber || ourUuid,
{ {
syncMessage: true, syncMessage: true,
} }
); );
await wrap( await wrap(
textsecure.messaging.syncViewOnceOpen(sender, timestamp, sendOptions) textsecure.messaging.syncViewOnceOpen(
sender,
senderUuid,
timestamp,
sendOptions
)
); );
} }
}, },
@ -1052,14 +1091,25 @@
return this.OUR_NUMBER; return this.OUR_NUMBER;
}, },
getSourceUuid() {
if (this.isIncoming()) {
return this.get('sourceUuid');
}
return this.OUR_UUID;
},
getContact() { getContact() {
const source = this.getSource(); const source = this.getSource();
const sourceUuid = this.getSourceUuid();
if (!source) { if (!source && !sourceUuid) {
return null; return null;
} }
return ConversationController.getOrCreate(source, 'private'); return ConversationController.getOrCreate(
source || sourceUuid,
'private'
);
}, },
isOutgoing() { isOutgoing() {
return this.get('type') === 'outgoing'; return this.get('type') === 'outgoing';
@ -1237,9 +1287,9 @@
// Special-case the self-send case - we send only a sync message // Special-case the self-send case - we send only a sync message
if (recipients.length === 1 && recipients[0] === this.OUR_NUMBER) { if (recipients.length === 1 && recipients[0] === this.OUR_NUMBER) {
const [number] = recipients; const [identifier] = recipients;
const dataMessage = await textsecure.messaging.getMessageProto( const dataMessage = await textsecure.messaging.getMessageProto(
number, identifier,
body, body,
attachments, attachments,
quoteWithData, quoteWithData,
@ -1257,9 +1307,9 @@
const options = conversation.getSendOptions(); const options = conversation.getSendOptions();
if (conversation.isPrivate()) { if (conversation.isPrivate()) {
const [number] = recipients; const [identifer] = recipients;
promise = textsecure.messaging.sendMessageToNumber( promise = textsecure.messaging.sendMessageToIdentifier(
number, identifer,
body, body,
attachments, attachments,
quoteWithData, quoteWithData,
@ -1327,8 +1377,8 @@
// Called when the user ran into an error with a specific user, wants to send to them // Called when the user ran into an error with a specific user, wants to send to them
// One caller today: ConversationView.forceSend() // One caller today: ConversationView.forceSend()
async resend(number) { async resend(identifier) {
const error = this.removeOutgoingErrors(number); const error = this.removeOutgoingErrors(identifier);
if (!error) { if (!error) {
window.log.warn('resend: requested number was not present in errors'); window.log.warn('resend: requested number was not present in errors');
return null; return null;
@ -1349,9 +1399,9 @@
const stickerWithData = await loadStickerData(this.get('sticker')); const stickerWithData = await loadStickerData(this.get('sticker'));
// Special-case the self-send case - we send only a sync message // Special-case the self-send case - we send only a sync message
if (number === this.OUR_NUMBER) { if (identifier === this.OUR_NUMBER || identifier === this.OUR_UUID) {
const dataMessage = await textsecure.messaging.getMessageProto( const dataMessage = await textsecure.messaging.getMessageProto(
number, identifier,
body, body,
attachments, attachments,
quoteWithData, quoteWithData,
@ -1366,10 +1416,10 @@
} }
const { wrap, sendOptions } = ConversationController.prepareForSend( const { wrap, sendOptions } = ConversationController.prepareForSend(
number identifier
); );
const promise = textsecure.messaging.sendMessageToNumber( const promise = textsecure.messaging.sendMessageToNumber(
number, identifier,
body, body,
attachments, attachments,
quoteWithData, quoteWithData,
@ -1411,7 +1461,7 @@
const sentTo = this.get('sent_to') || []; const sentTo = this.get('sent_to') || [];
this.set({ this.set({
sent_to: _.union(sentTo, result.successfulNumbers), sent_to: _.union(sentTo, result.successfulIdentifiers),
sent: true, sent: true,
expirationStartTimestamp: Date.now(), expirationStartTimestamp: Date.now(),
unidentifiedDeliveries: result.unidentifiedDeliveries, unidentifiedDeliveries: result.unidentifiedDeliveries,
@ -1442,7 +1492,7 @@
promises.push(c.getProfiles()); promises.push(c.getProfiles());
} }
} else { } else {
if (result.successfulNumbers.length > 0) { if (result.successfulIdentifiers.length > 0) {
const sentTo = this.get('sent_to') || []; const sentTo = this.get('sent_to') || [];
// In groups, we don't treat unregistered users as a user-visible // In groups, we don't treat unregistered users as a user-visible
@ -1462,7 +1512,7 @@
this.saveErrors(filteredErrors); this.saveErrors(filteredErrors);
this.set({ this.set({
sent_to: _.union(sentTo, result.successfulNumbers), sent_to: _.union(sentTo, result.successfulIdentifiers),
sent: true, sent: true,
expirationStartTimestamp, expirationStartTimestamp,
unidentifiedDeliveries: result.unidentifiedDeliveries, unidentifiedDeliveries: result.unidentifiedDeliveries,
@ -1488,12 +1538,13 @@
}, },
async sendSyncMessageOnly(dataMessage) { async sendSyncMessageOnly(dataMessage) {
const conv = this.getConversation();
this.set({ dataMessage }); this.set({ dataMessage });
try { try {
this.set({ this.set({
// These are the same as a normal send() // These are the same as a normal send()
sent_to: [this.OUR_NUMBER], sent_to: [conv.get('uuid') || conv.get('e164')],
sent: true, sent: true,
expirationStartTimestamp: Date.now(), expirationStartTimestamp: Date.now(),
}); });
@ -1503,8 +1554,8 @@
unidentifiedDeliveries: result ? result.unidentifiedDeliveries : null, unidentifiedDeliveries: result ? result.unidentifiedDeliveries : null,
// These are unique to a Note to Self message - immediately read/delivered // These are unique to a Note to Self message - immediately read/delivered
delivered_to: [this.OUR_NUMBER], delivered_to: [this.OUR_UUID || this.OUR_NUMBER],
read_by: [this.OUR_NUMBER], read_by: [this.OUR_UUID || this.OUR_NUMBER],
}); });
} catch (result) { } catch (result) {
const errors = (result && result.errors) || [ const errors = (result && result.errors) || [
@ -1528,8 +1579,9 @@
sendSyncMessage() { sendSyncMessage() {
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const { wrap, sendOptions } = ConversationController.prepareForSend( const { wrap, sendOptions } = ConversationController.prepareForSend(
ourNumber, ourUuid || ourNumber,
{ {
syncMessage: true, syncMessage: true,
} }
@ -1542,12 +1594,14 @@
return Promise.resolve(); return Promise.resolve();
} }
const isUpdate = Boolean(this.get('synced')); const isUpdate = Boolean(this.get('synced'));
const conv = this.getConversation();
return wrap( return wrap(
textsecure.messaging.sendSyncMessage( textsecure.messaging.sendSyncMessage(
dataMessage, dataMessage,
this.get('sent_at'), this.get('sent_at'),
this.get('destination'), conv.get('e164'),
conv.get('uuid'),
this.get('expirationStartTimestamp'), this.get('expirationStartTimestamp'),
this.get('sent_to'), this.get('sent_to'),
this.get('unidentifiedDeliveries'), this.get('unidentifiedDeliveries'),
@ -1773,7 +1827,11 @@
const found = collection.find(item => { const found = collection.find(item => {
const messageAuthor = item.getContact(); const messageAuthor = item.getContact();
return messageAuthor && author === messageAuthor.id; return (
messageAuthor &&
ConversationController.getConversationId(author) ===
messageAuthor.get('id')
);
}); });
if (!found) { if (!found) {
@ -1873,6 +1931,7 @@
// still go through one of the previous two codepaths // still go through one of the previous two codepaths
const message = this; const message = this;
const source = message.get('source'); const source = message.get('source');
const sourceUuid = message.get('sourceUuid');
const type = message.get('type'); const type = message.get('type');
let conversationId = message.get('conversationId'); let conversationId = message.get('conversationId');
if (initialMessage.group) { if (initialMessage.group) {
@ -1952,6 +2011,7 @@
// We drop incoming messages for groups we already know about, which we're not a // We drop incoming messages for groups we already know about, which we're not a
// part of, except for group updates. // part of, except for group updates.
const ourUuid = textsecure.storage.user.getUuid();
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const isGroupUpdate = const isGroupUpdate =
initialMessage.group && initialMessage.group &&
@ -1960,7 +2020,7 @@
if ( if (
type === 'incoming' && type === 'incoming' &&
!conversation.isPrivate() && !conversation.isPrivate() &&
!conversation.hasMember(ourNumber) && !conversation.hasMember(ourNumber || ourUuid) &&
!isGroupUpdate !isGroupUpdate
) { ) {
window.log.warn( window.log.warn(
@ -1982,6 +2042,7 @@
Whisper.deliveryReceiptQueue.add(() => { Whisper.deliveryReceiptQueue.add(() => {
Whisper.deliveryReceiptBatcher.add({ Whisper.deliveryReceiptBatcher.add({
source, source,
sourceUuid,
timestamp: this.get('sent_at'), timestamp: this.get('sent_at'),
}); });
}); });
@ -2044,6 +2105,20 @@
}; };
if (dataMessage.group) { if (dataMessage.group) {
let groupUpdate = null; let groupUpdate = null;
const memberConversations = await Promise.all(
(
dataMessage.group.members || dataMessage.group.membersE164
).map(member => {
if (member.e164 || member.uuid) {
return ConversationController.getOrCreateAndWait(
member.e164 || member.uuid,
'private'
);
}
return ConversationController.getOrCreateAndWait(member);
})
);
const members = memberConversations.map(c => c.get('id'));
attributes = { attributes = {
...attributes, ...attributes,
type: 'group', type: 'group',
@ -2053,10 +2128,7 @@
attributes = { attributes = {
...attributes, ...attributes,
name: dataMessage.group.name, name: dataMessage.group.name,
members: _.union( members: _.union(members, conversation.get('members')),
dataMessage.group.members,
conversation.get('members')
),
}; };
groupUpdate = groupUpdate =
@ -2065,7 +2137,7 @@
) || {}; ) || {};
const difference = _.difference( const difference = _.difference(
attributes.members, members,
conversation.get('members') conversation.get('members')
); );
if (difference.length > 0) { if (difference.length > 0) {
@ -2076,15 +2148,22 @@
attributes.left = false; attributes.left = false;
} }
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) { } else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
if (source === textsecure.storage.user.getNumber()) { if (
source === textsecure.storage.user.getNumber() ||
sourceUuid === textsecure.storage.user.getUuid()
) {
attributes.left = true; attributes.left = true;
groupUpdate = { left: 'You' }; groupUpdate = { left: 'You' };
} else { } else {
groupUpdate = { left: source }; const myConversation = ConversationController.get(
source || sourceUuid
);
groupUpdate = { left: myConversation.get('id') };
} }
attributes.members = _.without( attributes.members = _.without(
conversation.get('members'), conversation.get('members'),
source source,
sourceUuid
); );
} }
@ -2102,7 +2181,7 @@
message.set({ message.set({
delivered: (message.get('delivered') || 0) + 1, delivered: (message.get('delivered') || 0) + 1,
delivered_to: _.union(message.get('delivered_to') || [], [ delivered_to: _.union(message.get('delivered_to') || [], [
receipt.get('source'), receipt.get('deliveredTo'),
]), ]),
}) })
); );
@ -2216,13 +2295,16 @@
if (dataMessage.profileKey) { if (dataMessage.profileKey) {
const profileKey = dataMessage.profileKey.toString('base64'); const profileKey = dataMessage.profileKey.toString('base64');
if (source === textsecure.storage.user.getNumber()) { if (
source === textsecure.storage.user.getNumber() ||
sourceUuid === textsecure.storage.user.getUuid()
) {
conversation.set({ profileSharing: true }); conversation.set({ profileSharing: true });
} else if (conversation.isPrivate()) { } else if (conversation.isPrivate()) {
conversation.setProfileKey(profileKey); conversation.setProfileKey(profileKey);
} else { } else {
ConversationController.getOrCreateAndWait( ConversationController.getOrCreateAndWait(
source, source || sourceUuid,
'private' 'private'
).then(sender => { ).then(sender => {
sender.setProfileKey(profileKey); sender.setProfileKey(profileKey);

View file

@ -1118,8 +1118,14 @@ async function importConversations(dir, options) {
function getMessageKey(message) { function getMessageKey(message) {
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const source = message.source || ourNumber; const source = message.source || ourNumber;
if (source === ourNumber) { const sourceUuid = message.sourceUuid || ourUuid;
if (
(source && source === ourNumber) ||
(sourceUuid && sourceUuid === ourUuid)
) {
return `${source} ${message.timestamp}`; return `${source} ${message.timestamp}`;
} }

View file

@ -1,4 +1,4 @@
/* global window, setTimeout, IDBKeyRange */ /* global window, setTimeout, IDBKeyRange, ConversationController */
const electron = require('electron'); const electron = require('electron');
@ -84,10 +84,10 @@ module.exports = {
createOrUpdateSession, createOrUpdateSession,
createOrUpdateSessions, createOrUpdateSessions,
getSessionById, getSessionById,
getSessionsByNumber, getSessionsById,
bulkAddSessions, bulkAddSessions,
removeSessionById, removeSessionById,
removeSessionsByNumber, removeSessionsById,
removeAllSessions, removeAllSessions,
getAllSessions, getAllSessions,
@ -431,10 +431,14 @@ async function removeIndexedDBFiles() {
const IDENTITY_KEY_KEYS = ['publicKey']; const IDENTITY_KEY_KEYS = ['publicKey'];
async function createOrUpdateIdentityKey(data) { async function createOrUpdateIdentityKey(data) {
const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, data); const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, {
...data,
id: ConversationController.getConversationId(data.id),
});
await channels.createOrUpdateIdentityKey(updated); await channels.createOrUpdateIdentityKey(updated);
} }
async function getIdentityKeyById(id) { async function getIdentityKeyById(identifier) {
const id = ConversationController.getConversationId(identifier);
const data = await channels.getIdentityKeyById(id); const data = await channels.getIdentityKeyById(id);
return keysToArrayBuffer(IDENTITY_KEY_KEYS, data); return keysToArrayBuffer(IDENTITY_KEY_KEYS, data);
} }
@ -444,7 +448,8 @@ async function bulkAddIdentityKeys(array) {
); );
await channels.bulkAddIdentityKeys(updated); await channels.bulkAddIdentityKeys(updated);
} }
async function removeIdentityKeyById(id) { async function removeIdentityKeyById(identifier) {
const id = ConversationController.getConversationId(identifier);
await channels.removeIdentityKeyById(id); await channels.removeIdentityKeyById(id);
} }
async function removeAllIdentityKeys() { async function removeAllIdentityKeys() {
@ -515,6 +520,11 @@ const ITEM_KEYS = {
'value.signature', 'value.signature',
'value.serialized', 'value.serialized',
], ],
senderCertificateWithUuid: [
'value.certificate',
'value.signature',
'value.serialized',
],
signaling_key: ['value'], signaling_key: ['value'],
profileKey: ['value'], profileKey: ['value'],
}; };
@ -572,8 +582,8 @@ async function getSessionById(id) {
const session = await channels.getSessionById(id); const session = await channels.getSessionById(id);
return session; return session;
} }
async function getSessionsByNumber(number) { async function getSessionsById(id) {
const sessions = await channels.getSessionsByNumber(number); const sessions = await channels.getSessionsById(id);
return sessions; return sessions;
} }
async function bulkAddSessions(array) { async function bulkAddSessions(array) {
@ -582,8 +592,8 @@ async function bulkAddSessions(array) {
async function removeSessionById(id) { async function removeSessionById(id) {
await channels.removeSessionById(id); await channels.removeSessionById(id);
} }
async function removeSessionsByNumber(number) { async function removeSessionsById(id) {
await channels.removeSessionsByNumber(number); await channels.removeSessionsById(id);
} }
async function removeAllSessions(id) { async function removeAllSessions(id) {
await channels.removeAllSessions(id); await channels.removeAllSessions(id);
@ -799,11 +809,12 @@ async function getAllMessageIds() {
async function getMessageBySender( async function getMessageBySender(
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
{ source, sourceDevice, sent_at }, { source, sourceUuid, sourceDevice, sent_at },
{ Message } { Message }
) { ) {
const messages = await channels.getMessageBySender({ const messages = await channels.getMessageBySender({
source, source,
sourceUuid,
sourceDevice, sourceDevice,
sent_at, sent_at,
}); });

View file

@ -117,13 +117,14 @@ function _createSenderCertificateFromBuffer(serialized) {
!certificate.identityKey || !certificate.identityKey ||
!certificate.senderDevice || !certificate.senderDevice ||
!certificate.expires || !certificate.expires ||
!certificate.sender !(certificate.sender || certificate.senderUuid)
) { ) {
throw new Error('Missing fields'); throw new Error('Missing fields');
} }
return { return {
sender: certificate.sender, sender: certificate.sender,
senderUuid: certificate.senderUuid,
senderDevice: certificate.senderDevice, senderDevice: certificate.senderDevice,
expires: certificate.expires.toNumber(), expires: certificate.expires.toNumber(),
identityKey: certificate.identityKey.toArrayBuffer(), identityKey: certificate.identityKey.toArrayBuffer(),
@ -344,7 +345,7 @@ SecretSessionCipher.prototype = {
// public Pair<SignalProtocolAddress, byte[]> decrypt( // public Pair<SignalProtocolAddress, byte[]> decrypt(
// CertificateValidator validator, byte[] ciphertext, long timestamp) // CertificateValidator validator, byte[] ciphertext, long timestamp)
async decrypt(validator, ciphertext, timestamp, me) { async decrypt(validator, ciphertext, timestamp, me = {}) {
// Capture this.xxx variables to replicate Java's implicit this syntax // Capture this.xxx variables to replicate Java's implicit this syntax
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this); const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
@ -401,18 +402,29 @@ SecretSessionCipher.prototype = {
); );
} }
const { sender, senderDevice } = content.senderCertificate; const { sender, senderUuid, senderDevice } = content.senderCertificate;
const { number, deviceId } = me || {}; if (
if (sender === number && senderDevice === deviceId) { ((sender && me.number && sender === me.number) ||
(senderUuid && me.uuid && senderUuid === me.uuid)) &&
senderDevice === me.deviceId
) {
return { return {
isMe: true, isMe: true,
}; };
} }
const address = new libsignal.SignalProtocolAddress(sender, senderDevice); const addressE164 =
sender && new libsignal.SignalProtocolAddress(sender, senderDevice);
const addressUuid =
senderUuid &&
new libsignal.SignalProtocolAddress(
senderUuid.toLowerCase(),
senderDevice
);
try { try {
return { return {
sender: address, sender: addressE164,
senderUuid: addressUuid,
content: await _decryptWithUnidentifiedSenderMessage(content), content: await _decryptWithUnidentifiedSenderMessage(content),
}; };
} catch (error) { } catch (error) {
@ -421,7 +433,8 @@ SecretSessionCipher.prototype = {
error = new Error('Decryption error was falsey!'); error = new Error('Decryption error was falsey!');
} }
error.sender = address; error.sender = addressE164;
error.senderUuid = addressUuid;
throw error; throw error;
} }
@ -504,7 +517,7 @@ SecretSessionCipher.prototype = {
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
const sender = new libsignal.SignalProtocolAddress( const sender = new libsignal.SignalProtocolAddress(
message.senderCertificate.sender, message.senderCertificate.sender || message.senderCertificate.senderUuid,
message.senderCertificate.senderDevice message.senderCertificate.senderDevice
); );

View file

@ -8,6 +8,7 @@ const { escapeRegExp } = require('lodash');
const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..'); const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..');
const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g; const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g;
const UUID_PATTERN = /[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{10}([0-9A-F]{2})/gi;
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g; const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
const REDACTION_PLACEHOLDER = '[REDACTED]'; const REDACTION_PLACEHOLDER = '[REDACTED]';
@ -64,6 +65,15 @@ exports.redactPhoneNumbers = text => {
return text.replace(PHONE_NUMBER_PATTERN, `+${REDACTION_PLACEHOLDER}$1`); return text.replace(PHONE_NUMBER_PATTERN, `+${REDACTION_PLACEHOLDER}$1`);
}; };
// redactUuids :: String -> String
exports.redactUuids = text => {
if (!is.string(text)) {
throw new TypeError("'text' must be a string");
}
return text.replace(UUID_PATTERN, `${REDACTION_PLACEHOLDER}$1`);
};
// redactGroupIds :: String -> String // redactGroupIds :: String -> String
exports.redactGroupIds = text => { exports.redactGroupIds = text => {
if (!is.string(text)) { if (!is.string(text)) {
@ -84,7 +94,8 @@ exports.redactSensitivePaths = exports._redactPath(APP_ROOT_PATH);
exports.redactAll = compose( exports.redactAll = compose(
exports.redactSensitivePaths, exports.redactSensitivePaths,
exports.redactGroupIds, exports.redactGroupIds,
exports.redactPhoneNumbers exports.redactPhoneNumbers,
exports.redactUuids
); );
const removeNewlines = text => text.replace(/\r?\n|\r/g, ''); const removeNewlines = text => text.replace(/\r?\n|\r/g, '');

View file

@ -16,7 +16,15 @@ let scheduleNext = null;
function refreshOurProfile() { function refreshOurProfile() {
window.log.info('refreshOurProfile'); window.log.info('refreshOurProfile');
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const conversation = ConversationController.getOrCreate(ourNumber, 'private'); const ourUuid = textsecure.storage.user.getUuid();
const conversation = ConversationController.getOrCreate(
// This is explicitly ourNumber first in order to avoid creating new
// conversations when an old one exists
ourNumber || ourUuid,
'private'
);
conversation.updateUuid(ourUuid);
conversation.updateE164(ourNumber);
conversation.getProfiles(); conversation.getProfiles();
} }
@ -66,21 +74,36 @@ function initialize({ events, storage, navigator, logger }) {
async function run() { async function run() {
logger.info('refreshSenderCertificate: Getting new certificate...'); logger.info('refreshSenderCertificate: Getting new certificate...');
try { try {
const username = storage.get('number_id'); const OLD_USERNAME = storage.get('number_id');
const password = storage.get('password'); const USERNAME = storage.get('uuid_id');
const server = WebAPI.connect({ username, password }); const PASSWORD = storage.get('password');
const server = WebAPI.connect({
username: USERNAME || OLD_USERNAME,
password: PASSWORD,
});
const { certificate } = await server.getSenderCertificate(); await Promise.all(
const arrayBuffer = window.Signal.Crypto.base64ToArrayBuffer(certificate); [false, true].map(async withUuid => {
const decoded = textsecure.protobuf.SenderCertificate.decode(arrayBuffer); const { certificate } = await server.getSenderCertificate(withUuid);
const arrayBuffer = window.Signal.Crypto.base64ToArrayBuffer(
certificate
);
const decoded = textsecure.protobuf.SenderCertificate.decode(
arrayBuffer
);
decoded.certificate = decoded.certificate.toArrayBuffer(); decoded.certificate = decoded.certificate.toArrayBuffer();
decoded.signature = decoded.signature.toArrayBuffer(); decoded.signature = decoded.signature.toArrayBuffer();
decoded.serialized = arrayBuffer; decoded.serialized = arrayBuffer;
storage.put(
`senderCertificate${withUuid ? 'WithUuid' : ''}`,
decoded
);
})
);
storage.put('senderCertificate', decoded);
scheduledTime = null; scheduledTime = null;
scheduleNextRotation(); scheduleNextRotation();
} catch (error) { } catch (error) {
logger.error( logger.error(

View file

@ -394,12 +394,14 @@ const URL_CALLS = {
attachmentId: 'v2/attachments/form/upload', attachmentId: 'v2/attachments/form/upload',
deliveryCert: 'v1/certificate/delivery', deliveryCert: 'v1/certificate/delivery',
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery', supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
registerCapabilities: 'v1/devices/capabilities',
devices: 'v1/devices', devices: 'v1/devices',
keys: 'v2/keys', keys: 'v2/keys',
messages: 'v1/messages', messages: 'v1/messages',
profile: 'v1/profile', profile: 'v1/profile',
signed: 'v2/keys/signed', signed: 'v2/keys/signed',
getStickerPackUpload: 'v1/sticker/pack/form', getStickerPackUpload: 'v1/sticker/pack/form',
whoami: 'v1/accounts/whoami',
}; };
module.exports = { module.exports = {
@ -451,8 +453,8 @@ function initialize({
getAttachment, getAttachment,
getAvatar, getAvatar,
getDevices, getDevices,
getKeysForNumber, getKeysForIdentifier,
getKeysForNumberUnauth, getKeysForIdentifierUnauth,
getMessageSocket, getMessageSocket,
getMyKeys, getMyKeys,
getProfile, getProfile,
@ -463,6 +465,7 @@ function initialize({
getStickerPackManifest, getStickerPackManifest,
makeProxiedRequest, makeProxiedRequest,
putAttachment, putAttachment,
registerCapabilities,
putStickers, putStickers,
registerKeys, registerKeys,
registerSupportForUnauthenticatedDelivery, registerSupportForUnauthenticatedDelivery,
@ -473,6 +476,7 @@ function initialize({
sendMessagesUnauth, sendMessagesUnauth,
setSignedPreKey, setSignedPreKey,
updateDeviceName, updateDeviceName,
whoami,
}; };
function _ajax(param) { function _ajax(param) {
@ -535,12 +539,21 @@ function initialize({
}); });
} }
function getSenderCertificate() { function whoami() {
return _ajax({
call: 'whoami',
httpType: 'GET',
responseType: 'json',
});
}
function getSenderCertificate(withUuid = false) {
return _ajax({ return _ajax({
call: 'deliveryCert', call: 'deliveryCert',
httpType: 'GET', httpType: 'GET',
responseType: 'json', responseType: 'json',
schema: { certificate: 'string' }, schema: { certificate: 'string' },
urlParameters: withUuid ? '?includeUuid=true' : undefined,
}); });
} }
@ -552,19 +565,27 @@ function initialize({
}); });
} }
function getProfile(number) { function registerCapabilities(capabilities) {
return _ajax({
call: 'registerCapabilities',
httpType: 'PUT',
jsonData: { capabilities },
});
}
function getProfile(identifier) {
return _ajax({ return _ajax({
call: 'profile', call: 'profile',
httpType: 'GET', httpType: 'GET',
urlParameters: `/${number}`, urlParameters: `/${identifier}`,
responseType: 'json', responseType: 'json',
}); });
} }
function getProfileUnauth(number, { accessKey } = {}) { function getProfileUnauth(identifier, { accessKey } = {}) {
return _ajax({ return _ajax({
call: 'profile', call: 'profile',
httpType: 'GET', httpType: 'GET',
urlParameters: `/${number}`, urlParameters: `/${identifier}`,
responseType: 'json', responseType: 'json',
unauthenticated: true, unauthenticated: true,
accessKey, accessKey,
@ -623,17 +644,17 @@ function initialize({
let call; let call;
let urlPrefix; let urlPrefix;
let schema; let schema;
let responseType;
if (deviceName) { if (deviceName) {
jsonData.name = deviceName; jsonData.name = deviceName;
call = 'devices'; call = 'devices';
urlPrefix = '/'; urlPrefix = '/';
schema = { deviceId: 'number' };
responseType = 'json';
} else { } else {
call = 'accounts'; call = 'accounts';
urlPrefix = '/code/'; urlPrefix = '/code/';
jsonData.capabilities = {
uuid: true,
};
} }
// We update our saved username and password, since we're creating a new account // We update our saved username and password, since we're creating a new account
@ -643,14 +664,14 @@ function initialize({
const response = await _ajax({ const response = await _ajax({
call, call,
httpType: 'PUT', httpType: 'PUT',
responseType: 'json',
urlParameters: urlPrefix + code, urlParameters: urlPrefix + code,
jsonData, jsonData,
responseType,
validateResponse: schema, validateResponse: schema,
}); });
// From here on out, our username will be our phone number combined with device // From here on out, our username will be our UUID or E164 combined with device
username = `${number}.${response.deviceId || 1}`; username = `${response.uuid || number}.${response.deviceId || 1}`;
return response; return response;
} }
@ -768,25 +789,25 @@ function initialize({
return res; return res;
} }
function getKeysForNumber(number, deviceId = '*') { function getKeysForIdentifier(identifier, deviceId = '*') {
return _ajax({ return _ajax({
call: 'keys', call: 'keys',
httpType: 'GET', httpType: 'GET',
urlParameters: `/${number}/${deviceId}`, urlParameters: `/${identifier}/${deviceId}`,
responseType: 'json', responseType: 'json',
validateResponse: { identityKey: 'string', devices: 'object' }, validateResponse: { identityKey: 'string', devices: 'object' },
}).then(handleKeys); }).then(handleKeys);
} }
function getKeysForNumberUnauth( function getKeysForIdentifierUnauth(
number, identifier,
deviceId = '*', deviceId = '*',
{ accessKey } = {} { accessKey } = {}
) { ) {
return _ajax({ return _ajax({
call: 'keys', call: 'keys',
httpType: 'GET', httpType: 'GET',
urlParameters: `/${number}/${deviceId}`, urlParameters: `/${identifier}/${deviceId}`,
responseType: 'json', responseType: 'json',
validateResponse: { identityKey: 'string', devices: 'object' }, validateResponse: { identityKey: 'string', devices: 'object' },
unauthenticated: true, unauthenticated: true,

View file

@ -36,7 +36,9 @@
const found = messages.find( const found = messages.find(
item => item =>
item.isIncoming() && item.get('source') === receipt.get('sender') item.isIncoming() &&
(item.get('source') === receipt.get('sender') ||
item.get('sourceUuid') === receipt.get('senderUuid'))
); );
const notificationForMessage = found const notificationForMessage = found
? Whisper.Notifications.findWhere({ messageId: found.id }) ? Whisper.Notifications.findWhere({ messageId: found.id })
@ -47,6 +49,7 @@
window.log.info( window.log.info(
'No message for read sync', 'No message for read sync',
receipt.get('sender'), receipt.get('sender'),
receipt.get('senderUuid'),
receipt.get('timestamp') receipt.get('timestamp')
); );
return; return;

View file

@ -147,6 +147,24 @@
}, },
}); });
async function normalizeEncodedAddress(encodedAddress) {
const [identifier, deviceId] = textsecure.utils.unencodeNumber(
encodedAddress
);
try {
const conv = await ConversationController.getOrCreateAndWait(
identifier,
'private'
);
return `${conv.get('id')}.${deviceId}`;
} catch (e) {
window.log.error(
`could not get conversation for identifier ${identifier}`
);
throw e;
}
}
function SignalProtocolStore() { function SignalProtocolStore() {
this.sessionUpdateBatcher = window.Signal.Util.createBatcher({ this.sessionUpdateBatcher = window.Signal.Util.createBatcher({
wait: 500, wait: 500,
@ -322,66 +340,98 @@
// Sessions // Sessions
async loadSession(encodedNumber) { async loadSession(encodedAddress) {
if (encodedNumber === null || encodedNumber === undefined) { if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('Tried to get session for undefined/null number'); throw new Error('Tried to get session for undefined/null number');
} }
const session = this.sessions[encodedNumber]; try {
if (session) { const id = await normalizeEncodedAddress(encodedAddress);
return session.record; const session = this.sessions[id];
if (session) {
return session.record;
}
} catch (e) {
window.log.error(`could not load session ${encodedAddress}`);
} }
return undefined; return undefined;
}, },
async storeSession(encodedNumber, record) { async storeSession(encodedAddress, record) {
if (encodedNumber === null || encodedNumber === undefined) { if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('Tried to put session for undefined/null number'); throw new Error('Tried to put session for undefined/null number');
} }
const unencoded = textsecure.utils.unencodeNumber(encodedNumber); const unencoded = textsecure.utils.unencodeNumber(encodedAddress);
const number = unencoded[0];
const deviceId = parseInt(unencoded[1], 10); const deviceId = parseInt(unencoded[1], 10);
const data = { try {
id: encodedNumber, const id = await normalizeEncodedAddress(encodedAddress);
number,
deviceId,
record,
};
this.sessions[encodedNumber] = data; const data = {
id,
conversationId: textsecure.utils.unencodeNumber(id)[0],
deviceId,
record,
};
// Note: Because these are cached in memory, we batch and make these database this.sessions[id] = data;
// updates out of band.
this.sessionUpdateBatcher.add(data); // Note: Because these are cached in memory, we batch and make these database
// updates out of band.
this.sessionUpdateBatcher.add(data);
} catch (e) {
window.log.error(`could not store session for ${encodedAddress}`);
}
}, },
async getDeviceIds(number) { async getDeviceIds(identifier) {
if (number === null || number === undefined) { if (identifier === null || identifier === undefined) {
throw new Error('Tried to get device ids for undefined/null number'); throw new Error('Tried to get device ids for undefined/null number');
} }
const allSessions = Object.values(this.sessions); try {
const sessions = allSessions.filter(session => session.number === number); const id = ConversationController.getConversationId(identifier);
return _.pluck(sessions, 'deviceId'); const allSessions = Object.values(this.sessions);
const sessions = allSessions.filter(
session => session.conversationId === id
);
return _.pluck(sessions, 'deviceId');
} catch (e) {
window.log.error(
`could not get device ids for identifier ${identifier}`
);
}
return [];
}, },
async removeSession(encodedNumber) { async removeSession(encodedAddress) {
window.log.info('deleting session for ', encodedNumber); window.log.info('deleting session for ', encodedAddress);
delete this.sessions[encodedNumber]; try {
await window.Signal.Data.removeSessionById(encodedNumber); const id = await normalizeEncodedAddress(encodedAddress);
delete this.sessions[id];
await window.Signal.Data.removeSessionById(id);
} catch (e) {
window.log.error(`could not delete session for ${encodedAddress}`);
}
}, },
async removeAllSessions(number) { async removeAllSessions(identifier) {
if (number === null || number === undefined) { if (identifier === null || identifier === undefined) {
throw new Error('Tried to remove sessions for undefined/null number'); throw new Error('Tried to remove sessions for undefined/null number');
} }
const id = ConversationController.getConversationId(identifier);
const allSessions = Object.values(this.sessions); const allSessions = Object.values(this.sessions);
for (let i = 0, max = allSessions.length; i < max; i += 1) { for (let i = 0, max = allSessions.length; i < max; i += 1) {
const session = allSessions[i]; const session = allSessions[i];
if (session.number === number) { if (session.conversationId === id) {
delete this.sessions[session.id]; delete this.sessions[session.id];
} }
} }
await window.Signal.Data.removeSessionsByNumber(number);
await window.Signal.Data.removeSessionsById(identifier);
}, },
async archiveSiblingSessions(identifier) { async archiveSiblingSessions(identifier) {
const address = libsignal.SignalProtocolAddress.fromString(identifier); const address = libsignal.SignalProtocolAddress.fromString(identifier);
@ -404,12 +454,15 @@
}) })
); );
}, },
async archiveAllSessions(number) { async archiveAllSessions(identifier) {
const deviceIds = await this.getDeviceIds(number); const deviceIds = await this.getDeviceIds(identifier);
await Promise.all( await Promise.all(
deviceIds.map(async deviceId => { deviceIds.map(async deviceId => {
const address = new libsignal.SignalProtocolAddress(number, deviceId); const address = new libsignal.SignalProtocolAddress(
identifier,
deviceId
);
window.log.info('closing session for', address.toString()); window.log.info('closing session for', address.toString());
const sessionCipher = new libsignal.SessionCipher( const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol, textsecure.storage.protocol,
@ -426,16 +479,35 @@
// Identity Keys // Identity Keys
async isTrustedIdentity(identifier, publicKey, direction) { getIdentityRecord(identifier) {
if (identifier === null || identifier === undefined) { try {
const id = ConversationController.getConversationId(identifier);
const record = this.identityKeys[id];
if (record) {
return record;
}
} catch (e) {
window.log.error(
`could not get identity record for identifier ${identifier}`
);
}
return undefined;
},
async isTrustedIdentity(encodedAddress, publicKey, direction) {
if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('Tried to get identity key for undefined/null key'); throw new Error('Tried to get identity key for undefined/null key');
} }
const number = textsecure.utils.unencodeNumber(identifier)[0]; const identifier = textsecure.utils.unencodeNumber(encodedAddress)[0];
const isOurNumber = number === textsecure.storage.user.getNumber(); const isOurIdentifier =
identifier === textsecure.storage.user.getNumber() ||
identifier === textsecure.storage.user.getUuid();
const identityRecord = this.identityKeys[number]; const identityRecord = this.getIdentityRecord(identifier);
if (isOurNumber) { if (isOurIdentifier) {
const existing = identityRecord ? identityRecord.publicKey : null; const existing = identityRecord ? identityRecord.publicKey : null;
return equalArrayBuffers(existing, publicKey); return equalArrayBuffers(existing, publicKey);
} }
@ -482,8 +554,8 @@
if (identifier === null || identifier === undefined) { if (identifier === null || identifier === undefined) {
throw new Error('Tried to get identity key for undefined/null key'); throw new Error('Tried to get identity key for undefined/null key');
} }
const number = textsecure.utils.unencodeNumber(identifier)[0]; const id = textsecure.utils.unencodeNumber(identifier)[0];
const identityRecord = this.identityKeys[number]; const identityRecord = this.getIdentityRecord(id);
if (identityRecord) { if (identityRecord) {
return identityRecord.publicKey; return identityRecord.publicKey;
@ -496,8 +568,8 @@
this.identityKeys[id] = data; this.identityKeys[id] = data;
await window.Signal.Data.createOrUpdateIdentityKey(data); await window.Signal.Data.createOrUpdateIdentityKey(data);
}, },
async saveIdentity(identifier, publicKey, nonblockingApproval) { async saveIdentity(encodedAddress, publicKey, nonblockingApproval) {
if (identifier === null || identifier === undefined) { if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('Tried to put identity key for undefined/null key'); throw new Error('Tried to put identity key for undefined/null key');
} }
if (!(publicKey instanceof ArrayBuffer)) { if (!(publicKey instanceof ArrayBuffer)) {
@ -509,14 +581,15 @@
nonblockingApproval = false; nonblockingApproval = false;
} }
const number = textsecure.utils.unencodeNumber(identifier)[0]; const identifer = textsecure.utils.unencodeNumber(encodedAddress)[0];
const identityRecord = this.identityKeys[number]; const identityRecord = this.getIdentityRecord(identifer);
const id = ConversationController.getConversationId(identifer);
if (!identityRecord || !identityRecord.publicKey) { if (!identityRecord || !identityRecord.publicKey) {
// Lookup failed, or the current key was removed, so save this one. // Lookup failed, or the current key was removed, so save this one.
window.log.info('Saving new identity...'); window.log.info('Saving new identity...');
await this._saveIdentityKey({ await this._saveIdentityKey({
id: number, id,
publicKey, publicKey,
firstUse: true, firstUse: true,
timestamp: Date.now(), timestamp: Date.now(),
@ -542,7 +615,7 @@
} }
await this._saveIdentityKey({ await this._saveIdentityKey({
id: number, id,
publicKey, publicKey,
firstUse: false, firstUse: false,
timestamp: Date.now(), timestamp: Date.now(),
@ -551,14 +624,14 @@
}); });
try { try {
this.trigger('keychange', number); this.trigger('keychange', identifer);
} catch (error) { } catch (error) {
window.log.error( window.log.error(
'saveIdentity error triggering keychange:', 'saveIdentity error triggering keychange:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
} }
await this.archiveSiblingSessions(identifier); await this.archiveSiblingSessions(encodedAddress);
return true; return true;
} else if (this.isNonBlockingApprovalRequired(identityRecord)) { } else if (this.isNonBlockingApprovalRequired(identityRecord)) {
@ -579,16 +652,21 @@
!identityRecord.nonblockingApproval !identityRecord.nonblockingApproval
); );
}, },
async saveIdentityWithAttributes(identifier, attributes) { async saveIdentityWithAttributes(encodedAddress, attributes) {
if (identifier === null || identifier === undefined) { if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('Tried to put identity key for undefined/null key'); throw new Error('Tried to put identity key for undefined/null key');
} }
const number = textsecure.utils.unencodeNumber(identifier)[0]; const identifier = textsecure.utils.unencodeNumber(encodedAddress)[0];
const identityRecord = this.identityKeys[number]; const identityRecord = this.getIdentityRecord(identifier);
const conv = await ConversationController.getOrCreateAndWait(
identifier,
'private'
);
const id = conv.get('id');
const updates = { const updates = {
id: number, id,
...identityRecord, ...identityRecord,
...attributes, ...attributes,
}; };
@ -600,26 +678,26 @@
throw model.validationError; throw model.validationError;
} }
}, },
async setApproval(identifier, nonblockingApproval) { async setApproval(encodedAddress, nonblockingApproval) {
if (identifier === null || identifier === undefined) { if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('Tried to set approval for undefined/null identifier'); throw new Error('Tried to set approval for undefined/null identifier');
} }
if (typeof nonblockingApproval !== 'boolean') { if (typeof nonblockingApproval !== 'boolean') {
throw new Error('Invalid approval status'); throw new Error('Invalid approval status');
} }
const number = textsecure.utils.unencodeNumber(identifier)[0]; const identifier = textsecure.utils.unencodeNumber(encodedAddress)[0];
const identityRecord = this.identityKeys[number]; const identityRecord = this.getIdentityRecord(identifier);
if (!identityRecord) { if (!identityRecord) {
throw new Error(`No identity record for ${number}`); throw new Error(`No identity record for ${identifier}`);
} }
identityRecord.nonblockingApproval = nonblockingApproval; identityRecord.nonblockingApproval = nonblockingApproval;
await this._saveIdentityKey(identityRecord); await this._saveIdentityKey(identityRecord);
}, },
async setVerified(number, verifiedStatus, publicKey) { async setVerified(encodedAddress, verifiedStatus, publicKey) {
if (number === null || number === undefined) { if (encodedAddress === null || encodedAddress === undefined) {
throw new Error('Tried to set verified for undefined/null key'); throw new Error('Tried to set verified for undefined/null key');
} }
if (!validateVerifiedStatus(verifiedStatus)) { if (!validateVerifiedStatus(verifiedStatus)) {
@ -629,9 +707,10 @@
throw new Error('Invalid public key'); throw new Error('Invalid public key');
} }
const identityRecord = this.identityKeys[number]; const identityRecord = this.getIdentityRecord(encodedAddress);
if (!identityRecord) { if (!identityRecord) {
throw new Error(`No identity record for ${number}`); throw new Error(`No identity record for ${encodedAddress}`);
} }
if ( if (
@ -650,14 +729,14 @@
window.log.info('No identity record for specified publicKey'); window.log.info('No identity record for specified publicKey');
} }
}, },
async getVerified(number) { async getVerified(identifier) {
if (number === null || number === undefined) { if (identifier === null || identifier === undefined) {
throw new Error('Tried to set verified for undefined/null key'); throw new Error('Tried to set verified for undefined/null key');
} }
const identityRecord = this.identityKeys[number]; const identityRecord = this.getIdentityRecord(identifier);
if (!identityRecord) { if (!identityRecord) {
throw new Error(`No identity record for ${number}`); throw new Error(`No identity record for ${identifier}`);
} }
const verifiedStatus = identityRecord.verified; const verifiedStatus = identityRecord.verified;
@ -681,15 +760,16 @@
// This function encapsulates the non-Java behavior, since the mobile apps don't // This function encapsulates the non-Java behavior, since the mobile apps don't
// currently receive contact syncs and therefore will see a verify sync with // currently receive contact syncs and therefore will see a verify sync with
// UNVERIFIED status // UNVERIFIED status
async processUnverifiedMessage(number, verifiedStatus, publicKey) { async processUnverifiedMessage(identifier, verifiedStatus, publicKey) {
if (number === null || number === undefined) { if (identifier === null || identifier === undefined) {
throw new Error('Tried to set verified for undefined/null key'); throw new Error('Tried to set verified for undefined/null key');
} }
if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) { if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) {
throw new Error('Invalid public key'); throw new Error('Invalid public key');
} }
const identityRecord = this.identityKeys[number]; const identityRecord = this.getIdentityRecord(identifier);
const isPresent = Boolean(identityRecord); const isPresent = Boolean(identityRecord);
let isEqual = false; let isEqual = false;
@ -703,7 +783,7 @@
identityRecord.verified !== VerifiedStatus.UNVERIFIED identityRecord.verified !== VerifiedStatus.UNVERIFIED
) { ) {
await textsecure.storage.protocol.setVerified( await textsecure.storage.protocol.setVerified(
number, identifier,
verifiedStatus, verifiedStatus,
publicKey publicKey
); );
@ -711,17 +791,20 @@
} }
if (!isPresent || !isEqual) { if (!isPresent || !isEqual) {
await textsecure.storage.protocol.saveIdentityWithAttributes(number, { await textsecure.storage.protocol.saveIdentityWithAttributes(
publicKey, identifier,
verified: verifiedStatus, {
firstUse: false, publicKey,
timestamp: Date.now(), verified: verifiedStatus,
nonblockingApproval: true, firstUse: false,
}); timestamp: Date.now(),
nonblockingApproval: true,
}
);
if (isPresent && !isEqual) { if (isPresent && !isEqual) {
try { try {
this.trigger('keychange', number); this.trigger('keychange', identifier);
} catch (error) { } catch (error) {
window.log.error( window.log.error(
'processUnverifiedMessage error triggering keychange:', 'processUnverifiedMessage error triggering keychange:',
@ -729,7 +812,7 @@
); );
} }
await this.archiveAllSessions(number); await this.archiveAllSessions(identifier);
return true; return true;
} }
@ -743,8 +826,8 @@
}, },
// This matches the Java method as of // This matches the Java method as of
// https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188 // https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188
async processVerifiedMessage(number, verifiedStatus, publicKey) { async processVerifiedMessage(identifier, verifiedStatus, publicKey) {
if (number === null || number === undefined) { if (identifier === null || identifier === undefined) {
throw new Error('Tried to set verified for undefined/null key'); throw new Error('Tried to set verified for undefined/null key');
} }
if (!validateVerifiedStatus(verifiedStatus)) { if (!validateVerifiedStatus(verifiedStatus)) {
@ -754,7 +837,7 @@
throw new Error('Invalid public key'); throw new Error('Invalid public key');
} }
const identityRecord = this.identityKeys[number]; const identityRecord = this.getIdentityRecord(identifier);
const isPresent = Boolean(identityRecord); const isPresent = Boolean(identityRecord);
let isEqual = false; let isEqual = false;
@ -775,7 +858,7 @@
verifiedStatus === VerifiedStatus.DEFAULT verifiedStatus === VerifiedStatus.DEFAULT
) { ) {
await textsecure.storage.protocol.setVerified( await textsecure.storage.protocol.setVerified(
number, identifier,
verifiedStatus, verifiedStatus,
publicKey publicKey
); );
@ -788,17 +871,20 @@
(isPresent && !isEqual) || (isPresent && !isEqual) ||
(isPresent && identityRecord.verified !== VerifiedStatus.VERIFIED)) (isPresent && identityRecord.verified !== VerifiedStatus.VERIFIED))
) { ) {
await textsecure.storage.protocol.saveIdentityWithAttributes(number, { await textsecure.storage.protocol.saveIdentityWithAttributes(
publicKey, identifier,
verified: verifiedStatus, {
firstUse: false, publicKey,
timestamp: Date.now(), verified: verifiedStatus,
nonblockingApproval: true, firstUse: false,
}); timestamp: Date.now(),
nonblockingApproval: true,
}
);
if (isPresent && !isEqual) { if (isPresent && !isEqual) {
try { try {
this.trigger('keychange', number); this.trigger('keychange', identifier);
} catch (error) { } catch (error) {
window.log.error( window.log.error(
'processVerifiedMessage error triggering keychange:', 'processVerifiedMessage error triggering keychange:',
@ -806,7 +892,7 @@
); );
} }
await this.archiveAllSessions(number); await this.archiveAllSessions(identifier);
// true signifies that we overwrote a previous key with a new one // true signifies that we overwrote a previous key with a new one
return true; return true;
@ -818,14 +904,14 @@
// state we had before. // state we had before.
return false; return false;
}, },
async isUntrusted(number) { async isUntrusted(identifier) {
if (number === null || number === undefined) { if (identifier === null || identifier === undefined) {
throw new Error('Tried to set verified for undefined/null key'); throw new Error('Tried to set verified for undefined/null key');
} }
const identityRecord = this.identityKeys[number]; const identityRecord = this.getIdentityRecord(identifier);
if (!identityRecord) { if (!identityRecord) {
throw new Error(`No identity record for ${number}`); throw new Error(`No identity record for ${identifier}`);
} }
if ( if (
@ -838,10 +924,13 @@
return false; return false;
}, },
async removeIdentityKey(number) { async removeIdentityKey(identifier) {
delete this.identityKeys[number]; const id = ConversationController.getConversationId(identifier);
await window.Signal.Data.removeIdentityKeyById(number); if (id) {
await textsecure.storage.protocol.removeAllSessions(number); delete this.identityKeys[id];
await window.Signal.Data.removeIdentityKeyById(id);
await textsecure.storage.protocol.removeAllSessions(id);
}
}, },
// Not yet processed messages - for resiliency // Not yet processed messages - for resiliency

View file

@ -14,7 +14,7 @@
Whisper.ViewSyncs = new (Backbone.Collection.extend({ Whisper.ViewSyncs = new (Backbone.Collection.extend({
forMessage(message) { forMessage(message) {
const sync = this.findWhere({ const sync = this.findWhere({
source: message.get('source'), conversationId: message.get('conversationId'),
timestamp: message.get('sent_at'), timestamp: message.get('sent_at'),
}); });
if (sync) { if (sync) {
@ -35,13 +35,15 @@
); );
const found = messages.find( const found = messages.find(
item => item.get('source') === sync.get('source') item => item.get('conversationId') === sync.get('conversationId')
); );
const syncSource = sync.get('source'); const syncSource = sync.get('source');
const syncSourceUuid = sync.get('sourceUuid');
const syncTimestamp = sync.get('timestamp'); const syncTimestamp = sync.get('timestamp');
const wasMessageFound = Boolean(found); const wasMessageFound = Boolean(found);
window.log.info('Receive view sync:', { window.log.info('Receive view sync:', {
syncSource, syncSource,
syncSourceUuid,
syncTimestamp, syncTimestamp,
wasMessageFound, wasMessageFound,
}); });

View file

@ -26,7 +26,7 @@
this.contactView = null; this.contactView = null;
} }
const isMe = this.ourNumber === this.model.id; const isMe = this.model.isMe();
this.contactView = new Whisper.ReactWrapperView({ this.contactView = new Whisper.ReactWrapperView({
className: 'contact-wrapper', className: 'contact-wrapper',
@ -47,7 +47,7 @@
return this; return this;
}, },
showIdentity() { showIdentity() {
if (this.model.id === this.ourNumber || this.loading) { if (this.model.isMe() || this.loading) {
return; return;
} }

View file

@ -2004,7 +2004,7 @@
await contact.setApproved(); await contact.setApproved();
} }
message.resend(contact.id); message.resend(contact.get('e164'), contact.get('uuid'));
}, },
}); });
@ -2483,6 +2483,7 @@
try { try {
await this.model.sendReactionMessage(reaction, { await this.model.sendReactionMessage(reaction, {
targetAuthorE164: messageModel.getSource(), targetAuthorE164: messageModel.getSource(),
targetAuthorUuid: messageModel.getSourceUuid(),
targetTimestamp: messageModel.get('sent_at'), targetTimestamp: messageModel.get('sent_at'),
}); });
} catch (error) { } catch (error) {
@ -2668,10 +2669,17 @@
if (window.reduxStore.getState().expiration.hasExpired) { if (window.reduxStore.getState().expiration.hasExpired) {
ToastView = Whisper.ExpiredToast; ToastView = Whisper.ExpiredToast;
} }
if (this.model.isPrivate() && storage.isBlocked(this.model.id)) { if (
this.model.isPrivate() &&
(storage.isBlocked(this.model.get('e164')) ||
storage.isUuidBlocked(this.model.get('uuid')))
) {
ToastView = Whisper.BlockedToast; ToastView = Whisper.BlockedToast;
} }
if (!this.model.isPrivate() && storage.isGroupBlocked(this.model.id)) { if (
!this.model.isPrivate() &&
storage.isGroupBlocked(this.model.get('groupId'))
) {
ToastView = Whisper.BlockedGroupToast; ToastView = Whisper.BlockedGroupToast;
} }
if (!this.model.isPrivate() && this.model.get('left')) { if (!this.model.isPrivate() && this.model.get('left')) {

View file

@ -16,6 +16,7 @@
}, },
initialize(options) { initialize(options) {
this.ourNumber = textsecure.storage.user.getNumber(); this.ourNumber = textsecure.storage.user.getNumber();
this.ourUuid = textsecure.storage.user.getUuid();
if (options.newKey) { if (options.newKey) {
this.theirKey = options.newKey; this.theirKey = options.newKey;
} }
@ -44,16 +45,29 @@
); );
}, },
loadTheirKey() { loadTheirKey() {
const item = textsecure.storage.protocol.identityKeys[this.model.id]; const item = textsecure.storage.protocol.getIdentityRecord(
this.model.get('id')
);
this.theirKey = item ? item.publicKey : null; this.theirKey = item ? item.publicKey : null;
}, },
loadOurKey() { loadOurKey() {
const item = textsecure.storage.protocol.identityKeys[this.ourNumber]; const item = textsecure.storage.protocol.getIdentityRecord(
this.ourUuid || this.ourNumber
);
this.ourKey = item ? item.publicKey : null; this.ourKey = item ? item.publicKey : null;
}, },
generateSecurityNumber() { generateSecurityNumber() {
return new libsignal.FingerprintGenerator(5200) return new libsignal.FingerprintGenerator(5200)
.createFor(this.ourNumber, this.ourKey, this.model.id, this.theirKey) .createFor(
// TODO: we cannot use UUIDs for safety numbers yet
// this.ourUuid || this.ourNumber,
this.ourNumber,
this.ourKey,
// TODO: we cannot use UUIDs for safety numbers yet
// this.model.get('uuid') || this.model.get('e164'),
this.model.get('e164'),
this.theirKey
)
.then(securityNumber => { .then(securityNumber => {
this.securityNumber = securityNumber; this.securityNumber = securityNumber;
}); });

View file

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

View file

@ -36,6 +36,22 @@ ProtoParser.prototype = {
proto.profileKey = proto.profileKey.toArrayBuffer(); 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; return proto;
} catch (error) { } catch (error) {
window.log.error( window.log.error(

View file

@ -36521,7 +36521,7 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ
})(); })();
(function() { (function() {
var VERSION = 0; var VERSION = shortToArrayBuffer(0);
function iterateHash(data, key, count) { function iterateHash(data, key, count) {
data = dcodeIO.ByteBuffer.concat([data, key]).toArrayBuffer(); data = dcodeIO.ByteBuffer.concat([data, key]).toArrayBuffer();
@ -36551,10 +36551,21 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ
return s; 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) { function getDisplayStringFor(identifier, key, iterations) {
var bytes = dcodeIO.ByteBuffer.concat([ 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);
shortToArrayBuffer(VERSION), key, identifier var encodedIdentifier = isUuid ? decodeUuid(identifier) : identifier;
]).toArrayBuffer(); var bytes = dcodeIO.ByteBuffer.concat([VERSION, key, encodedIdentifier]).toArrayBuffer();
return iterateHash(bytes, key, iterations).then(function(output) { return iterateHash(bytes, key, iterations).then(function(output) {
output = new Uint8Array(output); output = new Uint8Array(output);
return getEncodedChunk(output, 0) + return getEncodedChunk(output, 0) +

View file

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

View file

@ -5,7 +5,7 @@
function OutgoingMessage( function OutgoingMessage(
server, server,
timestamp, timestamp,
numbers, identifiers,
message, message,
silent, silent,
callback, callback,
@ -19,41 +19,43 @@ function OutgoingMessage(
} }
this.server = server; this.server = server;
this.timestamp = timestamp; this.timestamp = timestamp;
this.numbers = numbers; this.identifiers = identifiers;
this.message = message; // ContentMessage proto this.message = message; // ContentMessage proto
this.callback = callback; this.callback = callback;
this.silent = silent; this.silent = silent;
this.numbersCompleted = 0; this.identifiersCompleted = 0;
this.errors = []; this.errors = [];
this.successfulNumbers = []; this.successfulIdentifiers = [];
this.failoverNumbers = []; this.failoverIdentifiers = [];
this.unidentifiedDeliveries = []; this.unidentifiedDeliveries = [];
const { numberInfo, senderCertificate, online } = options || {}; const { sendMetadata, senderCertificate, senderCertificateWithUuid, online } =
this.numberInfo = numberInfo; options || {};
this.sendMetadata = sendMetadata;
this.senderCertificate = senderCertificate; this.senderCertificate = senderCertificate;
this.senderCertificateWithUuid = senderCertificateWithUuid;
this.online = online; this.online = online;
} }
OutgoingMessage.prototype = { OutgoingMessage.prototype = {
constructor: OutgoingMessage, constructor: OutgoingMessage,
numberCompleted() { numberCompleted() {
this.numbersCompleted += 1; this.identifiersCompleted += 1;
if (this.numbersCompleted >= this.numbers.length) { if (this.identifiersCompleted >= this.identifiers.length) {
this.callback({ this.callback({
successfulNumbers: this.successfulNumbers, successfulIdentifiers: this.successfulIdentifiers,
failoverNumbers: this.failoverNumbers, failoverIdentifiers: this.failoverIdentifiers,
errors: this.errors, errors: this.errors,
unidentifiedDeliveries: this.unidentifiedDeliveries, unidentifiedDeliveries: this.unidentifiedDeliveries,
}); });
} }
}, },
registerError(number, reason, error) { registerError(identifier, reason, error) {
if (!error || (error.name === 'HTTPError' && error.code !== 404)) { if (!error || (error.name === 'HTTPError' && error.code !== 404)) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
error = new textsecure.OutgoingMessageError( error = new textsecure.OutgoingMessageError(
number, identifier,
this.message.toArrayBuffer(), this.message.toArrayBuffer(),
this.timestamp, this.timestamp,
error error
@ -61,27 +63,27 @@ OutgoingMessage.prototype = {
} }
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
error.number = number; error.number = identifier;
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
error.reason = reason; error.reason = reason;
this.errors[this.errors.length] = error; this.errors[this.errors.length] = error;
this.numberCompleted(); this.numberCompleted();
}, },
reloadDevicesAndSend(number, recurse) { reloadDevicesAndSend(identifier, recurse) {
return () => return () =>
textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => { textsecure.storage.protocol.getDeviceIds(identifier).then(deviceIds => {
if (deviceIds.length === 0) { if (deviceIds.length === 0) {
return this.registerError( return this.registerError(
number, identifier,
'Got empty device list when loading device keys', 'Got empty device list when loading device keys',
null null
); );
} }
return this.doSendMessage(number, deviceIds, recurse); return this.doSendMessage(identifier, deviceIds, recurse);
}); });
}, },
getKeysForNumber(number, updateDevices) { getKeysForIdentifier(identifier, updateDevices) {
const handleResult = response => const handleResult = response =>
Promise.all( Promise.all(
response.devices.map(device => { response.devices.map(device => {
@ -92,7 +94,7 @@ OutgoingMessage.prototype = {
updateDevices.indexOf(device.deviceId) > -1 updateDevices.indexOf(device.deviceId) > -1
) { ) {
const address = new libsignal.SignalProtocolAddress( const address = new libsignal.SignalProtocolAddress(
number, identifier,
device.deviceId device.deviceId
); );
const builder = new libsignal.SessionBuilder( const builder = new libsignal.SessionBuilder(
@ -119,27 +121,30 @@ OutgoingMessage.prototype = {
}) })
); );
const { numberInfo } = this; const { sendMetadata } = this;
const info = numberInfo && numberInfo[number] ? numberInfo[number] : {}; const info =
sendMetadata && sendMetadata[identifier] ? sendMetadata[identifier] : {};
const { accessKey } = info || {}; const { accessKey } = info || {};
if (updateDevices === undefined) { if (updateDevices === undefined) {
if (accessKey) { if (accessKey) {
return this.server return this.server
.getKeysForNumberUnauth(number, '*', { accessKey }) .getKeysForIdentifierUnauth(identifier, '*', { accessKey })
.catch(error => { .catch(error => {
if (error.code === 401 || error.code === 403) { if (error.code === 401 || error.code === 403) {
if (this.failoverNumbers.indexOf(number) === -1) { if (this.failoverIdentifiers.indexOf(identifier) === -1) {
this.failoverNumbers.push(number); this.failoverIdentifiers.push(identifier);
} }
return this.server.getKeysForNumber(number, '*'); return this.server.getKeysForIdentifier(identifier, '*');
} }
throw error; throw error;
}) })
.then(handleResult); .then(handleResult);
} }
return this.server.getKeysForNumber(number, '*').then(handleResult); return this.server
.getKeysForIdentifier(identifier, '*')
.then(handleResult);
} }
let promise = Promise.resolve(); let promise = Promise.resolve();
@ -149,31 +154,31 @@ OutgoingMessage.prototype = {
if (accessKey) { if (accessKey) {
innerPromise = this.server innerPromise = this.server
.getKeysForNumberUnauth(number, deviceId, { accessKey }) .getKeysForIdentifierUnauth(identifier, deviceId, { accessKey })
.then(handleResult) .then(handleResult)
.catch(error => { .catch(error => {
if (error.code === 401 || error.code === 403) { if (error.code === 401 || error.code === 403) {
if (this.failoverNumbers.indexOf(number) === -1) { if (this.failoverIdentifiers.indexOf(identifier) === -1) {
this.failoverNumbers.push(number); this.failoverIdentifiers.push(identifier);
} }
return this.server return this.server
.getKeysForNumber(number, deviceId) .getKeysForIdentifier(identifier, deviceId)
.then(handleResult); .then(handleResult);
} }
throw error; throw error;
}); });
} else { } else {
innerPromise = this.server innerPromise = this.server
.getKeysForNumber(number, deviceId) .getKeysForIdentifier(identifier, deviceId)
.then(handleResult); .then(handleResult);
} }
return innerPromise.catch(e => { return innerPromise.catch(e => {
if (e.name === 'HTTPError' && e.code === 404) { if (e.name === 'HTTPError' && e.code === 404) {
if (deviceId !== 1) { 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 { } else {
throw e; throw e;
} }
@ -184,12 +189,12 @@ OutgoingMessage.prototype = {
return promise; return promise;
}, },
transmitMessage(number, jsonData, timestamp, { accessKey } = {}) { transmitMessage(identifier, jsonData, timestamp, { accessKey } = {}) {
let promise; let promise;
if (accessKey) { if (accessKey) {
promise = this.server.sendMessagesUnauth( promise = this.server.sendMessagesUnauth(
number, identifier,
jsonData, jsonData,
timestamp, timestamp,
this.silent, this.silent,
@ -198,7 +203,7 @@ OutgoingMessage.prototype = {
); );
} else { } else {
promise = this.server.sendMessages( promise = this.server.sendMessages(
number, identifier,
jsonData, jsonData,
timestamp, timestamp,
this.silent, this.silent,
@ -212,10 +217,10 @@ OutgoingMessage.prototype = {
// 404 should throw UnregisteredUserError // 404 should throw UnregisteredUserError
// all other network errors can be retried later. // all other network errors can be retried later.
if (e.code === 404) { if (e.code === 404) {
throw new textsecure.UnregisteredUserError(number, e); throw new textsecure.UnregisteredUserError(identifier, e);
} }
throw new textsecure.SendMessageNetworkError( throw new textsecure.SendMessageNetworkError(
number, identifier,
jsonData, jsonData,
e, e,
timestamp timestamp
@ -248,13 +253,17 @@ OutgoingMessage.prototype = {
return this.plaintext; return this.plaintext;
}, },
doSendMessage(number, deviceIds, recurse) { doSendMessage(identifier, deviceIds, recurse) {
const ciphers = {}; const ciphers = {};
const plaintext = this.getPlaintext(); const plaintext = this.getPlaintext();
const { numberInfo, senderCertificate } = this; const { sendMetadata } = this;
const info = numberInfo && numberInfo[number] ? numberInfo[number] : {}; const info =
const { accessKey } = info || {}; sendMetadata && sendMetadata[identifier] ? sendMetadata[identifier] : {};
const { accessKey, useUuidSenderCert } = info || {};
const senderCertificate = useUuidSenderCert
? this.senderCertificateWithUuid
: this.senderCertificate;
if (accessKey && !senderCertificate) { if (accessKey && !senderCertificate) {
window.log.warn( window.log.warn(
@ -266,8 +275,9 @@ OutgoingMessage.prototype = {
// We don't send to ourselves if unless sealedSender is enabled // We don't send to ourselves if unless sealedSender is enabled
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const ourDeviceId = textsecure.storage.user.getDeviceId(); const ourDeviceId = textsecure.storage.user.getDeviceId();
if (number === ourNumber && !sealedSender) { if ((identifier === ourNumber || identifier === ourUuid) && !sealedSender) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
deviceIds = _.reject( deviceIds = _.reject(
deviceIds, deviceIds,
@ -279,12 +289,15 @@ OutgoingMessage.prototype = {
return Promise.all( return Promise.all(
deviceIds.map(async deviceId => { deviceIds.map(async deviceId => {
const address = new libsignal.SignalProtocolAddress(number, deviceId); const address = new libsignal.SignalProtocolAddress(
identifier,
deviceId
);
const options = {}; const options = {};
// No limit on message keys if we're communicating with our other devices // No limit on message keys if we're communicating with our other devices
if (ourNumber === number) { if (ourNumber === identifier || ourUuid === identifier) {
options.messageKeysLimit = false; options.messageKeysLimit = false;
} }
@ -299,6 +312,7 @@ OutgoingMessage.prototype = {
senderCertificate, senderCertificate,
plaintext plaintext
); );
return { return {
type: textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER, type: textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
destinationDeviceId: address.getDeviceId(), destinationDeviceId: address.getDeviceId(),
@ -327,18 +341,18 @@ OutgoingMessage.prototype = {
) )
.then(jsonData => { .then(jsonData => {
if (sealedSender) { if (sealedSender) {
return this.transmitMessage(number, jsonData, this.timestamp, { return this.transmitMessage(identifier, jsonData, this.timestamp, {
accessKey, accessKey,
}).then( }).then(
() => { () => {
this.unidentifiedDeliveries.push(number); this.unidentifiedDeliveries.push(identifier);
this.successfulNumbers.push(number); this.successfulIdentifiers.push(identifier);
this.numberCompleted(); this.numberCompleted();
}, },
error => { error => {
if (error.code === 401 || error.code === 403) { if (error.code === 401 || error.code === 403) {
if (this.failoverNumbers.indexOf(number) === -1) { if (this.failoverIdentifiers.indexOf(identifier) === -1) {
this.failoverNumbers.push(number); this.failoverIdentifiers.push(identifier);
} }
if (info) { if (info) {
info.accessKey = null; info.accessKey = null;
@ -346,7 +360,7 @@ OutgoingMessage.prototype = {
// Set final parameter to true to ensure we don't hit this codepath a // Set final parameter to true to ensure we don't hit this codepath a
// second time. // second time.
return this.doSendMessage(number, deviceIds, recurse, true); return this.doSendMessage(identifier, deviceIds, recurse, true);
} }
throw error; 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(); this.numberCompleted();
} }
); );
@ -369,22 +383,22 @@ OutgoingMessage.prototype = {
) { ) {
if (!recurse) if (!recurse)
return this.registerError( return this.registerError(
number, identifier,
'Hit retry limit attempting to reload device list', 'Hit retry limit attempting to reload device list',
error error
); );
let p; let p;
if (error.code === 409) { if (error.code === 409) {
p = this.removeDeviceIdsForNumber( p = this.removeDeviceIdsForIdentifier(
number, identifier,
error.response.extraDevices error.response.extraDevices
); );
} else { } else {
p = Promise.all( p = Promise.all(
error.response.staleDevices.map(deviceId => error.response.staleDevices.map(deviceId =>
ciphers[deviceId].closeOpenSessionForDevice( ciphers[deviceId].closeOpenSessionForDevice(
new libsignal.SignalProtocolAddress(number, deviceId) new libsignal.SignalProtocolAddress(identifier, deviceId)
) )
) )
); );
@ -395,10 +409,10 @@ OutgoingMessage.prototype = {
error.code === 410 error.code === 410
? error.response.staleDevices ? error.response.staleDevices
: error.response.missingDevices; : 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 // 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. // 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') { } else if (error.message === 'Identity key changed') {
@ -408,13 +422,12 @@ OutgoingMessage.prototype = {
error.originalMessage = this.message.toArrayBuffer(); error.originalMessage = this.message.toArrayBuffer();
window.log.error( window.log.error(
'Got "key changed" error from encrypt - no identityKey for application layer', 'Got "key changed" error from encrypt - no identityKey for application layer',
number, identifier,
deviceIds deviceIds
); );
const address = new libsignal.SignalProtocolAddress(number, 1); window.log.info('closing all sessions for', identifier);
const identifier = address.toString(); const address = new libsignal.SignalProtocolAddress(identifier, 1);
window.log.info('closing all sessions for', number);
const sessionCipher = new libsignal.SessionCipher( const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol, textsecure.storage.protocol,
@ -425,7 +438,9 @@ OutgoingMessage.prototype = {
// Primary device // Primary device
sessionCipher.closeOpenSessionForDevice(), sessionCipher.closeOpenSessionForDevice(),
// The rest of their devices // The rest of their devices
textsecure.storage.protocol.archiveSiblingSessions(identifier), textsecure.storage.protocol.archiveSiblingSessions(
address.toString()
),
]).then( ]).then(
() => { () => {
throw error; 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; return null;
}); });
}, },
getStaleDeviceIdsForNumber(number) { getStaleDeviceIdsForIdentifier(identifier) {
return textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => { return textsecure.storage.protocol
if (deviceIds.length === 0) { .getDeviceIds(identifier)
return [1]; .then(deviceIds => {
} if (deviceIds.length === 0) {
const updateDevices = []; return [1];
return Promise.all( }
deviceIds.map(deviceId => { const updateDevices = [];
const address = new libsignal.SignalProtocolAddress(number, deviceId); return Promise.all(
const sessionCipher = new libsignal.SessionCipher( deviceIds.map(deviceId => {
textsecure.storage.protocol, const address = new libsignal.SignalProtocolAddress(
address identifier,
); deviceId
return sessionCipher.hasOpenSession().then(hasSession => { );
if (!hasSession) { const sessionCipher = new libsignal.SessionCipher(
updateDevices.push(deviceId); textsecure.storage.protocol,
} address
}); );
}) return sessionCipher.hasOpenSession().then(hasSession => {
).then(() => updateDevices); if (!hasSession) {
}); updateDevices.push(deviceId);
}
});
})
).then(() => updateDevices);
});
}, },
removeDeviceIdsForNumber(number, deviceIdsToRemove) { removeDeviceIdsForIdentifier(identifier, deviceIdsToRemove) {
let promise = Promise.resolve(); let promise = Promise.resolve();
// eslint-disable-next-line no-restricted-syntax, guard-for-in // eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const j in deviceIdsToRemove) { for (const j in deviceIdsToRemove) {
promise = promise.then(() => { promise = promise.then(() => {
const encodedNumber = `${number}.${deviceIdsToRemove[j]}`; const encodedAddress = `${identifier}.${deviceIdsToRemove[j]}`;
return textsecure.storage.protocol.removeSession(encodedNumber); return textsecure.storage.protocol.removeSession(encodedAddress);
}); });
} }
return promise; return promise;
}, },
async sendToNumber(number) { async sendToIdentifier(identifier) {
try { try {
const updateDevices = await this.getStaleDeviceIdsForNumber(number); const updateDevices = await this.getStaleDeviceIdsForIdentifier(
await this.getKeysForNumber(number, updateDevices); identifier
await this.reloadDevicesAndSend(number, true)(); );
await this.getKeysForIdentifier(identifier, updateDevices);
await this.reloadDevicesAndSend(identifier, true)();
} catch (error) { } catch (error) {
if (error.message === 'Identity key changed') { if (error.message === 'Identity key changed') {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
const newError = new textsecure.OutgoingIdentityKeyError( const newError = new textsecure.OutgoingIdentityKeyError(
number, identifier,
error.originalMessage, error.originalMessage,
error.timestamp, error.timestamp,
error.identityKey error.identityKey
); );
this.registerError(number, 'Identity key changed', newError); this.registerError(identifier, 'Identity key changed', newError);
} else { } else {
this.registerError( this.registerError(
number, identifier,
`Failed to retrieve new device keys for number ${number}`, `Failed to retrieve new device keys for number ${identifier}`,
error 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 */ /* eslint-disable more/no-then, no-bitwise */
@ -246,18 +247,22 @@ MessageSender.prototype = {
return proto; return proto;
}, },
queueJobForNumber(number, runJob) { async queueJobForIdentifier(identifier, runJob) {
this.pendingMessages[number] = const { id } = await ConversationController.getOrCreateAndWait(
this.pendingMessages[number] || new window.PQueue({ concurrency: 1 }); 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( const taskWithTimeout = textsecure.createTaskWithTimeout(
runJob, runJob,
`queueJobForNumber ${number}` `queueJobForIdentifier ${identifier} ${id}`
); );
queue.add(taskWithTimeout); return queue.add(taskWithTimeout);
}, },
uploadAttachments(message) { uploadAttachments(message) {
@ -361,7 +366,7 @@ MessageSender.prototype = {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
this.sendMessageProto( this.sendMessageProto(
message.timestamp, message.timestamp,
message.recipients, message.recipients || [],
message.toProto(), message.toProto(),
res => { res => {
res.dataMessage = message.toArrayBuffer(); res.dataMessage = message.toArrayBuffer();
@ -379,8 +384,8 @@ MessageSender.prototype = {
}, },
sendMessageProto( sendMessageProto(
timestamp, timestamp,
numbers, recipients,
message, messageProto,
callback, callback,
silent, silent,
options = {} options = {}
@ -388,8 +393,8 @@ MessageSender.prototype = {
const rejections = textsecure.storage.get('signedKeyRotationRejected', 0); const rejections = textsecure.storage.get('signedKeyRotationRejected', 0);
if (rejections > 5) { if (rejections > 5) {
throw new textsecure.SignedPreKeyRotationError( throw new textsecure.SignedPreKeyRotationError(
numbers, recipients,
message.toArrayBuffer(), messageProto.toArrayBuffer(),
timestamp timestamp
); );
} }
@ -397,19 +402,27 @@ MessageSender.prototype = {
const outgoing = new OutgoingMessage( const outgoing = new OutgoingMessage(
this.server, this.server,
timestamp, timestamp,
numbers, recipients,
message, messageProto,
silent, silent,
callback, callback,
options options
); );
numbers.forEach(number => { recipients.forEach(identifier => {
this.queueJobForNumber(number, () => outgoing.sendToNumber(number)); this.queueJobForIdentifier(identifier, () =>
outgoing.sendToIdentifier(identifier)
);
}); });
}, },
sendMessageProtoAndWait(timestamp, numbers, message, silent, options = {}) { sendMessageProtoAndWait(
timestamp,
identifiers,
messageProto,
silent,
options = {}
) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const callback = result => { const callback = result => {
if (result && result.errors && result.errors.length > 0) { if (result && result.errors && result.errors.length > 0) {
@ -421,8 +434,8 @@ MessageSender.prototype = {
this.sendMessageProto( this.sendMessageProto(
timestamp, timestamp,
numbers, identifiers,
message, messageProto,
callback, callback,
silent, silent,
options options
@ -430,7 +443,7 @@ MessageSender.prototype = {
}); });
}, },
sendIndividualProto(number, proto, timestamp, silent, options = {}) { sendIndividualProto(identifier, proto, timestamp, silent, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const callback = res => { const callback = res => {
if (res && res.errors && res.errors.length > 0) { if (res && res.errors && res.errors.length > 0) {
@ -441,7 +454,7 @@ MessageSender.prototype = {
}; };
this.sendMessageProto( this.sendMessageProto(
timestamp, timestamp,
[number], [identifier],
proto, proto,
callback, callback,
silent, silent,
@ -467,6 +480,7 @@ MessageSender.prototype = {
encodedDataMessage, encodedDataMessage,
timestamp, timestamp,
destination, destination,
destinationUuid,
expirationStartTimestamp, expirationStartTimestamp,
sentTo = [], sentTo = [],
unidentifiedDeliveries = [], unidentifiedDeliveries = [],
@ -474,7 +488,9 @@ MessageSender.prototype = {
options options
) { ) {
const myNumber = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId(); const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') { if (myDevice === 1 || myDevice === '1') {
return Promise.resolve(); return Promise.resolve();
} }
@ -488,6 +504,9 @@ MessageSender.prototype = {
if (destination) { if (destination) {
sentMessage.destination = destination; sentMessage.destination = destination;
} }
if (destinationUuid) {
sentMessage.destinationUuid = destinationUuid;
}
if (expirationStartTimestamp) { if (expirationStartTimestamp) {
sentMessage.expirationStartTimestamp = expirationStartTimestamp; sentMessage.expirationStartTimestamp = expirationStartTimestamp;
} }
@ -508,10 +527,16 @@ MessageSender.prototype = {
// Though this field has 'unidenified' in the name, it should have entries for each // Though this field has 'unidenified' in the name, it should have entries for each
// number we sent to. // number we sent to.
if (sentTo && sentTo.length) { if (sentTo && sentTo.length) {
sentMessage.unidentifiedStatus = sentTo.map(number => { sentMessage.unidentifiedStatus = sentTo.map(identifier => {
const status = new textsecure.protobuf.SyncMessage.Sent.UnidentifiedDeliveryStatus(); const status = new textsecure.protobuf.SyncMessage.Sent.UnidentifiedDeliveryStatus();
status.destination = number; const conv = ConversationController.get(identifier);
status.unidentified = Boolean(unidentifiedLookup[number]); 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; return status;
}); });
} }
@ -523,7 +548,7 @@ MessageSender.prototype = {
const silent = true; const silent = true;
return this.sendIndividualProto( return this.sendIndividualProto(
myNumber, myUuid || myNumber,
contentMessage, contentMessage,
timestamp, timestamp,
silent, silent,
@ -552,6 +577,7 @@ MessageSender.prototype = {
sendRequestBlockSyncMessage(options) { sendRequestBlockSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId(); const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') { if (myDevice !== 1 && myDevice !== '1') {
const request = new textsecure.protobuf.SyncMessage.Request(); const request = new textsecure.protobuf.SyncMessage.Request();
@ -563,7 +589,7 @@ MessageSender.prototype = {
const silent = true; const silent = true;
return this.sendIndividualProto( return this.sendIndividualProto(
myNumber, myUuid || myNumber,
contentMessage, contentMessage,
Date.now(), Date.now(),
silent, silent,
@ -576,6 +602,7 @@ MessageSender.prototype = {
sendRequestConfigurationSyncMessage(options) { sendRequestConfigurationSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId(); const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') { if (myDevice !== 1 && myDevice !== '1') {
const request = new textsecure.protobuf.SyncMessage.Request(); const request = new textsecure.protobuf.SyncMessage.Request();
@ -587,7 +614,7 @@ MessageSender.prototype = {
const silent = true; const silent = true;
return this.sendIndividualProto( return this.sendIndividualProto(
myNumber, myUuid || myNumber,
contentMessage, contentMessage,
Date.now(), Date.now(),
silent, silent,
@ -600,6 +627,7 @@ MessageSender.prototype = {
sendRequestGroupSyncMessage(options) { sendRequestGroupSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId(); const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') { if (myDevice !== 1 && myDevice !== '1') {
const request = new textsecure.protobuf.SyncMessage.Request(); const request = new textsecure.protobuf.SyncMessage.Request();
@ -611,7 +639,7 @@ MessageSender.prototype = {
const silent = true; const silent = true;
return this.sendIndividualProto( return this.sendIndividualProto(
myNumber, myUuid || myNumber,
contentMessage, contentMessage,
Date.now(), Date.now(),
silent, silent,
@ -624,6 +652,8 @@ MessageSender.prototype = {
sendRequestContactSyncMessage(options) { sendRequestContactSyncMessage(options) {
const myNumber = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId(); const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') { if (myDevice !== 1 && myDevice !== '1') {
const request = new textsecure.protobuf.SyncMessage.Request(); const request = new textsecure.protobuf.SyncMessage.Request();
@ -635,7 +665,7 @@ MessageSender.prototype = {
const silent = true; const silent = true;
return this.sendIndividualProto( return this.sendIndividualProto(
myNumber, myUuid || myNumber,
contentMessage, contentMessage,
Date.now(), Date.now(),
silent, silent,
@ -653,7 +683,8 @@ MessageSender.prototype = {
// We don't want to send typing messages to our other devices, but we will // We don't want to send typing messages to our other devices, but we will
// in the group case. // in the group case.
const myNumber = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
if (recipientId && myNumber === recipientId) { const myUuid = textsecure.storage.user.getUuid();
if (recipientId && (myNumber === recipientId || myUuid === recipientId)) {
return null; return null;
} }
@ -662,7 +693,7 @@ MessageSender.prototype = {
} }
const recipients = groupId const recipients = groupId
? _.without(groupNumbers, myNumber) ? _.without(groupNumbers, myNumber, myUuid)
: [recipientId]; : [recipientId];
const groupIdBuffer = groupId const groupIdBuffer = groupId
? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(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 myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId(); 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(); return Promise.resolve();
} }
@ -710,7 +745,7 @@ MessageSender.prototype = {
const silent = true; const silent = true;
return this.sendIndividualProto( return this.sendIndividualProto(
recipientId, recipientUuid || recipientE164,
contentMessage, contentMessage,
Date.now(), Date.now(),
silent, silent,
@ -718,7 +753,7 @@ MessageSender.prototype = {
); );
}, },
sendReadReceipts(sender, timestamps, options) { sendReadReceipts(senderE164, senderUuid, timestamps, options) {
const receiptMessage = new textsecure.protobuf.ReceiptMessage(); const receiptMessage = new textsecure.protobuf.ReceiptMessage();
receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ; receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ;
receiptMessage.timestamp = timestamps; receiptMessage.timestamp = timestamps;
@ -728,7 +763,7 @@ MessageSender.prototype = {
const silent = true; const silent = true;
return this.sendIndividualProto( return this.sendIndividualProto(
sender, senderUuid || senderE164,
contentMessage, contentMessage,
Date.now(), Date.now(),
silent, silent,
@ -737,6 +772,7 @@ MessageSender.prototype = {
}, },
syncReadMessages(reads, options) { syncReadMessages(reads, options) {
const myNumber = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId(); const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice !== 1 && myDevice !== '1') { if (myDevice !== 1 && myDevice !== '1') {
const syncMessage = this.createSyncMessage(); const syncMessage = this.createSyncMessage();
@ -745,6 +781,7 @@ MessageSender.prototype = {
const read = new textsecure.protobuf.SyncMessage.Read(); const read = new textsecure.protobuf.SyncMessage.Read();
read.timestamp = reads[i].timestamp; read.timestamp = reads[i].timestamp;
read.sender = reads[i].sender; read.sender = reads[i].sender;
syncMessage.read.push(read); syncMessage.read.push(read);
} }
const contentMessage = new textsecure.protobuf.Content(); const contentMessage = new textsecure.protobuf.Content();
@ -752,7 +789,7 @@ MessageSender.prototype = {
const silent = true; const silent = true;
return this.sendIndividualProto( return this.sendIndividualProto(
myNumber, myUuid || myNumber,
contentMessage, contentMessage,
Date.now(), Date.now(),
silent, silent,
@ -763,8 +800,9 @@ MessageSender.prototype = {
return Promise.resolve(); return Promise.resolve();
}, },
async syncViewOnceOpen(sender, timestamp, options) { async syncViewOnceOpen(sender, senderUuid, timestamp, options) {
const myNumber = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId(); const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') { if (myDevice === 1 || myDevice === '1') {
return null; return null;
@ -774,6 +812,7 @@ MessageSender.prototype = {
const viewOnceOpen = new textsecure.protobuf.SyncMessage.ViewOnceOpen(); const viewOnceOpen = new textsecure.protobuf.SyncMessage.ViewOnceOpen();
viewOnceOpen.sender = sender; viewOnceOpen.sender = sender;
viewOnceOpen.senderUuid = senderUuid;
viewOnceOpen.timestamp = timestamp; viewOnceOpen.timestamp = timestamp;
syncMessage.viewOnceOpen = viewOnceOpen; syncMessage.viewOnceOpen = viewOnceOpen;
@ -782,7 +821,7 @@ MessageSender.prototype = {
const silent = true; const silent = true;
return this.sendIndividualProto( return this.sendIndividualProto(
myNumber, myUuid || myNumber,
contentMessage, contentMessage,
Date.now(), Date.now(),
silent, silent,
@ -797,6 +836,7 @@ MessageSender.prototype = {
} }
const myNumber = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const ENUM = textsecure.protobuf.SyncMessage.StickerPackOperation.Type; const ENUM = textsecure.protobuf.SyncMessage.StickerPackOperation.Type;
const packOperations = operations.map(item => { const packOperations = operations.map(item => {
@ -818,15 +858,23 @@ MessageSender.prototype = {
const silent = true; const silent = true;
return this.sendIndividualProto( return this.sendIndividualProto(
myNumber, myUuid || myNumber,
contentMessage, contentMessage,
Date.now(), Date.now(),
silent, silent,
options options
); );
}, },
syncVerification(destination, state, identityKey, options) {
syncVerification(
destinationE164,
destinationUuid,
state,
identityKey,
options
) {
const myNumber = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
const myUuid = textsecure.storage.user.getUuid();
const myDevice = textsecure.storage.user.getDeviceId(); const myDevice = textsecure.storage.user.getDeviceId();
const now = Date.now(); const now = Date.now();
@ -850,7 +898,7 @@ MessageSender.prototype = {
// We want the NullMessage to look like a normal outgoing message; not silent // We want the NullMessage to look like a normal outgoing message; not silent
const silent = false; const silent = false;
const promise = this.sendIndividualProto( const promise = this.sendIndividualProto(
destination, destinationUuid || destinationE164,
contentMessage, contentMessage,
now, now,
silent, silent,
@ -860,7 +908,12 @@ MessageSender.prototype = {
return promise.then(() => { return promise.then(() => {
const verified = new textsecure.protobuf.Verified(); const verified = new textsecure.protobuf.Verified();
verified.state = state; verified.state = state;
verified.destination = destination; if (destinationE164) {
verified.destination = destinationE164;
}
if (destinationUuid) {
verified.destinationUuid = destinationUuid;
}
verified.identityKey = identityKey; verified.identityKey = identityKey;
verified.nullMessage = nullMessage.padding; verified.nullMessage = nullMessage.padding;
@ -872,7 +925,7 @@ MessageSender.prototype = {
const innerSilent = true; const innerSilent = true;
return this.sendIndividualProto( return this.sendIndividualProto(
myNumber, myUuid || myNumber,
secondMessage, secondMessage,
now, now,
innerSilent, innerSilent,
@ -881,13 +934,22 @@ MessageSender.prototype = {
}); });
}, },
sendGroupProto(providedNumbers, proto, timestamp = Date.now(), options = {}) { sendGroupProto(
const me = textsecure.storage.user.getNumber(); providedIdentifiers,
const numbers = providedNumbers.filter(number => number !== me); proto,
if (numbers.length === 0) { 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({ return Promise.resolve({
successfulNumbers: [], successfulIdentifiers: [],
failoverNumbers: [], failoverIdentifiers: [],
errors: [], errors: [],
unidentifiedDeliveries: [], unidentifiedDeliveries: [],
dataMessage: proto.toArrayBuffer(), dataMessage: proto.toArrayBuffer(),
@ -907,7 +969,7 @@ MessageSender.prototype = {
this.sendMessageProto( this.sendMessageProto(
timestamp, timestamp,
numbers, providedIdentifiers,
proto, proto,
callback, callback,
silent, silent,
@ -917,7 +979,7 @@ MessageSender.prototype = {
}, },
async getMessageProto( async getMessageProto(
number, destination,
body, body,
attachments, attachments,
quote, quote,
@ -930,7 +992,8 @@ MessageSender.prototype = {
flags flags
) { ) {
const attributes = { const attributes = {
recipients: [number], recipients: [destination],
destination,
body, body,
timestamp, timestamp,
attachments, attachments,
@ -958,8 +1021,8 @@ MessageSender.prototype = {
return message.toArrayBuffer(); return message.toArrayBuffer();
}, },
sendMessageToNumber( sendMessageToIdentifier(
number, identifier,
messageText, messageText,
attachments, attachments,
quote, quote,
@ -973,7 +1036,7 @@ MessageSender.prototype = {
) { ) {
return this.sendMessage( return this.sendMessage(
{ {
recipients: [number], recipients: [identifier],
body: messageText, body: messageText,
timestamp, timestamp,
attachments, attachments,
@ -988,7 +1051,7 @@ MessageSender.prototype = {
); );
}, },
resetSession(number, timestamp, options) { resetSession(identifier, timestamp, options) {
window.log.info('resetting secure session'); window.log.info('resetting secure session');
const silent = false; const silent = false;
const proto = new textsecure.protobuf.DataMessage(); 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:')) .catch(logError('resetSession/deleteAllSessions1 error:'))
.then(() => { .then(() => {
window.log.info( window.log.info(
'finished closing local sessions, now sending to contact' 'finished closing local sessions, now sending to contact'
); );
return this.sendIndividualProto( return this.sendIndividualProto(
number, identifier,
proto, proto,
timestamp, timestamp,
silent, silent,
@ -1032,14 +1095,15 @@ MessageSender.prototype = {
).catch(logError('resetSession/sendToContact error:')); ).catch(logError('resetSession/sendToContact error:'));
}) })
.then(() => .then(() =>
deleteAllSessions(number).catch( deleteAllSessions(identifier).catch(
logError('resetSession/deleteAllSessions2 error:') logError('resetSession/deleteAllSessions2 error:')
) )
); );
const myNumber = textsecure.storage.user.getNumber(); 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! // We already sent the reset session to our other devices in the code above!
if (number === myNumber) { if (identifier === myNumber || identifier === myUuid) {
return sendToContactPromise; return sendToContactPromise;
} }
@ -1047,7 +1111,7 @@ MessageSender.prototype = {
const sendSyncPromise = this.sendSyncMessage( const sendSyncPromise = this.sendSyncMessage(
buffer, buffer,
timestamp, timestamp,
number, identifier,
null, null,
[], [],
[], [],
@ -1059,7 +1123,7 @@ MessageSender.prototype = {
async sendMessageToGroup( async sendMessageToGroup(
groupId, groupId,
groupNumbers, recipients,
messageText, messageText,
attachments, attachments,
quote, quote,
@ -1071,10 +1135,10 @@ MessageSender.prototype = {
profileKey, profileKey,
options options
) { ) {
const me = textsecure.storage.user.getNumber(); const myE164 = textsecure.storage.user.getNumber();
const numbers = groupNumbers.filter(number => number !== me); const myUuid = textsecure.storage.user.getNumber();
const attrs = { const attrs = {
recipients: numbers, recipients: recipients.filter(r => r !== myE164 && r !== myUuid),
body: messageText, body: messageText,
timestamp, timestamp,
attachments, attachments,
@ -1090,10 +1154,10 @@ MessageSender.prototype = {
}, },
}; };
if (numbers.length === 0) { if (recipients.length === 0) {
return Promise.resolve({ return Promise.resolve({
successfulNumbers: [], successfulIdentifiers: [],
failoverNumbers: [], failoverIdentifiers: [],
errors: [], errors: [],
unidentifiedDeliveries: [], unidentifiedDeliveries: [],
dataMessage: await this.getMessageProtoObj(attrs), dataMessage: await this.getMessageProtoObj(attrs),
@ -1103,19 +1167,20 @@ MessageSender.prototype = {
return this.sendMessage(attrs, options); return this.sendMessage(attrs, options);
}, },
createGroup(targetNumbers, id, name, avatar, options) { createGroup(targetIdentifiers, id, name, avatar, options) {
const proto = new textsecure.protobuf.DataMessage(); const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(id); proto.group.id = stringToArrayBuffer(id);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = targetNumbers; // TODO
proto.group.members = targetIdentifiers;
proto.group.name = name; proto.group.name = name;
return this.makeAttachmentPointer(avatar).then(attachment => { return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = attachment; proto.group.avatar = attachment;
return this.sendGroupProto( return this.sendGroupProto(
targetNumbers, targetIdentifiers,
proto, proto,
Date.now(), Date.now(),
options 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(); const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId); proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name; proto.group.name = name;
proto.group.members = targetNumbers; proto.group.members = targetIdentifiers;
return this.makeAttachmentPointer(avatar).then(attachment => { return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = attachment; proto.group.avatar = attachment;
return this.sendGroupProto( return this.sendGroupProto(
targetNumbers, targetIdentifiers,
proto, proto,
Date.now(), Date.now(),
options options
@ -1143,58 +1208,61 @@ MessageSender.prototype = {
}); });
}, },
addNumberToGroup(groupId, newNumbers, options) { addIdentifierToGroup(groupId, newIdentifiers, options) {
const proto = new textsecure.protobuf.DataMessage(); const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId); proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = newNumbers; proto.group.members = newIdentifiers;
return this.sendGroupProto(newNumbers, proto, Date.now(), options); return this.sendGroupProto(newIdentifiers, proto, Date.now(), options);
}, },
setGroupName(groupId, name, groupNumbers, options) { setGroupName(groupId, name, groupIdentifiers, options) {
const proto = new textsecure.protobuf.DataMessage(); const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId); proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.name = name; 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(); const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId); proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE;
proto.group.members = groupNumbers; proto.group.members = groupIdentifiers;
return this.makeAttachmentPointer(avatar).then(attachment => { return this.makeAttachmentPointer(avatar).then(attachment => {
proto.group.avatar = 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(); const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId); proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT; 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( async sendExpirationTimerUpdateToGroup(
groupId, groupId,
groupNumbers, groupIdentifiers,
expireTimer, expireTimer,
timestamp, timestamp,
profileKey, profileKey,
options options
) { ) {
const me = textsecure.storage.user.getNumber(); const myNumber = textsecure.storage.user.getNumber();
const numbers = groupNumbers.filter(number => number !== me); const myUuid = textsecure.storage.user.getUuid();
const recipients = groupIdentifiers.filter(
identifier => identifier !== myNumber && identifier !== myUuid
);
const attrs = { const attrs = {
recipients: numbers, recipients,
timestamp, timestamp,
expireTimer, expireTimer,
profileKey, profileKey,
@ -1205,10 +1273,10 @@ MessageSender.prototype = {
}, },
}; };
if (numbers.length === 0) { if (recipients.length === 0) {
return Promise.resolve({ return Promise.resolve({
successfulNumbers: [], successfulIdentifiers: [],
failoverNumbers: [], failoverIdentifiers: [],
errors: [], errors: [],
unidentifiedDeliveries: [], unidentifiedDeliveries: [],
dataMessage: await this.getMessageProtoObj(attrs), dataMessage: await this.getMessageProtoObj(attrs),
@ -1217,8 +1285,8 @@ MessageSender.prototype = {
return this.sendMessage(attrs, options); return this.sendMessage(attrs, options);
}, },
sendExpirationTimerUpdateToNumber( sendExpirationTimerUpdateToIdentifier(
number, identifier,
expireTimer, expireTimer,
timestamp, timestamp,
profileKey, profileKey,
@ -1226,7 +1294,7 @@ MessageSender.prototype = {
) { ) {
return this.sendMessage( return this.sendMessage(
{ {
recipients: [number], recipients: [identifier],
timestamp, timestamp,
expireTimer, expireTimer,
profileKey, profileKey,
@ -1245,7 +1313,7 @@ window.textsecure = window.textsecure || {};
textsecure.MessageSender = function MessageSenderWrapper(username, password) { textsecure.MessageSender = function MessageSenderWrapper(username, password) {
const sender = new MessageSender(username, password); const sender = new MessageSender(username, password);
this.sendExpirationTimerUpdateToNumber = sender.sendExpirationTimerUpdateToNumber.bind( this.sendExpirationTimerUpdateToIdentifier = sender.sendExpirationTimerUpdateToIdentifier.bind(
sender sender
); );
this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup.bind( this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup.bind(
@ -1264,14 +1332,14 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
sender sender
); );
this.sendMessageToNumber = sender.sendMessageToNumber.bind(sender); this.sendMessageToIdentifier = sender.sendMessageToIdentifier.bind(sender);
this.sendMessage = sender.sendMessage.bind(sender); this.sendMessage = sender.sendMessage.bind(sender);
this.resetSession = sender.resetSession.bind(sender); this.resetSession = sender.resetSession.bind(sender);
this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender); this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender);
this.sendTypingMessage = sender.sendTypingMessage.bind(sender); this.sendTypingMessage = sender.sendTypingMessage.bind(sender);
this.createGroup = sender.createGroup.bind(sender); this.createGroup = sender.createGroup.bind(sender);
this.updateGroup = sender.updateGroup.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.setGroupName = sender.setGroupName.bind(sender);
this.setGroupAvatar = sender.setGroupAvatar.bind(sender); this.setGroupAvatar = sender.setGroupAvatar.bind(sender);
this.leaveGroup = sender.leaveGroup.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() { getNumber() {
const numberId = textsecure.storage.get('number_id'); const numberId = textsecure.storage.get('number_id');
if (numberId === undefined) return undefined; if (numberId === undefined) return undefined;
return textsecure.utils.unencodeNumber(numberId)[0]; 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() { 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'); const numberId = textsecure.storage.get('number_id');
if (numberId === undefined) return undefined; if (numberId === undefined) return undefined;
return textsecure.utils.unencodeNumber(numberId)[1]; return textsecure.utils.unencodeNumber(numberId)[1];

View file

@ -13,6 +13,7 @@ describe('ContactBuffer', () => {
const contactInfo = new textsecure.protobuf.ContactDetails({ const contactInfo = new textsecure.protobuf.ContactDetails({
name: 'Zero Cool', name: 'Zero Cool',
number: '+10000000000', number: '+10000000000',
uuid: '7198E1BD-1293-452A-A098-F982FF201902',
avatar: { contentType: 'image/jpeg', length: avatarLen }, avatar: { contentType: 'image/jpeg', length: avatarLen },
}); });
const contactInfoBuffer = contactInfo.encode().toArrayBuffer(); const contactInfoBuffer = contactInfo.encode().toArrayBuffer();
@ -37,6 +38,7 @@ describe('ContactBuffer', () => {
count += 1; count += 1;
assert.strictEqual(contact.name, 'Zero Cool'); assert.strictEqual(contact.name, 'Zero Cool');
assert.strictEqual(contact.number, '+10000000000'); 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.contentType, 'image/jpeg');
assert.strictEqual(contact.avatar.length, 255); assert.strictEqual(contact.avatar.length, 255);
assert.strictEqual(contact.avatar.data.byteLength, 255); assert.strictEqual(contact.avatar.data.byteLength, 255);
@ -63,7 +65,13 @@ describe('GroupBuffer', () => {
const groupInfo = new textsecure.protobuf.GroupDetails({ const groupInfo = new textsecure.protobuf.GroupDetails({
id: new Uint8Array([1, 3, 3, 7]).buffer, id: new Uint8Array([1, 3, 3, 7]).buffer,
name: 'Hackers', 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 }, avatar: { contentType: 'image/jpeg', length: avatarLen },
}); });
const groupInfoBuffer = groupInfo.encode().toArrayBuffer(); const groupInfoBuffer = groupInfo.encode().toArrayBuffer();
@ -91,7 +99,21 @@ describe('GroupBuffer', () => {
group.id.toArrayBuffer(), group.id.toArrayBuffer(),
new Uint8Array([1, 3, 3, 7]).buffer 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.contentType, 'image/jpeg');
assert.strictEqual(group.avatar.length, 255); assert.strictEqual(group.avatar.length, 255);
assert.strictEqual(group.avatar.data.byteLength, 255); assert.strictEqual(group.avatar.data.byteLength, 255);

View file

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

View file

@ -4,6 +4,12 @@ function SignalProtocolStore() {
SignalProtocolStore.prototype = { SignalProtocolStore.prototype = {
Direction: { SENDING: 1, RECEIVING: 2 }, Direction: { SENDING: 1, RECEIVING: 2 },
VerifiedStatus: {
DEFAULT: 0,
VERIFIED: 1,
UNVERIFIED: 2,
},
getIdentityKeyPair() { getIdentityKeyPair() {
return Promise.resolve(this.get('identityKey')); 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="../protobufs.js" data-cover></script>
<script type="text/javascript" src="../errors.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="../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="../event_target.js" data-cover></script>
<script type="text/javascript" src="../websocket-resources.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="../account_manager.js" data-cover></script>
<script type="text/javascript" src="../contacts_parser.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="../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="errors_test.js"></script>
<script type="text/javascript" src="helpers_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(); textsecure.storage.impl = new SignalProtocolStore();
const { WebSocket } = window; const { WebSocket } = window;
const number = '+19999999999'; const number = '+19999999999';
const uuid = 'AAAAAAAA-BBBB-4CCC-9DDD-EEEEEEEEEEEE';
const deviceId = 1; const deviceId = 1;
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20); const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
before(() => { before(() => {
window.WebSocket = MockSocket; window.WebSocket = MockSocket;
textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name'); textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name');
textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId);
textsecure.storage.put('password', 'password'); textsecure.storage.put('password', 'password');
textsecure.storage.put('signaling_key', signalingKey); textsecure.storage.put('signaling_key', signalingKey);
}); });
@ -21,6 +23,7 @@ describe('MessageReceiver', () => {
const attrs = { const attrs = {
type: textsecure.protobuf.Envelope.Type.CIPHERTEXT, type: textsecure.protobuf.Envelope.Type.CIPHERTEXT,
source: number, source: number,
sourceUuid: uuid,
sourceDevice: deviceId, sourceDevice: deviceId,
timestamp: Date.now(), timestamp: Date.now(),
}; };
@ -72,7 +75,7 @@ describe('MessageReceiver', () => {
it('connects', done => { it('connects', done => {
const mockServer = new MockServer( const mockServer = new MockServer(
`ws://localhost:8080/v1/websocket/?login=${encodeURIComponent( `ws://localhost:8080/v1/websocket/?login=${encodeURIComponent(
number uuid
)}.1&password=password` )}.1&password=password`
); );

View file

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

View file

@ -68,7 +68,8 @@ const userConfig = require('./app/user_config');
const importMode = const importMode =
process.argv.some(arg => arg === '--import') || config.get('import'); process.argv.some(arg => arg === '--import') || config.get('import');
const development = config.environment === 'development'; const development =
config.environment === 'development' || config.environment === 'staging';
// We generally want to pull in our own modules after this point, after the user // We generally want to pull in our own modules after this point, after the user
// data directory has been set. // data directory has been set.

View file

@ -6,6 +6,7 @@ try {
const electron = require('electron'); const electron = require('electron');
const semver = require('semver'); const semver = require('semver');
const curve = require('curve25519-n'); const curve = require('curve25519-n');
const _ = require('lodash');
const { installGetter, installSetter } = require('./preload_utils'); const { installGetter, installSetter } = require('./preload_utils');
const { deferredToPromise } = require('./js/modules/deferred_to_promise'); const { deferredToPromise } = require('./js/modules/deferred_to_promise');
@ -254,6 +255,30 @@ try {
window.loadImage = require('blueimp-load-image'); window.loadImage = require('blueimp-load-image');
window.getGuid = require('uuid/v4'); window.getGuid = require('uuid/v4');
window.isValidGuid = maybeGuid =>
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
maybeGuid
);
// https://stackoverflow.com/a/23299989
window.isValidE164 = maybeE164 => /^\+?[1-9]\d{1,14}$/.test(maybeE164);
window.normalizeUuids = (obj, paths, context) => {
if (!obj) {
return;
}
paths.forEach(path => {
const val = _.get(obj, path);
if (val) {
if (!window.isValidGuid(val)) {
window.log.warn(
`Normalizing invalid uuid: ${val} at path ${path} in context "${context}"`
);
}
_.set(obj, path, val.toLowerCase());
}
});
};
window.React = require('react'); window.React = require('react');
window.ReactDOM = require('react-dom'); window.ReactDOM = require('react-dom');
window.moment = require('moment'); window.moment = require('moment');

View file

@ -11,10 +11,20 @@ message ProvisionEnvelope {
} }
message ProvisionMessage { message ProvisionMessage {
optional bytes identityKeyPrivate = 2; optional bytes identityKeyPrivate = 2;
optional string number = 3; optional string number = 3;
optional string provisioningCode = 4; optional string uuid = 8;
optional string userAgent = 5; optional string provisioningCode = 4;
optional bytes profileKey = 6; optional string userAgent = 5;
optional bool readReceipts = 7; optional bytes profileKey = 6;
optional bool readReceipts = 7;
optional uint32 ProvisioningVersion = 9;
} }
enum ProvisioningVersion {
option allow_alias = true;
INITIAL = 0;
TABLET_SUPPORT = 1;
CURRENT = 1;
}

View file

@ -16,6 +16,7 @@ message Envelope {
optional Type type = 1; optional Type type = 1;
optional string source = 2; optional string source = 2;
optional string sourceUuid = 11;
optional uint32 sourceDevice = 7; optional uint32 sourceDevice = 7;
optional string relay = 3; optional string relay = 3;
optional uint64 timestamp = 5; optional uint64 timestamp = 5;
@ -85,6 +86,7 @@ message DataMessage {
optional uint64 id = 1; optional uint64 id = 1;
optional string author = 2; optional string author = 2;
optional string authorUuid = 5;
optional string text = 3; optional string text = 3;
repeated QuotedAttachment attachments = 4; repeated QuotedAttachment attachments = 4;
} }
@ -236,20 +238,23 @@ message Verified {
UNVERIFIED = 2; UNVERIFIED = 2;
} }
optional string destination = 1; optional string destination = 1;
optional bytes identityKey = 2; optional string destinationUuid = 5;
optional State state = 3; optional bytes identityKey = 2;
optional bytes nullMessage = 4; optional State state = 3;
optional bytes nullMessage = 4;
} }
message SyncMessage { message SyncMessage {
message Sent { message Sent {
message UnidentifiedDeliveryStatus { message UnidentifiedDeliveryStatus {
optional string destination = 1; optional string destination = 1;
optional bool unidentified = 2; optional string destinationUuid = 3;
optional bool unidentified = 2;
} }
optional string destination = 1; optional string destination = 1;
optional string destinationUuid = 7;
optional uint64 timestamp = 2; optional uint64 timestamp = 2;
optional DataMessage message = 3; optional DataMessage message = 3;
optional uint64 expirationStartTimestamp = 4; optional uint64 expirationStartTimestamp = 4;
@ -268,6 +273,7 @@ message SyncMessage {
message Blocked { message Blocked {
repeated string numbers = 1; repeated string numbers = 1;
repeated string uuids = 3;
repeated bytes groupIds = 2; repeated bytes groupIds = 2;
} }
@ -284,8 +290,9 @@ message SyncMessage {
} }
message Read { message Read {
optional string sender = 1; optional string sender = 1;
optional uint64 timestamp = 2; optional string senderUuid = 3;
optional uint64 timestamp = 2;
} }
message Configuration { message Configuration {
@ -306,8 +313,9 @@ message SyncMessage {
} }
message ViewOnceOpen { message ViewOnceOpen {
optional string sender = 1; optional string sender = 1;
optional uint64 timestamp = 2; optional string senderUuid = 3;
optional uint64 timestamp = 2;
} }
optional Sent sent = 1; optional Sent sent = 1;
@ -349,11 +357,18 @@ message GroupContext {
QUIT = 3; QUIT = 3;
REQUEST_INFO = 4; REQUEST_INFO = 4;
} }
optional bytes id = 1;
optional Type type = 2; message Member {
optional string name = 3; optional string uuid = 1;
repeated string members = 4; optional string e164 = 2;
optional AttachmentPointer avatar = 5; }
optional bytes id = 1;
optional Type type = 2;
optional string name = 3;
repeated string membersE164 = 4;
repeated Member members = 6;
optional AttachmentPointer avatar = 5;
} }
message ContactDetails { message ContactDetails {
@ -363,6 +378,7 @@ message ContactDetails {
} }
optional string number = 1; optional string number = 1;
optional string uuid = 9;
optional string name = 2; optional string name = 2;
optional Avatar avatar = 3; optional Avatar avatar = 3;
optional string color = 4; optional string color = 4;
@ -378,9 +394,15 @@ message GroupDetails {
optional uint32 length = 2; optional uint32 length = 2;
} }
message Member {
optional string uuid = 1;
optional string e164 = 2;
}
optional bytes id = 1; optional bytes id = 1;
optional string name = 2; optional string name = 2;
repeated string members = 3; repeated string membersE164 = 3;
repeated Member members = 9;
optional Avatar avatar = 4; optional Avatar avatar = 4;
optional bool active = 5 [default = true]; optional bool active = 5 [default = true];
optional uint32 expireTimer = 6; optional uint32 expireTimer = 6;

View file

@ -15,11 +15,12 @@ message ServerCertificate {
message SenderCertificate { message SenderCertificate {
message Certificate { message Certificate {
optional string sender = 1; optional string sender = 1;
optional uint32 senderDevice = 2; optional string senderUuid = 6;
optional fixed64 expires = 3; optional uint32 senderDevice = 2;
optional bytes identityKey = 4; optional fixed64 expires = 3;
optional ServerCertificate signer = 5; optional bytes identityKey = 4;
optional ServerCertificate signer = 5;
} }
optional bytes certificate = 1; optional bytes certificate = 1;

View file

@ -69,10 +69,11 @@ window.encryptAndUpload = async (
cover, cover,
onProgress = noop onProgress = noop
) => { ) => {
const usernameItem = await window.Signal.Data.getItemById('number_id'); const usernameItem = await window.Signal.Data.getItemById('uuid_id');
const oldUsernameItem = await window.Signal.Data.getItemById('number_id');
const passwordItem = await window.Signal.Data.getItemById('password'); const passwordItem = await window.Signal.Data.getItemById('password');
if (!usernameItem || !passwordItem) { if (!oldUsernameItem || !passwordItem) {
const { message } = window.localeMessages[ const { message } = window.localeMessages[
'StickerCreator--Authentication--error' 'StickerCreator--Authentication--error'
]; ];
@ -86,13 +87,17 @@ window.encryptAndUpload = async (
} }
const { value: username } = usernameItem; const { value: username } = usernameItem;
const { value: oldUsername } = oldUsernameItem;
const { value: password } = passwordItem; const { value: password } = passwordItem;
const packKey = window.libsignal.crypto.getRandomBytes(32); const packKey = window.libsignal.crypto.getRandomBytes(32);
const encryptionKey = await deriveStickerPackKey(packKey); const encryptionKey = await deriveStickerPackKey(packKey);
const iv = window.libsignal.crypto.getRandomBytes(16); const iv = window.libsignal.crypto.getRandomBytes(16);
const server = WebAPI.connect({ username, password }); const server = WebAPI.connect({
username: username || oldUsername,
password,
});
const uniqueStickers = uniqBy([...stickers, { webp: cover }], 'webp'); const uniqueStickers = uniqBy([...stickers, { webp: cover }], 'webp');

View file

@ -261,6 +261,8 @@ describe('Backup', () => {
const CONTACT_ONE_NUMBER = '+12025550001'; const CONTACT_ONE_NUMBER = '+12025550001';
const CONTACT_TWO_NUMBER = '+12025550002'; const CONTACT_TWO_NUMBER = '+12025550002';
const CONVERSATION_ID = 'bdaa7f4f-e9bd-493e-ab0d-8331ad604269';
const toArrayBuffer = nodeBuffer => const toArrayBuffer = nodeBuffer =>
nodeBuffer.buffer.slice( nodeBuffer.buffer.slice(
nodeBuffer.byteOffset, nodeBuffer.byteOffset,
@ -405,7 +407,7 @@ describe('Backup', () => {
const CONVERSATION_COUNT = 1; const CONVERSATION_COUNT = 1;
const messageWithAttachments = { const messageWithAttachments = {
conversationId: CONTACT_ONE_NUMBER, conversationId: CONVERSATION_ID,
body: 'Totally!', body: 'Totally!',
source: OUR_NUMBER, source: OUR_NUMBER,
received_at: 1524185933350, received_at: 1524185933350,
@ -493,7 +495,7 @@ describe('Backup', () => {
active_at: 1524185933350, active_at: 1524185933350,
color: 'orange', color: 'orange',
expireTimer: 0, expireTimer: 0,
id: CONTACT_ONE_NUMBER, id: CONVERSATION_ID,
name: 'Someone Somewhere', name: 'Someone Somewhere',
profileAvatar: { profileAvatar: {
contentType: 'image/jpeg', contentType: 'image/jpeg',

View file

@ -21,6 +21,20 @@ describe('Privacy', () => {
}); });
}); });
describe('redactUuids', () => {
it('should redact all uuids', () => {
const text =
'This is a log line with a uuid 9e420799-acdf-4bf4-8dee-353d7e2096b4\n' +
'and another one IN ALL UPPERCASE 340727FB-E43A-413B-941B-AADA033B6CA3';
const actual = Privacy.redactUuids(text);
const expected =
'This is a log line with a uuid [REDACTED]b4\n' +
'and another one IN ALL UPPERCASE [REDACTED]A3';
assert.equal(actual, expected);
});
});
describe('redactGroupIds', () => { describe('redactGroupIds', () => {
it('should redact all group IDs', () => { it('should redact all group IDs', () => {
const text = const text =

View file

@ -1,4 +1,4 @@
/* global _, textsecure, libsignal, storage */ /* global _, textsecure, libsignal, storage, ConversationController */
'use strict'; 'use strict';
@ -8,7 +8,7 @@ describe('SignalProtocolStore', () => {
let identityKey; let identityKey;
let testKey; let testKey;
before(done => { before(async () => {
store = textsecure.storage.protocol; store = textsecure.storage.protocol;
store.hydrateCaches(); store.hydrateCaches();
identityKey = { identityKey = {
@ -22,7 +22,10 @@ describe('SignalProtocolStore', () => {
storage.put('registrationId', 1337); storage.put('registrationId', 1337);
storage.put('identityKey', identityKey); storage.put('identityKey', identityKey);
storage.fetch().then(done, done); await storage.fetch();
ConversationController.reset();
await ConversationController.load();
await ConversationController.getOrCreateAndWait(number, 'private');
}); });
describe('getLocalRegistrationId', () => { describe('getLocalRegistrationId', () => {

View file

@ -16,6 +16,8 @@ export interface PropsType {
startSearchCounter: number; startSearchCounter: number;
// To be used as an ID // To be used as an ID
ourConversationId: string;
ourUuid: string;
ourNumber: string; ourNumber: string;
regionCode: string; regionCode: string;
@ -40,7 +42,9 @@ export interface PropsType {
searchDiscussions: ( searchDiscussions: (
query: string, query: string,
options: { options: {
ourConversationId: string;
ourNumber: string; ourNumber: string;
ourUuid: string;
noteToSelf: string; noteToSelf: string;
} }
) => void; ) => void;
@ -147,7 +151,9 @@ export class MainHeader extends React.Component<PropsType, StateType> {
public search = debounce((searchTerm: string) => { public search = debounce((searchTerm: string) => {
const { const {
i18n, i18n,
ourConversationId,
ourNumber, ourNumber,
ourUuid,
regionCode, regionCode,
searchDiscussions, searchDiscussions,
searchMessages, searchMessages,
@ -157,7 +163,9 @@ export class MainHeader extends React.Component<PropsType, StateType> {
if (searchDiscussions && !searchConversationId) { if (searchDiscussions && !searchConversationId) {
searchDiscussions(searchTerm, { searchDiscussions(searchTerm, {
noteToSelf: i18n('noteToSelf').toLowerCase(), noteToSelf: i18n('noteToSelf').toLowerCase(),
ourConversationId,
ourNumber, ourNumber,
ourUuid,
}); });
} }

View file

@ -148,7 +148,7 @@ function searchMessages(
function searchDiscussions( function searchDiscussions(
query: string, query: string,
options: { options: {
ourNumber: string; ourConversationId: string;
noteToSelf: string; noteToSelf: string;
} }
): SearchDiscussionsResultsKickoffActionType { ): SearchDiscussionsResultsKickoffActionType {
@ -180,15 +180,15 @@ async function doSearchMessages(
async function doSearchDiscussions( async function doSearchDiscussions(
query: string, query: string,
options: { options: {
ourNumber: string; ourConversationId: string;
noteToSelf: string; noteToSelf: string;
} }
): Promise<SearchDiscussionsResultsPayloadType> { ): Promise<SearchDiscussionsResultsPayloadType> {
const { ourNumber, noteToSelf } = options; const { ourConversationId, noteToSelf } = options;
const { conversations, contacts } = await queryConversationsAndContacts( const { conversations, contacts } = await queryConversationsAndContacts(
query, query,
{ {
ourNumber, ourConversationId,
noteToSelf, noteToSelf,
} }
); );
@ -271,9 +271,12 @@ async function queryMessages(query: string, searchConversationId?: string) {
async function queryConversationsAndContacts( async function queryConversationsAndContacts(
providedQuery: string, providedQuery: string,
options: { ourNumber: string; noteToSelf: string } options: {
ourConversationId: string;
noteToSelf: string;
}
) { ) {
const { ourNumber, noteToSelf } = options; const { ourConversationId, noteToSelf } = options;
const query = providedQuery.replace(/[+-.()]*/g, ''); const query = providedQuery.replace(/[+-.()]*/g, '');
const searchResults: Array<DBConversationType> = await dataSearchConversations( const searchResults: Array<DBConversationType> = await dataSearchConversations(
@ -294,13 +297,20 @@ async function queryConversationsAndContacts(
} }
} }
// // @ts-ignore
// console._log(
// '%cqueryConversationsAndContacts',
// 'background:black;color:red;',
// { searchResults, conversations, ourNumber, ourUuid }
// );
// Inject synthetic Note to Self entry if query matches localized 'Note to Self' // Inject synthetic Note to Self entry if query matches localized 'Note to Self'
if (noteToSelf.indexOf(providedQuery.toLowerCase()) !== -1) { if (noteToSelf.indexOf(providedQuery.toLowerCase()) !== -1) {
// ensure that we don't have duplicates in our results // ensure that we don't have duplicates in our results
contacts = contacts.filter(id => id !== ourNumber); contacts = contacts.filter(id => id !== ourConversationId);
conversations = conversations.filter(id => id !== ourNumber); conversations = conversations.filter(id => id !== ourConversationId);
contacts.unshift(ourNumber); contacts.unshift(ourConversationId);
} }
return { conversations, contacts }; return { conversations, contacts };

View file

@ -6,6 +6,8 @@ export type UserStateType = {
attachmentsPath: string; attachmentsPath: string;
stickersPath: string; stickersPath: string;
tempPath: string; tempPath: string;
ourConversationId: string;
ourUuid: string;
ourNumber: string; ourNumber: string;
platform: string; platform: string;
regionCode: string; regionCode: string;
@ -18,6 +20,8 @@ export type UserStateType = {
type UserChangedActionType = { type UserChangedActionType = {
type: 'USER_CHANGED'; type: 'USER_CHANGED';
payload: { payload: {
ourConversationId?: string;
ourUuid?: string;
ourNumber?: string; ourNumber?: string;
regionCode?: string; regionCode?: string;
interactionMode?: 'mouse' | 'keyboard'; interactionMode?: 'mouse' | 'keyboard';
@ -34,7 +38,9 @@ export const actions = {
function userChanged(attributes: { function userChanged(attributes: {
interactionMode?: 'mouse' | 'keyboard'; interactionMode?: 'mouse' | 'keyboard';
ourConversationId: string;
ourNumber: string; ourNumber: string;
ourUuid: string;
regionCode: string; regionCode: string;
}): UserChangedActionType { }): UserChangedActionType {
return { return {
@ -50,6 +56,8 @@ function getEmptyState(): UserStateType {
attachmentsPath: 'missing', attachmentsPath: 'missing',
stickersPath: 'missing', stickersPath: 'missing',
tempPath: 'missing', tempPath: 'missing',
ourConversationId: 'missing',
ourUuid: 'missing',
ourNumber: 'missing', ourNumber: 'missing',
regionCode: 'missing', regionCode: 'missing',
platform: 'missing', platform: 'missing',

View file

@ -22,6 +22,7 @@ import {
getInteractionMode, getInteractionMode,
getIntl, getIntl,
getRegionCode, getRegionCode,
getUserConversationId,
getUserNumber, getUserNumber,
} from './user'; } from './user';
@ -181,9 +182,12 @@ export const getLeftPaneLists = createSelector(
); );
export const getMe = createSelector( export const getMe = createSelector(
[getConversationLookup, getUserNumber], [getConversationLookup, getUserConversationId],
(lookup: ConversationLookupType, ourNumber: string): ConversationType => { (
return lookup[ourNumber]; lookup: ConversationLookupType,
ourConversationId: string
): ConversationType => {
return lookup[ourConversationId];
} }
); );

View file

@ -149,6 +149,7 @@ export const getSearchResults = createSelector(
}); });
contacts.forEach(id => { contacts.forEach(id => {
const data = lookup[id]; const data = lookup[id];
items.push({ items.push({
type: 'contact', type: 'contact',
data: { data: {

View file

@ -17,6 +17,16 @@ export const getRegionCode = createSelector(
(state: UserStateType): string => state.regionCode (state: UserStateType): string => state.regionCode
); );
export const getUserConversationId = createSelector(
getUser,
(state: UserStateType): string => state.ourConversationId
);
export const getUserUuid = createSelector(
getUser,
(state: UserStateType): string => state.ourUuid
);
export const getIntl = createSelector( export const getIntl = createSelector(
getUser, getUser,
(state: UserStateType): LocalizerType => state.i18n (state: UserStateType): LocalizerType => state.i18n

View file

@ -10,7 +10,13 @@ import {
getSearchConversationName, getSearchConversationName,
getStartSearchCounter, getStartSearchCounter,
} from '../selectors/search'; } from '../selectors/search';
import { getIntl, getRegionCode, getUserNumber } from '../selectors/user'; import {
getIntl,
getRegionCode,
getUserConversationId,
getUserNumber,
getUserUuid,
} from '../selectors/user';
import { getMe } from '../selectors/conversations'; import { getMe } from '../selectors/conversations';
const mapStateToProps = (state: StateType) => { const mapStateToProps = (state: StateType) => {
@ -20,7 +26,9 @@ const mapStateToProps = (state: StateType) => {
searchConversationName: getSearchConversationName(state), searchConversationName: getSearchConversationName(state),
startSearchCounter: getStartSearchCounter(state), startSearchCounter: getStartSearchCounter(state),
regionCode: getRegionCode(state), regionCode: getRegionCode(state),
ourConversationId: getUserConversationId(state),
ourNumber: getUserNumber(state), ourNumber: getUserNumber(state),
ourUuid: getUserUuid(state),
...getMe(state), ...getMe(state),
i18n: getIntl(state), i18n: getIntl(state),
}; };

View file

@ -164,17 +164,17 @@
"rule": "jQuery-load(", "rule": "jQuery-load(",
"path": "js/conversation_controller.js", "path": "js/conversation_controller.js",
"line": " async load() {", "line": " async load() {",
"lineNumber": 171, "lineNumber": 210,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-07-31T00:19:18.696Z" "updated": "2020-02-14T20:02:37.507Z"
}, },
{ {
"rule": "jQuery-load(", "rule": "jQuery-load(",
"path": "js/conversation_controller.js", "path": "js/conversation_controller.js",
"line": " this._initialPromise = load();", "line": " this._initialPromise = load();",
"lineNumber": 216, "lineNumber": 255,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-07-31T00:19:18.696Z" "updated": "2020-02-14T20:02:37.507Z"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
@ -301,9 +301,9 @@
"rule": "jQuery-load(", "rule": "jQuery-load(",
"path": "js/signal_protocol_store.js", "path": "js/signal_protocol_store.js",
"line": " await ConversationController.load();", "line": " await ConversationController.load();",
"lineNumber": 894, "lineNumber": 983,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2018-09-15T00:38:04.183Z" "updated": "2020-02-28T18:14:42.951Z"
}, },
{ {
"rule": "DOM-innerHTML", "rule": "DOM-innerHTML",
@ -669,44 +669,44 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/key_verification_view.js", "path": "js/views/key_verification_view.js",
"line": " new QRCode(this.$('.qr')[0]).makeCode(", "line": " new QRCode(this.$('.qr')[0]).makeCode(",
"lineNumber": 42, "lineNumber": 43,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-07-31T00:19:18.696Z", "updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Hardcoded selector" "reasonDetail": "Hardcoded selector"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "js/views/key_verification_view.js", "path": "js/views/key_verification_view.js",
"line": " dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64')", "line": " dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64')",
"lineNumber": 43, "lineNumber": 44,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-07-31T00:19:18.696Z" "updated": "2020-02-14T20:02:37.507Z"
}, },
{ {
"rule": "jQuery-insertBefore(", "rule": "jQuery-insertBefore(",
"path": "js/views/key_verification_view.js", "path": "js/views/key_verification_view.js",
"line": " dialog.$el.insertBefore(this.el);", "line": " dialog.$el.insertBefore(this.el);",
"lineNumber": 72, "lineNumber": 86,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-07-31T00:19:18.696Z", "updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Known DOM elements" "reasonDetail": "Known DOM elements"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/key_verification_view.js", "path": "js/views/key_verification_view.js",
"line": " this.$('button.verify').attr('disabled', true);", "line": " this.$('button.verify').attr('disabled', true);",
"lineNumber": 76, "lineNumber": 90,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-07-31T00:19:18.696Z", "updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Hardcoded selector" "reasonDetail": "Hardcoded selector"
}, },
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/key_verification_view.js", "path": "js/views/key_verification_view.js",
"line": " this.$('button.verify').removeAttr('disabled');", "line": " this.$('button.verify').removeAttr('disabled');",
"lineNumber": 107, "lineNumber": 121,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-07-31T00:19:18.696Z", "updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Hardcoded selector" "reasonDetail": "Hardcoded selector"
}, },
{ {
@ -1203,65 +1203,65 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "libtextsecure/message_receiver.js", "path": "libtextsecure/message_receiver.js",
"line": " dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();", "line": " dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();",
"lineNumber": 62, "lineNumber": 72,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-09-20T18:36:19.909Z" "updated": "2020-02-14T20:02:37.507Z"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "libtextsecure/message_receiver.js", "path": "libtextsecure/message_receiver.js",
"line": " dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');", "line": " dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');",
"lineNumber": 64, "lineNumber": 74,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-09-20T18:36:19.909Z" "updated": "2020-02-14T20:02:37.507Z"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "libtextsecure/message_receiver.js", "path": "libtextsecure/message_receiver.js",
"line": " dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();", "line": " dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
"lineNumber": 66, "lineNumber": 76,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-09-20T18:36:19.909Z" "updated": "2020-02-14T20:02:37.507Z"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "libtextsecure/message_receiver.js", "path": "libtextsecure/message_receiver.js",
"line": " dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');", "line": " dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
"lineNumber": 68, "lineNumber": 78,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-09-20T18:36:19.909Z" "updated": "2020-02-14T20:02:37.507Z"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "libtextsecure/message_receiver.js", "path": "libtextsecure/message_receiver.js",
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);", "line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
"lineNumber": 752, "lineNumber": 812,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-10-21T22:30:15.622Z" "updated": "2020-03-04T21:24:23.269Z"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "libtextsecure/message_receiver.js", "path": "libtextsecure/message_receiver.js",
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);", "line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
"lineNumber": 777, "lineNumber": 837,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-10-21T22:30:15.622Z" "updated": "2020-03-04T21:24:23.269Z"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "libtextsecure/sendmessage.js", "path": "libtextsecure/sendmessage.js",
"line": " return dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();", "line": " return dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();",
"lineNumber": 17, "lineNumber": 18,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-04-26T17:48:30.675Z" "updated": "2020-02-14T20:02:37.507Z"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "libtextsecure/sendmessage.js", "path": "libtextsecure/sendmessage.js",
"line": " return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();", "line": " return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
"lineNumber": 20, "lineNumber": 21,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-04-26T17:48:30.675Z" "updated": "2020-02-14T20:02:37.507Z"
}, },
{ {
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
@ -11663,18 +11663,18 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/MainHeader.js", "path": "ts/components/MainHeader.js",
"line": " this.inputRef = react_1.default.createRef();", "line": " this.inputRef = react_1.default.createRef();",
"lineNumber": 144, "lineNumber": 146,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-08-09T21:17:57.798Z", "updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Used only to set focus" "reasonDetail": "Used only to set focus"
}, },
{ {
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/MainHeader.tsx", "path": "ts/components/MainHeader.tsx",
"line": " this.inputRef = React.createRef();", "line": " this.inputRef = React.createRef();",
"lineNumber": 65, "lineNumber": 69,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-08-09T21:17:57.798Z", "updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Used only to set focus" "reasonDetail": "Used only to set focus"
}, },
{ {
@ -11816,4 +11816,4 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-02-07T19:52:28.522Z" "updated": "2020-02-07T19:52:28.522Z"
} }
] ]