batata_client/crypto/
cipher.rs

1//! Cipher utilities for encrypting and decrypting configuration content
2//!
3//! Supports AES-GCM encryption for Nacos cipher- prefixed configurations.
4
5use aes_gcm::{
6    aead::{Aead, KeyInit},
7    Aes256Gcm, Nonce,
8};
9use base64::Engine;
10
11use crate::error::{BatataError, Result};
12
13/// Cipher prefix for encrypted configurations
14pub const CIPHER_PREFIX: &str = "cipher-";
15
16/// Check if content is encrypted (has cipher- prefix in dataId)
17pub fn is_encrypted_data_id(data_id: &str) -> bool {
18    data_id.starts_with(CIPHER_PREFIX)
19}
20
21/// Get the actual data ID without cipher- prefix
22pub fn strip_cipher_prefix(data_id: &str) -> &str {
23    data_id.strip_prefix(CIPHER_PREFIX).unwrap_or(data_id)
24}
25
26/// Decrypt content using AES-256-GCM
27///
28/// # Arguments
29/// * `ciphertext` - Base64 encoded ciphertext (nonce + encrypted data)
30/// * `data_key` - 32-byte AES-256 key
31///
32/// # Returns
33/// Decrypted plaintext as UTF-8 string
34pub fn decrypt_content(ciphertext: &str, data_key: &[u8]) -> Result<String> {
35    if data_key.len() != 32 {
36        return Err(BatataError::EncryptionError {
37            message: format!("Invalid key length: expected 32 bytes, got {}", data_key.len()),
38        });
39    }
40
41    // Decode base64 ciphertext
42    let ciphertext_bytes = base64::engine::general_purpose::STANDARD
43        .decode(ciphertext)
44        .map_err(|e| BatataError::EncryptionError {
45            message: format!("Failed to decode ciphertext: {}", e),
46        })?;
47
48    // Extract nonce (first 12 bytes) and encrypted data
49    if ciphertext_bytes.len() < 12 {
50        return Err(BatataError::EncryptionError {
51            message: "Ciphertext too short: missing nonce".to_string(),
52        });
53    }
54
55    let (nonce_bytes, encrypted_data) = ciphertext_bytes.split_at(12);
56    let nonce = Nonce::from_slice(nonce_bytes);
57
58    // Create cipher
59    let cipher = Aes256Gcm::new_from_slice(data_key).map_err(|e| BatataError::EncryptionError {
60        message: format!("Failed to create cipher: {}", e),
61    })?;
62
63    // Decrypt
64    let plaintext = cipher
65        .decrypt(nonce, encrypted_data)
66        .map_err(|e| BatataError::EncryptionError {
67            message: format!("Decryption failed: {}", e),
68        })?;
69
70    String::from_utf8(plaintext).map_err(|e| BatataError::EncryptionError {
71        message: format!("Invalid UTF-8 in decrypted content: {}", e),
72    })
73}
74
75/// Encrypt content using AES-256-GCM
76///
77/// # Arguments
78/// * `plaintext` - Content to encrypt
79/// * `data_key` - 32-byte AES-256 key
80///
81/// # Returns
82/// Base64 encoded ciphertext (nonce + encrypted data)
83pub fn encrypt_content(plaintext: &str, data_key: &[u8]) -> Result<String> {
84    if data_key.len() != 32 {
85        return Err(BatataError::EncryptionError {
86            message: format!("Invalid key length: expected 32 bytes, got {}", data_key.len()),
87        });
88    }
89
90    // Create cipher
91    let cipher = Aes256Gcm::new_from_slice(data_key).map_err(|e| BatataError::EncryptionError {
92        message: format!("Failed to create cipher: {}", e),
93    })?;
94
95    // Generate random nonce
96    let nonce_bytes: [u8; 12] = rand_nonce();
97    let nonce = Nonce::from_slice(&nonce_bytes);
98
99    // Encrypt
100    let ciphertext = cipher
101        .encrypt(nonce, plaintext.as_bytes())
102        .map_err(|e| BatataError::EncryptionError {
103            message: format!("Encryption failed: {}", e),
104        })?;
105
106    // Combine nonce and ciphertext
107    let mut result = Vec::with_capacity(12 + ciphertext.len());
108    result.extend_from_slice(&nonce_bytes);
109    result.extend(ciphertext);
110
111    Ok(base64::engine::general_purpose::STANDARD.encode(result))
112}
113
114/// Generate a random 12-byte nonce
115fn rand_nonce() -> [u8; 12] {
116    use std::time::{SystemTime, UNIX_EPOCH};
117
118    let nanos = SystemTime::now()
119        .duration_since(UNIX_EPOCH)
120        .unwrap()
121        .as_nanos();
122
123    let mut nonce = [0u8; 12];
124    let bytes = nanos.to_le_bytes();
125    nonce[..8].copy_from_slice(&bytes[..8]);
126
127    // Add some additional randomness
128    let ptr = &nonce as *const _ as usize;
129    let extra = (ptr ^ (nanos as usize)).to_le_bytes();
130    nonce[8..12].copy_from_slice(&extra[..4]);
131
132    nonce
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_is_encrypted_data_id() {
141        assert!(is_encrypted_data_id("cipher-test-config"));
142        assert!(!is_encrypted_data_id("test-config"));
143        assert!(!is_encrypted_data_id("test-cipher-config"));
144    }
145
146    #[test]
147    fn test_strip_cipher_prefix() {
148        assert_eq!(strip_cipher_prefix("cipher-test-config"), "test-config");
149        assert_eq!(strip_cipher_prefix("test-config"), "test-config");
150    }
151
152    #[test]
153    fn test_encrypt_decrypt_roundtrip() {
154        let key = [0u8; 32]; // Test key
155        let plaintext = "Hello, World! This is a test message.";
156
157        let ciphertext = encrypt_content(plaintext, &key).unwrap();
158        let decrypted = decrypt_content(&ciphertext, &key).unwrap();
159
160        assert_eq!(decrypted, plaintext);
161    }
162
163    #[test]
164    fn test_decrypt_invalid_key_length() {
165        let key = [0u8; 16]; // Invalid key length
166        let ciphertext = "dGVzdA=="; // Base64 "test"
167
168        let result = decrypt_content(ciphertext, &key);
169        assert!(result.is_err());
170    }
171
172    #[test]
173    fn test_decrypt_invalid_ciphertext() {
174        let key = [0u8; 32];
175        let ciphertext = "!!!invalid!!!";
176
177        let result = decrypt_content(ciphertext, &key);
178        assert!(result.is_err());
179    }
180}