batata_client/crypto/
cipher.rs1use aes_gcm::{
6 aead::{Aead, KeyInit},
7 Aes256Gcm, Nonce,
8};
9use base64::Engine;
10
11use crate::error::{BatataError, Result};
12
13pub const CIPHER_PREFIX: &str = "cipher-";
15
16pub fn is_encrypted_data_id(data_id: &str) -> bool {
18 data_id.starts_with(CIPHER_PREFIX)
19}
20
21pub fn strip_cipher_prefix(data_id: &str) -> &str {
23 data_id.strip_prefix(CIPHER_PREFIX).unwrap_or(data_id)
24}
25
26pub 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 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 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 let cipher = Aes256Gcm::new_from_slice(data_key).map_err(|e| BatataError::EncryptionError {
60 message: format!("Failed to create cipher: {}", e),
61 })?;
62
63 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
75pub 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 let cipher = Aes256Gcm::new_from_slice(data_key).map_err(|e| BatataError::EncryptionError {
92 message: format!("Failed to create cipher: {}", e),
93 })?;
94
95 let nonce_bytes: [u8; 12] = rand_nonce();
97 let nonce = Nonce::from_slice(&nonce_bytes);
98
99 let ciphertext = cipher
101 .encrypt(nonce, plaintext.as_bytes())
102 .map_err(|e| BatataError::EncryptionError {
103 message: format!("Encryption failed: {}", e),
104 })?;
105
106 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
114fn 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 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]; 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]; let ciphertext = "dGVzdA=="; 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}