huddle_protocol/crypto/
code_join.rs1use hkdf::Hkdf;
12use sha2::Sha256;
13use x25519_dalek::{PublicKey, StaticSecret};
14
15use crate::crypto::passphrase::KEY_LEN;
16use crate::error::{ProtocolError, Result};
17
18const CODE_JOIN_INFO: &[u8] = b"huddle-code-join-v1";
21
22pub fn derive_wrap_key(our_secret: &StaticSecret, their_pub: &PublicKey) -> Result<[u8; KEY_LEN]> {
26 let shared = our_secret.diffie_hellman(their_pub);
27 if !shared.was_contributory() {
32 return Err(ProtocolError::Session(
33 "code-join key agreement rejected: peer X25519 pubkey is non-contributory \
34 (small-order point)"
35 .into(),
36 ));
37 }
38 let hk = Hkdf::<Sha256>::new(None, shared.as_bytes());
39 let mut wrap_key = [0u8; KEY_LEN];
40 hk.expand(CODE_JOIN_INFO, &mut wrap_key)
41 .expect("32 bytes is within HKDF-SHA256's output limit");
42 Ok(wrap_key)
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use rand::rngs::OsRng;
49
50 #[test]
51 fn both_sides_derive_the_same_wrap_key() {
52 let owner = StaticSecret::random_from_rng(OsRng);
53 let joiner = StaticSecret::random_from_rng(OsRng);
54 let owner_pub = PublicKey::from(&owner);
55 let joiner_pub = PublicKey::from(&joiner);
56 let k_owner = derive_wrap_key(&owner, &joiner_pub).unwrap();
58 let k_joiner = derive_wrap_key(&joiner, &owner_pub).unwrap();
59 assert_eq!(k_owner, k_joiner, "ECDH is commutative -> same wrap key");
60 }
61
62 #[test]
63 fn different_peers_derive_different_keys() {
64 let owner = StaticSecret::random_from_rng(OsRng);
65 let a = PublicKey::from(&StaticSecret::random_from_rng(OsRng));
66 let b = PublicKey::from(&StaticSecret::random_from_rng(OsRng));
67 assert_ne!(
68 derive_wrap_key(&owner, &a).unwrap(),
69 derive_wrap_key(&owner, &b).unwrap()
70 );
71 }
72
73 #[test]
74 fn rejects_small_order_peer_pubkey() {
75 let owner = StaticSecret::random_from_rng(OsRng);
78 let small_order = PublicKey::from([0u8; 32]);
80 assert!(derive_wrap_key(&owner, &small_order).is_err());
81 }
82}