integrationos_domain/algebra/
crypto.rs1use crate::{secrets::SecretsConfig, IntegrationOSError, InternalError, SecretVersion};
2use async_trait::async_trait;
3use base64::{prelude::BASE64_STANDARD, Engine};
4use chacha20poly1305::aead::generic_array::typenum::Unsigned;
5use chacha20poly1305::aead::generic_array::GenericArray;
6use chacha20poly1305::aead::{Aead, AeadCore, KeyInit, OsRng};
7use chacha20poly1305::ChaCha20Poly1305;
8use google_cloud_kms::{
9 client::{Client, ClientConfig},
10 grpc::kms::v1::DecryptRequest,
11};
12use secrecy::ExposeSecret;
13use tracing::debug;
14
15#[async_trait]
16pub trait CryptoExt {
17 async fn encrypt(&self, encrypted_secret: String) -> Result<String, IntegrationOSError>;
18
19 async fn decrypt(
20 &self,
21 data: String,
22 version: Option<SecretVersion>,
23 ) -> Result<String, IntegrationOSError>;
24}
25
26type NonceSize = <ChaCha20Poly1305 as AeadCore>::NonceSize;
27
28#[derive(Debug, Clone)]
29pub struct IOSCrypto {
30 key: Vec<u8>,
31}
32
33#[async_trait]
34impl CryptoExt for IOSCrypto {
35 async fn encrypt(&self, encrypted_secret: String) -> Result<String, IntegrationOSError> {
36 self.encrypt(encrypted_secret).await
37 }
38
39 async fn decrypt(
40 &self,
41 data: String,
42 _: Option<SecretVersion>,
43 ) -> Result<String, IntegrationOSError> {
44 self.decrypt(data).await
45 }
46}
47
48impl IOSCrypto {
49 pub fn new(config: SecretsConfig) -> Result<Self, IntegrationOSError> {
50 let len = config.ios_crypto_secret.expose_secret().as_bytes().len();
51
52 if len != 32 {
53 return Err(InternalError::invalid_argument(
54 "The provided value is not a valid UTF-8 string",
55 None,
56 ));
57 }
58
59 let key: [u8; 32] = config
60 .ios_crypto_secret
61 .expose_secret()
62 .as_bytes()
63 .iter()
64 .take(32)
65 .map(|b| b.to_owned())
66 .collect::<Vec<_>>()
67 .try_into()
68 .map_err(|_| {
69 InternalError::invalid_argument(
70 "The provided value is not a valid UTF-8 string",
71 None,
72 )
73 })?;
74
75 Ok(Self { key: key.to_vec() })
76 }
77
78 async fn decrypt(&self, encrypted_secret: String) -> Result<String, IntegrationOSError> {
79 let obsf = hex::decode(encrypted_secret).map_err(|_| {}).map_err(|_| {
80 InternalError::deserialize_error("The provided value is not a valid UTF-8 string", None)
81 })?;
82 let cipher = ChaCha20Poly1305::new(GenericArray::from_slice(&self.key));
83 let (nonce, ciphertext) = obsf.split_at(NonceSize::to_usize());
84 let nonce = GenericArray::from_slice(nonce);
85 let plaintext = cipher.decrypt(nonce, ciphertext).map_err(|_| {
86 InternalError::deserialize_error("The provided value is not a valid UTF-8 string", None)
87 })?;
88 let plaintext = String::from_utf8(plaintext).map_err(|_| {
89 InternalError::deserialize_error("The provided value is not a valid UTF-8 string", None)
90 })?;
91
92 Ok(plaintext)
93 }
94
95 async fn encrypt(&self, secret: String) -> Result<String, IntegrationOSError> {
96 let cipher = ChaCha20Poly1305::new(GenericArray::from_slice(&self.key));
97 let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
98 let mut obsf = cipher.encrypt(&nonce, secret.as_bytes()).map_err(|_| {
99 InternalError::serialize_error("The provided value is not a valid UTF-8 string", None)
100 })?;
101 obsf.splice(..0, nonce.iter().copied());
102
103 Ok(hex::encode(obsf))
104 }
105}
106
107#[derive(Debug, Clone)]
108pub struct GoogleCryptoKms {
109 client: Client,
110 config: SecretsConfig,
111 fallback: IOSCrypto,
112}
113
114#[async_trait]
115impl CryptoExt for GoogleCryptoKms {
116 async fn encrypt(&self, encrypted_secret: String) -> Result<String, IntegrationOSError> {
117 self.encrypt(encrypted_secret).await
118 }
119
120 async fn decrypt(
121 &self,
122 data: String,
123 version: Option<SecretVersion>,
124 ) -> Result<String, IntegrationOSError> {
125 self.decrypt(data, version).await
126 }
127}
128
129impl GoogleCryptoKms {
130 pub async fn new(secrets_config: &SecretsConfig) -> Result<Self, IntegrationOSError> {
131 let fallback = IOSCrypto::new(secrets_config.clone())?;
132 let config = ClientConfig::default().with_auth().await.map_err(|e| {
133 InternalError::connection_error(
134 &format!("Failed to create GoogleCryptoKms client: {e}"),
135 Some("Failed to create client"),
136 )
137 })?;
138 let client = Client::new(config).await.map_err(|e| {
139 InternalError::connection_error(
140 &format!("Failed to create GoogleCryptoKms client: {e}"),
141 Some("Failed to create client"),
142 )
143 })?;
144
145 Ok(Self {
146 client,
147 config: secrets_config.clone(),
148 fallback,
149 })
150 }
151
152 async fn decrypt(
153 &self,
154 encrypted_secret: String,
155 version: Option<SecretVersion>,
156 ) -> Result<String, IntegrationOSError> {
157 match version {
158 Some(SecretVersion::V2) => self.fallback.decrypt(encrypted_secret).await,
159 Some(SecretVersion::V1) | None => {
160 let request = DecryptRequest {
161 name: format!(
162 "projects/{project_id}/locations/{location_id}/keyRings/{key_ring_id}/cryptoKeys/{key_id}",
163 project_id = self.config.google_kms_project_id,
164 location_id = self.config.google_kms_location_id,
165 key_ring_id = self.config.google_kms_key_ring_id,
166 key_id = self.config.google_kms_key_id,
167 ),
168 ciphertext: BASE64_STANDARD.decode(encrypted_secret.as_bytes())
169 .map_err(|e| {
170 debug!("Error decoding secret: {e}");
171 InternalError::deserialize_error("The provided value is not a valid UTF-8 string", None)
172 })?,
173 ..Default::default()
174 };
175
176 let decriptes_bytes = self.client.decrypt(request, None).await.map_err(|e| {
177 debug!("Error decrypting secret: {e}");
178 InternalError::connection_error(
179 "The provided value is not a valid UTF-8 string",
180 None,
181 )
182 })?;
183
184 let plaintext = String::from_utf8(decriptes_bytes.plaintext).map_err(|e| {
185 debug!("Error converting decrypted secret to string: {e}");
186 InternalError::deserialize_error(
187 "The provided value is not a valid UTF-8 string",
188 None,
189 )
190 })?;
191
192 Ok(plaintext)
193 }
194 }
195 }
196
197 async fn encrypt(&self, secret: String) -> Result<String, IntegrationOSError> {
198 self.fallback.encrypt(secret).await
200 }
201}
202
203#[cfg(test)]
204mod tests {
205
206 use crate::secrets::SecretServiceProvider;
207
208 use super::*;
209
210 #[tokio::test]
211 async fn should_encrypt_and_decrypt_data() {
212 let config = SecretsConfig::default().with_provider(SecretServiceProvider::IosKms);
213 let crypto = IOSCrypto::new(config).expect("Failed to create IOSCrypto client");
214
215 let data = "lorem_ipsum-dolor_sit-amet";
216 let encrypted = crypto
217 .encrypt(data.to_owned())
218 .await
219 .expect("Failed to encrypt data");
220 let decrypted = crypto
221 .decrypt(encrypted.to_owned())
222 .await
223 .expect("Failed to decrypt data");
224
225 assert_eq!(data, decrypted);
226 }
227
228 #[tokio::test]
229 async fn should_fail_to_decrypt_if_the_key_is_different() {
230 let config = SecretsConfig::default().with_provider(SecretServiceProvider::IosKms);
231 let crypto = IOSCrypto::new(config).expect("Failed to create IOSCrypto client");
232
233 let data = "lorem_ipsum-dolor_sit-amet";
234 let encrypted = crypto
235 .encrypt(data.to_owned())
236 .await
237 .expect("Failed to encrypt data");
238
239 let config = SecretsConfig::new()
240 .with_secret("lorem_ipsum-dolor_sit_amet-neque".into())
241 .with_provider(SecretServiceProvider::IosKms);
242 let crypto = IOSCrypto::new(config).expect("Failed to create IOSCrypto client");
243
244 let decrypted = crypto.decrypt(encrypted).await;
245
246 assert!(decrypted.is_err());
247 }
248
249 #[tokio::test]
250 async fn should_fail_to_decrypt_if_the_data_is_tampered() {
251 let config = SecretsConfig::default().with_provider(SecretServiceProvider::IosKms);
252 let crypto = IOSCrypto::new(config).expect("Failed to create IOSCrypto client");
253
254 let data = "lorem_ipsum-dolor_sit-amet";
255 let encrypted = crypto
256 .encrypt(data.to_owned())
257 .await
258 .expect("Failed to encrypt data");
259
260 let mut obsf = hex::decode(encrypted).expect("Failed to decode encrypted data");
261 obsf[0] = 0;
262 let tampered = hex::encode(obsf);
263
264 let decrypted = crypto.decrypt(tampered).await;
265
266 assert!(decrypted.is_err());
267 }
268}