use ed25519_dalek::{Signer, SigningKey};
use sha2::{Digest, Sha256};
use zeroize::Zeroizing;
use crate::crypto::pqc::{self, PqKeypair};
use crate::error::Result;
pub struct IdentityKeys {
signing_key: SigningKey,
fingerprint: String,
}
impl IdentityKeys {
pub fn generate() -> Result<Self> {
let mut rng = rand::thread_rng();
Ok(Self::from_signing_key(SigningKey::generate(&mut rng)))
}
pub fn from_secret_bytes(bytes: [u8; 32]) -> Result<Self> {
Ok(Self::from_signing_key(SigningKey::from_bytes(&bytes)))
}
fn from_signing_key(signing_key: SigningKey) -> Self {
let public = signing_key.verifying_key().to_bytes();
let fingerprint = compute_fingerprint(&public);
Self {
signing_key,
fingerprint,
}
}
pub fn fingerprint(&self) -> &str {
&self.fingerprint
}
pub fn secret_bytes(&self) -> [u8; 32] {
self.signing_key.to_bytes()
}
pub fn public_bytes(&self) -> [u8; 32] {
self.signing_key.verifying_key().to_bytes()
}
pub fn sign(&self, msg: &[u8]) -> [u8; 64] {
self.signing_key.sign(msg).to_bytes()
}
pub fn pq_keypair(&self) -> PqKeypair {
let seed = Zeroizing::new(self.signing_key.to_bytes());
PqKeypair::from_identity_seed(&seed)
}
pub fn mlkem_public_bytes(&self) -> [u8; pqc::MLKEM_EK_LEN] {
self.pq_keypair().encapsulation_key_bytes()
}
pub fn seed(&self) -> Zeroizing<[u8; 32]> {
Zeroizing::new(self.signing_key.to_bytes())
}
pub fn from_seed(seed: Zeroizing<[u8; 32]>) -> Result<Self> {
Ok(Self::from_signing_key(SigningKey::from_bytes(&seed)))
}
}
pub fn compute_fingerprint(public_key: &[u8; 32]) -> String {
let hash = Sha256::digest(public_key);
let hex_str = hex::encode(&hash[..12]);
hex_str
.as_bytes()
.chunks(4)
.map(|chunk| std::str::from_utf8(chunk).unwrap())
.collect::<Vec<&str>>()
.join("-")
}
pub const RELAY_AUTH_DOMAIN: &[u8] = b"huddle-relay-auth-v1";
pub fn relay_auth_msg(nonce: &[u8]) -> Vec<u8> {
let mut m = Vec::with_capacity(RELAY_AUTH_DOMAIN.len() + nonce.len());
m.extend_from_slice(RELAY_AUTH_DOMAIN);
m.extend_from_slice(nonce);
m
}
pub fn safety_code(public_key: &[u8; 32]) -> String {
let hash = Sha256::digest(public_key);
let hex_str = hex::encode(&hash[..6]).to_ascii_uppercase();
let groups: Vec<&str> = hex_str
.as_bytes()
.chunks(4)
.map(|chunk| std::str::from_utf8(chunk).unwrap())
.collect();
format!("SAFE-{}", groups.join("-"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fingerprint_is_deterministic_and_well_formed() {
let id = IdentityKeys::from_secret_bytes([42u8; 32]).unwrap();
let id2 = IdentityKeys::from_secret_bytes([42u8; 32]).unwrap();
assert_eq!(id.fingerprint(), id2.fingerprint());
let parts: Vec<&str> = id.fingerprint().split('-').collect();
assert_eq!(parts.len(), 6);
for p in &parts {
assert_eq!(p.len(), 4);
assert!(p.chars().all(|c| c.is_ascii_hexdigit()));
}
}
#[test]
fn mlkem_pubkey_is_stable_and_per_identity() {
let bytes = IdentityKeys::generate().unwrap().secret_bytes();
let a = IdentityKeys::from_secret_bytes(bytes).unwrap();
let b = IdentityKeys::from_secret_bytes(bytes).unwrap();
assert_eq!(a.mlkem_public_bytes(), b.mlkem_public_bytes());
assert_eq!(a.mlkem_public_bytes().len(), pqc::MLKEM_EK_LEN);
let other = IdentityKeys::generate().unwrap();
assert_ne!(a.mlkem_public_bytes(), other.mlkem_public_bytes());
}
#[test]
fn seed_round_trips_keys() {
let id = IdentityKeys::generate().unwrap();
assert_eq!(*id.seed(), id.secret_bytes());
let restored = IdentityKeys::from_seed(id.seed()).unwrap();
assert_eq!(id.fingerprint(), restored.fingerprint());
assert_eq!(id.mlkem_public_bytes(), restored.mlkem_public_bytes());
}
#[test]
fn safety_code_is_stable_and_well_formed() {
let a = safety_code(&[7u8; 32]);
assert_eq!(a, safety_code(&[7u8; 32]));
assert!(a.starts_with("SAFE-"));
let groups: Vec<&str> = a.trim_start_matches("SAFE-").split('-').collect();
assert_eq!(groups.len(), 3);
for g in &groups {
assert_eq!(g.len(), 4);
}
}
#[test]
fn relay_auth_msg_is_domain_prefixed() {
let m = relay_auth_msg(&[9u8; 32]);
assert!(m.starts_with(RELAY_AUTH_DOMAIN));
assert_eq!(m.len(), RELAY_AUTH_DOMAIN.len() + 32);
}
}