mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-10 23:26:53 +00:00
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:
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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),
|
||||
)?)
|
||||
}
|
||||
@@ -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),
|
||||
)?)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user