From d20861490972ca771a36b6c66ba98f8a5ea8bfaa Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Tue, 6 Aug 2019 14:49:01 +1000 Subject: [PATCH] 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;