Skip to main content

ironcore_documents/v3/
mod.rs

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
22/// For external users to check the first bytes of an edoc.
23pub const VERSION_AND_MAGIC: [u8; 5] = [V3, MAGIC[0], MAGIC[1], MAGIC[2], MAGIC[3]];
24
25// [3, b"IRON]
26const MAGIC_HEADER_LEN: usize = 5;
27// 2 bytes indicate the length of the protobuf header
28const HEADER_LEN_LEN: usize = 2;
29const DETACHED_HEADER_LEN: usize = MAGIC_HEADER_LEN + HEADER_LEN_LEN;
30
31/// These are detached encrypted bytes, which means they have a `3IRON` +
32/// `<2 bytes of header length>` + `<proto V3DocumentHeader>` + IV + CIPHERTEXT.
33/// Not created directly, use the TryFrom implementation instead.
34#[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    /// Decrypt a V3 detached document and verify its signature.
78    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
89/// Encrypt a plaintext document into the V3 detached format:
90/// `[3][IRON][header_len_u16_BE][v3DocumentHeader proto][IV][AES-GCM ciphertext + tag]`
91pub 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    // Signature = AES-GCM(DEK, serialized header) → store IV ++ GCM tag
105    let (signature_iv, key_ciphertext) = aes_encrypt(key, &saas_header_bytes, &[], rng)?;
106    let signature: Vec<u8> = signature_iv
107        .iter()
108        // Just the tag bytes off the back
109        .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    // Sequence all our document pieces, see the method comment to match the order
127    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(),           // Length was validated up-front
151            gcm_tag: gcm_tag.try_into().unwrap(), // Length was validated up-front
152        })
153    }
154}
155
156pub fn verify_signature(key: [u8; AES_KEY_LEN], v3_header: &V3DocumentHeader) -> bool {
157    // If we have no header or authTag, that means this document was encrypted before the header was added, which would only happen if the
158    // document was encrypted with the Java SDK. In that case, we'll just ignore the verification and try to decrypt, which might still work.
159    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        // dek and proto_bytes copied from TSC-java test
193        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        // {
209        //   "sig": [1, 2, 3],
210        //   "saas_shield": {
211        //     "tenant_id": "tenantId"
212        //   }
213        // }
214        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        // {
230        //   "sig": [],
231        //   "saas_shield": {
232        //     "tenant_id": "tenantId"
233        //   }
234        // }
235        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        // A tenant_id large enough to push the serialized V3DocumentHeader past u16::MAX bytes.
350        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}