Merge pull request #50 from msgmaxim/master

Onion requests v2
This commit is contained in:
Maxim Shishmarev 2020-10-21 12:33:13 +11:00 committed by GitHub
commit 1d6c54f54f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 210 additions and 126 deletions

View File

@ -450,11 +450,188 @@ module.exports = (app, prefix) => {
}, res);
});
app.post(prefix + '/loki/v1/lsrpc', async (req, res) => {
// console.log('loki/v1/lsrpc - start', typeof(req.body))
function parseBodyPlusJSON(blob) {
//console.log('headers', req.headers);
//console.log('req.originalBody', req.originalBody);
try {
let view = new DataView(blob.buffer);
let len = view.getUint32(0, true);
let payload = blob.subarray(4, len + 4);
let json_payload = blob.subarray(4 + len);
let decoder = new TextDecoder("utf-8");
let json_str = decoder.decode(json_payload);
return {
body: payload,
json: JSON.parse(json_str)
}
} catch (err) {
console.log("Error parsing body: ", err);
}
}
function parse_v2_onions(blob) {
let { body, json } = parseBodyPlusJSON(blob);
json.ciphertext = body;
return json;
}
function derive_onion_key(ephemeralPubKeyHex) {
if (!ephemeralPubKeyHex || ephemeralPubKeyHex.length < 64) {
if (!configUtil.isQuiet()) {
console.error('No ephemeral_key in JSON body or sent of at least 65 bytes');
}
throw new Error('No ephemeral_key in JSON body or sent of at least 65 bytes');
}
// decode hex ephemeral key into buffer
// FIXME: needs a try/catch
const ephemeralPubKey = Buffer.from(
bb.wrap(ephemeralPubKeyHex,'hex').toArrayBuffer()
);
// build symmetrical keypair mix client pub key with server priv key
const sym_key = libloki_crypt.makeSymmetricKey(serverPrivKey, ephemeralPubKey);
return sym_key
}
/// Returns request object
function decrypt_onion_req(ciphertext, shared_key) {
let decrypted;
try {
decrypted = libloki_crypt.decryptGCM(shared_key, ciphertext);
} catch(e) {
console.error('decryption error', e.code, e.message);
throw e;
}
let requestObj;
try {
requestObj = JSON.parse(decrypted.toString());
} catch (e) {
if (!configUtil.isQuiet()) {
console.warn('Could not parse JSON', decrypted.toString(), e);
}
throw e;
}
return requestObj;
}
async function process_onion_request(req, res, ciphertext, ephemeral_key) {
let shared_key;
try {
shared_key = derive_onion_key(ephemeral_key);
} catch (e) {
// Not able to encrypt response at this point
sendresponse({
meta: {
code: 400,
error: e.message,
headers: req.headers,
body: req.body,
},
}, res);
return;
}
try {
let req_obj = decrypt_onion_req(ciphertext, shared_key);
await process_onion_post_decryption(res, req, shared_key, req_obj);
} catch (e) {
const code = 400;
const adnResObj = {
meta: {
code,
error: e.code + ' ' + e.messsage
},
}
res.end(encryptResp(JSON.stringify(adnResObj), shared_key, code));
}
}
app.post(prefix + '/loki/v2/lsrpc', async (req, res) => {
// this hack is to work around no json header passed
await req.lokiReady;
let array = new Uint8Array(req.originalBody);
let body = parse_v2_onions(array);
await process_onion_request(req, res, body.ciphertext, body.ephemeral_key);
});
// Returns ciphertext in base64
function encryptResp(resultBody, symKey, code = 200, headers = {}) {
// encryptGCM handles the textEncoder
const plaintextEnc = JSON.stringify({
body: resultBody,
headers: headers,
status: code
});
const cipheredBuffer = libloki_crypt.encryptGCM(symKey, plaintextEnc);
// convert final buffer to base64
const cipherText64 = bb.wrap(cipheredBuffer).toString('base64');
return cipherText64;
}
async function process_onion_post_decryption(res, req, shared_key, requestObj) {
const fakeReq = await createFakeReq(req, requestObj)
const diff = fakeReq.start - res.start;
if (!configUtil.isQuiet()) {
console.log('lsrpc', fakeReq.method, fakeReq.path, 'decoding took', diff, 'ms');
}
fakeReq.runMiddleware(fakeReq.path, (code, resultBody, headers) => {
// never gets here...
const execStart = Date.now();
if (!configUtil.isQuiet()) {
const execDiff = execStart - fakeReq.start;
console.log(fakeReq.method, fakeReq.path, 'lspc execution took', execDiff, 'ms');
// console.log('body', resultBody)
}
res.end(encryptResp(resultBody, shared_key, code, headers));
if (!configUtil.isQuiet()) {
const respDiff = Date.now() - execStart;
console.log(fakeReq.method, fakeReq.path, 'lspc response took', respDiff, 'ms');
}
});
}
app.post(prefix + '/loki/v1/lsrpc', async (req, res) => {
// this hack is to work around no json header passed
if (Object.keys(req.body) == 0 && req.lokiReady) {
@ -488,131 +665,15 @@ module.exports = (app, prefix) => {
}, res);
}
// after we get our bearings
// get the base64 body...
const cipherText64 = req.body.ciphertext; // snode uses ciphertext
const debugHeaders = req.headers['x-loki-debug-headers'];
const debugCryptoValues = req.headers['x-loki-debug-crypto-values'];
const debugPayload = req.headers['x-loki-debug-payload'];
const debugFup = req.headers['x-loki-debug-file-upload'];
let ephemeralPubKeyHex = req.body.ephemeral_key;
// console.log('ephemeralPubKeyHexIn', ephemeralPubKeyHex, ephemeralPubKeyHex.length);
if (!ephemeralPubKeyHex || ephemeralPubKeyHex.length < 64) {
if (!configUtil.isQuiet()) {
console.error('No ephemeral_key in JSON body or sent of at least 65 bytes')
}
return sendresponse({
meta: {
code: 400,
error: "No ephemeral_key in JSON body or sent of at least 65 bytes",
headers: req.headers,
body: req.body,
},
}, res);
}
// decode hex ephemeral key into buffer
// FIXME: needs a try/catch
const ephemeralPubKey = Buffer.from(
bb.wrap(ephemeralPubKeyHex,'hex').toArrayBuffer()
);
if (debugCryptoValues) console.log('private key', serverPrivKey.toString('hex'))
if (debugCryptoValues) console.log('ephemeral key', ephemeralPubKey.toString('hex'))
// build symmetrical keypair mix client pub key with server priv key
const symKey = libloki_crypt.makeSymmetricKey(serverPrivKey, ephemeralPubKey)
if (debugCryptoValues) console.log('symKeySF ', symKey.toString('hex'), symKey.byteLength);
//console.log('base64 decoding', cipherText64)
const cipherText64 = req.body.ciphertext;
// base64 decode cipherText64 into buffer
const nonceCiphertextAndTag = Buffer.from(
const ciphertext = Buffer.from(
bb.wrap(cipherText64, 'base64').toArrayBuffer()
);
if (debugCryptoValues) console.log('nonceCiphertextAndTag', nonceCiphertextAndTag.toString('hex'))
await process_onion_request(req, res, ciphertext, req.body.ephemeral_key);
// captures res, symKey
function encryptResp(resultBody, code = 200, headers = {}) {
// encryptGCM handles the textEncoder
const plaintextEnc = JSON.stringify({
body: resultBody,
headers: headers,
status: code
});
//console.log('encryptResp - plaintextEnc', plaintextEnc)
//console.log('symKey components', ephemeralPubKey.toString('hex'), serverPrivKey.toString('hex'))
//console.log('response symKey', symKey.toString('hex'), plaintextEnc.toString('hex'), plaintextEnc)
const cipheredBuffer = libloki_crypt.encryptGCM(symKey, plaintextEnc);
//console.log('response encObj.cipheredBuffer', cipheredBuffer.toString('hex'))
// convert final buffer to base64
const cipherText64 = bb.wrap(cipheredBuffer).toString('base64');
res.end(cipherText64);
}
let decrypted = '{}';
try {
decrypted = libloki_crypt.decryptGCM(symKey, nonceCiphertextAndTag);
} catch(e) {
if (!configUtil.isQuiet()) {
console.error('decryption error', e.code, e.message);
}
const adnResObj = {
meta: {
code: 400,
error: e.code + ' ' + e.messsage
},
}
return encryptResp(JSON.stringify(adnResObj), adnResObj.meta.code);
}
if (debugPayload) console.log('decrypted', decrypted);
let requestObj;
try {
requestObj = JSON.parse(decrypted.toString());
} catch(e) {
if (!configUtil.isQuiet()) {
console.warn('Cant JSON parse', decrypted.toString());
}
const adnResObj = {
meta: {
code: 400,
error: e.code + ' ' + e.messsage
},
}
return encryptResp(JSON.stringify(adnResObj), adnResObj.meta.code);
}
if (debugPayload) console.log('JSON decoded', requestObj);
const fakeReq = await createFakeReq(req, requestObj)
const diff = fakeReq.start - res.start;
if (!configUtil.isQuiet()) {
console.log('lsrpc', fakeReq.method, fakeReq.path, 'decoding took', diff, 'ms');
}
fakeReq.runMiddleware(fakeReq.path, (code, resultBody, headers) => {
// never gets here...
const execStart = Date.now();
if (!configUtil.isQuiet()) {
const execDiff = execStart - fakeReq.start;
console.log(fakeReq.method, fakeReq.path, 'lspc execution took', execDiff, 'ms');
// console.log('body', resultBody)
}
encryptResp(resultBody, code, headers);
if (!configUtil.isQuiet()) {
const respDiff = Date.now() - execStart;
console.log(fakeReq.method, fakeReq.path, 'lspc response took', respDiff, 'ms');
}
});
});
app.post(prefix + '/loki/v1/secure_rpc_debug', async (req, res) => {
@ -630,7 +691,7 @@ module.exports = (app, prefix) => {
// proxy version
app.post(prefix + '/loki/v1/secure_rpc', async (req, res) => {
res.start = Date.now()
//console.log('got secure_rpc', req.path);
// console.log('got secure_rpc', req.path);
// should have debug asks and ephermal key
//console.log('headers', req.headers);
//console.log('secure_rpc body', req.body, typeof req.body);

View File

@ -1,4 +1,6 @@
const ByteBuffer = require('bytebuffer');
// snode hack work around
// preserve original body
function snodeOnionMiddleware(req, res, next) {
@ -19,6 +21,27 @@ function snodeOnionMiddleware(req, res, next) {
// console.log('perserved', body);
resolver(); // resolve promise
});
} else if (req.method === 'POST' && req.path === '/loki/v2/lsrpc') {
let resolver;
req.lokiReady = new Promise(res => {
resolver = res
});
let buffer = new ByteBuffer(ByteBuffer.DEFAULT_CAPACITY, true);
let size = 0;
req.on('data', function (data) {
size += data.length;
buffer.append(data);
});
req.on('end', function() {
// reset buffer's offset and set limit to capacity
buffer.compact(0, size);
buffer.clear();
req.originalBody = buffer.toArrayBuffer();
resolver(); // resolve promise
});
}
next();
}

2
server

@ -1 +1 @@
Subproject commit 3c124100565188508aeceed4fa0cb360a45d9e0e
Subproject commit 44b07fac3381d9cb9eb83024cc5b85b424e14ddc