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