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}