rustywallet_export/
bip38.rs

1//! BIP38 encryption export.
2
3use crate::error::{ExportError, Result};
4use rustywallet_keys::prelude::PrivateKey;
5use rustywallet_address::{P2PKHAddress, Network as AddrNetwork};
6use sha2::{Sha256, Digest};
7use aes::cipher::{BlockEncrypt, KeyInit};
8use aes::Aes256;
9use scrypt::{scrypt, Params};
10
11/// Export a private key to BIP38 encrypted format.
12///
13/// BIP38 keys start with "6P" and are password-protected.
14///
15/// # Example
16///
17/// ```rust,ignore
18/// use rustywallet_export::{export_bip38, Network};
19/// use rustywallet_keys::prelude::PrivateKey;
20///
21/// let key = PrivateKey::random();
22/// let encrypted = export_bip38(&key, "mypassword", true).unwrap();
23/// assert!(encrypted.starts_with("6P"));
24/// ```
25pub fn export_bip38(key: &PrivateKey, password: &str, compressed: bool) -> Result<String> {
26    let public_key = key.public_key();
27    
28    // Generate address for hash
29    let address = P2PKHAddress::from_public_key(&public_key, AddrNetwork::BitcoinMainnet)
30        .map_err(|e| ExportError::AddressError(e.to_string()))?
31        .to_string();
32    
33    // Address hash (first 4 bytes of double SHA256)
34    let addr_hash1 = Sha256::digest(address.as_bytes());
35    let addr_hash2 = Sha256::digest(addr_hash1);
36    let address_hash: [u8; 4] = addr_hash2[0..4].try_into().unwrap();
37    
38    // Derive key using scrypt
39    // BIP38 parameters: N=16384, r=8, p=8
40    let params = Params::new(14, 8, 8, 64) // log2(16384) = 14
41        .map_err(|e| ExportError::EncryptionFailed(format!("Scrypt params error: {}", e)))?;
42    
43    let mut derived_key = [0u8; 64];
44    scrypt(password.as_bytes(), &address_hash, &params, &mut derived_key)
45        .map_err(|e| ExportError::EncryptionFailed(format!("Scrypt failed: {}", e)))?;
46    
47    let derived_half1 = &derived_key[0..32];
48    let derived_half2 = &derived_key[32..64];
49    
50    // XOR private key with derived_half1
51    let key_bytes = key.to_bytes();
52    let mut xored = [0u8; 32];
53    for i in 0..32 {
54        xored[i] = key_bytes[i] ^ derived_half1[i];
55    }
56    
57    // AES-256-ECB encrypt
58    let cipher = Aes256::new_from_slice(derived_half2)
59        .map_err(|e| ExportError::EncryptionFailed(format!("AES init failed: {}", e)))?;
60    
61    let mut encrypted = [0u8; 32];
62    
63    // Encrypt first half
64    let mut block1: [u8; 16] = xored[0..16].try_into().unwrap();
65    cipher.encrypt_block((&mut block1).into());
66    encrypted[0..16].copy_from_slice(&block1);
67    
68    // Encrypt second half
69    let mut block2: [u8; 16] = xored[16..32].try_into().unwrap();
70    cipher.encrypt_block((&mut block2).into());
71    encrypted[16..32].copy_from_slice(&block2);
72    
73    // Build payload
74    // Prefix: 0x01 0x42 (non-EC-multiply)
75    // Flag: 0xC0 (compressed) or 0x00 (uncompressed)
76    let flag = if compressed { 0xE0 } else { 0xC0 };
77    
78    let mut payload = Vec::with_capacity(39);
79    payload.push(0x01);
80    payload.push(0x42);
81    payload.push(flag);
82    payload.extend_from_slice(&address_hash);
83    payload.extend_from_slice(&encrypted);
84    
85    // Add checksum
86    let check1 = Sha256::digest(&payload);
87    let check2 = Sha256::digest(check1);
88    payload.extend_from_slice(&check2[0..4]);
89    
90    // Base58 encode
91    Ok(bs58::encode(payload).into_string())
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    
98    #[test]
99    fn test_export_bip38_format() {
100        let key = PrivateKey::random();
101        let encrypted = export_bip38(&key, "testpassword", true).unwrap();
102        
103        assert!(encrypted.starts_with("6P"));
104        assert_eq!(encrypted.len(), 58);
105    }
106    
107    // Note: Full roundtrip test requires rustywallet-import
108    // which is in dev-dependencies
109}