feat: featherChat-compatible identity — seed, mnemonic, fingerprint

New identity module (wzp-crypto/src/identity.rs) mirrors featherChat's
warzone-protocol identity.rs exactly:
- Seed: 32 bytes, from hex or BIP39 mnemonic (24 words)
- HKDF derivation: same salt (None), same info strings
- Fingerprint: SHA-256(Ed25519 pub)[:16], same xxxx:xxxx format
- Cross-verified: test proves identity module matches KeyExchange trait

CLI flags:
- --seed <64 hex chars>: use a specific identity
- --mnemonic <24 words>: use BIP39 mnemonic from featherChat
- Without either: generates ephemeral identity

Also adds featherChat as git submodule at deps/featherchat for reference.

32 crypto tests passing (27 original + 5 identity tests).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-28 09:09:38 +04:00
parent 97402f6e60
commit 12cdfe6c8a
6 changed files with 308 additions and 0 deletions

View File

@@ -40,6 +40,33 @@ struct CliArgs {
send_file: Option<String>,
record_file: Option<String>,
echo_test_secs: Option<u32>,
seed_hex: Option<String>,
mnemonic: Option<String>,
}
impl CliArgs {
/// Resolve the identity seed from --seed, --mnemonic, or generate a new one.
pub fn resolve_seed(&self) -> wzp_crypto::Seed {
if let Some(ref hex_str) = self.seed_hex {
let seed = wzp_crypto::Seed::from_hex(hex_str).expect("invalid --seed hex");
let id = seed.derive_identity();
let fp = id.public_identity().fingerprint;
info!(fingerprint = %fp, "identity from --seed");
seed
} else if let Some(ref words) = self.mnemonic {
let seed = wzp_crypto::Seed::from_mnemonic(words).expect("invalid --mnemonic");
let id = seed.derive_identity();
let fp = id.public_identity().fingerprint;
info!(fingerprint = %fp, "identity from --mnemonic");
seed
} else {
let seed = wzp_crypto::Seed::generate();
let id = seed.derive_identity();
let fp = id.public_identity().fingerprint;
info!(fingerprint = %fp, "generated ephemeral identity");
seed
}
}
}
fn parse_args() -> CliArgs {
@@ -49,6 +76,8 @@ fn parse_args() -> CliArgs {
let mut send_file = None;
let mut record_file = None;
let mut echo_test_secs = None;
let mut seed_hex = None;
let mut mnemonic = None;
let mut relay_str = None;
let mut i = 1;
@@ -72,6 +101,21 @@ fn parse_args() -> CliArgs {
.to_string(),
);
}
"--seed" => {
i += 1;
seed_hex = Some(args.get(i).expect("--seed requires hex string").to_string());
}
"--mnemonic" => {
// Consume all remaining words until next flag or end
i += 1;
let mut words = Vec::new();
while i < args.len() && !args[i].starts_with('-') {
words.push(args[i].clone());
i += 1;
}
i -= 1; // back up since outer loop will increment
mnemonic = Some(words.join(" "));
}
"--record" => {
i += 1;
record_file = Some(
@@ -98,6 +142,8 @@ fn parse_args() -> CliArgs {
eprintln!(" --send-file <file> Send a raw PCM file (48kHz mono s16le)");
eprintln!(" --record <file.raw> Record received audio to raw PCM file");
eprintln!(" --echo-test <secs> Run automated echo quality test");
eprintln!(" --seed <hex> Identity seed (64 hex chars, featherChat compatible)");
eprintln!(" --mnemonic <words...> Identity seed as BIP39 mnemonic (24 words)");
eprintln!(" (48kHz mono s16le, play with ffplay -f s16le -ar 48000 -ch_layout mono file.raw)");
eprintln!();
eprintln!("Default relay: 127.0.0.1:4433");
@@ -127,6 +173,8 @@ fn parse_args() -> CliArgs {
send_file,
record_file,
echo_test_secs,
seed_hex,
mnemonic,
}
}
@@ -135,6 +183,7 @@ async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt().init();
let cli = parse_args();
let _seed = cli.resolve_seed();
info!(
relay = %cli.relay_addr,