1use aes_gcm::{
2 Aes256Gcm, Nonce,
3 aead::{Aead, KeyInit},
4};
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6use rand::RngExt;
7use serde::{Deserialize, Serialize};
8
9#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
10pub struct SecretString(String);
11
12impl SecretString {
13 pub fn new(val: &str) -> Self {
14 SecretString(val.to_string())
15 }
16
17 pub fn reveal_audited(&self) -> &str {
20 &self.0
21 }
22}
23
24impl std::fmt::Debug for SecretString {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.write_str("[ENCRYPTED_SECRET]")
28 }
29}
30
31pub struct PrivacyReport {
32 pub table_name: String,
33 pub has_encrypted_data: bool,
34 pub encrypted_fields: Vec<&'static str>,
35}
36
37pub trait ComplianceModel {
38 fn compliance_schema() -> PrivacyReport;
39}
40
41pub fn encrypt_aes_gcm(plaintext: &str, key: &str) -> Result<String, String> {
42 let key_bytes = key.as_bytes();
43 if key_bytes.len() != 32 {
44 return Err("RULLST_ENCRYPTION_KEY must be exactly 32 bytes long".to_string());
45 }
46
47 let cipher = Aes256Gcm::new_from_slice(key_bytes).map_err(|e| e.to_string())?;
48
49 let mut nonce_bytes = [0u8; 12];
50 rand::rng().fill(&mut nonce_bytes);
51 let nonce = Nonce::try_from(&nonce_bytes[..]).map_err(|e| e.to_string())?;
52
53 let ciphertext = cipher
54 .encrypt(&nonce, plaintext.as_bytes())
55 .map_err(|e| e.to_string())?;
56
57 let mut payload = nonce_bytes.to_vec();
58 payload.extend_from_slice(&ciphertext);
59
60 Ok(STANDARD.encode(payload))
61}
62
63pub fn decrypt_aes_gcm(encrypted: &str, key: &str) -> Result<String, String> {
64 let key_bytes = key.as_bytes();
65 if key_bytes.len() != 32 {
66 return Err("RULLST_ENCRYPTION_KEY must be exactly 32 bytes long".to_string());
67 }
68
69 let payload = STANDARD.decode(encrypted).map_err(|e| e.to_string())?;
70 if payload.len() < 12 {
71 return Err("Invalid encrypted payload (too short)".to_string());
72 }
73
74 let cipher = Aes256Gcm::new_from_slice(key_bytes).map_err(|e| e.to_string())?;
75 let nonce = Nonce::try_from(&payload[..12]).map_err(|e| e.to_string())?;
76 let ciphertext = &payload[12..];
77
78 let plaintext = cipher
79 .decrypt(&nonce, ciphertext)
80 .map_err(|e| e.to_string())?;
81
82 String::from_utf8(plaintext).map_err(|e| e.to_string())
83}
84
85#[cfg(not(any(
86 feature = "strict-postgres",
87 feature = "strict-mysql",
88 feature = "strict-sqlite"
89)))]
90impl<'r> sqlx::Decode<'r, sqlx::Any> for SecretString {
91 fn decode(
92 value: sqlx::any::AnyValueRef<'r>,
93 ) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
94 let text = <String as sqlx::Decode<sqlx::Any>>::decode(value)?;
95 let encryption_key = std::env::var("RULLST_ENCRYPTION_KEY")
96 .map_err(|_| "RULLST_ENCRYPTION_KEY is not set in environment")?;
97
98 let decrypted = decrypt_aes_gcm(&text, &encryption_key)?;
99 Ok(SecretString(decrypted))
100 }
101}
102
103#[cfg(not(any(
104 feature = "strict-postgres",
105 feature = "strict-mysql",
106 feature = "strict-sqlite"
107)))]
108impl<'q> sqlx::Encode<'q, sqlx::Any> for SecretString {
109 fn encode_by_ref(
110 &self,
111 buf: &mut <sqlx::Any as sqlx::database::Database>::ArgumentBuffer,
112 ) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync>> {
113 let encryption_key = std::env::var("RULLST_ENCRYPTION_KEY")
114 .map_err(|_| "RULLST_ENCRYPTION_KEY is not set in environment")?;
115
116 let encrypted = encrypt_aes_gcm(&self.0, &encryption_key)?;
117 <String as sqlx::Encode<sqlx::Any>>::encode(encrypted, buf)
118 }
119}
120
121#[cfg(not(any(
122 feature = "strict-postgres",
123 feature = "strict-mysql",
124 feature = "strict-sqlite"
125)))]
126impl sqlx::Type<sqlx::Any> for SecretString {
127 fn type_info() -> sqlx::any::AnyTypeInfo {
128 <String as sqlx::Type<sqlx::Any>>::type_info()
129 }
130}
131
132#[cfg_attr(test, mutants::skip)]
134#[cfg(any(
135 feature = "strict-postgres",
136 feature = "strict-mysql",
137 feature = "strict-sqlite"
138))]
139impl<'r> sqlx::Decode<'r, crate::database::RullstDatabase> for SecretString {
140 fn decode(
141 value: <crate::database::RullstDatabase as sqlx::database::Database>::ValueRef<'r>,
142 ) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
143 let text = <String as sqlx::Decode<crate::database::RullstDatabase>>::decode(value)?;
144 let encryption_key = std::env::var("RULLST_ENCRYPTION_KEY")
145 .map_err(|_| "RULLST_ENCRYPTION_KEY is not set in environment")?;
146
147 let decrypted = decrypt_aes_gcm(&text, &encryption_key)?;
148 Ok(SecretString(decrypted))
149 }
150}
151
152#[cfg_attr(test, mutants::skip)]
153#[cfg(any(
154 feature = "strict-postgres",
155 feature = "strict-mysql",
156 feature = "strict-sqlite"
157))]
158impl<'q> sqlx::Encode<'q, crate::database::RullstDatabase> for SecretString {
159 fn encode_by_ref(
160 &self,
161 buf: &mut <crate::database::RullstDatabase as sqlx::database::Database>::ArgumentBuffer,
162 ) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync>> {
163 let encryption_key = std::env::var("RULLST_ENCRYPTION_KEY")
164 .map_err(|_| "RULLST_ENCRYPTION_KEY is not set in environment")?;
165
166 let encrypted = encrypt_aes_gcm(&self.0, &encryption_key)?;
167 <String as sqlx::Encode<crate::database::RullstDatabase>>::encode(encrypted, buf)
168 }
169}
170
171#[cfg_attr(test, mutants::skip)]
172#[cfg(any(
173 feature = "strict-postgres",
174 feature = "strict-mysql",
175 feature = "strict-sqlite"
176))]
177impl sqlx::Type<crate::database::RullstDatabase> for SecretString {
178 fn type_info() -> <crate::database::RullstDatabase as sqlx::database::Database>::TypeInfo {
179 <String as sqlx::Type<crate::database::RullstDatabase>>::type_info()
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_secret_string_encryption() {
189 let key = "01234567890123456789012345678901"; let plaintext = "Sensitive Data 123";
191
192 let encrypted = encrypt_aes_gcm(plaintext, key).unwrap();
193 assert_ne!(encrypted, plaintext);
194
195 let decrypted = decrypt_aes_gcm(&encrypted, key).unwrap();
196 assert_eq!(decrypted, plaintext);
197 }
198
199 #[test]
200 fn test_secret_string_debug() {
201 let secret = SecretString::new("my-cpf-123");
202 let debug_str = format!("{:?}", secret);
203 assert_eq!(debug_str, "[ENCRYPTED_SECRET]");
204 }
205
206 #[test]
207 fn test_decrypt_aes_gcm_invalid_length() {
208 let key = "01234567890123456789012345678901";
209
210 let short_payload = STANDARD.encode([0u8; 11]);
211 let result = decrypt_aes_gcm(&short_payload, key);
212 assert!(result.is_err());
213 assert_eq!(result.unwrap_err(), "Invalid encrypted payload (too short)");
214
215 let exactly_12_payload = STANDARD.encode([0u8; 12]);
216 let result = decrypt_aes_gcm(&exactly_12_payload, key);
217 assert!(result.is_err());
218 assert_ne!(result.unwrap_err(), "Invalid encrypted payload (too short)");
219 }
220
221 #[test]
222 fn test_secret_string_reveal_audited() {
223 let secret = SecretString::new("my-secret-data");
224 assert_eq!(secret.reveal_audited(), "my-secret-data");
225 }
226}