upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/key_handling/encryption.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/key_handling/encryption.rs')
-rw-r--r--src/key_handling/encryption.rs247
1 files changed, 247 insertions, 0 deletions
diff --git a/src/key_handling/encryption.rs b/src/key_handling/encryption.rs
new file mode 100644
index 0000000..0ef7f69
--- /dev/null
+++ b/src/key_handling/encryption.rs
@@ -0,0 +1,247 @@
1use anyhow::{anyhow, bail, ensure, Context, Result};
2use chacha20poly1305::{
3 aead::{rand_core::RngCore, Aead, AeadCore, KeyInit, OsRng, Payload},
4 XChaCha20Poly1305,
5};
6#[cfg(test)]
7use mockall::*;
8use nostr::{prelude::*, Keys};
9use rand::{distributions::Alphanumeric, thread_rng, Rng};
10use zeroize::Zeroize;
11
12#[derive(Default)]
13pub struct Encryptor;
14
15#[cfg_attr(test, automock)]
16pub trait EncryptDecrypt {
17 /// requires less CPU time if the password is long
18 fn encrypt_key(&self, keys: &Keys, password: &str) -> Result<String>;
19 fn decrypt_key(&self, encrypted_key: &str, password: &str) -> Result<Keys>;
20 /// generates a long random string
21 fn random_token(&self) -> String;
22}
23
24/// approach and code adapted from nostr gossip client
25impl EncryptDecrypt for Encryptor {
26 fn encrypt_key(&self, keys: &Keys, password: &str) -> Result<String> {
27 // Generate a random 16-byte salt
28 let salt = {
29 let mut salt: [u8; 16] = [0; 16];
30 OsRng.fill_bytes(&mut salt);
31 salt
32 };
33
34 let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
35
36 let log2_rounds: u8 = if password.len() > 20 {
37 // we have enough of entropy - no need to spend CPU time adding much more
38 1
39 } else {
40 // default (scrypt::Params::RECOMMENDED_LOG_N) is 17 but 30s is too long to wait
41 15
42 };
43
44 let associated_data: Vec<u8> = vec![1];
45
46 let ciphertext = {
47 let cipher = {
48 let symmetric_key = password_to_key(password, &salt, log2_rounds)
49 .context("failed create encryption key from password")?;
50 XChaCha20Poly1305::new((&symmetric_key).into())
51 };
52 cipher
53 .encrypt(
54 &nonce,
55 Payload {
56 msg: keys
57 .secret_key()
58 .context(
59 "supplied key should reveal secret key. Is this a public key only?",
60 )?
61 .display_secret()
62 .to_string()
63 .as_bytes(),
64 aad: &associated_data,
65 },
66 )
67 .map_err(|_| anyhow!("ChaChaPoly1305 failed to encrypt nsec with password"))?
68 };
69 // Combine salt, IV and ciphertext
70 let mut concatenation: Vec<u8> = Vec::new();
71 concatenation.push(0x1); // 1 byte version number
72 concatenation.push(log2_rounds); // 1 byte for scrypt N (rounds)
73 concatenation.extend(salt); // 16 bytes of salt
74 concatenation.extend(nonce); // 24 bytes of nonce
75 concatenation.extend(associated_data); // 1 byte of key security
76 concatenation.extend(ciphertext); // 48 bytes of ciphertext expected
77 // Total length is 91 = 1 + 1 + 16 + 24 + 1 + 48
78
79 bech32::encode(
80 "ncryptsec",
81 concatenation.to_base32(),
82 bech32::Variant::Bech32,
83 )
84 .context("encrypted nsec failed to encode")
85 }
86
87 fn decrypt_key(&self, encrypted_key: &str, password: &str) -> Result<nostr::Keys> {
88 let data =
89 bech32::decode(encrypted_key).context("failed to decode encrypted key as bech32")?;
90 if data.0 != "ncryptsec" {
91 bail!("encrypted key is in the wrong format - it doesnt start with ncryptsec");
92 }
93 let concatenation = Vec::<u8>::from_base32(&data.1)
94 .context("failed to convert bech32::decode output to Vec<u8>")?;
95
96 // Break into parts
97 let version: u8 = concatenation[0];
98 ensure!(version == 0x1, "encryption version is incorrect");
99 let log2_rounds: u8 = concatenation[1];
100 let salt: [u8; 16] = concatenation[2..2 + 16].try_into()?;
101 let nonce = &concatenation[2 + 16..2 + 16 + 24];
102 let associated_data = &concatenation[(2 + 16 + 24)..=(2 + 16 + 24)];
103 let ciphertext = &concatenation[2 + 16 + 24 + 1..];
104
105 let cipher = {
106 let symmetric_key = password_to_key(password, &salt, log2_rounds)?;
107 XChaCha20Poly1305::new((&symmetric_key).into())
108 };
109
110 let payload = Payload {
111 msg: ciphertext,
112 aad: associated_data,
113 };
114
115 let mut inner_secret = cipher
116 .decrypt(nonce.into(), payload)
117 .map_err(|_| anyhow!("failed to decrypt"))?;
118
119 if associated_data.is_empty() {
120 bail!("invalid encrypted key");
121 }
122
123 let key = Keys::from_sk_str(
124 std::str::from_utf8(&inner_secret).context("inner secret is not [u8]")?,
125 )
126 .context("incorrect password. Key decrypted with password did not produce a valid nsec.")?;
127
128 inner_secret.zeroize();
129
130 Ok(key)
131 }
132
133 fn random_token(&self) -> String {
134 thread_rng()
135 .sample_iter(&Alphanumeric)
136 .take(32)
137 .map(char::from)
138 .collect()
139 }
140}
141
142/// uses scrypt to stretch password into key
143fn password_to_key(password: &str, salt: &[u8; 16], log_n: u8) -> Result<[u8; 32]> {
144 let params = scrypt::Params::new(log_n, 8, 1, 32)
145 .context("scrypt failed to generate params to stretch password")?;
146 let mut key: [u8; 32] = [0; 32];
147 if log_n > 14 {
148 println!("this may take a few seconds...");
149 }
150
151 scrypt::scrypt(password.as_bytes(), salt, &params, &mut key)
152 .context("scrypt failed to stretch password")?;
153 Ok(key)
154}
155
156#[cfg(test)]
157mod tests {
158 use test_utils::*;
159
160 use super::*;
161
162 #[test]
163 fn encrypt_key_produces_string_prefixed_with() -> Result<()> {
164 let s = Encryptor.encrypt_key(&nostr::Keys::generate(), TEST_PASSWORD)?;
165 assert!(s.starts_with("ncryptsec"));
166 Ok(())
167 }
168
169 #[test]
170 // ensures password encryption hasn't changed
171 fn decrypts_with_strong_password_from_reference_string() -> Result<()> {
172 let encryptor = Encryptor;
173 let decrypted_key = encryptor.decrypt_key(TEST_KEY_1_ENCRYPTED, TEST_PASSWORD)?;
174
175 assert_eq!(
176 format!(
177 "{}",
178 TEST_KEY_1_KEYS.secret_key().unwrap().to_bech32().unwrap()
179 ),
180 format!(
181 "{}",
182 decrypted_key.secret_key().unwrap().to_bech32().unwrap()
183 ),
184 );
185 Ok(())
186 }
187
188 #[test]
189 // ensures password encryption hasn't changed
190 fn decrypts_with_weak_password_from_reference_string() -> Result<()> {
191 let encryptor = Encryptor;
192 let decrypted_key = encryptor.decrypt_key(TEST_KEY_1_ENCRYPTED_WEAK, TEST_WEAK_PASSWORD)?;
193
194 assert_eq!(
195 format!(
196 "{}",
197 TEST_KEY_1_KEYS.secret_key().unwrap().to_bech32().unwrap()
198 ),
199 format!(
200 "{}",
201 decrypted_key.secret_key().unwrap().to_bech32().unwrap()
202 ),
203 );
204 Ok(())
205 }
206
207 #[test]
208 fn decrypts_key_encrypted_using_encrypt_key() -> Result<()> {
209 let encryptor = Encryptor;
210 let key = nostr::Keys::generate();
211 let s = encryptor.encrypt_key(&key, TEST_PASSWORD)?;
212 let newkey = encryptor.decrypt_key(s.as_str(), TEST_PASSWORD)?;
213
214 assert_eq!(
215 format!("{}", key.secret_key().unwrap().to_bech32().unwrap()),
216 format!("{}", newkey.secret_key().unwrap().to_bech32().unwrap()),
217 );
218 Ok(())
219 }
220
221 #[test]
222 fn decrypt_key_successfully_decrypts_key_encrypted_using_encrypt_key() -> Result<()> {
223 let encryptor = Encryptor;
224 let key = nostr::Keys::generate();
225 let s = encryptor.encrypt_key(&key, TEST_PASSWORD)?;
226 let newkey = encryptor.decrypt_key(s.as_str(), TEST_PASSWORD)?;
227
228 assert_eq!(
229 format!("{}", key.secret_key().unwrap().to_bech32().unwrap()),
230 format!("{}", newkey.secret_key().unwrap().to_bech32().unwrap()),
231 );
232 Ok(())
233 }
234
235 #[test]
236 fn password_to_key_returns_ok_with_standard_password() {
237 let salt = {
238 let mut salt: [u8; 16] = [0; 16];
239 OsRng.fill_bytes(&mut salt);
240 salt
241 };
242
243 let log2_rounds: u8 = 1;
244
245 assert!(password_to_key(TEST_PASSWORD, &salt, log2_rounds).is_ok());
246 }
247}