use std::sync::LazyLock;
use subtle::ConstantTimeEq;
use tor_hscrypto::pk::{HsBlindId, HsClientDescEncSecretKey, HsSvcDescEncKey};
use tor_hscrypto::{RevisionCounter, Subcredential};
use tor_llcrypto::pk::curve25519;
use tor_llcrypto::util::ct::CtByteArray;
use crate::doc::hsdesc::desc_enc::build_descriptor_cookie_key;
use crate::parse::tokenize::{Item, NetDocReader};
use crate::parse::{keyword::Keyword, parser::SectionRules};
use crate::types::misc::B64;
use crate::{Pos, Result};
use super::HsDescError;
use super::desc_enc::{
HS_DESC_CLIENT_ID_LEN, HS_DESC_ENC_NONCE_LEN, HS_DESC_IV_LEN, HsDescEncNonce, HsDescEncryption,
};
pub(super) const HS_DESC_AUTH_TYPE: &str = "x25519";
#[derive(Debug, Clone)]
#[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
pub(super) struct HsDescMiddle {
svc_desc_enc_key: HsSvcDescEncKey,
auth_clients: Vec<AuthClient>,
encrypted: Vec<u8>,
}
impl HsDescMiddle {
#[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
pub(super) fn decrypt_inner(
&self,
blinded_id: &HsBlindId,
revision: RevisionCounter,
subcredential: &Subcredential,
key: Option<&HsClientDescEncSecretKey>,
) -> std::result::Result<Vec<u8>, super::HsDescError> {
let desc_enc_nonce = key.and_then(|k| self.find_cookie(subcredential, k));
let decrypt = HsDescEncryption {
blinded_id,
desc_enc_nonce: desc_enc_nonce.as_ref(),
subcredential,
revision,
string_const: b"hsdir-encrypted-data",
};
match decrypt.decrypt(&self.encrypted) {
Ok(mut v) => {
if !v.ends_with(b"\n") {
v.push(b'\n');
}
Ok(v)
}
Err(_) => match (key, desc_enc_nonce) {
(Some(_), None) => Err(HsDescError::WrongDecryptionKey),
(Some(_), Some(_)) => Err(HsDescError::DecryptionFailed),
(None, _) => Err(HsDescError::MissingDecryptionKey),
},
}
}
fn find_cookie(
&self,
subcredential: &Subcredential,
ks_hsc_desc_enc: &HsClientDescEncSecretKey,
) -> Option<HsDescEncNonce> {
use cipher::{KeyIvInit, StreamCipher};
use tor_llcrypto::cipher::aes::Aes256Ctr as Cipher;
use tor_llcrypto::util::ct::ct_lookup;
let (client_id, cookie_key) = build_descriptor_cookie_key(
ks_hsc_desc_enc.as_ref(),
&self.svc_desc_enc_key,
subcredential,
);
let auth_client = ct_lookup(&self.auth_clients, |c| c.client_id.ct_eq(&client_id))?;
let mut cookie = auth_client.encrypted_cookie;
let mut cipher = Cipher::new(&cookie_key.into(), &auth_client.iv.into());
cipher.apply_keystream(&mut cookie);
Some(cookie.into())
}
}
#[derive(Debug, Clone)]
pub(super) struct AuthClient {
pub(super) client_id: CtByteArray<HS_DESC_CLIENT_ID_LEN>,
pub(super) iv: [u8; HS_DESC_IV_LEN],
pub(super) encrypted_cookie: [u8; HS_DESC_ENC_NONCE_LEN],
}
impl AuthClient {
fn from_item(item: &Item<'_, HsMiddleKwd>) -> Result<Self> {
use crate::NetdocErrorKind as EK;
if item.kwd() != HsMiddleKwd::AUTH_CLIENT {
return Err(EK::Internal.with_msg("called with invalid argument."));
}
let client_id = item.parse_arg::<B64>(0)?.into_array()?.into();
let iv = item.parse_arg::<B64>(1)?.into_array()?;
let encrypted_cookie = item.parse_arg::<B64>(2)?.into_array()?;
Ok(AuthClient {
client_id,
iv,
encrypted_cookie,
})
}
}
decl_keyword! {
pub(crate) HsMiddleKwd {
"desc-auth-type" => DESC_AUTH_TYPE,
"desc-auth-ephemeral-key" => DESC_AUTH_EPHEMERAL_KEY,
"auth-client" => AUTH_CLIENT,
"encrypted" => ENCRYPTED,
}
}
static HS_MIDDLE_RULES: LazyLock<SectionRules<HsMiddleKwd>> = LazyLock::new(|| {
use HsMiddleKwd::*;
let mut rules = SectionRules::builder();
rules.add(DESC_AUTH_TYPE.rule().required().args(1..));
rules.add(DESC_AUTH_EPHEMERAL_KEY.rule().required().args(1..));
rules.add(AUTH_CLIENT.rule().required().may_repeat().args(3..));
rules.add(ENCRYPTED.rule().required().obj_required());
rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
rules.build()
});
impl HsDescMiddle {
#[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
pub(super) fn parse(s: &str) -> Result<HsDescMiddle> {
let mut reader = NetDocReader::new(s)?;
let result = HsDescMiddle::take_from_reader(&mut reader).map_err(|e| e.within(s))?;
Ok(result)
}
fn take_from_reader(reader: &mut NetDocReader<'_, HsMiddleKwd>) -> Result<HsDescMiddle> {
use crate::NetdocErrorKind as EK;
use HsMiddleKwd::*;
let body = HS_MIDDLE_RULES.parse(reader)?;
{
let auth_type = body.required(DESC_AUTH_TYPE)?.required_arg(0)?;
if auth_type != HS_DESC_AUTH_TYPE {
return Err(EK::BadDocumentVersion
.at_pos(Pos::at(auth_type))
.with_msg(format!("Unrecognized desc-auth-type {auth_type:?}")));
}
}
let ephemeral_key: HsSvcDescEncKey = {
let token = body.required(DESC_AUTH_EPHEMERAL_KEY)?;
let key = curve25519::PublicKey::from(token.parse_arg::<B64>(0)?.into_array()?);
key.into()
};
let auth_clients: Vec<AuthClient> = body
.slice(AUTH_CLIENT)
.iter()
.map(AuthClient::from_item)
.collect::<Result<Vec<_>>>()?;
let encrypted_body: Vec<u8> = body.required(ENCRYPTED)?.obj("MESSAGE")?;
Ok(HsDescMiddle {
svc_desc_enc_key: ephemeral_key,
auth_clients,
encrypted: encrypted_body,
})
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use hex_literal::hex;
use tor_checkable::{SelfSigned, Timebound};
use super::*;
use crate::doc::hsdesc::{
outer::HsDescOuter,
test_data::{TEST_DATA, TEST_SUBCREDENTIAL},
};
#[test]
fn parse_good() -> Result<()> {
let desc = HsDescOuter::parse(TEST_DATA)?
.dangerously_assume_wellsigned()
.dangerously_assume_timely();
let subcred = TEST_SUBCREDENTIAL.into();
let body = desc.decrypt_body(&subcred).unwrap();
let body = std::str::from_utf8(&body[..]).unwrap();
let middle = HsDescMiddle::parse(body)?;
assert_eq!(
middle.svc_desc_enc_key.as_bytes(),
&hex!("161090571E6DB517C0C8591CE524A56DF17BAE3FF8DCD50735F9AEB89634073E")
);
assert_eq!(middle.auth_clients.len(), 16);
let _inner_body = middle
.decrypt_inner(&desc.blinded_id(), desc.revision_counter(), &subcred, None)
.unwrap();
Ok(())
}
}