use k256::ecdsa::signature::hazmat::{PrehashSigner, PrehashVerifier};
use k256::ecdsa::{RecoveryId, Signature, SigningKey, VerifyingKey};
use sha3::{Digest, Keccak256};
use zeroize::Zeroize;
pub struct GeneratedWallet {
pub signer: SigningKey,
pub address: [u8; 20],
pub private_key_hex: String,
}
impl GeneratedWallet {
pub fn address_hex(&self) -> String {
format!("0x{}", hex_encode(&self.address))
}
}
impl Drop for GeneratedWallet {
fn drop(&mut self) {
self.private_key_hex.zeroize();
}
}
pub fn generate() -> GeneratedWallet {
let signer = SigningKey::random(&mut rand_core::OsRng);
finalize(signer)
}
pub fn from_signing_key(signer: SigningKey) -> GeneratedWallet {
finalize(signer)
}
pub fn generate_with_mnemonic() -> (bip39::Mnemonic, SigningKey) {
let mnemonic = bip39::Mnemonic::generate(12).expect("12 is a valid word count");
let signer = signer_from_mnemonic(&mnemonic);
(mnemonic, signer)
}
pub fn signer_from_mnemonic(mnemonic: &bip39::Mnemonic) -> SigningKey {
let mut entropy = mnemonic.to_entropy(); let mut hasher = Keccak256::new();
hasher.update(b"localharness/v0/identity");
hasher.update(&entropy);
let mut digest = [0u8; 32];
digest.copy_from_slice(&hasher.finalize());
let signer = SigningKey::from_slice(&digest)
.expect("keccak256 output is 32 bytes; SigningKey is infallible for valid scalars");
entropy.zeroize();
digest.zeroize();
signer
}
pub fn mnemonic_from_phrase(phrase: &str) -> Result<bip39::Mnemonic, String> {
let normalised: String = phrase
.split_whitespace()
.map(|w| w.to_lowercase())
.collect::<Vec<_>>()
.join(" ");
bip39::Mnemonic::parse_in_normalized(bip39::Language::English, &normalised)
.map_err(|e| e.to_string())
}
pub fn from_private_key_hex(hex: &str) -> Result<SigningKey, String> {
let trimmed = hex.trim().trim_start_matches("0x").trim_start_matches("0X");
let bytes = hex_decode(trimmed).map_err(|e| format!("invalid hex: {e}"))?;
if bytes.len() != 32 {
return Err(format!("expected 32-byte private key, got {}", bytes.len()));
}
SigningKey::from_slice(&bytes).map_err(|e| format!("invalid scalar: {e}"))
}
pub fn address(signer: &SigningKey) -> [u8; 20] {
let verifying = VerifyingKey::from(signer);
let encoded = verifying.to_encoded_point(false); let bytes = encoded.as_bytes();
debug_assert_eq!(bytes.len(), 65);
let mut hasher = Keccak256::new();
hasher.update(&bytes[1..]); let digest = hasher.finalize();
let mut addr = [0u8; 20];
addr.copy_from_slice(&digest[12..]);
addr
}
pub fn sign_hash(signer: &SigningKey, hash: &[u8; 32]) -> [u8; 65] {
let (sig, rec): (Signature, RecoveryId) = signer
.sign_prehash(hash)
.expect("k256 sign_prehash is infallible for a valid SigningKey");
let sig_bytes = sig.to_bytes(); let mut out = [0u8; 65];
out[..64].copy_from_slice(&sig_bytes);
out[64] = 27 + u8::from(rec); out
}
pub fn personal_sign_digest(message: &[u8]) -> [u8; 32] {
let mut hasher = Keccak256::new();
hasher.update(b"\x19Ethereum Signed Message:\n");
hasher.update(message.len().to_string().as_bytes());
hasher.update(message);
let digest = hasher.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&digest);
out
}
pub fn personal_sign(signer: &SigningKey, message: &[u8]) -> [u8; 65] {
sign_hash(signer, &personal_sign_digest(message))
}
pub fn recover_address(signature: &[u8; 65], prehash: &[u8; 32]) -> Result<[u8; 20], String> {
let v = signature[64];
let rec_id = match v {
0 | 27 => 0u8,
1 | 28 => 1u8,
_ => return Err(format!("invalid v: {v}")),
};
let rec = RecoveryId::try_from(rec_id).map_err(|e| e.to_string())?;
let sig = Signature::from_slice(&signature[..64]).map_err(|e| e.to_string())?;
let verifying = VerifyingKey::recover_from_prehash(prehash, &sig, rec)
.map_err(|e| e.to_string())?;
let encoded = verifying.to_encoded_point(false);
let bytes = encoded.as_bytes();
let mut hasher = Keccak256::new();
hasher.update(&bytes[1..]);
let digest = hasher.finalize();
let mut addr = [0u8; 20];
addr.copy_from_slice(&digest[12..]);
Ok(addr)
}
#[allow(dead_code)]
pub fn verify_hash(
signer: &SigningKey,
hash: &[u8; 32],
signature: &[u8; 65],
) -> Result<(), String> {
let verifying = VerifyingKey::from(signer);
let sig = Signature::from_slice(&signature[..64]).map_err(|e| e.to_string())?;
verifying.verify_prehash(hash, &sig).map_err(|e| e.to_string())
}
pub fn pubkey_compressed(signer: &SigningKey) -> Vec<u8> {
let verifying = VerifyingKey::from(signer);
verifying.to_encoded_point(true).as_bytes().to_vec()
}
pub fn ephemeral_keypair() -> (Vec<u8>, SigningKey) {
let signer = SigningKey::random(&mut rand_core::OsRng);
(pubkey_compressed(&signer), signer)
}
pub fn ecdh_shared_key(my: &SigningKey, their_pubkey_sec1: &[u8]) -> Result<[u8; 32], String> {
use k256::{PublicKey, SecretKey};
let their = PublicKey::from_sec1_bytes(their_pubkey_sec1)
.map_err(|e| format!("bad recipient pubkey: {e}"))?;
let secret =
SecretKey::from_bytes(&my.to_bytes()).map_err(|e| format!("bad scalar: {e}"))?;
let shared = k256::ecdh::diffie_hellman(secret.to_nonzero_scalar(), their.as_affine());
let mut hasher = Keccak256::new();
hasher.update(b"localharness/v0/ecies");
hasher.update(shared.raw_secret_bytes());
let mut out = [0u8; 32];
out.copy_from_slice(&hasher.finalize());
Ok(out)
}
pub fn keysync_key_from_entropy(entropy: &[u8]) -> [u8; 32] {
let mut hasher = Keccak256::new();
hasher.update(b"localharness/v0/keysync");
hasher.update(entropy);
let mut out = [0u8; 32];
out.copy_from_slice(&hasher.finalize());
out
}
pub fn sharedfs_key_from_entropy(entropy: &[u8]) -> [u8; 32] {
let mut hasher = Keccak256::new();
hasher.update(b"localharness/v0/sharedfs");
hasher.update(entropy);
let mut out = [0u8; 32];
out.copy_from_slice(&hasher.finalize());
out
}
pub fn at_rest_key_from_entropy(entropy: &[u8]) -> [u8; 32] {
let mut hasher = Keccak256::new();
hasher.update(b"localharness/v0/opfs-at-rest");
hasher.update(entropy);
let mut out = [0u8; 32];
out.copy_from_slice(&hasher.finalize());
out
}
pub fn adopt_code_key(code: &str) -> [u8; 32] {
let mut hasher = Keccak256::new();
hasher.update(b"localharness/v0/adopt");
hasher.update(code.trim().to_uppercase().as_bytes());
let mut out = [0u8; 32];
out.copy_from_slice(&hasher.finalize());
out
}
fn finalize(signer: SigningKey) -> GeneratedWallet {
let address = address(&signer);
let private_key_hex = format!("0x{}", hex_encode(&signer.to_bytes()));
GeneratedWallet {
signer,
address,
private_key_hex,
}
}
pub fn rlp_bytes(input: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(input.len() + 9);
if input.len() == 1 && input[0] < 0x80 {
out.push(input[0]);
} else if input.len() <= 55 {
out.push(0x80 + input.len() as u8);
out.extend_from_slice(input);
} else {
let len_bytes = be_bytes_no_leading_zero(input.len() as u128);
out.push(0xb7 + len_bytes.len() as u8);
out.extend_from_slice(&len_bytes);
out.extend_from_slice(input);
}
out
}
pub fn rlp_list(items: &[Vec<u8>]) -> Vec<u8> {
let body_len: usize = items.iter().map(|i| i.len()).sum();
let mut out = Vec::with_capacity(body_len + 9);
if body_len <= 55 {
out.push(0xc0 + body_len as u8);
} else {
let len_bytes = be_bytes_no_leading_zero(body_len as u128);
out.push(0xf7 + len_bytes.len() as u8);
out.extend_from_slice(&len_bytes);
}
for item in items {
out.extend_from_slice(item);
}
out
}
fn be_bytes_no_leading_zero(value: u128) -> Vec<u8> {
let bytes = value.to_be_bytes();
let first_non_zero = bytes.iter().position(|b| *b != 0).unwrap_or(bytes.len() - 1);
bytes[first_non_zero..].to_vec()
}
pub fn rlp_uint(value: u128) -> Vec<u8> {
if value == 0 {
rlp_bytes(&[])
} else {
rlp_bytes(&be_bytes_no_leading_zero(value))
}
}
use crate::encoding::bytes_to_hex as hex_encode;
use crate::encoding::hex_to_bytes as hex_decode;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_then_restore_round_trips_the_address() {
let w = generate();
assert_eq!(w.private_key_hex.len(), 66);
assert!(w.private_key_hex.starts_with("0x"));
let restored = from_private_key_hex(&w.private_key_hex).unwrap();
assert_eq!(address(&restored), w.address);
}
#[test]
fn address_is_20_bytes() {
let w = generate();
assert_eq!(w.address.len(), 20);
assert_eq!(w.address_hex().len(), 42); }
#[test]
fn sign_then_recover_returns_signing_address() {
let w = generate();
let hash = [0x42u8; 32];
let sig = sign_hash(&w.signer, &hash);
assert_eq!(sig.len(), 65);
assert!(matches!(sig[64], 27 | 28));
let recovered = recover_address(&sig, &hash).unwrap();
assert_eq!(recovered, w.address);
}
#[test]
fn personal_sign_roundtrips_through_recover() {
let w = generate();
let msg = b"localharness-proxy:0xabc:1717200000";
let sig = personal_sign(&w.signer, msg);
assert!(matches!(sig[64], 27 | 28));
let recovered = recover_address(&sig, &personal_sign_digest(msg)).unwrap();
assert_eq!(recovered, w.address);
}
#[test]
fn recover_rejects_invalid_v() {
let w = generate();
let hash = [0x99u8; 32];
let mut sig = sign_hash(&w.signer, &hash);
sig[64] = 99; assert!(recover_address(&sig, &hash).is_err());
}
#[test]
fn verify_hash_accepts_own_signature() {
let w = generate();
let hash = [0x01u8; 32];
let sig = sign_hash(&w.signer, &hash);
verify_hash(&w.signer, &hash, &sig).unwrap();
}
#[test]
fn mnemonic_round_trips_through_phrase_to_address() {
let (m, k1) = generate_with_mnemonic();
let phrase = m.to_string();
assert_eq!(phrase.split_whitespace().count(), 12);
let restored = mnemonic_from_phrase(&phrase).unwrap();
let k2 = signer_from_mnemonic(&restored);
assert_eq!(address(&k1), address(&k2));
}
#[test]
fn rlp_short_string_round_trip() {
assert_eq!(rlp_bytes(&[]), vec![0x80]);
assert_eq!(rlp_bytes(&[0x7f]), vec![0x7f]);
assert_eq!(rlp_bytes(b"dog"), vec![0x83, b'd', b'o', b'g']);
}
#[test]
fn rlp_long_string_uses_length_prefix() {
let s = vec![0xaa; 100];
let enc = rlp_bytes(&s);
assert_eq!(enc[0], 0xb8); assert_eq!(enc[1], 100);
assert_eq!(&enc[2..], &s[..]);
}
#[test]
fn rlp_uint_zero_is_empty_string() {
assert_eq!(rlp_uint(0), vec![0x80]);
}
#[test]
fn rlp_uint_small_minimal() {
assert_eq!(rlp_uint(15), vec![0x0f]);
assert_eq!(rlp_uint(256), vec![0x82, 0x01, 0x00]);
}
#[test]
fn rlp_list_known_vector() {
let cat = rlp_bytes(b"cat");
let dog = rlp_bytes(b"dog");
let enc = rlp_list(&[cat, dog]);
assert_eq!(
enc,
vec![0xc8, 0x83, b'c', b'a', b't', 0x83, b'd', b'o', b'g']
);
}
#[test]
fn mnemonic_known_vector_pins_identity_derivation() {
let phrase = "abandon abandon abandon abandon abandon abandon \
abandon abandon abandon abandon abandon about";
let m = mnemonic_from_phrase(phrase).unwrap();
assert_eq!(m.to_entropy(), vec![0u8; 16]);
let signer = signer_from_mnemonic(&m);
assert_eq!(
format!("0x{}", hex_encode(&address(&signer))),
"0x4800ae69a4855281a1251f8c3beab064eb7da012",
"identity derivation changed — this re-keys EVERY returning user"
);
let k1 = from_private_key_hex(
"0x0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap();
assert_eq!(
format!("0x{}", hex_encode(&address(&k1))),
"0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
"address derivation no longer matches the EVM standard"
);
}
#[test]
fn ecdh_shared_key_is_symmetric_and_pinned() {
let a = from_private_key_hex(
"0x000000000000000000000000000000000000000000000000000000000000000a",
)
.unwrap();
let b = from_private_key_hex(
"0x000000000000000000000000000000000000000000000000000000000000000b",
)
.unwrap();
let pub_a = pubkey_compressed(&a);
let pub_b = pubkey_compressed(&b);
let k_ab = ecdh_shared_key(&a, &pub_b).unwrap();
let k_ba = ecdh_shared_key(&b, &pub_a).unwrap();
assert_eq!(k_ab, k_ba);
assert_eq!(
hex_encode(&k_ab),
"3225f3c45abcb834b362af592bdcb9b999380d22521627ae7e71f9bbce614e47",
"ECIES shared-key derivation changed"
);
assert_eq!(pub_a.len(), 33);
assert!(matches!(pub_a[0], 0x02 | 0x03));
assert!(ecdh_shared_key(&a, &[0u8; 33]).is_err());
}
#[test]
fn keysync_and_sharedfs_keys_pinned_and_distinct() {
let entropy = [0u8; 16];
let keysync = keysync_key_from_entropy(&entropy);
let sharedfs = sharedfs_key_from_entropy(&entropy);
assert_eq!(
hex_encode(&keysync),
"d3ddc0e89ef28726b10fa9aed5fdb086d9dd79aad14b37c9b8fb7b49c9cf77f5",
"keysync key derivation changed — sealed Gemini keys orphaned"
);
assert_eq!(
hex_encode(&sharedfs),
"5d0d6e8c644245c728b0248c30ab02f0a2492f982c99c572ce54210592ca739b",
"sharedfs key derivation changed — sealed shared folders orphaned"
);
assert_ne!(keysync, sharedfs);
}
#[test]
fn at_rest_key_pinned_and_distinct() {
let entropy = [0u8; 16];
let at_rest = at_rest_key_from_entropy(&entropy);
assert_eq!(
hex_encode(&at_rest),
"a0c9c69ced27af86580487d0e3f487ef7143ecfbf69045335e9ea53809a92ced",
"at-rest key derivation changed — every sealed OPFS file orphaned"
);
assert_ne!(at_rest, keysync_key_from_entropy(&entropy));
assert_ne!(at_rest, sharedfs_key_from_entropy(&entropy));
}
#[test]
fn adopt_code_key_pinned_and_case_insensitive() {
assert_eq!(
hex_encode(&adopt_code_key("ABC234")),
"76375069e23267cc461bdc0d247401b56d022d8b33a0155ddbb46784d81ebd77",
"adopt-code key derivation changed — in-flight adopt links break"
);
assert_eq!(adopt_code_key("abc234"), adopt_code_key("ABC234"));
assert_eq!(adopt_code_key(" abc234 \n"), adopt_code_key("ABC234"));
assert_ne!(adopt_code_key("ABC234"), adopt_code_key("ABC235"));
}
#[test]
fn mnemonic_phrase_is_case_and_whitespace_tolerant() {
let (m, k1) = generate_with_mnemonic();
let messy = m
.to_string()
.split_whitespace()
.map(|w| if w.len() > 3 { w.to_uppercase() } else { w.to_string() })
.collect::<Vec<_>>()
.join(" ");
let restored = mnemonic_from_phrase(&messy).unwrap();
let k2 = signer_from_mnemonic(&restored);
assert_eq!(address(&k1), address(&k2));
}
}