350 lines
11 KiB
JavaScript
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;
|