use crate::crypto::{CryptoError, CryptoResult, defaults, DerivedKey};
use chacha20poly1305::{ChaCha20Poly1305, Nonce, aead::{Aead, KeyInit}};
use serde::{Deserialize, Serialize};
use zeroize::ZeroizeOnDrop;
use std::fmt;
#[derive(Clone, Serialize, Deserialize)]
pub struct EncryptedSecret {
ciphertext: Vec<u8>,
nonce: [u8; defaults::NONCE_LENGTH],
salt: [u8; defaults::SALT_LENGTH],
#[serde(default)]
metadata: SecretMetadata,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SecretMetadata {
pub description: Option<String>,
pub created_at: Option<u64>,
pub tags: Vec<String>,
pub secret_type: Option<SecretType>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum SecretType {
Generic,
ApiKey,
Password,
PrivateKey,
DatabaseUrl,
Config,
Custom(String),
}
impl fmt::Display for SecretType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SecretType::Generic => write!(f, "generic"),
SecretType::ApiKey => write!(f, "api_key"),
SecretType::Password => write!(f, "password"),
SecretType::PrivateKey => write!(f, "private_key"),
SecretType::DatabaseUrl => write!(f, "database_url"),
SecretType::Config => write!(f, "config"),
SecretType::Custom(name) => write!(f, "custom:{}", name),
}
}
}
#[derive(Clone, ZeroizeOnDrop)]
pub struct PlaintextSecret {
data: Vec<u8>,
}
impl PlaintextSecret {
pub fn from_bytes(data: Vec<u8>) -> Self {
Self { data }
}
pub fn new(data: Vec<u8>) -> Self {
Self { data }
}
pub fn from_string(data: String) -> Self {
Self {
data: data.into_bytes(),
}
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
pub fn as_string(&self) -> CryptoResult<&str> {
std::str::from_utf8(&self.data)
.map_err(|e| CryptoError::invalid_input(format!("Invalid UTF-8: {}", e)))
}
pub fn into_string(self) -> CryptoResult<String> {
String::from_utf8(self.data.clone())
.map_err(|e| CryptoError::invalid_input(format!("Invalid UTF-8: {}", e.utf8_error())))
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
impl fmt::Debug for PlaintextSecret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PlaintextSecret")
.field("data", &format!("[{} bytes, REDACTED]", self.data.len()))
.finish()
}
}
impl EncryptedSecret {
pub fn encrypt_with_password(
plaintext: PlaintextSecret,
password: &str,
metadata: Option<SecretMetadata>,
) -> CryptoResult<Self> {
let key = DerivedKey::from_password_with_random_salt(password)?;
Self::encrypt_with_key(plaintext, &key, metadata)
}
pub fn encrypt_with_key(
plaintext: PlaintextSecret,
key: &DerivedKey,
metadata: Option<SecretMetadata>,
) -> CryptoResult<Self> {
let nonce_bytes = crate::crypto::keys::SecureRandom::generate_nonce()?;
let nonce = Nonce::from_slice(&nonce_bytes);
let cipher = ChaCha20Poly1305::new(key.key());
let ciphertext = cipher
.encrypt(nonce, plaintext.as_bytes())
.map_err(CryptoError::from)?;
Ok(Self {
ciphertext,
nonce: nonce_bytes,
salt: *key.salt(),
metadata: metadata.unwrap_or_default(),
})
}
pub fn decrypt_with_password(&self, password: &str) -> CryptoResult<PlaintextSecret> {
let key = DerivedKey::from_password_with_salt(password, &self.salt)?;
self.decrypt_with_key(&key)
}
pub fn decrypt_with_key(&self, key: &DerivedKey) -> CryptoResult<PlaintextSecret> {
if key.salt() != &self.salt {
return Err(CryptoError::decryption("Salt mismatch"));
}
let nonce = Nonce::from_slice(&self.nonce);
let cipher = ChaCha20Poly1305::new(key.key());
let plaintext_bytes = cipher
.decrypt(nonce, self.ciphertext.as_slice())
.map_err(|_| CryptoError::AuthenticationFailed)?;
Ok(PlaintextSecret::from_bytes(plaintext_bytes))
}
pub fn metadata(&self) -> &SecretMetadata {
&self.metadata
}
pub fn set_metadata(&mut self, metadata: SecretMetadata) {
self.metadata = metadata;
}
pub fn salt(&self) -> &[u8; defaults::SALT_LENGTH] {
&self.salt
}
pub fn nonce(&self) -> &[u8; defaults::NONCE_LENGTH] {
&self.nonce
}
pub fn ciphertext_len(&self) -> usize {
self.ciphertext.len()
}
pub fn to_json(&self) -> CryptoResult<String> {
serde_json::to_string(self).map_err(CryptoError::from)
}
pub fn from_json(json: &str) -> CryptoResult<Self> {
serde_json::from_str(json).map_err(CryptoError::from)
}
pub fn to_bytes(&self) -> CryptoResult<Vec<u8>> {
bincode::serialize(self)
.map_err(|e| CryptoError::serialization(e.to_string()))
}
pub fn from_bytes(bytes: &[u8]) -> CryptoResult<Self> {
bincode::deserialize(bytes)
.map_err(|e| CryptoError::serialization(e.to_string()))
}
pub fn reencrypt_with_password(&self, old_password: &str, new_password: &str) -> CryptoResult<Self> {
let plaintext = self.decrypt_with_password(old_password)?;
Self::encrypt_with_password(plaintext, new_password, Some(self.metadata.clone()))
}
pub fn verify_password(&self, password: &str) -> bool {
self.decrypt_with_password(password).is_ok()
}
}
impl fmt::Debug for EncryptedSecret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EncryptedSecret")
.field("ciphertext_len", &self.ciphertext.len())
.field("nonce", &hex::encode(&self.nonce))
.field("salt", &hex::encode(&self.salt))
.field("metadata", &self.metadata)
.finish()
}
}
impl SecretMetadata {
pub fn new() -> Self {
Self {
description: None,
created_at: Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
),
tags: Vec::new(),
secret_type: None,
}
}
pub fn with_description<S: Into<String>>(description: S) -> Self {
let mut metadata = Self::new();
metadata.description = Some(description.into());
metadata
}
pub fn with_type(secret_type: SecretType) -> Self {
let mut metadata = Self::new();
metadata.secret_type = Some(secret_type);
metadata
}
pub fn add_tag<S: Into<String>>(&mut self, tag: S) -> &mut Self {
self.tags.push(tag.into());
self
}
pub fn set_description<S: Into<String>>(&mut self, description: S) -> &mut Self {
self.description = Some(description.into());
self
}
pub fn set_type(&mut self, secret_type: SecretType) -> &mut Self {
self.secret_type = Some(secret_type);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encrypt_decrypt_string() {
let secret_data = "This is a secret message!";
let password = "test_password_123";
let plaintext = PlaintextSecret::from_string(secret_data.to_string());
let encrypted = EncryptedSecret::encrypt_with_password(
plaintext,
password,
Some(SecretMetadata::with_description("Test secret")),
).unwrap();
let decrypted = encrypted.decrypt_with_password(password).unwrap();
assert_eq!(decrypted.as_string().unwrap(), secret_data);
}
#[test]
fn test_encrypt_decrypt_bytes() {
let secret_data = vec![1, 2, 3, 4, 5, 255, 0, 128];
let password = "test_password_123";
let plaintext = PlaintextSecret::from_bytes(secret_data.clone());
let encrypted = EncryptedSecret::encrypt_with_password(
plaintext,
password,
None,
).unwrap();
let decrypted = encrypted.decrypt_with_password(password).unwrap();
assert_eq!(decrypted.as_bytes(), &secret_data);
}
#[test]
fn test_wrong_password() {
let secret_data = "This is a secret message!";
let password = "correct_password";
let wrong_password = "wrong_password";
let plaintext = PlaintextSecret::from_string(secret_data.to_string());
let encrypted = EncryptedSecret::encrypt_with_password(plaintext, password, None).unwrap();
let result = encrypted.decrypt_with_password(wrong_password);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), CryptoError::AuthenticationFailed));
}
#[test]
fn test_password_verification() {
let secret_data = "secret";
let password = "test_password";
let plaintext = PlaintextSecret::from_string(secret_data.to_string());
let encrypted = EncryptedSecret::encrypt_with_password(plaintext, password, None).unwrap();
assert!(encrypted.verify_password(password));
assert!(!encrypted.verify_password("wrong_password"));
}
#[test]
fn test_json_serialization() {
let secret_data = "This is a secret message!";
let password = "test_password_123";
let plaintext = PlaintextSecret::from_string(secret_data.to_string());
let encrypted = EncryptedSecret::encrypt_with_password(
plaintext,
password,
Some(SecretMetadata::with_description("Test secret")),
).unwrap();
let json = encrypted.to_json().unwrap();
let deserialized = EncryptedSecret::from_json(&json).unwrap();
let decrypted = deserialized.decrypt_with_password(password).unwrap();
assert_eq!(decrypted.as_string().unwrap(), secret_data);
}
#[test]
fn test_reencryption() {
let secret_data = "This is a secret message!";
let old_password = "old_password";
let new_password = "new_password";
let plaintext = PlaintextSecret::from_string(secret_data.to_string());
let encrypted = EncryptedSecret::encrypt_with_password(plaintext, old_password, None).unwrap();
let reencrypted = encrypted.reencrypt_with_password(old_password, new_password).unwrap();
assert!(!reencrypted.verify_password(old_password));
let decrypted = reencrypted.decrypt_with_password(new_password).unwrap();
assert_eq!(decrypted.as_string().unwrap(), secret_data);
}
#[test]
fn test_metadata() {
let mut metadata = SecretMetadata::new();
metadata
.set_description("API Key for service X")
.add_tag("production")
.add_tag("api")
.set_type(SecretType::ApiKey);
assert_eq!(metadata.description.as_ref().unwrap(), "API Key for service X");
assert_eq!(metadata.tags, vec!["production", "api"]);
assert_eq!(metadata.secret_type.as_ref().unwrap(), &SecretType::ApiKey);
assert!(metadata.created_at.is_some());
}
#[test]
fn test_secret_types() {
assert_eq!(SecretType::Generic.to_string(), "generic");
assert_eq!(SecretType::ApiKey.to_string(), "api_key");
assert_eq!(SecretType::Custom("jwt".to_string()).to_string(), "custom:jwt");
}
#[test]
fn test_plaintext_secret_zeroization() {
let data = "sensitive_data".to_string();
let secret = PlaintextSecret::from_string(data);
assert_eq!(secret.len(), 14);
assert!(!secret.is_empty());
drop(secret);
}
}