btc-keygen 0.3.0

Minimal offline Bitcoin key generator for cold storage
Documentation
//! Minimal offline Bitcoin key generator for cold storage.
//!
//! Generates a secp256k1 private key from OS-provided cryptographic randomness
//! and derives the corresponding WIF, compressed public key, and native SegWit
//! (Bech32) address. Designed for air-gapped key ceremonies.
//!
//! # Library usage
//!
//! ```no_run
//! // 1. Generate a private key from OS randomness
//! let key = btc_keygen::generate()?;
//!
//! // 2. Encode as WIF (for wallet import)
//! let wif = btc_keygen::encode_wif(&key);
//! println!("{}", wif.expose_str());
//!
//! // 3. Derive the compressed public key
//! let pubkey = btc_keygen::derive_pubkey(&key);
//!
//! // 4. Derive the Bitcoin address
//! let address = btc_keygen::derive_address(&pubkey);
//! # Ok::<(), btc_keygen::Error>(())
//! ```
//!
//! To use an existing key instead of OS randomness, see
//! [`PrivateKey::from_bytes`] and [`PrivateKey::from_hex`].
//!
//! # Security
//!
//! - Entropy comes from the OS CSPRNG via [`getrandom`](https://docs.rs/getrandom).
//! - Private key bytes are zeroized in memory when [`PrivateKey`] is dropped.
//! - Secret output never leaves this crate as a `String`. [`encode_wif`] returns
//!   a [`SecretWif`] and [`PrivateKey::to_hex`] returns a [`SecretKeyHex`]:
//!   fixed-size buffers that zeroize on drop, redact their `Debug`, and cannot
//!   be cloned, copied, or printed with `{}`. They are filled in place, so no
//!   secret-bearing `String`, `Vec`, or `format!` temporary is allocated along
//!   the way.
//! - Exposing a secret is explicit (`expose_bytes`, `expose_str`) and is the
//!   point where copies become the caller's responsibility: writing the bytes
//!   to a terminal, or copying them into a `String`, puts key material in
//!   memory this crate cannot erase.
//! - [`PrivateKey::as_bytes`] and [`PrivateKey::to_secret_key`] exist for
//!   interoperability and hand out key material the crate no longer controls.
//!   In particular [`secp256k1::SecretKey`](https://docs.rs/secp256k1) is `Copy`
//!   and does not erase itself on drop; its `non_secure_erase` is best-effort.
//! - Erasure is best-effort in general. It covers the buffers this crate owns,
//!   not memory the OS relocated to swap or a crash dump, not the stack
//!   libsecp256k1 uses while deriving a public key, and not copies the optimizer
//!   keeps alive. The [`zeroize`](https://docs.rs/zeroize) crate documents that
//!   last limit for itself.
//! - No networking code, so the crate cannot leak secrets over the network.
//! - Elliptic curve operations use Bitcoin Core's
//!   [`libsecp256k1`](https://docs.rs/secp256k1).

use std::fmt;

pub(crate) mod address;
pub(crate) mod entropy;
pub(crate) mod keygen;
pub(crate) mod pubkey;
pub(crate) mod secret;
pub(crate) mod wif;

pub use address::derive_address;
pub use keygen::PrivateKey;
pub use keygen::generate;
pub use pubkey::derive_pubkey;
pub use secret::{SecretAscii, SecretKeyHex, SecretWif};
pub use wif::encode_wif;

/// Error returned when key generation fails.
///
/// This typically indicates a problem with the operating system's random
/// number generator. In normal operation this should never occur.
#[derive(Debug)]
pub struct Error(pub(crate) String);

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl std::error::Error for Error {}

impl From<entropy::EntropyError> for Error {
    fn from(e: entropy::EntropyError) -> Self {
        Error(e.0)
    }
}

#[cfg(test)]
mod pipeline_tests {
    use crate::address;
    use crate::entropy::FixedEntropy;
    use crate::keygen;
    use crate::pubkey;
    use crate::wif;

    /// Full end-to-end test with private key = 1.
    ///
    /// Expected values:
    /// - Private key hex: 0000...0001
    /// - WIF: KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn
    /// - Compressed pubkey: 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
    /// - Address: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
    #[test]
    fn test_full_pipeline_deterministic() {
        let mut key_bytes = [0u8; 32];
        key_bytes[31] = 0x01;

        let entropy = FixedEntropy::new(key_bytes.to_vec());
        let private_key = keygen::generate_with_entropy(&entropy).expect("generation must succeed");

        assert_eq!(private_key.as_bytes(), &key_bytes);

        let wif = wif::encode_wif(&private_key);
        assert_eq!(
            wif.expose_str(),
            "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn"
        );

        assert_eq!(
            private_key.to_hex().expose_str(),
            "0000000000000000000000000000000000000000000000000000000000000001"
        );

        let compressed_pubkey = pubkey::derive_pubkey(&private_key);
        let pubkey_hex: String = compressed_pubkey
            .iter()
            .map(|b| format!("{:02x}", b))
            .collect();
        assert_eq!(
            pubkey_hex,
            "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
        );

        let addr = address::derive_address(&compressed_pubkey);
        assert_eq!(addr, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4");
    }

    /// Second full pipeline test with private key = 2.
    #[test]
    fn test_full_pipeline_known_vector_two() {
        let mut key_bytes = [0u8; 32];
        key_bytes[31] = 0x02;

        let entropy = FixedEntropy::new(key_bytes.to_vec());
        let private_key = keygen::generate_with_entropy(&entropy).expect("generation must succeed");

        let wif = wif::encode_wif(&private_key);
        // WIF for private key = 2 (compressed, mainnet).
        assert_eq!(
            wif.expose_str(),
            "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU74NMTptX4"
        );

        let compressed_pubkey = pubkey::derive_pubkey(&private_key);
        let pubkey_hex: String = compressed_pubkey
            .iter()
            .map(|b| format!("{:02x}", b))
            .collect();
        assert_eq!(
            pubkey_hex,
            "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"
        );

        let addr = address::derive_address(&compressed_pubkey);
        assert_eq!(addr, "bc1qq6hag67dl53wl99vzg42z8eyzfz2xlkvxechjp");
    }

    /// Two different entropy inputs must produce entirely different outputs.
    #[test]
    fn test_pipeline_different_entropy_different_outputs() {
        let mut bytes_a = [0u8; 32];
        bytes_a[31] = 0x01;
        let mut bytes_b = [0u8; 32];
        bytes_b[31] = 0x02;

        let key_a = keygen::generate_with_entropy(&FixedEntropy::new(bytes_a.to_vec())).unwrap();
        let key_b = keygen::generate_with_entropy(&FixedEntropy::new(bytes_b.to_vec())).unwrap();

        let pubkey_a = pubkey::derive_pubkey(&key_a);
        let pubkey_b = pubkey::derive_pubkey(&key_b);

        let addr_a = address::derive_address(&pubkey_a);
        let addr_b = address::derive_address(&pubkey_b);

        assert_ne!(key_a.as_bytes(), key_b.as_bytes());
        assert_ne!(pubkey_a, pubkey_b);
        assert_ne!(addr_a, addr_b);
        assert_ne!(
            wif::encode_wif(&key_a).expose_str(),
            wif::encode_wif(&key_b).expose_str()
        );
    }
}