tor_netdoc/doc/hsdesc/
middle.rs1use std::sync::LazyLock;
4use subtle::ConstantTimeEq;
5use tor_hscrypto::pk::{HsBlindId, HsClientDescEncSecretKey, HsSvcDescEncKey};
6use tor_hscrypto::{RevisionCounter, Subcredential};
7use tor_llcrypto::pk::curve25519;
8use tor_llcrypto::util::ct::CtByteArray;
9
10use crate::doc::hsdesc::desc_enc::build_descriptor_cookie_key;
11use crate::parse::tokenize::{Item, NetDocReader};
12use crate::parse::{keyword::Keyword, parser::SectionRules};
13use crate::types::misc::B64;
14use crate::{Pos, Result};
15
16use super::HsDescError;
17use super::desc_enc::{
18 HS_DESC_CLIENT_ID_LEN, HS_DESC_ENC_NONCE_LEN, HS_DESC_IV_LEN, HsDescEncNonce, HsDescEncryption,
19};
20
21pub(super) const HS_DESC_AUTH_TYPE: &str = "x25519";
25
26#[derive(Debug, Clone)]
29#[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
30pub(super) struct HsDescMiddle {
31 svc_desc_enc_key: HsSvcDescEncKey,
39 auth_clients: Vec<AuthClient>,
44 encrypted: Vec<u8>,
46}
47
48impl HsDescMiddle {
49 #[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
58 pub(super) fn decrypt_inner(
59 &self,
60 blinded_id: &HsBlindId,
61 revision: RevisionCounter,
62 subcredential: &Subcredential,
63 key: Option<&HsClientDescEncSecretKey>,
64 ) -> std::result::Result<Vec<u8>, super::HsDescError> {
65 let desc_enc_nonce = key.and_then(|k| self.find_cookie(subcredential, k));
66 let decrypt = HsDescEncryption {
67 blinded_id,
68 desc_enc_nonce: desc_enc_nonce.as_ref(),
69 subcredential,
70 revision,
71 string_const: b"hsdir-encrypted-data",
72 };
73
74 match decrypt.decrypt(&self.encrypted) {
75 Ok(mut v) => {
76 if !v.ends_with(b"\n") {
80 v.push(b'\n');
81 }
82 Ok(v)
83 }
84 Err(_) => match (key, desc_enc_nonce) {
85 (Some(_), None) => Err(HsDescError::WrongDecryptionKey),
86 (Some(_), Some(_)) => Err(HsDescError::DecryptionFailed),
87 (None, _) => Err(HsDescError::MissingDecryptionKey),
88 },
89 }
90 }
91
92 fn find_cookie(
103 &self,
104 subcredential: &Subcredential,
105 ks_hsc_desc_enc: &HsClientDescEncSecretKey,
106 ) -> Option<HsDescEncNonce> {
107 use cipher::{KeyIvInit, StreamCipher};
108 use tor_llcrypto::cipher::aes::Aes256Ctr as Cipher;
109 use tor_llcrypto::util::ct::ct_lookup;
110
111 let (client_id, cookie_key) = build_descriptor_cookie_key(
112 ks_hsc_desc_enc.as_ref(),
113 &self.svc_desc_enc_key,
114 subcredential,
115 );
116 let auth_client = ct_lookup(&self.auth_clients, |c| c.client_id.ct_eq(&client_id))?;
118
119 let mut cookie = auth_client.encrypted_cookie;
121 let mut cipher = Cipher::new(&cookie_key.into(), &auth_client.iv.into());
122 cipher.apply_keystream(&mut cookie);
123 Some(cookie.into())
124 }
125}
126
127#[derive(Debug, Clone)]
130pub(super) struct AuthClient {
131 pub(super) client_id: CtByteArray<HS_DESC_CLIENT_ID_LEN>,
136 pub(super) iv: [u8; HS_DESC_IV_LEN],
140 pub(super) encrypted_cookie: [u8; HS_DESC_ENC_NONCE_LEN],
148}
149
150impl AuthClient {
151 fn from_item(item: &Item<'_, HsMiddleKwd>) -> Result<Self> {
153 use crate::NetdocErrorKind as EK;
154
155 if item.kwd() != HsMiddleKwd::AUTH_CLIENT {
156 return Err(EK::Internal.with_msg("called with invalid argument."));
157 }
158 let client_id = item.parse_arg::<B64>(0)?.into_array()?.into();
159 let iv = item.parse_arg::<B64>(1)?.into_array()?;
160 let encrypted_cookie = item.parse_arg::<B64>(2)?.into_array()?;
161 Ok(AuthClient {
162 client_id,
163 iv,
164 encrypted_cookie,
165 })
166 }
167}
168
169decl_keyword! {
170 pub(crate) HsMiddleKwd {
171 "desc-auth-type" => DESC_AUTH_TYPE,
172 "desc-auth-ephemeral-key" => DESC_AUTH_EPHEMERAL_KEY,
173 "auth-client" => AUTH_CLIENT,
174 "encrypted" => ENCRYPTED,
175 }
176}
177
178static HS_MIDDLE_RULES: LazyLock<SectionRules<HsMiddleKwd>> = LazyLock::new(|| {
181 use HsMiddleKwd::*;
182
183 let mut rules = SectionRules::builder();
184 rules.add(DESC_AUTH_TYPE.rule().required().args(1..));
185 rules.add(DESC_AUTH_EPHEMERAL_KEY.rule().required().args(1..));
186 rules.add(AUTH_CLIENT.rule().required().may_repeat().args(3..));
187 rules.add(ENCRYPTED.rule().required().obj_required());
188 rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
189
190 rules.build()
191});
192
193impl HsDescMiddle {
194 #[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
197 pub(super) fn parse(s: &str) -> Result<HsDescMiddle> {
198 let mut reader = NetDocReader::new(s)?;
199 let result = HsDescMiddle::take_from_reader(&mut reader).map_err(|e| e.within(s))?;
200 Ok(result)
201 }
202
203 fn take_from_reader(reader: &mut NetDocReader<'_, HsMiddleKwd>) -> Result<HsDescMiddle> {
207 use crate::NetdocErrorKind as EK;
208 use HsMiddleKwd::*;
209
210 let body = HS_MIDDLE_RULES.parse(reader)?;
211
212 {
214 let auth_type = body.required(DESC_AUTH_TYPE)?.required_arg(0)?;
215 if auth_type != HS_DESC_AUTH_TYPE {
216 return Err(EK::BadDocumentVersion
217 .at_pos(Pos::at(auth_type))
218 .with_msg(format!("Unrecognized desc-auth-type {auth_type:?}")));
219 }
220 }
221
222 let ephemeral_key: HsSvcDescEncKey = {
224 let token = body.required(DESC_AUTH_EPHEMERAL_KEY)?;
225 let key = curve25519::PublicKey::from(token.parse_arg::<B64>(0)?.into_array()?);
226 key.into()
227 };
228
229 let auth_clients: Vec<AuthClient> = body
231 .slice(AUTH_CLIENT)
232 .iter()
233 .map(AuthClient::from_item)
234 .collect::<Result<Vec<_>>>()?;
235
236 let encrypted_body: Vec<u8> = body.required(ENCRYPTED)?.obj("MESSAGE")?;
238
239 Ok(HsDescMiddle {
240 svc_desc_enc_key: ephemeral_key,
241 auth_clients,
242 encrypted: encrypted_body,
243 })
244 }
245}
246
247#[cfg(test)]
248mod test {
249 #![allow(clippy::bool_assert_comparison)]
251 #![allow(clippy::clone_on_copy)]
252 #![allow(clippy::dbg_macro)]
253 #![allow(clippy::mixed_attributes_style)]
254 #![allow(clippy::print_stderr)]
255 #![allow(clippy::print_stdout)]
256 #![allow(clippy::single_char_pattern)]
257 #![allow(clippy::unwrap_used)]
258 #![allow(clippy::unchecked_duration_subtraction)]
259 #![allow(clippy::useless_vec)]
260 #![allow(clippy::needless_pass_by_value)]
261 use hex_literal::hex;
264 use tor_checkable::{SelfSigned, Timebound};
265
266 use super::*;
267 use crate::doc::hsdesc::{
268 outer::HsDescOuter,
269 test_data::{TEST_DATA, TEST_SUBCREDENTIAL},
270 };
271
272 #[test]
273 fn parse_good() -> Result<()> {
274 let desc = HsDescOuter::parse(TEST_DATA)?
275 .dangerously_assume_wellsigned()
276 .dangerously_assume_timely();
277 let subcred = TEST_SUBCREDENTIAL.into();
278 let body = desc.decrypt_body(&subcred).unwrap();
279 let body = std::str::from_utf8(&body[..]).unwrap();
280
281 let middle = HsDescMiddle::parse(body)?;
282 assert_eq!(
283 middle.svc_desc_enc_key.as_bytes(),
284 &hex!("161090571E6DB517C0C8591CE524A56DF17BAE3FF8DCD50735F9AEB89634073E")
285 );
286 assert_eq!(middle.auth_clients.len(), 16);
287
288 let _inner_body = middle
297 .decrypt_inner(&desc.blinded_id(), desc.revision_counter(), &subcred, None)
298 .unwrap();
299
300 Ok(())
301 }
302}