upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-04-18 07:39:27 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-06-11 12:33:09 +0100
commit7c6a5ab4c5e7a81c7442061029b9230748a6639d (patch)
treeaea6567080857b629c826c7921314a6ce323a6db /src
parent3b4f0b0eee124133b641d6770704c368712f3dff (diff)
refactor: bump rust-nostr to v0.30 use ncryptsec
bump nostr and nostr-sdk packages and also in test_utils remove custom ncryptsec implementation and use the newly added implementation nip49 version in rust-nostr note a patched v0.30 is used so that log_n is exposed so that user can be warned it might take a few seconds to decrypt. this has now been merged into the library. note that this will no longer decrypt existing ncryptsec values as it is uses a longer string. this should therefore be bundled with the upcoming change to storing nsec and ncryptsec in git config.
Diffstat (limited to 'src')
-rw-r--r--src/key_handling/encryption.rs155
1 files changed, 14 insertions, 141 deletions
diff --git a/src/key_handling/encryption.rs b/src/key_handling/encryption.rs
index 54002fa..3f4ee41 100644
--- a/src/key_handling/encryption.rs
+++ b/src/key_handling/encryption.rs
@@ -1,16 +1,7 @@
1use std::str::FromStr; 1use anyhow::Result;
2
3use anyhow::{anyhow, bail, ensure, Context, Result};
4use chacha20poly1305::{
5 aead::{rand_core::RngCore, Aead, AeadCore, KeyInit, OsRng, Payload},
6 XChaCha20Poly1305,
7};
8#[cfg(test)] 2#[cfg(test)]
9use mockall::*; 3use mockall::*;
10use nostr::{prelude::*, Keys}; 4use nostr::{prelude::*, Keys};
11use nostr_sdk::bech32::{self, FromBase32, ToBase32};
12use rand::{distributions::Alphanumeric, thread_rng, Rng};
13use zeroize::Zeroize;
14 5
15#[derive(Default)] 6#[derive(Default)]
16pub struct Encryptor; 7pub struct Encryptor;
@@ -20,143 +11,38 @@ pub trait EncryptDecrypt {
20 /// requires less CPU time if the password is long 11 /// requires less CPU time if the password is long
21 fn encrypt_key(&self, keys: &Keys, password: &str) -> Result<String>; 12 fn encrypt_key(&self, keys: &Keys, password: &str) -> Result<String>;
22 fn decrypt_key(&self, encrypted_key: &str, password: &str) -> Result<Keys>; 13 fn decrypt_key(&self, encrypted_key: &str, password: &str) -> Result<Keys>;
23 /// generates a long random string
24 fn random_token(&self) -> String;
25} 14}
26 15
27/// approach and code adapted from nostr gossip client 16/// approach and code adapted from nostr gossip client
28impl EncryptDecrypt for Encryptor { 17impl EncryptDecrypt for Encryptor {
29 fn encrypt_key(&self, keys: &Keys, password: &str) -> Result<String> { 18 fn encrypt_key(&self, keys: &Keys, password: &str) -> Result<String> {
30 // Generate a random 16-byte salt
31 let salt = {
32 let mut salt: [u8; 16] = [0; 16];
33 OsRng.fill_bytes(&mut salt);
34 salt
35 };
36
37 let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
38
39 let log2_rounds: u8 = if password.len() > 20 { 19 let log2_rounds: u8 = if password.len() > 20 {
40 // we have enough of entropy - no need to spend CPU time adding much more 20 // we have enough of entropy - no need to spend CPU time adding much more
41 1 21 1
42 } else { 22 } else {
23 println!("this may take a few seconds...");
43 // default (scrypt::Params::RECOMMENDED_LOG_N) is 17 but 30s is too long to wait 24 // default (scrypt::Params::RECOMMENDED_LOG_N) is 17 but 30s is too long to wait
44 15 25 15
45 }; 26 };
46 27 Ok(nostr::nips::nip49::EncryptedSecretKey::new(
47 let associated_data: Vec<u8> = vec![1]; 28 keys.secret_key()?,
48 29 password,
49 let ciphertext = { 30 log2_rounds,
50 let cipher = { 31 KeySecurity::Medium,
51 let symmetric_key = password_to_key(password, &salt, log2_rounds) 32 )?
52 .context("failed create encryption key from password")?; 33 .to_bech32()?)
53 XChaCha20Poly1305::new((&symmetric_key).into())
54 };
55 cipher
56 .encrypt(
57 &nonce,
58 Payload {
59 msg: keys
60 .secret_key()
61 .context(
62 "supplied key should reveal secret key. Is this a public key only?",
63 )?
64 .display_secret()
65 .to_string()
66 .as_bytes(),
67 aad: &associated_data,
68 },
69 )
70 .map_err(|_| anyhow!("ChaChaPoly1305 failed to encrypt nsec with password"))?
71 };
72 // Combine salt, IV and ciphertext
73 let mut concatenation: Vec<u8> = Vec::new();
74 concatenation.push(0x1); // 1 byte version number
75 concatenation.push(log2_rounds); // 1 byte for scrypt N (rounds)
76 concatenation.extend(salt); // 16 bytes of salt
77 concatenation.extend(nonce); // 24 bytes of nonce
78 concatenation.extend(associated_data); // 1 byte of key security
79 concatenation.extend(ciphertext); // 48 bytes of ciphertext expected
80 // Total length is 91 = 1 + 1 + 16 + 24 + 1 + 48
81
82 bech32::encode(
83 "ncryptsec",
84 concatenation.to_base32(),
85 bech32::Variant::Bech32,
86 )
87 .context("encrypted nsec failed to encode")
88 } 34 }
89 35
90 fn decrypt_key(&self, encrypted_key: &str, password: &str) -> Result<nostr::Keys> { 36 fn decrypt_key(&self, encrypted_key: &str, password: &str) -> Result<nostr::Keys> {
91 let data = 37 let encrypted_key = nostr::nips::nip49::EncryptedSecretKey::from_bech32(encrypted_key)?;
92 bech32::decode(encrypted_key).context("failed to decode encrypted key as bech32")?; 38 // to request that log_n gets exposed
93 if data.0 != "ncryptsec" { 39 if encrypted_key.log_n() > 14 {
94 bail!("encrypted key is in the wrong format - it doesnt start with ncryptsec"); 40 println!("this may take a few seconds...");
95 }
96 let concatenation = Vec::<u8>::from_base32(&data.1)
97 .context("failed to convert bech32::decode output to Vec<u8>")?;
98
99 // Break into parts
100 let version: u8 = concatenation[0];
101 ensure!(version == 0x1, "encryption version is incorrect");
102 let log2_rounds: u8 = concatenation[1];
103 let salt: [u8; 16] = concatenation[2..2 + 16].try_into()?;
104 let nonce = &concatenation[2 + 16..2 + 16 + 24];
105 let associated_data = &concatenation[(2 + 16 + 24)..=(2 + 16 + 24)];
106 let ciphertext = &concatenation[2 + 16 + 24 + 1..];
107
108 let cipher = {
109 let symmetric_key = password_to_key(password, &salt, log2_rounds)?;
110 XChaCha20Poly1305::new((&symmetric_key).into())
111 };
112
113 let payload = Payload {
114 msg: ciphertext,
115 aad: associated_data,
116 };
117
118 let mut inner_secret = cipher
119 .decrypt(nonce.into(), payload)
120 .map_err(|_| anyhow!("failed to decrypt"))?;
121
122 if associated_data.is_empty() {
123 bail!("invalid encrypted key");
124 } 41 }
125 42 Ok(nostr::Keys::new(encrypted_key.to_secret_key(password)?))
126 let key =
127 Keys::from_str(std::str::from_utf8(&inner_secret).context("inner secret is not [u8]")?)
128 .context(
129 "incorrect password. Key decrypted with password did not produce a valid nsec.",
130 )?;
131
132 inner_secret.zeroize();
133
134 Ok(key)
135 }
136
137 fn random_token(&self) -> String {
138 thread_rng()
139 .sample_iter(&Alphanumeric)
140 .take(32)
141 .map(char::from)
142 .collect()
143 } 43 }
144} 44}
145 45
146/// uses scrypt to stretch password into key
147fn password_to_key(password: &str, salt: &[u8; 16], log_n: u8) -> Result<[u8; 32]> {
148 let params = scrypt::Params::new(log_n, 8, 1, 32)
149 .context("scrypt failed to generate params to stretch password")?;
150 let mut key: [u8; 32] = [0; 32];
151 if log_n > 14 {
152 println!("this may take a few seconds...");
153 }
154
155 scrypt::scrypt(password.as_bytes(), salt, &params, &mut key)
156 .context("scrypt failed to stretch password")?;
157 Ok(key)
158}
159
160#[cfg(test)] 46#[cfg(test)]
161mod tests { 47mod tests {
162 use test_utils::*; 48 use test_utils::*;
@@ -235,17 +121,4 @@ mod tests {
235 ); 121 );
236 Ok(()) 122 Ok(())
237 } 123 }
238
239 #[test]
240 fn password_to_key_returns_ok_with_standard_password() {
241 let salt = {
242 let mut salt: [u8; 16] = [0; 16];
243 OsRng.fill_bytes(&mut salt);
244 salt
245 };
246
247 let log2_rounds: u8 = 1;
248
249 assert!(password_to_key(TEST_PASSWORD, &salt, log2_rounds).is_ok());
250 }
251} 124}