Skip to main content

ironcore_documents/v5/
mod.rs

1// Reexport the v4 aes, because we also use it for v5.
2pub use crate::v4::aes;
3pub mod attached;
4pub mod key_id_header;
5use crate::{
6    Error, Result,
7    aes::{EncryptionKey, IvAndCiphertext, PlaintextDocument, aes_encrypt},
8    icl_header_v4::V4DocumentHeader,
9};
10use bytes::{Buf, Bytes};
11use key_id_header::KeyIdHeader;
12use rand::CryptoRng;
13
14// The V5 data format is defined by 2 data formats. One for the edek and one for encrypted data encrypted with that edek.
15// The edek format is a 6 byte key id (see the key_id_header module) followed by a V4DocumentHeader proto.
16// The edoc format is the EncryptedPayload below, which is 0 + IRON folowed by the encrypted data (iv, aes encrypted data and tag).
17const MAGIC: &[u8; 4] = crate::v4::MAGIC;
18pub(crate) const V0: u8 = 0u8;
19/// For external users to check the first bytes of an edoc.
20pub const VERSION_AND_MAGIC: [u8; 5] = [V0, MAGIC[0], MAGIC[1], MAGIC[2], MAGIC[3]];
21/// This is 0 + IRON
22pub(crate) const DETACHED_HEADER_LEN: usize = 5;
23
24/// These are detached encrypted bytes, which means they have a `0IRON` + IV + CIPHERTEXT.
25/// This value is correct by construction and will be validated when we create it.
26/// There is no public constructor, only the TryFrom implementations.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct EncryptedPayload(IvAndCiphertext);
29
30impl Default for EncryptedPayload {
31    fn default() -> EncryptedPayload {
32        EncryptedPayload(Bytes::new().into())
33    }
34}
35
36impl TryFrom<Bytes> for EncryptedPayload {
37    type Error = Error;
38
39    fn try_from(mut value: Bytes) -> core::result::Result<Self, Self::Error> {
40        if value.len() < DETACHED_HEADER_LEN {
41            Err(Error::EdocTooShort(value.len()))
42        } else if value.get_u8() == V0 {
43            let maybe_magic = value.split_to(MAGIC.len());
44            if maybe_magic.as_ref() == MAGIC {
45                Ok(EncryptedPayload(value.into()))
46            } else {
47                Err(Error::NoIronCoreMagic)
48            }
49        } else {
50            Err(Error::HeaderParseErr(
51                "`0IRON` magic expected on the encrypted document.".to_string(),
52            ))
53        }
54    }
55}
56
57impl TryFrom<Vec<u8>> for EncryptedPayload {
58    type Error = Error;
59    fn try_from(value: Vec<u8>) -> core::result::Result<Self, Self::Error> {
60        Bytes::from(value).try_into()
61    }
62}
63
64impl From<IvAndCiphertext> for EncryptedPayload {
65    fn from(value: IvAndCiphertext) -> Self {
66        EncryptedPayload(value)
67    }
68}
69
70impl EncryptedPayload {
71    /// Convert the encrypted payload to t
72    pub fn to_aes_value_with_attached_iv(self) -> IvAndCiphertext {
73        self.0
74    }
75
76    /// Decrypt a V5 detached document. The document should have the expected header
77    pub fn decrypt(self, key: &EncryptionKey) -> Result<PlaintextDocument> {
78        crate::aes::decrypt_document_with_attached_iv(key, &self.to_aes_value_with_attached_iv())
79    }
80
81    pub fn write_to_bytes(&self) -> Vec<u8> {
82        let mut result = Vec::with_capacity(self.0.len() + DETACHED_HEADER_LEN);
83        result.push(V0);
84        result.extend_from_slice(MAGIC);
85        result.extend_from_slice(self.0.as_ref());
86        result
87    }
88}
89
90/// Encrypt a document to be used as a detached document. This means it will have a header of `0IRON` as the first
91/// 5 bytes.
92pub fn encrypt_detached_document<R: CryptoRng>(
93    rng: &mut R,
94    key: EncryptionKey,
95    document: PlaintextDocument,
96) -> Result<EncryptedPayload> {
97    let (iv, enc_data) = aes_encrypt(key, &document.0, &[], rng)?;
98    Ok(EncryptedPayload(IvAndCiphertext(
99        iv.into_iter().chain(enc_data.0).collect(),
100    )))
101}
102
103pub fn parse_standard_edek(edek_bytes: Bytes) -> Result<(KeyIdHeader, V4DocumentHeader)> {
104    let (key_id_header, proto_bytes) = key_id_header::decode_version_prefixed_value(edek_bytes)?;
105    let pb = protobuf::Message::parse_from_bytes(&proto_bytes[..])
106        .map_err(|e| Error::HeaderParseErr(e.to_string()))?;
107    Ok((key_id_header, pb))
108}
109
110pub fn parse_standard_edoc(edoc: Bytes) -> Result<IvAndCiphertext> {
111    let encrypted_payload: EncryptedPayload = edoc.try_into()?;
112    Ok(encrypted_payload.to_aes_value_with_attached_iv())
113}
114
115#[cfg(test)]
116mod test {
117    use super::*;
118    use hex_literal::hex;
119    use rand::SeedableRng;
120    use rand_chacha::ChaCha20Rng;
121    #[test]
122    fn encrypt_decrypt_detached_document_roundtrips() {
123        let mut rng = ChaCha20Rng::seed_from_u64(172u64);
124        let key = EncryptionKey(hex!(
125            "fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
126        ));
127        let plaintext = PlaintextDocument(vec![100u8, 200u8]);
128        let encrypted = encrypt_detached_document(&mut rng, key, plaintext.clone()).unwrap();
129        let result = encrypted.decrypt(&key).unwrap();
130        assert_eq!(result, plaintext);
131    }
132
133    #[test]
134    fn creation_fails_too_short() {
135        let encrypted_payload_or_error: Result<EncryptedPayload> =
136            hex!("00495241").to_vec().try_into();
137
138        let result = encrypted_payload_or_error.unwrap_err();
139        assert_eq!(result, Error::EdocTooShort(4));
140    }
141
142    #[test]
143    fn creation_fails_wrong_bytes() {
144        // Wrong first byte.
145        let encrypted_payload_or_error: Result<EncryptedPayload> =
146            hex!("0149524f4efa5111111111").to_vec().try_into();
147        let result = encrypted_payload_or_error.unwrap_err();
148        assert_eq!(
149            result,
150            Error::HeaderParseErr("`0IRON` magic expected on the encrypted document.".to_string())
151        );
152
153        // right first byte, but IRON magic wrong.
154        let encrypted_payload_or_error: Result<EncryptedPayload> =
155            hex!("0000524f4efa5111111111").to_vec().try_into();
156        let result = encrypted_payload_or_error.unwrap_err();
157        assert_eq!(result, Error::NoIronCoreMagic);
158    }
159}