huddle_protocol/crypto/
passphrase.rs1use argon2::{Algorithm, Argon2, Params, Version};
14use base64::Engine;
15use chacha20poly1305::aead::{Aead, AeadCore, KeyInit, OsRng};
16use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
17use rand::RngCore;
18use zeroize::Zeroizing;
19
20use crate::error::{ProtocolError, Result};
21
22pub const SALT_LEN: usize = 16;
23pub const KEY_LEN: usize = 32;
24pub const NONCE_LEN: usize = 12;
25
26pub fn random_salt() -> [u8; SALT_LEN] {
28 let mut salt = [0u8; SALT_LEN];
29 OsRng.fill_bytes(&mut salt);
30 salt
31}
32
33pub fn argon2id_params(out_len: usize) -> Result<Params> {
41 Params::new(65_536, 3, 4, Some(out_len))
42 .map_err(|e| ProtocolError::Session(format!("argon2 params: {e}")))
43}
44
45pub fn derive_key(passphrase: &str, salt: &[u8]) -> Result<[u8; KEY_LEN]> {
50 let zeroizing = derive_key_zeroizing(passphrase, salt)?;
51 Ok(*zeroizing)
55}
56
57pub fn derive_key_zeroizing(passphrase: &str, salt: &[u8]) -> Result<Zeroizing<[u8; KEY_LEN]>> {
61 let params = argon2id_params(KEY_LEN)?;
62 let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
63 let mut out = Zeroizing::new([0u8; KEY_LEN]);
64 argon
65 .hash_password_into(passphrase.as_bytes(), salt, out.as_mut_slice())
66 .map_err(|e| ProtocolError::Session(format!("argon2 derive: {e}")))?;
67 Ok(out)
68}
69
70pub fn wrap(plaintext: &[u8], passphrase_key: &[u8; KEY_LEN]) -> Result<String> {
73 let cipher = ChaCha20Poly1305::new(Key::from_slice(passphrase_key));
74 let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
75 let ciphertext = cipher
76 .encrypt(&nonce, plaintext)
77 .map_err(|e| ProtocolError::Session(format!("wrap failed: {e}")))?;
78 let mut combined = Vec::with_capacity(NONCE_LEN + ciphertext.len());
79 combined.extend_from_slice(&nonce);
80 combined.extend_from_slice(&ciphertext);
81 Ok(base64::engine::general_purpose::STANDARD.encode(&combined))
82}
83
84pub fn unwrap(encoded: &str, passphrase_key: &[u8; KEY_LEN]) -> Result<Vec<u8>> {
86 let bytes = base64::engine::general_purpose::STANDARD
87 .decode(encoded)
88 .map_err(|e| ProtocolError::Session(format!("bad base64: {e}")))?;
89 if bytes.len() < NONCE_LEN + 16 {
90 return Err(ProtocolError::Session("wrapped key too short".into()));
91 }
92 let (nonce_bytes, ciphertext) = bytes.split_at(NONCE_LEN);
93 let cipher = ChaCha20Poly1305::new(Key::from_slice(passphrase_key));
94 let nonce = Nonce::from_slice(nonce_bytes);
95 cipher
96 .decrypt(nonce, ciphertext)
97 .map_err(|e| ProtocolError::Session(format!("unwrap failed (wrong passphrase?): {e}")))
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn derive_is_deterministic() {
106 let salt = [42u8; SALT_LEN];
107 let k1 = derive_key("hunter2", &salt).unwrap();
108 let k2 = derive_key("hunter2", &salt).unwrap();
109 assert_eq!(k1, k2);
110 }
111
112 #[test]
113 fn different_passphrases_different_keys() {
114 let salt = [42u8; SALT_LEN];
115 let k1 = derive_key("hunter2", &salt).unwrap();
116 let k2 = derive_key("hunter3", &salt).unwrap();
117 assert_ne!(k1, k2);
118 }
119
120 #[test]
121 fn different_salts_different_keys() {
122 let k1 = derive_key("same", &[1u8; SALT_LEN]).unwrap();
123 let k2 = derive_key("same", &[2u8; SALT_LEN]).unwrap();
124 assert_ne!(k1, k2);
125 }
126
127 #[test]
128 fn wrap_unwrap_round_trip() {
129 let salt = random_salt();
130 let key = derive_key("hunter2", &salt).unwrap();
131 let secret = b"this is a megolm session key";
132 let wrapped = wrap(secret, &key).unwrap();
133 let recovered = unwrap(&wrapped, &key).unwrap();
134 assert_eq!(recovered, secret);
135 }
136
137 #[test]
138 fn wrong_passphrase_fails_unwrap() {
139 let salt = random_salt();
140 let right_key = derive_key("hunter2", &salt).unwrap();
141 let wrong_key = derive_key("hunter3", &salt).unwrap();
142 let wrapped = wrap(b"secret", &right_key).unwrap();
143 assert!(unwrap(&wrapped, &wrong_key).is_err());
144 }
145
146 #[test]
147 fn wrapped_output_is_nondeterministic() {
148 let salt = random_salt();
149 let key = derive_key("hunter2", &salt).unwrap();
150 let w1 = wrap(b"hello", &key).unwrap();
151 let w2 = wrap(b"hello", &key).unwrap();
152 assert_ne!(w1, w2, "nonce should differ each time");
153 }
154}