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 a59012244..ab8e98157 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -52,9 +52,11 @@ const migrations = [ const contactPreKeys = transaction.db.createObjectStore('contactPreKeys', { keyPath: 'id', autoIncrement : true }); contactPreKeys.createIndex('identityKeyString', 'identityKeyString', { unique: false }); + contactPreKeys.createIndex('keyId', 'keyId', { unique: false }); const contactSignedPreKeys = transaction.db.createObjectStore('contactSignedPreKeys', { keyPath: 'id', autoIncrement : true }); contactSignedPreKeys.createIndex('identityKeyString', 'identityKeyString', { unique: false }); + contactSignedPreKeys.createIndex('keyId', 'keyId', { unique: false }); window.log.info('creating debug log'); transaction.db.createObjectStore('debug'); diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index 078d643f9..b0b8df694 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -179,7 +179,23 @@ const Group = Model.extend({ storeName: 'groups' }); const Item = Model.extend({ storeName: 'items' }); const ContactPreKey = Model.extend({ storeName: 'contactPreKeys' }); + const ContactPreKeyCollection = Backbone.Collection.extend({ + storeName: 'contactPreKeys', + database: Whisper.Database, + model: ContactPreKey, + fetchBy(filter) { + return this.fetch({ conditions: filter, }); + }, + }); const ContactSignedPreKey = Model.extend({ storeName: 'contactSignedPreKeys' }); + const ContactSignedPreKeyCollection = Backbone.Collection.extend({ + storeName: 'contactSignedPreKeys', + database: Whisper.Database, + model: ContactSignedPreKey, + fetchBy(filter) { + return this.fetch({ conditions: filter, }); + }, + }); function SignalProtocolStore() {} @@ -261,6 +277,24 @@ ); }); }, + loadContactPreKeys(filters) { + const contactPreKeys = new ContactPreKeyCollection(); + return new Promise((resolve, reject) => { + contactPreKeys.fetchBy(filters).then(() => { + resolve( + contactPreKeys.map(prekey => ({ + id: prekey.get('id'), + keyId: prekey.get('keyId'), + publicKey: prekey.get('publicKey'), + identityKeyString: prekey.get('identityKeyString'), + })) + ); + }).fail(e => { + window.log.error('Failed to fetch signed prekey with filters', filters); + reject(e); + }); + }); + }, storeContactPreKey(pubKey, preKey) { const prekey = new ContactPreKey({ // id: (autoincrement) @@ -340,6 +374,27 @@ }); }); }, + loadContactSignedPreKeys(filters) { + const contactSignedPreKeys = new ContactSignedPreKeyCollection(); + return new Promise((resolve, reject) => { + contactSignedPreKeys.fetchBy(filters).then(() => { + resolve( + contactSignedPreKeys.map(prekey => ({ + id: prekey.get('id'), + identityKeyString: prekey.get('identityKeyString'), + publicKey: prekey.get('publicKey'), + signature: prekey.get('signature'), + created_at: prekey.get('created_at'), + keyId: prekey.get('keyId'), + confirmed: prekey.get('confirmed'), + })) + ); + }).fail(e => { + window.log.error('Failed to fetch signed prekey with filters', filters); + reject(e); + }); + }); + }, loadContactSignedPreKey(pubKey) { const prekey = new ContactSignedPreKey({ identityKeyString: pubKey }); return new Promise(resolve => { diff --git a/libloki/libloki-protocol.js b/libloki/libloki-protocol.js index 059b261cb..37f89ff6b 100644 --- a/libloki/libloki-protocol.js +++ b/libloki/libloki-protocol.js @@ -1,4 +1,4 @@ -/* global window, libsignal, textsecure */ +/* global window, libsignal, textsecure, OutgoingMessage */ // eslint-disable-next-line func-names (function() { @@ -74,20 +74,77 @@ ]); const preKeyMessage = new textsecure.protobuf.PreKeyBundleMessage({ - identityKey, + identityKey: new Uint8Array(identityKey), deviceId: 1, // TODO: fetch from somewhere preKeyId: preKey.keyId, signedKeyId, - preKey: preKey.pubKey, - signedKey: signedKey.pubKey, - signature: signedKey.signature, + preKey: new Uint8Array(preKey.pubKey), + signedKey: new Uint8Array(signedKey.pubKey), + signature: new Uint8Array(signedKey.signature), }); return preKeyMessage; } + + savePreKeyBundleForNumber = async function({ pubKey, preKeyId, preKey, signedKeyId, signedKey, signature }) { + const signedKeyPromise = new Promise(async (resolve) => { + const existingSignedKeys = await textsecure.storage.protocol.loadContactSignedPreKeys({ identityKeyString: pubKey, keyId: signedKeyId }); + if (!existingSignedKeys || (existingSignedKeys instanceof Array && existingSignedKeys.length == 0)) + { + const signedPreKey = { + keyId: signedKeyId, + publicKey: signedKey, + signature, + }; + await textsecure.storage.protocol.storeContactSignedPreKey(pubKey, signedPreKey); + } + resolve(); + }); + + const preKeyPromise = new Promise(async (resolve) => { + const existingPreKeys = textsecure.storage.protocol.loadContactPreKeys({ identityKeyString: pubKey, keyId: preKeyId }); + if (!existingPreKeys || (existingPreKeys instanceof Array && existingPreKeys.length == 0)) + { + const preKeyObject = { + publicKey: preKey, + keyId: preKeyId, + } + await textsecure.storage.protocol.storeContactPreKey(pubKey, preKeyObject); + } + resolve(); + }); + + await Promise.all([signedKeyPromise, preKeyPromise]); + } + + sendEmptyMessageWithPreKeys = async function(pubKey) { + // empty content message + const content = new textsecure.protobuf.Content(); + + // will be called once the transmission succeeded or failed + const callback = res => { + if (res.errors.length > 0) { + res.errors.forEach(error => console.error(error)); + } else { + console.log('empty message sent successfully'); + } + }; + // send an empty message. The logic in ougoing_message will attach the prekeys. + const outgoingMessage = new textsecure.OutgoingMessage( + null, //server + Date.now(), //timestamp, + [pubKey], //numbers + content, //message + true, //silent + callback, //callback + ); + await outgoingMessage.sendToNumber(pubKey); + } window.libloki.FallBackSessionCipher = FallBackSessionCipher; window.libloki.getPreKeyBundleForNumber = getPreKeyBundleForNumber; window.libloki.FallBackDecryptionError = FallBackDecryptionError; + window.libloki.savePreKeyBundleForNumber = savePreKeyBundleForNumber; + window.libloki.sendEmptyMessageWithPreKeys = sendEmptyMessageWithPreKeys; })(); \ No newline at end of file diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 4176dfa7b..1499a647b 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -836,8 +836,13 @@ MessageReceiver.prototype.extend({ } }); }, - innerHandleContentMessage(envelope, plaintext) { + async innerHandleContentMessage(envelope, plaintext) { const content = textsecure.protobuf.Content.decode(plaintext); + + if (content.preKeyBundleMessage) { + await this.handlePreKeyBundleMessage(envelope, content.preKeyBundleMessage); + } + if (content.syncMessage) { return this.handleSyncMessage(envelope, content.syncMessage); } else if (content.dataMessage) { @@ -1059,6 +1064,30 @@ MessageReceiver.prototype.extend({ return this.removeFromCache(envelope); }, + async handlePreKeyBundleMessage(envelope, preKeyBundleMessage) { + + const { preKeyId, signedKeyId } = preKeyBundleMessage; + const [ identityKey, preKey, signedKey, signature ] = [ + preKeyBundleMessage.identityKey, + preKeyBundleMessage.preKey, + preKeyBundleMessage.signedKey, + preKeyBundleMessage.signature + ].map(k => dcodeIO.ByteBuffer.wrap(k).toArrayBuffer()); + + if (envelope.source != StringView.arrayBufferToHex(identityKey)) { + throw new Error("Error in handlePreKeyBundleMessage: envelope pubkey does not match pubkey in prekey bundle"); + } + const pubKey = envelope.source; + + return await libloki.savePreKeyBundleForNumber({ + pubKey, + preKeyId, + signedKeyId, + preKey, + signedKey, + signature, + }); + }, isBlocked(number) { return textsecure.storage.get('blocked', []).indexOf(number) >= 0; }, diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 0b6f4501c..70a6fed8a 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -116,7 +116,7 @@ OutgoingMessage.prototype = { if (updateDevices === undefined) { return this.server.getKeysForNumber(number).then(handleResult); } - let promise = Promise.resolve(); + let promise = Promise.resolve(true); updateDevices.forEach(device => { promise = promise.then(() => Promise.all([ @@ -217,7 +217,7 @@ OutgoingMessage.prototype = { request: requestMessage }); const bytes = new Uint8Array(websocketMessage.encode().toArrayBuffer()) - bytes.toString(); // print bytes for debugging purposes: can be injected in mock socket server + console.log(bytes.toString()); // print bytes for debugging purposes: can be injected in mock socket server return bytes; }, doSendMessage(number, deviceIds, recurse) { @@ -413,3 +413,6 @@ OutgoingMessage.prototype = { ); }, }; + +window.textsecure = window.textsecure || {}; +window.textsecure.OutgoingMessage = OutgoingMessage;