From 982f10a50513486281a4e820f5843da1e510f2f0 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 10 Jan 2019 09:59:45 +1100 Subject: [PATCH] Split libloki logic. Added to gruntfile. --- Gruntfile.js | 4 +- libloki/api.js | 41 ++++++++++++++++++ libloki/crypto.js | 53 +++++++++++++++++++++++ libloki/service_nodes.js | 5 ++- libloki/storage.js | 93 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 libloki/api.js create mode 100644 libloki/crypto.js create mode 100644 libloki/storage.js diff --git a/Gruntfile.js b/Gruntfile.js index 9ca02afd2..8921a0d79 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -101,8 +101,10 @@ module.exports = grunt => { }, libloki: { src: [ - 'libloki/libloki-protocol.js', + 'libloki/api.js', + 'libloki/crypto.js', 'libloki/service_nodes.js', + 'libloki/storage.js', ], dest: 'js/libloki.js', }, diff --git a/libloki/api.js b/libloki/api.js new file mode 100644 index 000000000..b714e2cb5 --- /dev/null +++ b/libloki/api.js @@ -0,0 +1,41 @@ +/* global window, textsecure, log */ + +// eslint-disable-next-line func-names +(function () { + window.libloki = window.libloki || {}; + + 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.api = { + sendFriendRequestAccepted, + sendEmptyMessage, + }; +})(); diff --git a/libloki/crypto.js b/libloki/crypto.js new file mode 100644 index 000000000..1042f59c2 --- /dev/null +++ b/libloki/crypto.js @@ -0,0 +1,53 @@ +/* global window, libsignal, textsecure, StringView */ + +// 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.`); + } + } + } + + window.libloki.crypto = { + FallBackSessionCipher, + FallBackDecryptionError, + }; +})(); diff --git a/libloki/service_nodes.js b/libloki/service_nodes.js index 21e676493..58698bd7e 100644 --- a/libloki/service_nodes.js +++ b/libloki/service_nodes.js @@ -3,7 +3,6 @@ // eslint-disable-next-line func-names (function () { window.libloki = window.libloki || {}; - window.libloki.serviceNodes = window.libloki.serviceNodes || {}; function consolidateLists(lists, threshold = 1){ if (typeof threshold !== 'number') { @@ -31,5 +30,7 @@ .map(keyValue => keyValue[0]); } - window.libloki.serviceNodes.consolidateLists = consolidateLists; + window.libloki.serviceNodes = { + consolidateLists, + }; })(); diff --git a/libloki/storage.js b/libloki/storage.js new file mode 100644 index 000000000..654addc48 --- /dev/null +++ b/libloki/storage.js @@ -0,0 +1,93 @@ +/* global window, libsignal, textsecure */ + +// eslint-disable-next-line func-names +(function () { + window.libloki = window.libloki || {}; + + 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), + ]); + } + + window.libloki.storage = { + getPreKeyBundleForContact, + saveContactPreKeyBundle, + removeContactPreKeyBundle, + }; +})();