Merge pull request #213 from sachaaaaa/channel_encryption

Hook up channel encryption
pull/240/head
Beaudan Campbell-Brown 6 years ago committed by GitHub
commit 8e3542b511
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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);

@ -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,
};
})();

@ -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);
});
});
});

@ -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",

@ -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');

@ -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"

Loading…
Cancel
Save