rustywallet_keys/private_key.rs
1//! Private key implementation
2//!
3//! This module provides the [`PrivateKey`] type for working with secp256k1 private keys.
4
5use crate::encoding::{hex, wif};
6use crate::error::PrivateKeyError;
7use crate::network::Network;
8use rand::rngs::OsRng;
9use secp256k1::{Secp256k1, SecretKey};
10use std::fmt;
11use zeroize::Zeroize;
12
13/// A secp256k1 private key with secure memory handling.
14///
15/// This struct wraps a `secp256k1::SecretKey` and provides convenient methods
16/// for key generation, import, and export. The key is automatically zeroized
17/// when dropped for security.
18///
19/// # Example
20///
21/// ```
22/// use rustywallet_keys::private_key::PrivateKey;
23/// use rustywallet_keys::network::Network;
24///
25/// // Generate a random key
26/// let key = PrivateKey::random();
27///
28/// // Export to hex
29/// let hex = key.to_hex();
30/// assert_eq!(hex.len(), 64);
31///
32/// // Export to WIF
33/// let wif = key.to_wif(Network::Mainnet);
34/// assert!(wif.starts_with('K') || wif.starts_with('L'));
35/// ```
36pub struct PrivateKey {
37 inner: SecretKey,
38}
39
40impl PrivateKey {
41 /// Generate a new random private key using a cryptographically secure RNG.
42 ///
43 /// This method uses the operating system's secure random number generator
44 /// and automatically regenerates if an invalid key is produced (which is
45 /// extremely unlikely).
46 ///
47 /// # Example
48 ///
49 /// ```
50 /// use rustywallet_keys::private_key::PrivateKey;
51 ///
52 /// let key = PrivateKey::random();
53 /// ```
54 pub fn random() -> Self {
55 let secp = Secp256k1::new();
56 let (secret_key, _) = secp.generate_keypair(&mut OsRng);
57 Self { inner: secret_key }
58 }
59
60 /// Create a private key from a 32-byte array.
61 ///
62 /// # Errors
63 ///
64 /// Returns [`PrivateKeyError::OutOfRange`] if the bytes represent a value
65 /// that is zero or greater than or equal to the curve order.
66 ///
67 /// # Example
68 ///
69 /// ```
70 /// use rustywallet_keys::private_key::PrivateKey;
71 ///
72 /// let bytes = [1u8; 32];
73 /// let key = PrivateKey::from_bytes(bytes).unwrap();
74 /// ```
75 pub fn from_bytes(bytes: [u8; 32]) -> Result<Self, PrivateKeyError> {
76 let secret_key = SecretKey::from_slice(&bytes).map_err(|_| PrivateKeyError::OutOfRange)?;
77 Ok(Self { inner: secret_key })
78 }
79
80 /// Check if a 32-byte array represents a valid private key.
81 ///
82 /// A valid private key must be non-zero and less than the secp256k1 curve order.
83 ///
84 /// # Example
85 ///
86 /// ```
87 /// use rustywallet_keys::private_key::PrivateKey;
88 ///
89 /// let valid_bytes = [1u8; 32];
90 /// assert!(PrivateKey::is_valid(&valid_bytes));
91 ///
92 /// let zero_bytes = [0u8; 32];
93 /// assert!(!PrivateKey::is_valid(&zero_bytes));
94 /// ```
95 pub fn is_valid(bytes: &[u8; 32]) -> bool {
96 SecretKey::from_slice(bytes).is_ok()
97 }
98
99 /// Create a private key from a hex string.
100 ///
101 /// The hex string must be exactly 64 characters (32 bytes).
102 /// Both uppercase and lowercase characters are accepted.
103 ///
104 /// # Errors
105 ///
106 /// - [`PrivateKeyError::InvalidLength`] if the hex string is not 64 characters
107 /// - [`PrivateKeyError::InvalidHex`] if the string contains invalid hex characters
108 /// - [`PrivateKeyError::OutOfRange`] if the decoded value is invalid
109 ///
110 /// # Example
111 ///
112 /// ```
113 /// use rustywallet_keys::private_key::PrivateKey;
114 ///
115 /// let hex = "0000000000000000000000000000000000000000000000000000000000000001";
116 /// let key = PrivateKey::from_hex(hex).unwrap();
117 /// ```
118 pub fn from_hex(hex_str: &str) -> Result<Self, PrivateKeyError> {
119 if hex_str.len() != 64 {
120 return Err(PrivateKeyError::InvalidLength(hex_str.len() / 2));
121 }
122
123 let bytes = hex::decode(hex_str).map_err(|e| PrivateKeyError::InvalidHex(e.to_string()))?;
124
125 let mut arr = [0u8; 32];
126 arr.copy_from_slice(&bytes);
127 Self::from_bytes(arr)
128 }
129
130 /// Create a private key from a WIF (Wallet Import Format) string.
131 ///
132 /// # Errors
133 ///
134 /// - [`PrivateKeyError::InvalidWif`] if the WIF format is invalid
135 /// - [`PrivateKeyError::InvalidChecksum`] if the checksum doesn't match
136 /// - [`PrivateKeyError::OutOfRange`] if the decoded key is invalid
137 ///
138 /// # Example
139 ///
140 /// ```
141 /// use rustywallet_keys::private_key::PrivateKey;
142 ///
143 /// let wif = "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ";
144 /// let key = PrivateKey::from_wif(wif).unwrap();
145 /// ```
146 pub fn from_wif(wif_str: &str) -> Result<Self, PrivateKeyError> {
147 let (bytes, _network, _compressed) = wif::decode(wif_str).map_err(|e| match e {
148 wif::WifError::InvalidChecksum => PrivateKeyError::InvalidChecksum,
149 other => PrivateKeyError::InvalidWif(other.to_string()),
150 })?;
151 Self::from_bytes(bytes)
152 }
153
154 /// Export the private key as a 32-byte array.
155 ///
156 /// # Example
157 ///
158 /// ```
159 /// use rustywallet_keys::private_key::PrivateKey;
160 ///
161 /// let key = PrivateKey::random();
162 /// let bytes = key.to_bytes();
163 /// assert_eq!(bytes.len(), 32);
164 /// ```
165 pub fn to_bytes(&self) -> [u8; 32] {
166 self.inner.secret_bytes()
167 }
168
169 /// Export the private key as a lowercase hex string.
170 ///
171 /// # Example
172 ///
173 /// ```
174 /// use rustywallet_keys::private_key::PrivateKey;
175 ///
176 /// let key = PrivateKey::random();
177 /// let hex = key.to_hex();
178 /// assert_eq!(hex.len(), 64);
179 /// ```
180 pub fn to_hex(&self) -> String {
181 hex::encode(&self.to_bytes())
182 }
183
184 /// Export the private key as a WIF (Wallet Import Format) string.
185 ///
186 /// The WIF format includes the network version byte and uses compressed
187 /// public key format by default.
188 ///
189 /// # Example
190 ///
191 /// ```
192 /// use rustywallet_keys::private_key::PrivateKey;
193 /// use rustywallet_keys::network::Network;
194 ///
195 /// let key = PrivateKey::random();
196 /// let wif = key.to_wif(Network::Mainnet);
197 /// assert!(wif.starts_with('K') || wif.starts_with('L'));
198 /// ```
199 pub fn to_wif(&self, network: Network) -> String {
200 wif::encode(&self.to_bytes(), network, true)
201 }
202
203 /// Export the private key as a decimal string.
204 ///
205 /// This converts the 256-bit key to its decimal representation.
206 ///
207 /// # Example
208 ///
209 /// ```
210 /// use rustywallet_keys::private_key::PrivateKey;
211 ///
212 /// let key = PrivateKey::from_hex(
213 /// "0000000000000000000000000000000000000000000000000000000000000001"
214 /// ).unwrap();
215 /// assert_eq!(key.to_decimal(), "1");
216 /// ```
217 pub fn to_decimal(&self) -> String {
218 let bytes = self.to_bytes();
219 bytes_to_decimal(&bytes)
220 }
221
222 /// Derive the corresponding public key.
223 ///
224 /// # Example
225 ///
226 /// ```
227 /// use rustywallet_keys::private_key::PrivateKey;
228 ///
229 /// let private_key = PrivateKey::random();
230 /// let public_key = private_key.public_key();
231 /// ```
232 pub fn public_key(&self) -> crate::public_key::PublicKey {
233 crate::public_key::PublicKey::from_private_key(self)
234 }
235}
236
237/// Convert a 32-byte array to decimal string
238fn bytes_to_decimal(bytes: &[u8; 32]) -> String {
239 // Handle zero case
240 if bytes.iter().all(|&b| b == 0) {
241 return "0".to_string();
242 }
243
244 // Convert bytes to decimal using repeated division
245 let mut result = Vec::new();
246 let mut temp = bytes.to_vec();
247
248 while temp.iter().any(|&b| b != 0) {
249 let mut remainder = 0u32;
250 for byte in temp.iter_mut() {
251 let value = (remainder << 8) | (*byte as u32);
252 *byte = (value / 10) as u8;
253 remainder = value % 10;
254 }
255 result.push((remainder as u8) + b'0');
256 }
257
258 result.reverse();
259 String::from_utf8(result).unwrap_or_else(|_| "0".to_string())
260}
261
262impl Drop for PrivateKey {
263 fn drop(&mut self) {
264 // SecretKey doesn't expose mutable access to its bytes,
265 // but secp256k1 crate handles zeroization internally.
266 // We create a temporary copy and zeroize it to ensure
267 // any stack copies are cleared.
268 let mut bytes = self.inner.secret_bytes();
269 bytes.zeroize();
270 }
271}
272
273impl fmt::Debug for PrivateKey {
274 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275 write!(
276 f,
277 "PrivateKey {{ hex: {}, decimal: {} }}",
278 self.to_hex(),
279 self.to_decimal()
280 )
281 }
282}
283
284impl Clone for PrivateKey {
285 fn clone(&self) -> Self {
286 Self { inner: self.inner }
287 }
288}
289
290impl PartialEq for PrivateKey {
291 fn eq(&self, other: &Self) -> bool {
292 self.inner == other.inner
293 }
294}
295
296impl Eq for PrivateKey {}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use proptest::prelude::*;
302
303 /// Strategy to generate valid private key bytes
304 fn valid_private_key_bytes() -> impl Strategy<Value = [u8; 32]> {
305 // Generate random bytes and filter to valid keys
306 prop::array::uniform32(any::<u8>()).prop_filter("must be valid secp256k1 key", |bytes| {
307 PrivateKey::is_valid(bytes)
308 })
309 }
310
311 // **Feature: rustywallet-keys, Property 1: Random Key Validity**
312 // **Validates: Requirements 1.1, 1.2**
313 // For any randomly generated private key, the key SHALL be valid
314 // (non-zero and less than the secp256k1 curve order).
315 #[test]
316 fn property_random_key_validity() {
317 // Generate 100 random keys and verify each is valid
318 for _ in 0..100 {
319 let key = PrivateKey::random();
320 let bytes = key.to_bytes();
321
322 // Verify the key is valid
323 assert!(PrivateKey::is_valid(&bytes), "Random key should be valid");
324
325 // Verify the key is non-zero
326 assert!(
327 bytes.iter().any(|&b| b != 0),
328 "Random key should not be all zeros"
329 );
330
331 // Verify we can reconstruct the key from bytes
332 let reconstructed =
333 PrivateKey::from_bytes(bytes).expect("Should be able to reconstruct from bytes");
334 assert_eq!(
335 key, reconstructed,
336 "Reconstructed key should equal original"
337 );
338 }
339 }
340
341 // **Feature: rustywallet-keys, Property 2: Hex Round-Trip**
342 // **Validates: Requirements 2.1, 3.1, 3.5**
343 // For any valid private key, converting to hex and parsing back SHALL produce
344 // an equivalent private key.
345 proptest! {
346 #![proptest_config(ProptestConfig::with_cases(100))]
347 #[test]
348 fn property_hex_roundtrip(bytes in valid_private_key_bytes()) {
349 let key = PrivateKey::from_bytes(bytes).unwrap();
350 let hex = key.to_hex();
351 let recovered = PrivateKey::from_hex(&hex).unwrap();
352 prop_assert_eq!(key, recovered);
353 }
354 }
355
356 // **Feature: rustywallet-keys, Property 3: Bytes Round-Trip**
357 // **Validates: Requirements 2.2, 3.2, 3.5**
358 // For any valid private key, exporting to bytes and importing back SHALL produce
359 // an equivalent private key.
360 proptest! {
361 #![proptest_config(ProptestConfig::with_cases(100))]
362 #[test]
363 fn property_bytes_roundtrip(bytes in valid_private_key_bytes()) {
364 let key = PrivateKey::from_bytes(bytes).unwrap();
365 let exported = key.to_bytes();
366 let recovered = PrivateKey::from_bytes(exported).unwrap();
367 prop_assert_eq!(key, recovered);
368 }
369 }
370
371 // **Feature: rustywallet-keys, Property 4: WIF Round-Trip**
372 // **Validates: Requirements 2.3, 3.3, 3.4, 3.5**
373 // For any valid private key and network, encoding to WIF and decoding back
374 // SHALL produce an equivalent private key.
375 proptest! {
376 #![proptest_config(ProptestConfig::with_cases(100))]
377 #[test]
378 fn property_wif_roundtrip(
379 bytes in valid_private_key_bytes(),
380 use_mainnet in any::<bool>()
381 ) {
382 let network = if use_mainnet { Network::Mainnet } else { Network::Testnet };
383 let key = PrivateKey::from_bytes(bytes).unwrap();
384 let wif = key.to_wif(network);
385 let recovered = PrivateKey::from_wif(&wif).unwrap();
386 prop_assert_eq!(key, recovered);
387 }
388 }
389
390 // **Feature: rustywallet-keys, Property 5: Hex Case Insensitivity**
391 // **Validates: Requirements 2.5**
392 // For any valid hex string representing a private key, both uppercase and
393 // lowercase versions SHALL parse to equivalent keys.
394 proptest! {
395 #![proptest_config(ProptestConfig::with_cases(100))]
396 #[test]
397 fn property_hex_case_insensitivity(bytes in valid_private_key_bytes()) {
398 let key = PrivateKey::from_bytes(bytes).unwrap();
399 let hex_lower = key.to_hex();
400 let hex_upper = hex_lower.to_uppercase();
401
402 let from_lower = PrivateKey::from_hex(&hex_lower).unwrap();
403 let from_upper = PrivateKey::from_hex(&hex_upper).unwrap();
404
405 prop_assert_eq!(from_lower, from_upper);
406 }
407 }
408
409 // **Feature: rustywallet-keys, Property 6: Invalid Input Rejection**
410 // **Validates: Requirements 2.4, 4.1, 4.2, 4.3**
411 // For any byte array that is zero or >= curve order, the validation function
412 // SHALL return false and construction SHALL return an error.
413 #[test]
414 fn property_invalid_input_rejection() {
415 // Test zero key
416 let zero_bytes = [0u8; 32];
417 assert!(
418 !PrivateKey::is_valid(&zero_bytes),
419 "Zero key should be invalid"
420 );
421 assert!(
422 PrivateKey::from_bytes(zero_bytes).is_err(),
423 "Zero key should fail construction"
424 );
425
426 // Test curve order (n) - this is >= curve order so should be invalid
427 // secp256k1 curve order n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
428 let curve_order: [u8; 32] = [
429 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
430 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C,
431 0xD0, 0x36, 0x41, 0x41,
432 ];
433 assert!(
434 !PrivateKey::is_valid(&curve_order),
435 "Curve order should be invalid"
436 );
437 assert!(
438 PrivateKey::from_bytes(curve_order).is_err(),
439 "Curve order should fail construction"
440 );
441
442 // Test value greater than curve order
443 let above_order: [u8; 32] = [0xFF; 32];
444 assert!(
445 !PrivateKey::is_valid(&above_order),
446 "Value above curve order should be invalid"
447 );
448 assert!(
449 PrivateKey::from_bytes(above_order).is_err(),
450 "Value above curve order should fail construction"
451 );
452
453 // Test that n-1 is valid (maximum valid key)
454 let max_valid: [u8; 32] = [
455 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
456 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C,
457 0xD0, 0x36, 0x41, 0x40,
458 ];
459 assert!(PrivateKey::is_valid(&max_valid), "n-1 should be valid");
460 assert!(
461 PrivateKey::from_bytes(max_valid).is_ok(),
462 "n-1 should succeed construction"
463 );
464
465 // Test that 1 is valid (minimum valid key)
466 let min_valid: [u8; 32] = [
467 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
468 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
469 0x00, 0x00, 0x00, 0x01,
470 ];
471 assert!(PrivateKey::is_valid(&min_valid), "1 should be valid");
472 assert!(
473 PrivateKey::from_bytes(min_valid).is_ok(),
474 "1 should succeed construction"
475 );
476 }
477
478 // **Feature: rustywallet-keys, Property 11: Debug Output Format**
479 // **Validates: Requirements 8.5**
480 // For any private key, the Debug trait output SHALL contain the key in hex format.
481 proptest! {
482 #![proptest_config(ProptestConfig::with_cases(100))]
483 #[test]
484 fn property_debug_output_format(bytes in valid_private_key_bytes()) {
485 let key = PrivateKey::from_bytes(bytes).unwrap();
486 let debug_output = format!("{:?}", key);
487 let hex_output = key.to_hex();
488 let decimal_output = key.to_decimal();
489
490 // Debug output should contain both hex and decimal
491 let expected = format!("PrivateKey {{ hex: {}, decimal: {} }}", hex_output, decimal_output);
492 prop_assert_eq!(&debug_output, &expected);
493 }
494 }
495
496 // Test decimal conversion
497 #[test]
498 fn test_decimal_conversion() {
499 // Key = 1
500 let key = PrivateKey::from_hex(
501 "0000000000000000000000000000000000000000000000000000000000000001",
502 )
503 .unwrap();
504 assert_eq!(key.to_decimal(), "1");
505
506 // Key = 256
507 let key = PrivateKey::from_hex(
508 "0000000000000000000000000000000000000000000000000000000000000100",
509 )
510 .unwrap();
511 assert_eq!(key.to_decimal(), "256");
512
513 // Key = 65536
514 let key = PrivateKey::from_hex(
515 "0000000000000000000000000000000000000000000000000000000000010000",
516 )
517 .unwrap();
518 assert_eq!(key.to_decimal(), "65536");
519 }
520
521 // ==================== Known Test Vectors ====================
522
523 /// Test vector from Bitcoin Wiki
524 /// https://en.bitcoin.it/wiki/Wallet_import_format
525 #[test]
526 fn test_vector_wif_bitcoin_wiki() {
527 // Private key: 0C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D
528 // WIF (uncompressed): 5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ
529 let hex = "0C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D";
530 let expected_wif_uncompressed = "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ";
531
532 let key = PrivateKey::from_hex(hex).unwrap();
533
534 // Import from WIF should work
535 let from_wif = PrivateKey::from_wif(expected_wif_uncompressed).unwrap();
536 assert_eq!(key, from_wif);
537 }
538
539 /// Test vector: Key value 1 (minimum valid key)
540 #[test]
541 fn test_vector_key_one() {
542 let hex = "0000000000000000000000000000000000000000000000000000000000000001";
543 let key = PrivateKey::from_hex(hex).unwrap();
544
545 // Verify hex round-trip
546 assert_eq!(key.to_hex(), hex.to_lowercase());
547
548 // Verify bytes
549 let mut expected_bytes = [0u8; 32];
550 expected_bytes[31] = 1;
551 assert_eq!(key.to_bytes(), expected_bytes);
552
553 // Verify public key derivation works
554 let public_key = key.public_key();
555 let compressed = public_key.to_compressed();
556 assert_eq!(compressed.len(), 33);
557 assert!(compressed[0] == 0x02 || compressed[0] == 0x03);
558 }
559
560 /// Test vector: Known public key derivation
561 /// Private key 1 should produce a specific public key
562 #[test]
563 fn test_vector_pubkey_derivation() {
564 let hex = "0000000000000000000000000000000000000000000000000000000000000001";
565 let key = PrivateKey::from_hex(hex).unwrap();
566 let public_key = key.public_key();
567
568 // Known compressed public key for private key = 1
569 // 0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
570 let compressed_hex = public_key.to_hex(crate::public_key::PublicKeyFormat::Compressed);
571 assert_eq!(
572 compressed_hex.to_lowercase(),
573 "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
574 );
575 }
576
577 /// Test WIF encoding for testnet
578 #[test]
579 fn test_vector_wif_testnet() {
580 let key = PrivateKey::from_hex(
581 "0000000000000000000000000000000000000000000000000000000000000001",
582 )
583 .unwrap();
584
585 let wif_mainnet = key.to_wif(Network::Mainnet);
586 let wif_testnet = key.to_wif(Network::Testnet);
587
588 // Mainnet compressed WIF starts with K or L
589 assert!(
590 wif_mainnet.starts_with('K') || wif_mainnet.starts_with('L'),
591 "Mainnet WIF should start with K or L"
592 );
593
594 // Testnet compressed WIF starts with c
595 assert!(
596 wif_testnet.starts_with('c'),
597 "Testnet WIF should start with c"
598 );
599
600 // Both should decode back to the same key
601 let from_mainnet = PrivateKey::from_wif(&wif_mainnet).unwrap();
602 let from_testnet = PrivateKey::from_wif(&wif_testnet).unwrap();
603 assert_eq!(key, from_mainnet);
604 assert_eq!(key, from_testnet);
605 }
606}