diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index fcf4d67fe..36d71c306 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,26 @@ 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'); + 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 = { + ...options.headers, + 'Content-Type': 'text/plain', + [LOKI_EPHEMKEY_HEADER]: libloki.crypto.snodeCipher.getChannelPublicKeyHex(), + }; + } catch (e) { + log.warn(`Could not encrypt channel for ${address}: `, e); + } + } + try { const response = await nodeFetch(url, { ...options, @@ -45,6 +67,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; @@ -70,10 +104,7 @@ const rpc = (address, port, method, params, options = {}) => { method: 'POST', ...options, body: JSON.stringify(body), - headers: { - 'X-Loki-EphemKey': 'not implemented yet', - ...headers, - }, + headers, }; return fetch(url, fetchOptions); 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/libloki/test/snode_channel_test.js b/libloki/test/snode_channel_test.js index 692633c55..9960dde68 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,28 @@ 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 encryptedArrayBuffer = dcodeIO.ByteBuffer.wrap( + encrypted, + 'base64' + ).toArrayBuffer(); + const decrypted = await libloki.crypto.DHDecrypt( + symmetricKey, + encryptedArrayBuffer ); - const decrypted = await libloki.crypto.DHDecrypt(symmetricKey, encrypted); const textDecoder = new TextDecoder(); const messageReceived = textDecoder.decode(decrypted); assert.strictEqual(messageSent, messageReceived); @@ -98,24 +114,28 @@ 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); }); }); }); diff --git a/package.json b/package.json index 3522f979f..8dc78a07c 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 7b641e8bc..9d0cfe5f8 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'); diff --git a/yarn.lock b/yarn.lock index 1d45613a3..1e885175d 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"