xmtp_attachments/
derive.rs1use sha2::{Digest as _, Sha256};
2use xmtp_content_types::remote_attachment::RemoteAttachment;
3
4use crate::{AttachmentError, KeyMaterial, local_file_name};
5
6pub fn download_url(base_url: &str, content_digest_hex: &str) -> String {
8 format!("{base_url}/{content_digest_hex}")
9}
10
11pub fn remote_attachment(
13 base_url: &str,
14 content_digest_hex: &str,
15 material: &KeyMaterial,
16 content_length: u32,
17 filename: Option<&str>,
18) -> RemoteAttachment {
19 let url = download_url(base_url, content_digest_hex);
20 let scheme = url
21 .split_once("://")
22 .map_or(String::new(), |(scheme, _)| format!("{scheme}://"));
23 RemoteAttachment {
24 url,
25 content_digest: content_digest_hex.to_owned(),
26 secret: material.secret.to_vec(),
27 salt: material.salt.to_vec(),
28 nonce: material.nonce.to_vec(),
29 content_length: Some(content_length),
30 filename: filename.map(str::to_owned),
31 scheme,
32 }
33}
34
35pub fn attachment_key(ra: &RemoteAttachment) -> Result<String, AttachmentError> {
37 let material = KeyMaterial::from_remote(ra)?;
38 let mut digest = [0u8; 32];
39 hex::decode_to_slice(&ra.content_digest, &mut digest)
40 .map_err(|_| AttachmentError::new(crate::AttachmentFailureCause::Malformed))?;
41 let mut hash = Sha256::new();
42 hash.update(digest);
43 hash.update(material.secret);
44 hash.update(material.salt);
45 hash.update(material.nonce);
46 Ok(hex::encode(hash.finalize()))
47}
48
49pub fn plaintext_rel_path(ra: &RemoteAttachment) -> Result<String, AttachmentError> {
51 Ok(format!(
52 "{}/{}",
53 attachment_key(ra)?,
54 local_file_name(ra.filename.as_deref())
55 ))
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::AttachmentFailureCause;
62
63 fn material() -> KeyMaterial {
64 KeyMaterial {
65 secret: std::array::from_fn(|index| index as u8 + 32),
66 salt: std::array::from_fn(|index| index as u8 + 64),
67 nonce: std::array::from_fn(|index| index as u8 + 96),
68 }
69 }
70
71 fn remote() -> RemoteAttachment {
72 remote_attachment(
73 "https://attachments.example.test/objects",
74 &hex::encode((0u8..32).collect::<Vec<_>>()),
75 &material(),
76 1234,
77 Some("report.pdf"),
78 )
79 }
80
81 #[xmtp_common::test(unwrap_try = true)]
83 async fn download_url_form() {
84 assert_eq!(
85 download_url("https://example.test/objects", "abc123"),
86 "https://example.test/objects/abc123"
87 );
88 }
89
90 #[xmtp_common::test(unwrap_try = true)]
91 async fn remote_attachment_fields() {
92 let ra = remote();
93 assert_eq!(
94 ra.url,
95 format!(
96 "https://attachments.example.test/objects/{}",
97 ra.content_digest
98 )
99 );
100 assert_eq!(ra.scheme, "https://");
101 assert_eq!(ra.content_length, Some(1234));
102 assert_eq!(ra.filename.as_deref(), Some("report.pdf"));
103 assert_eq!(ra.secret, material().secret);
104 assert_eq!(ra.salt, material().salt);
105 assert_eq!(ra.nonce, material().nonce);
106 assert_eq!(KeyMaterial::from_remote(&ra)?, material());
107 }
108
109 #[xmtp_common::test(unwrap_try = true)]
112 async fn attachment_key_vector() {
113 let ra = remote();
114 let expected = "44d21db70716bd7644cb0d819fa6791805ebc526ea32996a60e41dc753fcfafc";
115 assert_eq!(attachment_key(&ra)?, expected);
116 assert_eq!(plaintext_rel_path(&ra)?, format!("{expected}/report.pdf"));
117 }
118
119 #[xmtp_common::test(unwrap_try = true)]
120 async fn malformed_key_material() {
121 let valid = remote();
122 let mut bad_cases = Vec::new();
123 let mut bad = valid.clone();
124 bad.content_digest.pop();
125 bad_cases.push(bad);
126 let mut bad = valid.clone();
127 bad.content_digest.replace_range(..1, "A");
128 bad_cases.push(bad);
129 let mut bad = valid.clone();
130 bad.secret.pop();
131 bad_cases.push(bad);
132 let mut bad = valid.clone();
133 bad.salt.pop();
134 bad_cases.push(bad);
135 let mut bad = valid;
136 bad.nonce.pop();
137 bad_cases.push(bad);
138 for ra in bad_cases {
139 assert_eq!(
140 KeyMaterial::from_remote(&ra).unwrap_err().cause,
141 AttachmentFailureCause::Malformed
142 );
143 assert_eq!(
144 attachment_key(&ra).unwrap_err().cause,
145 AttachmentFailureCause::Malformed
146 );
147 assert_eq!(
148 plaintext_rel_path(&ra).unwrap_err().cause,
149 AttachmentFailureCause::Malformed
150 );
151 }
152 }
153}