diff --git a/Cargo.lock b/Cargo.lock index edb49a87b..e01e21bff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4068,8 +4068,10 @@ dependencies = [ name = "tor-netdoc" version = "0.6.1" dependencies = [ + "arrayref", "base64ct", "bitflags", + "cipher", "derive_more", "digest 0.10.6", "educe", @@ -4087,6 +4089,7 @@ dependencies = [ "thiserror", "time", "tinystr", + "tor-basic-utils", "tor-bytes", "tor-cert", "tor-checkable", @@ -4098,6 +4101,7 @@ dependencies = [ "visibility", "visible", "weak-table", + "zeroize", ] [[package]] diff --git a/crates/tor-netdoc/Cargo.toml b/crates/tor-netdoc/Cargo.toml index fbfe6136a..6748da8e0 100644 --- a/crates/tor-netdoc/Cargo.toml +++ b/crates/tor-netdoc/Cargo.toml @@ -52,8 +52,10 @@ experimental-api = [] dangerous-expose-struct-fields = ["visible", "visibility"] [dependencies] +arrayref = "0.3" base64ct = { version = "1.5.1", features = ["alloc"] } bitflags = "1" +cipher = { version = "0.4.1", features = ["zeroize"] } derive_more = "0.99.3" digest = "0.10.0" educe = "0.4.6" @@ -72,18 +74,21 @@ tor-bytes = { path = "../tor-bytes", version = "0.6.1" } tor-cert = { path = "../tor-cert", version = "0.6.1" } tor-checkable = { path = "../tor-checkable", version = "0.4.0" } tor-error = { path = "../tor-error", version = "0.4.1" } -tor-hscrypto = { path = "../tor-hscrypto", version = "0.1.0", optional = true } +tor-hscrypto = { path = "../tor-hscrypto", version = "0.1.0", optional = true } tor-linkspec = { path = "../tor-linkspec", version = "0.6.0", optional = true } tor-llcrypto = { path = "../tor-llcrypto", version = "0.4.1" } tor-protover = { path = "../tor-protover", version = "0.4.0" } visibility = { version = "0.0.1", optional = true } visible = { version = "0.0.1", optional = true } weak-table = "0.3.0" +zeroize = "1" [dev-dependencies] hex-literal = "0.3" itertools = "0.10.1" serde_json = "1.0.50" +tor-basic-utils = { version = "0.5.0", path = "../tor-basic-utils" } + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/tor-netdoc/src/doc/hsdesc/desc_enc.rs b/crates/tor-netdoc/src/doc/hsdesc/desc_enc.rs index eec127895..22da13d75 100644 --- a/crates/tor-netdoc/src/doc/hsdesc/desc_enc.rs +++ b/crates/tor-netdoc/src/doc/hsdesc/desc_enc.rs @@ -2,8 +2,17 @@ //! //! TODO hs: It's possible that this should move to tor-netdoc. -use rand::CryptoRng; use tor_hscrypto::{pk::BlindedOnionId, RevisionCounter, Subcredential}; +use tor_llcrypto::cipher::aes::Aes256Ctr as Cipher; +use tor_llcrypto::d::Sha3_256 as Hash; +use tor_llcrypto::d::Shake256 as KDF; + +use arrayref::array_ref; +use cipher::{KeyIvInit, StreamCipher}; +use digest::{ExtendableOutput, FixedOutput, Update, XofReader}; +use rand::{CryptoRng, Rng}; +use tor_llcrypto::util::ct::CtByteArray; +use zeroize::Zeroizing as Z; /// Parameters for encrypting or decrypting part of an onion service descriptor. /// @@ -24,17 +33,113 @@ pub(super) struct HsDescEncryption<'a> { /// A value used in deriving the encryption key for the inner layer of onion /// service encryption. +#[derive(derive_more::AsRef)] pub(super) struct DescEncryptionCookie([u8; 32]); +/// Length of our cryptographic salt. +const SALT_LEN: usize = 16; +/// Length of our ersatz MAC. +const MAC_LEN: usize = 32; +/// An instance of our cryptographic salt. +type Salt = [u8; SALT_LEN]; + impl<'a> HsDescEncryption<'a> { /// Encrypt a given bytestring using these encryption parameters. - pub(super) fn encrypt(&self, rng: &mut R, data: &[u8]) -> Vec { - todo!() // TODO hs + pub(super) fn encrypt(&self, rng: &mut R, data: &[u8]) -> Vec { + let output_len = data.len() + SALT_LEN + MAC_LEN; + let mut output = Vec::with_capacity(output_len); + let salt: [u8; SALT_LEN] = rng.gen(); + + let (mut cipher, mut mac) = self.init(&salt); + + output.extend_from_slice(&salt[..]); + output.extend_from_slice(data); + cipher.apply_keystream(&mut output[SALT_LEN..]); + mac.update(&output[SALT_LEN..]); + let mut mac_val = Default::default(); + let mac = mac.finalize_into(&mut mac_val); + output.extend_from_slice(&mac_val); + debug_assert_eq!(output.len(), output_len); + + output } /// Decrypt a given bytestring that was first encrypted using these /// encryption parameters. pub(super) fn decrypt(&self, data: &[u8]) -> Result, DecryptionError> { - todo!() // TODO hs + if data.len() < SALT_LEN + MAC_LEN { + return Err(DecryptionError::default()); + } + let msg_len = data.len() - SALT_LEN - MAC_LEN; + + let salt = *array_ref![data, 0, SALT_LEN]; + let ciphertext = &data[SALT_LEN..(SALT_LEN + msg_len)]; + + let expected_mac = CtByteArray::from(*array_ref![data, SALT_LEN + msg_len, MAC_LEN]); + let (mut cipher, mut mac) = self.init(&salt); + + // check mac. + mac.update(ciphertext); + let mut received_mac = CtByteArray::from([0_u8; MAC_LEN]); + mac.finalize_into(received_mac.as_mut().into()); + if received_mac != expected_mac { + return Err(DecryptionError::default()); + } + + let mut decrypted = ciphertext.to_vec(); + cipher.apply_keystream(&mut decrypted[..]); + + Ok(decrypted) + } + + /// Return the cryptographic objects that are used for en/decrypting and + /// authenticating a HsDesc layer, given these parameters and a provided + /// salt. + fn init(&self, salt: &[u8; 16]) -> (Cipher, Hash) { + let mut key_stream = self.get_kdf(salt).finalize_xof(); + + /// Length of our MAC key. + const MAC_KEY_LEN: usize = 32; + /// Length of the cipher key that we use. + const CIPHER_KEY_LEN: usize = 32; + /// Length of our cipher's IV. + const IV_LEN: usize = 16; + + let mut key = Z::new([0_u8; CIPHER_KEY_LEN]); + let mut iv = Z::new([0_u8; IV_LEN]); + let mut mac_key = Z::new([0_u8; MAC_KEY_LEN]); // XXXX conjectural! + key_stream.read(&mut key[..]); + key_stream.read(&mut iv[..]); + key_stream.read(&mut mac_key[..]); + + let cipher = Cipher::new(key.as_ref().into(), iv.as_ref().into()); + + let mut mac = Hash::default(); + mac.update(&(MAC_KEY_LEN as u64).to_be_bytes()); + mac.update(&mac_key[..]); + mac.update(&(salt.len() as u64).to_be_bytes()); + mac.update(&salt[..]); + + (cipher, mac) + } + + /// Return a KDF that can yield the keys to be used for encryption with + /// these key parameters. + fn get_kdf(&self, salt: &[u8; 16]) -> KDF { + let mut kdf = KDF::default(); + + // secret_input = SECRET_DATA | N_hs_subcred | INT_8(revision_counter) + kdf.update(self.blinded_id.as_ref()); + if let Some(cookie) = self.encryption_cookie { + kdf.update(cookie.as_ref()); + } + kdf.update(self.subcredential.as_ref()); + kdf.update(&u64::from(self.revision).to_be_bytes()); + + // keys = KDF(secret_input | salt | STRING_CONSTANT, S_KEY_LEN + S_IV_LEN + MAC_KEY_LEN) + kdf.update(salt); + kdf.update(self.string_const); + + kdf } } @@ -42,6 +147,74 @@ impl<'a> HsDescEncryption<'a> { /// /// This error is deliberately uninformative, to avoid side channels. #[non_exhaustive] -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Clone, Debug, Default, thiserror::Error)] #[error("Unable to decrypt onion service decryptor.")] pub struct DecryptionError {} + +#[cfg(test)] +mod test { + // @@ begin test lint list maintained by maint/add_warning @@ + #![allow(clippy::bool_assert_comparison)] + #![allow(clippy::clone_on_copy)] + #![allow(clippy::dbg_macro)] + #![allow(clippy::print_stderr)] + #![allow(clippy::print_stdout)] + #![allow(clippy::single_char_pattern)] + #![allow(clippy::unwrap_used)] + #![allow(clippy::unchecked_duration_subtraction)] + //! + + use super::*; + use tor_basic_utils::test_rng::testing_rng; + + #[test] + fn roundtrip_basics() { + let blinded_id = [7; 32].into(); + let subcredential = [11; 32].into(); + let revision = 13.into(); + let string_const = "greetings puny humans"; + let params = HsDescEncryption { + blinded_id: &blinded_id, + encryption_cookie: None, + subcredential: &subcredential, + revision, + string_const: string_const.as_bytes(), + }; + + let mut rng = testing_rng(); + + let bigmsg: Vec = (1..123).cycle().take(1021).collect(); + for message in [&b""[..], &b"hello world"[..], &bigmsg[..]] { + let mut encrypted = params.encrypt(&mut rng, message); + assert_eq!(encrypted.len(), message.len() + 48); + let decrypted = params.decrypt(&encrypted[..]).unwrap(); + assert_eq!(message, &decrypted); + + // Make sure we can't decrypt a partial input. + let decryption_err = params.decrypt(&encrypted[..encrypted.len() - 1]); + assert!(decryption_err.is_err()); + // Frob a point in the encrypted form and ensure we won't decrypt. + encrypted[7] ^= 3; + let decryption_err = params.decrypt(&encrypted[..]); + assert!(decryption_err.is_err()); + } + } + + #[test] + fn too_short() { + let blinded_id = [7; 32].into(); + let subcredential = [11; 32].into(); + let revision = 13.into(); + let string_const = "greetings puny humans"; + let params = HsDescEncryption { + blinded_id: &blinded_id, + encryption_cookie: None, + subcredential: &subcredential, + revision, + string_const: string_const.as_bytes(), + }; + + assert!(params.decrypt(b"").is_err()); + assert!(params.decrypt(&[0_u8; 47]).is_err()); + } +} diff --git a/crates/tor-netdoc/src/doc/hsdesc/outer_layer.rs b/crates/tor-netdoc/src/doc/hsdesc/outer_layer.rs index 5ee1da5a9..11265ca2f 100644 --- a/crates/tor-netdoc/src/doc/hsdesc/outer_layer.rs +++ b/crates/tor-netdoc/src/doc/hsdesc/outer_layer.rs @@ -5,12 +5,16 @@ use tor_cert::Ed25519Cert; use tor_checkable::signed::SignatureGated; use tor_checkable::timed::TimerangeBound; use tor_checkable::Timebound; +use tor_hscrypto::pk::BlindedOnionId; +use tor_hscrypto::{RevisionCounter, Subcredential}; use tor_llcrypto::pk::ed25519::{self, ValidatableEd25519Signature}; use crate::parse::{keyword::Keyword, parser::SectionRules, tokenize::NetDocReader}; use crate::types::misc::{UnvalidatedEdCert, B64}; use crate::{Pos, Result}; +use super::desc_enc; + /// A more-or-less verbatim representation of the outermost layer of an onion /// service descriptor. #[derive(Clone, Debug)] @@ -37,10 +41,36 @@ pub(super) struct HsDescOuter { pub(super) encrypted_body: Vec, } -/// An `HsDescOuter` whose signatures have all been verified, but which has not -/// been checked for timeliness. -#[derive(Clone, Debug)] -pub(super) struct HsDescOuterSigChecked(HsDescOuter); +impl HsDescOuter { + /// Return the blinded Id for this onion service descriptor. + pub(super) fn blinded_id(&self) -> BlindedOnionId { + let ident = self + .desc_signing_key_cert + .signing_key() + .expect("signing key was absent!?"); + (*ident).into() + } + + /// Decrypt and return the encrypted (middle-layer) body of this onion + /// service descriptor. + pub(super) fn decrypt_body( + &self, + subcredential: &Subcredential, + ) -> std::result::Result, desc_enc::DecryptionError> { + let decrypt = desc_enc::HsDescEncryption { + blinded_id: &self.blinded_id(), + encryption_cookie: None, + subcredential, + revision: self.revision_counter, + string_const: b"hsdir-superencrypted-data", + }; + + let mut body = decrypt.decrypt(&self.encrypted_body[..])?; + let n_padding = body.iter().rev().take_while(|n| **n == 0).count(); + body.truncate(body.len() - n_padding); + Ok(body) + } +} /// An `HsDescOuter` whose signatures have not yet been verified, and whose /// timeliness has not been checked. @@ -249,6 +279,15 @@ mod test { // TODO HS: Add checks for the specific fields here once I'm more // confident that this is the example descriptor I'm using. + let subcred: tor_hscrypto::Subcredential = + hex_literal::hex!("78210A0D2C72BB7A0CAF606BCD938B9A3696894FDDDBC3B87D424753A7E3DF37") + .into(); + let inner = desc.decrypt_body(&subcred).unwrap(); + + assert!(std::str::from_utf8(&inner) + .unwrap() + .starts_with("desc-auth-type")); + Ok(()) } }