From aa722590fab4e79aa8247cc6162f7ad547d5add4 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Wed, 27 Feb 2019 13:35:46 +1100 Subject: [PATCH 1/4] use channel encryption with storage server --- js/modules/loki_message_api.js | 39 +++++++++++++++++++++++++--- libloki/crypto.js | 47 ++++++++++++++++++++++++++-------- package.json | 1 + preload.js | 7 +++++ 4 files changed, 80 insertions(+), 14 deletions(-) diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 599ddc282..0972a9831 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -1,11 +1,13 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-loop-func */ -/* global log, dcodeIO, window, callWorker, lokiP2pAPI, lokiSnodeAPI */ +/* global log, dcodeIO, window, callWorker, lokiP2pAPI, lokiSnodeAPI, libloki */ const nodeFetch = require('node-fetch'); const _ = require('lodash'); +const { parse } = require('url'); const endpointBase = '/v1/storage_rpc'; +const LOKI_EPHEMKEY_HEADER = 'X-Loki-EphemKey'; class HTTPError extends Error { constructor(response) { @@ -27,6 +29,25 @@ const fetch = async (url, options = {}) => { const timeout = options.timeout || 10000; const method = options.method || 'GET'; + const address = parse(url).hostname; + const doEncryptChannel = + address.endsWith('.snode') && + options.headers && + LOKI_EPHEMKEY_HEADER in options.headers; + if (doEncryptChannel) { + try { + // eslint-disable-next-line no-param-reassign + options.body = await libloki.crypto.snodeCipher.encrypt( + address, + options.body + ); + // eslint-disable-next-line no-param-reassign + options.headers['Content-Type'] = 'text/plain'; + } catch (e) { + log.warn(`Could not encrypt channel for ${address}: `, e); + } + } + try { const response = await nodeFetch(url, { ...options, @@ -45,6 +66,18 @@ const fetch = async (url, options = {}) => { result = await response.buffer(); } else { result = await response.text(); + if (doEncryptChannel) { + try { + result = await libloki.crypto.snodeCipher.decrypt(address, result); + } catch (e) { + log.warn(`Could not decrypt response from ${address}`, e); + } + try { + result = JSON.parse(result); + } catch(e) { + log.warn(`Could not parse string to json ${result}`, e); + } + } } return result; @@ -151,7 +184,7 @@ class LokiMessageAPI { method: 'POST', body: JSON.stringify(body), headers: { - 'X-Loki-EphemKey': 'not implemented yet', + [LOKI_EPHEMKEY_HEADER]: libloki.crypto.snodeCipher.getChannelPublicKeyHex(), }, }; @@ -247,7 +280,7 @@ class LokiMessageAPI { }, }; const headers = { - 'X-Loki-EphemKey': 'not implemented yet', + [LOKI_EPHEMKEY_HEADER]: libloki.crypto.snodeCipher.getChannelPublicKeyHex(), }; const fetchOptions = { method: 'POST', diff --git a/libloki/crypto.js b/libloki/crypto.js index 858d17898..f97a5a3fd 100644 --- a/libloki/crypto.js +++ b/libloki/crypto.js @@ -1,4 +1,13 @@ -/* global window, libsignal, textsecure, StringView, Multibase */ +/* global + window, + libsignal, + textsecure, + StringView, + Multibase, + TextEncoder, + TextDecoder, + dcodeIO +*/ // eslint-disable-next-line func-names (function() { @@ -81,7 +90,7 @@ return ab; } - function decodeSnodeAddressToBuffer(snodeAddress) { + function decodeSnodeAddressToPubKey(snodeAddress) { const snodeAddressClean = snodeAddress .replace('.snode', '') .replace('http://', ''); @@ -99,12 +108,16 @@ this._cache = {}; } - _getSymmetricKey(snodeAddress) { + async _getSymmetricKey(snodeAddress) { if (snodeAddress in this._cache) { return this._cache[snodeAddress]; } - const buffer = decodeSnodeAddressToBuffer(snodeAddress); - const snodePubKeyArrayBuffer = bufferToArrayBuffer(buffer); + const ed25519PubKey = decodeSnodeAddressToPubKey(snodeAddress); + const sodium = await window.getSodium(); + const curve25519PubKey = sodium.crypto_sign_ed25519_pk_to_curve25519( + ed25519PubKey + ); + const snodePubKeyArrayBuffer = bufferToArrayBuffer(curve25519PubKey); const symmetricKey = libsignal.Curve.calculateAgreement( snodePubKeyArrayBuffer, this._ephemeralKeyPair.privKey @@ -117,18 +130,30 @@ return this._ephemeralPubKeyHex; } - async decrypt(snodeAddress, ivAndCipherText) { - const symmetricKey = this._getSymmetricKey(snodeAddress); + async decrypt(snodeAddress, ivAndCipherTextBase64) { + const ivAndCipherText = dcodeIO.ByteBuffer.wrap( + ivAndCipherTextBase64, + 'base64' + ).toArrayBuffer(); + const symmetricKey = await this._getSymmetricKey(snodeAddress); try { - return await DHDecrypt(symmetricKey, ivAndCipherText); + const decrypted = await DHDecrypt(symmetricKey, ivAndCipherText); + const decoder = new TextDecoder(); + return decoder.decode(decrypted); } catch (e) { return ivAndCipherText; } } async encrypt(snodeAddress, plainText) { - const symmetricKey = this._getSymmetricKey(snodeAddress); - return DHEncrypt(symmetricKey, plainText); + if (typeof plainText === 'string') { + const textEncoder = new TextEncoder(); + // eslint-disable-next-line no-param-reassign + plainText = textEncoder.encode(plainText); + } + const symmetricKey = await this._getSymmetricKey(snodeAddress); + const cipherText = await DHEncrypt(symmetricKey, plainText); + return dcodeIO.ByteBuffer.wrap(cipherText).toString('base64'); } } @@ -142,6 +167,6 @@ snodeCipher, // for testing _LokiSnodeChannel: LokiSnodeChannel, - _decodeSnodeAddressToBuffer: decodeSnodeAddressToBuffer, + _decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey, }; })(); diff --git a/package.json b/package.json index 110f42128..aee86ff2f 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "jquery": "3.3.1", "js-sha512": "0.8.0", "jsbn": "1.1.0", + "libsodium-wrappers": "^0.7.4", "linkify-it": "2.0.3", "lodash": "4.17.11", "mkdirp": "0.5.1", diff --git a/preload.js b/preload.js index a01aec06f..fe3fdd3a3 100644 --- a/preload.js +++ b/preload.js @@ -339,6 +339,13 @@ window.React = require('react'); window.ReactDOM = require('react-dom'); window.moment = require('moment'); +const _sodium = require('libsodium-wrappers'); + +window.getSodium = async () => { + await _sodium.ready; + return _sodium; +}; + window.clipboard = clipboard; const Signal = require('./js/modules/signal'); From 3285b2d1b90a4c3f7be8bdd9c3d6d1aaa4d0bdca Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Wed, 27 Feb 2019 13:36:10 +1100 Subject: [PATCH 2/4] Update snode channel tests --- libloki/test/snode_channel_test.js | 60 ++++++++++++++++++------------ 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/libloki/test/snode_channel_test.js b/libloki/test/snode_channel_test.js index 692633c55..d6cd918a9 100644 --- a/libloki/test/snode_channel_test.js +++ b/libloki/test/snode_channel_test.js @@ -1,16 +1,21 @@ -/* global libloki, Multibase, libsignal, StringView */ +/* global libloki, Multibase, libsignal, StringView, dcodeIO */ 'use strict'; -function generateSnodeKeysAndAddress() { - const keyPair = libsignal.Curve.generateKeyPair(); - // Signal protocol prepends with "0x05" - keyPair.pubKey = keyPair.pubKey.slice(1); +async function generateSnodeKeysAndAddress() { + // snode identitys is a ed25519 keypair + const sodium = await window.getSodium(); + const ed25519KeyPair = sodium.crypto_sign_keypair(); + const keyPair = { + pubKey: ed25519KeyPair.publicKey, + privKey: ed25519KeyPair.privateKey, + }; + // snode address is the pubkey in base32z let address = Multibase.encode( 'base32z', Multibase.Buffer.from(keyPair.pubKey) ).toString(); - // first letter is the encoding code + // remove first letter, which is the encoding code address = address.substring(1); return { keyPair, address }; } @@ -25,11 +30,11 @@ describe('Snode Channel', () => { }); }); - describe('#decodeSnodeAddressToBuffer', () => { - it('should decode a base32z encoded .snode address', () => { - const { keyPair, address } = generateSnodeKeysAndAddress(); + describe('#decodeSnodeAddressToPubKey', () => { + it('should decode a base32z encoded .snode address', async () => { + const { keyPair, address } = await generateSnodeKeysAndAddress(); - const buffer = libloki.crypto._decodeSnodeAddressToBuffer( + const buffer = libloki.crypto._decodeSnodeAddressToPubKey( `http://${address}.snode` ); @@ -55,15 +60,15 @@ describe('Snode Channel', () => { assert.strictEqual(channel.getChannelPublicKeyHex(), pubKeyHex); }); - it('should cache something by snode address', () => { - const { address } = generateSnodeKeysAndAddress(); + it('should cache something by snode address', async () => { + const { address } = await generateSnodeKeysAndAddress(); const channel = new libloki.crypto._LokiSnodeChannel(); // cache should be empty assert.strictEqual(Object.keys(channel._cache).length, 0); // push to cache - channel._getSymmetricKey(address); + await channel._getSymmetricKey(address); assert.strictEqual(Object.keys(channel._cache).length, 1); assert.strictEqual(Object.keys(channel._cache)[0], address); @@ -71,7 +76,7 @@ describe('Snode Channel', () => { it('should encrypt data correctly', async () => { // message sent by Loki Messenger - const snode = generateSnodeKeysAndAddress(); + const snode = await generateSnodeKeysAndAddress(); const messageSent = 'I am Groot'; const textEncoder = new TextEncoder(); const data = textEncoder.encode(messageSent); @@ -79,17 +84,22 @@ describe('Snode Channel', () => { const channel = new libloki.crypto._LokiSnodeChannel(); const encrypted = await channel.encrypt(snode.address, data); - assert.isTrue(encrypted instanceof Uint8Array); + assert.strictEqual(typeof encrypted, 'string'); // message received by storage server const senderPubKey = StringView.hexToArrayBuffer( channel.getChannelPublicKeyHex() ); + const sodium = await window.getSodium(); + const snodePrivKey = sodium.crypto_sign_ed25519_sk_to_curve25519( + snode.keyPair.privKey + ).buffer; const symmetricKey = libsignal.Curve.calculateAgreement( senderPubKey, - snode.keyPair.privKey + snodePrivKey ); - const decrypted = await libloki.crypto.DHDecrypt(symmetricKey, encrypted); + const encryptedArrayBuffer = dcodeIO.ByteBuffer.wrap(encrypted, 'base64').toArrayBuffer(); + const decrypted = await libloki.crypto.DHDecrypt(symmetricKey, encryptedArrayBuffer); const textDecoder = new TextDecoder(); const messageReceived = textDecoder.decode(decrypted); assert.strictEqual(messageSent, messageReceived); @@ -98,24 +108,26 @@ describe('Snode Channel', () => { it('should decrypt data correctly', async () => { const channel = new libloki.crypto._LokiSnodeChannel(); // message sent by storage server - const snode = generateSnodeKeysAndAddress(); + const snode = await generateSnodeKeysAndAddress(); const messageSent = 'You are Groot'; const textEncoder = new TextEncoder(); const data = textEncoder.encode(messageSent); const senderPubKey = StringView.hexToArrayBuffer( channel.getChannelPublicKeyHex() ); + const sodium = await window.getSodium(); + const snodePrivKey = sodium.crypto_sign_ed25519_sk_to_curve25519( + snode.keyPair.privKey + ).buffer; const symmetricKey = libsignal.Curve.calculateAgreement( senderPubKey, - snode.keyPair.privKey + snodePrivKey ); const encrypted = await libloki.crypto.DHEncrypt(symmetricKey, data); - + const encryptedBase64 = dcodeIO.ByteBuffer.wrap(encrypted).toString('base64'); // message received by Loki Messenger - const decrypted = await channel.decrypt(snode.address, encrypted); - const textDecoder = new TextDecoder(); - const messageReceived = textDecoder.decode(decrypted); - assert.strictEqual(messageSent, messageReceived); + const decrypted = await channel.decrypt(snode.address, encryptedBase64); + assert.strictEqual(messageSent, decrypted); }); }); }); From 16b96f3d210aad39fc28bd3c05377e0c05f9ce9f Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Wed, 27 Feb 2019 13:36:21 +1100 Subject: [PATCH 3/4] update yarn.lock --- yarn.lock | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index b48169ffd..7e7549465 100644 --- a/yarn.lock +++ b/yarn.lock @@ -758,13 +758,6 @@ balanced-match@^0.4.1, balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" -base-x@3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.4.tgz#94c1788736da065edb1d68808869e357c977fa77" - integrity sha512-UYOadoSIkEI/VrRGSG6qp93rp2WdokiAiNYDfGW5qURAY8GiAQkvMbwNNSDYiVJopqv4gCna7xqf4rrNGp+5AA== - dependencies: - safe-buffer "^5.0.1" - base64-js@^1.0.2: version "1.2.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" @@ -5081,6 +5074,18 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +libsodium-wrappers@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.4.tgz#cdb3ce6553e4864c0a68070c4313583489bd765d" + integrity sha512-axKkW01L0q+urLeE7UMSZKWwk4LrRbi6s5pjKBAvbgDBYnsSaolK1oN/Syilm1dqJFkJQNi6qodwOp8dzSoc9Q== + dependencies: + libsodium "0.7.4" + +libsodium@0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.4.tgz#a5bccd65e3a13b34147ea109be3c65d89f90b074" + integrity sha512-fTU3vUdrxQzhPAAjmTSqKk4LzYbR0OtcYjp1P92AlH50JIxXZFEIXWh1yryCmU6RLGfwS2IzBdZjbmpYf/TlyQ== + lie@*: version "3.2.0" resolved "https://registry.yarnpkg.com/lie/-/lie-3.2.0.tgz#4f13f2f8bbb027d383db338c43043545791aa8dc" @@ -5724,13 +5729,6 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -multibase@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/multibase/-/multibase-0.6.0.tgz#0216e350614c7456da5e8e5b20d3fcd4c9104f56" - integrity sha512-R9bNLQhbD7MsitPm1NeY7w9sDgu6d7cuj25snAWH7k5PSNPSwIQQBpcpj8jx1W96dLbdigZqmUWOdQRMnAmgjA== - dependencies: - base-x "3.0.4" - multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" From ad5efc9ba385df7562179709bb66462d9a21a2c3 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Wed, 27 Feb 2019 14:07:02 +1100 Subject: [PATCH 4/4] lint --- js/modules/loki_message_api.js | 2 +- libloki/test/snode_channel_test.js | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 0972a9831..b5ba47d0c 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -74,7 +74,7 @@ const fetch = async (url, options = {}) => { } try { result = JSON.parse(result); - } catch(e) { + } catch (e) { log.warn(`Could not parse string to json ${result}`, e); } } diff --git a/libloki/test/snode_channel_test.js b/libloki/test/snode_channel_test.js index d6cd918a9..9960dde68 100644 --- a/libloki/test/snode_channel_test.js +++ b/libloki/test/snode_channel_test.js @@ -98,8 +98,14 @@ describe('Snode Channel', () => { senderPubKey, snodePrivKey ); - const encryptedArrayBuffer = dcodeIO.ByteBuffer.wrap(encrypted, 'base64').toArrayBuffer(); - const decrypted = await libloki.crypto.DHDecrypt(symmetricKey, encryptedArrayBuffer); + const encryptedArrayBuffer = dcodeIO.ByteBuffer.wrap( + encrypted, + 'base64' + ).toArrayBuffer(); + const decrypted = await libloki.crypto.DHDecrypt( + symmetricKey, + encryptedArrayBuffer + ); const textDecoder = new TextDecoder(); const messageReceived = textDecoder.decode(decrypted); assert.strictEqual(messageSent, messageReceived); @@ -124,7 +130,9 @@ describe('Snode Channel', () => { snodePrivKey ); const encrypted = await libloki.crypto.DHEncrypt(symmetricKey, data); - const encryptedBase64 = dcodeIO.ByteBuffer.wrap(encrypted).toString('base64'); + const encryptedBase64 = dcodeIO.ByteBuffer.wrap(encrypted).toString( + 'base64' + ); // message received by Loki Messenger const decrypted = await channel.decrypt(snode.address, encryptedBase64); assert.strictEqual(messageSent, decrypted);