1use crate::chunk::{ChunkingAlg, Manifest, chunk_payload, reassemble};
2use crate::crypto::{
3 CryptoProvider, DefaultProvider, ENC_SCHEME, PrivateIdentity, decrypt_payload, encrypt_payload,
4 generate_cek, push_lp_str, random_bytes, u64_be, unwrap_cek, wrap_cek,
5};
6use crate::{
7 DEFAULT_TTL_SECS, DdError, Destination, DropEnvelope, ErrorCode, HashAlgorithm, MAX_RECIPIENTS,
8 MAX_TTL_SECS, MIN_TTL_SECS, PROTOCOL_VERSION, PayloadDescriptor, PeerId, Priority,
9 PublicIdentity, Result, RoutingPolicy, SecurityDescriptor,
10};
11
12pub struct CreateDrop<'a> {
13 pub author: &'a PrivateIdentity,
14 pub recipients: Vec<(PeerId, PublicIdentity)>,
15 pub destination: Destination,
16 pub plaintext: Vec<u8>,
17 pub now: u64,
18 pub ttl_secs: Option<u64>,
19 pub priority: Priority,
20 pub routing: RoutingPolicy,
21 pub application: String,
22 pub topic: Option<String>,
23 pub chunking: ChunkingAlg,
24 pub compress: bool,
25 pub hop_limit: u8,
26 pub public: bool,
27 pub seal_until: Option<u64>,
28 pub seal_quorum: Option<u32>,
29 pub erasure: Option<crate::chunk::ErasureSpec>,
30}
31
32pub struct BuiltDrop {
33 pub envelope: DropEnvelope,
34 pub manifest: Manifest,
35 pub chunks: Vec<Vec<u8>>,
36}
37
38pub fn envelope_preimage(env: &DropEnvelope) -> Vec<u8> {
39 let mut buf = Vec::from(&b"ddp-env-v2"[..]);
40 buf.extend_from_slice(&env.protocol_version.to_be_bytes());
41 buf.extend_from_slice(env.source.as_bytes());
42 buf.extend_from_slice(&u64_be(env.creation_time));
43 buf.extend_from_slice(&u64_be(env.expiration));
44 buf.push(env.priority.as_u8());
45 buf.push(env.hop_limit);
46 push_lp_str(&mut buf, &env.application);
47 match &env.payload_descriptor {
48 PayloadDescriptor::Chunked {
49 length,
50 manifest_id,
51 content_id,
52 } => {
53 buf.extend_from_slice(&u64_be(*length));
54 buf.extend_from_slice(manifest_id.as_bytes());
55 buf.extend_from_slice(content_id.as_bytes());
56 }
57 PayloadDescriptor::Inline { length, content_id }
58 | PayloadDescriptor::Blob { length, content_id } => {
59 buf.extend_from_slice(&u64_be(*length));
60 buf.extend_from_slice(content_id.as_bytes());
61 }
62 PayloadDescriptor::Manifest { manifest_id }
63 | PayloadDescriptor::StreamManifest { manifest_id } => {
64 buf.extend_from_slice(manifest_id.as_bytes());
65 }
66 PayloadDescriptor::Collection { members } => {
67 buf.extend_from_slice(&(members.len() as u32).to_be_bytes());
68 for m in members {
69 buf.extend_from_slice(m.as_bytes());
70 }
71 }
72 }
73 buf.extend_from_slice(&env.author_pk);
74 if !env.extensions.is_empty() {
75 buf.extend_from_slice(b"ddp-ext-v2");
76 for e in &env.extensions {
77 crate::crypto::push_lp_str(&mut buf, &e.name);
78 buf.push(u8::from(e.critical));
79 buf.extend_from_slice(&(e.data.len() as u32).to_be_bytes());
80 buf.extend_from_slice(&e.data);
81 }
82 }
83 buf
84}
85
86pub fn build_drop(req: CreateDrop<'_>) -> Result<BuiltDrop> {
87 if req.recipients.len() > MAX_RECIPIENTS {
88 return Err(DdError::protocol(
89 ErrorCode::Ddp1006LimitExceeded,
90 "too many recipients",
91 ));
92 }
93 let ttl = req
94 .ttl_secs
95 .unwrap_or(DEFAULT_TTL_SECS)
96 .clamp(MIN_TTL_SECS, MAX_TTL_SECS);
97 let expires = req.now.saturating_add(ttl);
98 let mut body = req.plaintext;
99 if req.compress {
100 body = zstd_compress(&body)?;
101 }
102 let (ciphertext, security) = if req.public || matches!(req.destination, Destination::Public) {
103 (
104 body,
105 SecurityDescriptor {
106 scheme: "signed-only-v2".into(),
107 public: true,
108 wraps: vec![],
109 content_nonce: [0u8; 12],
110 },
111 )
112 } else {
113 let cek = generate_cek();
114 let nonce = random_bytes::<12>();
115 let mut aad = Vec::from(&b"ddp-payload-aad-v2"[..]);
116 aad.extend_from_slice(req.author.peer_id.as_bytes());
117 let ct = encrypt_payload(&cek, &nonce, &aad, &body)?;
118 let mut wraps = Vec::new();
119 for (peer, ident) in &req.recipients {
120 wraps.push(wrap_cek(&ident.x25519_pk, *peer, &cek)?);
121 }
122 (
123 ct,
124 SecurityDescriptor {
125 scheme: ENC_SCHEME.into(),
126 public: false,
127 wraps,
128 content_nonce: nonce,
129 },
130 )
131 };
132 let chunked = if let Some(spec) = req.erasure {
133 crate::chunk::apply_erasure(chunk_payload(&ciphertext, req.chunking)?, spec)?
134 } else {
135 chunk_payload(&ciphertext, req.chunking)?
136 };
137 let manifest_id = chunked.manifest.id();
138 let mut env = DropEnvelope {
139 protocol_version: PROTOCOL_VERSION,
140 object_id: crate::ObjectId::blake3([0u8; 32]),
141 source: req.author.peer_id,
142 destination: req.destination,
143 creation_time: req.now,
144 expiration: expires,
145 priority: req.priority,
146 hop_limit: req.hop_limit.clamp(1, crate::MAX_HOP_LIMIT),
147 hop_count: 0,
148 payload_descriptor: PayloadDescriptor::Chunked {
149 length: chunked.manifest.total_length,
150 manifest_id,
151 content_id: chunked.manifest.payload_hash,
152 },
153 routing_policy: req.routing,
154 security_descriptor: security,
155 application: req.application,
156 topic: req.topic,
157 extensions: seal_extensions(req.seal_until, req.seal_quorum),
158 author_pk: req.author.public.ed25519_pk,
159 signature: [0u8; 64],
160 };
161 let pre = envelope_preimage(&env);
162 env.signature = DefaultProvider.sign(&req.author.signing_key(), &pre);
163 let oid_digest = DefaultProvider.hash(crate::HashAlgorithm::Blake3, &{
164 let mut b = pre.clone();
165 b.extend_from_slice(&env.signature);
166 b
167 });
168 env.object_id = crate::ObjectId::blake3(oid_digest.0);
169 Ok(BuiltDrop {
170 envelope: env,
171 manifest: chunked.manifest,
172 chunks: chunked.chunks,
173 })
174}
175
176fn seal_extensions(until: Option<u64>, quorum: Option<u32>) -> Vec<crate::Extension> {
177 let mut out = Vec::new();
178 if let Some(ts) = until {
179 out.push(crate::Extension {
180 name: crate::SEAL_UNTIL_EXT.into(),
181 critical: false,
182 data: ts.to_be_bytes().to_vec(),
183 });
184 }
185 if let Some(n) = quorum {
186 out.push(crate::Extension {
187 name: crate::SEAL_QUORUM_EXT.into(),
188 critical: false,
189 data: n.to_be_bytes().to_vec(),
190 });
191 }
192 out
193}
194
195fn zstd_compress(data: &[u8]) -> Result<Vec<u8>> {
196 zstd::encode_all(data, 3).map_err(|e| DdError::crypto(e.to_string()))
197}
198
199pub fn zstd_decompress(data: &[u8]) -> Result<Vec<u8>> {
200 zstd::decode_all(data).map_err(|e| DdError::crypto(e.to_string()))
201}
202
203pub fn verify_envelope(env: &DropEnvelope, now: u64) -> Result<()> {
204 if env.protocol_version != PROTOCOL_VERSION {
205 return Err(DdError::protocol(
206 ErrorCode::Ddp1002UnsupportedVersion,
207 format!("DDP/{}", env.protocol_version),
208 ));
209 }
210 env.validate_extension_count()?;
211 for ext in &env.extensions {
212 if ext.critical && ext.name != crate::SEAL_UNTIL_EXT && ext.name != crate::SEAL_QUORUM_EXT {
213 return Err(DdError::protocol(
214 ErrorCode::Ddp1003UnknownCriticalExtension,
215 ext.name.clone(),
216 ));
217 }
218 }
219 if env.expiration <= env.creation_time {
220 return Err(DdError::protocol(
221 ErrorCode::Ddp1001InvalidFrame,
222 "expiration",
223 ));
224 }
225 if now >= env.expiration {
226 return Err(DdError::protocol(ErrorCode::Ddp1007Expired, "expired"));
227 }
228 if env.hop_count > env.hop_limit {
229 return Err(DdError::protocol(ErrorCode::Ddp1001InvalidFrame, "hops"));
230 }
231 let pre = envelope_preimage(env);
232 DefaultProvider.verify(&env.author_pk, &pre, &env.signature)?;
233 let oid = crate::ObjectId::blake3(
234 DefaultProvider
235 .hash(HashAlgorithm::Blake3, &{
236 let mut b = pre;
237 b.extend_from_slice(&env.signature);
238 b
239 })
240 .0,
241 );
242 if oid != env.object_id {
243 return Err(DdError::invalid_frame("object_id mismatch"));
244 }
245 Ok(())
246}
247
248pub fn decrypt_payload_for(
249 identity: &PrivateIdentity,
250 env: &DropEnvelope,
251 ciphertext: &[u8],
252) -> Result<Vec<u8>> {
253 if env.security_descriptor.public {
254 return Ok(ciphertext.to_vec());
255 }
256 let wrap = env
257 .security_descriptor
258 .wraps
259 .iter()
260 .find(|w| w.peer == identity.peer_id)
261 .ok_or_else(|| DdError::protocol(ErrorCode::Ddp1008NotRecipient, "no wrap"))?;
262 let cek = unwrap_cek(identity, wrap)?;
263 let mut aad = Vec::from(&b"ddp-payload-aad-v2"[..]);
264 aad.extend_from_slice(env.source.as_bytes());
265 decrypt_payload(
266 &cek,
267 &env.security_descriptor.content_nonce,
268 &aad,
269 ciphertext,
270 )
271}
272
273pub fn open_drop(
274 identity: &PrivateIdentity,
275 env: &DropEnvelope,
276 manifest: &Manifest,
277 chunks: &[Vec<u8>],
278) -> Result<Vec<u8>> {
279 open_drop_at(identity, env, manifest, chunks, crate::store::unix_now(), 0)
280}
281
282pub fn open_drop_at(
285 identity: &PrivateIdentity,
286 env: &DropEnvelope,
287 manifest: &Manifest,
288 chunks: &[Vec<u8>],
289 now: u64,
290 receipt_issuers: u32,
291) -> Result<Vec<u8>> {
292 crate::sealed::enforce(env, now, receipt_issuers)?;
293 let ct = if let Some(info) = &manifest.erasure {
294 let _ = info;
295 reassemble(manifest, chunks)?
296 } else {
297 reassemble(manifest, chunks)?
298 };
299 decrypt_payload_for(identity, env, &ct)
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use crate::crypto::verify_identity;
306
307 #[test]
308 fn build_verify_open() {
309 let alice = PrivateIdentity::generate();
310 let charlie = PrivateIdentity::generate();
311 let cid = verify_identity(&charlie.public).unwrap();
312 let built = build_drop(CreateDrop {
313 author: &alice,
314 recipients: vec![(cid, charlie.public.clone())],
315 destination: Destination::One { peer: cid },
316 plaintext: b"secret".to_vec(),
317 now: 1_700_000_000,
318 ttl_secs: Some(3600),
319 priority: Priority::Normal,
320 routing: RoutingPolicy::default(),
321 application: "dd.file".into(),
322 topic: None,
323 chunking: crate::chunk::default_fixed(),
324 compress: false,
325 hop_limit: 8,
326 public: false,
327 seal_until: None,
328 seal_quorum: None,
329 erasure: None,
330 })
331 .unwrap();
332 verify_envelope(&built.envelope, 1_700_000_100).unwrap();
333 let pt = open_drop(&charlie, &built.envelope, &built.manifest, &built.chunks).unwrap();
334 assert_eq!(pt, b"secret");
335 assert!(open_drop(&alice, &built.envelope, &built.manifest, &built.chunks).is_err());
336 }
337}