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}