diff --git a/src/v3/mod.rs b/src/v3/mod.rs
index 077d366..1e4df0d 100644
--- a/src/v3/mod.rs
+++ b/src/v3/mod.rs
@@ -14,7 +14,10 @@ use protobuf::Message;
const IV_LEN: usize = 12;
const GCM_TAG_LEN: usize = 16;
-pub const V3: u8 = 3u8;
+const V3: u8 = 3u8;
+
+/// For external users to check the first bytes of an edoc.
+pub const VERSION_AND_MAGIC: [u8; 5] = [V3, MAGIC[0], MAGIC[1], MAGIC[2], MAGIC[3]];
// [3, b"IRON]
const MAGIC_HEADER_LEN: usize = 5;
diff --git a/src/v5/attached.rs b/src/v5/attached.rs
new file mode 100644
index 0000000..2001afd
--- /dev/null
+++ b/src/v5/attached.rs
@@ -0,0 +1,160 @@
+use bytes::{Buf, Bytes};
+use protobuf::Message;
+
+use crate::{aes::IvAndCiphertext, icl_header_v4::V4DocumentHeader, Error};
+
+use super::key_id_header::{self, KeyIdHeader};
+
+type Result = std::result::Result;
+
+#[derive(Debug, PartialEq)]
+pub struct AttachedDocument {
+ pub key_id_header: KeyIdHeader,
+ pub edek: V4DocumentHeader,
+ pub edoc: IvAndCiphertext,
+}
+
+impl TryFrom> for AttachedDocument {
+ type Error = Error;
+
+ /// Breaks apart an attached edoc into its parts.
+ fn try_from(value: Vec) -> Result {
+ Bytes::from(value).try_into()
+ }
+}
+
+impl TryFrom for AttachedDocument {
+ type Error = Error;
+
+ /// Breaks apart an attached edoc into its parts.
+ fn try_from(value: Bytes) -> Result {
+ let (key_id_header, mut attached_document_with_header) =
+ key_id_header::decode_version_prefixed_value(value)?;
+ if attached_document_with_header.len() > 2 {
+ let edek_len = attached_document_with_header.get_u16();
+ if attached_document_with_header.len() > edek_len as usize {
+ let header_bytes = attached_document_with_header.split_to(edek_len as usize);
+ let edek = protobuf::Message::parse_from_bytes(&header_bytes[..])
+ .map_err(|e| Error::HeaderParseErr(e.to_string()))?;
+ Ok(AttachedDocument {
+ key_id_header,
+ edek,
+ edoc: IvAndCiphertext(attached_document_with_header),
+ })
+ } else {
+ Err(Error::HeaderParseErr(
+ "The EDEK you passed in was too short based on the length bytes.".to_string(),
+ ))
+ }
+ } else {
+ Err(Error::HeaderParseErr("Header is too short.".to_string()))
+ }
+ }
+}
+
+impl AttachedDocument {
+ /// Write out the entire v5 attached documents to bytes.
+ pub fn write_to_bytes(&self) -> Result {
+ let AttachedDocument {
+ key_id_header,
+ edek,
+ edoc,
+ } = self;
+ let key_id_header_bytes = key_id_header.write_to_bytes();
+ let encoded_edek = edek.write_to_bytes().expect("Writing to bytes is safe");
+ if encoded_edek.len() > u16::MAX as usize {
+ Err(Error::HeaderLengthOverflow(encoded_edek.len() as u64))
+ } else {
+ let len = encoded_edek.len() as u16;
+
+ let result = [
+ key_id_header_bytes.as_ref(),
+ &len.to_be_bytes(),
+ &encoded_edek,
+ &edoc.0, // Note that the edoc is written without the leading OIRON since it's an attached document.
+ ]
+ .concat();
+ Ok(result.into())
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{
+ aes::IvAndCiphertext,
+ icl_header_v4::{
+ v4document_header::{
+ edek_wrapper::{Aes256GcmEncryptedDek, Edek},
+ EdekWrapper, SignedPayload,
+ },
+ V4DocumentHeader,
+ },
+ v5::key_id_header::{EdekType, KeyId, KeyIdHeader, PayloadType},
+ Error,
+ };
+
+ use super::*;
+
+ #[test]
+ fn test_roundtrip() {
+ let key_id_header = KeyIdHeader::new(
+ EdekType::SaasShield,
+ PayloadType::StandardEdek,
+ KeyId(u32::MAX),
+ );
+
+ let edek_wrapper = EdekWrapper {
+ edek: Some(Edek::Aes256GcmEdek(Aes256GcmEncryptedDek {
+ ciphertext: [42u8; 255].as_ref().into(),
+ ..Default::default()
+ })),
+ ..Default::default()
+ };
+
+ let edek = V4DocumentHeader {
+ signed_payload: Some(SignedPayload {
+ edeks: vec![edek_wrapper],
+ ..Default::default()
+ })
+ .into(),
+ ..Default::default()
+ };
+
+ let edoc = IvAndCiphertext(vec![100, 200, 0].into());
+ let expected_result = AttachedDocument {
+ key_id_header,
+ edek,
+ edoc,
+ };
+ let encoded = expected_result.write_to_bytes().unwrap();
+ let result: AttachedDocument = encoded.try_into().unwrap();
+ assert_eq!(result, expected_result);
+ }
+
+ #[test]
+ fn test_len_too_long() {
+ // The `0,255` after the `0` is the u16 representing length. It is too large.
+ let encoded = vec![
+ 255u8, 255, 255, 255, 2, 0, 0, 255, 18, 7, 18, 5, 26, 3, 18, 1, 42, 100, 200,
+ ];
+ let result = AttachedDocument::try_from(encoded).unwrap_err();
+ assert_eq!(
+ result,
+ Error::HeaderParseErr(
+ "The EDEK you passed in was too short based on the length bytes.".to_string()
+ )
+ );
+ }
+
+ #[test]
+ fn test_header_too_short() {
+ // This value is just a key_id header with 1 0 after.
+ let encoded = vec![255u8, 255, 255, 255, 2, 0, 0];
+ let result = AttachedDocument::try_from(encoded).unwrap_err();
+ assert_eq!(
+ result,
+ Error::HeaderParseErr("Header is too short.".to_string())
+ );
+ }
+}
diff --git a/src/v5/key_id_header.rs b/src/v5/key_id_header.rs
index da961b8..5de72e2 100644
--- a/src/v5/key_id_header.rs
+++ b/src/v5/key_id_header.rs
@@ -109,6 +109,7 @@ impl EdekType {
}
/// The key id header parsed into its pieces.
+#[derive(Debug, PartialEq)]
pub struct KeyIdHeader {
pub key_id: KeyId,
pub edek_type: EdekType,
diff --git a/src/v5/mod.rs b/src/v5/mod.rs
index ea05e05..44794b8 100644
--- a/src/v5/mod.rs
+++ b/src/v5/mod.rs
@@ -1,5 +1,6 @@
// Reexport the v4 aes, because we also use it for v5.
pub use crate::v4::aes;
+pub mod attached;
pub mod key_id_header;
use crate::{
aes::{aes_encrypt, EncryptionKey, IvAndCiphertext, PlaintextDocument},
@@ -17,6 +18,8 @@ use rand::{CryptoRng, RngCore};
type Result = core::result::Result;
const MAGIC: &[u8; 4] = crate::v4::MAGIC;
pub(crate) const V0: u8 = 0u8;
+/// For external users to check the first bytes of an edoc.
+pub const VERSION_AND_MAGIC: [u8; 5] = [V0, MAGIC[0], MAGIC[1], MAGIC[2], MAGIC[3]];
/// This is 0 + IRON
pub(crate) const DETACHED_HEADER_LEN: usize = 5;