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;