From d20861490972ca771a36b6c66ba98f8a5ea8bfaa Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Tue, 6 Aug 2019 14:49:01 +1000 Subject: [PATCH 01/64] Multi-device part 1: make primary account generate and send authorisation to secondary --- app/sql.js | 73 +++++++++++++++++++++++++++++++ js/modules/data.js | 20 +++++++++ libloki/api.js | 28 ++++++++++++ libloki/crypto.js | 43 ++++++++++++++++++ libloki/storage.js | 24 ++++++++++ libtextsecure/account_manager.js | 36 +++++++++++++++ libtextsecure/message_receiver.js | 49 +++++++++++++++++++++ libtextsecure/outgoing_message.js | 5 ++- protos/SignalService.proto | 12 +++++ 9 files changed, 288 insertions(+), 2 deletions(-) diff --git a/app/sql.js b/app/sql.js index ac5d5ec2c..f069c5159 100644 --- a/app/sql.js +++ b/app/sql.js @@ -72,6 +72,8 @@ module.exports = { removeContactSignedPreKeyByIdentityKey, removeAllContactSignedPreKeys, + createOrUpdatePairingAuthorisation, + createOrUpdateItem, getItemById, getAllItems, @@ -737,6 +739,28 @@ async function updateToSchemaVersion11(currentVersion, instance) { console.log('updateToSchemaVersion11: success!'); } +async function updateToSchemaVersion12(currentVersion, instance) { + if (currentVersion >= 12) { + return; + } + console.log('updateToSchemaVersion12: starting...'); + await instance.run('BEGIN TRANSACTION;'); + + await instance.run( + `CREATE TABLE pairingAuthorisations( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + issuerPubKey VARCHAR(255), + secondaryDevicePubKey VARCHAR(255), + signature VARCHAR(255), + json TEXT + );` + ); + + await instance.run('PRAGMA schema_version = 12;'); + await instance.run('COMMIT TRANSACTION;'); + console.log('updateToSchemaVersion12: success!'); +} + const SCHEMA_VERSIONS = [ updateToSchemaVersion1, updateToSchemaVersion2, @@ -749,6 +773,7 @@ const SCHEMA_VERSIONS = [ updateToSchemaVersion9, updateToSchemaVersion10, updateToSchemaVersion11, + updateToSchemaVersion12, ]; async function updateSchema(instance) { @@ -1135,6 +1160,54 @@ async function removeAllSignedPreKeys() { return removeAllFromTable(SIGNED_PRE_KEYS_TABLE); } +const PAIRING_AUTHORISATIONS_TABLE = 'pairingAuthorisations'; +async function getPairingAuthorisation(issuerPubKey, secondaryDevicePubKey) { + const row = await db.get( + `SELECT * FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE + issuerPubKey = $issuerPubKey AND secondaryDevicePubKey = $secondaryDevicePubKey + LIMIT 1;`, + { + $issuerPubKey: issuerPubKey, + $secondaryDevicePubKey: secondaryDevicePubKey, + } + ); + + if (!row) { + return null; + } + + return jsonToObject(row.json); +} +async function createOrUpdatePairingAuthorisation(data) { + const { issuerPubKey, secondaryDevicePubKey, signature } = data; + + const existing = await getPairingAuthorisation(issuerPubKey, secondaryDevicePubKey); + // prevent adding duplicate entries + if (existing) { + return; + } + + await db.run( + `INSERT INTO ${PAIRING_AUTHORISATIONS_TABLE} ( + issuerPubKey, + secondaryDevicePubKey, + signature, + json + ) values ( + $issuerPubKey, + $secondaryDevicePubKey, + $signature, + $json + )`, + { + $issuerPubKey: issuerPubKey, + $secondaryDevicePubKey: secondaryDevicePubKey, + $signature: signature, + $json: objectToJSON(data), + } + ); +} + const ITEMS_TABLE = 'items'; async function createOrUpdateItem(data) { return createOrUpdate(ITEMS_TABLE, data); diff --git a/js/modules/data.js b/js/modules/data.js index e5a9e1af1..72d8485e8 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -12,6 +12,7 @@ const { merge, set, omit, + isArrayBuffer, } = require('lodash'); const { base64ToArrayBuffer, arrayBufferToBase64 } = require('./crypto'); @@ -88,6 +89,8 @@ module.exports = { removeContactSignedPreKeyByIdentityKey, removeAllContactSignedPreKeys, + createOrUpdatePairingAuthorisation, + createOrUpdateItem, getItemById, getAllItems, @@ -570,6 +573,23 @@ async function removeAllContactSignedPreKeys() { await channels.removeAllContactSignedPreKeys(); } +async function createOrUpdatePairingAuthorisation(data) { + let sig; + if (isArrayBuffer(data.signature)) { + sig = arrayBufferToBase64(data.signature); + } else if (typeof signature === 'string') { + sig = data.signature; + } else { + throw new Error( + 'Invalid signature provided in createOrUpdatePairingAuthorisation. Needs to be either ArrayBuffer or string.' + ); + } + return channels.createOrUpdatePairingAuthorisation({ + ...data, + signature: sig, + }); +} + // Items const ITEM_KEYS = { diff --git a/libloki/api.js b/libloki/api.js index e25d69cba..ccc15ef12 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -63,9 +63,37 @@ await outgoingMessage.sendToNumber(pubKey); } + async function sendPairingAuthorisation(secondaryDevicePubKey, signature) { + const pairingAuthorisation = new textsecure.protobuf.PairingAuthorisationMessage( + { + signature, + primaryDevicePubKey: textsecure.storage.user.getNumber(), + secondaryDevicePubKey, + type: + textsecure.protobuf.PairingAuthorisationMessage.Type.PAIRING_REQUEST, + } + ); + const content = new textsecure.protobuf.Content({ + pairingAuthorisation, + }); + const options = {}; + // Send a empty message with information about how to contact us directly + const outgoingMessage = new textsecure.OutgoingMessage( + null, // server + Date.now(), // timestamp, + [secondaryDevicePubKey], // numbers + content, // message + true, // silent + () => null, // callback + options + ); + await outgoingMessage.sendToNumber(secondaryDevicePubKey); + } + window.libloki.api = { sendBackgroundMessage, sendOnlineBroadcastMessage, broadcastOnlineStatus, + sendPairingAuthorisation, }; })(); diff --git a/libloki/crypto.js b/libloki/crypto.js index 7a0c7a271..b5201183b 100644 --- a/libloki/crypto.js +++ b/libloki/crypto.js @@ -158,6 +158,47 @@ } } + async function generateSignatureForPairing(secondaryPubKey, type) { + const pubKeyArrayBuffer = StringView.hexToArrayBuffer(secondaryPubKey); + // Make sure the signature includes the pairing action (pairing or unpairing) + const len = pubKeyArrayBuffer.byteLength; + const data = new Uint8Array(len + 1); + data.set(new Uint8Array(pubKeyArrayBuffer), 0); + data[len] = type; + + const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair(); + const signature = await libsignal.Curve.async.calculateSignature( + myKeyPair.privKey, + data.buffer + ); + return new Uint8Array(signature); + } + + async function verifyPairingAuthorisation( + issuerPubKey, + secondaryPubKey, + signature, + type + ) { + const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair(); + if (StringView.arrayBufferToHex(myKeyPair.pubKey) !== secondaryPubKey) { + throw new Error( + 'Invalid pairing authorisation: we are not the recipient of the authorisation!' + ); + } + const len = myKeyPair.pubKey.byteLength; + const data = new Uint8Array(len + 1); + data.set(new Uint8Array(myKeyPair.pubKey), 0); + data[len] = type; + const issuerPubKeyArrayBuffer = StringView.hexToArrayBuffer(issuerPubKey); + // Throws for invalid signature + await libsignal.Curve.async.verifySignature( + issuerPubKeyArrayBuffer, + data.buffer, + signature + ); + } + const snodeCipher = new LokiSnodeChannel(); window.libloki.crypto = { @@ -166,6 +207,8 @@ FallBackSessionCipher, FallBackDecryptionError, snodeCipher, + generateSignatureForPairing, + verifyPairingAuthorisation, // for testing _LokiSnodeChannel: LokiSnodeChannel, _decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey, diff --git a/libloki/storage.js b/libloki/storage.js index 8d61881bd..81be176b4 100644 --- a/libloki/storage.js +++ b/libloki/storage.js @@ -113,11 +113,24 @@ } } + async function savePairingAuthorisation( + issuerPubKey, + secondaryDevicePubKey, + signature + ) { + return textsecure.storage.protocol.storePairingAuthorisation( + issuerPubKey, + secondaryDevicePubKey, + signature + ); + } + window.libloki.storage = { getPreKeyBundleForContact, saveContactPreKeyBundle, removeContactPreKeyBundle, verifyFriendRequestAcceptPreKey, + savePairingAuthorisation, }; // Libloki protocol store @@ -243,4 +256,15 @@ store.clearContactSignedPreKeysStore = async () => { await window.Signal.Data.removeAllContactSignedPreKeys(); }; + + store.storePairingAuthorisation = ( + issuerPubKey, + secondaryDevicePubKey, + signature + ) => + window.Signal.Data.createOrUpdatePairingAuthorisation({ + issuerPubKey, + secondaryDevicePubKey, + signature, + }); })(); diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index db0c9192a..39806415d 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -2,6 +2,8 @@ window, textsecure, libsignal, + libloki, + Whisper, mnemonic, btoa, Signal, @@ -553,6 +555,40 @@ this.dispatchEvent(new Event('registration')); }, + async authoriseSecondaryDevice(secondaryDevicePubKey) { + if (secondaryDevicePubKey === textsecure.storage.user.getNumber()) { + throw new Error( + 'Cannot register primary device pubkey as secondary device' + ); + } + + // Validate pubKey + const c = new Whisper.Conversation({ + id: secondaryDevicePubKey, + type: 'private', + }); + const validationError = c.validateNumber(); + if (validationError) { + throw new Error('Invalid secondary device pubkey provided'); + } + // Ensure there is a conversation existing + try { + await ConversationController.getOrCreateAndWait( + secondaryDevicePubKey, + 'private' + ); + } catch (e) { + window.log.error(e); + } + const signature = await libloki.crypto.generateSignatureForPairing( + secondaryDevicePubKey, + textsecure.protobuf.PairingAuthorisationMessage.Type.PAIRING_REQUEST + ); + await libloki.api.sendPairingAuthorisation( + secondaryDevicePubKey, + signature + ); + }, }); textsecure.AccountManager = AccountManager; })(); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 9c356df08..ee575edc0 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1022,6 +1022,49 @@ MessageReceiver.prototype.extend({ } return this.removeFromCache(envelope); }, + async handlePairingAuthorisationMessage(envelope, pairingAuthorisation) { + const { + type, + primaryDevicePubKey, + secondaryDevicePubKey, + signature, + } = pairingAuthorisation; + const sigArrayBuffer = dcodeIO.ByteBuffer.wrap(signature).toArrayBuffer(); + if ( + type === + textsecure.protobuf.PairingAuthorisationMessage.Type.PAIRING_REQUEST + ) { + window.log.info( + `Received pairing authorisation from ${primaryDevicePubKey}` + ); + let validAuthorisation = false; + try { + await libloki.crypto.verifyPairingAuthorisation( + primaryDevicePubKey, + secondaryDevicePubKey, + sigArrayBuffer, + type + ); + validAuthorisation = true; + } catch (e) { + window.log.error(e); + } + if (validAuthorisation) { + await libloki.storage.savePairingAuthorisation( + primaryDevicePubKey, + secondaryDevicePubKey, + sigArrayBuffer + ); + } else { + window.log.warn( + 'Could not verify pairing authorisation signature. Ignoring message.' + ); + } + } else { + window.log.warn('Unimplemented pairing authorisation message type'); + } + return this.removeFromCache(envelope); + }, handleDataMessage(envelope, msg) { if (!envelope.isP2p) { const timestamp = envelope.timestamp.toNumber(); @@ -1133,6 +1176,12 @@ MessageReceiver.prototype.extend({ content.lokiAddressMessage ); } + if (content.pairingAuthorisation) { + return this.handlePairingAuthorisationMessage( + envelope, + content.pairingAuthorisation + ); + } if (content.syncMessage) { return this.handleSyncMessage(envelope, content.syncMessage); } diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index ad12ee51a..3dbb22fd6 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -302,12 +302,13 @@ OutgoingMessage.prototype = { // Check if we need to attach the preKeys let sessionCipher; const isFriendRequest = this.messageType === 'friend-request'; + this.fallBackEncryption = this.fallBackEncryption || isFriendRequest; const flags = this.message.dataMessage ? this.message.dataMessage.get_flags() : null; const isEndSession = flags === textsecure.protobuf.DataMessage.Flags.END_SESSION; - if (isFriendRequest || isEndSession) { + if (this.fallBackEncryption || isEndSession) { // Encrypt them with the fallback const pkb = await libloki.storage.getPreKeyBundleForContact(number); const preKeyBundleMessage = new textsecure.protobuf.PreKeyBundleMessage( @@ -316,7 +317,7 @@ OutgoingMessage.prototype = { this.message.preKeyBundleMessage = preKeyBundleMessage; window.log.info('attaching prekeys to outgoing message'); } - if (isFriendRequest) { + if (this.fallBackEncryption) { sessionCipher = fallBackCipher; } else { sessionCipher = new libsignal.SessionCipher( diff --git a/protos/SignalService.proto b/protos/SignalService.proto index be745b285..35a64ff98 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -36,6 +36,7 @@ message Content { optional TypingMessage typingMessage = 6; optional PreKeyBundleMessage preKeyBundleMessage = 101; optional LokiAddressMessage lokiAddressMessage = 102; + optional PairingAuthorisationMessage pairingAuthorisation = 103; } message LokiAddressMessage { @@ -48,6 +49,17 @@ message LokiAddressMessage { optional Type type = 3; } +message PairingAuthorisationMessage { + enum Type { + PAIRING_REQUEST = 1; + UNPAIRING_REQUEST = 2; + } + optional string primaryDevicePubKey = 1; + optional string secondaryDevicePubKey = 2; + optional bytes signature = 3; + optional Type type = 4; +} + message PreKeyBundleMessage { optional bytes identityKey = 1; optional uint32 deviceId = 2; From 88c1454c185d3cb83f059acf153a978bfde781eb Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Wed, 7 Aug 2019 08:53:41 +1000 Subject: [PATCH 02/64] Use Loki Schema for database versioning --- app/sql.js | 102 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 79 insertions(+), 23 deletions(-) diff --git a/app/sql.js b/app/sql.js index f069c5159..7d151169f 100644 --- a/app/sql.js +++ b/app/sql.js @@ -739,28 +739,6 @@ async function updateToSchemaVersion11(currentVersion, instance) { console.log('updateToSchemaVersion11: success!'); } -async function updateToSchemaVersion12(currentVersion, instance) { - if (currentVersion >= 12) { - return; - } - console.log('updateToSchemaVersion12: starting...'); - await instance.run('BEGIN TRANSACTION;'); - - await instance.run( - `CREATE TABLE pairingAuthorisations( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - issuerPubKey VARCHAR(255), - secondaryDevicePubKey VARCHAR(255), - signature VARCHAR(255), - json TEXT - );` - ); - - await instance.run('PRAGMA schema_version = 12;'); - await instance.run('COMMIT TRANSACTION;'); - console.log('updateToSchemaVersion12: success!'); -} - const SCHEMA_VERSIONS = [ updateToSchemaVersion1, updateToSchemaVersion2, @@ -773,7 +751,6 @@ const SCHEMA_VERSIONS = [ updateToSchemaVersion9, updateToSchemaVersion10, updateToSchemaVersion11, - updateToSchemaVersion12, ]; async function updateSchema(instance) { @@ -795,6 +772,85 @@ async function updateSchema(instance) { // eslint-disable-next-line no-await-in-loop await runSchemaUpdate(schemaVersion, instance); } + await updateLokiSchema(instance); +} + +const LOKI_SCHEMA_VERSIONS = [ + updateToLokiSchemaVersion2, +]; + +async function updateToLokiSchemaVersion2(currentVersion, instance) { + if (currentVersion >= 2) { + return; + } + console.log('updateToLokiSchemaVersion2: starting...'); + await instance.run('BEGIN TRANSACTION;'); + + await instance.run( + `CREATE TABLE pairingAuthorisations( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + issuerPubKey VARCHAR(255), + secondaryDevicePubKey VARCHAR(255), + signature VARCHAR(255), + json TEXT + );` + ); + + await instance.run( + `INSERT INTO loki_schema ( + version + ) values ( + 2 + );` + ); + await instance.run('COMMIT TRANSACTION;'); + console.log('updateToLokiSchemaVersion2: success!'); +} + +async function updateLokiSchema(instance) { + const result = await instance.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema'"); + if (!result) { + await createLokiSchemaTable(instance); + } + const lokiSchemaVersion = await getLokiSchemaVersion(instance); + console.log( + 'updateLokiSchema:', + `Current loki schema version: ${lokiSchemaVersion};`, + `Most recent schema version: ${LOKI_SCHEMA_VERSIONS.length};` + ); + for (let index = 0, max = LOKI_SCHEMA_VERSIONS.length; index < max; index += 1) { + const runSchemaUpdate = LOKI_SCHEMA_VERSIONS[index]; + + // Yes, we really want to do this asynchronously, in order + // eslint-disable-next-line no-await-in-loop + await runSchemaUpdate(lokiSchemaVersion, instance); + } +} + +async function getLokiSchemaVersion(instance) { + const result = await instance.get('SELECT version FROM loki_schema WHERE version = (SELECT MAX(version) FROM loki_schema);'); + if (!result.version) { + return 0; + } + return result.version; +} + +async function createLokiSchemaTable(instance) { + await instance.run('BEGIN TRANSACTION;'); + await instance.run( + `CREATE TABLE loki_schema( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + version INTEGER + );` + ); + await instance.run( + `INSERT INTO loki_schema ( + version + ) values ( + 0 + );` + ); + await instance.run('COMMIT TRANSACTION;'); } let db; From bf8bb236833e7fe52f7be8988bef430cb372c843 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Wed, 7 Aug 2019 16:45:07 +1000 Subject: [PATCH 03/64] remove stale comment and simplify conversation creation --- libloki/api.js | 1 - libtextsecure/account_manager.js | 16 ++++------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/libloki/api.js b/libloki/api.js index ccc15ef12..9c1314285 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -77,7 +77,6 @@ pairingAuthorisation, }); const options = {}; - // Send a empty message with information about how to contact us directly const outgoingMessage = new textsecure.OutgoingMessage( null, // server Date.now(), // timestamp, diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 39806415d..7ad16c766 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -563,23 +563,15 @@ } // Validate pubKey - const c = new Whisper.Conversation({ - id: secondaryDevicePubKey, - type: 'private', - }); + const c = await ConversationController.getOrCreateAndWait( + secondaryDevicePubKey, + 'private' + ); const validationError = c.validateNumber(); if (validationError) { throw new Error('Invalid secondary device pubkey provided'); } // Ensure there is a conversation existing - try { - await ConversationController.getOrCreateAndWait( - secondaryDevicePubKey, - 'private' - ); - } catch (e) { - window.log.error(e); - } const signature = await libloki.crypto.generateSignatureForPairing( secondaryDevicePubKey, textsecure.protobuf.PairingAuthorisationMessage.Type.PAIRING_REQUEST From edd800d3825820dc6ce626491f796ef2ff2558a1 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Wed, 7 Aug 2019 17:41:17 +1000 Subject: [PATCH 04/64] Make linter happy --- app/sql.js | 23 ++++++++++++++++------- libtextsecure/account_manager.js | 1 - 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/sql.js b/app/sql.js index 7d151169f..8911ce294 100644 --- a/app/sql.js +++ b/app/sql.js @@ -775,9 +775,7 @@ async function updateSchema(instance) { await updateLokiSchema(instance); } -const LOKI_SCHEMA_VERSIONS = [ - updateToLokiSchemaVersion2, -]; +const LOKI_SCHEMA_VERSIONS = [updateToLokiSchemaVersion2]; async function updateToLokiSchemaVersion2(currentVersion, instance) { if (currentVersion >= 2) { @@ -808,7 +806,9 @@ async function updateToLokiSchemaVersion2(currentVersion, instance) { } async function updateLokiSchema(instance) { - const result = await instance.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema'"); + const result = await instance.get( + "SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema'" + ); if (!result) { await createLokiSchemaTable(instance); } @@ -818,7 +818,11 @@ async function updateLokiSchema(instance) { `Current loki schema version: ${lokiSchemaVersion};`, `Most recent schema version: ${LOKI_SCHEMA_VERSIONS.length};` ); - for (let index = 0, max = LOKI_SCHEMA_VERSIONS.length; index < max; index += 1) { + for ( + let index = 0, max = LOKI_SCHEMA_VERSIONS.length; + index < max; + index += 1 + ) { const runSchemaUpdate = LOKI_SCHEMA_VERSIONS[index]; // Yes, we really want to do this asynchronously, in order @@ -828,7 +832,9 @@ async function updateLokiSchema(instance) { } async function getLokiSchemaVersion(instance) { - const result = await instance.get('SELECT version FROM loki_schema WHERE version = (SELECT MAX(version) FROM loki_schema);'); + const result = await instance.get( + 'SELECT version FROM loki_schema WHERE version = (SELECT MAX(version) FROM loki_schema);' + ); if (!result.version) { return 0; } @@ -1237,7 +1243,10 @@ async function getPairingAuthorisation(issuerPubKey, secondaryDevicePubKey) { async function createOrUpdatePairingAuthorisation(data) { const { issuerPubKey, secondaryDevicePubKey, signature } = data; - const existing = await getPairingAuthorisation(issuerPubKey, secondaryDevicePubKey); + const existing = await getPairingAuthorisation( + issuerPubKey, + secondaryDevicePubKey + ); // prevent adding duplicate entries if (existing) { return; diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 7ad16c766..11a6b6d6f 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -3,7 +3,6 @@ textsecure, libsignal, libloki, - Whisper, mnemonic, btoa, Signal, From 713e9a3acfefee76b0191323af462f88ff0a58d4 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Fri, 23 Aug 2019 14:28:23 +1000 Subject: [PATCH 05/64] Fix pubkey validation --- libtextsecure/account_manager.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 11a6b6d6f..c0b46e26c 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -13,7 +13,8 @@ log, storage, Event, - ConversationController + ConversationController, + Whisper */ /* eslint-disable more/no-then */ @@ -555,22 +556,20 @@ this.dispatchEvent(new Event('registration')); }, async authoriseSecondaryDevice(secondaryDevicePubKey) { - if (secondaryDevicePubKey === textsecure.storage.user.getNumber()) { + const ourPubKey = textsecure.storage.user.getNumber(); + if (secondaryDevicePubKey === ourPubKey) { throw new Error( 'Cannot register primary device pubkey as secondary device' ); } - // Validate pubKey - const c = await ConversationController.getOrCreateAndWait( + // throws if invalid + this.validatePubKeyHex(secondaryDevicePubKey); + // we need a conversation for sending a message + await ConversationController.getOrCreateAndWait( secondaryDevicePubKey, 'private' ); - const validationError = c.validateNumber(); - if (validationError) { - throw new Error('Invalid secondary device pubkey provided'); - } - // Ensure there is a conversation existing const signature = await libloki.crypto.generateSignatureForPairing( secondaryDevicePubKey, textsecure.protobuf.PairingAuthorisationMessage.Type.PAIRING_REQUEST @@ -580,6 +579,16 @@ signature ); }, + validatePubKeyHex(pubKey) { + const c = new Whisper.Conversation({ + id: pubKey, + type: 'private', + }); + const validationError = c.validateNumber(); + if (validationError) { + throw new Error(validationError); + } + }, }); textsecure.AccountManager = AccountManager; })(); From 794e6416c881f74b693eb5fd0bac19dab8c87f31 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Fri, 23 Aug 2019 15:07:24 +1000 Subject: [PATCH 06/64] Add requestPairing high-level function --- libtextsecure/account_manager.js | 57 +++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index c0b46e26c..7684605d6 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -555,6 +555,35 @@ this.dispatchEvent(new Event('registration')); }, + async requestPairing(primaryDevicePubKey) { + // throws if invalid + this.validatePubKeyHex(primaryDevicePubKey); + // we need a conversation for sending a message + await ConversationController.getOrCreateAndWait( + primaryDevicePubKey, + 'private' + ); + const ourPubKey = textsecure.storage.user.getNumber(); + if (primaryDevicePubKey === ourPubKey) { + throw new Error('Cannot request to pair with ourselves'); + } + const requestType = + textsecure.protobuf.PairingAuthorisationMessage.Type.REQUEST; + const requestSignature = await libloki.crypto.generateSignatureForPairing( + primaryDevicePubKey, + requestType + ); + const authorisation = { + primaryDevicePubKey, + secondaryDevicePubKey: ourPubKey, + requestSignature, + type: requestType, + }; + await libloki.api.sendPairingAuthorisation( + authorisation, + primaryDevicePubKey + ); + }, async authoriseSecondaryDevice(secondaryDevicePubKey) { const ourPubKey = textsecure.storage.user.getNumber(); if (secondaryDevicePubKey === ourPubKey) { @@ -570,13 +599,33 @@ secondaryDevicePubKey, 'private' ); - const signature = await libloki.crypto.generateSignatureForPairing( + const grantType = + textsecure.protobuf.PairingAuthorisationMessage.Type.GRANT; + const grantSignature = await libloki.crypto.generateSignatureForPairing( secondaryDevicePubKey, - textsecure.protobuf.PairingAuthorisationMessage.Type.PAIRING_REQUEST + grantType ); - await libloki.api.sendPairingAuthorisation( + const existingAuthorisation = await libloki.storage.getAuthorisationForSecondaryPubKey( + secondaryDevicePubKey + ); + if (!existingAuthorisation) { + throw new Error( + 'authoriseSecondaryDevice: request signature missing from database!' + ); + } + const { requestSignature } = existingAuthorisation; + const authorisation = { + primaryDevicePubKey: ourPubKey, secondaryDevicePubKey, - signature + requestSignature, + grantSignature, + type: grantType, + }; + // Update authorisation in database with the new grant signature + await libloki.storage.savePairingAuthorisation(authorisation); + await libloki.api.sendPairingAuthorisation( + authorisation, + secondaryDevicePubKey ); }, validatePubKeyHex(pubKey) { From a4411007b05ab0338aced12609cb5b1f2fd0bf7e Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Fri, 23 Aug 2019 14:57:07 +1000 Subject: [PATCH 07/64] Pairing authorisations: refactor proto, change sql table, add getters --- app/sql.js | 60 +++++++++++++++++------------ js/modules/data.js | 63 ++++++++++++++++++++++++------ libloki/api.js | 79 +++++++++++++++++++++++++++++--------- libloki/crypto.js | 38 ++++++++++-------- libloki/storage.js | 47 ++++++++++++++--------- protos/SignalService.proto | 10 +++-- 6 files changed, 202 insertions(+), 95 deletions(-) diff --git a/app/sql.js b/app/sql.js index 8911ce294..5164b1f0b 100644 --- a/app/sql.js +++ b/app/sql.js @@ -73,6 +73,8 @@ module.exports = { removeAllContactSignedPreKeys, createOrUpdatePairingAuthorisation, + getAuthorisationForPubKey, + getSecondaryDevicesFor, createOrUpdateItem, getItemById, @@ -787,13 +789,17 @@ async function updateToLokiSchemaVersion2(currentVersion, instance) { await instance.run( `CREATE TABLE pairingAuthorisations( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - issuerPubKey VARCHAR(255), + primaryDevicePubKey VARCHAR(255), secondaryDevicePubKey VARCHAR(255), - signature VARCHAR(255), + isGranted BOOLEAN, json TEXT );` ); + await instance.run(`CREATE UNIQUE INDEX pairing_authorisations_secondary_device_pubkey ON pairingAuthorisations ( + secondaryDevicePubKey + );`); + await instance.run( `INSERT INTO loki_schema ( version @@ -1223,14 +1229,16 @@ async function removeAllSignedPreKeys() { } const PAIRING_AUTHORISATIONS_TABLE = 'pairingAuthorisations'; -async function getPairingAuthorisation(issuerPubKey, secondaryDevicePubKey) { +async function getAuthorisationForPubKey(pubKey, options) { + const granted = options && options.granted; + let filter = ''; + if (granted) { + filter = 'AND isGranted = 1'; + } const row = await db.get( - `SELECT * FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE - issuerPubKey = $issuerPubKey AND secondaryDevicePubKey = $secondaryDevicePubKey - LIMIT 1;`, + `SELECT json FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE secondaryDevicePubKey = $secondaryDevicePubKey ${filter};`, { - $issuerPubKey: issuerPubKey, - $secondaryDevicePubKey: secondaryDevicePubKey, + $secondaryDevicePubKey: pubKey, } ); @@ -1240,39 +1248,41 @@ async function getPairingAuthorisation(issuerPubKey, secondaryDevicePubKey) { return jsonToObject(row.json); } -async function createOrUpdatePairingAuthorisation(data) { - const { issuerPubKey, secondaryDevicePubKey, signature } = data; - const existing = await getPairingAuthorisation( - issuerPubKey, - secondaryDevicePubKey - ); - // prevent adding duplicate entries - if (existing) { - return; - } +async function createOrUpdatePairingAuthorisation(data) { + const { primaryDevicePubKey, secondaryDevicePubKey, grantSignature } = data; await db.run( - `INSERT INTO ${PAIRING_AUTHORISATIONS_TABLE} ( - issuerPubKey, + `INSERT OR REPLACE INTO ${PAIRING_AUTHORISATIONS_TABLE} ( + primaryDevicePubKey, secondaryDevicePubKey, - signature, + isGranted, json ) values ( - $issuerPubKey, + $primaryDevicePubKey, $secondaryDevicePubKey, - $signature, + $isGranted, $json )`, { - $issuerPubKey: issuerPubKey, + $primaryDevicePubKey: primaryDevicePubKey, $secondaryDevicePubKey: secondaryDevicePubKey, - $signature: signature, + $isGranted: Boolean(grantSignature), $json: objectToJSON(data), } ); } +async function getSecondaryDevicesFor(primaryDevicePubKey) { + const rows = await db.all( + `SELECT secondaryDevicePubKey FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE primaryDevicePubKey = $primaryDevicePubKey AND isGranted = 1 ORDER BY secondaryDevicePubKey ASC;`, + { + $primaryDevicePubKey: primaryDevicePubKey, + } + ); + return map(rows, row => row.secondaryDevicePubKey); +} + const ITEMS_TABLE = 'items'; async function createOrUpdateItem(data) { return createOrUpdate(ITEMS_TABLE, data); diff --git a/js/modules/data.js b/js/modules/data.js index 72d8485e8..c0418fb65 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -1,4 +1,4 @@ -/* global window, setTimeout, IDBKeyRange */ +/* global window, setTimeout, IDBKeyRange, dcodeIO */ const electron = require('electron'); @@ -90,6 +90,9 @@ module.exports = { removeAllContactSignedPreKeys, createOrUpdatePairingAuthorisation, + getGrantAuthorisationForPubKey, + getAuthorisationForPubKey, + getSecondaryDevicesFor, createOrUpdateItem, getItemById, @@ -573,21 +576,57 @@ async function removeAllContactSignedPreKeys() { await channels.removeAllContactSignedPreKeys(); } -async function createOrUpdatePairingAuthorisation(data) { - let sig; - if (isArrayBuffer(data.signature)) { - sig = arrayBufferToBase64(data.signature); - } else if (typeof signature === 'string') { - sig = data.signature; - } else { - throw new Error( - 'Invalid signature provided in createOrUpdatePairingAuthorisation. Needs to be either ArrayBuffer or string.' - ); +function signatureToBase64(signature) { + if (signature.constructor === dcodeIO.ByteBuffer) { + return dcodeIO.ByteBuffer.wrap(signature).toString('base64'); + } else if (isArrayBuffer(signature)) { + return arrayBufferToBase64(signature); } + throw new Error( + 'Invalid signature provided in createOrUpdatePairingAuthorisation. Needs to be either ArrayBuffer or ByteBuffer.' + ); +} + +async function createOrUpdatePairingAuthorisation(data) { + const { requestSignature, grantSignature } = data; + return channels.createOrUpdatePairingAuthorisation({ ...data, - signature: sig, + requestSignature: signatureToBase64(requestSignature), + grantSignature: grantSignature ? signatureToBase64(grantSignature) : null, + }); +} + +async function getGrantAuthorisationForPubKey(pubKey) { + const authorisation = await channels.getAuthorisationForPubKey(pubKey, { + granted: true, }); + if (!authorisation) { + return null; + } + return { + ...authorisation, + requestSignature: base64ToArrayBuffer(authorisation.requestSignature), + grantSignature: base64ToArrayBuffer(authorisation.grantSignature), + }; +} + +async function getAuthorisationForPubKey(pubKey) { + const authorisation = await channels.getAuthorisationForPubKey(pubKey); + if (!authorisation) { + return null; + } + return { + ...authorisation, + requestSignature: base64ToArrayBuffer(authorisation.requestSignature), + grantSignature: authorisation.grantSignature + ? base64ToArrayBuffer(authorisation.grantSignature) + : null, + }; +} + +function getSecondaryDevicesFor(primareyDevicePubKey) { + return channels.getSecondaryDevicesFor(primareyDevicePubKey); } // Items diff --git a/libloki/api.js b/libloki/api.js index 9c1314285..071fc97fd 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -63,30 +63,70 @@ await outgoingMessage.sendToNumber(pubKey); } - async function sendPairingAuthorisation(secondaryDevicePubKey, signature) { - const pairingAuthorisation = new textsecure.protobuf.PairingAuthorisationMessage( - { - signature, - primaryDevicePubKey: textsecure.storage.user.getNumber(), - secondaryDevicePubKey, - type: - textsecure.protobuf.PairingAuthorisationMessage.Type.PAIRING_REQUEST, - } + function createPairingAuthorisationProtoMessage({ + primaryDevicePubKey, + secondaryDevicePubKey, + requestSignature, + grantSignature, + type, + }) { + if ( + !primaryDevicePubKey || + !secondaryDevicePubKey || + type === undefined || + type === null + ) { + throw new Error( + 'createPairingAuthorisationProtoMessage: pubkeys or type is not set' + ); + } + if (requestSignature.constructor !== ArrayBuffer) { + throw new Error( + 'createPairingAuthorisationProtoMessage expects a signature as ArrayBuffer' + ); + } + if (grantSignature && grantSignature.constructor !== ArrayBuffer) { + throw new Error( + 'createPairingAuthorisationProtoMessage expects a signature as ArrayBuffer' + ); + } + return new textsecure.protobuf.PairingAuthorisationMessage({ + requestSignature: new Uint8Array(requestSignature), + grantSignature: grantSignature ? new Uint8Array(grantSignature) : null, + primaryDevicePubKey, + secondaryDevicePubKey, + type, + }); + } + + async function sendPairingAuthorisation(authorisation, recipientPubKey) { + const pairingAuthorisation = createPairingAuthorisationProtoMessage( + authorisation ); const content = new textsecure.protobuf.Content({ pairingAuthorisation, }); const options = {}; - const outgoingMessage = new textsecure.OutgoingMessage( - null, // server - Date.now(), // timestamp, - [secondaryDevicePubKey], // numbers - content, // message - true, // silent - () => null, // callback - options - ); - await outgoingMessage.sendToNumber(secondaryDevicePubKey); + const p = new Promise((resolve, reject) => { + const outgoingMessage = new textsecure.OutgoingMessage( + null, // server + Date.now(), // timestamp, + [recipientPubKey], // numbers + content, // message + true, // silent + result => { + // callback + if (result.errors.length > 0) { + reject(result.errors[0]); + } else { + resolve(); + } + }, + options + ); + outgoingMessage.sendToNumber(recipientPubKey); + }); + return p; } window.libloki.api = { @@ -94,5 +134,6 @@ sendOnlineBroadcastMessage, broadcastOnlineStatus, sendPairingAuthorisation, + createPairingAuthorisationProtoMessage, }; })(); diff --git a/libloki/crypto.js b/libloki/crypto.js index b5201183b..5159562fd 100644 --- a/libloki/crypto.js +++ b/libloki/crypto.js @@ -171,32 +171,38 @@ myKeyPair.privKey, data.buffer ); - return new Uint8Array(signature); + return signature; } async function verifyPairingAuthorisation( - issuerPubKey, + primaryDevicePubKey, secondaryPubKey, signature, type ) { - const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair(); - if (StringView.arrayBufferToHex(myKeyPair.pubKey) !== secondaryPubKey) { - throw new Error( - 'Invalid pairing authorisation: we are not the recipient of the authorisation!' - ); - } - const len = myKeyPair.pubKey.byteLength; + const secondaryPubKeyArrayBuffer = StringView.hexToArrayBuffer( + secondaryPubKey + ); + const primaryDevicePubKeyArrayBuffer = StringView.hexToArrayBuffer( + primaryDevicePubKey + ); + const len = secondaryPubKeyArrayBuffer.byteLength; const data = new Uint8Array(len + 1); - data.set(new Uint8Array(myKeyPair.pubKey), 0); + // For REQUEST type message, the secondary device signs the primary device pubkey + // For GRANT type message, the primary device signs the secondary device pubkey + let issuer; + if (type === textsecure.protobuf.PairingAuthorisationMessage.Type.GRANT) { + data.set(new Uint8Array(secondaryPubKeyArrayBuffer)); + issuer = primaryDevicePubKeyArrayBuffer; + } else if ( + type === textsecure.protobuf.PairingAuthorisationMessage.Type.REQUEST + ) { + data.set(new Uint8Array(primaryDevicePubKeyArrayBuffer)); + issuer = secondaryPubKeyArrayBuffer; + } data[len] = type; - const issuerPubKeyArrayBuffer = StringView.hexToArrayBuffer(issuerPubKey); // Throws for invalid signature - await libsignal.Curve.async.verifySignature( - issuerPubKeyArrayBuffer, - data.buffer, - signature - ); + await libsignal.Curve.async.verifySignature(issuer, data.buffer, signature); } const snodeCipher = new LokiSnodeChannel(); diff --git a/libloki/storage.js b/libloki/storage.js index 81be176b4..556133896 100644 --- a/libloki/storage.js +++ b/libloki/storage.js @@ -113,16 +113,33 @@ } } - async function savePairingAuthorisation( - issuerPubKey, + function savePairingAuthorisation({ + primaryDevicePubKey, secondaryDevicePubKey, - signature - ) { - return textsecure.storage.protocol.storePairingAuthorisation( - issuerPubKey, + requestSignature, + grantSignature, + }) { + return window.Signal.Data.createOrUpdatePairingAuthorisation({ + primaryDevicePubKey, secondaryDevicePubKey, - signature - ); + requestSignature, + grantSignature, + }); + } + + function getGrantAuthorisationForSecondaryPubKey(secondaryPubKey) { + return window.Signal.Data.getGrantAuthorisationForPubKey(secondaryPubKey); + } + + function getAuthorisationForSecondaryPubKey(secondaryPubKey) { + return window.Signal.Data.getAuthorisationForPubKey(secondaryPubKey); + } + + async function getAllDevicePubKeysForPrimaryPubKey(primaryDevicePubKey) { + const secondaryPubKeys = + (await window.Signal.Data.getSecondaryDevicesFor(primaryDevicePubKey)) || + []; + return secondaryPubKeys.concat(primaryDevicePubKey); } window.libloki.storage = { @@ -131,6 +148,9 @@ removeContactPreKeyBundle, verifyFriendRequestAcceptPreKey, savePairingAuthorisation, + getGrantAuthorisationForSecondaryPubKey, + getAuthorisationForSecondaryPubKey, + getAllDevicePubKeysForPrimaryPubKey, }; // Libloki protocol store @@ -256,15 +276,4 @@ store.clearContactSignedPreKeysStore = async () => { await window.Signal.Data.removeAllContactSignedPreKeys(); }; - - store.storePairingAuthorisation = ( - issuerPubKey, - secondaryDevicePubKey, - signature - ) => - window.Signal.Data.createOrUpdatePairingAuthorisation({ - issuerPubKey, - secondaryDevicePubKey, - signature, - }); })(); diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 35a64ff98..439166e8e 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -51,13 +51,15 @@ message LokiAddressMessage { message PairingAuthorisationMessage { enum Type { - PAIRING_REQUEST = 1; - UNPAIRING_REQUEST = 2; + REQUEST = 1; + GRANT = 2; + REVOKE = 3; } optional string primaryDevicePubKey = 1; optional string secondaryDevicePubKey = 2; - optional bytes signature = 3; - optional Type type = 4; + optional bytes requestSignature = 3; + optional bytes grantSignature = 4; + optional Type type = 5; } message PreKeyBundleMessage { From 6ab05e28dfdb34f9ff75dcb71b80f61a78a685e2 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Fri, 23 Aug 2019 16:09:18 +1000 Subject: [PATCH 08/64] Handle incoming pairing authorisation message --- libtextsecure/message_receiver.js | 133 +++++++++++++++++++++++++----- 1 file changed, 112 insertions(+), 21 deletions(-) diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index ee575edc0..632f73a15 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -16,6 +16,7 @@ /* global localServerPort: false */ /* global lokiMessageAPI: false */ /* global lokiP2pAPI: false */ +/* global Whisper: false */ /* eslint-disable more/no-then */ /* eslint-disable no-unreachable */ @@ -1022,47 +1023,137 @@ MessageReceiver.prototype.extend({ } return this.removeFromCache(envelope); }, - async handlePairingAuthorisationMessage(envelope, pairingAuthorisation) { + async validateAuthorisation(authorisation) { const { type, primaryDevicePubKey, secondaryDevicePubKey, - signature, - } = pairingAuthorisation; - const sigArrayBuffer = dcodeIO.ByteBuffer.wrap(signature).toArrayBuffer(); - if ( - type === - textsecure.protobuf.PairingAuthorisationMessage.Type.PAIRING_REQUEST - ) { - window.log.info( - `Received pairing authorisation from ${primaryDevicePubKey}` + requestSignature, + grantSignature, + } = authorisation; + const alreadySecondaryDevice = !!window.storage.get('isSecondaryDevice'); + const ourPubKey = textsecure.storage.user.getNumber(); + const isRequest = + type === textsecure.protobuf.PairingAuthorisationMessage.Type.REQUEST; + const isGrant = + type === textsecure.protobuf.PairingAuthorisationMessage.Type.GRANT; + if (!primaryDevicePubKey || !secondaryDevicePubKey) { + window.log.warn( + 'Received a pairing request with missing pubkeys. Ignored.' + ); + return false; + } else if (!requestSignature) { + window.log.warn( + 'Received a pairing request with missing request signature. Ignored.' + ); + return false; + } else if (isRequest && alreadySecondaryDevice) { + window.log.warn( + 'Received a pairing request while being a secondary device. Ignored.' + ); + return false; + } else if (isRequest && authorisation.primaryDevicePubKey !== ourPubKey) { + window.log.warn( + 'Received a pairing request addressed to another pubkey. Ignored.' + ); + return false; + } else if (authorisation.secondaryDevicePubKey === ourPubKey) { + window.log.warn('Received a pairing request from ourselves. Ignored.'); + return false; + } + try { + await libloki.crypto.verifyPairingAuthorisation( + primaryDevicePubKey, + secondaryDevicePubKey, + dcodeIO.ByteBuffer.wrap(requestSignature).toArrayBuffer(), + type + ); + } catch (e) { + window.log.warn( + 'Could not verify pairing request authorisation signature. Ignoring message.' ); - let validAuthorisation = false; + window.log.error(e); + return false; + } + if (isGrant) { try { await libloki.crypto.verifyPairingAuthorisation( primaryDevicePubKey, secondaryDevicePubKey, - sigArrayBuffer, + dcodeIO.ByteBuffer.wrap(grantSignature).toArrayBuffer(), type ); - validAuthorisation = true; } catch (e) { + window.log.warn( + 'Could not verify pairing grant authorisation signature. Ignoring message.' + ); window.log.error(e); + return false; } - if (validAuthorisation) { - await libloki.storage.savePairingAuthorisation( - primaryDevicePubKey, - secondaryDevicePubKey, - sigArrayBuffer - ); - } else { + } + return true; + }, + async handlePairingRequest(pairingRequest) { + if (!this.validateAuthorisation(pairingRequest)) { + return; + } + window.libloki.storage.savePairingAuthorisation(pairingRequest); + Whisper.events.trigger( + 'devicePairingRequestReceived', + pairingRequest.secondaryDevicePubKey + ); + }, + async handleAuthorisationForSelf(pairingAuthorisation) { + if (!this.validateAuthorisation(pairingAuthorisation)) { + return; + } + const { type, primaryDevicePubKey } = pairingAuthorisation; + if (type === textsecure.protobuf.PairingAuthorisationMessage.Type.GRANT) { + // Authorisation received to become a secondary device + window.log.info( + `Received pairing authorisation from ${primaryDevicePubKey}` + ); + const alreadySecondaryDevice = window.storage.get('isSecondaryDevice'); + if (alreadySecondaryDevice) { window.log.warn( - 'Could not verify pairing authorisation signature. Ignoring message.' + 'Received an unexpected pairing authorisation (device is already paired as secondary device). Ignoring.' ); + return; } + await libloki.storage.savePairingAuthorisation(pairingAuthorisation); + // Set current device as secondary. + // This will ensure the authorisation is sent + // along with each friend request. + window.storage.remove('secondaryDeviceStatus'); + window.storage.put('isSecondaryDevice', true); + Whisper.events.trigger('secondaryDeviceRegistration'); } else { window.log.warn('Unimplemented pairing authorisation message type'); } + }, + async handleAuthorisationForContact(pairingAuthorisation) { + if (!this.validateAuthorisation(pairingAuthorisation)) { + return; + } + const { primaryDevicePubKey, secondaryDevicePubKey } = pairingAuthorisation; + // ensure the primary device is a friend + const c = window.ConversationController.get(primaryDevicePubKey); + if (!c || !c.isFriend()) { + return; + } + await libloki.storage.savePairingAuthorisation(pairingAuthorisation); + // send friend accept? + window.libloki.api.sendBackgroundMessage(secondaryDevicePubKey); + }, + async handlePairingAuthorisationMessage(envelope, pairingAuthorisation) { + const { type, secondaryDevicePubKey } = pairingAuthorisation; + if (type === textsecure.protobuf.PairingAuthorisationMessage.Type.REQUEST) { + await this.handlePairingRequest(pairingAuthorisation); + } else if (secondaryDevicePubKey === textsecure.storage.user.getNumber()) { + await this.handleAuthorisationForSelf(pairingAuthorisation); + } else { + await this.handleAuthorisationForContact(pairingAuthorisation); + } return this.removeFromCache(envelope); }, handleDataMessage(envelope, msg) { From 632e35c8a544a791f2a82d611b3b9188057767ce Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Fri, 23 Aug 2019 16:41:16 +1000 Subject: [PATCH 09/64] Secondary device registration UI --- background.html | 48 ++++++++++++++++-------- js/views/standalone_registration_view.js | 35 +++++++++++++++++ 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/background.html b/background.html index f0a891ea0..ea02be8c1 100644 --- a/background.html +++ b/background.html @@ -574,21 +574,7 @@
-

Restore using seed

-
-
- -
-
-
- Language: -
- -
-
- Restore -
-
+

Register a new account

@@ -609,6 +595,38 @@ Register
+ +

Restore using seed

+
+
+ +
+
+
+ Language: +
+ +
+
+ Restore +
+
+ +

Link device to an existing account

+
+

+ You will need your Primary Device handy during this step. +
+ Open the Loki Messenger App on your Primary Device +
+ and select "Pair New Device" in the main menu. +

+
+ +
+
+ Link +
diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index 027bc0815..4ebc9bac2 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -26,6 +26,7 @@ this.$('#error').hide(); this.$('.standalone-mnemonic').hide(); + this.$('.standalone-secondary-device').hide(); this.onGenerateMnemonic(); @@ -49,6 +50,7 @@ this.registrationParams = {}; this.$pages = this.$('.page'); + this.pairingTimeout = null; this.showRegisterPage(); this.onValidatePassword(); @@ -60,6 +62,7 @@ 'change #code': 'onChangeCode', 'click #register': 'registerWithoutMnemonic', 'click #register-mnemonic': 'registerWithMnemonic', + 'click #register-secondary-device': 'registerSecondaryDevice', 'click #back-button': 'onBack', 'click #save-button': 'onSaveProfile', 'change #mnemonic': 'onChangeMnemonic', @@ -123,6 +126,38 @@ const language = this.$('#mnemonic-display-language').val(); this.showProfilePage(mnemonic, language); }, + async registerSecondaryDevice() { + const mnemonic = this.$('#mnemonic-display').text(); + const language = this.$('#mnemonic-display-language').val(); + const primaryPubKey = this.$('#primary-pubkey').val(); + this.$('.standalone-secondary-device #error').hide(); + Whisper.events.on('secondaryDeviceRegistration', () => { + // Ensure the left menu is updated + Whisper.events.trigger('userChanged', { isSecondaryDevice: true }); + this.$el.trigger('openInbox'); + }); + clearTimeout(this.pairingTimeout); + this.pairingTimeout = setTimeout(() => { + this.$('.standalone-secondary-device #error').text( + 'The primary device has not responded within 1 minute. Ensure it is connected.' + ); + this.$('.standalone-secondary-device #error').show(); + }, 60000); + textsecure.storage.put('secondaryDeviceStatus', 'ongoing'); + try { + await this.accountManager.registerSingleDevice( + mnemonic, + language, + 'John Smith' + ); + await this.accountManager.requestPairing(primaryPubKey); + } catch (e) { + textsecure.storage.remove('secondaryDeviceStatus'); + this.$('.standalone-secondary-device #error').text(e); + this.$('.standalone-secondary-device #error').show(); + clearTimeout(this.pairingTimeout); + } + }, registerWithMnemonic() { const mnemonic = this.$('#mnemonic').val(); const language = this.$('#mnemonic-language').val(); From b5aee4f4be1874fc166621b0669391a7ea95bb9d Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Fri, 23 Aug 2019 16:51:52 +1000 Subject: [PATCH 10/64] Display "Pair New Device" in menu for non-secondary devices --- js/background.js | 1 + js/views/inbox_view.js | 1 + ts/components/MainHeader.tsx | 18 ++++++++++++++++-- ts/state/ducks/user.ts | 4 ++++ ts/state/selectors/user.ts | 5 +++++ ts/state/smart/MainHeader.tsx | 8 +++++++- 6 files changed, 34 insertions(+), 3 deletions(-) diff --git a/js/background.js b/js/background.js index 3cd81dcbd..29dbf195e 100644 --- a/js/background.js +++ b/js/background.js @@ -181,6 +181,7 @@ const user = { regionCode: window.storage.get('regionCode'), ourNumber: textsecure.storage.user.getNumber(), + isSecondaryDevice: !!textsecure.storage.get('isSecondaryDevice'), }; Whisper.events.trigger('userChanged', user); diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index e1bb7ef69..c4f1dc7ff 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -156,6 +156,7 @@ user: { regionCode: window.storage.get('regionCode'), ourNumber: textsecure.storage.user.getNumber(), + isSecondaryDevice: !!window.storage.get('isSecondaryDevice'), i18n: window.i18n, }, }; diff --git a/ts/components/MainHeader.tsx b/ts/components/MainHeader.tsx index 946077894..47cbac33d 100644 --- a/ts/components/MainHeader.tsx +++ b/ts/components/MainHeader.tsx @@ -37,6 +37,7 @@ export interface Props { verified: boolean; profileName?: string; avatarPath?: string; + isSecondaryDevice?: boolean; i18n: LocalizerType; updateSearchTerm: (searchTerm: string) => void; @@ -98,7 +99,10 @@ export class MainHeader extends React.Component { } public componentDidUpdate(_prevProps: Props, prevState: any) { - if (prevState.hasPass !== this.state.hasPass) { + if ( + prevState.hasPass !== this.state.hasPass || + _prevProps.isSecondaryDevice !== this.props.isSecondaryDevice + ) { this.updateMenuItems(); } } @@ -304,7 +308,7 @@ export class MainHeader extends React.Component { } private updateMenuItems() { - const { i18n, onCopyPublicKey } = this.props; + const { i18n, onCopyPublicKey, isSecondaryDevice } = this.props; const { hasPass } = this.state; const menuItems = [ @@ -351,6 +355,16 @@ export class MainHeader extends React.Component { menuItems.push(passItem('set')); } + if (!isSecondaryDevice) { + menuItems.push({ + id: 'pairNewDevice', + name: 'Pair new Device', + onClick: () => { + trigger('showDevicePairingDialog'); + }, + }); + } + this.setState({ menuItems }); } } diff --git a/ts/state/ducks/user.ts b/ts/state/ducks/user.ts index 4123170cd..c49cf6386 100644 --- a/ts/state/ducks/user.ts +++ b/ts/state/ducks/user.ts @@ -5,6 +5,7 @@ import { LocalizerType } from '../../types/Util'; export type UserStateType = { ourNumber: string; regionCode: string; + isSecondaryDevice: boolean; i18n: LocalizerType; }; @@ -15,6 +16,7 @@ type UserChangedActionType = { payload: { ourNumber: string; regionCode: string; + isSecondaryDevice: boolean; }; }; @@ -29,6 +31,7 @@ export const actions = { function userChanged(attributes: { ourNumber: string; regionCode: string; + isSecondaryDevice: boolean; }): UserChangedActionType { return { type: 'USER_CHANGED', @@ -42,6 +45,7 @@ function getEmptyState(): UserStateType { return { ourNumber: 'missing', regionCode: 'missing', + isSecondaryDevice: false, i18n: () => 'missing', }; } diff --git a/ts/state/selectors/user.ts b/ts/state/selectors/user.ts index 67478ab9f..6963ad8a3 100644 --- a/ts/state/selectors/user.ts +++ b/ts/state/selectors/user.ts @@ -21,3 +21,8 @@ export const getIntl = createSelector( getUser, (state: UserStateType): LocalizerType => state.i18n ); + +export const getIsSecondaryDevice = createSelector( + getUser, + (state: UserStateType): boolean => state.isSecondaryDevice +); diff --git a/ts/state/smart/MainHeader.tsx b/ts/state/smart/MainHeader.tsx index e9db0c481..1c3a8e8cc 100644 --- a/ts/state/smart/MainHeader.tsx +++ b/ts/state/smart/MainHeader.tsx @@ -5,7 +5,12 @@ import { MainHeader } from '../../components/MainHeader'; import { StateType } from '../reducer'; import { getQuery } from '../selectors/search'; -import { getIntl, getRegionCode, getUserNumber } from '../selectors/user'; +import { + getIntl, + getIsSecondaryDevice, + getRegionCode, + getUserNumber, +} from '../selectors/user'; import { getMe } from '../selectors/conversations'; const mapStateToProps = (state: StateType) => { @@ -13,6 +18,7 @@ const mapStateToProps = (state: StateType) => { searchTerm: getQuery(state), regionCode: getRegionCode(state), ourNumber: getUserNumber(state), + isSecondaryDevice: getIsSecondaryDevice(state), ...getMe(state), i18n: getIntl(state), }; From ac4038e86ace5734893b4bfb55ef4ebb4fd10af6 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Mon, 26 Aug 2019 10:33:21 +1000 Subject: [PATCH 11/64] Dialogs for device pairing requests - Primary device side --- background.html | 35 ++++++++++ js/views/app_view.js | 10 +++ js/views/device_pairing_dialog_view.js | 91 ++++++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 js/views/device_pairing_dialog_view.js diff --git a/background.html b/background.html index f0a891ea0..ce87cf769 100644 --- a/background.html +++ b/background.html @@ -245,6 +245,40 @@ + + + diff --git a/js/views/app_view.js b/js/views/app_view.js index f9f306789..364e758e3 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -196,5 +196,15 @@ const dialog = new Whisper.SeedDialogView({ seed }); this.el.append(dialog.el); }, + showDevicePairingDialog() { + const dialog = new Whisper.DevicePairingDialogView(); + Whisper.events.on('devicePairingRequestReceived', pubKey => + dialog.requestReceived(pubKey) + ); + dialog.on('devicePairingRequestAccepted', (pubKey, cb) => + Whisper.events.trigger('devicePairingRequestAccepted', pubKey, cb) + ); + this.el.append(dialog.el); + }, }); })(); diff --git a/js/views/device_pairing_dialog_view.js b/js/views/device_pairing_dialog_view.js new file mode 100644 index 000000000..2e18ab1f4 --- /dev/null +++ b/js/views/device_pairing_dialog_view.js @@ -0,0 +1,91 @@ +/* global Whisper, i18n */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + + Whisper.DevicePairingDialogView = Whisper.View.extend({ + className: 'loki-dialog device-pairing-dialog modal', + templateName: 'device-pairing-dialog', + initialize() { + this.pubKeyRequests = []; + this.pubKey = null; + this.accepted = false; + this.view = ''; + this.render(); + this.showView(); + }, + events: { + 'click .waitingForRequestView .cancel': 'close', + 'click .requestReceivedView .skip': 'skipDevice', + 'click #allowPairing': 'allowDevice', + 'click .requestAcceptedView .ok': 'close', + }, + render_attributes() { + return { + waitingForRequestTitle: 'Waiting for device to register...', + requestReceivedTitle: 'Device Pairing Received', + requestAcceptedTitle: 'Device Pairing Accepted', + cancelText: i18n('cancel'), + skipText: 'Skip', + okText: i18n('ok'), + allowPairingText: 'Allow Pairing', + }; + }, + requestReceived(secondaryDevicePubKey) { + // FIFO: push at the front of the array with unshift() + this.pubKeyRequests.unshift(secondaryDevicePubKey); + if (!this.pubKey) { + this.nextPubKey(); + this.showView('requestReceived'); + } + }, + allowDevice() { + this.accepted = true; + this.trigger('devicePairingRequestAccepted', this.pubKey, errors => + this.transmisssionCB(errors) + ); + this.showView(); + }, + transmisssionCB(errors) { + if (!errors) { + this.$('.transmissionStatus').text('Sent successfully'); + } else { + this.$('.transmissionStatus').text(errors); + } + this.$('.requestAcceptedView .ok').show(); + }, + skipDevice() { + this.nextPubKey(); + this.showView(); + }, + nextPubKey() { + // FIFO: pop at the back of the array using pop() + this.pubKey = this.pubKeyRequests.pop(); + }, + showView() { + const waitingForRequestView = this.$('.waitingForRequestView'); + const requestReceivedView = this.$('.requestReceivedView'); + const requestAcceptedView = this.$('.requestAcceptedView'); + if (this.accepted) { + requestReceivedView.hide(); + waitingForRequestView.hide(); + requestAcceptedView.show(); + } else if (this.pubKey) { + this.$('.secondaryPubKey').text(this.pubKey); + requestReceivedView.show(); + waitingForRequestView.hide(); + requestAcceptedView.hide(); + } else { + waitingForRequestView.show(); + requestReceivedView.hide(); + requestAcceptedView.hide(); + } + }, + close() { + this.remove(); + }, + }); +})(); From a2463f74c3ce6af007de023e61b876f7d5316edc Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Mon, 26 Aug 2019 10:45:38 +1000 Subject: [PATCH 12/64] Improve object validation in createPairingAuthorisationProtoMessage --- libloki/api.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libloki/api.js b/libloki/api.js index 071fc97fd..a07c37859 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -73,8 +73,8 @@ if ( !primaryDevicePubKey || !secondaryDevicePubKey || - type === undefined || - type === null + !requestSignature || + typeof type !== 'number' ) { throw new Error( 'createPairingAuthorisationProtoMessage: pubkeys or type is not set' From 4a35492fe034212be905da82c6224d10739d2fae Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Mon, 26 Aug 2019 11:16:30 +1000 Subject: [PATCH 13/64] Fix main header being redrawn every 100ms --- ts/components/MainHeader.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ts/components/MainHeader.tsx b/ts/components/MainHeader.tsx index 946077894..727145c2c 100644 --- a/ts/components/MainHeader.tsx +++ b/ts/components/MainHeader.tsx @@ -88,7 +88,9 @@ export class MainHeader extends React.Component { setInterval(() => { const clipboardText = clipboard.readText(); - this.setState({ clipboardText }); + if (this.state.clipboardText !== clipboardText) { + this.setState({ clipboardText }); + } }, 100); } From c7dc79e7f9705c2d2a1a766fe701707cf9119c9a Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Mon, 26 Aug 2019 15:00:41 +1000 Subject: [PATCH 14/64] Fix missing awaits! --- libtextsecure/message_receiver.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 632f73a15..100958825 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1094,17 +1094,19 @@ MessageReceiver.prototype.extend({ return true; }, async handlePairingRequest(pairingRequest) { - if (!this.validateAuthorisation(pairingRequest)) { + const valid = await this.validateAuthorisation(pairingRequest); + if (!valid) { return; } - window.libloki.storage.savePairingAuthorisation(pairingRequest); + await window.libloki.storage.savePairingAuthorisation(pairingRequest); Whisper.events.trigger( 'devicePairingRequestReceived', pairingRequest.secondaryDevicePubKey ); }, async handleAuthorisationForSelf(pairingAuthorisation) { - if (!this.validateAuthorisation(pairingAuthorisation)) { + const valid = await this.validateAuthorisation(pairingAuthorisation); + if (!valid) { return; } const { type, primaryDevicePubKey } = pairingAuthorisation; @@ -1132,7 +1134,8 @@ MessageReceiver.prototype.extend({ } }, async handleAuthorisationForContact(pairingAuthorisation) { - if (!this.validateAuthorisation(pairingAuthorisation)) { + const valid = this.validateAuthorisation(pairingAuthorisation); + if (!valid) { return; } const { primaryDevicePubKey, secondaryDevicePubKey } = pairingAuthorisation; From 8ef54890b2c9d722fb008256fae8c572f5df92a4 Mon Sep 17 00:00:00 2001 From: sachaaaaa <40749766+sachaaaaa@users.noreply.github.com> Date: Mon, 26 Aug 2019 15:17:51 +1000 Subject: [PATCH 15/64] Update libtextsecure/message_receiver.js Co-Authored-By: Mikunj Varsani --- libtextsecure/message_receiver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 100958825..9dce8934a 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1134,7 +1134,7 @@ MessageReceiver.prototype.extend({ } }, async handleAuthorisationForContact(pairingAuthorisation) { - const valid = this.validateAuthorisation(pairingAuthorisation); + const valid = await this.validateAuthorisation(pairingAuthorisation); if (!valid) { return; } From 2754969b233a52e3c0ec997a914b503f399be59a Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Mon, 26 Aug 2019 15:17:37 +1000 Subject: [PATCH 16/64] Ensure one listener and one event for 'secondaryDeviceRegistration' --- js/views/standalone_registration_view.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index 4ebc9bac2..c3262cecb 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -126,16 +126,25 @@ const language = this.$('#mnemonic-display-language').val(); this.showProfilePage(mnemonic, language); }, + onSecondaryDeviceRegistered() { + // Ensure the left menu is updated + Whisper.events.trigger('userChanged', { isSecondaryDevice: true }); + this.$el.trigger('openInbox'); + }, async registerSecondaryDevice() { const mnemonic = this.$('#mnemonic-display').text(); const language = this.$('#mnemonic-display-language').val(); const primaryPubKey = this.$('#primary-pubkey').val(); this.$('.standalone-secondary-device #error').hide(); - Whisper.events.on('secondaryDeviceRegistration', () => { - // Ensure the left menu is updated - Whisper.events.trigger('userChanged', { isSecondaryDevice: true }); - this.$el.trigger('openInbox'); - }); + // Ensure only one listener + Whisper.events.off( + 'secondaryDeviceRegistration', + this.onSecondaryDeviceRegistered + ); + Whisper.events.once( + 'secondaryDeviceRegistration', + this.onSecondaryDeviceRegistered + ); clearTimeout(this.pairingTimeout); this.pairingTimeout = setTimeout(() => { this.$('.standalone-secondary-device #error').text( From 79d842635da7317f20051b342436140c724df2f9 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Mon, 26 Aug 2019 15:45:56 +1000 Subject: [PATCH 17/64] fix events registration issues --- js/views/app_view.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/js/views/app_view.js b/js/views/app_view.js index 364e758e3..b23ce8223 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -198,10 +198,13 @@ }, showDevicePairingDialog() { const dialog = new Whisper.DevicePairingDialogView(); + // remove all listeners for this events is fine since the + // only listener is right below. + Whisper.events.off('devicePairingRequestReceived'); Whisper.events.on('devicePairingRequestReceived', pubKey => dialog.requestReceived(pubKey) ); - dialog.on('devicePairingRequestAccepted', (pubKey, cb) => + dialog.once('devicePairingRequestAccepted', (pubKey, cb) => Whisper.events.trigger('devicePairingRequestAccepted', pubKey, cb) ); this.el.append(dialog.el); From 4dd62a265cb7f3739b23759e415d2d8a5c4c470f Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Tue, 27 Aug 2019 13:50:39 +1000 Subject: [PATCH 18/64] Allow receiving and sending messages before lokiP2PApi is defined. --- js/models/conversations.js | 2 +- js/modules/loki_message_api.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 891a345d5..15d265021 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -167,7 +167,7 @@ if (this.id === this.ourNumber) { this.set({ friendRequestStatus: FriendRequestStatusEnum.friends }); - } else if (lokiP2pAPI) { + } else if (typeof lokiP2pAPI !== 'undefined') { // Online status handling, only for contacts that aren't us this.set({ isOnline: lokiP2pAPI.isOnline(this.id) }); } else { diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 199448a57..22fde099e 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -39,6 +39,9 @@ const calcNonce = (messageEventData, pubKey, data64, timestamp, ttl) => { }; const trySendP2p = async (pubKey, data64, isPing, messageEventData) => { + if (typeof lokiP2pAPI === 'undefined') { + return false; + } const p2pDetails = lokiP2pAPI.getContactP2pDetails(pubKey); if (!p2pDetails || (!isPing && !p2pDetails.isOnline)) { return false; From 415ea7992c13979a52fe6a285c30e4b1ab1b6c8b Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Tue, 27 Aug 2019 14:02:11 +1000 Subject: [PATCH 19/64] Prevent showing inbox if secondary device registration is still ongoing --- js/background.js | 5 ++++- js/registration.js | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/js/background.js b/js/background.js index 29dbf195e..b837a8f44 100644 --- a/js/background.js +++ b/js/background.js @@ -587,7 +587,10 @@ if (Whisper.Import.isIncomplete()) { window.log.info('Import was interrupted, showing import error screen'); appView.openImporter(); - } else if (Whisper.Registration.everDone()) { + } else if ( + Whisper.Registration.everDone() && + !Whisper.Registration.ongoingSecondaryDeviceRegistration() + ) { // listeners Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion); // window.Signal.RefreshSenderCertificate.initialize({ diff --git a/js/registration.js b/js/registration.js index 499e981bf..301e8cb88 100644 --- a/js/registration.js +++ b/js/registration.js @@ -21,6 +21,9 @@ storage.get('chromiumRegistrationDone') === '' ); }, + ongoingSecondaryDeviceRegistration() { + return storage.get('secondaryDeviceStatus') === 'ongoing'; + }, remove() { storage.remove('chromiumRegistrationDone'); }, From 2c052a65a26e9479d3f77922338c2088d43e2c6e Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Tue, 27 Aug 2019 14:23:00 +1000 Subject: [PATCH 20/64] Display incoming messages in the primary device conversation --- js/background.js | 22 ++++++++++++++++++++-- js/delivery_receipts.js | 12 +++++++++++- js/models/messages.js | 11 +++++++++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/js/background.js b/js/background.js index 29dbf195e..85a1c1e39 100644 --- a/js/background.js +++ b/js/background.js @@ -1016,7 +1016,7 @@ ev.confirm(); } - function onTyping(ev) { + async function onTyping(ev) { const { typing, sender, senderDevice } = ev; const { groupId, started } = typing || {}; @@ -1025,7 +1025,17 @@ return; } - const conversation = ConversationController.get(groupId || sender); + let primaryDevice = null; + const authorisation = await window.libloki.storage.getGrantAuthorisationForSecondaryPubKey( + sender + ); + if (authorisation) { + primaryDevice = authorisation.primaryDevicePubKey; + } + + const conversation = ConversationController.get( + groupId || primaryDevice || sender + ); if (conversation) { conversation.notifyTyping({ @@ -1239,6 +1249,14 @@ const messageDescriptor = getMessageDescriptor(data); + // Funnel messages to primary device conversation if multi-device + const authorisation = await window.libloki.storage.getGrantAuthorisationForSecondaryPubKey( + messageDescriptor.id + ); + if (authorisation) { + messageDescriptor.id = authorisation.primaryDevicePubKey; + } + const { PROFILE_KEY_UPDATE } = textsecure.protobuf.DataMessage.Flags; // eslint-disable-next-line no-bitwise const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE); diff --git a/js/delivery_receipts.js b/js/delivery_receipts.js index ea52a6822..c2fa998ef 100644 --- a/js/delivery_receipts.js +++ b/js/delivery_receipts.js @@ -3,7 +3,8 @@ Whisper, ConversationController, MessageController, - _ + _, + libloki, */ /* eslint-disable more/no-then */ @@ -34,6 +35,15 @@ if (messages.length === 0) { return null; } + + const authorisation = await libloki.storage.getGrantAuthorisationForSecondaryPubKey( + source + ); + if (authorisation) { + // eslint-disable-next-line no-param-reassign + source = authorisation.primaryDevicePubKey; + } + const message = messages.find( item => !item.isIncoming() && source === item.get('conversationId') ); diff --git a/js/models/messages.js b/js/models/messages.js index 8f91658a8..33c5bc7df 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -10,7 +10,8 @@ Signal, textsecure, Whisper, - clipboard + clipboard, + libloki, */ /* eslint-disable more/no-then */ @@ -1697,7 +1698,7 @@ return message; }, - handleDataMessage(initialMessage, confirm) { + async handleDataMessage(initialMessage, confirm) { // This function is called from the background script in a few scenarios: // 1. on an incoming message // 2. on a sent message sync'd from another device @@ -1707,9 +1708,15 @@ const source = message.get('source'); const type = message.get('type'); let conversationId = message.get('conversationId'); + const authorisation = await libloki.storage.getGrantAuthorisationForSecondaryPubKey( + source + ); if (initialMessage.group) { conversationId = initialMessage.group.id; + } else if (authorisation) { + conversationId = authorisation.primaryDevicePubKey; } + const GROUP_TYPES = textsecure.protobuf.GroupContext.Type; const conversation = ConversationController.get(conversationId); From 53b974d63fc6713784dd5a0c421126c4b9cfeb12 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Tue, 27 Aug 2019 15:07:54 +1000 Subject: [PATCH 21/64] Send messages to every devices linked to a primary device --- libloki/storage.js | 14 +--- libtextsecure/outgoing_message.js | 103 ++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 37 deletions(-) diff --git a/libloki/storage.js b/libloki/storage.js index 556133896..ab93cf644 100644 --- a/libloki/storage.js +++ b/libloki/storage.js @@ -113,18 +113,8 @@ } } - function savePairingAuthorisation({ - primaryDevicePubKey, - secondaryDevicePubKey, - requestSignature, - grantSignature, - }) { - return window.Signal.Data.createOrUpdatePairingAuthorisation({ - primaryDevicePubKey, - secondaryDevicePubKey, - requestSignature, - grantSignature, - }); + function savePairingAuthorisation(authorisation) { + return window.Signal.Data.createOrUpdatePairingAuthorisation(authorisation); } function getGrantAuthorisationForSecondaryPubKey(secondaryPubKey) { diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 3dbb22fd6..1b8f331e8 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -86,18 +86,20 @@ OutgoingMessage.prototype = { }, reloadDevicesAndSend(number, recurse) { return () => - textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => { - if (deviceIds.length === 0) { - // eslint-disable-next-line no-param-reassign - deviceIds = [1]; - // return this.registerError( - // number, - // 'Got empty device list when loading device keys', - // null - // ); - } - return this.doSendMessage(number, deviceIds, recurse); - }); + libloki.storage + .getAllDevicePubKeysForPrimaryPubKey(number) + .then(devicesPubKeys => { + if (devicesPubKeys.length === 0) { + // eslint-disable-next-line no-param-reassign + devicesPubKeys = [number]; + // return this.registerError( + // number, + // 'Got empty device list when loading device keys', + // null + // ); + } + return this.doSendMessage(number, devicesPubKeys, recurse); + }); }, getKeysForNumber(number, updateDevices) { @@ -257,9 +259,11 @@ OutgoingMessage.prototype = { const bytes = new Uint8Array(websocketMessage.encode().toArrayBuffer()); return bytes; }, - doSendMessage(number, deviceIds, recurse) { + async doSendMessage(number, devicesPubKeys, recurse) { const ciphers = {}; + this.numbers = devicesPubKeys; + /* Disabled because i'm not sure how senderCertificate works :thinking: const { numberInfo, senderCertificate } = this; const info = numberInfo && numberInfo[number] ? numberInfo[number] : {}; @@ -291,8 +295,14 @@ OutgoingMessage.prototype = { */ return Promise.all( - deviceIds.map(async deviceId => { - const address = new libsignal.SignalProtocolAddress(number, deviceId); + devicesPubKeys.map(async devicePubKey => { + // Loki Messenger doesn't use the deviceId scheme, it's always 1. + // Instead, there are multiple device public keys. + const deviceId = 1; + const address = new libsignal.SignalProtocolAddress( + devicePubKey, + deviceId + ); const ourKey = textsecure.storage.user.getNumber(); const options = {}; const fallBackCipher = new libloki.crypto.FallBackSessionCipher( @@ -302,6 +312,23 @@ OutgoingMessage.prototype = { // Check if we need to attach the preKeys let sessionCipher; const isFriendRequest = this.messageType === 'friend-request'; + const isSecondaryDevice = !!window.storage.get('isSecondaryDevice'); + if (isFriendRequest && isSecondaryDevice) { + // Attach authorisation from primary device ONLY FOR FRIEND REQUEST + const ourPubKeyHex = textsecure.storage.user.getNumber(); + const pairingAuthorisation = await libloki.storage.getGrantAuthorisationForSecondaryPubKey( + ourPubKeyHex + ); + if (pairingAuthorisation) { + this.message.pairingAuthorisation = libloki.api.createPairingAuthorisationProtoMessage( + pairingAuthorisation + ); + } else { + window.log.error( + 'Could not find authorisation for our own pubkey while being secondary device.' + ); + } + } this.fallBackEncryption = this.fallBackEncryption || isFriendRequest; const flags = this.message.dataMessage ? this.message.dataMessage.get_flags() @@ -362,20 +389,46 @@ OutgoingMessage.prototype = { sourceDevice: 1, destinationRegistrationId: ciphertext.registrationId, content: ciphertext.body, + number: devicePubKey, }; }) ) .then(async outgoingObjects => { // TODO: handle multiple devices/messages per transmit - const outgoingObject = outgoingObjects[0]; - const socketMessage = await this.wrapInWebsocketMessage(outgoingObject); - await this.transmitMessage( - number, - socketMessage, - this.timestamp, - outgoingObject.ttl - ); - this.successfulNumbers[this.successfulNumbers.length] = number; + let counter = 0; + const promises = outgoingObjects.map(async outgoingObject => { + const destination = outgoingObject.number; + try { + counter += 1; + if (counter > 1) { + throw new Error(`Error for device ${counter}`); + } + const socketMessage = await this.wrapInWebsocketMessage( + outgoingObject + ); + await this.transmitMessage( + destination, + socketMessage, + this.timestamp, + outgoingObject.ttl + ); + this.successfulNumbers.push(destination); + } catch (e) { + e.number = destination; + this.errors.push(e); + } + }); + await Promise.all(promises); + // TODO: the retrySend should only send to the devices + // for which the transmission failed. + + // ensure numberCompleted() will execute the callback + this.numbersCompleted += + this.errors.length + this.successfulNumbers.length; + // Absorb errors if message sent to at least 1 device + if (this.successfulNumbers.length > 0) { + this.errors = []; + } this.numberCompleted(); }) .catch(error => { @@ -431,7 +484,7 @@ OutgoingMessage.prototype = { window.log.error( 'Got "key changed" error from encrypt - no identityKey for application layer', number, - deviceIds + devicesPubKeys ); throw error; } else { From dce8c8bcf70eac772be617f411429ae871659723 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Tue, 27 Aug 2019 15:24:31 +1000 Subject: [PATCH 22/64] Format message details to show secondary pubkey properly --- js/models/messages.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/js/models/messages.js b/js/models/messages.js index 8f91658a8..900cda902 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -912,6 +912,7 @@ // that contact. Otherwise, it will be a standalone entry. const errors = _.reject(allErrors, error => Boolean(error.number)); const errorsGroupedById = _.groupBy(allErrors, 'number'); + const primaryDevicePubKey = this.get('conversationId'); const finalContacts = (phoneNumbers || []).map(id => { const errorsForContact = errorsGroupedById[id]; const isOutgoingKeyError = Boolean( @@ -921,12 +922,20 @@ storage.get('unidentifiedDeliveryIndicators') && this.isUnidentifiedDelivery(id, unidentifiedLookup); + const isPrimaryDevice = id === primaryDevicePubKey; + + const contact = this.findAndFormatContact(id); + const profileName = isPrimaryDevice + ? contact.profileName + : `${contact.profileName} (Secondary Device)`; return { - ...this.findAndFormatContact(id), + ...contact, status: this.getStatus(id), errors: errorsForContact, isOutgoingKeyError, isUnidentifiedDelivery, + isPrimaryDevice, + profileName, onSendAnyway: () => this.trigger('force-send', { contact: this.findContact(id), @@ -941,7 +950,8 @@ // first; otherwise it's alphabetical const sortedContacts = _.sortBy( finalContacts, - contact => `${contact.errors ? '0' : '1'}${contact.title}` + contact => + `${contact.isPrimaryDevice ? '0' : '1'}${contact.phoneNumber}` ); return { From 661b207245b9b171ba5a456d577578a409cb54e7 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Tue, 27 Aug 2019 17:29:37 +1000 Subject: [PATCH 23/64] Various fixes for bugs introduced during reviews --- js/views/standalone_registration_view.js | 2 ++ libtextsecure/message_receiver.js | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index c3262cecb..ef64c4be7 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -54,6 +54,8 @@ this.showRegisterPage(); this.onValidatePassword(); + + this.onSecondaryDeviceRegistered = this.onSecondaryDeviceRegistered.bind(this); }, events: { 'validation input.number': 'onValidation', diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 9dce8934a..aa271fa89 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1057,7 +1057,7 @@ MessageReceiver.prototype.extend({ 'Received a pairing request addressed to another pubkey. Ignored.' ); return false; - } else if (authorisation.secondaryDevicePubKey === ourPubKey) { + } else if (isRequest && authorisation.secondaryDevicePubKey === ourPubKey) { window.log.warn('Received a pairing request from ourselves. Ignored.'); return false; } @@ -1066,7 +1066,7 @@ MessageReceiver.prototype.extend({ primaryDevicePubKey, secondaryDevicePubKey, dcodeIO.ByteBuffer.wrap(requestSignature).toArrayBuffer(), - type + textsecure.protobuf.PairingAuthorisationMessage.Type.REQUEST ); } catch (e) { window.log.warn( @@ -1081,7 +1081,7 @@ MessageReceiver.prototype.extend({ primaryDevicePubKey, secondaryDevicePubKey, dcodeIO.ByteBuffer.wrap(grantSignature).toArrayBuffer(), - type + textsecure.protobuf.PairingAuthorisationMessage.Type.GRANT ); } catch (e) { window.log.warn( From bb0150624fb28ad8e31d513eb8fe17d1a320c3c8 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Wed, 28 Aug 2019 16:32:21 +1000 Subject: [PATCH 24/64] Address reviews --- libtextsecure/outgoing_message.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 1b8f331e8..ea9e0446a 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -259,7 +259,7 @@ OutgoingMessage.prototype = { const bytes = new Uint8Array(websocketMessage.encode().toArrayBuffer()); return bytes; }, - async doSendMessage(number, devicesPubKeys, recurse) { + doSendMessage(number, devicesPubKeys, recurse) { const ciphers = {}; this.numbers = devicesPubKeys; @@ -389,20 +389,15 @@ OutgoingMessage.prototype = { sourceDevice: 1, destinationRegistrationId: ciphertext.registrationId, content: ciphertext.body, - number: devicePubKey, + pubKey: devicePubKey, }; }) ) .then(async outgoingObjects => { // TODO: handle multiple devices/messages per transmit - let counter = 0; const promises = outgoingObjects.map(async outgoingObject => { - const destination = outgoingObject.number; + const destination = outgoingObject.pubKey; try { - counter += 1; - if (counter > 1) { - throw new Error(`Error for device ${counter}`); - } const socketMessage = await this.wrapInWebsocketMessage( outgoingObject ); From aa2438ad001f5268a0ca5ba006fd897a2fd1adeb Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Fri, 30 Aug 2019 10:54:52 +1000 Subject: [PATCH 25/64] Shorter TTL for pairing requests --- libloki/api.js | 2 +- libtextsecure/outgoing_message.js | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/libloki/api.js b/libloki/api.js index a07c37859..b5ba22d98 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -106,7 +106,7 @@ const content = new textsecure.protobuf.Content({ pairingAuthorisation, }); - const options = {}; + const options = { messageType: 'pairing-request' }; const p = new Promise((resolve, reject) => { const outgoingMessage = new textsecure.OutgoingMessage( null, // server diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index ea9e0446a..701b94419 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -370,17 +370,21 @@ OutgoingMessage.prototype = { dcodeIO.ByteBuffer.wrap(ciphertext.body, 'binary').toArrayBuffer() ); } - let ttl; - if (this.messageType === 'friend-request') { - ttl = 4 * 24 * 60 * 60 * 1000; // 4 days for friend request message - } else if (this.messageType === 'onlineBroadcast') { - ttl = 60 * 1000; // 1 minute for online broadcast message - } else if (this.messageType === 'typing') { - ttl = 60 * 1000; // 1 minute for typing indicators - } else { - const hours = window.getMessageTTL() || 24; // 1 day default for any other message - ttl = hours * 60 * 60 * 1000; + const getTTL = (type) => { + switch (type) { + case 'friend-request': + return 4 * 24 * 60 * 60 * 1000; // 4 days for friend request message + case 'onlineBroadcast': + return 60 * 1000; // 1 minute for online broadcast message + case 'typing': + return 60 * 1000; // 1 minute for typing indicators + case 'pairing-request': + return 2 * 60 * 1000; // 2 minutes for pairing requests + default: + return (window.getMessageTTL() || 24) * 60 * 60 * 1000; // 1 day default for any other message + } } + const ttl = getTTL(this.messageType); return { type: ciphertext.type, // FallBackSessionCipher sets this to FRIEND_REQUEST From 7b9e578d8a0df555141f27c0d24ed9fbe67a3745 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Fri, 30 Aug 2019 11:33:56 +1000 Subject: [PATCH 26/64] lint --- js/views/standalone_registration_view.js | 4 +++- libtextsecure/outgoing_message.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index ef64c4be7..423123a57 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -55,7 +55,9 @@ this.onValidatePassword(); - this.onSecondaryDeviceRegistered = this.onSecondaryDeviceRegistered.bind(this); + this.onSecondaryDeviceRegistered = this.onSecondaryDeviceRegistered.bind( + this + ); }, events: { 'validation input.number': 'onValidation', diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 701b94419..a1feea2ae 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -370,7 +370,7 @@ OutgoingMessage.prototype = { dcodeIO.ByteBuffer.wrap(ciphertext.body, 'binary').toArrayBuffer() ); } - const getTTL = (type) => { + const getTTL = type => { switch (type) { case 'friend-request': return 4 * 24 * 60 * 60 * 1000; // 4 days for friend request message @@ -383,7 +383,7 @@ OutgoingMessage.prototype = { default: return (window.getMessageTTL() || 24) * 60 * 60 * 1000; // 1 day default for any other message } - } + }; const ttl = getTTL(this.messageType); return { From 3a78f47cd7249c862a609a3d12c4a9280ff09f9a Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Fri, 30 Aug 2019 11:29:53 +1000 Subject: [PATCH 27/64] Secondary device registration: UX changes (show countdown, disable button, etc.) --- background.html | 3 +- js/views/standalone_registration_view.js | 74 +++++++++++++++++++----- stylesheets/_global.scss | 8 +++ 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/background.html b/background.html index a1f9c94c9..bc0ed8687 100644 --- a/background.html +++ b/background.html @@ -658,8 +658,9 @@
+
- Link + diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index ef64c4be7..cbf91f068 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -13,6 +13,8 @@ className: 'full-screen-flow standalone-fullscreen', initialize() { this.accountManager = getAccountManager(); + // Clean status in case the app closed unexpectedly + textsecure.storage.remove('secondaryDeviceStatus'); this.render(); @@ -50,12 +52,14 @@ this.registrationParams = {}; this.$pages = this.$('.page'); - this.pairingTimeout = null; + this.pairingInterval = null; this.showRegisterPage(); this.onValidatePassword(); - this.onSecondaryDeviceRegistered = this.onSecondaryDeviceRegistered.bind(this); + this.onSecondaryDeviceRegistered = this.onSecondaryDeviceRegistered.bind( + this + ); }, events: { 'validation input.number': 'onValidation', @@ -108,6 +112,9 @@ const input = this.trim(this.$passwordInput.val()); + // Ensure we clear the secondary device registration status + textsecure.storage.remove('secondaryDeviceStatus'); + try { await window.setPassword(input); await this.accountManager.registerSingleDevice( @@ -129,11 +136,19 @@ this.showProfilePage(mnemonic, language); }, onSecondaryDeviceRegistered() { + clearInterval(this.pairingInterval); // Ensure the left menu is updated Whisper.events.trigger('userChanged', { isSecondaryDevice: true }); this.$el.trigger('openInbox'); }, async registerSecondaryDevice() { + if (textsecure.storage.get('secondaryDeviceStatus') === 'ongoing') { + return; + } + textsecure.storage.put('secondaryDeviceStatus', 'ongoing'); + this.$('#register-secondary-device') + .attr('disabled', 'disabled') + .text('Sending...'); const mnemonic = this.$('#mnemonic-display').text(); const language = this.$('#mnemonic-display-language').val(); const primaryPubKey = this.$('#primary-pubkey').val(); @@ -147,26 +162,57 @@ 'secondaryDeviceRegistration', this.onSecondaryDeviceRegistered ); - clearTimeout(this.pairingTimeout); - this.pairingTimeout = setTimeout(() => { - this.$('.standalone-secondary-device #error').text( - 'The primary device has not responded within 1 minute. Ensure it is connected.' + clearInterval(this.pairingInterval); + let countDown = 60; + const onError = async error => { + clearInterval(this.pairingInterval); + this.$('.standalone-secondary-device #error') + .text(error) + .show(); + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + // If the registration started, ensure it's finished + while ( + textsecure.storage.protocol.getIdentityKeyPair() && + !Whisper.Registration.isDone() + ) { + // eslint-disable-next-line no-await-in-loop + await sleep(100); + } + Whisper.Registration.remove(); + Whisper.RotateSignedPreKeyListener.stop(); + textsecure.storage.remove('secondaryDeviceStatus'); + window.ConversationController.reset(); + this.$('#register-secondary-device') + .removeAttr('disabled') + .text('Link'); + }; + const countDownCallBack = () => { + if (countDown > 0) { + this.$('#register-secondary-device').text( + `Waiting for Primary Device... (${countDown})` + ); + countDown -= 1; + return; + } + onError( + 'The primary device has not responded within 1 minute. Ensure that you accept the pairing on the primary device.' ); - this.$('.standalone-secondary-device #error').show(); - }, 60000); - textsecure.storage.put('secondaryDeviceStatus', 'ongoing'); + }; try { await this.accountManager.registerSingleDevice( mnemonic, language, - 'John Smith' + null ); await this.accountManager.requestPairing(primaryPubKey); + countDownCallBack(); + this.pairingInterval = setInterval(countDownCallBack, 1000); + const pubkey = textsecure.storage.user.getNumber(); + this.$('.standalone-secondary-device #pubkey').text( + `Here is your pubkey:\n${pubkey}` + ); } catch (e) { - textsecure.storage.remove('secondaryDeviceStatus'); - this.$('.standalone-secondary-device #error').text(e); - this.$('.standalone-secondary-device #error').show(); - clearTimeout(this.pairingTimeout); + onError(e); } }, registerWithMnemonic() { diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 973f420ea..e8cbafeb1 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -714,6 +714,14 @@ $loading-height: 16px; .button { background: $color-loki-green-gradient; border-radius: 100px; + + &[disabled='disabled'] { + &, + &:hover { + background: $color-loki-dark-gray; + cursor: default; + } + } } #mnemonic-display { From 97970ff4659d6212b9639cb367ae8bd4928e61ca Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Fri, 30 Aug 2019 12:12:06 +1000 Subject: [PATCH 28/64] Share primary device profile name upon secondary registration --- libloki/api.js | 12 ++++++++++++ libtextsecure/message_receiver.js | 27 +++++++++++++++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/libloki/api.js b/libloki/api.js index a07c37859..575a757a9 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -103,8 +103,20 @@ const pairingAuthorisation = createPairingAuthorisationProtoMessage( authorisation ); + // Send profile name to secondary device + const ourNumber = textsecure.storage.user.getNumber(); + const conversation = await window.ConversationController.getOrCreateAndWait( + ourNumber, + 'private' + ); + const lokiProfile = conversation.getLokiProfile(); + const profile = new textsecure.protobuf.DataMessage.LokiProfile(lokiProfile); + const dataMessage = new textsecure.protobuf.DataMessage({ + profile, + }); const content = new textsecure.protobuf.Content({ pairingAuthorisation, + dataMessage, }); const options = {}; const p = new Promise((resolve, reject) => { diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index aa271fa89..ca192ca5e 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1104,7 +1104,7 @@ MessageReceiver.prototype.extend({ pairingRequest.secondaryDevicePubKey ); }, - async handleAuthorisationForSelf(pairingAuthorisation) { + async handleAuthorisationForSelf(pairingAuthorisation, dataMessage) { const valid = await this.validateAuthorisation(pairingAuthorisation); if (!valid) { return; @@ -1115,7 +1115,7 @@ MessageReceiver.prototype.extend({ window.log.info( `Received pairing authorisation from ${primaryDevicePubKey}` ); - const alreadySecondaryDevice = window.storage.get('isSecondaryDevice'); + const alreadySecondaryDevice = !!window.storage.get('isSecondaryDevice'); if (alreadySecondaryDevice) { window.log.warn( 'Received an unexpected pairing authorisation (device is already paired as secondary device). Ignoring.' @@ -1129,6 +1129,17 @@ MessageReceiver.prototype.extend({ window.storage.remove('secondaryDeviceStatus'); window.storage.put('isSecondaryDevice', true); Whisper.events.trigger('secondaryDeviceRegistration'); + // Update profile name + if (dataMessage) { + const { profile } = dataMessage; + if (profile) { + const ourNumber = textsecure.storage.user.getNumber(); + const me = window.ConversationController.get(ourNumber); + if (me) { + me.setLokiProfile(profile); + } + } + } } else { window.log.warn('Unimplemented pairing authorisation message type'); } @@ -1148,12 +1159,15 @@ MessageReceiver.prototype.extend({ // send friend accept? window.libloki.api.sendBackgroundMessage(secondaryDevicePubKey); }, - async handlePairingAuthorisationMessage(envelope, pairingAuthorisation) { + async handlePairingAuthorisationMessage( + envelope, + { pairingAuthorisation, dataMessage } + ) { const { type, secondaryDevicePubKey } = pairingAuthorisation; if (type === textsecure.protobuf.PairingAuthorisationMessage.Type.REQUEST) { await this.handlePairingRequest(pairingAuthorisation); } else if (secondaryDevicePubKey === textsecure.storage.user.getNumber()) { - await this.handleAuthorisationForSelf(pairingAuthorisation); + await this.handleAuthorisationForSelf(pairingAuthorisation, dataMessage); } else { await this.handleAuthorisationForContact(pairingAuthorisation); } @@ -1271,10 +1285,7 @@ MessageReceiver.prototype.extend({ ); } if (content.pairingAuthorisation) { - return this.handlePairingAuthorisationMessage( - envelope, - content.pairingAuthorisation - ); + return this.handlePairingAuthorisationMessage(envelope, content); } if (content.syncMessage) { return this.handleSyncMessage(envelope, content.syncMessage); From 7b262561a5b1f8c6163fcc57c04cf6561a98a958 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Mon, 2 Sep 2019 10:19:09 +1000 Subject: [PATCH 29/64] simplify scss for disabled button --- stylesheets/_global.scss | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index e8cbafeb1..cd3628cd0 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -715,12 +715,9 @@ $loading-height: 16px; background: $color-loki-green-gradient; border-radius: 100px; - &[disabled='disabled'] { - &, - &:hover { - background: $color-loki-dark-gray; - cursor: default; - } + &:disabled, &:disabled:hover { + background: $color-loki-dark-gray; + cursor: default; } } From 9f8ee875b7ca724564e2dba1847fc72603289f4c Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Mon, 2 Sep 2019 13:44:56 +1000 Subject: [PATCH 30/64] address review --- libtextsecure/message_receiver.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index ca192ca5e..e10354443 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1130,14 +1130,11 @@ MessageReceiver.prototype.extend({ window.storage.put('isSecondaryDevice', true); Whisper.events.trigger('secondaryDeviceRegistration'); // Update profile name - if (dataMessage) { - const { profile } = dataMessage; - if (profile) { - const ourNumber = textsecure.storage.user.getNumber(); - const me = window.ConversationController.get(ourNumber); - if (me) { - me.setLokiProfile(profile); - } + if (dataMessage && dataMessage.profile) { + const ourNumber = textsecure.storage.user.getNumber(); + const me = window.ConversationController.get(ourNumber); + if (me) { + me.setLokiProfile(dataMessage.profile); } } } else { From 831ae0957681e94f8555a58e2993c43c78654dcc Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 30 Aug 2019 09:42:13 +1000 Subject: [PATCH 31/64] Cherry pick "profile simplification" from Mik --- background.html | 1 - js/background.js | 20 +++--- js/conversation_controller.js | 2 +- js/models/conversations.js | 14 ++-- js/models/messages.js | 6 ++ js/models/profile.js | 56 ---------------- js/views/inbox_view.js | 2 +- libtextsecure/account_manager.js | 10 +-- libtextsecure/message_receiver.js | 2 +- libtextsecure/sendmessage.js | 22 +++++- test/index.html | 1 - test/models/profile_test.js | 108 ------------------------------ 12 files changed, 45 insertions(+), 199 deletions(-) delete mode 100644 js/models/profile.js delete mode 100644 test/models/profile_test.js diff --git a/background.html b/background.html index bc0ed8687..a7a201643 100644 --- a/background.html +++ b/background.html @@ -705,7 +705,6 @@ - diff --git a/js/background.js b/js/background.js index dd63f1151..88c2cd755 100644 --- a/js/background.js +++ b/js/background.js @@ -652,25 +652,21 @@ } }); - Whisper.events.on('onEditProfile', () => { + Whisper.events.on('onEditProfile', async () => { const ourNumber = textsecure.storage.user.getNumber(); - const profile = storage.getLocalProfile(); + const conversation = await ConversationController.getOrCreateAndWait( + ourNumber, + 'private' + ); + const profile = conversation.getLokiProfile(); const displayName = profile && profile.displayName; if (appView) { appView.showNicknameDialog({ title: window.i18n('editProfileTitle'), message: window.i18n('editProfileDisplayNameWarning'), nickname: displayName, - onOk: async newName => { - await storage.setProfileName(newName); - - // Update the conversation if we have it - const conversation = ConversationController.get(ourNumber); - if (conversation) { - const newProfile = storage.getLocalProfile(); - conversation.setProfile(newProfile); - } - }, + onOk: newName => + conversation.setLokiProfile({ displayName: newName }), }); } }); diff --git a/js/conversation_controller.js b/js/conversation_controller.js index fe2a45074..04894042c 100644 --- a/js/conversation_controller.js +++ b/js/conversation_controller.js @@ -238,7 +238,7 @@ } promises.concat([ - conversation.updateProfile(), + conversation.updateProfileName(), conversation.updateProfileAvatar(), conversation.resetPendingSend(), conversation.setFriendRequestExpiryTimeout(), diff --git a/js/models/conversations.js b/js/models/conversations.js index 15d265021..076effe27 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1988,9 +1988,9 @@ Conversation: Whisper.Conversation, }); - await this.updateProfile(); + await this.updateProfileName(); }, - async setProfile(profile) { + async setLokiProfile(profile) { if (!_.isEqual(this.get('profile'), profile)) { this.set({ profile }); await window.Signal.Data.updateConversation(this.id, this.attributes, { @@ -1998,18 +1998,18 @@ }); } - await this.updateProfile(); + await this.updateProfileName(); }, - async updateProfile() { + async updateProfileName() { // Prioritise nickname over the profile display name const nickname = this.getNickname(); - const profile = this.getLocalProfile(); + const profile = this.getLokiProfile(); const displayName = profile && profile.displayName; const profileName = nickname || displayName || null; await this.setProfileName(profileName); }, - getLocalProfile() { + getLokiProfile() { return this.get('profile'); }, getNickname() { @@ -2041,7 +2041,7 @@ const c = await ConversationController.getOrCreateAndWait(id, 'private'); // We only need to update the profile as they are all stored inside the conversation - await c.updateProfile(); + await c.updateProfileName(); }, async setProfileName(name) { const profileName = this.get('profileName'); diff --git a/js/models/messages.js b/js/models/messages.js index 4b4a9cd55..ed5ae4197 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1977,6 +1977,12 @@ } ); } + } else if (dataMessage.profile) { + ConversationController.getOrCreateAndWait(source, 'private').then( + sender => { + sender.setLokiProfile(dataMessage.profile); + } + ); } let autoAccept = false; diff --git a/js/models/profile.js b/js/models/profile.js deleted file mode 100644 index 16ab2f1da..000000000 --- a/js/models/profile.js +++ /dev/null @@ -1,56 +0,0 @@ -/* global storage, _ */ -/* global storage: false */ - -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - const PROFILE_ID = 'local-profile'; - - storage.getLocalProfile = () => { - const profile = storage.get(PROFILE_ID, null); - return profile; - }; - - storage.setProfileName = async newName => { - if (typeof newName !== 'string' && newName !== null) { - throw Error('Name must be a string!'); - } - - // Update our profiles accordingly' - const trimmed = newName && newName.trim(); - - // If we get an empty name then unset the name property - // Otherwise update it - const profile = storage.getLocalProfile(); - const newProfile = { ...profile }; - if (_.isEmpty(trimmed)) { - delete newProfile.displayName; - } else { - newProfile.displayName = trimmed; - } - - await storage.saveLocalProfile(newProfile); - }; - - storage.saveLocalProfile = async profile => { - const storedProfile = storage.get(PROFILE_ID, null); - - // Only store the profile if we have a different object - if (storedProfile && _.isEqual(storedProfile, profile)) { - return; - } - - window.log.info('saving local profile ', profile); - await storage.put(PROFILE_ID, profile); - }; - - storage.removeLocalProfile = async () => { - window.log.info('removing local profile'); - await storage.remove(PROFILE_ID); - }; -})(); diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index c4f1dc7ff..4ac23f855 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -303,7 +303,7 @@ } if (conversation) { - conversation.updateProfile(); + conversation.updateProfileName(); } this.conversation_stack.open(conversation); diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 7684605d6..3fc9060f8 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -11,7 +11,6 @@ dcodeIO, StringView, log, - storage, Event, ConversationController, Whisper @@ -538,7 +537,7 @@ saveMnemonic(mnemonic) { return textsecure.storage.put('mnemonic', mnemonic); }, - async registrationDone(number, profileName) { + async registrationDone(number, displayName) { window.log.info('registration done'); // Ensure that we always have a conversation for ourself @@ -546,12 +545,7 @@ number, 'private' ); - - await storage.setProfileName(profileName); - - // Update the conversation if we have it - const newProfile = storage.getLocalProfile(); - await conversation.setProfile(newProfile); + await conversation.setLokiProfile({ displayName }); this.dispatchEvent(new Event('registration')); }, diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index e10354443..b1dffd7c8 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1206,7 +1206,7 @@ MessageReceiver.prototype.extend({ } // Update the conversation - await conversation.setProfile(profile); + await conversation.setLokiProfile(profile); } if (friendRequest && isMe) { diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index c2be038d8..dcf2b6500 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -819,7 +819,18 @@ MessageSender.prototype = { return message.toArrayBuffer(); }, - sendMessageToNumber( + getOurProfile() { + try { + const ourNumber = textsecure.storage.user.getNumber(); + const conversation = window.ConversationController.get(ourNumber); + return conversation.getLokiProfile(); + } catch (e) { + window.log.error(`Failed to get our profile: ${e}`); + return null; + } + }, + + async sendMessageToNumber( number, messageText, attachments, @@ -830,7 +841,7 @@ MessageSender.prototype = { profileKey, options ) { - const profile = textsecure.storage.impl.getLocalProfile(); + const profile = this.getOurProfile(); return this.sendMessage( { recipients: [number], @@ -942,7 +953,11 @@ MessageSender.prototype = { options ) { const me = textsecure.storage.user.getNumber(); - const numbers = groupNumbers.filter(number => number !== me); + let numbers = groupNumbers.filter(number => number !== me); + if (options.isPublic) { + numbers = [groupId]; + } + const profile = this.getOurProfile(); const attrs = { recipients: numbers, body: messageText, @@ -953,6 +968,7 @@ MessageSender.prototype = { needsSync: true, expireTimer, profileKey, + profile, group: { id: groupId, type: textsecure.protobuf.GroupContext.Type.DELIVER, diff --git a/test/index.html b/test/index.html index 61087bb86..7a3454115 100644 --- a/test/index.html +++ b/test/index.html @@ -530,7 +530,6 @@ - diff --git a/test/models/profile_test.js b/test/models/profile_test.js deleted file mode 100644 index 9f9db696a..000000000 --- a/test/models/profile_test.js +++ /dev/null @@ -1,108 +0,0 @@ -/* global storage */ - -/* eslint no-await-in-loop: 0 */ - -'use strict'; - -const PROFILE_ID = 'local-profile'; - -describe('Profile', () => { - beforeEach(async () => { - await clearDatabase(); - await storage.remove(PROFILE_ID); - }); - - describe('getLocalProfile', () => { - it('returns the local profile', async () => { - const values = [null, 'hello', { a: 'b' }]; - for (let i = 0; i < values.length; i += 1) { - await storage.put(PROFILE_ID, values[i]); - assert.strictEqual(values[i], storage.getLocalProfile()); - } - }); - }); - - describe('saveLocalProfile', () => { - it('saves a profile', async () => { - const values = [null, 'hello', { a: 'b' }]; - for (let i = 0; i < values.length; i += 1) { - await storage.saveLocalProfile(values[i]); - assert.strictEqual(values[i], storage.get(PROFILE_ID)); - } - }); - }); - - describe('removeLocalProfile', () => { - it('removes a profile', async () => { - await storage.saveLocalProfile('a'); - assert.strictEqual('a', storage.getLocalProfile()); - - await storage.removeLocalProfile(); - assert.strictEqual(null, storage.getLocalProfile()); - }); - }); - - describe('setProfileName', () => { - it('throws if a name is not a string', async () => { - const values = [0, { a: 'b' }, [1, 2]]; - for (let i = 0; i < values.length; i += 1) { - try { - await storage.setProfileName(values[i]); - assert.fail( - `setProfileName did not throw an error for ${typeof values[i]}` - ); - } catch (e) { - assert.throws(() => { - throw e; - }, 'Name must be a string!'); - } - } - }); - - it('does not throw if we pass a string or null', async () => { - const values = [null, '1']; - for (let i = 0; i < values.length; i += 1) { - try { - await storage.setProfileName(values[i]); - } catch (e) { - assert.fail('setProfileName threw an error'); - } - } - }); - - it('saves the display name', async () => { - await storage.setProfileName('hi there!'); - const profile = storage.getLocalProfile(); - assert.deepEqual(profile.displayName, 'hi there!'); - }); - - it('saves the display name without overwriting the other profile properties', async () => { - const profile = { title: 'hello' }; - await storage.put(PROFILE_ID, profile); - await storage.setProfileName('hi there!'); - - const expected = { - ...profile, - displayName: 'hi there!', - }; - assert.deepEqual(expected, storage.getLocalProfile()); - }); - - it('trims the display name', async () => { - await storage.setProfileName(' in middle '); - const profile = storage.getLocalProfile(); - assert.deepEqual('in middle', profile.displayName); - }); - - it('unsets the display name property if it is empty', async () => { - const profile = { - displayName: 'HI THERE!', - }; - await storage.put(PROFILE_ID, profile); - assert.exists(storage.getLocalProfile().displayName); - - await storage.setProfileName(''); - assert.notExists(storage.getLocalProfile().displayName); - }); - }); -}); From e50a29ffa5df4d7e65177e444f46d03c56f2f8d1 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Mon, 2 Sep 2019 16:33:19 +1000 Subject: [PATCH 32/64] Show 3 last mnemonic representation of public key instead of public key during pairing --- background.html | 6 +++--- js/views/device_pairing_dialog_view.js | 7 ++++++- js/views/standalone_registration_view.js | 7 ++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/background.html b/background.html index a7a201643..909e38813 100644 --- a/background.html +++ b/background.html @@ -258,8 +258,8 @@