ironcore_documents/v3/
mod.rs

1use crate::aes::IvAndCiphertext;
2use crate::v4::MAGIC;
3use crate::{
4    Error,
5    aes::{
6        EncryptionKey, PlaintextDocument, aes_encrypt_with_iv, decrypt_document_with_attached_iv,
7    },
8    icl_header_v3::V3DocumentHeader,
9    signing::AES_KEY_LEN,
10};
11use bytes::Bytes;
12use protobuf::Message;
13
14const IV_LEN: usize = 12;
15const GCM_TAG_LEN: usize = 16;
16
17const V3: u8 = 3u8;
18
19/// For external users to check the first bytes of an edoc.
20pub const VERSION_AND_MAGIC: [u8; 5] = [V3, MAGIC[0], MAGIC[1], MAGIC[2], MAGIC[3]];
21
22// [3, b"IRON]
23const MAGIC_HEADER_LEN: usize = 5;
24// 2 bytes indicate the length of the protobuf header
25const HEADER_LEN_LEN: usize = 2;
26const DETACHED_HEADER_LEN: usize = MAGIC_HEADER_LEN + HEADER_LEN_LEN;
27
28/// These are detached encrypted bytes, which means they have a `3IRON` +
29/// `<2 bytes of header length>` + `<proto V3DocumentHeader>` + IV + CIPHERTEXT.
30/// Not created directly, use the TryFrom implementation instead.
31#[derive(Debug, Clone)]
32pub struct EncryptedPayload {
33    v3_document_header: V3DocumentHeader,
34    iv_and_ciphertext: IvAndCiphertext,
35}
36
37impl TryFrom<Vec<u8>> for EncryptedPayload {
38    type Error = Error;
39    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
40        let value_len = value.len();
41        if value_len < DETACHED_HEADER_LEN {
42            Err(Error::EdocTooShort(value_len))?
43        };
44        let (magic_header, header_len_and_rest) = value.split_at(MAGIC_HEADER_LEN);
45        let (header_len_len, header_and_cipher) = header_len_and_rest.split_at(HEADER_LEN_LEN);
46        if magic_header != [&[V3], &MAGIC[..]].concat() {
47            Err(Error::NoIronCoreMagic)?
48        };
49        let header_len = u16::from_be_bytes(
50            header_len_len
51                .try_into()
52                .expect("This is safe as we split off 2 bytes."),
53        ) as usize;
54        if header_and_cipher.len() < header_len {
55            Err(Error::HeaderParseErr(format!(
56                "Proto header length specified: {}, bytes remaining: {}",
57                header_len,
58                header_and_cipher.len()
59            )))?
60        };
61        let (header, iv_and_cipher) = header_and_cipher.split_at(header_len);
62        let v3_document_header: V3DocumentHeader =
63            Message::parse_from_bytes(header).map_err(|_| {
64                Error::HeaderParseErr("Unable to parse header as V3DocumentHeader".to_string())
65            })?;
66        Ok(EncryptedPayload {
67            v3_document_header,
68            iv_and_ciphertext: iv_and_cipher.to_vec().into(),
69        })
70    }
71}
72
73impl EncryptedPayload {
74    /// Decrypt a V3 detached document and verify its signature.
75    pub fn decrypt(self, key: &EncryptionKey) -> Result<PlaintextDocument, Error> {
76        if verify_signature(key.0, &self.v3_document_header) {
77            decrypt_document_with_attached_iv(key, &self.iv_and_ciphertext)
78        } else {
79            Err(Error::DecryptError(
80                "Signature validation failed.".to_string(),
81            ))
82        }
83    }
84}
85
86struct V3Signature {
87    iv: [u8; IV_LEN],
88    gcm_tag: [u8; GCM_TAG_LEN],
89}
90
91fn decompose_signature(sig: &Bytes) -> Option<V3Signature> {
92    if sig.len() < IV_LEN + GCM_TAG_LEN {
93        None
94    } else {
95        let (iv, _) = sig.split_at(IV_LEN);
96        let (_, gcm_tag) = sig.split_at(sig.len() - GCM_TAG_LEN);
97        Some(V3Signature {
98            iv: iv.try_into().unwrap(),           // Length was validated up-front
99            gcm_tag: gcm_tag.try_into().unwrap(), // Length was validated up-front
100        })
101    }
102}
103
104pub fn verify_signature(key: [u8; AES_KEY_LEN], v3_header: &V3DocumentHeader) -> bool {
105    // If we have no header or authTag, that means this document was encrypted before the header was added, which would only happen if the
106    // document was encrypted with the Java SDK. In that case, we'll just ignore the verification and try to decrypt, which might still work.
107    if v3_header.header.is_none() || !v3_header.has_saas_shield() || v3_header.sig.is_empty() {
108        true
109    } else {
110        let maybe_sig = decompose_signature(&v3_header.sig);
111        match maybe_sig {
112            Some(sig) => aes_encrypt_with_iv(
113                crate::aes::EncryptionKey(key),
114                &v3_header
115                    .saas_shield()
116                    .write_to_bytes()
117                    .expect("Writing proto to bytes failed."),
118                sig.iv,
119                &[],
120            )
121            .map(|(_, new_sig)| {
122                let new_sig_length = new_sig.0.len();
123                let (_, new_gcm_tag) = new_sig.0.split_at(new_sig_length - GCM_TAG_LEN);
124                new_gcm_tag == sig.gcm_tag
125            })
126            .unwrap_or(false),
127            _ => false,
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use hex_literal::hex;
136    use itertools::Itertools;
137
138    #[test]
139    fn verify_known_good_sig_in_v3_header() {
140        // dek and proto_bytes copied from TSC-java test
141        let dek: [u8; 32] = (0..32).into_iter().collect_vec().try_into().unwrap();
142        let proto_bytes = vec![
143            10, 28, 49, 113, -17, 60, -119, -97, -121, 94, 89, 92, 34, 19, -54, -49, -110, -121,
144            -57, -116, -15, -106, 69, -116, -42, -112, 84, 73, -128, -57, 26, 10, 10, 8, 116, 101,
145            110, 97, 110, 116, 73, 100,
146        ]
147        .into_iter()
148        .map(|x| x as u8)
149        .collect_vec();
150        let header = Message::parse_from_bytes(&proto_bytes).unwrap();
151        assert!(verify_signature(dek, &header))
152    }
153
154    #[test]
155    fn verify_known_bad_sig_in_v3_header() {
156        // {
157        //   "sig": [1, 2, 3],
158        //   "saas_shield": {
159        //     "tenant_id": "tenantId"
160        //   }
161        // }
162        let proto_bytes = hex!("0a030102031a0a0a0874656e616e744964");
163        let dek: [u8; 32] = (0..32).into_iter().collect_vec().try_into().unwrap();
164        let header = Message::parse_from_bytes(&proto_bytes).unwrap();
165        assert!(!verify_signature(dek, &header));
166    }
167
168    #[test]
169    fn verify_empty_v3_header() {
170        let dek: [u8; 32] = (0..32).into_iter().collect_vec().try_into().unwrap();
171        let empty_header = V3DocumentHeader::new();
172        assert!(verify_signature(dek, &empty_header))
173    }
174
175    #[test]
176    fn verify_empty_sig_v3_header() {
177        // {
178        //   "sig": [],
179        //   "saas_shield": {
180        //     "tenant_id": "tenantId"
181        //   }
182        // }
183        let proto_bytes = hex!("0a001a0a0a0874656e616e744964");
184        let dek: [u8; 32] = (0..32).into_iter().collect_vec().try_into().unwrap();
185        let header = Message::parse_from_bytes(&proto_bytes).unwrap();
186        assert!(verify_signature(dek, &header));
187    }
188
189    #[test]
190    fn decompose_signature_works() {
191        let sig_1 = (0..28).collect_vec();
192        let decomposed_1 = decompose_signature(&sig_1.into()).unwrap();
193        let expected_iv_1 = (0..12).collect_vec();
194        let expected_tag_1 = (12..28).collect_vec();
195        assert_eq!(&decomposed_1.iv[..], expected_iv_1);
196        assert_eq!(&decomposed_1.gcm_tag[..], expected_tag_1);
197
198        let sig_2 = (0..100).collect_vec();
199        let decomposed_2 = decompose_signature(&sig_2.into()).unwrap();
200        let expected_iv_2 = (0..12).collect_vec();
201        let expected_tag_2 = (84..100).collect_vec();
202        assert_eq!(&decomposed_2.iv[..], expected_iv_2);
203        assert_eq!(&decomposed_2.gcm_tag[..], expected_tag_2);
204
205        let sig_3 = (0..10).collect_vec();
206        let decomposed_failure = decompose_signature(&sig_3.into());
207        assert!(decomposed_failure.is_none());
208    }
209
210    #[test]
211    fn encrypted_payload_too_short() {
212        let document = vec![3, 73, 82, 79, 78, 0];
213        let err = EncryptedPayload::try_from(document);
214        assert!(matches!(err, Err(Error::EdocTooShort(_))))
215    }
216
217    #[test]
218    fn encrypted_payload_invalid_header_len() {
219        let document = vec![
220            3, 73, 82, 79, 78, 0, 100, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
221        ];
222        let err = EncryptedPayload::try_from(document);
223        assert!(matches!(err, Err(Error::HeaderParseErr(_))))
224    }
225
226    #[test]
227    fn encrypted_payload_no_magic() {
228        let document = vec![1, 73, 82, 79, 78, 0, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
229        let err = EncryptedPayload::try_from(document);
230        assert!(matches!(err, Err(Error::NoIronCoreMagic)))
231    }
232
233    #[test]
234    fn form_good_encrypted_payload() {
235        let document = vec![
236            3, 73, 82, 79, 78, 0, 42, 10, 28, 20, 31, 98, 61, 23, 74, 221, 61, 102, 44, 153, 142,
237            172, 70, 145, 180, 36, 193, 133, 249, 72, 1, 181, 31, 205, 205, 1, 197, 26, 10, 10, 8,
238            116, 101, 110, 97, 110, 116, 73, 100, 49, 113, 239, 60, 137, 159, 135, 94, 89, 92, 34,
239            19, 231, 165, 112, 184, 171, 237, 133, 20, 97, 193, 60, 0, 85, 139, 184, 144, 44, 184,
240            129, 210, 203, 21, 167, 53, 17, 51, 49, 42, 92, 207, 102, 98, 174, 198, 128, 199, 19,
241            42, 145, 251, 86, 201, 214, 33, 117, 232, 18, 93,
242        ];
243        let payload = EncryptedPayload::try_from(document);
244        assert!(payload.is_ok());
245    }
246
247    #[test]
248    fn decrypt_good_document() {
249        let dek = EncryptionKey((0..32).collect_vec().try_into().unwrap());
250        let document = vec![
251            3, 73, 82, 79, 78, 0, 42, 10, 28, 20, 31, 98, 61, 23, 74, 221, 61, 102, 44, 153, 142,
252            172, 70, 145, 180, 36, 193, 133, 249, 72, 1, 181, 31, 205, 205, 1, 197, 26, 10, 10, 8,
253            116, 101, 110, 97, 110, 116, 73, 100, 49, 113, 239, 60, 137, 159, 135, 94, 89, 92, 34,
254            19, 231, 165, 112, 184, 171, 237, 133, 20, 97, 193, 60, 0, 85, 139, 184, 144, 44, 184,
255            129, 210, 203, 21, 167, 53, 17, 51, 49, 42, 92, 207, 102, 98, 174, 198, 128, 199, 19,
256            42, 145, 251, 86, 201, 214, 33, 117, 232, 18, 93,
257        ];
258        let payload = EncryptedPayload::try_from(document).unwrap();
259        let decrypted = payload.decrypt(&dek).unwrap();
260        assert_eq!(decrypted.0, (0..32).collect_vec());
261    }
262
263    #[test]
264    fn decrypt_bad_signature_document() {
265        let dek = EncryptionKey((0..32).collect_vec().try_into().unwrap());
266        let document = vec![
267            3, 73, 82, 79, 78, 0, 42, 10, 28, 20, 32, 98, 61, 23, 74, 221, 61, 102, 44, 153, 142,
268            172, 70, 145, 180, 36, 193, 133, 249, 72, 1, 181, 31, 205, 205, 1, 197, 26, 10, 10, 8,
269            116, 101, 110, 97, 110, 116, 73, 100, 49, 113, 239, 60, 137, 159, 135, 94, 89, 92, 34,
270            19, 231, 165, 112, 184, 171, 237, 133, 20, 97, 193, 60, 0, 85, 139, 184, 144, 44, 184,
271            129, 210, 203, 21, 167, 53, 17, 51, 49, 42, 92, 207, 102, 98, 174, 198, 128, 199, 19,
272            42, 145, 251, 86, 201, 214, 33, 117, 232, 18, 93,
273        ];
274        let payload = EncryptedPayload::try_from(document).unwrap();
275        let err = payload.decrypt(&dek).unwrap_err();
276        assert!(matches!(err, Error::DecryptError(_)));
277    }
278}