wip accessing onion address

This commit is contained in:
Nicolas Dextraze 2021-01-11 19:38:42 -08:00
parent a5230b9105
commit 40ffd49b89
9 changed files with 574 additions and 16 deletions

27
package-lock.json generated
View File

@ -2,6 +2,14 @@
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"base32": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/base32/-/base32-0.0.6.tgz",
"integrity": "sha1-eQOLy1rsLY8ivMHChAKST1Cm0qw=",
"requires": {
"optimist": ">=0.1.0"
}
},
"curve25519-js": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz",
@ -12,10 +20,29 @@
"resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.3.2.tgz",
"integrity": "sha512-3EVi3ETTyJg5PSXlxLCaUVVn0pSbDf62L3Gwxne7Uq+d8adOSNWQAad4gg7WToHkcgnCJb3Wlb1P8r4Evj4GPw=="
},
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
},
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
"requires": {
"minimist": "~0.0.1",
"wordwrap": "~0.0.2"
}
},
"tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
},
"wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
}
}
}

View File

@ -1,5 +1,6 @@
{
"dependencies": {
"base32": "0.0.6",
"curve25519-js": "0.0.4",
"futoin-hkdf": "^1.3.2",
"tweetnacl": "^1.0.3"

View File

@ -1,8 +1,7 @@
const fs = require("fs");
const path = require("path");
const url = require("url");
const http = require("http");
const https = require("https");
const util = require("util");
const Consensus = require("./tor/Consensus");
const OR = require("./tor/OnionRouter");
const Socket = require("./tor/Socket");
@ -24,19 +23,23 @@ function timestamp() {
const logger = {
debug(...args) {
if (LogLevels.debug > logLevel) return;
console.debug(timestamp(), "D", ...args);
const [format, ...rest] = args;
console.debug(timestamp(), "D", util.format(format, ...rest));
},
info(...args) {
if (LogLevels.info > logLevel) return;
console.log(timestamp(), "I", ...args);
const [format, ...rest] = args;
console.debug(timestamp(), "I", util.format(format, ...rest));
},
warn(...args) {
if (LogLevels.warn > logLevel) return;
console.warn(timestamp(), "W", ...args);
const [format, ...rest] = args;
console.debug(timestamp(), "W", util.format(format, ...rest));
},
error(...args) {
if (LogLevels.error > logLevel) return;
console.error(timestamp(), "E", ...args);
const [format, ...rest] = args;
console.debug(timestamp(), "E", util.format(format, ...rest));
},
};
@ -71,7 +74,8 @@ const logger = {
/** @type Circuit */
let circuit;
try {
const app_cache_path = path.join(process.env.HOME, '.cache', 'mini-tor-js');
const home = process.env.HOME || process.env.USERPROFILE;
const app_cache_path = path.join(home, '.cache', 'mini-tor-js');
const cached_consensus_path = path.join(app_cache_path, 'cached-consensus')
fs.mkdirSync(app_cache_path, { recursive: true });
// Load consensus
@ -131,7 +135,13 @@ const logger = {
}
}
/** @type TorStream */
const tor_stream = await circuit.create_stream(uri.hostname, uri.port);
let tor_stream;
if (uri.hostname.endsWith('.onion')) {
const onion = uri.hostname.substr(0, uri.hostname.length - 6);
tor_stream = await circuit.create_onion_stream(onion, uri.port);
} else {
tor_stream = await circuit.create_stream(uri.hostname, uri.port);
}
if (!tor_stream) {
logger.error("Could not create stream to", uri.href);
return;

View File

@ -1,7 +1,9 @@
const HiddenServiceConnector = require('./HiddenServiceConnector');
const CircuitNode = require('./CircuitNode');
const Cell = require('./Cell')
const RelayCell = require('./RelayCell')
const Stream = require('./TorStream')
const {sha1} = require('../utils/crypto')
const { parseIp } = require('../utils/net')
const { defer } = require('../utils/time')
@ -49,6 +51,10 @@ class Circuit {
return this._node_list;
}
get tor_socket() {
return this._socket;
}
get state() { return this._state };
set state(state) {
@ -417,7 +423,20 @@ class Circuit {
}
}
extend(or, handshake_type) {
async create_onion_stream(onion, port) {
const hidden_service_connector = new HiddenServiceConnector(this, onion);
return await hidden_service_connector.connect()
? this.create_stream(onion, port)
: null;
}
create_dir_stream() {
//TODO
throw new Error('Not implemented');
}
extend(or, handshake_type = 'ntor') {
switch (handshake_type) {
case 'ntor':
return this._extend_ntor(or);
@ -615,6 +634,132 @@ class Circuit {
(this._waits[desired_state] = this._waits[desired_state] || []).push(d);
return d.promise;
}
async rendezvous_establish(rendezvous_cookie) {
//TODO
throw new Error("Not implemented");
}
async rendezvous_introduce(rendezvous_circuit, rendezvous_cookie) {
//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;
this._logger.debug("circuit::rendezvous_introduce() [or: %s, state: introducing]", introduction_point.name);
this.state = States.rendezvous_introducing;
this._logger.debug("circuit::rendezvous_introduce() [or: %s, state: completing]", introduction_point.name);
rendezvous_circuit.state = States.rendezvous_completing;
//
// payload of the RELAY_COMMAND_INTRODUCE1
// command:
//
// PK_ID Identifier for Bob's PK [20 octets]
// VER Version byte: set to 2. [1 octet]
// IP Rendezvous point's address [4 octets]
// PORT Rendezvous point's OR port [2 octets]
// ID Rendezvous point identity ID [20 octets]
// KLEN Length of onion key [2 octets]
// KEY Rendezvous point onion key [KLEN octets]
// RC Rendezvous cookie [20 octets]
// g^x Diffie-Hellman data, part 1 [128 octets]
//
//
// compute PK_ID, aka hash of the service key.
//
const service_key_hash = sha1(introduction_point.service_key);
//
// create rest of the payload in separate buffer;
// it will be encrypted.
//
const handshake_bytes = Buffer.alloc(
1 + // version
4 + // ip address
2 + // port
20 + // identity_fingerprint
2 + // onion key size
32 + // onion key
20 + // rendezvous cookie
128); // DH
//io::memory_stream handshake_stream(handshake_bytes);
//io::stream_wrapper handshake_buffer(handshake_stream, endianness::big_endian);
rendezvous_circuit._extend_node = new CircuitNode(this, introduction_point, circuit_node_type::introduction_point);
handshake_buffer.write(static_cast<uint8_t>(2));
handshake_buffer.write(swap_endianness(introducee->get_ip_address().to_int()));
handshake_buffer.write(introducee->get_or_port());
handshake_buffer.write(introducee->get_identity_fingerprint());
handshake_buffer.write(static_cast<payload_size_type>(introducee->get_onion_key().get_size()));
handshake_buffer.write(introducee->get_onion_key());
handshake_buffer.write(rendezvous_cookie);
handshake_buffer.write(rendezvous_circuit->_extend_node->get_key_agreement().get_public_key());
const handshake_encrypted = hybrid_encryption::encrypt(
handshake_bytes,
introduction_point.service_key);
//
// compose the final payload.
//
const relay_payload_bytes = Buffer.concat([
service_key_hash,
handshake_encrypted
], service_key_hash.length + handshake_encrypted.length);
//
// send the cell.
//
this.send_relay_cell(
0,
Cell.commands.relay_command_introduce1,
relay_payload_bytes);
if (await this.wait_for_state(States.rendezvous_introduced))
{
this._logger.debug("circuit::rendezvous_introduce() [or: %s, state: introduced]", introduction_point.name);
}
else
{
this._logger.error("circuit::rendezvous_introduce() [or: %s, is_rendezvous_introduced() == false]", introduction_point.name);
//
// we cannot expect the rendezvous will be completed.
//
return;
}
if (await rendezvous_circuit.wait_for_state(States.rendezvous_completed))
{
this._logger.debug("circuit::rendezvous_introduce() [or: %s, state: completed]", introduction_point.name);
}
else
{
this._logger.error("circuit::rendezvous_introduce() [or: %s, is_rendezvous_completed() == false]", introduction_point.name);
}
}
is_ready() {
return this.state === States.ready;
}
is_rendezvous_established() {
return this.state === States.rendezvous_established;
}
is_rendezvous_completed() {
return this.state === States.rendezvous_completed;
}
is_rendezvous_introduced() {
return this.state === States.rendezvous_introduced;
}
}
module.exports = Circuit;

View File

@ -18,7 +18,7 @@ const authorities = [
authority_onion_router("moria1", "128.31.0.34", 9101, 9131),
//authority_onion_router("tor26", "86.59.21.38", 443, 80},
authority_onion_router("bastet", "204.13.164.118", 443, 80),
authority_onion_router("maatuska", "171.25.193.9", 80, 443),
//authority_onion_router("maatuska", "171.25.193.9", 80, 443),
authority_onion_router("dannenberg", "193.23.244.244", 443, 80),
authority_onion_router("Faravahar", "154.35.175.225", 443, 80),
authority_onion_router("gabelmoo", "131.188.40.189", 443, 80),
@ -197,7 +197,7 @@ class Consensus {
port = router.dir_port;
}
this._logger.debug(`consensus::download_from_random_authority() [path: http://${ip}:${port}${path}]`);
this._logger.debug(`Consensus._download_from_random_router_impl() [path: http://${ip}:${port}${path}]`);
return get('http:', ip, port, path);
}
@ -236,6 +236,14 @@ class Consensus {
}
}
}
/**
* @param {Buffer} identity_fingerprint
* @returns {OnionRouter}
*/
get_onion_router_by_identity_fingerprint(identity_fingerprint) {
return this._onion_router_map[identity_fingerprint.toString('hex')];
}
}
module.exports = Consensus;

View File

@ -0,0 +1,346 @@
const crypto = require('crypto');
const base32 = require('base32')
const OnionRouter = require('./OnionRouter')
const {get} = require('../utils/http')
const {time} = require('../utils/time')
const {sha1} = require('../utils/crypto')
class HiddenServiceConnector {
/** @type Consensus */
_consensus = null;
/** @type OnionRouter[] */
_introduction_point_list = [];
/** @type OnionRouter[] */
_responsible_directory_list = [];
/** @type Buffer */
_rendezvous_cookie = null;
constructor (rendezvous_circuit, onion) {
this._rendezvous_circuit = rendezvous_circuit;
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));
}
async connect() {
this.find_responsible_directories();
if (this._responsible_directory_list.length)
{
//
// create rendezvous cookie.
//
this._rendezvous_cookie = crypto.randomBytes(20);
//
// establish rendezvous.
//
await this._rendezvous_circuit.rendezvous_establish(this._rendezvous_cookie);
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)
{
await this.introduce();
if (this._rendezvous_circuit.is_rendezvous_completed())
{
return true;
}
}
}
}
return false;
}
find_responsible_directories() {
//
// rend-spec.txt
// 1.4.
// At any time, there are 6 hidden service directories responsible for
// keeping replicas of a descriptor; they consist of 2 sets of 3 hidden
// service directories with consecutive onion IDs. Bob's OP learns about
// the complete list of hidden service directories by filtering the
// consensus status document received from the directory authorities. A
// hidden service directory is deemed responsible for a descriptor ID if
// it has the HSDir flag and its identity digest is one of the first three
// identity digests of HSDir relays following the descriptor ID in a
// circular list. A hidden service directory will only accept a descriptor
// whose timestamp is no more than three days before or one day after the
// current time according to the directory's clock.
//
this._responsible_directory_list = [];
const directory_list = this._consensus.get_onion_routers_by_criteria({
flags: OnionRouter.hsdir
});
//
// search for the 2 sets of 3 hidden service directories.
//
for (let replica = 0; replica < 2; replica++)
{
const descriptor_id = this.get_descriptor_id(replica);
const index = directory_list.findIndex(x => Buffer.compare(x.identity_fingerprint, descriptor_id) < 0);
for (let i = 0; i < 3; i++)
{
this._responsible_directory_list.push(directory_list[(index + i) % directory_list.length]);
}
}
}
get_secret_id(replica) {
const permanent_id_byte = this._permanent_id[0];
//
// rend-spec.txt
// 1.3.
//
// "time-period" changes periodically as a function of time and
// "permanent-id". The current value for "time-period" can be calculated
// using the following formula:
//
// time-period = (current-time + permanent-id-byte * 86400 / 256)
// / 86400
//
const time_period = (time() + (permanent_id_byte * 86400 / 256)) / 86400;
const secret_bytes = Buffer.alloc(5);
secret_bytes.writeInt32BE(time_period);
secret_bytes.writeUInt8(replica);
return sha1(secret_bytes);
}
/**
* @param replica
* @returns {Buffer}
*/
get_descriptor_id(replica) {
const secret_id = this.get_secret_id(replica);
const descriptor_id_bytes = Buffer.concat([
this._permanent_id,
secret_id
], this._permanent_id.length + secret_id.length);
return sha1(descriptor_id_bytes);
}
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];
//
// create new circuit and extend it with responsible directory.
//
this._logger.info(
"\tCreating circuit for hidden service (try #%i), connecting to '%s' (%s:%i)",
i + 1,
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();
if (!directory_circuit)
{
//
// either tor socket is destroyed
// or we couldn't create circuit with the first
// onion router. try it again anyway.
// but if the socket is destroyed, we're out of luck.
//
continue;
}
this._logger.info(
"\tExtending circuit for hidden service, connecting to responsible directory '%s' (%s:%u)",
responsible_directory.name,
responsible_directory.ip,
responsible_directory.or_port);
await directory_circuit.extend(responsible_directory);
if (!directory_circuit.is_ready())
{
this._logger.warn("\tError while extending the directory circuit");
continue;
}
//
// circuit must have exactly 2 nodes now.
//
//mini_assert(directory_circuit->get_circuit_node_list_size() == 2);
this._logger.info("\tExtended...");
const replica = i >= 3;
//
// create the directory stream on the directory circuit.
//
const directory_stream = directory_circuit.create_dir_stream();
if (!directory_stream)
{
this._logger.warn("\tError while establishing the directory stream");
continue;
}
//
// request the hidden service descriptor.
//
const descriptor_path = "/tor/rendezvous2/" + base32.encode(this.get_descriptor_id(replica));
this._logger.debug(
"hidden_service::fetch_hidden_service_descriptor() [path: %s]",
descriptor_path);
this._logger.info("\tSending request for hidden service descriptor...");
const hidden_service_descriptor = await get(
'http:',
responsible_directory.ip,
responsible_directory.dir_port.toString(),
descriptor_path,
directory_stream);
this._logger.info("\tHidden service descriptor received...");
//
// parse hidden service descriptor.
//
if (!hidden_service_descriptor &&
!hidden_service_descriptor.includes("404 Not found"))
{
this._logger.info("\tHidden service descriptor is valid...");
let lines = hidden_service_descriptor.split('\n');
let desc = '', capture = false;
for (const line of lines) {
if (line === '-----BEGIN MESSAGE-----') {
capture = true;
} else if (line === '-----END MESSAGE-----') {
capture = false;
break;
} else if (capture) {
desc += line;
}
}
const introduction_point_list = [];
const introduction_point_desc = Buffer.from(desc, 'base64').toString();
lines = introduction_point_desc.split('\n');
let current_router, serviceKey = null;
for (const line of lines) {
const parts = line.split(' ');
if (parts[0] === 'introduction-point') {
const identity_fingerprint = Buffer.from(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) {
capture = true;
} else if (parts[0] === '-----END RSA PUBLIC KEY-----' && serviceKey !== null) {
current_router.service_key = serviceKey;
introduction_point_list.push(current_router);
capture = false;
serviceKey = null;
} else if (capture) {
serviceKey += line;
}
}
//mini_assert(!parser.introduction_point_list.is_empty());
if (introduction_point_list.length)
{
this._introduction_point_list = introduction_point_list
}
else
{
this._logger.warn("\tHidden service descriptor contains no introduction points...");
}
return i;
}
else
{
this._logger.warn("\tHidden service descriptor is invalid...");
}
}
return -1;
}
async introduce() {
for (const introduction_point of this._introduction_point_list)
{
this._logger.info(
"\tCreating circuit for hidden service introduce, connecting to '%s' (%s:%i)",
this._socket.onion_router.name,
this._socket.onion_router.ip,
this._socket.onion_router.or_port);
/** @type Circuit */
const introduce_circuit = await this._socket.create_circuit();
if (!introduce_circuit)
{
//
// either tor socket is destroyed
// or we couldn't create circuit with the first
// onion router. try it again anyway.
// but if the socket is destroyed, we're out of luck.
//
continue;
}
this._logger.info("\tConnected...");
this._logger.info(
"\tExtending circuit to introduction point '%s' (%s:%u)",
introduction_point.name,
introduction_point.ip,
introduction_point.or_port);
await introduce_circuit.extend(introduction_point);
if (!introduce_circuit.is_ready())
{
this._logger.warn("\tError while extending the introduce circuit");
continue;
}
//
// circuit must have exactly 2 nodes now.
//
//mini_assert(introduce_circuit->get_circuit_node_list_size() == 2);
this._logger.info("\tExtended...");
this._logger.info("\tSending introduce...");
await introduce_circuit.rendezvous_introduce(this._rendezvous_circuit, this._rendezvous_cookie);
if (introduce_circuit.is_rendezvous_introduced())
{
this._logger.info("\tIntroduced successfully...");
break;
}
else
{
this._logger.warn("\tIntroduce failed...");
}
}
}
}
module.exports = HiddenServiceConnector;

View File

@ -24,6 +24,7 @@ class OnionRouter {
_identity_fingerprint = null;
_descriptor_fetched = false;
_ntor_onion_key = null;
_service_key = null;
constructor (consensus, nickname, ip, or_port, dir_port, identity_fingerprint) {
this._consensus = consensus;
@ -34,17 +35,20 @@ class OnionRouter {
this._identity_fingerprint = identity_fingerprint;
}
get name() { return this._nickname };
get ip() { return this._ip };
get flags() { return this._flags };
get or_port() { return this._or_port };
get dir_port() { return this._dir_port };
get consensus() { return this._consensus; }
get name() { return this._nickname; }
get ip() { return this._ip; }
get flags() { return this._flags; }
get or_port() { return this._or_port; }
get dir_port() { return this._dir_port; }
set flags(flags) { this._flags = flags; }
get ntor_onion_key() {
if (!this._descriptor_fetched) throw new Error('forgot to call fetch_descriptor for onion router');
return this._ntor_onion_key;
}
get identity_fingerprint() { return this._identity_fingerprint; }
set service_key(key) { this._service_key = key; }
get service_key() { return this._service_key; }
async fetch_descriptor() {
const descriptor = await this._consensus.get_onion_router_descriptor(this._identity_fingerprint);

View File

@ -34,6 +34,9 @@ class Socket {
this._logger = logger;
}
get onion_router() {
return this._onion_router;
}
get state() {
return this._state;
}
@ -90,6 +93,10 @@ class Socket {
return this._socket && !this._socket.connecting;
}
/**
* @param handshake
* @returns {Promise<Circuit>}
*/
async create_circuit(handshake = 'ntor') {
if (this.state !== States.ready)
{

View File

@ -7,3 +7,13 @@ function hmac_sha256(key, data) {
return h.update(data).digest();
}
exports.hmac_sha256 = hmac_sha256;
/**
* @param {BinaryLike} data
* @returns {Buffer}
*/
function sha1(data) {
const h = crypto.createHash('sha1');
return h.update(data).digest();
}
exports.sha1 = sha1;