refactor: Restructure project layout and clean up dependencies (#30)

This commit introduces a significant reorganization of the project structure to improve maintainability and clarity.

Key changes include:
- Adjusted the directory layout for a more logical module organization.
- Removed unused crate dependencies, reducing the overall project size and potentially speeding up build times.
- Updated import paths and configuration files to reflect the structural changes.
This commit is contained in:
houseme
2025-07-02 19:33:12 +08:00
committed by GitHub
parent 0be4264eb1
commit 5826396cd0
322 changed files with 977 additions and 1542 deletions
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[cfg(not(feature = "fips"))]
mod aes;
#[cfg(any(test, feature = "crypto"))]
pub(crate) mod id;
pub(crate) mod decrypt;
pub(crate) mod encrypt;
#[cfg(test)]
mod tests;
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub fn native_aes() -> bool {
cfg_if::cfg_if! {
if #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] {
std::is_x86_feature_detected!("aes") && std::is_x86_feature_detected!("pclmulqdq")
} else if #[cfg(target_arch = "aarch64")] {
std::arch::is_aarch64_feature_detected!("aes")
} else if #[cfg(target_arch = "powerpc64")] {
false
} else if #[cfg(target_arch = "s390x")] {
std::is_s390x_feature_detected!("aes")
&& std::is_s390x_feature_detected!("aescbc")
&& std::is_s390x_feature_detected!("aesctr")
&& (std::is_s390x_feature_detected!("aesgcm") || std::is_s390x_feature_detected!("ghash"))
} else {
false
}
}
}
+148
View File
@@ -0,0 +1,148 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[cfg(any(test, feature = "crypto"))]
pub fn decrypt_data(password: &[u8], data: &[u8]) -> Result<Vec<u8>, crate::Error> {
use crate::encdec::id::ID;
use crate::error::Error;
use aes_gcm::{Aes256Gcm, KeyInit as _};
use chacha20poly1305::ChaCha20Poly1305;
// 32: salt
// 1: id
// 12: nonce
const HEADER_LENGTH: usize = 45;
if data.len() < HEADER_LENGTH {
return Err(Error::ErrUnexpectedHeader);
}
let (salt, id, nonce) = (&data[..32], ID::try_from(data[32])?, &data[33..45]);
let data = &data[HEADER_LENGTH..];
match id {
ID::Argon2idChaCHa20Poly1305 => {
let key = id.get_key(password, salt)?;
decryp(ChaCha20Poly1305::new_from_slice(&key)?, nonce, data)
}
_ => {
let key = id.get_key(password, salt)?;
decryp(Aes256Gcm::new_from_slice(&key)?, nonce, data)
}
}
}
// use argon2::{Argon2, PasswordHasher};
// use argon2::password_hash::{SaltString};
// use aes_gcm::{Aes256Gcm, Key, Nonce}; // For AES-GCM
// use chacha20poly1305::{ChaCha20Poly1305, Key as ChaChaKey, Nonce as ChaChaNonce}; // For ChaCha20
// use pbkdf2::pbkdf2;
// use sha2::Sha256;
// use std::io::{self, Read};
// use thiserror::Error;
// #[derive(Debug, Error)]
// pub enum DecryptError {
// #[error("unexpected header")]
// UnexpectedHeader,
// #[error("invalid encryption algorithm ID")]
// InvalidAlgorithmId,
// #[error("IO error")]
// Io(#[from] io::Error),
// #[error("decryption error")]
// DecryptionError,
// }
// pub fn decrypt_data2<R: Read>(password: &str, mut data: R) -> Result<Vec<u8>, DecryptError> {
// // Parse the stream header
// let mut hdr = [0u8; 32 + 1 + 8];
// if data.read_exact(&mut hdr).is_err() {
// return Err(DecryptError::UnexpectedHeader);
// }
// let salt = &hdr[0..32];
// let id = hdr[32];
// let nonce = &hdr[33..41];
// let key = match id {
// // Argon2id + AES-GCM
// 0x01 => {
// let salt = SaltString::encode_b64(salt).map_err(|_| DecryptError::DecryptionError)?;
// let argon2 = Argon2::default();
// let hashed_key = argon2.hash_password(password.as_bytes(), &salt)
// .map_err(|_| DecryptError::DecryptionError)?;
// hashed_key.hash.unwrap().as_bytes().to_vec()
// }
// // Argon2id + ChaCha20Poly1305
// 0x02 => {
// let salt = SaltString::encode_b64(salt).map_err(|_| DecryptError::DecryptionError)?;
// let argon2 = Argon2::default();
// let hashed_key = argon2.hash_password(password.as_bytes(), &salt)
// .map_err(|_| DecryptError::DecryptionError)?;
// hashed_key.hash.unwrap().as_bytes().to_vec()
// }
// // PBKDF2 + AES-GCM
// // 0x03 => {
// // let mut key = [0u8; 32];
// // pbkdf2::<Sha256>(password.as_bytes(), salt, 10000, &mut key);
// // key.to_vec()
// // }
// _ => return Err(DecryptError::InvalidAlgorithmId),
// };
// // Decrypt data using the corresponding cipher
// let mut encrypted_data = Vec::new();
// data.read_to_end(&mut encrypted_data)?;
// let plaintext = match id {
// 0x01 => {
// let cipher = Aes256Gcm::new(Key::from_slice(&key));
// let nonce = Nonce::from_slice(nonce);
// cipher
// .decrypt(nonce, encrypted_data.as_ref())
// .map_err(|_| DecryptError::DecryptionError)?
// }
// 0x02 => {
// let cipher = ChaCha20Poly1305::new(ChaChaKey::from_slice(&key));
// let nonce = ChaChaNonce::from_slice(nonce);
// cipher
// .decrypt(nonce, encrypted_data.as_ref())
// .map_err(|_| DecryptError::DecryptionError)?
// }
// 0x03 => {
// let cipher = Aes256Gcm::new(Key::from_slice(&key));
// let nonce = Nonce::from_slice(nonce);
// cipher
// .decrypt(nonce, encrypted_data.as_ref())
// .map_err(|_| DecryptError::DecryptionError)?
// }
// _ => return Err(DecryptError::InvalidAlgorithmId),
// };
// Ok(plaintext)
// }
#[cfg(any(test, feature = "crypto"))]
#[inline]
fn decryp<T: aes_gcm::aead::Aead>(stream: T, nonce: &[u8], data: &[u8]) -> Result<Vec<u8>, crate::Error> {
use crate::error::Error;
stream
.decrypt(aes_gcm::Nonce::from_slice(nonce), data)
.map_err(Error::ErrDecryptFailed)
}
#[cfg(not(any(test, feature = "crypto")))]
pub fn decrypt_data(_password: &[u8], data: &[u8]) -> Result<Vec<u8>, crate::Error> {
Ok(data.to_vec())
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[cfg(any(test, feature = "crypto"))]
pub fn encrypt_data(password: &[u8], data: &[u8]) -> Result<Vec<u8>, crate::Error> {
use crate::encdec::id::ID;
use aes_gcm::Aes256Gcm;
use aes_gcm::KeyInit as _;
use rand::random;
let salt: [u8; 32] = random();
#[cfg(feature = "fips")]
let id = ID::Pbkdf2AESGCM;
#[cfg(not(feature = "fips"))]
let id = if native_aes() {
ID::Argon2idAESGCM
} else {
ID::Argon2idChaCHa20Poly1305
};
let key = id.get_key(password, &salt)?;
#[cfg(feature = "fips")]
{
encrypt(Aes256Gcm::new_from_slice(&key)?, &salt, id, data)
}
#[cfg(not(feature = "fips"))]
{
if native_aes() {
encrypt(Aes256Gcm::new_from_slice(&key)?, &salt, id, data)
} else {
encrypt(ChaCha20Poly1305::new_from_slice(&key)?, &salt, id, data)
}
}
}
#[cfg(any(test, feature = "crypto"))]
fn encrypt<T: aes_gcm::aead::Aead>(
stream: T,
salt: &[u8],
id: crate::encdec::id::ID,
data: &[u8],
) -> Result<Vec<u8>, crate::Error> {
use crate::error::Error;
use aes_gcm::aead::rand_core::OsRng;
let nonce = T::generate_nonce(&mut OsRng);
let encryptor = stream.encrypt(&nonce, data).map_err(Error::ErrEncryptFailed)?;
let mut ciphertext = Vec::with_capacity(salt.len() + 1 + nonce.len() + encryptor.len());
ciphertext.extend_from_slice(salt);
ciphertext.push(id as u8);
ciphertext.extend_from_slice(nonce.as_slice());
ciphertext.extend_from_slice(&encryptor);
Ok(ciphertext)
}
#[cfg(not(any(test, feature = "crypto")))]
pub fn encrypt_data(_password: &[u8], data: &[u8]) -> Result<Vec<u8>, crate::Error> {
Ok(data.to_vec())
}
+271
View File
@@ -0,0 +1,271 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use argon2::{Algorithm, Argon2, Params, Version};
use pbkdf2::pbkdf2_hmac;
use sha2::Sha256;
#[repr(u8)]
#[derive(Debug, Clone, Copy)]
pub(crate) enum ID {
Argon2idAESGCM = 0x00,
Argon2idChaCHa20Poly1305 = 0x01,
Pbkdf2AESGCM = 0x02,
}
impl TryFrom<u8> for ID {
type Error = crate::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x00 => Ok(Self::Argon2idAESGCM),
0x01 => Ok(Self::Argon2idChaCHa20Poly1305),
0x02 => Ok(Self::Pbkdf2AESGCM),
_ => Err(crate::Error::ErrInvalidAlgID(value)),
}
}
}
impl ID {
pub(crate) fn get_key(&self, password: &[u8], salt: &[u8]) -> Result<[u8; 32], crate::Error> {
let mut key = [0u8; 32];
match self {
ID::Pbkdf2AESGCM => pbkdf2_hmac::<Sha256>(password, salt, 8192, &mut key),
_ => {
let params = Params::new(64 * 1024, 1, 4, Some(32))?;
let argon_2id = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
argon_2id.hash_password_into(password, salt, &mut key)?;
}
}
Ok(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_id_enum_values() {
// Test enum discriminant values
assert_eq!(ID::Argon2idAESGCM as u8, 0x00);
assert_eq!(ID::Argon2idChaCHa20Poly1305 as u8, 0x01);
assert_eq!(ID::Pbkdf2AESGCM as u8, 0x02);
}
#[test]
fn test_id_try_from_valid_values() {
// Test valid conversions from u8 to ID
assert!(matches!(ID::try_from(0x00), Ok(ID::Argon2idAESGCM)));
assert!(matches!(ID::try_from(0x01), Ok(ID::Argon2idChaCHa20Poly1305)));
assert!(matches!(ID::try_from(0x02), Ok(ID::Pbkdf2AESGCM)));
}
#[test]
fn test_id_try_from_invalid_values() {
// Test invalid conversions from u8 to ID
assert!(ID::try_from(0x03).is_err());
assert!(ID::try_from(0xFF).is_err());
assert!(ID::try_from(100).is_err());
// Verify error type
if let Err(crate::Error::ErrInvalidAlgID(value)) = ID::try_from(0x03) {
assert_eq!(value, 0x03);
} else {
panic!("Expected ErrInvalidAlgID error");
}
}
#[test]
fn test_id_debug_format() {
// Test Debug trait implementation
let argon2_aes = ID::Argon2idAESGCM;
let argon2_chacha = ID::Argon2idChaCHa20Poly1305;
let pbkdf2 = ID::Pbkdf2AESGCM;
assert_eq!(format!("{argon2_aes:?}"), "Argon2idAESGCM");
assert_eq!(format!("{argon2_chacha:?}"), "Argon2idChaCHa20Poly1305");
assert_eq!(format!("{pbkdf2:?}"), "Pbkdf2AESGCM");
}
#[test]
fn test_id_clone_and_copy() {
// Test Clone and Copy traits
let original = ID::Argon2idAESGCM;
let cloned = original;
let copied = original;
assert!(matches!(cloned, ID::Argon2idAESGCM));
assert!(matches!(copied, ID::Argon2idAESGCM));
}
#[test]
fn test_pbkdf2_key_generation() {
// Test PBKDF2 key generation
let id = ID::Pbkdf2AESGCM;
let password = b"test_password";
let salt = b"test_salt_16bytes";
let result = id.get_key(password, salt);
assert!(result.is_ok());
let key = result.expect("PBKDF2 key generation should succeed");
assert_eq!(key.len(), 32);
// Verify deterministic behavior - same inputs should produce same output
let result2 = id.get_key(password, salt);
assert!(result2.is_ok());
assert_eq!(key, result2.expect("PBKDF2 key generation should succeed"));
}
#[test]
fn test_argon2_key_generation() {
// Test Argon2id key generation
let id = ID::Argon2idAESGCM;
let password = b"test_password";
let salt = b"test_salt_16bytes";
let result = id.get_key(password, salt);
assert!(result.is_ok());
let key = result.expect("Argon2id key generation should succeed");
assert_eq!(key.len(), 32);
// Verify deterministic behavior
let result2 = id.get_key(password, salt);
assert!(result2.is_ok());
assert_eq!(key, result2.expect("Argon2id key generation should succeed"));
}
#[test]
fn test_argon2_chacha_key_generation() {
// Test Argon2id ChaCha20Poly1305 key generation
let id = ID::Argon2idChaCHa20Poly1305;
let password = b"test_password";
let salt = b"test_salt_16bytes";
let result = id.get_key(password, salt);
assert!(result.is_ok());
let key = result.expect("Argon2id ChaCha20Poly1305 key generation should succeed");
assert_eq!(key.len(), 32);
}
#[test]
fn test_key_generation_with_different_passwords() {
// Test that different passwords produce different keys
let id = ID::Pbkdf2AESGCM;
let salt = b"same_salt_for_all";
let key1 = id
.get_key(b"password1", salt)
.expect("Key generation with password1 should succeed");
let key2 = id
.get_key(b"password2", salt)
.expect("Key generation with password2 should succeed");
assert_ne!(key1, key2);
}
#[test]
fn test_key_generation_with_different_salts() {
// Test that different salts produce different keys
let id = ID::Pbkdf2AESGCM;
let password = b"same_password";
let key1 = id
.get_key(password, b"salt1_16_bytes__")
.expect("Key generation with salt1 should succeed");
let key2 = id
.get_key(password, b"salt2_16_bytes__")
.expect("Key generation with salt2 should succeed");
assert_ne!(key1, key2);
}
#[test]
fn test_key_generation_with_empty_inputs() {
// Test key generation with empty password and salt
let id = ID::Pbkdf2AESGCM;
let result1 = id.get_key(b"", b"salt");
assert!(result1.is_ok());
let result2 = id.get_key(b"password", b"");
assert!(result2.is_ok());
let result3 = id.get_key(b"", b"");
assert!(result3.is_ok());
}
#[test]
fn test_all_algorithms_produce_valid_keys() {
// Test that all algorithm variants can generate valid keys
let algorithms = [ID::Argon2idAESGCM, ID::Argon2idChaCHa20Poly1305, ID::Pbkdf2AESGCM];
let password = b"test_password_123";
let salt = b"test_salt_16bytes";
for algorithm in &algorithms {
let result = algorithm.get_key(password, salt);
assert!(result.is_ok(), "Algorithm {algorithm:?} should generate valid key");
let key = result.expect("Key generation should succeed for all algorithms");
assert_eq!(key.len(), 32, "Key length should be 32 bytes for {algorithm:?}");
// Verify key is not all zeros (very unlikely with proper implementation)
assert_ne!(key, [0u8; 32], "Key should not be all zeros for {algorithm:?}");
}
}
#[test]
fn test_round_trip_conversion() {
// Test round-trip conversion: ID -> u8 -> ID
let original_ids = [ID::Argon2idAESGCM, ID::Argon2idChaCHa20Poly1305, ID::Pbkdf2AESGCM];
for original in &original_ids {
let as_u8 = *original as u8;
let converted_back = ID::try_from(as_u8).expect("Round-trip conversion should succeed");
assert!(matches!(
(original, converted_back),
(ID::Argon2idAESGCM, ID::Argon2idAESGCM)
| (ID::Argon2idChaCHa20Poly1305, ID::Argon2idChaCHa20Poly1305)
| (ID::Pbkdf2AESGCM, ID::Pbkdf2AESGCM)
));
}
}
#[test]
fn test_key_generation_consistency_across_algorithms() {
// Test that different algorithms produce different keys for same input
let password = b"consistent_password";
let salt = b"consistent_salt_";
let key_argon2_aes = ID::Argon2idAESGCM
.get_key(password, salt)
.expect("Argon2id AES key generation should succeed");
let key_argon2_chacha = ID::Argon2idChaCHa20Poly1305
.get_key(password, salt)
.expect("Argon2id ChaCha key generation should succeed");
let key_pbkdf2 = ID::Pbkdf2AESGCM
.get_key(password, salt)
.expect("PBKDF2 key generation should succeed");
// Different algorithms should produce different keys
assert_ne!(key_argon2_aes, key_pbkdf2);
assert_ne!(key_argon2_chacha, key_pbkdf2);
// Note: Argon2 variants might produce same key since they use same algorithm
}
}
+319
View File
@@ -0,0 +1,319 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::{decrypt_data, encrypt_data};
const PASSWORD: &[u8] = "test_password".as_bytes();
const LONG_PASSWORD: &[u8] = "very_long_password_with_many_characters_for_testing_purposes_123456789".as_bytes();
const EMPTY_PASSWORD: &[u8] = b"";
#[test_case::test_case("hello world".as_bytes())]
#[test_case::test_case(&[])]
#[test_case::test_case(&[1, 2, 3])]
#[test_case::test_case(&[3, 2, 1])]
fn test_basic_encrypt_decrypt_roundtrip(input: &[u8]) -> Result<(), crate::Error> {
let encrypted = encrypt_data(PASSWORD, input)?;
let decrypted = decrypt_data(PASSWORD, &encrypted)?;
assert_eq!(input, decrypted, "input is not equal output");
Ok(())
}
#[test]
fn test_encrypt_decrypt_with_different_passwords() -> Result<(), crate::Error> {
let data = b"sensitive data";
let password1 = b"password1";
let password2 = b"password2";
let encrypted = encrypt_data(password1, data)?;
// Decrypting with correct password should work
let decrypted = decrypt_data(password1, &encrypted)?;
assert_eq!(data, decrypted.as_slice());
// Decrypting with wrong password should fail
let result = decrypt_data(password2, &encrypted);
assert!(result.is_err(), "Decryption with wrong password should fail");
Ok(())
}
#[test]
fn test_encrypt_decrypt_empty_data() -> Result<(), crate::Error> {
let empty_data = b"";
let encrypted = encrypt_data(PASSWORD, empty_data)?;
let decrypted = decrypt_data(PASSWORD, &encrypted)?;
assert_eq!(empty_data, decrypted.as_slice());
Ok(())
}
#[test]
fn test_encrypt_decrypt_large_data() -> Result<(), crate::Error> {
// Test with 1MB of data
let large_data = vec![0xAB; 1024 * 1024];
let encrypted = encrypt_data(PASSWORD, &large_data)?;
let decrypted = decrypt_data(PASSWORD, &encrypted)?;
assert_eq!(large_data, decrypted);
Ok(())
}
#[test]
fn test_encrypt_decrypt_with_empty_password() -> Result<(), crate::Error> {
let data = b"test data";
let encrypted = encrypt_data(EMPTY_PASSWORD, data)?;
let decrypted = decrypt_data(EMPTY_PASSWORD, &encrypted)?;
assert_eq!(data, decrypted.as_slice());
Ok(())
}
#[test]
fn test_encrypt_decrypt_with_long_password() -> Result<(), crate::Error> {
let data = b"test data with long password";
let encrypted = encrypt_data(LONG_PASSWORD, data)?;
let decrypted = decrypt_data(LONG_PASSWORD, &encrypted)?;
assert_eq!(data, decrypted.as_slice());
Ok(())
}
#[test]
fn test_encrypt_decrypt_binary_data() -> Result<(), crate::Error> {
// Test with various binary patterns
let binary_patterns = [
vec![0x00; 100], // All zeros
vec![0xFF; 100], // All ones
(0..=255u8).cycle().take(1000).collect::<Vec<u8>>(), // Sequential pattern
[0xAA, 0x55].repeat(500), // Alternating pattern
];
for pattern in &binary_patterns {
let encrypted = encrypt_data(PASSWORD, pattern)?;
let decrypted = decrypt_data(PASSWORD, &encrypted)?;
assert_eq!(pattern, &decrypted, "Binary pattern mismatch");
}
Ok(())
}
#[test]
fn test_encrypt_decrypt_unicode_data() -> Result<(), crate::Error> {
let unicode_strings = [
"Hello, 世界! 🌍",
"Тест на русском языке",
"العربية اختبار",
"🚀🔐💻🌟⭐",
"Mixed: ASCII + 中文 + العربية + 🎉",
];
for text in &unicode_strings {
let data = text.as_bytes();
let encrypted = encrypt_data(PASSWORD, data)?;
let decrypted = decrypt_data(PASSWORD, &encrypted)?;
assert_eq!(data, decrypted.as_slice(), "Unicode data mismatch for: {text}");
}
Ok(())
}
#[test]
fn test_decrypt_with_corrupted_data() {
let data = b"test data";
let encrypted = encrypt_data(PASSWORD, data).expect("Encryption should succeed");
// Test various corruption scenarios
let corruption_tests = [
(0, "Corrupt first byte"),
(encrypted.len() - 1, "Corrupt last byte"),
(encrypted.len() / 2, "Corrupt middle byte"),
];
for (corrupt_index, description) in &corruption_tests {
let mut corrupted = encrypted.clone();
corrupted[*corrupt_index] ^= 0xFF; // Flip all bits
let result = decrypt_data(PASSWORD, &corrupted);
assert!(result.is_err(), "{description} should cause decryption to fail");
}
}
#[test]
fn test_decrypt_with_truncated_data() {
let data = b"test data for truncation";
let encrypted = encrypt_data(PASSWORD, data).expect("Encryption should succeed");
// Test truncation at various lengths
let truncation_lengths = [
0, // Empty data
10, // Very short
32, // Salt length
44, // Just before nonce
encrypted.len() - 1, // Missing last byte
];
for &length in &truncation_lengths {
let truncated = &encrypted[..length.min(encrypted.len())];
let result = decrypt_data(PASSWORD, truncated);
assert!(result.is_err(), "Truncated data (length {length}) should cause decryption to fail");
}
}
#[test]
fn test_decrypt_with_invalid_header() {
let data = b"test data";
let mut encrypted = encrypt_data(PASSWORD, data).expect("Encryption should succeed");
// Corrupt the algorithm ID (byte 32)
if encrypted.len() > 32 {
encrypted[32] = 0xFF; // Invalid algorithm ID
let result = decrypt_data(PASSWORD, &encrypted);
assert!(result.is_err(), "Invalid algorithm ID should cause decryption to fail");
}
}
#[test]
fn test_encryption_produces_different_outputs() -> Result<(), crate::Error> {
let data = b"same data";
// Encrypt the same data multiple times
let encrypted1 = encrypt_data(PASSWORD, data)?;
let encrypted2 = encrypt_data(PASSWORD, data)?;
// Encrypted outputs should be different due to random salt and nonce
assert_ne!(encrypted1, encrypted2, "Encryption should produce different outputs for same input");
// But both should decrypt to the same original data
let decrypted1 = decrypt_data(PASSWORD, &encrypted1)?;
let decrypted2 = decrypt_data(PASSWORD, &encrypted2)?;
assert_eq!(decrypted1, decrypted2);
assert_eq!(data, decrypted1.as_slice());
Ok(())
}
#[test]
fn test_encrypted_data_structure() -> Result<(), crate::Error> {
let data = b"test data";
let encrypted = encrypt_data(PASSWORD, data)?;
// Encrypted data should be longer than original (due to salt, nonce, tag)
assert!(encrypted.len() > data.len(), "Encrypted data should be longer than original");
// Should have at least: 32 bytes salt + 1 byte ID + 12 bytes nonce + data + 16 bytes tag
let min_expected_length = 32 + 1 + 12 + data.len() + 16;
assert!(
encrypted.len() >= min_expected_length,
"Encrypted data length {} should be at least {}",
encrypted.len(),
min_expected_length
);
Ok(())
}
#[test]
fn test_password_variations() -> Result<(), crate::Error> {
let data = b"test data";
let password_variations = [
b"a".as_slice(), // Single character
b"12345".as_slice(), // Numeric
b"!@#$%^&*()".as_slice(), // Special characters
b"\x00\x01\x02\x03".as_slice(), // Binary password
"密码测试".as_bytes(), // Unicode password
&[0xFF; 64], // Long binary password
];
for password in &password_variations {
let encrypted = encrypt_data(password, data)?;
let decrypted = decrypt_data(password, &encrypted)?;
assert_eq!(data, decrypted.as_slice(), "Failed with password: {password:?}");
}
Ok(())
}
#[test]
fn test_deterministic_with_same_salt_and_nonce() {
// Note: This test is more for understanding the behavior
// In real implementation, salt and nonce should be random
let data = b"test data";
let encrypted1 = encrypt_data(PASSWORD, data).expect("Encryption should succeed");
let encrypted2 = encrypt_data(PASSWORD, data).expect("Encryption should succeed");
// Due to random salt and nonce, outputs should be different
assert_ne!(encrypted1, encrypted2, "Encryption should use random salt/nonce");
}
#[test]
fn test_cross_platform_compatibility() -> Result<(), crate::Error> {
// Test data that might behave differently on different platforms
let test_cases = [
vec![0x00, 0x01, 0x02, 0x03], // Low values
vec![0xFC, 0xFD, 0xFE, 0xFF], // High values
(0..256u16).map(|x| (x % 256) as u8).collect::<Vec<u8>>(), // Full byte range
];
for test_data in &test_cases {
let encrypted = encrypt_data(PASSWORD, test_data)?;
let decrypted = decrypt_data(PASSWORD, &encrypted)?;
assert_eq!(test_data, &decrypted, "Cross-platform compatibility failed");
}
Ok(())
}
#[test]
fn test_memory_safety_with_large_passwords() -> Result<(), crate::Error> {
let data = b"test data";
// Test with very large passwords
let large_passwords = [
vec![b'a'; 1024], // 1KB password
vec![b'x'; 10 * 1024], // 10KB password
(0..=255u8).cycle().take(5000).collect::<Vec<u8>>(), // 5KB varied password
];
for password in &large_passwords {
let encrypted = encrypt_data(password, data)?;
let decrypted = decrypt_data(password, &encrypted)?;
assert_eq!(data, decrypted.as_slice(), "Failed with large password of size {}", password.len());
}
Ok(())
}
#[test]
fn test_concurrent_encryption_safety() -> Result<(), crate::Error> {
use std::sync::Arc;
use std::thread;
let data = Arc::new(b"concurrent test data".to_vec());
let password = Arc::new(b"concurrent_password".to_vec());
let handles: Vec<_> = (0..10)
.map(|i| {
let data = Arc::clone(&data);
let password = Arc::clone(&password);
thread::spawn(move || {
let encrypted = encrypt_data(&password, &data).expect("Encryption should succeed");
let decrypted = decrypt_data(&password, &encrypted).expect("Decryption should succeed");
assert_eq!(**data, decrypted, "Thread {i} failed");
})
})
.collect();
for handle in handles {
handle.join().expect("Thread should complete successfully");
}
Ok(())
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("unexpected header")]
ErrUnexpectedHeader,
#[error("invalid encryption algorithm ID: {0}")]
ErrInvalidAlgID(u8),
#[cfg(any(test, feature = "crypto"))]
#[error("{0}")]
ErrInvalidLength(#[from] sha2::digest::InvalidLength),
#[cfg(any(test, feature = "crypto"))]
#[error("encrypt failed")]
ErrEncryptFailed(aes_gcm::aead::Error),
#[cfg(any(test, feature = "crypto"))]
#[error("decrypt failed")]
ErrDecryptFailed(aes_gcm::aead::Error),
#[cfg(any(test, feature = "crypto"))]
#[error("argon2 err: {0}")]
ErrArgon2(#[from] argon2::Error),
#[error("jwt err: {0}")]
ErrJwt(#[from] jsonwebtoken::errors::Error),
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod decode;
pub mod encode;
pub use serde_json::Value as Claims;
#[cfg(test)]
mod tests;
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use jsonwebtoken::{Algorithm, DecodingKey, TokenData, Validation};
use crate::Error;
use crate::jwt::Claims;
pub fn decode(token: &str, token_secret: &[u8]) -> Result<TokenData<Claims>, Error> {
Ok(jsonwebtoken::decode(
token,
&DecodingKey::from_secret(token_secret),
&Validation::new(Algorithm::HS512),
)?)
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use jsonwebtoken::{Algorithm, EncodingKey, Header};
use crate::Error;
use crate::jwt::Claims;
pub fn encode(token_secret: &[u8], claims: &Claims) -> Result<String, Error> {
Ok(jsonwebtoken::encode(
&Header::new(Algorithm::HS512),
claims,
&EncodingKey::from_secret(token_secret),
)?)
}
+354
View File
@@ -0,0 +1,354 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use serde_json::json;
use time::OffsetDateTime;
use super::{decode::decode, encode::encode};
#[test]
fn test_jwt_encode_decode_basic() {
let claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 1000,
"sub": "user123",
"iat": OffsetDateTime::now_utc().unix_timestamp(),
"role": "admin"
});
let secret = b"test_secret_key";
let jwt_token = encode(secret, &claims).expect("Failed to encode JWT");
let decoded = decode(&jwt_token, secret).expect("Failed to decode JWT");
assert_eq!(decoded.claims, claims);
}
#[test]
fn test_jwt_encode_decode_with_complex_claims() {
let claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 3600,
"sub": "user456",
"iat": OffsetDateTime::now_utc().unix_timestamp(),
"permissions": ["read", "write", "delete"],
"metadata": {
"department": "engineering",
"level": 5,
"active": true
},
"custom_field": null
});
let secret = b"complex_secret_key_123";
let jwt_token = encode(secret, &claims).expect("Failed to encode complex JWT");
let decoded = decode(&jwt_token, secret).expect("Failed to decode complex JWT");
assert_eq!(decoded.claims, claims);
}
#[test]
fn test_jwt_decode_with_wrong_secret() {
let claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 1000,
"sub": "user123"
});
let correct_secret = b"correct_secret";
let wrong_secret = b"wrong_secret";
let jwt_token = encode(correct_secret, &claims).expect("Failed to encode JWT");
// Decoding with wrong secret should fail
let result = decode(&jwt_token, wrong_secret);
assert!(result.is_err(), "Decoding with wrong secret should fail");
}
#[test]
fn test_jwt_decode_invalid_token_format() {
let secret = b"test_secret";
// Test various invalid token formats
let invalid_tokens = [
"", // Empty token
"invalid", // Not a JWT format
"header.payload", // Missing signature
"header.payload.signature.extra", // Too many parts
"invalid.header.signature", // Invalid base64
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.invalid.signature", // Invalid payload
];
for invalid_token in &invalid_tokens {
let result = decode(invalid_token, secret);
assert!(result.is_err(), "Invalid token '{invalid_token}' should fail to decode");
}
}
#[test]
fn test_jwt_with_expired_token() {
let expired_claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() - 1000, // Expired 1000 seconds ago
"sub": "user123",
"iat": OffsetDateTime::now_utc().unix_timestamp() - 2000
});
let secret = b"test_secret";
let jwt_token = encode(secret, &expired_claims).expect("Failed to encode expired JWT");
// Decoding expired token should fail
let result = decode(&jwt_token, secret);
assert!(result.is_err(), "Expired token should fail to decode");
}
#[test]
fn test_jwt_with_future_issued_at() {
let future_claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 3600,
"sub": "user123",
"iat": OffsetDateTime::now_utc().unix_timestamp() + 1000 // Issued in future
});
let secret = b"test_secret";
let jwt_token = encode(secret, &future_claims).expect("Failed to encode future JWT");
// Note: The current JWT implementation may not validate iat by default
// This test documents the current behavior - future iat tokens may still decode successfully
let result = decode(&jwt_token, secret);
// For now, we just verify the token can be decoded, but in a production system
// you might want to add custom validation for iat claims
assert!(
result.is_ok(),
"Token decoding should succeed, but iat validation should be handled separately"
);
}
#[test]
fn test_jwt_with_empty_claims() {
let empty_claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 1000, // Add required exp claim
});
let secret = b"test_secret";
let jwt_token = encode(secret, &empty_claims).expect("Failed to encode empty claims JWT");
let decoded = decode(&jwt_token, secret).expect("Failed to decode empty claims JWT");
assert_eq!(decoded.claims, empty_claims);
}
#[test]
fn test_jwt_with_different_secret_lengths() {
let claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 1000,
"sub": "user123"
});
// Test with various secret lengths
let secrets = [
b"a".as_slice(), // Very short
b"short_key".as_slice(), // Short
b"medium_length_secret_key".as_slice(), // Medium
b"very_long_secret_key_with_many_characters_for_testing_purposes".as_slice(), // Long
];
for secret in &secrets {
let jwt_token =
encode(secret, &claims).unwrap_or_else(|_| panic!("Failed to encode JWT with secret length {}", secret.len()));
let decoded =
decode(&jwt_token, secret).unwrap_or_else(|_| panic!("Failed to decode JWT with secret length {}", secret.len()));
assert_eq!(decoded.claims, claims);
}
}
#[test]
fn test_jwt_with_special_characters_in_claims() {
let claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 1000,
"sub": "user@example.com",
"name": "John Doe",
"description": "User with special chars: !@#$%^&*()_+-=[]{}|;':\",./<>?",
"unicode": "测试用户 🚀 émojis",
"newlines": "line1\nline2\r\nline3",
"quotes": "He said \"Hello\" and she replied 'Hi'"
});
let secret = b"test_secret";
let jwt_token = encode(secret, &claims).expect("Failed to encode JWT with special characters");
let decoded = decode(&jwt_token, secret).expect("Failed to decode JWT with special characters");
assert_eq!(decoded.claims, claims);
}
#[test]
fn test_jwt_with_large_payload() {
// Create a large payload to test size limits
let large_data = "x".repeat(10000); // 10KB of data
let claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 1000,
"sub": "user123",
"large_field": large_data
});
let secret = b"test_secret";
let jwt_token = encode(secret, &claims).expect("Failed to encode large JWT");
let decoded = decode(&jwt_token, secret).expect("Failed to decode large JWT");
assert_eq!(decoded.claims, claims);
}
#[test]
fn test_jwt_token_structure() {
let claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 1000,
"sub": "user123"
});
let secret = b"test_secret";
let jwt_token = encode(secret, &claims).expect("Failed to encode JWT");
// JWT should have exactly 3 parts separated by dots
let parts: Vec<&str> = jwt_token.split('.').collect();
assert_eq!(parts.len(), 3, "JWT should have exactly 3 parts");
// Each part should be non-empty
for (i, part) in parts.iter().enumerate() {
assert!(!part.is_empty(), "JWT part {i} should not be empty");
}
}
#[test]
fn test_jwt_deterministic_encoding() {
let claims = json!({
"exp": 1234567890, // Fixed timestamp for deterministic test
"sub": "user123",
"iat": 1234567800
});
let secret = b"test_secret";
// Encode the same claims multiple times
let token1 = encode(secret, &claims).expect("Failed to encode JWT 1");
let token2 = encode(secret, &claims).expect("Failed to encode JWT 2");
// Tokens should be identical for same input
assert_eq!(token1, token2, "JWT encoding should be deterministic");
}
#[test]
fn test_jwt_cross_compatibility() {
let claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 1000,
"sub": "user123"
});
let secret1 = b"secret1";
let secret2 = b"secret2";
// Encode with secret1
let token1 = encode(secret1, &claims).expect("Failed to encode with secret1");
// Decode with secret1 should work
let decoded1 = decode(&token1, secret1).expect("Failed to decode with correct secret");
assert_eq!(decoded1.claims, claims);
// Decode with secret2 should fail
let result2 = decode(&token1, secret2);
assert!(result2.is_err(), "Decoding with different secret should fail");
}
#[test]
fn test_jwt_header_algorithm() {
let claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 1000,
"sub": "user123"
});
let secret = b"test_secret";
let jwt_token = encode(secret, &claims).expect("Failed to encode JWT");
let decoded = decode(&jwt_token, secret).expect("Failed to decode JWT");
// Verify the algorithm in header is HS512
assert_eq!(decoded.header.alg, jsonwebtoken::Algorithm::HS512);
assert_eq!(decoded.header.typ, Some("JWT".to_string()));
}
#[test]
fn test_jwt_claims_validation() {
let now = OffsetDateTime::now_utc().unix_timestamp();
let valid_claims = json!({
"exp": now + 3600, // Expires in 1 hour
"iat": now - 60, // Issued 1 minute ago
"nbf": now - 30, // Not before 30 seconds ago
"sub": "user123"
});
let secret = b"test_secret";
let jwt_token = encode(secret, &valid_claims).expect("Failed to encode valid JWT");
let decoded = decode(&jwt_token, secret).expect("Failed to decode valid JWT");
assert_eq!(decoded.claims, valid_claims);
}
#[test]
fn test_jwt_with_numeric_claims() {
let claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 1000,
"sub": "user123",
"age": 25,
"score": 95.5,
"count": 0,
"negative": -10
});
let secret = b"test_secret";
let jwt_token = encode(secret, &claims).expect("Failed to encode numeric JWT");
let decoded = decode(&jwt_token, secret).expect("Failed to decode numeric JWT");
assert_eq!(decoded.claims, claims);
}
#[test]
fn test_jwt_with_boolean_claims() {
let claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 1000,
"sub": "user123",
"is_admin": true,
"is_active": false,
"email_verified": true
});
let secret = b"test_secret";
let jwt_token = encode(secret, &claims).expect("Failed to encode boolean JWT");
let decoded = decode(&jwt_token, secret).expect("Failed to decode boolean JWT");
assert_eq!(decoded.claims, claims);
}
#[test]
fn test_jwt_with_array_claims() {
let claims = json!({
"exp": OffsetDateTime::now_utc().unix_timestamp() + 1000,
"sub": "user123",
"roles": ["admin", "user", "moderator"],
"permissions": [1, 2, 3, 4, 5],
"tags": [],
"mixed_array": ["string", 123, true, null]
});
let secret = b"test_secret";
let jwt_token = encode(secret, &claims).expect("Failed to encode array JWT");
let decoded = decode(&jwt_token, secret).expect("Failed to decode array JWT");
assert_eq!(decoded.claims, claims);
}
+24
View File
@@ -0,0 +1,24 @@
#![deny(clippy::unwrap_used)]
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod encdec;
mod error;
mod jwt;
pub use encdec::decrypt::decrypt_data;
pub use encdec::encrypt::encrypt_data;
pub use error::Error;
pub use jwt::decode::decode as jwt_decode;
pub use jwt::encode::encode as jwt_encode;