Skip to main content

btc_keygen/
lib.rs

1//! Minimal offline Bitcoin key generator for cold storage.
2//!
3//! Generates a secp256k1 private key from OS-provided cryptographic randomness
4//! and derives the corresponding WIF, compressed public key, and native SegWit
5//! (Bech32) address. Designed for air-gapped key ceremonies.
6//!
7//! # Library usage
8//!
9//! ```no_run
10//! // 1. Generate a private key from OS randomness
11//! let key = btc_keygen::generate()?;
12//!
13//! // 2. Encode as WIF (for wallet import)
14//! let wif = btc_keygen::encode_wif(&key);
15//! println!("{}", wif.expose_str());
16//!
17//! // 3. Derive the compressed public key
18//! let pubkey = btc_keygen::derive_pubkey(&key);
19//!
20//! // 4. Derive the Bitcoin address
21//! let address = btc_keygen::derive_address(&pubkey);
22//! # Ok::<(), btc_keygen::Error>(())
23//! ```
24//!
25//! To use an existing key instead of OS randomness, see
26//! [`PrivateKey::from_bytes`] and [`PrivateKey::from_hex`].
27//!
28//! # Security
29//!
30//! - Entropy comes from the OS CSPRNG via [`getrandom`](https://docs.rs/getrandom).
31//! - Private key bytes are zeroized in memory when [`PrivateKey`] is dropped.
32//! - Secret output never leaves this crate as a `String`. [`encode_wif`] returns
33//!   a [`SecretWif`] and [`PrivateKey::to_hex`] returns a [`SecretKeyHex`]:
34//!   fixed-size buffers that zeroize on drop, redact their `Debug`, and cannot
35//!   be cloned, copied, or printed with `{}`. They are filled in place, so no
36//!   secret-bearing `String`, `Vec`, or `format!` temporary is allocated along
37//!   the way.
38//! - Exposing a secret is explicit (`expose_bytes`, `expose_str`) and is the
39//!   point where copies become the caller's responsibility: writing the bytes
40//!   to a terminal, or copying them into a `String`, puts key material in
41//!   memory this crate cannot erase.
42//! - [`PrivateKey::as_bytes`] and [`PrivateKey::to_secret_key`] exist for
43//!   interoperability and hand out key material the crate no longer controls.
44//!   In particular [`secp256k1::SecretKey`](https://docs.rs/secp256k1) is `Copy`
45//!   and does not erase itself on drop; its `non_secure_erase` is best-effort.
46//! - Erasure is best-effort in general. It covers the buffers this crate owns,
47//!   not memory the OS relocated to swap or a crash dump, not the stack
48//!   libsecp256k1 uses while deriving a public key, and not copies the optimizer
49//!   keeps alive. The [`zeroize`](https://docs.rs/zeroize) crate documents that
50//!   last limit for itself.
51//! - No networking code, so the crate cannot leak secrets over the network.
52//! - Elliptic curve operations use Bitcoin Core's
53//!   [`libsecp256k1`](https://docs.rs/secp256k1).
54
55use std::fmt;
56
57pub(crate) mod address;
58pub(crate) mod entropy;
59pub(crate) mod keygen;
60pub(crate) mod pubkey;
61pub(crate) mod secret;
62pub(crate) mod wif;
63
64pub use address::derive_address;
65pub use keygen::PrivateKey;
66pub use keygen::generate;
67pub use pubkey::derive_pubkey;
68pub use secret::{SecretAscii, SecretKeyHex, SecretWif};
69pub use wif::encode_wif;
70
71/// Error returned when key generation fails.
72///
73/// This typically indicates a problem with the operating system's random
74/// number generator. In normal operation this should never occur.
75#[derive(Debug)]
76pub struct Error(pub(crate) String);
77
78impl fmt::Display for Error {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        write!(f, "{}", self.0)
81    }
82}
83
84impl std::error::Error for Error {}
85
86impl From<entropy::EntropyError> for Error {
87    fn from(e: entropy::EntropyError) -> Self {
88        Error(e.0)
89    }
90}
91
92#[cfg(test)]
93mod pipeline_tests {
94    use crate::address;
95    use crate::entropy::FixedEntropy;
96    use crate::keygen;
97    use crate::pubkey;
98    use crate::wif;
99
100    /// Full end-to-end test with private key = 1.
101    ///
102    /// Expected values:
103    /// - Private key hex: 0000...0001
104    /// - WIF: KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn
105    /// - Compressed pubkey: 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
106    /// - Address: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
107    #[test]
108    fn test_full_pipeline_deterministic() {
109        let mut key_bytes = [0u8; 32];
110        key_bytes[31] = 0x01;
111
112        let entropy = FixedEntropy::new(key_bytes.to_vec());
113        let private_key = keygen::generate_with_entropy(&entropy).expect("generation must succeed");
114
115        assert_eq!(private_key.as_bytes(), &key_bytes);
116
117        let wif = wif::encode_wif(&private_key);
118        assert_eq!(
119            wif.expose_str(),
120            "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn"
121        );
122
123        assert_eq!(
124            private_key.to_hex().expose_str(),
125            "0000000000000000000000000000000000000000000000000000000000000001"
126        );
127
128        let compressed_pubkey = pubkey::derive_pubkey(&private_key);
129        let pubkey_hex: String = compressed_pubkey
130            .iter()
131            .map(|b| format!("{:02x}", b))
132            .collect();
133        assert_eq!(
134            pubkey_hex,
135            "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
136        );
137
138        let addr = address::derive_address(&compressed_pubkey);
139        assert_eq!(addr, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4");
140    }
141
142    /// Second full pipeline test with private key = 2.
143    #[test]
144    fn test_full_pipeline_known_vector_two() {
145        let mut key_bytes = [0u8; 32];
146        key_bytes[31] = 0x02;
147
148        let entropy = FixedEntropy::new(key_bytes.to_vec());
149        let private_key = keygen::generate_with_entropy(&entropy).expect("generation must succeed");
150
151        let wif = wif::encode_wif(&private_key);
152        // WIF for private key = 2 (compressed, mainnet).
153        assert_eq!(
154            wif.expose_str(),
155            "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU74NMTptX4"
156        );
157
158        let compressed_pubkey = pubkey::derive_pubkey(&private_key);
159        let pubkey_hex: String = compressed_pubkey
160            .iter()
161            .map(|b| format!("{:02x}", b))
162            .collect();
163        assert_eq!(
164            pubkey_hex,
165            "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"
166        );
167
168        let addr = address::derive_address(&compressed_pubkey);
169        assert_eq!(addr, "bc1qq6hag67dl53wl99vzg42z8eyzfz2xlkvxechjp");
170    }
171
172    /// Two different entropy inputs must produce entirely different outputs.
173    #[test]
174    fn test_pipeline_different_entropy_different_outputs() {
175        let mut bytes_a = [0u8; 32];
176        bytes_a[31] = 0x01;
177        let mut bytes_b = [0u8; 32];
178        bytes_b[31] = 0x02;
179
180        let key_a = keygen::generate_with_entropy(&FixedEntropy::new(bytes_a.to_vec())).unwrap();
181        let key_b = keygen::generate_with_entropy(&FixedEntropy::new(bytes_b.to_vec())).unwrap();
182
183        let pubkey_a = pubkey::derive_pubkey(&key_a);
184        let pubkey_b = pubkey::derive_pubkey(&key_b);
185
186        let addr_a = address::derive_address(&pubkey_a);
187        let addr_b = address::derive_address(&pubkey_b);
188
189        assert_ne!(key_a.as_bytes(), key_b.as_bytes());
190        assert_ne!(pubkey_a, pubkey_b);
191        assert_ne!(addr_a, addr_b);
192        assert_ne!(
193            wif::encode_wif(&key_a).expose_str(),
194            wif::encode_wif(&key_b).expose_str()
195        );
196    }
197}