use hkdf::Hkdf;
use sha2::Sha256;
use x25519_dalek::{EphemeralSecret, PublicKey};
use crate::keys::CryptoError;
const PROMOTE_INFO: &[u8] = b"koi-promote-v1";
pub struct EphemeralKeyPair {
secret: EphemeralSecret,
public: PublicKey,
}
impl EphemeralKeyPair {
pub fn generate() -> Self {
let secret = EphemeralSecret::random_from_rng(p256::elliptic_curve::rand_core::OsRng);
let public = PublicKey::from(&secret);
Self { secret, public }
}
pub fn public_key_bytes(&self) -> [u8; 32] {
self.public.to_bytes()
}
pub fn derive_shared_key(self, their_public: &[u8; 32]) -> Result<[u8; 32], CryptoError> {
let their_key = PublicKey::from(*their_public);
let shared_secret = self.secret.diffie_hellman(&their_key);
let hk = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
let mut okm = [0u8; 32];
hk.expand(PROMOTE_INFO, &mut okm)
.map_err(|_| CryptoError::KeyDerivation("HKDF-SHA256 expand failed".into()))?;
Ok(okm)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dh_round_trip_derives_same_shared_key() {
let alice = EphemeralKeyPair::generate();
let bob = EphemeralKeyPair::generate();
let alice_pub = alice.public_key_bytes();
let bob_pub = bob.public_key_bytes();
let alice_shared = alice.derive_shared_key(&bob_pub).unwrap();
let bob_shared = bob.derive_shared_key(&alice_pub).unwrap();
assert_eq!(alice_shared, bob_shared);
assert_ne!(alice_shared, [0u8; 32]);
}
#[test]
fn different_peers_produce_different_keys() {
let alice = EphemeralKeyPair::generate();
let bob = EphemeralKeyPair::generate();
let charlie = EphemeralKeyPair::generate();
let bob_pub = bob.public_key_bytes();
let charlie_pub = charlie.public_key_bytes();
let alice_bob = alice.derive_shared_key(&bob_pub).unwrap();
let charlie_bob = charlie.derive_shared_key(&bob_pub).unwrap();
let _ = charlie_pub; assert_ne!(alice_bob, charlie_bob);
}
#[test]
fn public_key_bytes_are_32_bytes() {
let kp = EphemeralKeyPair::generate();
let pk = kp.public_key_bytes();
assert_eq!(pk.len(), 32);
}
}