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,
libsignal, window, TextDecoder, TextEncoder, dcodeIO, process */
libsignal, window, TextDecoder, TextEncoder, dcodeIO, process, crypto */
const nodeFetch = require('node-fetch');
const https = require('https');
@ -16,138 +16,364 @@ let onionReqIdx = 0;
// Returns the actual ciphertext, symmetric key that will be used
// 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"?
const reqStr = JSON.stringify(reqJson);
const reqStr = JSON.stringify(reqObj);
const textEncoder = new TextEncoder();
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(
pubKeyAB,
pubKeyX25519AB,
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(
'raw',
salt,
{ name: 'HMAC', hash: { name: 'SHA-256' } },
false,
true,
['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(
{ name: 'HMAC', hash: 'SHA-256' },
key,
ephemeralSecret
);
// ArrayBuffer (object)
if (debug) {
log.debug(
'encryptForPubKey',
debug,
'- symmetricKey',
StringView.arrayBufferToHex(symmetricKey)
);
}
const ciphertext = await window.libloki.crypto.EncryptGCM(
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 };
};
// `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;
// 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'),
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(snPubkey, reqJson);
return encryptForPubKey(relayX25519AB, reqObj);
};
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
// 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];
// from (3) 2 to 0
const firstPos = nodePath.length - 1;
// console.log('targetPubKey', targetPubKey)
// console.log('targetED25519Hex', targetED25519Hex)
// console.log('nodePath', nodePath.length, 'first', firstPos)
for (let i = firstPos; i > -1; i -= 1) {
// console.log('makeOnionRequest - encryptForRelay', i)
// this nodePath points to the previous (i + 1) context
// 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)
ctxes.push(
// eslint-disable-next-line no-await-in-loop
await encryptForRelay(
nodePath[i],
i === firstPos ? targetPubKey : nodePath[i + 1].pubkey_ed25519,
ctxes[ctxes.length - 1]
)
let dest;
const relayingToFinalDestination = i === 0; // if last position
if (relayingToFinalDestination && finalRelayOptions) {
dest = {
host: finalRelayOptions.host,
target: '/loki/v1/lsrpc',
method: 'POST',
};
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 ciphertextBase64 = dcodeIO.ByteBuffer.wrap(
guardCtx.ciphertext
).toString('base64');
const payloadObj = {
ciphertext: ciphertextBase64,
ephemeral_key: StringView.arrayBufferToHex(guardCtx.ephemeralKey),
};
const payloadObj = makeGuardPayload(guardCtx);
// all these requests should use AesGcm
return payloadObj;
};
// May return false BAD_PATH, indicating that we should try a new
const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext, options = {}) => {
if (!targetNode) {
console.trace('loki_rpc::sendOnionRequest - no targetNode given')
return {}
// finalDestOptions is an object
// FIXME: internally track reqIdx, not externally
const sendOnionRequest = async (
reqIdx,
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)
return {}
// loki-storage may need this to function correctly
// 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, {
...options, body: plaintext, headers: '',
});
let id = '';
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',
body: JSON.stringify(payloadObj),
// we are talking to a snode...
agent: snodeHttpsAgent,
};
const url = `https://${nodePath[0].ip}:${nodePath[0].port}/onion_req`;
const response = await nodeFetch(url, fetchOptions);
const guardUrl = `https://${nodePath[0].ip}:${nodePath[0].port}/onion_req`;
const response = await nodeFetch(guardUrl, guardFetchOptions);
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
// 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?
// 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) {
log.warn(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - fetch unhandled error code: ${response.status}`
`(${reqIdx}) [path] lokiRpc::processOnionResponse - fetch unhandled error code: ${
response.status
}`
);
return false;
}
const ciphertext = await response.text();
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;
}
if (reqIdx === 0) {
//console.log(`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertext`, ciphertext)
if (debug) {
// log.debug(`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertext`, ciphertext)
}
let plaintext;
@ -192,25 +422,61 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
'base64'
).toArrayBuffer();
if (reqIdx === 0) {
console.log(`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`, StringView.arrayBufferToHex(ciphertextBuffer), 'useAesGcm', useAesGcm)
if (debug) {
log.debug(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`,
StringView.arrayBufferToHex(ciphertextBuffer),
'useAesGcm',
useAesGcm
);
}
const decryptFn = useAesGcm
? window.libloki.crypto.DecryptGCM
: 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();
plaintext = textDecoder.decode(plaintextBuffer);
} 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) {
log.error(`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`, ciphertextBuffer);
log.error(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`,
StringView.arrayBufferToHex(ciphertextBuffer)
);
}
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 {
const jsonRes = JSON.parse(plaintext);
@ -220,13 +486,22 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
const res = JSON.parse(jsonRes.body);
return res;
} 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 jsonRes;
} 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;
}
};
@ -523,6 +798,7 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
// Wrong PoW difficulty
if (response.status === 432) {
const result = await response.json();
log.error('WRONG POW', result);
throw new textsecure.WrongDifficultyError(result.difficulty);
}
@ -547,7 +823,7 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
onionReqIdx += 1;
// eslint-disable-next-line no-await-in-loop
const result = await sendOnionRequest(
const result = await sendOnionRequestSnodeDest(
thisIdx,
path,
targetNode,
@ -707,4 +983,5 @@ module.exports = {
encryptForPubKey,
encryptForRelay,
processOnionResponse,
sendOnionRequestLsrpcDest,
};

Loading…
Cancel
Save