diff --git a/libloki/libloki-protocol.js b/libloki/libloki-protocol.js deleted file mode 100644 index 252e423e4..000000000 --- a/libloki/libloki-protocol.js +++ /dev/null @@ -1,167 +0,0 @@ -/* global window, libsignal, textsecure, StringView, log */ - -// eslint-disable-next-line func-names -(function () { - window.libloki = window.libloki || {}; - - class FallBackDecryptionError extends Error { } - - const IV_LENGTH = 16; - - class FallBackSessionCipher { - - constructor(address) { - this.identityKeyString = address.getName(); - this.pubKey = StringView.hexToArrayBuffer(address.getName()); - } - - async encrypt(plaintext) { - const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair(); - const myPrivateKey = myKeyPair.privKey; - const symmetricKey = libsignal.Curve.calculateAgreement(this.pubKey, myPrivateKey); - const iv = libsignal.crypto.getRandomBytes(IV_LENGTH); - const ciphertext = await libsignal.crypto.encrypt(symmetricKey, plaintext, iv); - const ivAndCiphertext = new Uint8Array(iv.byteLength + ciphertext.byteLength); - ivAndCiphertext.set(new Uint8Array(iv)); - ivAndCiphertext.set(new Uint8Array(ciphertext), iv.byteLength); - return { - type: textsecure.protobuf.Envelope.Type.FRIEND_REQUEST, - body: ivAndCiphertext, - registrationId: null, - }; - } - - async decrypt(ivAndCiphertext) { - const iv = ivAndCiphertext.slice(0, IV_LENGTH); - const cipherText = ivAndCiphertext.slice(IV_LENGTH); - const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair(); - const myPrivateKey = myKeyPair.privKey; - const symmetricKey = libsignal.Curve.calculateAgreement(this.pubKey, myPrivateKey); - try { - return await libsignal.crypto.decrypt(symmetricKey, cipherText, iv); - } - catch (e) { - throw new FallBackDecryptionError(`Could not decrypt message from ${this.identityKeyString} using FallBack encryption.`); - } - } - } - - async function getPreKeyBundleForContact(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', 2) - 1; - - const [signedKey, preKey] = await Promise.all([ - textsecure.storage.protocol.loadSignedPreKey(signedKeyId), - new Promise(async resolve => { - // retrieve existing prekey if we already generated one for that recipient - const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContact( - 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 newPreKey = await libsignal.KeyHelper.generatePreKey(preKeyId); - await textsecure.storage.protocol.storePreKey( - newPreKey.keyId, - newPreKey.keyPair, - pubKey - ); - resolve({ pubKey: newPreKey.keyPair.pubKey, keyId: preKeyId }); - } - }), - ]); - - return { - identityKey: new Uint8Array(identityKey), - deviceId: 1, // TODO: fetch from somewhere - preKeyId: preKey.keyId, - signedKeyId, - preKey: new Uint8Array(preKey.pubKey), - signedKey: new Uint8Array(signedKey.pubKey), - signature: new Uint8Array(signedKey.signature), - }; - } - - async function saveContactPreKeyBundle({ - pubKey, - preKeyId, - preKey, - signedKeyId, - signedKey, - signature, - }) { - const signedPreKey = { - keyId: signedKeyId, - publicKey: signedKey, - signature, - }; - - const signedKeyPromise = textsecure.storage.protocol.storeContactSignedPreKey( - pubKey, - signedPreKey - ); - - const preKeyObject = { - publicKey: preKey, - keyId: preKeyId, - }; - - const preKeyPromise = textsecure.storage.protocol.storeContactPreKey( - pubKey, - preKeyObject - ); - - await Promise.all([signedKeyPromise, preKeyPromise]); - } - - async function removeContactPreKeyBundle(pubKey) { - await Promise.all([ - textsecure.storage.protocol.removeContactPreKey(pubKey), - textsecure.storage.protocol.removeContactSignedPreKey(pubKey), - ]); - } - - async function sendFriendRequestAccepted(pubKey) { - return sendEmptyMessage(pubKey); - } - - async function sendEmptyMessage(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 => log.error(error)); - } else { - log.info('empty message sent successfully'); - } - }; - const options = {}; - // 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 - options - ); - await outgoingMessage.sendToNumber(pubKey); - } - - window.libloki.FallBackSessionCipher = FallBackSessionCipher; - window.libloki.getPreKeyBundleForContact = getPreKeyBundleForContact; - window.libloki.FallBackDecryptionError = FallBackDecryptionError; - window.libloki.saveContactPreKeyBundle = saveContactPreKeyBundle; - window.libloki.removeContactPreKeyBundle = removeContactPreKeyBundle; - window.libloki.sendFriendRequestAccepted = sendFriendRequestAccepted; - window.libloki.sendEmptyMessage = sendEmptyMessage; -})(); diff --git a/libloki/test/crypto_test.js b/libloki/test/crypto_test.js new file mode 100644 index 000000000..15dfa02f0 --- /dev/null +++ b/libloki/test/crypto_test.js @@ -0,0 +1,38 @@ +/* global libsignal, libloki, textsecure, StringView */ + +'use strict'; + +describe('Crypto', () => { + describe('FallBackSessionCipher', () => { + let fallbackCipher; + let identityKey; + let address; + const store = textsecure.storage.protocol; + + before(async () => { + clearDatabase(); + identityKey = await libsignal.KeyHelper.generateIdentityKeyPair(); + store.put('identityKey', identityKey); + const key = libsignal.crypto.getRandomBytes(32); + const pubKeyString = StringView.arrayBufferToHex(key); + address = new libsignal.SignalProtocolAddress( + pubKeyString, + 1 + ); + fallbackCipher = new libloki.crypto.FallBackSessionCipher(address); + }); + + it('should encrypt fallback cipher messages as friend requests', async () => { + const buffer = new ArrayBuffer(10); + const { type } = await fallbackCipher.encrypt(buffer); + assert.strictEqual(type, textsecure.protobuf.Envelope.Type.FRIEND_REQUEST); + }); + + it('should encrypt and then decrypt a message with the same result', async () => { + const arr = new Uint8Array([1,2,3,4,5]); + const { body } = await fallbackCipher.encrypt(arr.buffer); + const result = await fallbackCipher.decrypt(body); + assert.deepEqual(result, arr.buffer); + }); + }); +}); diff --git a/libloki/test/index.html b/libloki/test/index.html index e690b0bb8..f9eb9c2e4 100644 --- a/libloki/test/index.html +++ b/libloki/test/index.html @@ -29,9 +29,10 @@ + - + diff --git a/libloki/test/libloki-protocol_test.js b/libloki/test/libloki-protocol_test.js deleted file mode 100644 index 53c9d923a..000000000 --- a/libloki/test/libloki-protocol_test.js +++ /dev/null @@ -1,103 +0,0 @@ -/* global libsignal, libloki, textsecure, StringView */ - -'use strict'; - -describe('FallBackSessionCipher', () => { - let fallbackCipher; - let identityKey; - let address; - const store = textsecure.storage.protocol; - - before(async () => { - clearDatabase(); - identityKey = await libsignal.KeyHelper.generateIdentityKeyPair(); - store.put('identityKey', identityKey); - const key = libsignal.crypto.getRandomBytes(32); - const pubKeyString = StringView.arrayBufferToHex(key); - address = new libsignal.SignalProtocolAddress( - pubKeyString, - 1 - ); - fallbackCipher = new libloki.crypto.FallBackSessionCipher(address); - }); - - it('should encrypt fallback cipher messages as friend requests', async () => { - const buffer = new ArrayBuffer(10); - const { type } = await fallbackCipher.encrypt(buffer); - assert.strictEqual(type, textsecure.protobuf.Envelope.Type.FRIEND_REQUEST); - }); - - it('should encrypt and then decrypt a message with the same result', async () => { - const arr = new Uint8Array([1,2,3,4,5]); - const { body } = await fallbackCipher.encrypt(arr.buffer); - const result = await fallbackCipher.decrypt(body); - assert.deepEqual(result, arr.buffer); - }); -}); - -describe('LibLoki Protocol', () => { - let testKey; - const store = textsecure.storage.protocol; - - beforeEach(async () => { - clearDatabase(); - testKey = { - pubKey: libsignal.crypto.getRandomBytes(33), - privKey: libsignal.crypto.getRandomBytes(32), - }; - textsecure.storage.put('signedKeyId', 2); - await store.storeSignedPreKey(1, testKey); - }); - - it('should generate a new prekey bundle for a new contact', async () => { - const pubKey = libsignal.crypto.getRandomBytes(32); - const pubKeyString = StringView.arrayBufferToHex(pubKey); - const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1); - const newBundle = await libloki.storage.getPreKeyBundleForContact(pubKeyString); - const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1); - assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1); - - const testKeyArray = new Uint8Array(testKey.pubKey); - assert.isDefined(newBundle); - assert.isDefined(newBundle.identityKey); - assert.isDefined(newBundle.deviceId); - assert.isDefined(newBundle.preKeyId); - assert.isDefined(newBundle.signedKeyId); - assert.isDefined(newBundle.preKey); - assert.isDefined(newBundle.signedKey); - assert.isDefined(newBundle.signature); - assert.strictEqual(testKeyArray.byteLength, newBundle.signedKey.byteLength); - for (let i = 0 ; i !== testKeyArray.byteLength; i += 1) - assert.strictEqual(testKeyArray[i], newBundle.signedKey[i]); - }); - - it('should return the same prekey bundle after creating a contact', async () => { - const pubKey = libsignal.crypto.getRandomBytes(32); - const pubKeyString = StringView.arrayBufferToHex(pubKey); - const bundle1 = await libloki.storage.getPreKeyBundleForContact(pubKeyString); - const bundle2 = await libloki.storage.getPreKeyBundleForContact(pubKeyString); - assert.isDefined(bundle1); - assert.isDefined(bundle2); - assert.deepEqual(bundle1, bundle2); - }); - - it('should save the signed keys and prekeys from a bundle', async () => { - const pubKey = libsignal.crypto.getRandomBytes(32); - const pubKeyString = StringView.arrayBufferToHex(pubKey); - const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1); - const newBundle = await libloki.storage.getPreKeyBundleForContact(pubKeyString); - const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1); - assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1); - - const testKeyArray = new Uint8Array(testKey.pubKey); - assert.isDefined(newBundle); - assert.isDefined(newBundle.identityKey); - assert.isDefined(newBundle.deviceId); - assert.isDefined(newBundle.preKeyId); - assert.isDefined(newBundle.signedKeyId); - assert.isDefined(newBundle.preKey); - assert.isDefined(newBundle.signedKey); - assert.isDefined(newBundle.signature); - assert.deepEqual(testKeyArray, newBundle.signedKey); - }); -}); diff --git a/libloki/test/storage_test.js b/libloki/test/storage_test.js new file mode 100644 index 000000000..dc883f06b --- /dev/null +++ b/libloki/test/storage_test.js @@ -0,0 +1,72 @@ +/* global libsignal, libloki, textsecure, StringView */ + +'use strict'; + +describe('Storage', () => { + let testKey; + const store = textsecure.storage.protocol; + + describe('#getPreKeyBundleForContact', () => { + beforeEach(async () => { + clearDatabase(); + testKey = { + pubKey: libsignal.crypto.getRandomBytes(33), + privKey: libsignal.crypto.getRandomBytes(32), + }; + textsecure.storage.put('signedKeyId', 2); + await store.storeSignedPreKey(1, testKey); + }); + + it('should generate a new prekey bundle for a new contact', async () => { + const pubKey = libsignal.crypto.getRandomBytes(32); + const pubKeyString = StringView.arrayBufferToHex(pubKey); + const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1); + const newBundle = await libloki.storage.getPreKeyBundleForContact(pubKeyString); + const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1); + assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1); + + const testKeyArray = new Uint8Array(testKey.pubKey); + assert.isDefined(newBundle); + assert.isDefined(newBundle.identityKey); + assert.isDefined(newBundle.deviceId); + assert.isDefined(newBundle.preKeyId); + assert.isDefined(newBundle.signedKeyId); + assert.isDefined(newBundle.preKey); + assert.isDefined(newBundle.signedKey); + assert.isDefined(newBundle.signature); + assert.strictEqual(testKeyArray.byteLength, newBundle.signedKey.byteLength); + for (let i = 0 ; i !== testKeyArray.byteLength; i += 1) + assert.strictEqual(testKeyArray[i], newBundle.signedKey[i]); + }); + + it('should return the same prekey bundle after creating a contact', async () => { + const pubKey = libsignal.crypto.getRandomBytes(32); + const pubKeyString = StringView.arrayBufferToHex(pubKey); + const bundle1 = await libloki.storage.getPreKeyBundleForContact(pubKeyString); + const bundle2 = await libloki.storage.getPreKeyBundleForContact(pubKeyString); + assert.isDefined(bundle1); + assert.isDefined(bundle2); + assert.deepEqual(bundle1, bundle2); + }); + + it('should save the signed keys and prekeys from a bundle', async () => { + const pubKey = libsignal.crypto.getRandomBytes(32); + const pubKeyString = StringView.arrayBufferToHex(pubKey); + const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1); + const newBundle = await libloki.storage.getPreKeyBundleForContact(pubKeyString); + const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1); + assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1); + + const testKeyArray = new Uint8Array(testKey.pubKey); + assert.isDefined(newBundle); + assert.isDefined(newBundle.identityKey); + assert.isDefined(newBundle.deviceId); + assert.isDefined(newBundle.preKeyId); + assert.isDefined(newBundle.signedKeyId); + assert.isDefined(newBundle.preKey); + assert.isDefined(newBundle.signedKey); + assert.isDefined(newBundle.signature); + assert.deepEqual(testKeyArray, newBundle.signedKey); + }); + }); +});