diff --git a/frontend/rust-lib/flowy-encrypt/src/encrypt.rs b/frontend/rust-lib/flowy-encrypt/src/encrypt.rs index dcc596a86d..88658858bb 100644 --- a/frontend/rust-lib/flowy-encrypt/src/encrypt.rs +++ b/frontend/rust-lib/flowy-encrypt/src/encrypt.rs @@ -10,19 +10,34 @@ use rand::distributions::Alphanumeric; use rand::Rng; use sha2::Sha256; +/// The length of the salt in bytes. const SALT_LENGTH: usize = 16; + +/// The length of the derived encryption key in bytes. const KEY_LENGTH: usize = 32; + +/// The number of iterations for the PBKDF2 key derivation. const ITERATIONS: u32 = 1000; + +/// The length of the nonce for AES-GCM encryption. const NONCE_LENGTH: usize = 12; + +/// Delimiter used to concatenate the passphrase and salt. const CONCATENATED_DELIMITER: &str = "$"; -pub fn generate_encrypt_secret() -> String { - let passphrase = generate_passphrase(); - let salt = generate_salt(); - concatenate_passphrase_and_salt(&passphrase, &salt) +/// Generate a new encryption secret consisting of a passphrase and a salt. +pub fn generate_encryption_secret() -> String { + let passphrase = generate_random_passphrase(); + let salt = generate_random_salt(); + combine_passphrase_and_salt(&passphrase, &salt) } -pub fn encrypt_bytes>(data: T, combined_passphrase_salt: &str) -> Result> { +/// Encrypt a byte slice using AES-GCM. +/// +/// # Arguments +/// * `data`: The data to encrypt. +/// * `combined_passphrase_salt`: The concatenated passphrase and salt. +pub fn encrypt_data>(data: T, combined_passphrase_salt: &str) -> Result> { let (passphrase, salt) = split_passphrase_and_salt(combined_passphrase_salt)?; let key = derive_key(passphrase, &salt)?; let cipher = Aes256Gcm::new(GenericArray::from_slice(&key)); @@ -34,7 +49,12 @@ pub fn encrypt_bytes>(data: T, combined_passphrase_salt: &str) -> Ok(nonce.into_iter().chain(ciphertext).collect()) } -pub fn decrypt_bytes>(data: T, combined_passphrase_salt: &str) -> Result> { +/// Decrypt a byte slice using AES-GCM. +/// +/// # Arguments +/// * `data`: The data to decrypt. +/// * `combined_passphrase_salt`: The concatenated passphrase and salt. +pub fn decrypt_data>(data: T, combined_passphrase_salt: &str) -> Result> { if data.as_ref().len() <= NONCE_LENGTH { return Err(anyhow::anyhow!("Ciphertext too short to include nonce.")); } @@ -47,18 +67,43 @@ pub fn decrypt_bytes>(data: T, combined_passphrase_salt: &str) -> .map_err(|e| anyhow::anyhow!("Decryption error: {:?}", e)) } -pub fn encrypt_string>(data: T, combined_passphrase_salt: &str) -> Result { - let encrypted = encrypt_bytes(data.as_ref(), combined_passphrase_salt)?; +/// Encrypt a string using AES-GCM and return the result as a base64 encoded string. +/// +/// # Arguments +/// * `data`: The string data to encrypt. +/// * `combined_passphrase_salt`: The concatenated passphrase and salt. +pub fn encrypt_text>(data: T, combined_passphrase_salt: &str) -> Result { + let encrypted = encrypt_data(data.as_ref(), combined_passphrase_salt)?; Ok(STANDARD.encode(encrypted)) } -pub fn decrypt_string>(data: T, combined_passphrase_salt: &str) -> Result { +/// Decrypt a base64 encoded string using AES-GCM. +/// +/// # Arguments +/// * `data`: The base64 encoded string to decrypt. +/// * `combined_passphrase_salt`: The concatenated passphrase and salt. +pub fn decrypt_text>(data: T, combined_passphrase_salt: &str) -> Result { let encrypted = STANDARD.decode(data)?; - let decrypted = decrypt_bytes(encrypted, combined_passphrase_salt)?; + let decrypted = decrypt_data(encrypted, combined_passphrase_salt)?; Ok(String::from_utf8(decrypted)?) } -fn generate_passphrase() -> String { +/// Generates a random passphrase consisting of alphanumeric characters. +/// +/// This function creates a passphrase with both uppercase and lowercase letters +/// as well as numbers. The passphrase is 30 characters in length. +/// +/// # Returns +/// +/// A `String` representing the generated passphrase. +/// +/// # Security Considerations +/// +/// The passphrase is derived from the `Alphanumeric` character set which includes 62 possible +/// characters (26 lowercase letters, 26 uppercase letters, 10 numbers). This results in a total +/// of `62^30` possible combinations, making it strong against brute force attacks. +/// +fn generate_random_passphrase() -> String { rand::thread_rng() .sample_iter(&Alphanumeric) .take(30) // e.g., 30 characters @@ -66,13 +111,13 @@ fn generate_passphrase() -> String { .collect() } -fn generate_salt() -> [u8; SALT_LENGTH] { +fn generate_random_salt() -> [u8; SALT_LENGTH] { let mut rng = rand::thread_rng(); let salt: [u8; SALT_LENGTH] = rng.gen(); salt } -fn concatenate_passphrase_and_salt(passphrase: &str, salt: &[u8; SALT_LENGTH]) -> String { +fn combine_passphrase_and_salt(passphrase: &str, salt: &[u8; SALT_LENGTH]) -> String { let salt_base64 = STANDARD.encode(salt); format!("{}{}{}", passphrase, CONCATENATED_DELIMITER, salt_base64) } @@ -103,16 +148,25 @@ mod tests { use super::*; #[test] - fn test_encrypt_decrypt() { - let secret = generate_encrypt_secret(); + fn encrypt_decrypt_test() { + let secret = generate_encryption_secret(); let data = b"hello world"; - let encrypted = encrypt_bytes(data, &secret).unwrap(); - let decrypted = decrypt_bytes(encrypted, &secret).unwrap(); + let encrypted = encrypt_data(data, &secret).unwrap(); + let decrypted = decrypt_data(encrypted, &secret).unwrap(); assert_eq!(data, decrypted.as_slice()); let s = "123".to_string(); - let encrypted = encrypt_string(&s, &secret).unwrap(); - let decrypted_str = decrypt_string(encrypted, &secret).unwrap(); + let encrypted = encrypt_text(&s, &secret).unwrap(); + let decrypted_str = decrypt_text(encrypted, &secret).unwrap(); assert_eq!(s, decrypted_str); } + + #[test] + fn decrypt_with_invalid_secret_test() { + let secret = generate_encryption_secret(); + let data = b"hello world"; + let encrypted = encrypt_data(data, &secret).unwrap(); + let decrypted = decrypt_data(encrypted, "invalid secret"); + assert!(decrypted.is_err()) + } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/util.rs b/frontend/rust-lib/flowy-server/src/supabase/api/util.rs index 5a438b36b4..49d5c269f6 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/util.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/util.rs @@ -3,7 +3,7 @@ use anyhow::Result; use reqwest::{Response, StatusCode}; use serde_json::Value; -use flowy_encrypt::{decrypt_bytes, encrypt_bytes}; +use flowy_encrypt::{decrypt_data, encrypt_data}; use flowy_error::{ErrorCode, FlowyError}; use lib_infra::future::{to_fut, Fut}; @@ -148,7 +148,7 @@ impl SupabaseBinaryColumnEncoder { let value = match encryption_secret { None => hex::encode(value), Some(encryption_secret) => { - let encrypt_data = encrypt_bytes(value, encryption_secret)?; + let encrypt_data = encrypt_data(value, encryption_secret)?; hex::encode(encrypt_data) }, }; @@ -191,7 +191,7 @@ impl SupabaseBinaryColumnDecoder { )), Some(encryption_secret) => { let encrypt_data = D::decode(s)?; - decrypt_bytes(encrypt_data, encryption_secret) + decrypt_data(encrypt_data, encryption_secret) }, } } diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs index 1919ac11d8..4f0effb715 100644 --- a/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs @@ -1,6 +1,6 @@ use uuid::Uuid; -use flowy_encrypt::{encrypt_string, generate_encrypt_secret}; +use flowy_encrypt::{encrypt_text, generate_encryption_secret}; use flowy_user_deps::entities::*; use lib_infra::box_any::BoxAny; @@ -126,8 +126,8 @@ async fn user_encryption_sign_test() { let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); // generate encryption sign - let secret = generate_encrypt_secret(); - let sign = encrypt_string(user.user_id.to_string(), &secret).unwrap(); + let secret = generate_encryption_secret(); + let sign = encrypt_text(user.user_id.to_string(), &secret).unwrap(); user_service .update_user( diff --git a/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs b/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs index fd979c8093..b3892f7f77 100644 --- a/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs +++ b/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use nanoid::nanoid; -use flowy_encrypt::decrypt_string; +use flowy_encrypt::decrypt_text; use flowy_server::supabase::define::{USER_EMAIL, USER_UUID}; use flowy_test::event_builder::EventBuilder; use flowy_test::FlowyCoreTest; @@ -51,7 +51,7 @@ async fn third_party_sign_up_with_encrypt_test() { let user_profile = test.get_user_profile().await.unwrap(); assert!(!user_profile.encryption_sign.is_empty()); - let decryption_sign = decrypt_string(user_profile.encryption_sign, &secret).unwrap(); + let decryption_sign = decrypt_text(user_profile.encryption_sign, &secret).unwrap(); assert_eq!(decryption_sign, user_profile.id.to_string()); } } diff --git a/frontend/rust-lib/flowy-user/src/services/cloud_config.rs b/frontend/rust-lib/flowy-user/src/services/cloud_config.rs index 1395d611c6..5c23beeeb6 100644 --- a/frontend/rust-lib/flowy-user/src/services/cloud_config.rs +++ b/frontend/rust-lib/flowy-user/src/services/cloud_config.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use flowy_encrypt::generate_encrypt_secret; +use flowy_encrypt::generate_encryption_secret; use flowy_error::FlowyResult; use flowy_sqlite::kv::StorePreferences; use flowy_user_deps::cloud::UserCloudConfig; @@ -8,7 +8,7 @@ use flowy_user_deps::cloud::UserCloudConfig; const CLOUD_CONFIG_KEY: &str = "af_user_cloud_config"; fn generate_cloud_config(uid: i64, store_preference: &Arc) -> UserCloudConfig { - let config = UserCloudConfig::new(generate_encrypt_secret()); + let config = UserCloudConfig::new(generate_encryption_secret()); let key = cache_key_for_cloud_config(uid); store_preference.set_object(&key, config.clone()).unwrap(); config diff --git a/frontend/rust-lib/flowy-user/src/services/user_encryption.rs b/frontend/rust-lib/flowy-user/src/services/user_encryption.rs index 06fedb8fb6..fc5d7c06b4 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_encryption.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_encryption.rs @@ -1,4 +1,4 @@ -use flowy_encrypt::{decrypt_string, encrypt_string}; +use flowy_encrypt::{decrypt_text, encrypt_text}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_user_deps::entities::{EncryptionType, UpdateUserProfileParams, UserCredentials}; @@ -24,7 +24,7 @@ impl UserManager { } pub fn generate_encryption_sign(&self, uid: i64, encrypt_secret: &str) -> FlowyResult { - let encrypt_sign = encrypt_string(uid.to_string(), encrypt_secret)?; + let encrypt_sign = encrypt_text(uid.to_string(), encrypt_secret)?; Ok(encrypt_sign) } @@ -51,7 +51,7 @@ impl UserManager { encrypt_sign: &str, encryption_secret: &str, ) -> FlowyResult<()> { - let decrypt_str = decrypt_string(encrypt_sign, encryption_secret) + let decrypt_str = decrypt_text(encrypt_sign, encryption_secret) .map_err(|_| FlowyError::new(ErrorCode::InvalidEncryptSecret, "Invalid decryption secret"))?; if uid.to_string() == decrypt_str { Ok(())