diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js index f4490d63b..e7ca8847d 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -44,7 +44,9 @@ const migrations = [ transaction.db.createObjectStore('sessions'); transaction.db.createObjectStore('identityKeys'); - transaction.db.createObjectStore('preKeys'); + const preKeys = transaction.db.createObjectStore('preKeys', { keyPath: 'id'}); + preKeys.createIndex('recipient', 'recipient', { unique: true }); + transaction.db.createObjectStore('signedPreKeys'); transaction.db.createObjectStore('items'); diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index 22310e3a6..467ab29b9 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -223,6 +223,24 @@ ); }); }, + loadPreKeyForContactIdentityKeyString(contactIdentityKeyString) { + const prekey = new PreKey({ recipient: contactIdentityKeyString }); + return new Promise(resolve => { + prekey.fetch().then( + () => { + window.log.info('Successfully fetched prekey for recipient :', contactIdentityKeyString); + resolve({ + pubKey: prekey.get('publicKey'), + privKey: prekey.get('privateKey'), + keyId: prekey.get('id'), + }); + }, + () => { + resolve(); + } + ); + }); + }, loadContactPreKey(pubKey) { const prekey = new ContactPreKey({ identityKeyString: pubKey }); return new Promise(resolve => { @@ -256,11 +274,12 @@ }); }); }, - storePreKey(keyId, keyPair) { + storePreKey(keyId, keyPair, contactIdentityKeyString) { const prekey = new PreKey({ id: keyId, publicKey: keyPair.pubKey, privateKey: keyPair.privKey, + recipient: contactIdentityKeyString, }); return new Promise(resolve => { prekey.save().always(() => { @@ -312,6 +331,7 @@ created_at: prekey.get('created_at'), keyId: prekey.get('id'), confirmed: prekey.get('confirmed'), + signature: prekey.get('signature'), }); }) .fail(() => { @@ -362,18 +382,20 @@ created_at: prekey.get('created_at'), keyId: prekey.get('id'), confirmed: prekey.get('confirmed'), + signature: prekey.get('signature'), })) ); }); }); }, - storeSignedPreKey(keyId, keyPair, confirmed) { + storeSignedPreKey(keyId, keyPair, confirmed, signature) { const prekey = new SignedPreKey({ id: keyId, publicKey: keyPair.pubKey, privateKey: keyPair.privKey, created_at: Date.now(), confirmed: Boolean(confirmed), + signature, }); return new Promise(resolve => { prekey.save().always(() => { diff --git a/libloki/libloki-protocol.js b/libloki/libloki-protocol.js index 904277b0f..059b261cb 100644 --- a/libloki/libloki-protocol.js +++ b/libloki/libloki-protocol.js @@ -47,8 +47,47 @@ } } } + + getPreKeyBundleForNumber = async function(pubKey) { + const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair(); + const identityKey = myKeyPair.pubKey; + + // Retrieve ids. The ids stored are always the latest generated + 1 + const signedKeyId = textsecure.storage.get('signedKeyId', 1) - 1; + + const [signedKey, preKey] = await Promise.all([ + textsecure.storage.protocol.loadSignedPreKey(signedKeyId), + new Promise(async (resolve, reject) => { + // retrieve existing prekey if we already generated one for that recipient + const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContactIdentityKeyString(pubKey); + if (storedPreKey) { + resolve({ pubKey: storedPreKey.pubKey, keyId: storedPreKey.keyId }); + } else { + // generate and store new prekey + const preKeyId = textsecure.storage.get('maxPreKeyId', 1); + textsecure.storage.put('maxPreKeyId', preKeyId + 1); + const preKey = await libsignal.KeyHelper.generatePreKey(preKeyId); + await textsecure.storage.protocol.storePreKey(preKey.keyId, preKey.keyPair, pubKey); + resolve({ pubKey: preKey.keyPair.pubKey, keyId: preKeyId }); + } + }) + ]); + + const preKeyMessage = new textsecure.protobuf.PreKeyBundleMessage({ + identityKey, + deviceId: 1, // TODO: fetch from somewhere + preKeyId: preKey.keyId, + signedKeyId, + preKey: preKey.pubKey, + signedKey: signedKey.pubKey, + signature: signedKey.signature, + }); + + return preKeyMessage; + } window.libloki.FallBackSessionCipher = FallBackSessionCipher; + window.libloki.getPreKeyBundleForNumber = getPreKeyBundleForNumber; window.libloki.FallBackDecryptionError = FallBackDecryptionError; })(); \ No newline at end of file diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 4e3b05c39..02a5e3748 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -47,7 +47,7 @@ registerSingleDevice() { const createAccount = this.createAccount.bind(this); const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); - const generateKeys = this.generateKeys.bind(this, 100); + const generateKeys = this.generateKeys.bind(this, 0); const confirmKeys = this.confirmKeys.bind(this); const registrationDone = this.registrationDone.bind(this); return this.queueTask(() => @@ -101,7 +101,7 @@ registerSecondDevice(setProvisioningUrl, confirmNumber, progressCallback) { const createAccount = this.createAccount.bind(this); const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); - const generateKeys = this.generateKeys.bind(this, 100, progressCallback); + const generateKeys = this.generateKeys.bind(this, 0, progressCallback); const confirmKeys = this.confirmKeys.bind(this); const registrationDone = this.registrationDone.bind(this); const registerKeys = this.server.registerKeys.bind(this.server); @@ -193,7 +193,7 @@ ); }, refreshPreKeys() { - const generateKeys = this.generateKeys.bind(this, 100); + const generateKeys = this.generateKeys.bind(this, 0); const registerKeys = this.server.registerKeys.bind(this.server); return this.queueTask(() => @@ -239,14 +239,14 @@ window.log.info('Saving new signed prekey', res.keyId); return Promise.all([ textsecure.storage.put('signedKeyId', signedKeyId + 1), - store.storeSignedPreKey(res.keyId, res.keyPair), + store.storeSignedPreKey(res.keyId, res.keyPair, undefined, res.signature), ]) .then(() => { const confirmed = true; window.log.info('Confirming new signed prekey', res.keyId); return Promise.all([ textsecure.storage.remove('signedKeyRotationRejected'), - store.storeSignedPreKey(res.keyId, res.keyPair, confirmed), + store.storeSignedPreKey(res.keyId, res.keyPair, confirmed, res.signature), ]); }) .then(() => cleanSignedPreKeys()); @@ -407,7 +407,7 @@ const confirmed = true; window.log.info('confirmKeys: confirming key', key.keyId); - return store.storeSignedPreKey(key.keyId, key.keyPair, confirmed); + return store.storeSignedPreKey(key.keyId, key.keyPair, confirmed, key.signature); }, generateKeys(count, providedProgressCallback) { const progressCallback = @@ -449,7 +449,7 @@ identityKey, signedKeyId ).then(res => { - store.storeSignedPreKey(res.keyId, res.keyPair); + store.storeSignedPreKey(res.keyId, res.keyPair, undefined, res.signature); result.signedPreKey = { keyId: res.keyId, publicKey: res.keyPair.pubKey, diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index fb57efd5b..665975f1b 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -109,7 +109,7 @@ OutgoingMessage.prototype = { }); } - return null; + return true; }) ); // TODO: check if still applicable @@ -125,9 +125,7 @@ OutgoingMessage.prototype = { ]).then((keys) => { const [preKey, signedPreKey] = keys; if (preKey == undefined || signedPreKey == undefined) { - log.error("Will need to request keys!") - this.fallBackEncryption = true; - return Promise.resolve(); + return false; } else { const identityKey = StringView.hexToArrayBuffer(number); @@ -369,7 +367,27 @@ OutgoingMessage.prototype = { sendToNumber(number) { return this.getStaleDeviceIdsForNumber(number).then(updateDevices => this.getKeysForNumber(number, updateDevices) - .then(this.reloadDevicesAndSend(number, true)) + .then(async (keysFound) => { + let attachPrekeys = false; + if (!keysFound) + { + log.info("Fallback encryption enabled"); + this.fallBackEncryption = true; + attachPrekeys = true; + } else { + try { + const conversation = ConversationController.get(number); + attachPrekeys = !conversation.isKeyExchangeCompleted(); + } catch(e) { + // do nothing + } + } + + if (attachPrekeys) { + log.info('attaching prekeys to outgoing message'); + this.message.preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(number); + } + }).then(this.reloadDevicesAndSend(number, true)) .catch(error => { if (error.message === 'Identity key changed') { // eslint-disable-next-line no-param-reassign diff --git a/protos/SignalService.proto b/protos/SignalService.proto index fa46ecaba..6189321de 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -11,7 +11,7 @@ message Envelope { KEY_EXCHANGE = 2; PREKEY_BUNDLE = 3; RECEIPT = 5; - FRIEND_REQUEST = 6; + FRIEND_REQUEST = 6; // contains prekeys + message and is using simple encryption } optional Type type = 1; @@ -29,6 +29,17 @@ message Content { optional CallMessage callMessage = 3; optional NullMessage nullMessage = 4; optional ReceiptMessage receiptMessage = 5; + optional PreKeyBundleMessage preKeyBundleMessage = 6; +} + +message PreKeyBundleMessage { + optional bytes identityKey = 1; + optional uint32 deviceId = 2; + optional uint32 preKeyId = 3; + optional uint32 signedKeyId = 4; + optional bytes preKey = 5; + optional bytes signedKey = 6; + optional bytes signature = 7; } message CallMessage {