use ed25519_dalek::{SigningKey, VerifyingKey}; use sha2::{Digest, Sha256}; use x25519_dalek::StaticSecret; use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::crypto::hkdf_derive; use crate::errors::ProtocolError; use crate::types::Fingerprint; /// The root secret — 32 bytes from which all keys are derived. /// Displayed to users as a BIP39 mnemonic (24 words). #[derive(Zeroize, ZeroizeOnDrop)] pub struct Seed(pub [u8; 32]); impl Seed { /// Generate a new random seed. pub fn generate() -> Self { let mut bytes = [0u8; 32]; rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut bytes); Seed(bytes) } /// Create seed from raw bytes. pub fn from_bytes(bytes: [u8; 32]) -> Self { Seed(bytes) } /// Derive the full identity keypair from this seed. pub fn derive_identity(&self) -> IdentityKeyPair { // Ed25519 signing key: HKDF(seed, info="warzone-ed25519") let ed_bytes = hkdf_derive(&self.0, b"", b"warzone-ed25519", 32); let mut ed_seed = [0u8; 32]; ed_seed.copy_from_slice(&ed_bytes); let signing = SigningKey::from_bytes(&ed_seed); ed_seed.zeroize(); // X25519 encryption key: HKDF(seed, info="warzone-x25519") let x_bytes = hkdf_derive(&self.0, b"", b"warzone-x25519", 32); let mut x_seed = [0u8; 32]; x_seed.copy_from_slice(&x_bytes); let encryption = StaticSecret::from(x_seed); x_seed.zeroize(); IdentityKeyPair { signing, encryption, } } /// Convert to BIP39 mnemonic words. pub fn to_mnemonic(&self) -> String { crate::mnemonic::seed_to_mnemonic(&self.0) } /// Recover seed from BIP39 mnemonic words. pub fn from_mnemonic(words: &str) -> Result { let bytes = crate::mnemonic::mnemonic_to_seed(words)?; Ok(Seed(bytes)) } } /// The full identity keypair derived from a seed. pub struct IdentityKeyPair { pub signing: SigningKey, pub encryption: StaticSecret, } impl IdentityKeyPair { /// Get the public identity (safe to share). pub fn public_identity(&self) -> PublicIdentity { let verifying = self.signing.verifying_key(); let encryption_pub = x25519_dalek::PublicKey::from(&self.encryption); let fingerprint = PublicIdentity::compute_fingerprint(&verifying); PublicIdentity { signing: verifying, encryption: encryption_pub, fingerprint, } } } /// The public portion of an identity — safe to share with anyone. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct PublicIdentity { #[serde(with = "verifying_key_serde")] pub signing: VerifyingKey, #[serde(with = "public_key_serde")] pub encryption: x25519_dalek::PublicKey, pub fingerprint: Fingerprint, } impl PublicIdentity { fn compute_fingerprint(key: &VerifyingKey) -> Fingerprint { let hash = Sha256::digest(key.as_bytes()); let mut fp = [0u8; 16]; fp.copy_from_slice(&hash[..16]); Fingerprint(fp) } } // Serde helpers for dalek types (serialize as bytes) mod verifying_key_serde { use ed25519_dalek::VerifyingKey; use serde::{self, Deserialize, Deserializer, Serializer}; pub fn serialize(key: &VerifyingKey, serializer: S) -> Result where S: Serializer, { serializer.serialize_bytes(key.as_bytes()) } pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let bytes: Vec = Deserialize::deserialize(deserializer)?; let arr: [u8; 32] = bytes .try_into() .map_err(|_| serde::de::Error::custom("invalid key length"))?; VerifyingKey::from_bytes(&arr).map_err(serde::de::Error::custom) } } mod public_key_serde { use serde::{self, Deserialize, Deserializer, Serializer}; use x25519_dalek::PublicKey; pub fn serialize(key: &PublicKey, serializer: S) -> Result where S: Serializer, { serializer.serialize_bytes(key.as_bytes()) } pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let bytes: Vec = Deserialize::deserialize(deserializer)?; let arr: [u8; 32] = bytes .try_into() .map_err(|_| serde::de::Error::custom("invalid key length"))?; Ok(PublicKey::from(arr)) } } #[cfg(test)] mod tests { use super::*; #[test] fn deterministic_derivation() { let seed = Seed::from_bytes([42u8; 32]); let id1 = seed.derive_identity(); let id2 = seed.derive_identity(); assert_eq!( id1.signing.verifying_key().as_bytes(), id2.signing.verifying_key().as_bytes(), ); } #[test] fn mnemonic_roundtrip() { let seed = Seed::generate(); let words = seed.to_mnemonic(); let recovered = Seed::from_mnemonic(&words).unwrap(); assert_eq!(seed.0, recovered.0); } #[test] fn fingerprint_display() { let seed = Seed::generate(); let id = seed.derive_identity(); let pub_id = id.public_identity(); let fp_str = pub_id.fingerprint.to_string(); // Format: xxxx:xxxx:xxxx:xxxx assert_eq!(fp_str.len(), 19); assert_eq!(fp_str.chars().filter(|c| *c == ':').count(), 3); } }