From 2f8dce44d3f2be74b5c6ec0a2e7f4ceced715328 Mon Sep 17 00:00:00 2001 From: pennae Date: Wed, 13 Jul 2022 10:33:30 +0200 Subject: initial import --- src/crypto.rs | 408 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 src/crypto.rs (limited to 'src/crypto.rs') diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..cf1044e --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,408 @@ +#![deny(clippy::pedantic)] +#![deny(clippy::restriction)] +#![allow(clippy::blanket_clippy_restriction_lints)] +#![allow(clippy::implicit_return)] +#![allow(clippy::missing_docs_in_private_items)] +#![allow(clippy::shadow_reuse)] + +use std::fmt::Debug; + +use hmac::{Hmac, Mac}; +use password_hash::{Output, Salt}; +use rand::RngCore; +use scrypt::scrypt; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; + +const NAMESPACE: &[u8] = b"identity.mozilla.com/picl/v1/"; + +#[derive(Clone, PartialEq, Eq, Zeroize, Serialize, Deserialize)] +#[serde(try_from = "String", into = "String")] +pub struct SecretBytes(pub [u8; N]); + +impl Drop for SecretBytes { + fn drop(&mut self) { + self.zeroize(); + } +} + +#[derive(Clone, PartialEq, Eq)] +pub struct TokenID(pub [u8; 32]); + +impl Debug for SecretBytes { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + fmt.write_fmt(format_args!("SecretBytes {{ raw: {} }}", hex::encode(&self.0))) + } +} + +impl SecretBytes { + fn xor(&self, other: &Self) -> Self { + let mut result = self.clone(); + for (a, b) in result.0.iter_mut().zip(other.0.iter()) { + *a ^= b; + } + result + } +} + +impl From> for String { + fn from(sb: SecretBytes) -> Self { + hex::encode(&sb.0) + } +} + +impl TryFrom for SecretBytes { + type Error = hex::FromHexError; + + fn try_from(value: String) -> Result { + let mut result = Self([0; N]); + hex::decode_to_slice(value, &mut result.0)?; + Ok(result) + } +} + +impl From> for Output { + fn from(s: SecretBytes<32>) -> Output { + #[allow(clippy::unwrap_used)] + Output::new(&s.0).unwrap() + } +} + +mod from_hkdf { + use hkdf::Hkdf; + use sha2::Sha256; + + // sealing lets us guarantee that SIZE is always correct, + // which means that from_hkdf always receives correctly sized slices + // and copies never fail + mod private { + pub trait Seal {} + impl Seal for super::super::SecretBytes {} + impl Seal for super::super::TokenID {} + impl Seal for (L, R) {} + } + + pub trait FromHkdf: private::Seal { + const SIZE: usize; + fn from_hkdf(bytes: &[u8]) -> Self; + } + + impl FromHkdf for super::SecretBytes { + const SIZE: usize = N; + fn from_hkdf(bytes: &[u8]) -> Self { + #[allow(clippy::unwrap_used)] + Self(bytes.try_into().unwrap()) + } + } + + impl FromHkdf for super::TokenID { + const SIZE: usize = 32; + fn from_hkdf(bytes: &[u8]) -> Self { + #[allow(clippy::expect_used)] + Self(bytes.try_into().expect("hkdf failed")) + } + } + + impl FromHkdf for (L, R) { + const SIZE: usize = L::SIZE + R::SIZE; + #[allow(clippy::indexing_slicing)] + fn from_hkdf(bytes: &[u8]) -> Self { + (L::from_hkdf(&bytes[0..L::SIZE]), R::from_hkdf(&bytes[L::SIZE..])) + } + } + + pub fn from_hkdf(key: &[u8], info: &[&[u8]]) -> O { + let hk = Hkdf::::new(None, key); + let mut buf = vec![0; O::SIZE]; + #[allow(clippy::expect_used)] + // worth keeping an eye out for very large results (>255*32 bytes) + hk.expand_multi_info(info, buf.as_mut_slice()).expect("hkdf failed"); + O::from_hkdf(&buf) + } +} + +use from_hkdf::from_hkdf; +use zeroize::Zeroize; + +impl SecretBytes { + pub fn generate() -> Self { + let mut result = Self([0; N]); + rand::rngs::OsRng.fill_bytes(&mut result.0); + result + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(transparent)] +pub struct AuthPW { + pub pw: SecretBytes<32>, +} + +pub struct StretchedPW { + pub pw: Output, +} + +impl AuthPW { + pub fn stretch(&self, salt: Salt) -> anyhow::Result { + let mut result = [0; 32]; + let mut buf = [0; Salt::MAX_LENGTH]; + let params = scrypt::Params::new(16, 8, 1)?; + let salt = salt.b64_decode(&mut buf)?; + scrypt(&self.pw.0, salt, ¶ms, &mut result)?; + Ok(StretchedPW { pw: Output::new(&result)? }) + } +} + +impl StretchedPW { + pub fn verify_hash(&self) -> Output { + let raw: SecretBytes<32> = from_hkdf(self.pw.as_bytes(), &[NAMESPACE, b"verifyHash"]); + raw.into() + } + + fn wrap_wrap_key(&self) -> SecretBytes<32> { + from_hkdf(self.pw.as_bytes(), &[NAMESPACE, b"wrapwrapKey"]) + } + + pub fn decrypt_wwkb(&self, wwkb: &SecretBytes<32>) -> SecretBytes<32> { + wwkb.xor(&self.wrap_wrap_key()) + } + + pub fn rewrap_wkb(&self, wkb: &SecretBytes<32>) -> SecretBytes<32> { + wkb.xor(&self.wrap_wrap_key()) + } +} + +pub struct SessionCredentials { + pub token_id: TokenID, + pub req_hmac_key: SecretBytes<32>, +} + +impl SessionCredentials { + pub fn derive(seed: &SecretBytes<32>) -> Self { + let (token_id, req_hmac_key) = from_hkdf(&seed.0, &[NAMESPACE, b"sessionToken"]); + Self { token_id, req_hmac_key } + } +} + +pub struct KeyFetchReq { + pub token_id: TokenID, + pub req_hmac_key: SecretBytes<32>, + pub key_request_key: SecretBytes<32>, +} + +impl KeyFetchReq { + pub fn from_token(key_fetch_token: &SecretBytes<32>) -> Self { + let (token_id, (req_hmac_key, key_request_key)) = + from_hkdf(&key_fetch_token.0, &[NAMESPACE, b"keyFetchToken"]); + Self { token_id, req_hmac_key, key_request_key } + } + + pub fn derive_resp(&self) -> KeyFetchResp { + let (resp_hmac_key, resp_xor_key) = + from_hkdf(&self.key_request_key.0, &[NAMESPACE, b"account/keys"]); + KeyFetchResp { resp_hmac_key, resp_xor_key } + } +} + +pub struct KeyFetchResp { + pub resp_hmac_key: SecretBytes<32>, + pub resp_xor_key: SecretBytes<64>, +} + +impl KeyFetchResp { + pub fn wrap_keys(&self, keys: &KeyBundle) -> WrappedKeyBundle { + let ciphertext = self.resp_xor_key.xor(&keys.to_bytes()); + #[allow(clippy::unwrap_used)] + let mut hmac = Hmac::::new_from_slice(&self.resp_hmac_key.0).unwrap(); + hmac.update(&ciphertext.0); + let hmac = *hmac.finalize().into_bytes().as_ref(); + WrappedKeyBundle { ciphertext, hmac } + } +} + +pub struct KeyBundle { + pub ka: SecretBytes<32>, + pub wrap_kb: SecretBytes<32>, +} + +impl KeyBundle { + pub fn to_bytes(&self) -> SecretBytes<64> { + let mut result = SecretBytes([0; 64]); + result.0[0..32].copy_from_slice(&self.ka.0); + result.0[32..].copy_from_slice(&self.wrap_kb.0); + result + } +} + +#[derive(Debug)] +pub struct WrappedKeyBundle { + pub ciphertext: SecretBytes<64>, + pub hmac: [u8; 32], +} + +impl WrappedKeyBundle { + pub fn to_bytes(&self) -> [u8; 96] { + let mut result = [0; 96]; + result[0..64].copy_from_slice(&self.ciphertext.0); + result[64..].copy_from_slice(&self.hmac); + result + } +} + +pub struct PasswordChangeReq { + pub token_id: TokenID, + pub req_hmac_key: SecretBytes<32>, +} + +impl PasswordChangeReq { + pub fn from_change_token(token: &SecretBytes<32>) -> Self { + let (token_id, req_hmac_key) = from_hkdf(&token.0, &[NAMESPACE, b"passwordChangeToken"]); + Self { token_id, req_hmac_key } + } + + pub fn from_forgot_token(token: &SecretBytes<32>) -> Self { + let (token_id, req_hmac_key) = from_hkdf(&token.0, &[NAMESPACE, b"passwordForgotToken"]); + Self { token_id, req_hmac_key } + } +} + +pub struct AccountResetReq { + pub token_id: TokenID, + pub req_hmac_key: SecretBytes<32>, +} + +impl AccountResetReq { + pub fn from_token(token: &SecretBytes<32>) -> Self { + let (token_id, req_hmac_key) = from_hkdf(&token.0, &[NAMESPACE, b"accountResetToken"]); + Self { token_id, req_hmac_key } + } +} + +#[cfg(test)] +mod test { + use hex_literal::hex; + use password_hash::{Output, SaltString}; + + use crate::crypto::{KeyBundle, KeyFetchReq, SessionCredentials}; + + use super::{AuthPW, SecretBytes}; + + macro_rules! shex { + ( $s: literal ) => { + SecretBytes(hex!($s)) + }; + } + + #[test] + fn test_derive_session() { + let creds = SessionCredentials::derive(&SecretBytes(hex!( + "a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf" + ))); + assert_eq!( + creds.token_id.0, + hex!("c0a29dcf46174973da1378696e4c82ae10f723cf4f4d9f75e39f4ae3851595ab") + ); + assert_eq!( + creds.req_hmac_key.0, + hex!("9d8f22998ee7f579 8b887042466b72d5 3e56ab0c094388bf 65831f702d2febc0") + ); + } + + #[test] + fn test_key_fetch() { + let key_fetch = KeyFetchReq::from_token(&shex!( + "8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f" + )); + assert_eq!( + key_fetch.token_id.0, + hex!("3d0a7c02a15a62a2882f76e39b6494b500c022a8816e048625a495718998ba60") + ); + assert_eq!( + key_fetch.req_hmac_key.0, + hex!("87b8937f61d38d0e 29cd2d5600b3f4da 0aa48ac41de36a0e fe84bb4a9872ceb7") + ); + assert_eq!( + key_fetch.key_request_key.0, + hex!("14f338a9e8c6324d 9e102d4e6ee83b20 9796d5c74bb734a4 10e729e014a4a546") + ); + + let resp = key_fetch.derive_resp(); + assert_eq!( + resp.resp_hmac_key.0, + hex!("f824d2953aab9faf 51a1cb65ba9e7f9e 5bf91c8d8fd1ac1c 8c2d31853a8a1210") + ); + assert_eq!( + resp.resp_xor_key.0, + hex!( + "ce7d7aa77859b235 9932970bbe2101f2 e80d01faf9191bd5 ee52181d2f0b7809 + 8281ba8cff392543 3a89f7c3095e0c89 900a469d60790c83 3281c4df1a11c763" + ) + ); + + let bundle = KeyBundle { + ka: shex!("2021222324252627 28292a2b2c2d2e2f 3031323334353637 38393a3b3c3d3e3f"), + wrap_kb: shex!("7effe354abecbcb2 34a8dfc2d7644b4a d339b525589738f2 d27341bb8622ecd8"), + }; + assert_eq!( + bundle.to_bytes().0, + hex!( + "2021222324252627 28292a2b2c2d2e2f 3031323334353637 38393a3b3c3d3e3f + 7effe354abecbcb2 34a8dfc2d7644b4a d339b525589738f2 d27341bb8622ecd8" + ) + ); + + let wrapped = resp.wrap_keys(&bundle); + assert_eq!( + wrapped.ciphertext.0, + hex!( + "ee5c58845c7c9412 b11bbd20920c2fdd d83c33c9cd2c2de2 d66b222613364636 + fc7e59d854d599f1 0e212801de3a47c3 4333f3b838ee3471 e0f285649c332bbb" + ) + ); + assert_eq!( + wrapped.hmac, + hex!("4c17f42a0b319bbb a327d2b326ad23e9 37219b4de32e3ec7 b3e3f740522ad6ef") + ); + assert_eq!( + wrapped.to_bytes(), + hex!( + "ee5c58845c7c9412 b11bbd20920c2fdd d83c33c9cd2c2de2 d66b222613364636 + fc7e59d854d599f1 0e212801de3a47c3 4333f3b838ee3471 e0f285649c332bbb + 4c17f42a0b319bbb a327d2b326ad23e9 37219b4de32e3ec7 b3e3f740522ad6ef" + ) + ); + } + + #[test] + fn test_stretch() -> anyhow::Result<()> { + let auth_pw = AuthPW { + pw: SecretBytes(hex!( + "247b675ffb4c4631 0bc87e26d712153a be5e1c90ef00a478 4594f97ef54f2375" + )), + }; + + let stretched = auth_pw.stretch( + SaltString::b64_encode(&hex!( + "00f0000000000000 0000000000000000 0000000000000000 0000000000000000" + ))? + .as_salt(), + )?; + assert_eq!( + stretched.pw, + Output::new(&hex!( + "441509e25c92ee10 3d5a1a874e6f155d f25a44d06e61c894 616c9e85181dba97" + ))? + ); + + assert_eq!( + stretched.verify_hash().as_bytes(), + hex!("a4765bf103dc057f 4cf4bc2c131ddb67 16e8a4333cc55e1d 3c449f31f0eec4f1") + ); + + assert_eq!( + stretched.wrap_wrap_key().0, + hex!("3ebea117efa9faf5 7ce195899b290505 8368e7760cc26ea5 8a2a1be0da7fb287") + ); + Ok(()) + } +} -- cgit v1.2.3