use ed25519_dalek::VerifyingKey;
use hkdf::Hkdf;
use sha2::{Digest, Sha256, Sha512};
use x25519_dalek::{PublicKey, StaticSecret};
use crate::crypto::passphrase::KEY_LEN;
use crate::error::{HuddleError, Result};
pub fn derive_dm_key(
our_ed25519_seed: &[u8; 32],
partner_ed25519_pubkey: &[u8; 32],
canonical_room_id: &str,
) -> Result<[u8; KEY_LEN]> {
let our_x = ed25519_seed_to_x25519_secret(our_ed25519_seed);
let partner_x = ed25519_pubkey_to_x25519(partner_ed25519_pubkey)?;
let shared = our_x.diffie_hellman(&partner_x);
let salt = b"huddle-dm-key-v1\0";
let h = Hkdf::<Sha256>::new(Some(salt), shared.as_bytes());
let mut out = [0u8; KEY_LEN];
h.expand(canonical_room_id.as_bytes(), &mut out)
.map_err(|e| HuddleError::Session(format!("hkdf expand: {e}")))?;
Ok(out)
}
fn ed25519_seed_to_x25519_secret(seed: &[u8; 32]) -> StaticSecret {
let h = Sha512::digest(seed);
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&h[..32]);
StaticSecret::from(bytes)
}
fn ed25519_pubkey_to_x25519(pubkey_bytes: &[u8; 32]) -> Result<PublicKey> {
let vk = VerifyingKey::from_bytes(pubkey_bytes)
.map_err(|e| HuddleError::Session(format!("bad ed25519 pubkey: {e}")))?;
Ok(PublicKey::from(vk.to_montgomery().to_bytes()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::Identity;
#[test]
fn dm_key_is_commutative() {
let alice = Identity::generate().unwrap();
let bob = Identity::generate().unwrap();
let room_id = "deadbeefcafef00d1234567890abcdef";
let k_a = derive_dm_key(&alice.secret_bytes(), &bob.public_bytes(), room_id).unwrap();
let k_b = derive_dm_key(&bob.secret_bytes(), &alice.public_bytes(), room_id).unwrap();
assert_eq!(k_a, k_b, "both peers must derive the same DM key");
}
#[test]
fn dm_key_is_deterministic() {
let alice = Identity::generate().unwrap();
let bob = Identity::generate().unwrap();
let room_id = "room-1";
let k1 = derive_dm_key(&alice.secret_bytes(), &bob.public_bytes(), room_id).unwrap();
let k2 = derive_dm_key(&alice.secret_bytes(), &bob.public_bytes(), room_id).unwrap();
assert_eq!(k1, k2);
}
#[test]
fn dm_key_binds_to_room_id() {
let alice = Identity::generate().unwrap();
let bob = Identity::generate().unwrap();
let k1 = derive_dm_key(&alice.secret_bytes(), &bob.public_bytes(), "room-1").unwrap();
let k2 = derive_dm_key(&alice.secret_bytes(), &bob.public_bytes(), "room-2").unwrap();
assert_ne!(
k1, k2,
"different room_ids must produce different keys (HKDF info parameter)"
);
}
#[test]
fn dm_key_differs_per_pair() {
let alice = Identity::generate().unwrap();
let bob = Identity::generate().unwrap();
let carol = Identity::generate().unwrap();
let room = "room";
let k_ab = derive_dm_key(&alice.secret_bytes(), &bob.public_bytes(), room).unwrap();
let k_ac = derive_dm_key(&alice.secret_bytes(), &carol.public_bytes(), room).unwrap();
assert_ne!(k_ab, k_ac);
}
#[test]
fn rejects_invalid_ed25519_pubkey() {
let alice = Identity::generate().unwrap();
let mut bad = [0u8; 32];
bad[31] = 0xff;
let r = derive_dm_key(&alice.secret_bytes(), &bad, "room");
let _ = r; }
}