fci4096 0.1.5

BIP39-inspired mnemonic wordlist using 4096 standard Chinese four-character idioms (chengyu). 12 bits/word, SHA-256 checksum, PBKDF2-SHA512 seed derivation, no_std + WASM support, offline local-only key generation.
Documentation
use pbkdf2::pbkdf2_hmac_array;
use sha2::Sha512;
use unicode_normalization::UnicodeNormalization;

#[cfg(not(feature = "std"))]
use alloc::string::String;

/// Default PBKDF2 iteration count (**Note: this project uses 4096, which differs from BIP39's 2048**).
///
/// Higher iteration counts provide stronger security but slower computation on
/// low-performance devices. Use [`mnemonic_to_seed_with_iterations`] for a custom count.
///
/// **Compatibility warning**: Seeds generated with 4096 iterations are **not compatible**
/// with standard BIP39 wallets (which use 2048 iterations). For BIP39 wallet
/// interoperability, use `mnemonic_to_seed_with_iterations(mnemonic, passphrase, 2048)`.
pub const PBKDF2_ITERATIONS: u32 = 4096;

/// Derive a 64-byte seed from a mnemonic phrase and passphrase.
///
/// Follows the BIP39 seed-derivation process:
/// 1. Apply NFKD Unicode normalization to the passphrase.
/// 2. Apply NFKD normalization to the mnemonic (ensuring different Unicode
///    representations of the same text produce an identical seed).
/// 3. Salt = `"mnemonic" + normalized_passphrase`.
/// 4. Derive a 64-byte seed via PBKDF2-HMAC-SHA512 with the default
///    [`PBKDF2_ITERATIONS`] (4096) iterations.
///
/// Suitable for deriving wallet master keys, encryption keys, or any
/// application requiring a deterministic 512-bit seed from a human-readable
/// mnemonic. Runs entirely offline with no network I/O.
///
/// # Arguments
///
/// * `mnemonic` — Space-separated idiom phrase (NFKD-normalized internally).
/// * `passphrase` — Optional passphrase; empty string for no passphrase.
///
/// # Returns
///
/// A `[u8; 64]` seed. This function is infallible (never returns an error).
#[must_use]
pub fn mnemonic_to_seed(mnemonic: &str, passphrase: &str) -> [u8; 64] {
    mnemonic_to_seed_with_iterations(mnemonic, passphrase, PBKDF2_ITERATIONS)
}

/// Derive a 64-byte seed with a custom PBKDF2 iteration count.
///
/// Both the mnemonic and passphrase are NFKD-normalized to ensure that
/// semantically identical text produces a consistent seed regardless of
/// Unicode representation. Use this function to select a non-default iteration
/// count — for example, `2048` for BIP39 wallet interoperability, or a higher
/// count for stronger KDF stretching on high-performance hardware.
///
/// # Arguments
///
/// * `mnemonic` — Space-separated idiom phrase.
/// * `passphrase` — Optional passphrase (empty string for none).
/// * `iterations` — PBKDF2 iteration count. Higher values increase brute-force
///   resistance but slow down derivation.
///
/// # Returns
///
/// A `[u8; 64]` seed. This function is infallible.
///
/// # See also
///
/// [`mnemonic_to_seed`] — Uses the default [`PBKDF2_ITERATIONS`] (4096).
#[must_use]
pub fn mnemonic_to_seed_with_iterations(
    mnemonic: &str,
    passphrase: &str,
    iterations: u32,
) -> [u8; 64] {
    let normalized_mnemonic: String = mnemonic.nfkd().collect();
    let normalized_passphrase: String = passphrase.nfkd().collect();

    // Pre-allocate capacity to avoid format! reallocation
    let mut salt = String::with_capacity(8 + normalized_passphrase.len());
    salt.push_str("mnemonic");
    salt.push_str(&normalized_passphrase);
    pbkdf2_hmac_array::<Sha512, 64>(normalized_mnemonic.as_bytes(), salt.as_bytes(), iterations)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_seed_generation() {
        let mnemonic =
            "哀感天地 哀毁瘠立 挨肩叠背 唉声叹气 挨三顶五 矮子看戏 爱才若渴 暧昧不明 碍手碍脚 安邦定国 安分守己 安魂定魄";
        let seed = mnemonic_to_seed(mnemonic, "");
        assert_eq!(seed.len(), 64);
    }

    #[test]
    fn test_seed_with_passphrase() {
        let mnemonic = "哀感天地 哀毁瘠立 挨肩叠背 唉声叹气 挨三顶五 矮子看戏 爱才若渴 暧昧不明 碍手碍脚 安邦定国 安分守己 安魂定魄";
        let seed1 = mnemonic_to_seed(mnemonic, "");
        let seed2 = mnemonic_to_seed(mnemonic, "password");
        assert_ne!(seed1, seed2);
    }

    #[test]
    fn test_seed_consistency() {
        let mnemonic = "哀感天地 哀毁瘠立 挨肩叠背 唉声叹气 挨三顶五 矮子看戏 爱才若渴 暧昧不明 碍手碍脚 安邦定国 安分守己 安魂定魄";
        let seed1 = mnemonic_to_seed(mnemonic, "test");
        let seed2 = mnemonic_to_seed(mnemonic, "test");
        assert_eq!(seed1, seed2);
    }

    #[test]
    fn test_seed_custom_iterations() {
        let mnemonic = "哀感天地 哀毁瘠立 挨肩叠背 唉声叹气 挨三顶五 矮子看戏 爱才若渴 暧昧不明 碍手碍脚 安邦定国 安分守己 安魂定魄";
        let seed_default = mnemonic_to_seed(mnemonic, "");
        let seed_custom = mnemonic_to_seed_with_iterations(mnemonic, "", 4096);
        assert_eq!(seed_default, seed_custom);
    }

    #[test]
    fn test_nfkd_normalization_consistency() {
        // Verify NFKD normalization ensures full-width/half-width characters produce
        // consistent results
        let mnemonic = "哀感天地 哀毁瘠立 挨肩叠背 唉声叹气 挨三顶五 矮子看戏 爱才若渴 暧昧不明 碍手碍脚 安邦定国 安分守己 安魂定魄";
        let seed1 = mnemonic_to_seed(mnemonic, "TEST");
        let seed2 = mnemonic_to_seed(mnemonic, "TEST");
        assert_eq!(seed1, seed2);

        // Passphrase with emoji and special characters
        let seed3 = mnemonic_to_seed(mnemonic, "密码🔑");
        assert_eq!(seed3.len(), 64);
    }
}