From aa722590fab4e79aa8247cc6162f7ad547d5add4 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Wed, 27 Feb 2019 13:35:46 +1100 Subject: [PATCH] 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');