From 6646e825c11a6a7f35bcec7ce826dd448b7030dc Mon Sep 17 00:00:00 2001 From: EETagent Date: Thu, 27 Oct 2022 22:12:37 +0200 Subject: [PATCH 01/13] feat: age password encryption (instead of AES256) --- core/src/crypto.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/core/src/crypto.rs b/core/src/crypto.rs index 7047e55..e0562c0 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -1,3 +1,4 @@ +use std::io::{Write, Read}; use argon2::{ Argon2, PasswordHasher as ArgonPasswordHasher, PasswordVerifier as ArgonPasswordVerifier, }; @@ -39,6 +40,42 @@ pub fn hash_password(password_plaint_text: &str) -> Result Result { + let encryptor = age::Encryptor::with_user_passphrase(age::secrecy::Secret::new(key.to_owned())); + + let mut encrypt_buffer = Vec::new(); + let mut encrypt_writer = encryptor.wrap_output(&mut encrypt_buffer)?; + + encrypt_writer.write_all(password_plaint_text.as_bytes())?; + + encrypt_writer.finish()?; + + + Ok(base64::encode(encrypt_buffer)) +} + +pub fn decrypt_password( + password_encrypted: &str, + key: &str, +) -> Result> { + let encrypted = base64::decode(password_encrypted)?; + + let decryptor = match age::Decryptor::new(&encrypted[..])? { + age::Decryptor::Passphrase(d) => d, + _ => unreachable!(), + }; + + let mut decrypt_buffer = Vec::new(); + let mut decrypt_writer = decryptor.decrypt(&age::secrecy::Secret::new(key.to_owned()), None)?; + + decrypt_writer.read_to_end(&mut decrypt_buffer)?; + + Ok(String::from_utf8(decrypt_buffer)?) +} + + + pub fn verify_password( password_plaint_text: &str, hash: &str, From 19341192bb270789c5402e9a55c5b848fd292fe4 Mon Sep 17 00:00:00 2001 From: EETagent Date: Thu, 27 Oct 2022 23:20:56 +0200 Subject: [PATCH 02/13] feat: async, fajnovka --- core/Cargo.toml | 8 +++++++- core/src/crypto.rs | 41 ++++++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index ad9a9b0..33a247d 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -7,10 +7,16 @@ edition = "2021" serde = { version = "1.0", features = ["derive"] } portfolio-entity = { path = "../entity" } rand = "0.8.5" -argon2 = "0.4.1" chrono = "0.4.22" jsonwebtoken = "8.1.1" dotenv = "0.15.0" +tokio = "1.21.2" +futures = "0.3.25" + +# crypto +argon2 = "0.4.1" +age = { version = "0.9.0", features = ["async"] } +base64 = "0.13.1" [dependencies.sea-orm] version = "^0.10.0" diff --git a/core/src/crypto.rs b/core/src/crypto.rs index e0562c0..63cb1e5 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -1,4 +1,4 @@ -use std::io::{Write, Read}; +use futures::io::{AsyncReadExt, AsyncWriteExt}; use argon2::{ Argon2, PasswordHasher as ArgonPasswordHasher, PasswordVerifier as ArgonPasswordVerifier, }; @@ -29,53 +29,56 @@ pub fn random_8_char_string() -> String { s } -pub fn hash_password(password_plaint_text: &str) -> Result { - let password = password_plaint_text.as_bytes(); - let salt = "c2VjcmV0bHl0ZXN0aW5nZXZlcnl0aGluZw"; - +pub async fn hash_password(password_plaint_text: String) -> Result { let argon_config = Argon2::default(); - let hash = argon_config.hash_password(password, salt)?; + let hash = tokio::task::spawn_blocking(move || { + let password = password_plaint_text.as_bytes(); + let salt = "c2VjcmV0bHl0ZXN0aW5nZXZlcnl0aGluZw"; + + let encrypted = argon_config.hash_password(password, salt); + encrypted + }).await; - return Ok(hash.to_string()); + let result = hash.unwrap()?; + + return Ok(result.to_string()); } -// TODO: Async? Zatím pod spawn_blocking -pub fn encrypt_password(password_plaint_text: &str, key: &str) -> Result { +pub async fn encrypt_password(password_plaint_text: &str, key: &str) -> Result { let encryptor = age::Encryptor::with_user_passphrase(age::secrecy::Secret::new(key.to_owned())); let mut encrypt_buffer = Vec::new(); - let mut encrypt_writer = encryptor.wrap_output(&mut encrypt_buffer)?; + let mut encrypt_writer = encryptor.wrap_async_output(&mut encrypt_buffer).await?; - encrypt_writer.write_all(password_plaint_text.as_bytes())?; + encrypt_writer.write_all(password_plaint_text.as_bytes()).await?; - encrypt_writer.finish()?; + encrypt_writer.flush().await?; + + encrypt_writer.close().await?; - Ok(base64::encode(encrypt_buffer)) } -pub fn decrypt_password( +pub async fn decrypt_password( password_encrypted: &str, key: &str, ) -> Result> { let encrypted = base64::decode(password_encrypted)?; - let decryptor = match age::Decryptor::new(&encrypted[..])? { + let decryptor = match age::Decryptor::new_async(&encrypted[..]).await? { age::Decryptor::Passphrase(d) => d, _ => unreachable!(), }; let mut decrypt_buffer = Vec::new(); - let mut decrypt_writer = decryptor.decrypt(&age::secrecy::Secret::new(key.to_owned()), None)?; + let mut decrypt_writer = decryptor.decrypt_async(&age::secrecy::Secret::new(key.to_owned()), None)?; - decrypt_writer.read_to_end(&mut decrypt_buffer)?; + decrypt_writer.read_to_end(&mut decrypt_buffer).await?; Ok(String::from_utf8(decrypt_buffer)?) } - - pub fn verify_password( password_plaint_text: &str, hash: &str, From 1d5352a1684a6c83c19d259fecaf25b8036532ec Mon Sep 17 00:00:00 2001 From: EETagent Date: Thu, 27 Oct 2022 23:21:12 +0200 Subject: [PATCH 03/13] fix: update mutation to use new async hash --- core/src/mutation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/mutation.rs b/core/src/mutation.rs index ac04626..069f83f 100644 --- a/core/src/mutation.rs +++ b/core/src/mutation.rs @@ -11,7 +11,7 @@ impl Mutation { plain_text_password: &String, ) -> Result { // TODO: unwrap pro testing.. - let hashed_password = hash_password(plain_text_password).unwrap(); + let hashed_password = hash_password(plain_text_password.to_string()).await.unwrap(); candidate::ActiveModel { application: Set(form_data.application), code: Set(hashed_password), From 0636a92b0100de23dd71a9bf8c7e032a75400c3f Mon Sep 17 00:00:00 2001 From: EETagent Date: Fri, 28 Oct 2022 11:08:35 +0200 Subject: [PATCH 04/13] feat: async even for verify --- core/src/crypto.rs | 75 ++++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/core/src/crypto.rs b/core/src/crypto.rs index 63cb1e5..7ee4c0e 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -1,10 +1,9 @@ -use futures::io::{AsyncReadExt, AsyncWriteExt}; use argon2::{ Argon2, PasswordHasher as ArgonPasswordHasher, PasswordVerifier as ArgonPasswordVerifier, }; +use futures::io::{AsyncReadExt, AsyncWriteExt}; use rand::Rng; - /// Foolproof random 8 char string /// only uppercase letters (except for 0 and O) and numbers /// TODO tests @@ -12,14 +11,11 @@ pub fn random_8_char_string() -> String { let iterator = rand::thread_rng() .sample_iter(&rand::distributions::Alphanumeric) .map(char::from); - let mut s = String::new(); - for c in iterator { // add all characters except for: lowercase chars, 0 and O - if ('1'..='9').contains(&c) || - ('A'..='N').contains(&c) || - ('P'..'Z').contains(&c) - { + for c in iterator { + // add all characters except for: lowercase chars, 0 and O + if ('1'..='9').contains(&c) || ('A'..='N').contains(&c) || ('P'..'Z').contains(&c) { s.push(c); if s.len() == 8 { break; @@ -29,34 +25,67 @@ pub fn random_8_char_string() -> String { s } -pub async fn hash_password(password_plaint_text: String) -> Result { +// TODO: No unwrap for spawn_blocking +pub async fn hash_password( + password_plaint_text: String, +) -> Result { let argon_config = Argon2::default(); let hash = tokio::task::spawn_blocking(move || { let password = password_plaint_text.as_bytes(); let salt = "c2VjcmV0bHl0ZXN0aW5nZXZlcnl0aGluZw"; - + let encrypted = argon_config.hash_password(password, salt); encrypted - }).await; + }) + .await.unwrap(); - let result = hash.unwrap()?; + let result = hash?; return Ok(result.to_string()); } -pub async fn encrypt_password(password_plaint_text: &str, key: &str) -> Result { +// TODO: No unwrap for spawn_blocking +pub async fn verify_password<'a>( + password_plaint_text: String, + hash: String, +) -> Result { + let argon_config = Argon2::default(); + + let result: Result = tokio::task::spawn_blocking(move || { + let parsed_hash = argon2::PasswordHash::new(&hash); + match parsed_hash { + Ok(parsed) => { + return Ok(argon_config + .verify_password(password_plaint_text.as_bytes(), &parsed) + .is_ok()) + } + Err(error) => return Err(error), + } + }) + .await + .unwrap(); + + result +} + +pub async fn encrypt_password( + password_plaint_text: &str, + key: &str, +) -> Result { let encryptor = age::Encryptor::with_user_passphrase(age::secrecy::Secret::new(key.to_owned())); let mut encrypt_buffer = Vec::new(); let mut encrypt_writer = encryptor.wrap_async_output(&mut encrypt_buffer).await?; - encrypt_writer.write_all(password_plaint_text.as_bytes()).await?; + encrypt_writer + .write_all(password_plaint_text.as_bytes()) + .await?; encrypt_writer.flush().await?; encrypt_writer.close().await?; - + Ok(base64::encode(encrypt_buffer)) } @@ -72,22 +101,10 @@ pub async fn decrypt_password( }; let mut decrypt_buffer = Vec::new(); - let mut decrypt_writer = decryptor.decrypt_async(&age::secrecy::Secret::new(key.to_owned()), None)?; + let mut decrypt_writer = + decryptor.decrypt_async(&age::secrecy::Secret::new(key.to_owned()), None)?; decrypt_writer.read_to_end(&mut decrypt_buffer).await?; Ok(String::from_utf8(decrypt_buffer)?) } - -pub fn verify_password( - password_plaint_text: &str, - hash: &str, -) -> Result { - let argon_config = Argon2::default(); - - let parsed_hash = argon2::PasswordHash::new(&hash)?; - - return Ok(argon_config - .verify_password(password_plaint_text.as_bytes(), &parsed_hash) - .is_ok()); -} From 0a7b0c902843857d52f6d2608a4c4e74eae66830 Mon Sep 17 00:00:00 2001 From: EETagent Date: Fri, 28 Oct 2022 11:08:53 +0200 Subject: [PATCH 05/13] fix: update code calling hash verify --- core/src/services/candidate_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 3ba2e8b..d91b155 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -17,7 +17,7 @@ impl CandidateService { }; - let valid = crypto::verify_password(&password,&candidate.code ) + let valid = crypto::verify_password(password,candidate.code.clone()).await .expect("Invalid password"); if !valid { From 7623bc80c2e08fbecabc1dcf2d28375fa37bda23 Mon Sep 17 00:00:00 2001 From: EETagent Date: Fri, 28 Oct 2022 12:00:31 +0200 Subject: [PATCH 06/13] feat: age key encryption --- core/src/crypto.rs | 91 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/core/src/crypto.rs b/core/src/crypto.rs index 7ee4c0e..3246197 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; +use std::iter; use argon2::{ Argon2, PasswordHasher as ArgonPasswordHasher, PasswordVerifier as ArgonPasswordVerifier, }; @@ -27,18 +29,19 @@ pub fn random_8_char_string() -> String { // TODO: No unwrap for spawn_blocking pub async fn hash_password( - password_plaint_text: String, + password_plain_text: String, ) -> Result { let argon_config = Argon2::default(); let hash = tokio::task::spawn_blocking(move || { - let password = password_plaint_text.as_bytes(); + let password = password_plain_text.as_bytes(); let salt = "c2VjcmV0bHl0ZXN0aW5nZXZlcnl0aGluZw"; let encrypted = argon_config.hash_password(password, salt); encrypted }) - .await.unwrap(); + .await + .unwrap(); let result = hash?; @@ -52,25 +55,26 @@ pub async fn verify_password<'a>( ) -> Result { let argon_config = Argon2::default(); - let result: Result = tokio::task::spawn_blocking(move || { - let parsed_hash = argon2::PasswordHash::new(&hash); - match parsed_hash { - Ok(parsed) => { - return Ok(argon_config - .verify_password(password_plaint_text.as_bytes(), &parsed) - .is_ok()) + let result: Result = + tokio::task::spawn_blocking(move || { + let parsed_hash = argon2::PasswordHash::new(&hash); + match parsed_hash { + Ok(parsed) => { + return Ok(argon_config + .verify_password(password_plaint_text.as_bytes(), &parsed) + .is_ok()) + } + Err(error) => return Err(error), } - Err(error) => return Err(error), - } - }) - .await - .unwrap(); + }) + .await + .unwrap(); result } pub async fn encrypt_password( - password_plaint_text: &str, + password_plain_text: &str, key: &str, ) -> Result { let encryptor = age::Encryptor::with_user_passphrase(age::secrecy::Secret::new(key.to_owned())); @@ -79,7 +83,7 @@ pub async fn encrypt_password( let mut encrypt_writer = encryptor.wrap_async_output(&mut encrypt_buffer).await?; encrypt_writer - .write_all(password_plaint_text.as_bytes()) + .write_all(password_plain_text.as_bytes()) .await?; encrypt_writer.flush().await?; @@ -108,3 +112,56 @@ pub async fn decrypt_password( Ok(String::from_utf8(decrypt_buffer)?) } + +pub async fn encrypt_password_with_recipients( + password_plain_text: &str, + recipients: Vec<&str>, +) -> Result { + let public_keys = recipients + .into_iter() + .map(|recipient| { + //TODO: No unwrap + Box::new(age::x25519::Recipient::from_str(recipient).unwrap()) as _ + }) + .collect(); + + let encryptor_option = age::Encryptor::with_recipients(public_keys); + + if let Some(encryptor) = encryptor_option { + let mut encrypt_buffer = Vec::new(); + let mut encrypt_writer = encryptor.wrap_async_output(&mut encrypt_buffer).await?; + + encrypt_writer + .write_all(password_plain_text.as_bytes()) + .await?; + + encrypt_writer.flush().await?; + + encrypt_writer.close().await?; + + Ok(base64::encode(encrypt_buffer)) + } else { + // TODO: Error handling + unreachable!("No recipients provided"); + } +} + +pub async fn decrypt_password_with_private_key( + password_encrypted: &str, + key: &str, +) -> Result> { + let encrypted = base64::decode(password_encrypted)?; + + let decryptor = match age::Decryptor::new_async(&encrypted[..]).await? { + age::Decryptor::Recipients(d) => d, + _ => unreachable!(), + }; + + let mut decrypt_buffer = Vec::new(); + let mut decrypt_writer = + decryptor.decrypt_async(iter::once(&age::x25519::Identity::from_str(key)? as &dyn age::Identity))?; + + decrypt_writer.read_to_end(&mut decrypt_buffer).await?; + + Ok(String::from_utf8(decrypt_buffer)?) +} From bd72aa1a6f3801bcb0b390d14bb49b03443e6a30 Mon Sep 17 00:00:00 2001 From: EETagent Date: Fri, 28 Oct 2022 13:42:44 +0200 Subject: [PATCH 07/13] feat: tests for crypto functions --- core/src/crypto.rs | 120 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/core/src/crypto.rs b/core/src/crypto.rs index 3246197..dbbc7ef 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -165,3 +165,123 @@ pub async fn decrypt_password_with_private_key( Ok(String::from_utf8(decrypt_buffer)?) } + +#[cfg(test)] +mod tests { + #[test] + fn test_random_8_char_string() { + let s = super::random_8_char_string(); + // Is 8 chars long + assert_eq!(s.len(), 8); + // Does not contain possibly confusing characters + assert!(!s.contains('0')); + assert!(!s.contains('O')); + } + + #[tokio::test] + async fn test_hash_password() { + const PASSWORD: &str = "test"; + let hash = super::hash_password(PASSWORD.to_string()).await.unwrap(); + + assert!(hash.contains("$argon2")); + } + + #[tokio::test] + async fn test_verify_password() { + const HASH: &str = "$argon2id$v=19$m=4096,t=3,p=1$c2VjcmV0bHl0ZXN0aW5nZXZlcnl0aGluZw$xEzH8wD/ZjzgZTDTl3YtzMFCfcVa5M5m9y6NfSyB1n4"; + const PASSWORD: &str = "test"; + + let result = super::verify_password(PASSWORD.to_string(), HASH.to_string()).await.unwrap(); + + assert!(result); + } + + #[tokio::test] + async fn test_hash_and_verify_password() { + const PASSWORD: &str = "test"; + + let hash = super::hash_password(PASSWORD.to_string()).await.unwrap(); + + let result = super::verify_password(PASSWORD.to_string(), hash).await.unwrap(); + + assert!(result); + } + + #[tokio::test] + async fn test_encrypt_password_is_valid_base64() { + const PASSWORD: &str = "test"; + const KEY: &str = "test"; + + let encrypted = super::encrypt_password(PASSWORD, KEY).await.unwrap(); + + assert!(base64::decode(encrypted).is_ok()); + } + + #[tokio::test] + async fn test_encrypt_decrypt_password() { + const PASSWORD: &str = "test"; + const KEY: &str = "test"; + + let encrypted = super::encrypt_password(PASSWORD, KEY).await.unwrap(); + + let decrypted = super::decrypt_password(&encrypted, KEY).await.unwrap(); + + assert_eq!(PASSWORD, decrypted); + } + + #[tokio::test] + async fn test_encrypt_password_with_recipients_is_valid_base64() { + const PASSWORD: &str = "test"; + const PUBLIC_KEY: &str = "age1t220v5c8ye0pjx99kw8nr57y7a5qlw4ke0wchjuxnr2gcvfzt3hq7fufz0"; + + let encrypted = super::encrypt_password_with_recipients(PASSWORD, vec![PUBLIC_KEY]).await.unwrap(); + + eprint!("{}", encrypted); + + assert!(base64::decode(encrypted).is_ok()); + } + + #[tokio::test] + async fn test_encrypt_password_with_recipients_multiple_is_valid_base64() { + const PASSWORD: &str = "test"; + const PUBLIC_KEY_1: &str = "age1t220v5c8ye0pjx99kw8nr57y7a5qlw4ke0wchjuxnr2gcvfzt3hq7fufz0"; + const PUBLIC_KEY_2: &str = "age1ygswsk38cq9r64um5klqxyvzemfdvx6qe5zed99pdexakwwhpatsgatgpw"; + + let encrypted = super::encrypt_password_with_recipients(PASSWORD, vec![PUBLIC_KEY_1, PUBLIC_KEY_2]).await.unwrap(); + + println!("{}", encrypted); + assert!(base64::decode(encrypted).is_ok()); + } + + #[tokio::test] + async fn test_decrypt_password_with_private_key() { + const PASSWORD: &str = "test"; + //const PUBLIC_KEY: &str = "age1t220v5c8ye0pjx99kw8nr57y7a5qlw4ke0wchjuxnr2gcvfzt3hq7fufz0"; + const PRIVATE_KEY: &str = "AGE-SECRET-KEY-1WPDHL2FLJ23T6RK5KCX8KS8DNLX0CGXMNZG0XNUAH4QP5C8ZZ46QGD3STV"; + const CIPHER: &str = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVWUNCY0RielVCaThLbGlIR1NZa0p6MlNiS0x5L3B2Y3B2b21XZHNaZUVjClpsVTRvUGVVQVYzS205VTVVMDlXYjFHVE5ZZzJOSEpyN1ZyT0tocFpIbUUKLT4gPy1ncmVhc2UgLltXKT9MJyBLQGouLWcgfCBQSm12JQp3bDhRTDd0ZGZWbU9mQ2FYVU9Cb2FjM3AwR243OGJNCi0tLSBSSzRxV3E2d0VscERvM3VHVUhOL3dPaGVBRHE3WkZrdzYxYUgyQVl6elh3CiFQOr28YvbEAkx0YgFnIxwvPNjjYZV6THArcMPM8i5flnmKPw=="; + + let decrypted = super::decrypt_password_with_private_key(CIPHER, PRIVATE_KEY).await.unwrap(); + + assert_eq!(PASSWORD, decrypted); + } + + #[tokio::test] + async fn test_decrypt_password_with_private_key_multiple() { + const PASSWORD: &str = "test"; + // const PUBLIC_KEY_1: &str = "age1t220v5c8ye0pjx99kw8nr57y7a5qlw4ke0wchjuxnr2gcvfzt3hq7fufz0"; + // const PUBLIC_KEY_2: &str = "age1ygswsk38cq9r64um5klqxyvzemfdvx6qe5zed99pdexakwwhpatsgatgpw"; + const PRIVATE_KEY_1: &str = "AGE-SECRET-KEY-1WPDHL2FLJ23T6RK5KCX8KS8DNLX0CGXMNZG0XNUAH4QP5C8ZZ46QGD3STV"; + const PRIVATE_KEY_2: &str = "AGE-SECRET-KEY-19RT6Z6TR0TE465EMJFDVXAFZ00YE65THLSS5LAY4W85L587DF95SPPDVND"; + + const CIPHER: &str = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBQ1BuSi9VMWIzeHg1TjQwMDNSUzlpZ0pGRWMxU2pFenVBekxGQTM0WGkwClkycytsNXNMbmVJTm5GT3VDRFBGQXE1ZFU5MzNzV0NXRWhmV1VGSjVNbU0KLT4gWDI1NTE5IHAvUjRLc3ROd2FkalZWTVIxRnBjaEluMXNtYWVScTVxdWxHY0x6ajZtUmMKYXkyNTExakZ0NWt5Vm85YUJSRlRmZTh4VEEyVEVrOFRyWDMxckNDVGkzOAotPiBbNVhfKS1ncmVhc2UgcysxIChlLTsKYU43T0lXUlUxZDFRVUpacXdJcm02Y3VzSjNMTVBtcy9pNm9yOEdETVplYjJrY1VsemRZU00rZ3NrSFZvUTBoSQovcEVrcmRmYlBPdzN3WWZTR0t1a1VFY0VTWXlIR1VPSUJRCi0tLSBYbmpxUHpVQzl5YnowdktIcTRjTklERXRDYVAxb0FmaWQwazgzRkp0U2pNCiAVlCPJ1+jroWQ7HBqjRUOcCBMyYvi9xIaklX2XDYPB2rd7Fw=="; + + let decrypted_1 = super::decrypt_password_with_private_key(CIPHER, PRIVATE_KEY_1).await.unwrap(); + + assert_eq!(PASSWORD, decrypted_1); + + let decrypted_2 = super::decrypt_password_with_private_key(CIPHER, PRIVATE_KEY_2).await.unwrap(); + + assert_eq!(PASSWORD, decrypted_2); + } + +} \ No newline at end of file From a7251e668e6129b69257f07455f29af74e85e395 Mon Sep 17 00:00:00 2001 From: EETagent Date: Fri, 28 Oct 2022 14:30:55 +0200 Subject: [PATCH 08/13] feat: file encryption & formatting --- core/src/crypto.rs | 94 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 77 insertions(+), 17 deletions(-) diff --git a/core/src/crypto.rs b/core/src/crypto.rs index dbbc7ef..c227b43 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -1,10 +1,10 @@ -use std::str::FromStr; -use std::iter; use argon2::{ Argon2, PasswordHasher as ArgonPasswordHasher, PasswordVerifier as ArgonPasswordVerifier, }; use futures::io::{AsyncReadExt, AsyncWriteExt}; use rand::Rng; +use std::iter; +use std::str::FromStr; /// Foolproof random 8 char string /// only uppercase letters (except for 0 and O) and numbers @@ -158,14 +158,57 @@ pub async fn decrypt_password_with_private_key( }; let mut decrypt_buffer = Vec::new(); - let mut decrypt_writer = - decryptor.decrypt_async(iter::once(&age::x25519::Identity::from_str(key)? as &dyn age::Identity))?; + let mut decrypt_writer = decryptor.decrypt_async(iter::once( + &age::x25519::Identity::from_str(key)? as &dyn age::Identity, + ))?; decrypt_writer.read_to_end(&mut decrypt_buffer).await?; Ok(String::from_utf8(decrypt_buffer)?) } +// TODO: Massive refactor of encrypt_file_with_recipients required +pub async fn encrypt_file_with_recipients( + plain_file_path: &str, + cipher_file_path: &str, + recipients: Vec<&str>, +) -> Result<(), age::EncryptError> { + let public_keys = recipients + .into_iter() + .map(|recipient| { + //TODO: No unwrap + Box::new(age::x25519::Recipient::from_str(recipient).unwrap()) as _ + }) + .collect(); + + let encryptor_option = age::Encryptor::with_recipients(public_keys); + + if let Some(encryptor) = encryptor_option { + let mut cipher_file = tokio::fs::File::create(cipher_file_path).await?; + let mut plain_file = tokio::fs::File::open(plain_file_path).await?; + + let mut plain_file_contents = Vec::new(); + + tokio::io::AsyncReadExt::read_to_end(&mut plain_file, &mut plain_file_contents).await?; + + let mut encrypt_buffer = Vec::new(); + let mut encrypt_writer = encryptor.wrap_async_output(&mut encrypt_buffer).await?; + + encrypt_writer.write_all(&plain_file_contents).await?; + + encrypt_writer.flush().await?; + + encrypt_writer.close().await?; + + tokio::io::AsyncWriteExt::write_all(&mut cipher_file, &encrypt_buffer).await?; + + return Ok(()); + } else { + // TODO: Error handling + unreachable!("No recipients provided"); + } +} + #[cfg(test)] mod tests { #[test] @@ -191,7 +234,9 @@ mod tests { const HASH: &str = "$argon2id$v=19$m=4096,t=3,p=1$c2VjcmV0bHl0ZXN0aW5nZXZlcnl0aGluZw$xEzH8wD/ZjzgZTDTl3YtzMFCfcVa5M5m9y6NfSyB1n4"; const PASSWORD: &str = "test"; - let result = super::verify_password(PASSWORD.to_string(), HASH.to_string()).await.unwrap(); + let result = super::verify_password(PASSWORD.to_string(), HASH.to_string()) + .await + .unwrap(); assert!(result); } @@ -202,11 +247,13 @@ mod tests { let hash = super::hash_password(PASSWORD.to_string()).await.unwrap(); - let result = super::verify_password(PASSWORD.to_string(), hash).await.unwrap(); + let result = super::verify_password(PASSWORD.to_string(), hash) + .await + .unwrap(); assert!(result); } - + #[tokio::test] async fn test_encrypt_password_is_valid_base64() { const PASSWORD: &str = "test"; @@ -234,7 +281,9 @@ mod tests { const PASSWORD: &str = "test"; const PUBLIC_KEY: &str = "age1t220v5c8ye0pjx99kw8nr57y7a5qlw4ke0wchjuxnr2gcvfzt3hq7fufz0"; - let encrypted = super::encrypt_password_with_recipients(PASSWORD, vec![PUBLIC_KEY]).await.unwrap(); + let encrypted = super::encrypt_password_with_recipients(PASSWORD, vec![PUBLIC_KEY]) + .await + .unwrap(); eprint!("{}", encrypted); @@ -247,7 +296,10 @@ mod tests { const PUBLIC_KEY_1: &str = "age1t220v5c8ye0pjx99kw8nr57y7a5qlw4ke0wchjuxnr2gcvfzt3hq7fufz0"; const PUBLIC_KEY_2: &str = "age1ygswsk38cq9r64um5klqxyvzemfdvx6qe5zed99pdexakwwhpatsgatgpw"; - let encrypted = super::encrypt_password_with_recipients(PASSWORD, vec![PUBLIC_KEY_1, PUBLIC_KEY_2]).await.unwrap(); + let encrypted = + super::encrypt_password_with_recipients(PASSWORD, vec![PUBLIC_KEY_1, PUBLIC_KEY_2]) + .await + .unwrap(); println!("{}", encrypted); assert!(base64::decode(encrypted).is_ok()); @@ -257,10 +309,13 @@ mod tests { async fn test_decrypt_password_with_private_key() { const PASSWORD: &str = "test"; //const PUBLIC_KEY: &str = "age1t220v5c8ye0pjx99kw8nr57y7a5qlw4ke0wchjuxnr2gcvfzt3hq7fufz0"; - const PRIVATE_KEY: &str = "AGE-SECRET-KEY-1WPDHL2FLJ23T6RK5KCX8KS8DNLX0CGXMNZG0XNUAH4QP5C8ZZ46QGD3STV"; + const PRIVATE_KEY: &str = + "AGE-SECRET-KEY-1WPDHL2FLJ23T6RK5KCX8KS8DNLX0CGXMNZG0XNUAH4QP5C8ZZ46QGD3STV"; const CIPHER: &str = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVWUNCY0RielVCaThLbGlIR1NZa0p6MlNiS0x5L3B2Y3B2b21XZHNaZUVjClpsVTRvUGVVQVYzS205VTVVMDlXYjFHVE5ZZzJOSEpyN1ZyT0tocFpIbUUKLT4gPy1ncmVhc2UgLltXKT9MJyBLQGouLWcgfCBQSm12JQp3bDhRTDd0ZGZWbU9mQ2FYVU9Cb2FjM3AwR243OGJNCi0tLSBSSzRxV3E2d0VscERvM3VHVUhOL3dPaGVBRHE3WkZrdzYxYUgyQVl6elh3CiFQOr28YvbEAkx0YgFnIxwvPNjjYZV6THArcMPM8i5flnmKPw=="; - let decrypted = super::decrypt_password_with_private_key(CIPHER, PRIVATE_KEY).await.unwrap(); + let decrypted = super::decrypt_password_with_private_key(CIPHER, PRIVATE_KEY) + .await + .unwrap(); assert_eq!(PASSWORD, decrypted); } @@ -270,18 +325,23 @@ mod tests { const PASSWORD: &str = "test"; // const PUBLIC_KEY_1: &str = "age1t220v5c8ye0pjx99kw8nr57y7a5qlw4ke0wchjuxnr2gcvfzt3hq7fufz0"; // const PUBLIC_KEY_2: &str = "age1ygswsk38cq9r64um5klqxyvzemfdvx6qe5zed99pdexakwwhpatsgatgpw"; - const PRIVATE_KEY_1: &str = "AGE-SECRET-KEY-1WPDHL2FLJ23T6RK5KCX8KS8DNLX0CGXMNZG0XNUAH4QP5C8ZZ46QGD3STV"; - const PRIVATE_KEY_2: &str = "AGE-SECRET-KEY-19RT6Z6TR0TE465EMJFDVXAFZ00YE65THLSS5LAY4W85L587DF95SPPDVND"; + const PRIVATE_KEY_1: &str = + "AGE-SECRET-KEY-1WPDHL2FLJ23T6RK5KCX8KS8DNLX0CGXMNZG0XNUAH4QP5C8ZZ46QGD3STV"; + const PRIVATE_KEY_2: &str = + "AGE-SECRET-KEY-19RT6Z6TR0TE465EMJFDVXAFZ00YE65THLSS5LAY4W85L587DF95SPPDVND"; const CIPHER: &str = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBQ1BuSi9VMWIzeHg1TjQwMDNSUzlpZ0pGRWMxU2pFenVBekxGQTM0WGkwClkycytsNXNMbmVJTm5GT3VDRFBGQXE1ZFU5MzNzV0NXRWhmV1VGSjVNbU0KLT4gWDI1NTE5IHAvUjRLc3ROd2FkalZWTVIxRnBjaEluMXNtYWVScTVxdWxHY0x6ajZtUmMKYXkyNTExakZ0NWt5Vm85YUJSRlRmZTh4VEEyVEVrOFRyWDMxckNDVGkzOAotPiBbNVhfKS1ncmVhc2UgcysxIChlLTsKYU43T0lXUlUxZDFRVUpacXdJcm02Y3VzSjNMTVBtcy9pNm9yOEdETVplYjJrY1VsemRZU00rZ3NrSFZvUTBoSQovcEVrcmRmYlBPdzN3WWZTR0t1a1VFY0VTWXlIR1VPSUJRCi0tLSBYbmpxUHpVQzl5YnowdktIcTRjTklERXRDYVAxb0FmaWQwazgzRkp0U2pNCiAVlCPJ1+jroWQ7HBqjRUOcCBMyYvi9xIaklX2XDYPB2rd7Fw=="; - let decrypted_1 = super::decrypt_password_with_private_key(CIPHER, PRIVATE_KEY_1).await.unwrap(); + let decrypted_1 = super::decrypt_password_with_private_key(CIPHER, PRIVATE_KEY_1) + .await + .unwrap(); assert_eq!(PASSWORD, decrypted_1); - let decrypted_2 = super::decrypt_password_with_private_key(CIPHER, PRIVATE_KEY_2).await.unwrap(); + let decrypted_2 = super::decrypt_password_with_private_key(CIPHER, PRIVATE_KEY_2) + .await + .unwrap(); assert_eq!(PASSWORD, decrypted_2); } - -} \ No newline at end of file +} From b760560b1ad33414624ed5c11f18dca78a2bf51a Mon Sep 17 00:00:00 2001 From: EETagent Date: Fri, 28 Oct 2022 14:41:07 +0200 Subject: [PATCH 09/13] refactor: use std:Error or compatible type in crypto.rs --- core/Cargo.toml | 6 +++--- core/src/crypto.rs | 16 ++++++---------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 33a247d..67be561 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,9 +14,9 @@ tokio = "1.21.2" futures = "0.3.25" # crypto -argon2 = "0.4.1" -age = { version = "0.9.0", features = ["async"] } -base64 = "0.13.1" +argon2 = { version = "0.4", features = ["std"] } +age = { version = "0.9", features = ["async"] } +base64 = "0.13" [dependencies.sea-orm] version = "^0.10.0" diff --git a/core/src/crypto.rs b/core/src/crypto.rs index c227b43..16d2c3b 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -30,7 +30,7 @@ pub fn random_8_char_string() -> String { // TODO: No unwrap for spawn_blocking pub async fn hash_password( password_plain_text: String, -) -> Result { +) -> Result> { let argon_config = Argon2::default(); let hash = tokio::task::spawn_blocking(move || { @@ -40,19 +40,16 @@ pub async fn hash_password( let encrypted = argon_config.hash_password(password, salt); encrypted }) - .await - .unwrap(); + .await??; - let result = hash?; - - return Ok(result.to_string()); + return Ok(hash.to_string()); } // TODO: No unwrap for spawn_blocking pub async fn verify_password<'a>( password_plaint_text: String, hash: String, -) -> Result { +) -> Result> { let argon_config = Argon2::default(); let result: Result = @@ -67,10 +64,9 @@ pub async fn verify_password<'a>( Err(error) => return Err(error), } }) - .await - .unwrap(); + .await?; - result + Ok(result?) } pub async fn encrypt_password( From 7b53891007f9fb9efb394fa7096af8663df767a6 Mon Sep 17 00:00:00 2001 From: EETagent Date: Fri, 28 Oct 2022 14:41:30 +0200 Subject: [PATCH 10/13] refactor: remove forgotten print --- core/src/crypto.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/crypto.rs b/core/src/crypto.rs index 16d2c3b..2c9706b 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -281,8 +281,6 @@ mod tests { .await .unwrap(); - eprint!("{}", encrypted); - assert!(base64::decode(encrypted).is_ok()); } From 9fd161b0e8044ef7969ff4f18817df423f378d3c Mon Sep 17 00:00:00 2001 From: EETagent Date: Fri, 28 Oct 2022 15:02:59 +0200 Subject: [PATCH 11/13] feat: use random salt for argon2, better security --- core/src/crypto.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/core/src/crypto.rs b/core/src/crypto.rs index 2c9706b..f236fa5 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -35,14 +35,16 @@ pub async fn hash_password( let hash = tokio::task::spawn_blocking(move || { let password = password_plain_text.as_bytes(); - let salt = "c2VjcmV0bHl0ZXN0aW5nZXZlcnl0aGluZw"; - let encrypted = argon_config.hash_password(password, salt); - encrypted - }) - .await??; + let salt_str = argon2::password_hash::SaltString::generate(rand::thread_rng()); + let salt = salt_str.as_salt(); - return Ok(hash.to_string()); + return argon_config.hash_password(password, &salt).map(|x| x.serialize().to_string()); + }); + + let hash_string = hash.await??; + + return Ok(hash_string); } // TODO: No unwrap for spawn_blocking From 3abf84e1633844f263c1fd857881471e25d99af9 Mon Sep 17 00:00:00 2001 From: EETagent Date: Fri, 28 Oct 2022 15:03:22 +0200 Subject: [PATCH 12/13] chore: remove todos for argon2 spawn_blocking error handling --- core/src/crypto.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/crypto.rs b/core/src/crypto.rs index f236fa5..3484592 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -27,7 +27,6 @@ pub fn random_8_char_string() -> String { s } -// TODO: No unwrap for spawn_blocking pub async fn hash_password( password_plain_text: String, ) -> Result> { @@ -47,7 +46,6 @@ pub async fn hash_password( return Ok(hash_string); } -// TODO: No unwrap for spawn_blocking pub async fn verify_password<'a>( password_plaint_text: String, hash: String, From 2ae22c7ec70910a469e4c11bd758ea47499b77f1 Mon Sep 17 00:00:00 2001 From: EETagent Date: Fri, 28 Oct 2022 15:24:07 +0200 Subject: [PATCH 13/13] feat: generate AGE keys --- core/Cargo.toml | 1 + core/src/crypto.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/core/Cargo.toml b/core/Cargo.toml index 67be561..7635b5b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,6 +16,7 @@ futures = "0.3.25" # crypto argon2 = { version = "0.4", features = ["std"] } age = { version = "0.9", features = ["async"] } +secrecy = { version = "0.8.0" } base64 = "0.13" [dependencies.sea-orm] diff --git a/core/src/crypto.rs b/core/src/crypto.rs index 3484592..bacc3fe 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -1,6 +1,7 @@ use argon2::{ Argon2, PasswordHasher as ArgonPasswordHasher, PasswordVerifier as ArgonPasswordVerifier, }; +use secrecy::ExposeSecret; use futures::io::{AsyncReadExt, AsyncWriteExt}; use rand::Rng; use std::iter; @@ -109,6 +110,13 @@ pub async fn decrypt_password( Ok(String::from_utf8(decrypt_buffer)?) } +pub fn create_identity() -> (String, String){ + let identity = age::x25519::Identity::generate(); + + // Public Key & Private Key + (identity.to_public().to_string(), identity.to_string().expose_secret().to_string()) +} + pub async fn encrypt_password_with_recipients( password_plain_text: &str, recipients: Vec<&str>, @@ -271,6 +279,14 @@ mod tests { assert_eq!(PASSWORD, decrypted); } + + #[test] + fn test_create_identity() { + let identity = super::create_identity(); + + assert!(identity.0.contains("age")); + assert!(identity.1.contains("AGE-SECRET-KEY-")); + } #[tokio::test] async fn test_encrypt_password_with_recipients_is_valid_base64() {