mini-tor-js/src/tor/HiddenServiceConnector.js

350 lines
11 KiB
JavaScript

const crypto = require('crypto');
const base32 = require('../utils/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;
_logger;
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._onion = onion;
this._permanent_id = Buffer.from(base32.decode(onion));
this._logger = logger;
this._time = time;
}
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 = Math.floor((this._time() + (permanent_id_byte * 86400 / 256)) / 86400);
const secret_bytes = Buffer.alloc(5);
secret_bytes.writeUInt32BE(time_period, 0);
secret_bytes.writeUInt8(replica, 4);
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);
/** @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("\tConnected...");
this._logger.info(
"\tExtending circuit for hidden service, connecting to responsible directory '%s' (%s:%i)",
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 = await 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... %s:%i", responsible_directory.ip, responsible_directory.dir_port);
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 = 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 (line === '-----BEGIN RSA PUBLIC KEY-----' && serviceKey !== null) {
capture = true;
} 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;
} 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:%i)",
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...");
return;
}
else
{
this._logger.warn("\tIntroduce failed...");
}
}
}
}
module.exports = HiddenServiceConnector;