gnostr_types/types/private_key/
encrypted_private_key.rs1use 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
21const 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#[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 pub fn from_bech32_string(s: String) -> EncryptedPrivateKey {
41 EncryptedPrivateKey(s)
42 }
43
44 pub fn as_bech32_string(&self) -> String {
46 self.0.clone()
47 }
48
49 pub fn decrypt(&self, password: &str) -> Result<PrivateKey, Error> {
54 PrivateKey::import_encrypted(self, password)
55 }
56
57 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) }
99 }
100}
101
102impl PrivateKey {
103 pub fn export_encrypted(
114 &self,
115 password: &str,
116 log2_rounds: u8,
117 ) -> Result<EncryptedPrivateKey, Error> {
118 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 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 let mut concatenation: Vec<u8> = Vec::new();
162 concatenation.push(0x2); concatenation.push(log2_rounds); concatenation.extend(salt); concatenation.extend(nonce); concatenation.extend(associated_data); concatenation.extend(ciphertext); Ok(EncryptedPrivateKey(bech32::encode::<bech32::Bech32>(
172 *crate::HRP_NCRYPTSEC,
173 &concatenation,
174 )?))
175 }
176
177 pub fn import_encrypted(
184 encrypted: &EncryptedPrivateKey,
185 password: &str,
186 ) -> Result<PrivateKey, Error> {
187 if encrypted.0.starts_with("ncryptsec1") {
188 Self::import_encrypted_bech32(encrypted, password)
190 } else {
191 Self::import_encrypted_base64(encrypted, password)
193 }
194 }
195
196 fn import_encrypted_bech32(
198 encrypted: &EncryptedPrivateKey,
199 password: &str,
200 ) -> Result<PrivateKey, Error> {
201 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 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 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 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)?; 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 fn import_encrypted_v1(concatenation: Vec<u8>, password: &str) -> Result<PrivateKey, Error> {
279 let salt: [u8; 16] = concatenation[..16].try_into()?;
281 let iv: [u8; 16] = concatenation[16..32].try_into()?;
282 let ciphertext = &concatenation[32..]; let key = Self::password_to_key_v1(password, &salt, V1_HMAC_ROUNDS)?;
285
286 let mut plaintext = cbc::Decryptor::<aes::Aes256>::new(&key.into(), &iv.into())
288 .decrypt_padded_vec_mut::<Pkcs7>(ciphertext)?; if plaintext.len() != 44 {
290 return Err(Error::InvalidEncryptedPrivateKey);
291 }
293
294 if plaintext[plaintext.len() - 12..plaintext.len() - 1] != V1_CHECK_VALUE {
296 return Err(Error::WrongDecryptionPassword);
297 }
298
299 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 plaintext.zeroize();
308
309 Ok(output)
310 }
311
312 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 return Err(Error::InvalidEncryptedPrivateKey);
322 }
323
324 let iv: [u8; 16] = iv_plus_ciphertext[..16].try_into()?;
326 let ciphertext = &iv_plus_ciphertext[16..]; let mut pt = cbc::Decryptor::<aes::Aes256>::new(&key.into(), &iv.into())
330 .decrypt_padded_vec_mut::<Pkcs7>(ciphertext)?; if pt[pt.len() - 12..pt.len() - 1] != V1_CHECK_VALUE {
334 return Err(Error::WrongDecryptionPassword);
335 }
336
337 let ks = KeySecurity::try_from(pt[pt.len() - 1])?;
339 let output = PrivateKey(secp256k1::SecretKey::from_slice(&pt[..pt.len() - 12])?, ks);
340
341 pt.zeroize();
343
344 Ok(output)
345 }
346
347 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 fn password_to_key_v2(password: &str, salt: &[u8; 16], log_n: u8) -> Result<[u8; 32], Error> {
356 let password = password.nfkc().collect::<String>();
358
359 let params = match scrypt::Params::new(log_n, 8, 1, 32) {
360 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, ¶ms, &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 let exported = pk.export_encrypted("secret", 13).unwrap();
381 println!("{exported}");
382 let imported_pk = PrivateKey::import_encrypted(&exported, "secret").unwrap();
383
384 assert_eq!(pk.public_key(), imported_pk.public_key());
386
387 assert_eq!(pk.key_security(), KeySecurity::Medium)
389 }
390
391 #[test]
392 fn test_import_old_formats() {
393 let decrypted = "a28129ab0b70c8d5e75aaf510ec00bff47fde7ca4ab9e3d9315c77edc86f037f";
394
395 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 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 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 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 let encrypted = EncryptedPrivateKey("ncryptsec1q9hnc06cs5tuk7znrxmetj4q9q2mjtccg995kp86jf3dsp3jykv4fhak730wds4s0mja6c9v2fvdr5dhzrstds8yks5j9ukvh25ydg6xtve6qvp90j0c8a2s5tv4xn7kvulg88".to_owned());
427 assert_eq!(
428 encrypted.decrypt("nostr").unwrap().as_hex_string(),
429 decrypted
430 );
431
432 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 let password1: [u8; 11] = [
445 0xE2, 0x84, 0xAB, 0xE2, 0x84, 0xA6, 0xE1, 0xBA, 0x9B, 0xCC, 0xA3,
446 ];
447
448 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