onionlink-core 0.1.2

Core Tor v3 onion-service client protocol implementation for onionlink
Documentation
use log::info;

use crate::crypto::{
    ct_equal, hkdf_sha256_expand, hmac_sha256, kdf_tor, random_bytes, x25519_public_from_private,
    x25519_shared, DigestKind, RelayCrypto,
};
use crate::directory::{decode_http_body, same_relay};
use crate::directory::{relay_link_specifiers, serialize_link_specifiers, Relay};
use crate::error::{ensure, err, Result};
use crate::tor::{
    TorChannel, CMD_CREATED_FAST, CMD_CREATE_FAST, CMD_DESTROY, CMD_RELAY, CMD_RELAY_EARLY,
    K_CELL_BODY_LEN, K_RELAY_HEADER_LEN, K_RELAY_PAYLOAD_LEN, RELAY_BEGIN_DIR, RELAY_CONNECTED,
    RELAY_DATA, RELAY_END, RELAY_EXTEND2, RELAY_EXTENDED2, RELAY_SENDME,
};
use crate::util::{from_string, put_u16, read_u16, Bytes};

#[derive(Clone, Debug, Default)]
pub struct NtorState {
    pub x: Bytes,
    pub x_pub: Bytes,
    pub b: Bytes,
    pub id: Bytes,
    pub bx: Bytes,
}

pub fn build_ntor_onionskin(relay: &Relay, st: &mut NtorState) -> Result<Bytes> {
    ensure(
        relay.rsa_id.len() == 20,
        "relay missing RSA identity for ntor",
    )?;
    ensure(relay.ntor_key.len() == 32, "relay missing ntor key")?;
    st.x = random_bytes(32);
    st.x_pub = x25519_public_from_private(&st.x)?;
    st.b = relay.ntor_key.clone();
    st.id = relay.rsa_id.clone();
    st.bx = x25519_shared(&st.x, &st.b)?;
    let mut hdata = st.id.clone();
    hdata.extend_from_slice(&st.b);
    hdata.extend_from_slice(&st.x_pub);
    Ok(hdata)
}

pub fn finish_ntor(st: &NtorState, hdata: &[u8]) -> Result<RelayCrypto> {
    ensure(hdata.len() >= 64, "short ntor CREATED2 data")?;
    let y = &hdata[..32];
    let auth = &hdata[32..64];
    let yx = x25519_shared(&st.x, y)?;
    let proto = from_string("ntor-curve25519-sha256-1");
    let mut secret = yx;
    secret.extend_from_slice(&st.bx);
    secret.extend_from_slice(&st.id);
    secret.extend_from_slice(&st.b);
    secret.extend_from_slice(&st.x_pub);
    secret.extend_from_slice(y);
    secret.extend_from_slice(&proto);
    let t_key = from_string("ntor-curve25519-sha256-1:key_extract");
    let t_verify = from_string("ntor-curve25519-sha256-1:verify");
    let t_mac = from_string("ntor-curve25519-sha256-1:mac");
    let key_seed = hmac_sha256(&t_key, &secret)?;
    let verify = hmac_sha256(&t_verify, &secret)?;
    let mut auth_input = verify;
    auth_input.extend_from_slice(&st.id);
    auth_input.extend_from_slice(&st.b);
    auth_input.extend_from_slice(y);
    auth_input.extend_from_slice(&st.x_pub);
    auth_input.extend_from_slice(&proto);
    auth_input.extend_from_slice(b"Server");
    let expected = hmac_sha256(&t_mac, &auth_input)?;
    ensure(ct_equal(auth, &expected), "ntor auth mismatch")?;
    let k = hkdf_sha256_expand(&key_seed, b"ntor-curve25519-sha256-1:key_expand", 92)?;
    Ok(RelayCrypto::new(
        &k[0..20],
        &k[20..40],
        &k[40..56],
        &k[56..72],
        DigestKind::Sha1,
    ))
}

#[derive(Clone, Debug, Default)]
pub struct RelayMessage {
    pub cmd: u8,
    pub stream_id: u16,
    pub data: Bytes,
}

pub struct Circuit {
    ch: TorChannel,
    id: u32,
    hops: Vec<RelayCrypto>,
}

impl Circuit {
    pub fn create_fast(mut ch: TorChannel) -> Result<Self> {
        let id = ch.new_circ_id();
        info!("creating fast circuit {id}");
        let x = random_bytes(20);
        let mut body = x.clone();
        body.resize(K_CELL_BODY_LEN, 0);
        ch.write_cell(id, CMD_CREATE_FAST, &body)?;
        loop {
            let c = ch.read_cell()?;
            if c.circ_id != id {
                continue;
            }
            if c.cmd == CMD_DESTROY {
                return err("CREATE_FAST was destroyed");
            }
            ensure(
                c.cmd == CMD_CREATED_FAST,
                "unexpected cell while waiting for CREATED_FAST",
            )?;
            let y = &c.body[..20];
            let kh = &c.body[20..40];
            let mut k0 = x;
            k0.extend_from_slice(y);
            let k = kdf_tor(&k0, 92);
            ensure(ct_equal(kh, &k[..20]), "CREATE_FAST key hash mismatch")?;
            let rc = RelayCrypto::new(
                &k[20..40],
                &k[40..60],
                &k[60..76],
                &k[76..92],
                DigestKind::Sha1,
            );
            return Ok(Self {
                ch,
                id,
                hops: vec![rc],
            });
        }
    }

    pub fn send_relay(&mut self, cmd: u8, stream_id: u16, data: &[u8]) -> Result<()> {
        let hop_index = self.hops.len() - 1;
        self.send_relay_cell(CMD_RELAY, hop_index, cmd, stream_id, data)
    }

    pub fn recv_relay(&mut self) -> Result<RelayMessage> {
        loop {
            let c = self.ch.read_cell()?;
            if c.circ_id != self.id {
                continue;
            }
            if c.cmd == CMD_DESTROY {
                return err("circuit destroyed");
            }
            if c.cmd != CMD_RELAY {
                continue;
            }
            let mut body = c.body;
            for hop in &mut self.hops {
                body = hop.decrypt_body_only(&body)?;
                if hop.recognize_decrypted(&body) {
                    return parse_relay_body(&body);
                }
            }
        }
    }

    pub fn send_raw_body(&mut self, body: &[u8]) -> Result<()> {
        ensure(
            body.len() == K_CELL_BODY_LEN,
            "raw relay body must be one cell body",
        )?;
        let mut encrypted = body.to_vec();
        for hop in self.hops.iter_mut().rev() {
            encrypted = hop.encrypt_body_only(&encrypted)?;
        }
        self.ch.write_cell(self.id, CMD_RELAY, &encrypted)
    }

    pub fn recv_raw_body(&mut self) -> Result<Bytes> {
        loop {
            let c = self.ch.read_cell()?;
            if c.circ_id != self.id {
                continue;
            }
            if c.cmd == CMD_DESTROY {
                return err("rendezvous circuit destroyed");
            }
            if c.cmd == CMD_RELAY {
                let mut body = c.body;
                for hop in &mut self.hops {
                    body = hop.decrypt_body_only(&body)?;
                }
                return Ok(body);
            }
        }
    }

    pub fn extend_ntor(&mut self, relay: &Relay) -> Result<()> {
        info!(
            "extending circuit to relay {} at {}:{}",
            relay.nickname, relay.ip, relay.or_port
        );
        let mut st = NtorState::default();
        let hdata = build_ntor_onionskin(relay, &mut st)?;
        let lspec = serialize_link_specifiers(&relay_link_specifiers(relay)?)?;
        let mut data = lspec;
        put_u16(&mut data, 2);
        put_u16(&mut data, hdata.len() as u16);
        data.extend_from_slice(&hdata);
        let hop_index = self.hops.len() - 1;
        self.send_relay_cell(CMD_RELAY_EARLY, hop_index, RELAY_EXTEND2, 0, &data)?;
        loop {
            let m = self.recv_relay()?;
            if m.cmd == RELAY_EXTENDED2 {
                ensure(m.data.len() >= 2, "short EXTENDED2 body")?;
                let hlen = read_u16(&m.data, 0)? as usize;
                ensure(m.data.len() >= 2 + hlen, "truncated EXTENDED2 handshake")?;
                let created = &m.data[2..2 + hlen];
                self.hops.push(finish_ntor(&st, created)?);
                return Ok(());
            }
        }
    }

    fn send_relay_cell(
        &mut self,
        cell_cmd: u8,
        hop_index: usize,
        relay_cmd: u8,
        stream_id: u16,
        data: &[u8],
    ) -> Result<()> {
        ensure(hop_index < self.hops.len(), "bad hop index")?;
        let mut body = self.hops[hop_index].encrypt_relay(relay_cmd, stream_id, data)?;
        for i in (0..hop_index).rev() {
            body = self.hops[i].encrypt_body_only(&body)?;
        }
        self.ch.write_cell(self.id, cell_cmd, &body)
    }
}

pub fn parse_relay_body(body: &[u8]) -> Result<RelayMessage> {
    ensure(body.len() == K_CELL_BODY_LEN, "bad relay body")?;
    let len = read_u16(body, 9)? as usize;
    ensure(len <= K_RELAY_PAYLOAD_LEN, "relay length too large")?;
    Ok(RelayMessage {
        cmd: body[0],
        stream_id: read_u16(body, 3)?,
        data: body[K_RELAY_HEADER_LEN..K_RELAY_HEADER_LEN + len].to_vec(),
    })
}

pub fn begin_dir_get_via(
    guard: &Relay,
    target: &Relay,
    path: &str,
    timeout_ms: i32,
) -> Result<Bytes> {
    info!(
        "opening BEGIN_DIR request to {} via guard {}",
        target.nickname, guard.nickname
    );
    let ch = TorChannel::new(guard.ip.clone(), guard.or_port, timeout_ms)?;
    let mut circ = Circuit::create_fast(ch)?;
    circ.extend_ntor(target)?;
    circ.send_relay(RELAY_BEGIN_DIR, 1, &[])?;
    loop {
        let m = circ.recv_relay()?;
        if m.cmd == RELAY_CONNECTED && m.stream_id == 1 {
            break;
        }
        if m.cmd == RELAY_END && m.stream_id == 1 {
            return err(format!("BEGIN_DIR rejected by {}", target.nickname));
        }
    }
    let req = format!(
        "GET {path} HTTP/1.0\r\nHost: {}\r\nUser-Agent: onionlink/0\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n",
        target.ip
    );
    let rb = from_string(req);
    for chunk in rb.chunks(K_RELAY_PAYLOAD_LEN) {
        circ.send_relay(RELAY_DATA, 1, chunk)?;
    }
    let mut response = Bytes::new();
    let mut circ_window = 1000;
    let mut stream_window = 500;
    loop {
        let m = circ.recv_relay()?;
        if m.cmd == RELAY_DATA && m.stream_id == 1 {
            response.extend_from_slice(&m.data);
            circ_window -= 1;
            if circ_window <= 900 {
                circ.send_relay(RELAY_SENDME, 0, &[0, 0, 0])?;
                circ_window += 100;
            }
            stream_window -= 1;
            if stream_window <= 450 {
                circ.send_relay(RELAY_SENDME, 1, &[])?;
                stream_window += 50;
            }
        } else if m.cmd == RELAY_END && m.stream_id == 1 {
            break;
        }
        if response.len() > 8 * 1024 * 1024 {
            return err("BEGIN_DIR response too large");
        }
    }
    decode_http_body(&response)
}

pub fn connect_guard_circuit(guard: &Relay, timeout_ms: i32) -> Result<Circuit> {
    Circuit::create_fast(TorChannel::new(
        guard.ip.clone(),
        guard.or_port,
        timeout_ms,
    )?)
}

pub fn extend_unless_same(circ: &mut Circuit, guard: &Relay, target: &Relay) -> Result<()> {
    if !same_relay(guard, target) {
        circ.extend_ntor(target)?;
    }
    Ok(())
}