1use ed25519_dalek::VerifyingKey;
27use hkdf::Hkdf;
28use sha2::{Digest, Sha256, Sha512};
29use x25519_dalek::{PublicKey, StaticSecret};
30
31use crate::crypto::passphrase::KEY_LEN;
32use crate::error::{HuddleError, Result};
33
34pub fn derive_dm_key(
40 our_ed25519_seed: &[u8; 32],
41 partner_ed25519_pubkey: &[u8; 32],
42 canonical_room_id: &str,
43) -> Result<[u8; KEY_LEN]> {
44 let our_x = ed25519_seed_to_x25519_secret(our_ed25519_seed);
45 let partner_x = ed25519_pubkey_to_x25519(partner_ed25519_pubkey)?;
46 let shared = our_x.diffie_hellman(&partner_x);
47 let salt = b"huddle-dm-key-v1\0";
52 let h = Hkdf::<Sha256>::new(Some(salt), shared.as_bytes());
53 let mut out = [0u8; KEY_LEN];
54 h.expand(canonical_room_id.as_bytes(), &mut out)
55 .map_err(|e| HuddleError::Session(format!("hkdf expand: {e}")))?;
56 Ok(out)
57}
58
59fn ed25519_seed_to_x25519_secret(seed: &[u8; 32]) -> StaticSecret {
60 let h = Sha512::digest(seed);
65 let mut bytes = [0u8; 32];
66 bytes.copy_from_slice(&h[..32]);
67 StaticSecret::from(bytes)
68}
69
70fn ed25519_pubkey_to_x25519(pubkey_bytes: &[u8; 32]) -> Result<PublicKey> {
71 let vk = VerifyingKey::from_bytes(pubkey_bytes)
72 .map_err(|e| HuddleError::Session(format!("bad ed25519 pubkey: {e}")))?;
73 Ok(PublicKey::from(vk.to_montgomery().to_bytes()))
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::identity::Identity;
80
81 #[test]
82 fn dm_key_is_commutative() {
83 let alice = Identity::generate().unwrap();
84 let bob = Identity::generate().unwrap();
85 let room_id = "deadbeefcafef00d1234567890abcdef";
86 let k_a = derive_dm_key(&alice.secret_bytes(), &bob.public_bytes(), room_id).unwrap();
87 let k_b = derive_dm_key(&bob.secret_bytes(), &alice.public_bytes(), room_id).unwrap();
88 assert_eq!(k_a, k_b, "both peers must derive the same DM key");
89 }
90
91 #[test]
92 fn dm_key_is_deterministic() {
93 let alice = Identity::generate().unwrap();
94 let bob = Identity::generate().unwrap();
95 let room_id = "room-1";
96 let k1 = derive_dm_key(&alice.secret_bytes(), &bob.public_bytes(), room_id).unwrap();
97 let k2 = derive_dm_key(&alice.secret_bytes(), &bob.public_bytes(), room_id).unwrap();
98 assert_eq!(k1, k2);
99 }
100
101 #[test]
102 fn dm_key_binds_to_room_id() {
103 let alice = Identity::generate().unwrap();
104 let bob = Identity::generate().unwrap();
105 let k1 = derive_dm_key(&alice.secret_bytes(), &bob.public_bytes(), "room-1").unwrap();
106 let k2 = derive_dm_key(&alice.secret_bytes(), &bob.public_bytes(), "room-2").unwrap();
107 assert_ne!(
108 k1, k2,
109 "different room_ids must produce different keys (HKDF info parameter)"
110 );
111 }
112
113 #[test]
114 fn dm_key_differs_per_pair() {
115 let alice = Identity::generate().unwrap();
116 let bob = Identity::generate().unwrap();
117 let carol = Identity::generate().unwrap();
118 let room = "room";
119 let k_ab = derive_dm_key(&alice.secret_bytes(), &bob.public_bytes(), room).unwrap();
120 let k_ac = derive_dm_key(&alice.secret_bytes(), &carol.public_bytes(), room).unwrap();
121 assert_ne!(k_ab, k_ac);
122 }
123
124 #[test]
125 fn rejects_invalid_ed25519_pubkey() {
126 let alice = Identity::generate().unwrap();
127 let mut bad = [0u8; 32];
129 bad[31] = 0xff;
130 let r = derive_dm_key(&alice.secret_bytes(), &bad, "room");
131 let _ = r; }
136}