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//! The public API is four functions and one type:
10//!
11//! | Function | Input | Output |
12//! |---|---|---|
13//! | [`generate`] | — | `Result<`[`PrivateKey`]`, `[`Error`]`>` |
14//! | [`encode_wif`] | `&PrivateKey` | `String` (starts with `K` or `L`, 52 chars) |
15//! | [`derive_pubkey`] | `&PrivateKey` | `[u8; 33]` (compressed public key) |
16//! | [`derive_address`] | `&[u8; 33]` | `String` (Bech32 address, starts with `bc1q`) |
17//!
18//! ```no_run
19//! // 1. Generate a private key from OS randomness
20//! let key = btc_keygen::generate()?;
21//!
22//! // 2. Encode as WIF (for wallet import)
23//! let wif = btc_keygen::encode_wif(&key);
24//!
25//! // 3. Derive the compressed public key
26//! let pubkey = btc_keygen::derive_pubkey(&key);
27//!
28//! // 4. Derive the Bitcoin address
29//! let address = btc_keygen::derive_address(&pubkey);
30//! # Ok::<(), btc_keygen::Error>(())
31//! ```
32//!
33//! [`PrivateKey`] zeroizes its bytes when dropped — you do not need to
34//! clear it manually.
35//!
36//! # Security
37//!
38//! - Entropy comes from the OS CSPRNG via [`getrandom`](https://docs.rs/getrandom).
39//! - Private key bytes are zeroized in memory when [`PrivateKey`] is dropped.
40//! - No networking code — the crate cannot leak secrets over the network.
41//! - Elliptic curve operations use Bitcoin Core's
42//!   [`libsecp256k1`](https://docs.rs/secp256k1).
43
44use std::fmt;
45
46pub(crate) mod address;
47pub(crate) mod entropy;
48pub(crate) mod keygen;
49pub(crate) mod pubkey;
50pub(crate) mod wif;
51
52pub use address::derive_address;
53pub use keygen::PrivateKey;
54pub use keygen::generate;
55pub use pubkey::derive_pubkey;
56pub use wif::encode_wif;
57
58/// Error returned when key generation fails.
59///
60/// This typically indicates a problem with the operating system's random
61/// number generator. In normal operation this should never occur.
62#[derive(Debug)]
63pub struct Error(pub(crate) String);
64
65impl fmt::Display for Error {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        write!(f, "{}", self.0)
68    }
69}
70
71impl std::error::Error for Error {}
72
73impl From<entropy::EntropyError> for Error {
74    fn from(e: entropy::EntropyError) -> Self {
75        Error(e.0)
76    }
77}
78
79#[cfg(test)]
80mod pipeline_tests {
81    use crate::address;
82    use crate::entropy::FixedEntropy;
83    use crate::keygen;
84    use crate::pubkey;
85    use crate::wif;
86
87    /// Full end-to-end test with private key = 1.
88    ///
89    /// Expected values:
90    /// - Private key hex: 0000...0001
91    /// - WIF: KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn
92    /// - Compressed pubkey: 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
93    /// - Address: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
94    #[test]
95    fn test_full_pipeline_deterministic() {
96        let mut key_bytes = [0u8; 32];
97        key_bytes[31] = 0x01;
98
99        let entropy = FixedEntropy::new(key_bytes.to_vec());
100        let private_key = keygen::generate_with_entropy(&entropy).expect("generation must succeed");
101
102        assert_eq!(private_key.as_bytes(), &key_bytes);
103
104        let wif_str = wif::encode_wif(&private_key);
105        assert_eq!(
106            wif_str,
107            "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn"
108        );
109
110        let compressed_pubkey = pubkey::derive_pubkey(&private_key);
111        let pubkey_hex: String = compressed_pubkey
112            .iter()
113            .map(|b| format!("{:02x}", b))
114            .collect();
115        assert_eq!(
116            pubkey_hex,
117            "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
118        );
119
120        let addr = address::derive_address(&compressed_pubkey);
121        assert_eq!(addr, "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4");
122    }
123
124    /// Second full pipeline test with private key = 2.
125    #[test]
126    fn test_full_pipeline_known_vector_two() {
127        let mut key_bytes = [0u8; 32];
128        key_bytes[31] = 0x02;
129
130        let entropy = FixedEntropy::new(key_bytes.to_vec());
131        let private_key = keygen::generate_with_entropy(&entropy).expect("generation must succeed");
132
133        let wif_str = wif::encode_wif(&private_key);
134        // WIF for private key = 2 (compressed, mainnet).
135        assert_eq!(
136            wif_str,
137            "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU74NMTptX4"
138        );
139
140        let compressed_pubkey = pubkey::derive_pubkey(&private_key);
141        let pubkey_hex: String = compressed_pubkey
142            .iter()
143            .map(|b| format!("{:02x}", b))
144            .collect();
145        assert_eq!(
146            pubkey_hex,
147            "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"
148        );
149
150        let addr = address::derive_address(&compressed_pubkey);
151        assert_eq!(addr, "bc1qq6hag67dl53wl99vzg42z8eyzfz2xlkvxechjp");
152    }
153
154    /// Two different entropy inputs must produce entirely different outputs.
155    #[test]
156    fn test_pipeline_different_entropy_different_outputs() {
157        let mut bytes_a = [0u8; 32];
158        bytes_a[31] = 0x01;
159        let mut bytes_b = [0u8; 32];
160        bytes_b[31] = 0x02;
161
162        let key_a = keygen::generate_with_entropy(&FixedEntropy::new(bytes_a.to_vec())).unwrap();
163        let key_b = keygen::generate_with_entropy(&FixedEntropy::new(bytes_b.to_vec())).unwrap();
164
165        let pubkey_a = pubkey::derive_pubkey(&key_a);
166        let pubkey_b = pubkey::derive_pubkey(&key_b);
167
168        let addr_a = address::derive_address(&pubkey_a);
169        let addr_b = address::derive_address(&pubkey_b);
170
171        assert_ne!(key_a.as_bytes(), key_b.as_bytes());
172        assert_ne!(pubkey_a, pubkey_b);
173        assert_ne!(addr_a, addr_b);
174        assert_ne!(wif::encode_wif(&key_a), wif::encode_wif(&key_b));
175    }
176}