diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2024-04-18 07:39:27 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2024-06-11 12:33:09 +0100 |
| commit | 7c6a5ab4c5e7a81c7442061029b9230748a6639d (patch) | |
| tree | aea6567080857b629c826c7921314a6ce323a6db /src/key_handling/encryption.rs | |
| parent | 3b4f0b0eee124133b641d6770704c368712f3dff (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/key_handling/encryption.rs')
| -rw-r--r-- | src/key_handling/encryption.rs | 155 |
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 @@ | |||
| 1 | use std::str::FromStr; | 1 | use anyhow::Result; |
| 2 | |||
| 3 | use anyhow::{anyhow, bail, ensure, Context, Result}; | ||
| 4 | use chacha20poly1305::{ | ||
| 5 | aead::{rand_core::RngCore, Aead, AeadCore, KeyInit, OsRng, Payload}, | ||
| 6 | XChaCha20Poly1305, | ||
| 7 | }; | ||
| 8 | #[cfg(test)] | 2 | #[cfg(test)] |
| 9 | use mockall::*; | 3 | use mockall::*; |
| 10 | use nostr::{prelude::*, Keys}; | 4 | use nostr::{prelude::*, Keys}; |
| 11 | use nostr_sdk::bech32::{self, FromBase32, ToBase32}; | ||
| 12 | use rand::{distributions::Alphanumeric, thread_rng, Rng}; | ||
| 13 | use zeroize::Zeroize; | ||
| 14 | 5 | ||
| 15 | #[derive(Default)] | 6 | #[derive(Default)] |
| 16 | pub struct Encryptor; | 7 | pub 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 |
| 28 | impl EncryptDecrypt for Encryptor { | 17 | impl 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 | ||
| 147 | fn 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, ¶ms, &mut key) | ||
| 156 | .context("scrypt failed to stretch password")?; | ||
| 157 | Ok(key) | ||
| 158 | } | ||
| 159 | |||
| 160 | #[cfg(test)] | 46 | #[cfg(test)] |
| 161 | mod tests { | 47 | mod 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 | } |