Files
featherChat/warzone/crates/warzone-protocol/src/identity.rs
Siavash Sameni 651396fa13 Scaffold Rust workspace: warzone-protocol, server, client, mule
4 crates, all compile. 16/17 tests pass.

warzone-protocol (core crypto):
- Seed-based identity (Ed25519 + X25519 from 32-byte seed via HKDF)
- BIP39 mnemonic encode/decode (24 words)
- Fingerprint type (SHA-256 truncated, displayed as xxxx:xxxx:xxxx:xxxx)
- ChaCha20-Poly1305 AEAD encrypt/decrypt with random nonce
- HKDF-SHA256 key derivation
- Pre-key bundle generation with Ed25519 signatures
- X3DH key exchange (simplified, needs X25519 identity key in bundle)
- Double Ratchet: full implementation with DH ratchet, chain ratchet,
  out-of-order message handling via skipped keys cache
- Message format (WarzoneMessage envelope + RatchetHeader)
- Session type with ratchet state
- Storage trait definitions (PreKeyStore, SessionStore, MessageQueue)

warzone-server (axum):
- sled database (keys, messages, one-time pre-keys)
- Routes: /v1/health, /v1/keys/register, /v1/keys/{fp},
  /v1/messages/send, /v1/messages/poll/{fp}, /v1/messages/{id}/ack

warzone-client (CLI):
- `warzone init` — generate seed, show mnemonic, save to ~/.warzone/
- `warzone recover <words>` — restore from mnemonic
- `warzone info` — show fingerprint and keys
- Seed storage at ~/.warzone/identity.seed (600 perms)
- Stubs for send, recv, chat commands

warzone-mule: Phase 4 placeholder

Known issue: X3DH test fails (initiate/respond use different DH ops
due to missing X25519 identity key in bundle). Fix in next step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:27:48 +04:00

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
assert_eq!(fp_str.len(), 19);
assert_eq!(fp_str.chars().filter(|c| *c == ':').count(), 3);
}
}