1use crate::{CodecId, ContentType, DntCompression, DntError, DntResult, KeyId};
14use appcore_contracts::ApplicationId;
15use appcore_types::TenantId;
16
17pub const DNT_MAGIC: [u8; 8] = *b"APDNT\0\0\x01";
19pub const DNT_ENVELOPE_VERSION_V1: u16 = 1;
21pub const DNT_MAX_HEADER_BYTES: usize = 64 * 1024;
23pub const DNT_MAX_ENCRYPTED_METADATA_BYTES: usize = 64 * 1024;
25pub const DNT_CONTENT_JSON: &str = "application/json";
27pub const DNT_CONTENT_OCTET_STREAM: &str = "application/octet-stream";
29pub const DNT_CONTENT_SECRET: &str = "appcore/secret";
31pub const DNT_CONTENT_SNAPSHOT: &str = "appcore/snapshot";
33pub const DNT_CONTENT_SYNC_EVENT: &str = "appcore/sync-event";
35pub const DNT_CONTENT_BACKUP: &str = "appcore/backup";
37
38const MIN_PREFIX_BYTES: usize = 14;
39const NONCE_BYTES: usize = 24;
40const HASH_BYTES: usize = 32;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum DntAlgorithm {
46 XChaCha20Poly1305,
48}
49
50impl DntAlgorithm {
51 pub(crate) fn id(self) -> u16 {
52 match self {
53 Self::XChaCha20Poly1305 => 1,
54 }
55 }
56
57 fn from_id(value: u16) -> DntResult<Self> {
58 match value {
59 1 => Ok(Self::XChaCha20Poly1305),
60 _ => Err(DntError::UnsupportedVersion),
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct DntHeader {
68 pub envelope_version: u16,
70 pub header_length: u32,
72 pub flags: u32,
74 pub algorithm: DntAlgorithm,
76 pub application_id: ApplicationId,
78 pub tenant_id: Option<TenantId>,
80 pub content_type: ContentType,
82 pub codec_id: CodecId,
84 pub key_id: KeyId,
86 pub schema_version: u32,
88 pub created_at_ms: u64,
90 pub payload_length: u64,
92 pub nonce: [u8; NONCE_BYTES],
94 pub payload_hash: [u8; HASH_BYTES],
96 pub public_metadata: Vec<u8>,
98 pub encrypted_metadata_length: u32,
100}
101
102impl DntHeader {
103 pub fn nonce(&self) -> &[u8; NONCE_BYTES] {
105 &self.nonce
106 }
107
108 pub fn compression(&self) -> DntCompression {
110 if self.flags & crate::DNT_FLAG_PAYLOAD_DEFLATE != 0 {
111 return DntCompression::Deflate;
112 }
113 DntCompression::None
114 }
115}
116
117pub(crate) struct HeaderParts {
118 pub(crate) flags: u32,
119 pub(crate) algorithm: DntAlgorithm,
120 pub(crate) application_id: ApplicationId,
121 pub(crate) tenant_id: Option<TenantId>,
122 pub(crate) content_type: ContentType,
123 pub(crate) codec_id: CodecId,
124 pub(crate) key_id: KeyId,
125 pub(crate) schema_version: u32,
126 pub(crate) created_at_ms: u64,
127 pub(crate) payload_length: u64,
128 pub(crate) nonce: [u8; NONCE_BYTES],
129 pub(crate) payload_hash: [u8; HASH_BYTES],
130 pub(crate) public_metadata: Vec<u8>,
131 pub(crate) encrypted_metadata_length: u32,
132}
133
134pub(crate) fn encode_header(parts: HeaderParts) -> DntResult<Vec<u8>> {
135 if parts.public_metadata.len() > DNT_MAX_HEADER_BYTES
136 || parts.encrypted_metadata_length as usize > DNT_MAX_ENCRYPTED_METADATA_BYTES
137 {
138 return Err(DntError::PayloadTooLarge);
139 }
140 let capacity = 256usize
141 .checked_add(parts.public_metadata.len())
142 .ok_or(DntError::InvalidFormat)?;
143 let mut header = Vec::with_capacity(capacity);
144 header.extend_from_slice(&DNT_MAGIC);
145 put_u16(&mut header, DNT_ENVELOPE_VERSION_V1);
146 put_u32(&mut header, 0);
147 put_u32(&mut header, parts.flags);
148 put_u16(&mut header, parts.algorithm.id());
149 put_u32(&mut header, parts.schema_version);
150 put_u64(&mut header, parts.created_at_ms);
151 put_u64(&mut header, parts.payload_length);
152 header.extend_from_slice(&parts.nonce);
153 header.extend_from_slice(&parts.payload_hash);
154 put_u32(
155 &mut header,
156 checked_u32(parts.public_metadata.len(), DntError::InvalidFormat)?,
157 );
158 put_u32(&mut header, parts.encrypted_metadata_length);
159 put_text(&mut header, parts.application_id.as_str())?;
160 put_optional_text(
161 &mut header,
162 parts.tenant_id.as_ref().map(|value| value.as_str()),
163 )?;
164 put_text(&mut header, parts.content_type.as_str())?;
165 put_text(&mut header, parts.codec_id.as_str())?;
166 put_text(&mut header, parts.key_id.as_str())?;
167 header.extend_from_slice(&parts.public_metadata);
168 if header.len() > DNT_MAX_HEADER_BYTES {
169 return Err(DntError::InvalidFormat);
170 }
171 let length = checked_u32(header.len(), DntError::InvalidFormat)?;
172 header[10..14].copy_from_slice(&length.to_be_bytes());
173 Ok(header)
174}
175
176pub fn inspect_header(input: &[u8]) -> DntResult<DntHeader> {
178 if input.len() < MIN_PREFIX_BYTES || input[..8] != DNT_MAGIC {
179 return Err(DntError::InvalidFormat);
180 }
181 let version = read_u16(input, 8)?;
182 if version != DNT_ENVELOPE_VERSION_V1 {
183 return Err(DntError::UnsupportedVersion);
184 }
185 let header_length = read_u32(input, 10)?;
186 let header_len = usize::try_from(header_length).map_err(|_| DntError::InvalidFormat)?;
187 if !(MIN_PREFIX_BYTES..=DNT_MAX_HEADER_BYTES).contains(&header_len) || input.len() < header_len
188 {
189 return Err(DntError::InvalidFormat);
190 }
191 parse_header(&input[..header_len], header_length)
192}
193
194fn parse_header(input: &[u8], header_length: u32) -> DntResult<DntHeader> {
195 let mut cursor = 14usize;
196 let flags = take_u32(input, &mut cursor)?;
197 crate::flags::validate_flags(flags)?;
198 let algorithm = DntAlgorithm::from_id(take_u16(input, &mut cursor)?)?;
199 let schema_version = take_u32(input, &mut cursor)?;
200 let created_at_ms = take_u64(input, &mut cursor)?;
201 let payload_length = take_u64(input, &mut cursor)?;
202 let nonce = take_array::<NONCE_BYTES>(input, &mut cursor)?;
203 let payload_hash = take_array::<HASH_BYTES>(input, &mut cursor)?;
204 let public_metadata_length = take_u32(input, &mut cursor)?;
205 let encrypted_metadata_length = take_u32(input, &mut cursor)?;
206 if encrypted_metadata_length as usize > DNT_MAX_ENCRYPTED_METADATA_BYTES {
207 return Err(DntError::PayloadTooLarge);
208 }
209 let application_id =
210 ApplicationId::new(take_text(input, &mut cursor)?).map_err(|_| DntError::InvalidFormat)?;
211 let tenant_text = take_optional_text(input, &mut cursor)?;
212 let tenant_id = tenant_text
213 .map(TenantId::new)
214 .transpose()
215 .map_err(|_| DntError::InvalidFormat)?;
216 let content_type = ContentType::new(take_text(input, &mut cursor)?)?;
217 let codec_id = CodecId::new(take_text(input, &mut cursor)?)?;
218 let key_id = KeyId::new(take_text(input, &mut cursor)?)?;
219 let metadata_len =
220 usize::try_from(public_metadata_length).map_err(|_| DntError::InvalidFormat)?;
221 let public_metadata = take_bytes(input, &mut cursor, metadata_len)?.to_vec();
222 if cursor != input.len() {
223 return Err(DntError::InvalidFormat);
224 }
225 Ok(DntHeader {
226 envelope_version: DNT_ENVELOPE_VERSION_V1,
227 header_length,
228 flags,
229 algorithm,
230 application_id,
231 tenant_id,
232 content_type,
233 codec_id,
234 key_id,
235 schema_version,
236 created_at_ms,
237 payload_length,
238 nonce,
239 payload_hash,
240 public_metadata,
241 encrypted_metadata_length,
242 })
243}
244
245fn put_u16(output: &mut Vec<u8>, value: u16) {
246 output.extend_from_slice(&value.to_be_bytes());
247}
248
249fn put_u32(output: &mut Vec<u8>, value: u32) {
250 output.extend_from_slice(&value.to_be_bytes());
251}
252
253fn put_u64(output: &mut Vec<u8>, value: u64) {
254 output.extend_from_slice(&value.to_be_bytes());
255}
256
257fn put_text(output: &mut Vec<u8>, value: &str) -> DntResult<()> {
258 let bytes = value.as_bytes();
259 let length = u16::try_from(bytes.len()).map_err(|_| DntError::InvalidFormat)?;
260 put_u16(output, length);
261 output.extend_from_slice(bytes);
262 Ok(())
263}
264
265fn put_optional_text(output: &mut Vec<u8>, value: Option<&str>) -> DntResult<()> {
266 match value {
267 Some(value) => put_text(output, value),
268 None => {
269 put_u16(output, 0);
270 Ok(())
271 }
272 }
273}
274
275fn read_u16(input: &[u8], offset: usize) -> DntResult<u16> {
276 let bytes = input
277 .get(offset..offset + 2)
278 .ok_or(DntError::InvalidFormat)?;
279 Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
280}
281
282fn read_u32(input: &[u8], offset: usize) -> DntResult<u32> {
283 let bytes = input
284 .get(offset..offset + 4)
285 .ok_or(DntError::InvalidFormat)?;
286 Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
287}
288
289fn take_u16(input: &[u8], cursor: &mut usize) -> DntResult<u16> {
290 let value = read_u16(input, *cursor)?;
291 *cursor += 2;
292 Ok(value)
293}
294
295fn take_u32(input: &[u8], cursor: &mut usize) -> DntResult<u32> {
296 let value = read_u32(input, *cursor)?;
297 *cursor += 4;
298 Ok(value)
299}
300
301fn take_u64(input: &[u8], cursor: &mut usize) -> DntResult<u64> {
302 let bytes = take_bytes(input, cursor, 8)?;
303 Ok(u64::from_be_bytes([
304 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
305 ]))
306}
307
308fn take_array<const N: usize>(input: &[u8], cursor: &mut usize) -> DntResult<[u8; N]> {
309 let mut output = [0u8; N];
310 output.copy_from_slice(take_bytes(input, cursor, N)?);
311 Ok(output)
312}
313
314fn take_text(input: &[u8], cursor: &mut usize) -> DntResult<String> {
315 let length = take_u16(input, cursor)? as usize;
316 if length == 0 {
317 return Err(DntError::InvalidFormat);
318 }
319 let bytes = take_bytes(input, cursor, length)?;
320 std::str::from_utf8(bytes)
321 .map(str::to_string)
322 .map_err(|_| DntError::InvalidFormat)
323}
324
325fn take_optional_text(input: &[u8], cursor: &mut usize) -> DntResult<Option<String>> {
326 let length = take_u16(input, cursor)? as usize;
327 if length == 0 {
328 return Ok(None);
329 }
330 let bytes = take_bytes(input, cursor, length)?;
331 std::str::from_utf8(bytes)
332 .map(|value| Some(value.to_string()))
333 .map_err(|_| DntError::InvalidFormat)
334}
335
336fn take_bytes<'a>(input: &'a [u8], cursor: &mut usize, length: usize) -> DntResult<&'a [u8]> {
337 let end = cursor.checked_add(length).ok_or(DntError::InvalidFormat)?;
338 let bytes = input.get(*cursor..end).ok_or(DntError::InvalidFormat)?;
339 *cursor = end;
340 Ok(bytes)
341}
342
343fn checked_u32(value: usize, error: DntError) -> DntResult<u32> {
344 u32::try_from(value).map_err(|_| error)
345}