1use crate::aes::IvAndCiphertext;
2use crate::icl_header_v3::v3document_header::Header;
3use crate::v4::MAGIC;
4use crate::{
5 Error, Result,
6 aes::{
7 EncryptionKey, PlaintextDocument, aes_encrypt, aes_encrypt_with_iv,
8 decrypt_document_with_attached_iv,
9 },
10 icl_header_v3::{SaaSShieldHeader, V3DocumentHeader},
11 signing::AES_KEY_LEN,
12};
13use bytes::Bytes;
14use protobuf::Message;
15use rand::CryptoRng;
16
17const IV_LEN: usize = 12;
18const GCM_TAG_LEN: usize = 16;
19
20const V3: u8 = 3u8;
21
22pub const VERSION_AND_MAGIC: [u8; 5] = [V3, MAGIC[0], MAGIC[1], MAGIC[2], MAGIC[3]];
24
25const MAGIC_HEADER_LEN: usize = 5;
27const HEADER_LEN_LEN: usize = 2;
29const DETACHED_HEADER_LEN: usize = MAGIC_HEADER_LEN + HEADER_LEN_LEN;
30
31#[derive(Debug, Clone)]
35pub struct EncryptedPayload {
36 v3_document_header: V3DocumentHeader,
37 iv_and_ciphertext: IvAndCiphertext,
38}
39
40impl TryFrom<Vec<u8>> for EncryptedPayload {
41 type Error = Error;
42 fn try_from(value: Vec<u8>) -> std::result::Result<Self, Self::Error> {
43 let value_len = value.len();
44 if value_len < DETACHED_HEADER_LEN {
45 Err(Error::EdocTooShort(value_len))?
46 };
47 let (magic_header, header_len_and_rest) = value.split_at(MAGIC_HEADER_LEN);
48 let (header_len_len, header_and_cipher) = header_len_and_rest.split_at(HEADER_LEN_LEN);
49 if magic_header != [&[V3], &MAGIC[..]].concat() {
50 Err(Error::NoIronCoreMagic)?
51 };
52 let header_len = u16::from_be_bytes(
53 header_len_len
54 .try_into()
55 .expect("This is safe as we split off 2 bytes."),
56 ) as usize;
57 if header_and_cipher.len() < header_len {
58 Err(Error::HeaderParseErr(format!(
59 "Proto header length specified: {}, bytes remaining: {}",
60 header_len,
61 header_and_cipher.len()
62 )))?
63 };
64 let (header, iv_and_cipher) = header_and_cipher.split_at(header_len);
65 let v3_document_header: V3DocumentHeader =
66 Message::parse_from_bytes(header).map_err(|_| {
67 Error::HeaderParseErr("Unable to parse header as V3DocumentHeader".to_string())
68 })?;
69 Ok(EncryptedPayload {
70 v3_document_header,
71 iv_and_ciphertext: iv_and_cipher.to_vec().into(),
72 })
73 }
74}
75
76impl EncryptedPayload {
77 pub fn decrypt(self, key: &EncryptionKey) -> Result<PlaintextDocument> {
79 if verify_signature(key.0, &self.v3_document_header) {
80 decrypt_document_with_attached_iv(key, &self.iv_and_ciphertext)
81 } else {
82 Err(Error::DecryptError(
83 "Signature validation failed.".to_string(),
84 ))
85 }
86 }
87}
88
89pub fn encrypt_detached_document<R: CryptoRng>(
92 rng: &mut R,
93 key: EncryptionKey,
94 tenant_id: &str,
95 plaintext: PlaintextDocument,
96) -> Result<Vec<u8>> {
97 let saas_header = SaaSShieldHeader {
98 tenant_id: tenant_id.into(),
99 ..Default::default()
100 };
101 let saas_header_bytes = saas_header
102 .write_to_bytes()
103 .map_err(|e| Error::ProtoSerializationErr(e.to_string()))?;
104 let (signature_iv, key_ciphertext) = aes_encrypt(key, &saas_header_bytes, &[], rng)?;
106 let signature: Vec<u8> = signature_iv
107 .iter()
108 .chain(&key_ciphertext.0[key_ciphertext.0.len() - GCM_TAG_LEN..])
110 .copied()
111 .collect();
112
113 let header_bytes = V3DocumentHeader {
114 sig: signature.into(),
115 header: Some(Header::SaasShield(saas_header)),
116 ..Default::default()
117 }
118 .write_to_bytes()
119 .map_err(|e| Error::ProtoSerializationErr(e.to_string()))?;
120 if header_bytes.len() > u16::MAX as usize {
121 return Err(Error::HeaderLengthOverflow(header_bytes.len() as u64));
122 }
123
124 let (document_iv, document_ciphertext) = aes_encrypt(key, &plaintext.0, &[], rng)?;
125
126 Ok([
128 [V3].as_slice(),
129 MAGIC.as_slice(),
130 &(header_bytes.len() as u16).to_be_bytes(),
131 &header_bytes,
132 &document_iv,
133 &document_ciphertext.0,
134 ]
135 .concat())
136}
137
138struct V3Signature {
139 iv: [u8; IV_LEN],
140 gcm_tag: [u8; GCM_TAG_LEN],
141}
142
143fn decompose_signature(sig: &Bytes) -> Option<V3Signature> {
144 if sig.len() < IV_LEN + GCM_TAG_LEN {
145 None
146 } else {
147 let (iv, _) = sig.split_at(IV_LEN);
148 let (_, gcm_tag) = sig.split_at(sig.len() - GCM_TAG_LEN);
149 Some(V3Signature {
150 iv: iv.try_into().unwrap(), gcm_tag: gcm_tag.try_into().unwrap(), })
153 }
154}
155
156pub fn verify_signature(key: [u8; AES_KEY_LEN], v3_header: &V3DocumentHeader) -> bool {
157 if v3_header.header.is_none() || !v3_header.has_saas_shield() || v3_header.sig.is_empty() {
160 true
161 } else {
162 let maybe_sig = decompose_signature(&v3_header.sig);
163 match maybe_sig {
164 Some(sig) => aes_encrypt_with_iv(
165 crate::aes::EncryptionKey(key),
166 &v3_header
167 .saas_shield()
168 .write_to_bytes()
169 .expect("Writing proto to bytes failed."),
170 sig.iv,
171 &[],
172 )
173 .map(|(_, new_sig)| {
174 let new_sig_length = new_sig.0.len();
175 let (_, new_gcm_tag) = new_sig.0.split_at(new_sig_length - GCM_TAG_LEN);
176 new_gcm_tag == sig.gcm_tag
177 })
178 .unwrap_or(false),
179 _ => false,
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use hex_literal::hex;
188 use itertools::Itertools;
189
190 #[test]
191 fn verify_known_good_sig_in_v3_header() {
192 let dek: [u8; 32] = (0..32).collect_vec().try_into().unwrap();
194 let proto_bytes = vec![
195 10, 28, 49, 113, -17, 60, -119, -97, -121, 94, 89, 92, 34, 19, -54, -49, -110, -121,
196 -57, -116, -15, -106, 69, -116, -42, -112, 84, 73, -128, -57, 26, 10, 10, 8, 116, 101,
197 110, 97, 110, 116, 73, 100,
198 ]
199 .into_iter()
200 .map(|x| x as u8)
201 .collect_vec();
202 let header = Message::parse_from_bytes(&proto_bytes).unwrap();
203 assert!(verify_signature(dek, &header))
204 }
205
206 #[test]
207 fn verify_known_bad_sig_in_v3_header() {
208 let proto_bytes = hex!("0a030102031a0a0a0874656e616e744964");
215 let dek: [u8; 32] = (0..32).collect_vec().try_into().unwrap();
216 let header = Message::parse_from_bytes(&proto_bytes).unwrap();
217 assert!(!verify_signature(dek, &header));
218 }
219
220 #[test]
221 fn verify_empty_v3_header() {
222 let dek: [u8; 32] = (0..32).collect_vec().try_into().unwrap();
223 let empty_header = V3DocumentHeader::new();
224 assert!(verify_signature(dek, &empty_header))
225 }
226
227 #[test]
228 fn verify_empty_sig_v3_header() {
229 let proto_bytes = hex!("0a001a0a0a0874656e616e744964");
236 let dek: [u8; 32] = (0..32).collect_vec().try_into().unwrap();
237 let header = Message::parse_from_bytes(&proto_bytes).unwrap();
238 assert!(verify_signature(dek, &header));
239 }
240
241 #[test]
242 fn decompose_signature_works() {
243 let sig_1 = (0..28).collect_vec();
244 let decomposed_1 = decompose_signature(&sig_1.into()).unwrap();
245 let expected_iv_1 = (0..12).collect_vec();
246 let expected_tag_1 = (12..28).collect_vec();
247 assert_eq!(&decomposed_1.iv[..], expected_iv_1);
248 assert_eq!(&decomposed_1.gcm_tag[..], expected_tag_1);
249
250 let sig_2 = (0..100).collect_vec();
251 let decomposed_2 = decompose_signature(&sig_2.into()).unwrap();
252 let expected_iv_2 = (0..12).collect_vec();
253 let expected_tag_2 = (84..100).collect_vec();
254 assert_eq!(&decomposed_2.iv[..], expected_iv_2);
255 assert_eq!(&decomposed_2.gcm_tag[..], expected_tag_2);
256
257 let sig_3 = (0..10).collect_vec();
258 let decomposed_failure = decompose_signature(&sig_3.into());
259 assert!(decomposed_failure.is_none());
260 }
261
262 #[test]
263 fn encrypted_payload_too_short() {
264 let document = vec![3, 73, 82, 79, 78, 0];
265 let err = EncryptedPayload::try_from(document);
266 assert!(matches!(err, Err(Error::EdocTooShort(_))))
267 }
268
269 #[test]
270 fn encrypted_payload_invalid_header_len() {
271 let document = vec![
272 3, 73, 82, 79, 78, 0, 100, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
273 ];
274 let err = EncryptedPayload::try_from(document);
275 assert!(matches!(err, Err(Error::HeaderParseErr(_))))
276 }
277
278 #[test]
279 fn encrypted_payload_no_magic() {
280 let document = vec![1, 73, 82, 79, 78, 0, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
281 let err = EncryptedPayload::try_from(document);
282 assert!(matches!(err, Err(Error::NoIronCoreMagic)))
283 }
284
285 #[test]
286 fn form_good_encrypted_payload() {
287 let document = vec![
288 3, 73, 82, 79, 78, 0, 42, 10, 28, 20, 31, 98, 61, 23, 74, 221, 61, 102, 44, 153, 142,
289 172, 70, 145, 180, 36, 193, 133, 249, 72, 1, 181, 31, 205, 205, 1, 197, 26, 10, 10, 8,
290 116, 101, 110, 97, 110, 116, 73, 100, 49, 113, 239, 60, 137, 159, 135, 94, 89, 92, 34,
291 19, 231, 165, 112, 184, 171, 237, 133, 20, 97, 193, 60, 0, 85, 139, 184, 144, 44, 184,
292 129, 210, 203, 21, 167, 53, 17, 51, 49, 42, 92, 207, 102, 98, 174, 198, 128, 199, 19,
293 42, 145, 251, 86, 201, 214, 33, 117, 232, 18, 93,
294 ];
295 let payload = EncryptedPayload::try_from(document);
296 assert!(payload.is_ok());
297 }
298
299 #[test]
300 fn decrypt_good_document() {
301 let dek = EncryptionKey((0..32).collect_vec().try_into().unwrap());
302 let document = vec![
303 3, 73, 82, 79, 78, 0, 42, 10, 28, 20, 31, 98, 61, 23, 74, 221, 61, 102, 44, 153, 142,
304 172, 70, 145, 180, 36, 193, 133, 249, 72, 1, 181, 31, 205, 205, 1, 197, 26, 10, 10, 8,
305 116, 101, 110, 97, 110, 116, 73, 100, 49, 113, 239, 60, 137, 159, 135, 94, 89, 92, 34,
306 19, 231, 165, 112, 184, 171, 237, 133, 20, 97, 193, 60, 0, 85, 139, 184, 144, 44, 184,
307 129, 210, 203, 21, 167, 53, 17, 51, 49, 42, 92, 207, 102, 98, 174, 198, 128, 199, 19,
308 42, 145, 251, 86, 201, 214, 33, 117, 232, 18, 93,
309 ];
310 let payload = EncryptedPayload::try_from(document).unwrap();
311 let decrypted = payload.decrypt(&dek).unwrap();
312 assert_eq!(decrypted.0, (0..32).collect_vec());
313 }
314
315 #[test]
316 fn decrypt_bad_signature_document() {
317 let dek = EncryptionKey((0..32).collect_vec().try_into().unwrap());
318 let document = vec![
319 3, 73, 82, 79, 78, 0, 42, 10, 28, 20, 32, 98, 61, 23, 74, 221, 61, 102, 44, 153, 142,
320 172, 70, 145, 180, 36, 193, 133, 249, 72, 1, 181, 31, 205, 205, 1, 197, 26, 10, 10, 8,
321 116, 101, 110, 97, 110, 116, 73, 100, 49, 113, 239, 60, 137, 159, 135, 94, 89, 92, 34,
322 19, 231, 165, 112, 184, 171, 237, 133, 20, 97, 193, 60, 0, 85, 139, 184, 144, 44, 184,
323 129, 210, 203, 21, 167, 53, 17, 51, 49, 42, 92, 207, 102, 98, 174, 198, 128, 199, 19,
324 42, 145, 251, 86, 201, 214, 33, 117, 232, 18, 93,
325 ];
326 let payload = EncryptedPayload::try_from(document).unwrap();
327 let err = payload.decrypt(&dek).unwrap_err();
328 assert!(matches!(err, Error::DecryptError(_)));
329 }
330
331 #[test]
332 fn encrypt_decrypt_roundtrip() {
333 let key = EncryptionKey((0..32).collect_vec().try_into().unwrap());
334 let plaintext = PlaintextDocument(vec![1, 2, 3, 4, 5]);
335 let mut rng = rand::rng();
336 let encrypted =
337 encrypt_detached_document(&mut rng, key, "tenantId", plaintext.clone()).unwrap();
338 assert!(encrypted.starts_with(&VERSION_AND_MAGIC));
339 let payload = EncryptedPayload::try_from(encrypted).unwrap();
340 let decrypted = payload.decrypt(&key).unwrap();
341 assert_eq!(decrypted, plaintext);
342 }
343
344 #[test]
345 fn encrypt_rejects_oversized_header() {
346 let key = EncryptionKey((0..32).collect_vec().try_into().unwrap());
347 let plaintext = PlaintextDocument(vec![1, 2, 3]);
348 let mut rng = rand::rng();
349 let huge_tenant_id = "a".repeat(u16::MAX as usize + 1);
351 let err = encrypt_detached_document(&mut rng, key, &huge_tenant_id, plaintext).unwrap_err();
352 assert!(matches!(err, Error::HeaderLengthOverflow(_)));
353 }
354}