Skip to main content

deaddrop_core/protocol/
envelope.rs

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
282/// `receipt_issuers` is the count of distinct stored witness/delivery receipts.
283/// Quorum seals are local policy, not a cryptographic time-lock.
284pub 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}