sendOnionRequestLsrpcDest() refactor, log WRONG POW, makeGuardPayload(), makeOnionRequest(), sendOnionRequest => sendOnionRequestSnodeDest

pull/1100/head
Ryan Tharp 5 years ago
parent ae210c4312
commit 2a889f5d99

@ -1,5 +1,5 @@
/* global log, libloki, textsecure, getStoragePubKey, lokiSnodeAPI, StringView, /* global log, libloki, textsecure, getStoragePubKey, lokiSnodeAPI, StringView,
libsignal, window, TextDecoder, TextEncoder, dcodeIO, process */ libsignal, window, TextDecoder, TextEncoder, dcodeIO, process, crypto */
const nodeFetch = require('node-fetch'); const nodeFetch = require('node-fetch');
const https = require('https'); const https = require('https');
@ -16,138 +16,364 @@ let onionReqIdx = 0;
// Returns the actual ciphertext, symmetric key that will be used // Returns the actual ciphertext, symmetric key that will be used
// for decryption, and an ephemeral_key to send to the next hop // for decryption, and an ephemeral_key to send to the next hop
const encryptForPubKey = async (pubKeyAB, reqJson) => { const encryptForPubKey = async (pubKeyX25519AB, reqObj, debug = false) => {
// Do we still need "headers"? // Do we still need "headers"?
const reqStr = JSON.stringify(reqObj);
const reqStr = JSON.stringify(reqJson);
const textEncoder = new TextEncoder(); const textEncoder = new TextEncoder();
const plaintext = textEncoder.encode(reqStr); const plaintext = textEncoder.encode(reqStr);
const ephemeral = libloki.crypto.generateEphemeralKeyPair(); const ephemeral = await libloki.crypto.generateEphemeralKeyPair();
if (debug) {
log.debug(
'encryptForPubKey',
debug,
'- pubKeyX25519AB',
StringView.arrayBufferToHex(pubKeyX25519AB)
);
log.debug(
'encryptForPubKey',
debug,
'- ephermalPriv',
StringView.arrayBufferToHex(ephemeral.privKey)
);
log.debug(
'encryptForPubKey',
debug,
'- ephermalPub',
StringView.arrayBufferToHex(ephemeral.pubKey)
);
}
const ephemeralSecret = libsignal.Curve.calculateAgreement( const ephemeralSecret = libsignal.Curve.calculateAgreement(
pubKeyAB, pubKeyX25519AB,
ephemeral.privKey ephemeral.privKey
); );
if (debug) {
log.debug(
'encryptForPubKey',
debug,
'- ephemeralSecret',
StringView.arrayBufferToHex(ephemeralSecret)
);
}
const salt = window.Signal.Crypto.bytesFromString('LOKI'); // FIXME: window.libloki.crypto.deriveSymmetricKey refactor
const salt = window.Signal.Crypto.bytesFromString('LOKI'); // ArrayBuffer (object)
if (debug) {
log.debug(
'encryptForPubKey',
debug,
'- salt',
StringView.arrayBufferToHex(salt)
);
}
const key = await crypto.subtle.importKey( const key = await crypto.subtle.importKey(
'raw', 'raw',
salt, salt,
{ name: 'HMAC', hash: { name: 'SHA-256' } }, { name: 'HMAC', hash: { name: 'SHA-256' } },
false, true,
['sign'] ['sign']
); );
// CrsptoKey (object)
const exportKey = await crypto.subtle.exportKey('raw', key);
// ArrayBuffer (object)
if (debug) {
log.error(
'encryptForPubKey',
debug,
'- key',
StringView.arrayBufferToHex(exportKey)
);
}
const symmetricKey = await crypto.subtle.sign( const symmetricKey = await crypto.subtle.sign(
{ name: 'HMAC', hash: 'SHA-256' }, { name: 'HMAC', hash: 'SHA-256' },
key, key,
ephemeralSecret ephemeralSecret
); );
// ArrayBuffer (object)
if (debug) {
log.debug(
'encryptForPubKey',
debug,
'- symmetricKey',
StringView.arrayBufferToHex(symmetricKey)
);
}
const ciphertext = await window.libloki.crypto.EncryptGCM( const ciphertext = await window.libloki.crypto.EncryptGCM(
symmetricKey, symmetricKey,
plaintext plaintext,
debug,
ephemeral.pubKey
); );
// looks textEncoder'd... Uint8Array
if (debug) {
log.debug(
'encryptForPubKey',
debug,
'- ciphertext',
StringView.arrayBufferToHex(ciphertext),
ciphertext
);
}
// ephemeral_key => ephemeralKey?
return { ciphertext, symmetricKey, ephemeral_key: ephemeral.pubKey }; return { ciphertext, symmetricKey, ephemeral_key: ephemeral.pubKey };
}; };
// `ctx` holds info used by `node` to relay further // `ctx` holds info used by `node` to relay further
const encryptForRelay = async (node, nextNodePubKey_ed25519_hex, ctx) => { // destination needs ed25519_hex
const encryptForRelay = async (relayX25519AB, destination, ctx) => {
// cyx contains: ciphertext, symmetricKey, ephemeral_key
const payload = ctx.ciphertext; const payload = ctx.ciphertext;
// ciphertext, symmetricKey, ephemeral_key
//console.log('encryptForRelay ctx', ctx)
const reqJson = { if (!destination.host && !destination.destination) {
log.warn(`loki_rpc::encryptForRelay - no destination`, destination);
}
const reqObj = {
...destination,
ciphertext: dcodeIO.ByteBuffer.wrap(payload).toString('base64'), ciphertext: dcodeIO.ByteBuffer.wrap(payload).toString('base64'),
ephemeral_key: StringView.arrayBufferToHex(ctx.ephemeral_key), ephemeral_key: StringView.arrayBufferToHex(ctx.ephemeral_key),
destination: nextNodePubKey_ed25519_hex,
//ephemeral_key: StringView.arrayBufferToHex(ctx.ephemeralKey),
//destination: nextNode.pubkey_ed25519,
}; };
const snPubkey = StringView.hexToArrayBuffer(node.pubkey_x25519); return encryptForPubKey(relayX25519AB, reqObj);
return encryptForPubKey(snPubkey, reqJson);
}; };
const BAD_PATH = 'bad_path'; const makeGuardPayload = guardCtx => {
const ciphertextBase64 = dcodeIO.ByteBuffer.wrap(
guardCtx.ciphertext
).toString('base64');
const guardPayloadObj = {
ciphertext: ciphertextBase64,
ephemeral_key: StringView.arrayBufferToHex(guardCtx.ephemeral_key),
};
return guardPayloadObj;
};
// May return false BAD_PATH, indicating that we should try a new
// we just need the targetNode.pubkey_ed25519 for the encryption // we just need the targetNode.pubkey_ed25519 for the encryption
// targetPubKey is ed25519 if snode is the target // targetPubKey is ed25519 if snode is the target
const makeOnionRequest = async (nodePath, destCtx, targetPubKey) => { const makeOnionRequest = async (
nodePath,
destCtx,
targetED25519Hex,
finalRelayOptions = false,
id = ''
) => {
const ctxes = [destCtx]; const ctxes = [destCtx];
// from (3) 2 to 0 // from (3) 2 to 0
const firstPos = nodePath.length - 1; const firstPos = nodePath.length - 1;
// console.log('targetPubKey', targetPubKey) // console.log('targetED25519Hex', targetED25519Hex)
// console.log('nodePath', nodePath.length, 'first', firstPos) // console.log('nodePath', nodePath.length, 'first', firstPos)
for (let i = firstPos; i > -1; i -= 1) { for (let i = firstPos; i > -1; i -= 1) {
// console.log('makeOnionRequest - encryptForRelay', i) let dest;
// this nodePath points to the previous (i + 1) context const relayingToFinalDestination = i === 0; // if last position
// console.log(i + 1, 'pubkey_ed25519', nodePath[i + 1] ? nodePath[i + 1].pubkey_ed25519 : null)
// console.log('node', i, 'to', i === firstPos ? targetPubKey : nodePath[i + 1].pubkey_ed25519) if (relayingToFinalDestination && finalRelayOptions) {
ctxes.push( dest = {
// eslint-disable-next-line no-await-in-loop host: finalRelayOptions.host,
await encryptForRelay( target: '/loki/v1/lsrpc',
nodePath[i], method: 'POST',
i === firstPos ? targetPubKey : nodePath[i + 1].pubkey_ed25519, };
ctxes[ctxes.length - 1] log.info(
) `loki_rpc:::makeOnionRequest ${id} - lsrpc destination set`,
dest
);
} else {
// set x25519 if destination snode
let pubkeyHex = targetED25519Hex; // relayingToFinalDestination
// or ed25519 snode destination
if (!relayingToFinalDestination) {
pubkeyHex = nodePath[i + 1].pubkey_ed25519;
if (!pubkeyHex) {
log.error(
`loki_rpc:::makeOnionRequest ${id} - no ed25519 for`,
nodePath[i + 1],
'path node',
i + 1
);
}
}
// destination takes a hex key
dest = {
destination: pubkeyHex,
};
}
// FIXME: we should store this inside snode pool
const relayX25519AB = StringView.hexToArrayBuffer(
nodePath[i].pubkey_x25519
); );
try {
ctxes.push(
// eslint-disable-next-line no-await-in-loop
await encryptForRelay(relayX25519AB, dest, ctxes[ctxes.length - 1])
);
} catch (e) {
log.error(
`loki_rpc:::makeOnionRequest ${id} - encryptForRelay failure`,
e.code,
e.message
);
throw e;
}
} }
const guardCtx = ctxes[ctxes.length - 1]; // last ctx const guardCtx = ctxes[ctxes.length - 1]; // last ctx
const ciphertextBase64 = dcodeIO.ByteBuffer.wrap( const payloadObj = makeGuardPayload(guardCtx);
guardCtx.ciphertext
).toString('base64');
const payloadObj = {
ciphertext: ciphertextBase64,
ephemeral_key: StringView.arrayBufferToHex(guardCtx.ephemeralKey),
};
// all these requests should use AesGcm // all these requests should use AesGcm
return payloadObj; return payloadObj;
}; };
// May return false BAD_PATH, indicating that we should try a new // finalDestOptions is an object
const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext, options = {}) => { // FIXME: internally track reqIdx, not externally
if (!targetNode) { const sendOnionRequest = async (
console.trace('loki_rpc::sendOnionRequest - no targetNode given') reqIdx,
return {} nodePath,
destX25519Any,
finalDestOptions,
finalRelayOptions = false,
lsrpcIdx
) => {
if (!destX25519Any) {
log.error('loki_rpc::sendOnionRequest - no destX25519Any given');
return {};
} }
if (!targetNode.pubkey_x25519) {
console.trace('loki_rpc::sendOnionRequest - pubkey_x25519 in targetNode', targetNode) // loki-storage may need this to function correctly
return {} // but ADN calls will not always have a body
/*
if (!finalDestOptions.body) {
finalDestOptions.body = '';
} }
const snPubkey = StringView.hexToArrayBuffer(targetNode.pubkey_x25519); */
const destCtx = await encryptForPubKey(snPubkey, { let id = '';
...options, body: plaintext, headers: '', if (lsrpcIdx !== undefined) {
}); id += `${lsrpcIdx}=>`;
}
if (reqIdx !== undefined) {
id += `${reqIdx}`;
}
const payloadObj = await makeOnionRequest(nodePath, destCtx, targetNode.pubkey_ed25519); // get destination pubkey in array buffer format
let destX25519AB = destX25519Any;
if (typeof destX25519AB === 'string') {
destX25519AB = StringView.hexToArrayBuffer(destX25519Any);
}
const fetchOptions = { // safely build destination
let targetEd25519hex;
if (finalDestOptions) {
if (finalDestOptions.destination_ed25519_hex) {
// snode destination
targetEd25519hex = finalDestOptions.destination_ed25519_hex;
// eslint-disable-next-line no-param-reassign
delete finalDestOptions.destination_ed25519_hex;
}
// else it's lsrpc...
} else {
// eslint-disable-next-line no-param-reassign
finalDestOptions = {};
log.warn(`loki_rpc::sendOnionRequest ${id} - no finalDestOptions`);
return {};
}
const options = finalDestOptions; // lint
// do we need this?
if (options.headers === undefined) {
options.headers = '';
}
let destCtx;
try {
destCtx = await encryptForPubKey(destX25519AB, options);
} catch (e) {
const hex = StringView.arrayBufferToHex(destX25519AB);
log.error(
`loki_rpc::sendOnionRequest ${id} - encryptForPubKey failure [`,
e.code,
e.message,
'] destination X25519',
hex.substr(0, 32),
'...',
hex.substr(32),
'options',
options
);
throw e;
}
const payloadObj = await makeOnionRequest(
nodePath,
destCtx,
targetEd25519hex,
finalRelayOptions,
id
);
const guardFetchOptions = {
method: 'POST', method: 'POST',
body: JSON.stringify(payloadObj), body: JSON.stringify(payloadObj),
// we are talking to a snode... // we are talking to a snode...
agent: snodeHttpsAgent, agent: snodeHttpsAgent,
}; };
const url = `https://${nodePath[0].ip}:${nodePath[0].port}/onion_req`; const guardUrl = `https://${nodePath[0].ip}:${nodePath[0].port}/onion_req`;
const response = await nodeFetch(url, fetchOptions); const response = await nodeFetch(guardUrl, guardFetchOptions);
return processOnionResponse(reqIdx, response, destCtx.symmetricKey, true); return processOnionResponse(reqIdx, response, destCtx.symmetricKey, true);
}; };
const sendOnionRequestSnodeDest = async (
reqIdx,
nodePath,
targetNode,
plaintext
) =>
sendOnionRequest(reqIdx, nodePath, targetNode.pubkey_x25519, {
destination_ed25519_hex: targetNode.pubkey_ed25519,
body: plaintext,
});
// need relay node's pubkey_x25519_hex
// always the same target: /loki/v1/lsrpc
const sendOnionRequestLsrpcDest = async (
reqIdx,
nodePath,
destX25519Any,
host,
payloadObj,
lsrpcIdx = 0
) =>
sendOnionRequest(
reqIdx,
nodePath,
destX25519Any,
payloadObj,
{ host },
lsrpcIdx
);
const BAD_PATH = 'bad_path';
// Process a response as it arrives from `nodeFetch`, handling // Process a response as it arrives from `nodeFetch`, handling
// http errors and attempting to decrypt the body with `sharedKey` // http errors and attempting to decrypt the body with `sharedKey`
const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => { // May return false BAD_PATH, indicating that we should try a new path.
const processOnionResponse = async (
reqIdx,
response,
sharedKey,
useAesGcm,
debug
) => {
// FIXME: 401/500 handling? // FIXME: 401/500 handling?
// detect SNode is not ready (not in swarm; not done syncing) // detect SNode is not ready (not in swarm; not done syncing)
@ -170,18 +396,22 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
if (response.status !== 200) { if (response.status !== 200) {
log.warn( log.warn(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - fetch unhandled error code: ${response.status}` `(${reqIdx}) [path] lokiRpc::processOnionResponse - fetch unhandled error code: ${
response.status
}`
); );
return false; return false;
} }
const ciphertext = await response.text(); const ciphertext = await response.text();
if (!ciphertext) { if (!ciphertext) {
log.warn(`(${reqIdx}) [path] lokiRpc::processOnionResponse - Target node return empty ciphertext`); log.warn(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - Target node return empty ciphertext`
);
return false; return false;
} }
if (reqIdx === 0) { if (debug) {
//console.log(`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertext`, ciphertext) // log.debug(`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertext`, ciphertext)
} }
let plaintext; let plaintext;
@ -192,25 +422,61 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
'base64' 'base64'
).toArrayBuffer(); ).toArrayBuffer();
if (reqIdx === 0) { if (debug) {
console.log(`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`, StringView.arrayBufferToHex(ciphertextBuffer), 'useAesGcm', useAesGcm) log.debug(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`,
StringView.arrayBufferToHex(ciphertextBuffer),
'useAesGcm',
useAesGcm
);
} }
const decryptFn = useAesGcm const decryptFn = useAesGcm
? window.libloki.crypto.DecryptGCM ? window.libloki.crypto.DecryptGCM
: window.libloki.crypto.DHDecrypt; : window.libloki.crypto.DHDecrypt;
const plaintextBuffer = await decryptFn(sharedKey, ciphertextBuffer); const plaintextBuffer = await decryptFn(sharedKey, ciphertextBuffer, debug);
if (debug) {
log.debug(
'lokiRpc::processOnionResponse - plaintextBuffer',
plaintextBuffer.toString()
);
}
const textDecoder = new TextDecoder(); const textDecoder = new TextDecoder();
plaintext = textDecoder.decode(plaintextBuffer); plaintext = textDecoder.decode(plaintextBuffer);
} catch (e) { } catch (e) {
log.error(`(${reqIdx}) [path] lokiRpc::processOnionResponse - decode error`, e.code, e.message); log.error(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - decode error`,
e.code,
e.message
);
log.error(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - symKey`,
StringView.arrayBufferToHex(sharedKey)
);
if (ciphertextBuffer) { if (ciphertextBuffer) {
log.error(`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`, ciphertextBuffer); log.error(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`,
StringView.arrayBufferToHex(ciphertextBuffer)
);
} }
return false; return false;
} }
/*
if (!plaintext) {
log.debug('Trying again with', useAesGcm?'gcm':'dh')
try {
const plaintextBuffer2 = await decryptFn(sharedKey, ciphertextBuffer, true);
log.info(`(${reqIdx}) [path] lokiRpc::processOnionResponse - plaintextBufferHex`, StringView.arrayBufferToHex(plaintextBuffer2));
} catch(e) {
}
}
*/
if (debug) {
log.debug('lokiRpc::processOnionResponse - plaintext', plaintext);
}
try { try {
const jsonRes = JSON.parse(plaintext); const jsonRes = JSON.parse(plaintext);
@ -220,13 +486,22 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
const res = JSON.parse(jsonRes.body); const res = JSON.parse(jsonRes.body);
return res; return res;
} catch (e) { } catch (e) {
log.error(`(${reqIdx}) [path] lokiRpc::processOnionResponse - parse error json: `, jsonRes.body); log.error(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - parse error inner json: `,
jsonRes.body
);
} }
return false; return false;
}; };
return jsonRes; return jsonRes;
} catch (e) { } catch (e) {
log.error(`(${reqIdx}) [path] lokiRpc::processOnionResponse - parse error`, e.code, e.message, `json:`, plaintext); log.error(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - parse error outer json`,
e.code,
e.message,
`json:`,
plaintext
);
return false; return false;
} }
}; };
@ -523,6 +798,7 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
// Wrong PoW difficulty // Wrong PoW difficulty
if (response.status === 432) { if (response.status === 432) {
const result = await response.json(); const result = await response.json();
log.error('WRONG POW', result);
throw new textsecure.WrongDifficultyError(result.difficulty); throw new textsecure.WrongDifficultyError(result.difficulty);
} }
@ -547,7 +823,7 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
onionReqIdx += 1; onionReqIdx += 1;
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const result = await sendOnionRequest( const result = await sendOnionRequestSnodeDest(
thisIdx, thisIdx,
path, path,
targetNode, targetNode,
@ -707,4 +983,5 @@ module.exports = {
encryptForPubKey, encryptForPubKey,
encryptForRelay, encryptForRelay,
processOnionResponse, processOnionResponse,
sendOnionRequestLsrpcDest,
}; };

Loading…
Cancel
Save