Was showing xxxx:xxxx:xxxx:xxxx (8 bytes) but from_hex expected 16 bytes, causing parse failure. Now displays all 16 bytes: xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx Users need to re-init to see the full fingerprint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
183 lines
5.4 KiB
Rust
183 lines
5.4 KiB
Rust
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<Self, ProtocolError> {
|
|
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<S>(key: &VerifyingKey, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
serializer.serialize_bytes(key.as_bytes())
|
|
}
|
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<VerifyingKey, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let bytes: Vec<u8> = 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<S>(key: &PublicKey, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
serializer.serialize_bytes(key.as_bytes())
|
|
}
|
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<PublicKey, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let bytes: Vec<u8> = 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:xxxx:xxxx:xxxx:xxxx
|
|
assert_eq!(fp_str.len(), 39);
|
|
assert_eq!(fp_str.chars().filter(|c| *c == ':').count(), 7);
|
|
}
|
|
}
|