finalize get from onion

This commit is contained in:
Nicolas Dextraze 2021-01-31 13:36:43 -08:00
parent 542e31cd96
commit a9f3348a01
14 changed files with 4534 additions and 39 deletions

4236
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@
"base32": "0.0.6",
"curve25519-js": "0.0.4",
"futoin-hkdf": "^1.3.2",
"jest": "^26.6.3",
"tweetnacl": "^1.0.3"
}
}

View File

@ -281,8 +281,34 @@ class Circuit {
}
_on_relay_extended_cell(cell) {
//TODO
throw new Error('Not implemented')
//
// The payload of an EXTENDED cell is the same as the payload of a
// CREATED cell.
//
//
// finish the handshake.
//
const handshake_data = cell.relay_payload;
this._extend_node.compute_shared_secret(handshake_data);
if (this._extend_node.has_valid_crypto_state())
{
this._node_list.push(this._extend_node);
//
// we're ready here.
//
this._extend_node = null;
this.state = States.ready;
}
else
{
this._logger.error("Circuit.handle_relay_extended_cell() extend node [ %s ] has invalid crypto state", this._extend_node.onion_router.name);
this.destroy();
}
}
_on_relay_extended2(cell) {
@ -752,7 +778,7 @@ class Circuit {
//mini_assert(rendezvous_cookie.get_size() == 20);
const introduction_point = this.get_final_circuit_node().onion_router;
const introducee = rendezvous_circuit.get_final_circuit_node.onion_router;
const introducee = rendezvous_circuit.get_final_circuit_node().onion_router;
this._logger.debug("Circuit.rendezvous_introduce() [or: %s, state: introducing]", introduction_point.name);
this.state = States.rendezvous_introducing;
@ -790,7 +816,7 @@ class Circuit {
2 + // port
20 + // identity_fingerprint
2 + // onion key size
32 + // onion key
introducee.onion_key.length + // onion key
20 + // rendezvous cookie
128); // DH
@ -808,7 +834,14 @@ class Circuit {
rendezvous_cookie.copy(handshake_bytes, 29 + introducee.onion_key.length);
rendezvous_circuit._extend_node.key_agreement.public_key.copy(handshake_bytes, 29 + introducee.onion_key.length + rendezvous_cookie.length);
const handshake_encrypted = hybrid_encrypt(handshake_bytes, introduction_point.service_key);
const b64 = introduction_point.service_key.toString('base64');
const parts = ['-----BEGIN RSA PUBLIC KEY-----'];
for (let i = 0; i < b64.length; i += 64) {
parts.push(b64.slice(i, i + 64));
}
parts.push('-----END RSA PUBLIC KEY-----');
const service_key = parts.join('\n')
const handshake_encrypted = hybrid_encrypt(handshake_bytes, service_key);
//
// compose the final payload.
@ -841,7 +874,7 @@ class Circuit {
return;
}
if (await rendezvous_circuit.wait_for_state(States.rendezvous_completed))
if (await rendezvous_circuit.wait_for_state(States.rendezvous_completed, 90000))
{
this._logger.debug("Circuit.rendezvous_introduce() [or: %s, state: completed]", introduction_point.name);
}

View File

@ -1,4 +1,5 @@
const KeyAgreementNtor = require('./KeyAgreementNtor');
const KeyAgreementTap = require('./KeyAgreementTap');
const CircuitNodeCryptoState = require('./CircuitNodeCryptoState');
class CircuitNode {
@ -6,7 +7,7 @@ class CircuitNode {
/** @type OnionRouter */
_onion_router = null;
_type = 'normal';
/** @type KeyAgreementNtor */
/** @type {KeyAgreementNtor|KeyAgreementTap} */
_handshake = null;
/** @type CircuitNodeCryptoState */
_crypto_state = null;
@ -22,6 +23,9 @@ class CircuitNode {
this._circuit = circuit;
this._onion_router = or;
this._type = type;
if (type === 'introductionpoint') {
this._handshake = new KeyAgreementTap(or);
}
}
create_onion_skin_ntor() {

View File

@ -193,7 +193,7 @@ class Consensus {
* @return {Promise<string>}
* @private
*/
_download_from_random_router_impl(path, only_authorities) {
async _download_from_random_router_impl(path, only_authorities) {
let ip;
let port;
@ -222,7 +222,11 @@ class Consensus {
this._logger.debug(`Consensus._download_from_random_router_impl() [path: http://${ip}:${port}${path}]`);
return get('http:', ip, port, path);
try {
return await get('http:', ip, port, path);
} catch (err) {
return '';
}
}
_parse_consensus(content) {

View File

@ -1,5 +1,5 @@
const crypto = require('crypto');
const base32 = require('base32')
const base32 = require('../utils/base32')
const OnionRouter = require('./OnionRouter')
const {get} = require('../utils/http')
const {time} = require('../utils/time')
@ -18,15 +18,16 @@ class HiddenServiceConnector {
constructor (rendezvous_circuit, onion, logger) {
this._rendezvous_circuit = rendezvous_circuit;
this._socket = rendezvous_circuit.tor_socket;
this._consensus = rendezvous_circuit.tor_socket.onion_router.consensus;
this._socket = rendezvous_circuit?.tor_socket;
this._consensus = rendezvous_circuit?.tor_socket.onion_router.consensus;
this._onion = onion;
this._permanent_id = Buffer.from(base32.decode(onion));
this._logger = logger;
this._time = time;
}
async connect() {
this.find_responsible_directories();
this._find_responsible_directories();
if (this._responsible_directory_list.length)
{
@ -43,7 +44,7 @@ class HiddenServiceConnector {
if (this._rendezvous_circuit.is_rendezvous_established())
{
let responsible_directory_index = 0;
while ((responsible_directory_index = await this.fetch_hidden_service_descriptor(responsible_directory_index)) !== -1)
while ((responsible_directory_index = await this._fetch_hidden_service_descriptor(responsible_directory_index)) !== -1)
{
await this.introduce();
@ -58,7 +59,7 @@ class HiddenServiceConnector {
return false;
}
find_responsible_directories() {
_find_responsible_directories() {
//
// rend-spec.txt
// 1.4.
@ -88,7 +89,7 @@ class HiddenServiceConnector {
{
const descriptor_id = this.get_descriptor_id(replica);
const index = directory_list.findIndex(x => Buffer.compare(x.identity_fingerprint, descriptor_id) < 0);
const index = directory_list.findIndex(x => Buffer.compare(x.identity_fingerprint, descriptor_id) > 0);
for (let i = 0; i < 3; i++)
{
@ -111,10 +112,10 @@ class HiddenServiceConnector {
// time-period = (current-time + permanent-id-byte * 86400 / 256)
// / 86400
//
const time_period = (time() + (permanent_id_byte * 86400 / 256)) / 86400;
const time_period = Math.floor((this._time() + (permanent_id_byte * 86400 / 256)) / 86400);
const secret_bytes = Buffer.alloc(5);
secret_bytes.writeInt32BE(time_period, 0);
secret_bytes.writeUInt32BE(time_period, 0);
secret_bytes.writeUInt8(replica, 4);
return sha1(secret_bytes);
@ -135,7 +136,7 @@ class HiddenServiceConnector {
return sha1(descriptor_id_bytes);
}
async fetch_hidden_service_descriptor(responsible_directory_index) {
async _fetch_hidden_service_descriptor(responsible_directory_index) {
for (let i = responsible_directory_index; i < this._responsible_directory_list.length; i++)
{
const responsible_directory = this._responsible_directory_list[i];
@ -149,7 +150,6 @@ class HiddenServiceConnector {
this._socket.onion_router.name,
this._socket.onion_router.ip,
this._socket.onion_router.or_port);
this._logger.info("\tConnected...");
/** @type Circuit */
const directory_circuit = await this._socket.create_circuit();
@ -164,9 +164,10 @@ class HiddenServiceConnector {
//
continue;
}
this._logger.info("\tConnected...");
this._logger.info(
"\tExtending circuit for hidden service, connecting to responsible directory '%s' (%s:%u)",
"\tExtending circuit for hidden service, connecting to responsible directory '%s' (%s:%i)",
responsible_directory.name,
responsible_directory.ip,
responsible_directory.or_port);
@ -205,10 +206,10 @@ class HiddenServiceConnector {
const descriptor_path = "/tor/rendezvous2/" + base32.encode(this.get_descriptor_id(replica));
this._logger.debug(
"hidden_service::fetch_hidden_service_descriptor() [path: %s]",
"hidden_service::_fetch_hidden_service_descriptor() [path: %s]",
descriptor_path);
this._logger.info("\tSending request for hidden service descriptor...");
this._logger.info("\tSending request for hidden service descriptor... %s:%i", responsible_directory.ip, responsible_directory.dir_port);
const hidden_service_descriptor = await get(
'http:',
@ -222,7 +223,7 @@ class HiddenServiceConnector {
//
// parse hidden service descriptor.
//
if (!hidden_service_descriptor &&
if (hidden_service_descriptor &&
!hidden_service_descriptor.includes("404 Not found"))
{
this._logger.info("\tHidden service descriptor is valid...");
@ -246,14 +247,14 @@ class HiddenServiceConnector {
for (const line of lines) {
const parts = line.split(' ');
if (parts[0] === 'introduction-point') {
const identity_fingerprint = Buffer.from(base32.decode(parts[1]));
const identity_fingerprint = base32.decode(parts[1]);
current_router = this._consensus.get_onion_router_by_identity_fingerprint(identity_fingerprint);
} else if (parts[0] === 'service-key') {
serviceKey = '';
} else if (parts[0] === '-----BEGIN RSA PUBLIC KEY-----' && serviceKey !== null) {
} else if (line === '-----BEGIN RSA PUBLIC KEY-----' && serviceKey !== null) {
capture = true;
} else if (parts[0] === '-----END RSA PUBLIC KEY-----' && serviceKey !== null) {
current_router.service_key = serviceKey;
} else if (line === '-----END RSA PUBLIC KEY-----' && serviceKey !== null) {
current_router.service_key = Buffer.from(serviceKey, 'base64');
introduction_point_list.push(current_router);
capture = false;
serviceKey = null;
@ -336,7 +337,7 @@ class HiddenServiceConnector {
if (introduce_circuit.is_rendezvous_introduced())
{
this._logger.info("\tIntroduced successfully...");
break;
return;
}
else
{

View File

@ -0,0 +1,22 @@
const HiddenServiceConnector = require('./HiddenServiceConnector');
const base32 = require('../utils/base32');
test('base32 decode', function() {
const decoded = base32.decode('duskgytldkxiuqc6');
//const decoded_buf = Array.prototype.map.call(decoded, (_, i) => decoded.charCodeAt(i))
expect(decoded).toEqual(Buffer.from([0x1d,0x24,0xa3,0x62,0x6b,0x1a,0xae,0x8a,0x40,0x5e]));
})
test('base32 encode', function() {
const encoded = base32.encode(Buffer.from([0x1d,0x24,0xa3,0x62,0x6b,0x1a,0xae,0x8a,0x40,0x5e]));
//const decoded_buf = Array.prototype.map.call(decoded, (_, i) => decoded.charCodeAt(i))
expect(encoded).toEqual('duskgytldkxiuqc6');
})
test('test descriptor', function() {
const time = 1611534264;
const onion = 'duskgytldkxiuqc6';
const sut = new HiddenServiceConnector(null, onion, null);
sut._time = () => time;
const descriptor = sut.get_descriptor_id(0);
const descriptor_base32 = base32.encode(descriptor);
expect(descriptor_base32).toBe('ek7vymlparcwqimmmfixlbars4xjz5jl');
})

123
src/tor/KeyAgreementTap.js Normal file
View File

@ -0,0 +1,123 @@
const crypto = require('crypto')
const dh1024 = require('../utils/dh1024')
const {sha1} = require('../utils/crypto')
const DH_P = new Buffer([
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34,
0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74,
0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd,
0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37,
0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6,
0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed,
0xee, 0x38, 0x6b, 0xfb, 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, 0x1f, 0xe6,
0x49, 0x28, 0x66, 0x51, 0xec, 0xe6, 0x53, 0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
]);
class KeyAgreementTap {
constructor (or) {
this._router = or;
this._dh = crypto.createDiffieHellman(DH_P);
this._dh.generateKeys();
}
get public_key() {
return this._dh.getPublicKey();
}
get private_key() {
return this._dh.getPrivateKey();
}
compute_shared_secret(handshake_data) {
return this._compute_shared_secret(
handshake_data.slice(0, dh1024.key_size_in_bytes),
handshake_data.slice(dh1024.key_size_in_bytes, dh1024.key_size_in_bytes + 20));
}
_compute_shared_secret(other_public_key, verification_data) {
//
// 5.1.3. The "TAP" handshake
//
// This handshake uses Diffie-Hellman in Z_p and RSA to compute a set of
// shared keys which the client knows are shared only with a particular
// server, and the server knows are shared with whomever sent the
// original handshake (or with nobody at all). It's not very fast and
// not very good. (See Goldberg's "On the Security of the Tor
// Authentication Protocol".)
//
// Define TAP_C_HANDSHAKE_LEN as DH_LEN+KEY_LEN+PK_PAD_LEN.
// Define TAP_S_HANDSHAKE_LEN as DH_LEN+HASH_LEN.
//
// The payload for a CREATE cell is an 'onion skin', which consists of
// the first step of the DH handshake data (also known as g^x). This
// value is hybrid-encrypted (see 0.3) to the server's onion key, giving
// a client handshake of:
//
// PK-encrypted:
// Padding [PK_PAD_LEN bytes]
// Symmetric key [KEY_LEN bytes]
// First part of g^x [PK_ENC_LEN-PK_PAD_LEN-KEY_LEN bytes]
// Symmetrically encrypted:
// Second part of g^x [DH_LEN-(PK_ENC_LEN-PK_PAD_LEN-KEY_LEN)
// bytes]
//
// The payload for a CREATED cell, or the relay payload for an
// EXTENDED cell, contains:
// DH data (g^y) [DH_LEN bytes]
// Derivative key data (KH) [HASH_LEN bytes] <see 5.2 below>
//
// Once the handshake between the OP and an OR is completed, both can
// now calculate g^xy with ordinary DH. Before computing g^xy, both parties
// MUST verify that the received g^x or g^y value is not degenerate;
// that is, it must be strictly greater than 1 and strictly less than p-1
// where p is the DH modulus. Implementations MUST NOT complete a handshake
// with degenerate keys. Implementations MUST NOT discard other "weak"
// g^x values.
//
// (Discarding degenerate keys is critical for security; if bad keys
// are not discarded, an attacker can substitute the OR's CREATED
// cell's g^y with 0 or 1, thus creating a known g^xy and impersonating
// the OR. Discarding other keys may allow attacks to learn bits of
// the private key.)
//
// Once both parties have g^xy, they derive their shared circuit keys
// and 'derivative key data' value via the KDF-TOR function in 5.2.1.
//
//mini_assert(verification_data.get_size() == crypto::sha1::hash_size_in_bytes);
const shared_secret = this._dh.computeSecret(other_public_key);
const derived = this._derive_keys(shared_secret);
//
// first 20 bytes of the derived key is the verification checksum.
// rest of it is the key material.
//
const computed_verification_data = derived.slice(0, 20);
const key_material = derived.slice(20);
if (computed_verification_data.equals(verification_data))
{
return key_material;
}
return Buffer.alloc(0);
}
_derive_keys(secret) {
const key_material = Buffer.alloc(100);
const hashdata = Buffer.alloc(secret.length + 1);
secret.copy(hashdata, 0);
for (let i = 0; i < 5; i++)
{
hashdata[secret.length] = i;
sha1(hashdata).copy(key_material, i * 20);
}
return key_material;
}
}
module.exports = KeyAgreementTap;

View File

@ -59,12 +59,26 @@ class OnionRouter {
const descriptor = await this._consensus.get_onion_router_descriptor(this._identity_fingerprint);
// parse
const lines = descriptor.split('\n');
let capture = '', onion_key = '';
for (const line of lines){
const parts = line.split(' ');
if (parts[0] === 'ntor-onion-key') {
if (parts[0] === 'onion-key') {
capture = 'onion-key';
}
else if (parts[0] === 'ntor-onion-key') {
this._ntor_onion_key = Buffer.from(parts[1], 'base64');
break;
}
else if (capture === 'onion-key' && line === '-----BEGIN RSA PUBLIC KEY-----') {
onion_key = '';
}
else if (capture === 'onion-key' && line === '-----END RSA PUBLIC KEY-----') {
this._onion_key = Buffer.from(onion_key, 'base64')
capture = '';
}
else if (capture === 'onion-key') {
onion_key += line;
}
}
this._descriptor_fetched = true;
}

View File

@ -40,6 +40,11 @@ class TorStream extends Duplex {
set state(state) {
this._state = state;
if (state === States.destroyed) {
if (this._buffer) {
this.push(this._buffer);
this._buffer = null;
}
this.push(null);
for (const k in this._waits) {
for (const wait of this._waits[k]) {
wait.resolve(false);
@ -57,12 +62,12 @@ class TorStream extends Duplex {
}
append_to_recv_buffer(data) {
this._buffer = this._buffer
? Buffer.concat([this._buffer, data], this._buffer.length + data.length)
: data;
if (this._canRead) {
this._canRead = this.push(data);
} else {
this._buffer = this._buffer
? Buffer.concat([this._buffer, data], this._buffer.length + data.length)
: data;
this._canRead = this.push(this._buffer);
this._buffer = null;
}
}

51
src/utils/base32.js Normal file
View File

@ -0,0 +1,51 @@
const alphabet = 'abcdefghijklmnopqrstuvwxyz234567'
function decode_chunk(input, in_offset, output, out_offset) {
let b = BigInt(0);
for (let i = 0; i < 8; i++) {
b = (b << 5n) | BigInt(alphabet.indexOf(input[in_offset + i]));
}
for (let j = 4; j >= 0; j--) {
output[out_offset + 4 - j] = Number(b >> BigInt(j * 8));
}
}
function decode(input) {
const out_len = Math.ceil((input.length / 8) * 5);
const output = Buffer.alloc(out_len);
const q = Math.floor(input.length / 8);
for (let i = 0; i < q; i++) {
decode_chunk(input, i * 8, output, i * 5);
}
return output;
}
exports.decode = decode;
function encode_chunk(input, in_offset, output, out_offset) {
let b = BigInt(0);
for (let i = 0; i < 5; i++) {
b = (b << 8n) | BigInt(input[in_offset + i]);
}
for (let i = 7; i >= 0; i--) {
b = b << BigInt(24 + (7 - i) * 5);
b = b >> BigInt(24 + (7 - i) * 5);
const c = Number(b >> BigInt(i * 5)) % 32;
output[out_offset + 7 - i] = alphabet.charCodeAt(c);
}
}
function encode(input) {
const out_len = Math.ceil(input.length / 5 * 8);
const output = Buffer.alloc(out_len);
const q = Math.floor(input.length / 5);
for (let i = 0; i < q; i++) {
encode_chunk(input, i * 5, output, i * 8);
}
return output.toString('ascii');
}
exports.encode = encode;

View File

@ -44,13 +44,13 @@ const PK_DATA_LEN_WITH_KEY = PK_DATA_LEN - KEY_LEN;
/**
* @param {Buffer} data
* @param {Buffer} public_key
* @param {string} public_key
* @returns {Buffer}
*/
function hybrid_encrypt(data, public_key) {
if (data.length < PK_DATA_LEN)
{
return rsa_1024(public_key).encrypt(data);
return crypto.publicEncrypt(public_key, data);
}
const random_key = crypto.randomBytes(KEY_LEN);
@ -62,7 +62,7 @@ function hybrid_encrypt(data, public_key) {
random_key, data.slice(0, PK_DATA_LEN_WITH_KEY)
], random_key.length + PK_DATA_LEN_WITH_KEY);
const c1 = rsa_1024(public_key).encrypt(k_and_m1);
const c1 = crypto.publicEncrypt(public_key, k_and_m1);
//
// AES_CTR(M2) --> C2

1
src/utils/dh1024.js Normal file
View File

@ -0,0 +1 @@
exports.key_size_in_bytes = (1024/8);

View File

@ -28,7 +28,7 @@ async function get(protocol, ip, port, path, stream) {
res.on('data', chunk => data += chunk.toString());
res.on('end', () => resolve(data));
res.on('error', (err) => reject(err));
});
}).on('error', err => reject(err));
});
}