gnostr_types/types/private_key/
encrypted_private_key.rs

1use super::{KeySecurity, PrivateKey};
2use crate::Error;
3use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
4use base64::Engine;
5use chacha20poly1305::{
6    aead::{Aead, AeadCore, KeyInit, Payload},
7    XChaCha20Poly1305,
8};
9use derive_more::Display;
10use hmac::Hmac;
11use pbkdf2::pbkdf2;
12use rand_core::{OsRng, RngCore};
13use serde::{Deserialize, Serialize};
14use sha2::Sha256;
15#[cfg(feature = "speedy")]
16use speedy::{Readable, Writable};
17use std::ops::Deref;
18use unicode_normalization::UnicodeNormalization;
19use zeroize::Zeroize;
20
21// This allows us to detect bad decryptions with wrong passwords.
22const V1_CHECK_VALUE: [u8; 11] = [15, 91, 241, 148, 90, 143, 101, 12, 172, 255, 103];
23const V1_HMAC_ROUNDS: u32 = 100_000;
24
25/// This is an encrypted private key (the string inside is the bech32 ncryptsec string)
26#[derive(Clone, Debug, Display, Serialize, Deserialize)]
27#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
28pub struct EncryptedPrivateKey(pub String);
29
30impl Deref for EncryptedPrivateKey {
31    type Target = String;
32
33    fn deref(&self) -> &String {
34        &self.0
35    }
36}
37
38impl EncryptedPrivateKey {
39    /// Create from a bech32 string (this just type wraps as the internal stringly already is one)
40    pub fn from_bech32_string(s: String) -> EncryptedPrivateKey {
41        EncryptedPrivateKey(s)
42    }
43
44    /// only correct for version 1 and onwards
45    pub fn as_bech32_string(&self) -> String {
46        self.0.clone()
47    }
48
49    /// Decrypt into a Private Key with a passphrase.
50    ///
51    /// We recommend you zeroize() the password you pass in after you are
52    /// done with it.
53    pub fn decrypt(&self, password: &str) -> Result<PrivateKey, Error> {
54        PrivateKey::import_encrypted(self, password)
55    }
56
57    /// Version
58    ///
59    /// Version -1:
60    ///    PBKDF = pbkdf2-hmac-sha256 ( salt = "nostr", rounds = 4096 )
61    ///    inside = concat(private_key, 15 specified bytes, key_security_byte)
62    ///    encrypt = AES-256-CBC with random IV
63    ///    compose = iv + ciphertext
64    ///    encode = base64
65    /// Version 0:
66    ///    PBKDF = pbkdf2-hmac-sha256 ( salt = concat(0x1, 15 random bytes), rounds = 100000 )
67    ///    inside = concat(private_key, 15 specified bytes, key_security_byte)
68    ///    encrypt = AES-256-CBC with random IV
69    ///    compose = salt + iv + ciphertext
70    ///    encode = base64
71    /// Version 1:
72    ///    PBKDF = pbkdf2-hmac-sha256 ( salt = concat(0x1, 15 random bytes), rounds = 100000 )
73    ///    inside = concat(private_key, 15 specified bytes, key_security_byte)
74    ///    encrypt = AES-256-CBC with random IV
75    ///    compose = salt + iv + ciphertext
76    ///    encode = bech32('ncryptsec')
77    /// Version 2:
78    ///    PBKDF = scrypt ( salt = 16 random bytes, log_n = user choice, r = 8, p = 1)
79    ///    inside = private_key
80    ///    associated_data = key_security_byte
81    ///    encrypt = XChaCha20-Poly1305
82    ///    compose = concat (0x2, log_n, salt, nonce, associated_data, ciphertext)
83    ///    encode = bech32('ncryptsec')
84    pub fn version(&self) -> Result<i8, Error> {
85        if self.0.starts_with("ncryptsec1") {
86            let data = bech32::decode(&self.0)?;
87            if data.0 != *crate::HRP_NCRYPTSEC {
88                return Err(Error::WrongBech32(
89                    crate::HRP_NCRYPTSEC.to_lowercase(),
90                    data.0.to_lowercase(),
91                ));
92            }
93            Ok(data.1[0] as i8)
94        } else if self.0.len() == 64 {
95            Ok(-1)
96        } else {
97            Ok(0) // base64 variant of v1
98        }
99    }
100}
101
102impl PrivateKey {
103    /// Export in a (non-portable) encrypted form. This does not downgrade
104    /// the security of the key, but you are responsible to keep it encrypted.
105    /// You should not attempt to decrypt it, only use `import_encrypted()` on
106    /// it, or something similar in another library/client which also respects key
107    /// security.
108    ///
109    /// This currently exports into EncryptedPrivateKey version 2.
110    ///
111    /// We recommend you zeroize() the password you pass in after you are
112    /// done with it.
113    pub fn export_encrypted(
114        &self,
115        password: &str,
116        log2_rounds: u8,
117    ) -> Result<EncryptedPrivateKey, Error> {
118        // Generate a random 16-byte salt
119        let salt = {
120            let mut salt: [u8; 16] = [0; 16];
121            OsRng.fill_bytes(&mut salt);
122            salt
123        };
124
125        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
126
127        let associated_data: Vec<u8> = {
128            let key_security: u8 = match self.1 {
129                KeySecurity::Weak => 0,
130                KeySecurity::Medium => 1,
131                KeySecurity::NotTracked => 2,
132            };
133            vec![key_security]
134        };
135
136        let ciphertext = {
137            let cipher = {
138                let symmetric_key = Self::password_to_key_v2(password, &salt, log2_rounds)?;
139                XChaCha20Poly1305::new((&symmetric_key).into())
140            };
141
142            // The inner secret. We don't have to drop this because we are encrypting-in-place
143            let mut inner_secret: Vec<u8> = self.0.secret_bytes().to_vec();
144
145            let payload = Payload {
146                msg: &inner_secret,
147                aad: &associated_data,
148            };
149
150            let ciphertext = match cipher.encrypt(&nonce, payload) {
151                Ok(c) => c,
152                Err(_) => return Err(Error::PrivateKeyEncryption),
153            };
154
155            inner_secret.zeroize();
156
157            ciphertext
158        };
159
160        // Combine salt, IV and ciphertext
161        let mut concatenation: Vec<u8> = Vec::new();
162        concatenation.push(0x2); // 1 byte version number
163        concatenation.push(log2_rounds); // 1 byte for scrypt N (rounds)
164        concatenation.extend(salt); // 16 bytes of salt
165        concatenation.extend(nonce); // 24 bytes of nonce
166        concatenation.extend(associated_data); // 1 byte of key security
167        concatenation.extend(ciphertext); // 48 bytes of ciphertext expected
168                                          // Total length is 91 = 1 + 1 + 16 + 24 + 1 + 48
169
170        // bech32 encode
171        Ok(EncryptedPrivateKey(bech32::encode::<bech32::Bech32>(
172            *crate::HRP_NCRYPTSEC,
173            &concatenation,
174        )?))
175    }
176
177    /// Import an encrypted private key which was exported with `export_encrypted()`.
178    ///
179    /// We recommend you zeroize() the password you pass in after you are
180    /// done with it.
181    ///
182    /// This is backwards-compatible with keys that were exported with older code.
183    pub fn import_encrypted(
184        encrypted: &EncryptedPrivateKey,
185        password: &str,
186    ) -> Result<PrivateKey, Error> {
187        if encrypted.0.starts_with("ncryptsec1") {
188            // Versioned
189            Self::import_encrypted_bech32(encrypted, password)
190        } else {
191            // Pre-versioned, deprecated
192            Self::import_encrypted_base64(encrypted, password)
193        }
194    }
195
196    // Current
197    fn import_encrypted_bech32(
198        encrypted: &EncryptedPrivateKey,
199        password: &str,
200    ) -> Result<PrivateKey, Error> {
201        // bech32 decode
202        let data = bech32::decode(&encrypted.0)?;
203        if data.0 != *crate::HRP_NCRYPTSEC {
204            return Err(Error::WrongBech32(
205                crate::HRP_NCRYPTSEC.to_lowercase(),
206                data.0.to_lowercase(),
207            ));
208        }
209        match data.1[0] {
210            1 => Self::import_encrypted_v1(data.1, password),
211            2 => Self::import_encrypted_v2(data.1, password),
212            _ => Err(Error::InvalidEncryptedPrivateKey),
213        }
214    }
215
216    // current
217    fn import_encrypted_v2(concatenation: Vec<u8>, password: &str) -> Result<PrivateKey, Error> {
218        if concatenation.len() < 91 {
219            return Err(Error::InvalidEncryptedPrivateKey);
220        }
221
222        // Break into parts
223        let version: u8 = concatenation[0];
224        assert_eq!(version, 2);
225        let log2_rounds: u8 = concatenation[1];
226        let salt: [u8; 16] = concatenation[2..2 + 16].try_into()?;
227        let nonce = &concatenation[2 + 16..2 + 16 + 24];
228        let associated_data = &concatenation[2 + 16 + 24..2 + 16 + 24 + 1];
229        let ciphertext = &concatenation[2 + 16 + 24 + 1..];
230
231        let cipher = {
232            let symmetric_key = Self::password_to_key_v2(password, &salt, log2_rounds)?;
233            XChaCha20Poly1305::new((&symmetric_key).into())
234        };
235
236        let payload = Payload {
237            msg: ciphertext,
238            aad: associated_data,
239        };
240
241        let mut inner_secret = match cipher.decrypt(nonce.into(), payload) {
242            Ok(is) => is,
243            Err(_) => return Err(Error::PrivateKeyEncryption),
244        };
245
246        if associated_data.is_empty() {
247            return Err(Error::InvalidEncryptedPrivateKey);
248        }
249        let key_security = match associated_data[0] {
250            0 => KeySecurity::Weak,
251            1 => KeySecurity::Medium,
252            2 => KeySecurity::NotTracked,
253            _ => return Err(Error::InvalidEncryptedPrivateKey),
254        };
255
256        let signing_key = secp256k1::SecretKey::from_slice(&inner_secret)?;
257        inner_secret.zeroize();
258
259        Ok(PrivateKey(signing_key, key_security))
260    }
261
262    // deprecated
263    fn import_encrypted_base64(
264        encrypted: &EncryptedPrivateKey,
265        password: &str,
266    ) -> Result<PrivateKey, Error> {
267        let concatenation = base64::engine::general_purpose::STANDARD.decode(&encrypted.0)?; // 64 or 80 bytes
268        if concatenation.len() == 64 {
269            Self::import_encrypted_pre_v1(concatenation, password)
270        } else if concatenation.len() == 80 {
271            Self::import_encrypted_v1(concatenation, password)
272        } else {
273            Err(Error::InvalidEncryptedPrivateKey)
274        }
275    }
276
277    // deprecated
278    fn import_encrypted_v1(concatenation: Vec<u8>, password: &str) -> Result<PrivateKey, Error> {
279        // Break into parts
280        let salt: [u8; 16] = concatenation[..16].try_into()?;
281        let iv: [u8; 16] = concatenation[16..32].try_into()?;
282        let ciphertext = &concatenation[32..]; // 48 bytes
283
284        let key = Self::password_to_key_v1(password, &salt, V1_HMAC_ROUNDS)?;
285
286        // AES-256-CBC decrypt
287        let mut plaintext = cbc::Decryptor::<aes::Aes256>::new(&key.into(), &iv.into())
288            .decrypt_padded_vec_mut::<Pkcs7>(ciphertext)?; // 44 bytes
289        if plaintext.len() != 44 {
290            return Err(Error::InvalidEncryptedPrivateKey);
291            //return Err(Error::AssertionFailed("Import encrypted plaintext len != 44".to_owned()));
292        }
293
294        // Verify the check value
295        if plaintext[plaintext.len() - 12..plaintext.len() - 1] != V1_CHECK_VALUE {
296            return Err(Error::WrongDecryptionPassword);
297        }
298
299        // Get the key security
300        let ks = KeySecurity::try_from(plaintext[plaintext.len() - 1])?;
301        let output = PrivateKey(
302            secp256k1::SecretKey::from_slice(&plaintext[..plaintext.len() - 12])?,
303            ks,
304        );
305
306        // Here we zeroize plaintext:
307        plaintext.zeroize();
308
309        Ok(output)
310    }
311
312    // deprecated
313    fn import_encrypted_pre_v1(
314        iv_plus_ciphertext: Vec<u8>,
315        password: &str,
316    ) -> Result<PrivateKey, Error> {
317        let key = Self::password_to_key_v1(password, b"nostr", 4096)?;
318
319        if iv_plus_ciphertext.len() < 48 {
320            // Should be 64 from padding, but we pushed in 48
321            return Err(Error::InvalidEncryptedPrivateKey);
322        }
323
324        // Pull the IV off
325        let iv: [u8; 16] = iv_plus_ciphertext[..16].try_into()?;
326        let ciphertext = &iv_plus_ciphertext[16..]; // 64 bytes
327
328        // AES-256-CBC decrypt
329        let mut pt = cbc::Decryptor::<aes::Aes256>::new(&key.into(), &iv.into())
330            .decrypt_padded_vec_mut::<Pkcs7>(ciphertext)?; // 48 bytes
331
332        // Verify the check value
333        if pt[pt.len() - 12..pt.len() - 1] != V1_CHECK_VALUE {
334            return Err(Error::WrongDecryptionPassword);
335        }
336
337        // Get the key security
338        let ks = KeySecurity::try_from(pt[pt.len() - 1])?;
339        let output = PrivateKey(secp256k1::SecretKey::from_slice(&pt[..pt.len() - 12])?, ks);
340
341        // Here we zeroize pt:
342        pt.zeroize();
343
344        Ok(output)
345    }
346
347    // Hash/Stretch password with pbkdf2 into a 32-byte (256-bit) key
348    fn password_to_key_v1(password: &str, salt: &[u8], rounds: u32) -> Result<[u8; 32], Error> {
349        let mut key: [u8; 32] = [0; 32];
350        pbkdf2::<Hmac<Sha256>>(password.as_bytes(), salt, rounds, &mut key)?;
351        Ok(key)
352    }
353
354    // Hash/Stretch password with scrypt into a 32-byte (256-bit) key
355    fn password_to_key_v2(password: &str, salt: &[u8; 16], log_n: u8) -> Result<[u8; 32], Error> {
356        // Normalize unicode (NFKC)
357        let password = password.nfkc().collect::<String>();
358
359        let params = match scrypt::Params::new(log_n, 8, 1, 32) {
360            // r=8, p=1
361            Ok(p) => p,
362            Err(_) => return Err(Error::Scrypt),
363        };
364        let mut key: [u8; 32] = [0; 32];
365        if scrypt::scrypt(password.as_bytes(), salt, &params, &mut key).is_err() {
366            return Err(Error::Scrypt);
367        }
368        Ok(key)
369    }
370}
371
372#[cfg(test)]
373mod test {
374    use super::*;
375
376    #[test]
377    fn test_export_import() {
378        let pk = PrivateKey::generate();
379        // we use a low log_n here because this is run slowly in debug mode
380        let exported = pk.export_encrypted("secret", 13).unwrap();
381        println!("{exported}");
382        let imported_pk = PrivateKey::import_encrypted(&exported, "secret").unwrap();
383
384        // Be sure the keys generate identical public keys
385        assert_eq!(pk.public_key(), imported_pk.public_key());
386
387        // Be sure the security level is still Medium
388        assert_eq!(pk.key_security(), KeySecurity::Medium)
389    }
390
391    #[test]
392    fn test_import_old_formats() {
393        let decrypted = "a28129ab0b70c8d5e75aaf510ec00bff47fde7ca4ab9e3d9315c77edc86f037f";
394
395        // pre-salt base64 (-2?)
396        let encrypted = EncryptedPrivateKey("F+VYIvTCtIZn4c6owPMZyu4Zn5DH9T5XcgZWmFG/3ma4C3PazTTQxQcIF+G+daeFlkqsZiNIh9bcmZ5pfdRPyg==".to_owned());
397        assert_eq!(
398            encrypted.decrypt("nostr").unwrap().as_hex_string(),
399            decrypted
400        );
401
402        // Version -1: post-salt base64
403        let encrypted = EncryptedPrivateKey("AZQYNwAGULWyKweTtw6WCljV+1cil8IMRxfZ7Rs3nCfwbVQBV56U6eV9ps3S1wU7ieCx6EraY9Uqdsw71TY5Yv/Ep6yGcy9m1h4YozuxWQE=".to_owned());
404        assert_eq!(
405            encrypted.decrypt("nostr").unwrap().as_hex_string(),
406            decrypted
407        );
408
409        let decrypted = "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683";
410
411        // Version -1
412        let encrypted = EncryptedPrivateKey("KlmfCiO+Tf8A/8bm/t+sXWdb1Op4IORdghC7n/9uk/vgJXIcyW7PBAx1/K834azuVmQnCzGq1pmFMF9rNPWQ9Q==".to_owned());
413        assert_eq!(
414            encrypted.decrypt("nostr").unwrap().as_hex_string(),
415            decrypted
416        );
417
418        // Version 0:
419        let encrypted = EncryptedPrivateKey("AZ/2MU2igqP0keoW08Z/rxm+/3QYcZn3oNbVhY6DSUxSDkibNp+bFN/WsRQxP7yBKwyEJVu/YSBtm2PI9DawbYOfXDqfmpA3NTPavgXwUrw=".to_owned());
420        assert_eq!(
421            encrypted.decrypt("nostr").unwrap().as_hex_string(),
422            decrypted
423        );
424
425        // Version 1:
426        let encrypted = EncryptedPrivateKey("ncryptsec1q9hnc06cs5tuk7znrxmetj4q9q2mjtccg995kp86jf3dsp3jykv4fhak730wds4s0mja6c9v2fvdr5dhzrstds8yks5j9ukvh25ydg6xtve6qvp90j0c8a2s5tv4xn7kvulg88".to_owned());
427        assert_eq!(
428            encrypted.decrypt("nostr").unwrap().as_hex_string(),
429            decrypted
430        );
431
432        // Version 2:
433        let encrypted = EncryptedPrivateKey("ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p".to_owned());
434        assert_eq!(
435            encrypted.decrypt("nostr").unwrap().as_hex_string(),
436            decrypted
437        );
438    }
439
440    #[test]
441    fn test_nfkc_unicode_normalization() {
442        // "ÅΩẛ̣"
443        // U+212B U+2126 U+1E9B U+0323
444        let password1: [u8; 11] = [
445            0xE2, 0x84, 0xAB, 0xE2, 0x84, 0xA6, 0xE1, 0xBA, 0x9B, 0xCC, 0xA3,
446        ];
447
448        // "ÅΩẛ̣"
449        // U+00C5 U+03A9 U+1E69
450        let password2: [u8; 7] = [0xC3, 0x85, 0xCE, 0xA9, 0xE1, 0xB9, 0xA9];
451
452        let password1_str = unsafe { std::str::from_utf8_unchecked(password1.as_slice()) };
453        let password2_str = unsafe { std::str::from_utf8_unchecked(password2.as_slice()) };
454
455        let password1_nfkc = password1_str.nfkc().collect::<String>();
456        assert_eq!(password1_nfkc, password2_str);
457    }
458}
459
460/*
461 * version -1 (if 64 bytes, base64 encoded)
462 *
463 *    symmetric_aes_key = pbkdf2_hmac_sha256(password,  salt="nostr", rounds=4096)
464 *    pre_encoded_encrypted_private_key = AES-256-CBC(IV=random, key=symmetric_aes_key, data=private_key)
465 *    encrypted_private_key = base64(concat(IV, pre_encoded_encrypted_private_key))
466 *
467 * version 0 (80 bytes, base64 encoded, same as v1 internally)
468 *
469 *    symmetric_aes_key = pbkdf2_hmac_sha256(password,  salt=concat(0x1, 15 random bytes), rounds=100000)
470 *    key_security_byte = 0x0 if weak, 0x1 if medium
471 *    inner_concatenation = concat(
472 *        private_key,                                         // 32 bytes
473 *        [15, 91, 241, 148, 90, 143, 101, 12, 172, 255, 103], // 11 bytes
474 *        key_security_byte                                    //  1 byte
475 *    )
476 *    pre_encoded_encrypted_private_key = AES-256-CBC(IV=random, key=symmetric_aes_key, data=private_key)
477 *    outer_concatenation = concat(IV, pre_encoded_encrypted_private_key)
478 *    encrypted_private_key = base64(outer_concatenation)
479 *
480 * version 1
481 *
482 *    salt = concat(byte(0x1), 15 random bytes)
483 *    symmetric_aes_key = pbkdf2_hmac_sha256(password, salt=salt, rounds=100,000)
484 *    key_security_byte = 0x0 if weak, 0x1 if medium
485 *    inner_concatenation = concat(
486 *        private_key,                                          // 32 bytes
487 *        [15, 91, 241, 148, 90, 143, 101, 12, 172, 255, 103],  // 11 bytes
488 *        key_security_byte                                     //  1 byte
489 *    )
490 *    pre_encoded_encrypted_private_key = AES-256-CBC(IV=random, key=symmetric_aes_key, data=private_key)
491 *    outer_concatenation = concat(salt, IV, pre_encoded_encrypted_private_key)
492 *    encrypted_private_key = bech32('ncryptsec', outer_concatenation)
493 *
494 * version 2 (scrypt, xchacha20-poly1305)
495 *
496 *    rounds = user selected power of 2
497 *    salt = 16 random bytes
498 *    symmetric_key = scrypt(password, salt=salt, r=8, p=1, N=rounds)
499 *    key_security_byte = 0x0 if weak, 0x1 if medium, 0x2 if not implemented
500 *    nonce = 12 random bytes
501 *    pre_encoded_encrypted_private_key = xchacha20-poly1305(
502 *        plaintext=private_key, nonce=nonce, key=symmetric_key,
503 *        associated_data=key_security_byte
504 *    )
505 *    version = byte(0x3)
506 *    outer_concatenation = concat(version, log2(rounds) as one byte, salt, nonce, pre_encoded_encrypted_private_key)
507 *    encrypted_private_key = bech32('ncryptsec', outer_concatenation)
508 */