mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
docs: documentation for encryption functions (#3243)
This commit is contained in:
parent
a1647bee78
commit
30155924a9
@ -10,19 +10,34 @@ use rand::distributions::Alphanumeric;
|
|||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
/// The length of the salt in bytes.
|
||||||
const SALT_LENGTH: usize = 16;
|
const SALT_LENGTH: usize = 16;
|
||||||
|
|
||||||
|
/// The length of the derived encryption key in bytes.
|
||||||
const KEY_LENGTH: usize = 32;
|
const KEY_LENGTH: usize = 32;
|
||||||
|
|
||||||
|
/// The number of iterations for the PBKDF2 key derivation.
|
||||||
const ITERATIONS: u32 = 1000;
|
const ITERATIONS: u32 = 1000;
|
||||||
|
|
||||||
|
/// The length of the nonce for AES-GCM encryption.
|
||||||
const NONCE_LENGTH: usize = 12;
|
const NONCE_LENGTH: usize = 12;
|
||||||
|
|
||||||
|
/// Delimiter used to concatenate the passphrase and salt.
|
||||||
const CONCATENATED_DELIMITER: &str = "$";
|
const CONCATENATED_DELIMITER: &str = "$";
|
||||||
|
|
||||||
pub fn generate_encrypt_secret() -> String {
|
/// Generate a new encryption secret consisting of a passphrase and a salt.
|
||||||
let passphrase = generate_passphrase();
|
pub fn generate_encryption_secret() -> String {
|
||||||
let salt = generate_salt();
|
let passphrase = generate_random_passphrase();
|
||||||
concatenate_passphrase_and_salt(&passphrase, &salt)
|
let salt = generate_random_salt();
|
||||||
|
combine_passphrase_and_salt(&passphrase, &salt)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encrypt_bytes<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<Vec<u8>> {
|
/// 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<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<Vec<u8>> {
|
||||||
let (passphrase, salt) = split_passphrase_and_salt(combined_passphrase_salt)?;
|
let (passphrase, salt) = split_passphrase_and_salt(combined_passphrase_salt)?;
|
||||||
let key = derive_key(passphrase, &salt)?;
|
let key = derive_key(passphrase, &salt)?;
|
||||||
let cipher = Aes256Gcm::new(GenericArray::from_slice(&key));
|
let cipher = Aes256Gcm::new(GenericArray::from_slice(&key));
|
||||||
@ -34,7 +49,12 @@ pub fn encrypt_bytes<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) ->
|
|||||||
Ok(nonce.into_iter().chain(ciphertext).collect())
|
Ok(nonce.into_iter().chain(ciphertext).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decrypt_bytes<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<Vec<u8>> {
|
/// 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<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<Vec<u8>> {
|
||||||
if data.as_ref().len() <= NONCE_LENGTH {
|
if data.as_ref().len() <= NONCE_LENGTH {
|
||||||
return Err(anyhow::anyhow!("Ciphertext too short to include nonce."));
|
return Err(anyhow::anyhow!("Ciphertext too short to include nonce."));
|
||||||
}
|
}
|
||||||
@ -47,18 +67,43 @@ pub fn decrypt_bytes<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) ->
|
|||||||
.map_err(|e| anyhow::anyhow!("Decryption error: {:?}", e))
|
.map_err(|e| anyhow::anyhow!("Decryption error: {:?}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encrypt_string<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<String> {
|
/// Encrypt a string using AES-GCM and return the result as a base64 encoded string.
|
||||||
let encrypted = encrypt_bytes(data.as_ref(), combined_passphrase_salt)?;
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `data`: The string data to encrypt.
|
||||||
|
/// * `combined_passphrase_salt`: The concatenated passphrase and salt.
|
||||||
|
pub fn encrypt_text<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<String> {
|
||||||
|
let encrypted = encrypt_data(data.as_ref(), combined_passphrase_salt)?;
|
||||||
Ok(STANDARD.encode(encrypted))
|
Ok(STANDARD.encode(encrypted))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decrypt_string<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<String> {
|
/// 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<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<String> {
|
||||||
let encrypted = STANDARD.decode(data)?;
|
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)?)
|
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()
|
rand::thread_rng()
|
||||||
.sample_iter(&Alphanumeric)
|
.sample_iter(&Alphanumeric)
|
||||||
.take(30) // e.g., 30 characters
|
.take(30) // e.g., 30 characters
|
||||||
@ -66,13 +111,13 @@ fn generate_passphrase() -> String {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_salt() -> [u8; SALT_LENGTH] {
|
fn generate_random_salt() -> [u8; SALT_LENGTH] {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let salt: [u8; SALT_LENGTH] = rng.gen();
|
let salt: [u8; SALT_LENGTH] = rng.gen();
|
||||||
salt
|
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);
|
let salt_base64 = STANDARD.encode(salt);
|
||||||
format!("{}{}{}", passphrase, CONCATENATED_DELIMITER, salt_base64)
|
format!("{}{}{}", passphrase, CONCATENATED_DELIMITER, salt_base64)
|
||||||
}
|
}
|
||||||
@ -103,16 +148,25 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encrypt_decrypt() {
|
fn encrypt_decrypt_test() {
|
||||||
let secret = generate_encrypt_secret();
|
let secret = generate_encryption_secret();
|
||||||
let data = b"hello world";
|
let data = b"hello world";
|
||||||
let encrypted = encrypt_bytes(data, &secret).unwrap();
|
let encrypted = encrypt_data(data, &secret).unwrap();
|
||||||
let decrypted = decrypt_bytes(encrypted, &secret).unwrap();
|
let decrypted = decrypt_data(encrypted, &secret).unwrap();
|
||||||
assert_eq!(data, decrypted.as_slice());
|
assert_eq!(data, decrypted.as_slice());
|
||||||
|
|
||||||
let s = "123".to_string();
|
let s = "123".to_string();
|
||||||
let encrypted = encrypt_string(&s, &secret).unwrap();
|
let encrypted = encrypt_text(&s, &secret).unwrap();
|
||||||
let decrypted_str = decrypt_string(encrypted, &secret).unwrap();
|
let decrypted_str = decrypt_text(encrypted, &secret).unwrap();
|
||||||
assert_eq!(s, decrypted_str);
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ use anyhow::Result;
|
|||||||
use reqwest::{Response, StatusCode};
|
use reqwest::{Response, StatusCode};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use flowy_encrypt::{decrypt_bytes, encrypt_bytes};
|
use flowy_encrypt::{decrypt_data, encrypt_data};
|
||||||
use flowy_error::{ErrorCode, FlowyError};
|
use flowy_error::{ErrorCode, FlowyError};
|
||||||
use lib_infra::future::{to_fut, Fut};
|
use lib_infra::future::{to_fut, Fut};
|
||||||
|
|
||||||
@ -148,7 +148,7 @@ impl SupabaseBinaryColumnEncoder {
|
|||||||
let value = match encryption_secret {
|
let value = match encryption_secret {
|
||||||
None => hex::encode(value),
|
None => hex::encode(value),
|
||||||
Some(encryption_secret) => {
|
Some(encryption_secret) => {
|
||||||
let encrypt_data = encrypt_bytes(value, encryption_secret)?;
|
let encrypt_data = encrypt_data(value, encryption_secret)?;
|
||||||
hex::encode(encrypt_data)
|
hex::encode(encrypt_data)
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -191,7 +191,7 @@ impl SupabaseBinaryColumnDecoder {
|
|||||||
)),
|
)),
|
||||||
Some(encryption_secret) => {
|
Some(encryption_secret) => {
|
||||||
let encrypt_data = D::decode(s)?;
|
let encrypt_data = D::decode(s)?;
|
||||||
decrypt_bytes(encrypt_data, encryption_secret)
|
decrypt_data(encrypt_data, encryption_secret)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use uuid::Uuid;
|
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 flowy_user_deps::entities::*;
|
||||||
use lib_infra::box_any::BoxAny;
|
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();
|
let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
|
||||||
|
|
||||||
// generate encryption sign
|
// generate encryption sign
|
||||||
let secret = generate_encrypt_secret();
|
let secret = generate_encryption_secret();
|
||||||
let sign = encrypt_string(user.user_id.to_string(), &secret).unwrap();
|
let sign = encrypt_text(user.user_id.to_string(), &secret).unwrap();
|
||||||
|
|
||||||
user_service
|
user_service
|
||||||
.update_user(
|
.update_user(
|
||||||
|
@ -2,7 +2,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use nanoid::nanoid;
|
use nanoid::nanoid;
|
||||||
|
|
||||||
use flowy_encrypt::decrypt_string;
|
use flowy_encrypt::decrypt_text;
|
||||||
use flowy_server::supabase::define::{USER_EMAIL, USER_UUID};
|
use flowy_server::supabase::define::{USER_EMAIL, USER_UUID};
|
||||||
use flowy_test::event_builder::EventBuilder;
|
use flowy_test::event_builder::EventBuilder;
|
||||||
use flowy_test::FlowyCoreTest;
|
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();
|
let user_profile = test.get_user_profile().await.unwrap();
|
||||||
assert!(!user_profile.encryption_sign.is_empty());
|
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());
|
assert_eq!(decryption_sign, user_profile.id.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use flowy_encrypt::generate_encrypt_secret;
|
use flowy_encrypt::generate_encryption_secret;
|
||||||
use flowy_error::FlowyResult;
|
use flowy_error::FlowyResult;
|
||||||
use flowy_sqlite::kv::StorePreferences;
|
use flowy_sqlite::kv::StorePreferences;
|
||||||
use flowy_user_deps::cloud::UserCloudConfig;
|
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";
|
const CLOUD_CONFIG_KEY: &str = "af_user_cloud_config";
|
||||||
|
|
||||||
fn generate_cloud_config(uid: i64, store_preference: &Arc<StorePreferences>) -> UserCloudConfig {
|
fn generate_cloud_config(uid: i64, store_preference: &Arc<StorePreferences>) -> UserCloudConfig {
|
||||||
let config = UserCloudConfig::new(generate_encrypt_secret());
|
let config = UserCloudConfig::new(generate_encryption_secret());
|
||||||
let key = cache_key_for_cloud_config(uid);
|
let key = cache_key_for_cloud_config(uid);
|
||||||
store_preference.set_object(&key, config.clone()).unwrap();
|
store_preference.set_object(&key, config.clone()).unwrap();
|
||||||
config
|
config
|
||||||
|
@ -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_error::{ErrorCode, FlowyError, FlowyResult};
|
||||||
use flowy_user_deps::entities::{EncryptionType, UpdateUserProfileParams, UserCredentials};
|
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<String> {
|
pub fn generate_encryption_sign(&self, uid: i64, encrypt_secret: &str) -> FlowyResult<String> {
|
||||||
let encrypt_sign = encrypt_string(uid.to_string(), encrypt_secret)?;
|
let encrypt_sign = encrypt_text(uid.to_string(), encrypt_secret)?;
|
||||||
Ok(encrypt_sign)
|
Ok(encrypt_sign)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ impl UserManager {
|
|||||||
encrypt_sign: &str,
|
encrypt_sign: &str,
|
||||||
encryption_secret: &str,
|
encryption_secret: &str,
|
||||||
) -> FlowyResult<()> {
|
) -> 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"))?;
|
.map_err(|_| FlowyError::new(ErrorCode::InvalidEncryptSecret, "Invalid decryption secret"))?;
|
||||||
if uid.to_string() == decrypt_str {
|
if uid.to_string() == decrypt_str {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
Loading…
Reference in New Issue
Block a user