Skip to main content

deaddrop_core/crypto/
mod.rs

1//! Cryptographic policy lives here. Networking crates MUST go through `CryptoProvider`.
2//!
3//! Constructions: Ed25519, X25519, HKDF-SHA256, ChaCha20-Poly1305, BLAKE3, HMAC-SHA256.
4//! No custom primitives.
5
6use crate::{
7    DIGEST_LEN, DdError, Digest, ErrorCode, HashAlgorithm, NONCE_LEN, PeerId, PublicIdentity,
8    RecipientWrap, Result,
9};
10use chacha20poly1305::ChaCha20Poly1305;
11use chacha20poly1305::aead::{Aead, KeyInit, Payload};
12use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
13use hkdf::Hkdf;
14use hmac::{Hmac, Mac};
15use rand::RngCore;
16use rand::rngs::OsRng;
17use sha2::{Digest as _, Sha256};
18use x25519_dalek::{PublicKey as XPublic, StaticSecret};
19use zeroize::{Zeroize, ZeroizeOnDrop};
20
21type HmacSha256 = Hmac<Sha256>;
22
23pub const ENC_SCHEME: &str = "x25519-hkdf-chacha20poly1305-v2";
24pub const SESSION_SCHEME: &str = "x25519-hkdf-chacha20poly1305-session-v2";
25
26pub trait CryptoProvider: Send + Sync {
27    fn hash(&self, alg: HashAlgorithm, data: &[u8]) -> Digest;
28    fn sign(&self, sk: &SigningKey, msg: &[u8]) -> [u8; 64];
29    fn verify(&self, pk: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> Result<()>;
30    fn aead_encrypt(
31        &self,
32        key: &[u8; 32],
33        nonce: &[u8; NONCE_LEN],
34        aad: &[u8],
35        pt: &[u8],
36    ) -> Result<Vec<u8>>;
37    fn aead_decrypt(
38        &self,
39        key: &[u8; 32],
40        nonce: &[u8; NONCE_LEN],
41        aad: &[u8],
42        ct: &[u8],
43    ) -> Result<Vec<u8>>;
44}
45
46#[derive(Debug, Default, Clone, Copy)]
47pub struct DefaultProvider;
48
49impl CryptoProvider for DefaultProvider {
50    fn hash(&self, alg: HashAlgorithm, data: &[u8]) -> Digest {
51        match alg {
52            HashAlgorithm::Blake3 => Digest(*blake3::hash(data).as_bytes()),
53            HashAlgorithm::Sha256 => Digest(Sha256::digest(data).into()),
54        }
55    }
56
57    fn sign(&self, sk: &SigningKey, msg: &[u8]) -> [u8; 64] {
58        sk.sign(msg).to_bytes()
59    }
60
61    fn verify(&self, pk: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> Result<()> {
62        let vk =
63            VerifyingKey::from_bytes(pk).map_err(|_| DdError::crypto("bad ed25519 public key"))?;
64        vk.verify(msg, &Signature::from_bytes(sig))
65            .map_err(|_| DdError::crypto("signature invalid"))
66    }
67
68    fn aead_encrypt(
69        &self,
70        key: &[u8; 32],
71        nonce: &[u8; NONCE_LEN],
72        aad: &[u8],
73        pt: &[u8],
74    ) -> Result<Vec<u8>> {
75        let aead =
76            ChaCha20Poly1305::new_from_slice(key).map_err(|_| DdError::crypto("aead key"))?;
77        aead.encrypt(nonce.into(), Payload { msg: pt, aad })
78            .map_err(|_| DdError::crypto("encrypt failed"))
79    }
80
81    fn aead_decrypt(
82        &self,
83        key: &[u8; 32],
84        nonce: &[u8; NONCE_LEN],
85        aad: &[u8],
86        ct: &[u8],
87    ) -> Result<Vec<u8>> {
88        let aead =
89            ChaCha20Poly1305::new_from_slice(key).map_err(|_| DdError::crypto("aead key"))?;
90        aead.decrypt(nonce.into(), Payload { msg: ct, aad })
91            .map_err(|_| DdError::crypto("decrypt failed"))
92    }
93}
94
95impl DefaultProvider {
96    pub fn peer_id(&self, ed: &[u8; 32], x: &[u8; 32]) -> PeerId {
97        let mut buf = Vec::from(&b"ddp-nid-v2"[..]);
98        buf.extend_from_slice(ed);
99        buf.extend_from_slice(x);
100        PeerId::from_digest(self.hash(HashAlgorithm::Blake3, &buf).0)
101    }
102}
103
104#[derive(Zeroize, ZeroizeOnDrop)]
105pub struct PrivateIdentity {
106    #[zeroize(skip)]
107    pub public: PublicIdentity,
108    #[zeroize(skip)]
109    pub peer_id: PeerId,
110    ed25519: [u8; 32],
111    x25519: [u8; 32],
112}
113
114impl PrivateIdentity {
115    pub fn generate() -> Self {
116        let mut ed = [0u8; 32];
117        let mut x = [0u8; 32];
118        OsRng.fill_bytes(&mut ed);
119        OsRng.fill_bytes(&mut x);
120        Self::from_secrets(ed, x)
121    }
122
123    pub fn from_secrets(ed25519: [u8; 32], x25519: [u8; 32]) -> Self {
124        let p = DefaultProvider;
125        let signing = SigningKey::from_bytes(&ed25519);
126        let ed_pk = signing.verifying_key().to_bytes();
127        let x_sk = StaticSecret::from(x25519);
128        let x_pk = XPublic::from(&x_sk).to_bytes();
129        let mut pre = Vec::from(&b"ddp-id-v2"[..]);
130        pre.extend_from_slice(&ed_pk);
131        pre.extend_from_slice(&x_pk);
132        let signature = p.sign(&signing, &pre);
133        let public = PublicIdentity {
134            version: 2,
135            ed25519_pk: ed_pk,
136            x25519_pk: x_pk,
137            signature,
138        };
139        let peer_id = p.peer_id(&ed_pk, &x_pk);
140        Self {
141            public,
142            peer_id,
143            ed25519,
144            x25519,
145        }
146    }
147
148    pub fn signing_key(&self) -> SigningKey {
149        SigningKey::from_bytes(&self.ed25519)
150    }
151
152    pub fn x25519_secret(&self) -> StaticSecret {
153        StaticSecret::from(self.x25519)
154    }
155
156    pub fn ed25519_bytes(&self) -> [u8; 32] {
157        self.ed25519
158    }
159
160    pub fn x25519_bytes(&self) -> [u8; 32] {
161        self.x25519
162    }
163}
164
165pub fn verify_identity(id: &PublicIdentity) -> Result<PeerId> {
166    if id.version != 2 {
167        return Err(DdError::protocol(
168            ErrorCode::Ddp1002UnsupportedVersion,
169            "identity version",
170        ));
171    }
172    let p = DefaultProvider;
173    let mut pre = Vec::from(&b"ddp-id-v2"[..]);
174    pre.extend_from_slice(&id.ed25519_pk);
175    pre.extend_from_slice(&id.x25519_pk);
176    p.verify(&id.ed25519_pk, &pre, &id.signature)?;
177    Ok(p.peer_id(&id.ed25519_pk, &id.x25519_pk))
178}
179
180pub fn random_bytes<const N: usize>() -> [u8; N] {
181    let mut b = [0u8; N];
182    OsRng.fill_bytes(&mut b);
183    b
184}
185
186pub fn generate_cek() -> [u8; 32] {
187    random_bytes()
188}
189
190fn wrap_derive(shared: &[u8; 32], eph_pk: &[u8; 32]) -> Result<[u8; 32]> {
191    let hk = Hkdf::<Sha256>::new(Some(eph_pk), shared);
192    let mut key = [0u8; 32];
193    hk.expand(b"ddp-wrap-v2", &mut key)
194        .map_err(|_| DdError::crypto("hkdf wrap"))?;
195    Ok(key)
196}
197
198pub fn wrap_cek(
199    recipient_x25519: &[u8; 32],
200    recipient: PeerId,
201    cek: &[u8; 32],
202) -> Result<RecipientWrap> {
203    let eph_sk = StaticSecret::random_from_rng(OsRng);
204    let eph_pk = XPublic::from(&eph_sk);
205    let shared = eph_sk.diffie_hellman(&XPublic::from(*recipient_x25519));
206    let key = wrap_derive(shared.as_bytes(), eph_pk.as_bytes())?;
207    let nonce = random_bytes::<NONCE_LEN>();
208    let p = DefaultProvider;
209    let mut aad = Vec::from(&b"ddp-wrap-aad-v2"[..]);
210    aad.extend_from_slice(recipient.as_bytes());
211    let wrapped = p.aead_encrypt(&key, &nonce, &aad, cek)?;
212    Ok(RecipientWrap {
213        peer: recipient,
214        eph_pk: eph_pk.to_bytes(),
215        nonce,
216        wrapped_key: wrapped,
217    })
218}
219
220pub fn unwrap_cek(identity: &PrivateIdentity, wrap: &RecipientWrap) -> Result<[u8; 32]> {
221    if wrap.peer != identity.peer_id {
222        return Err(DdError::protocol(
223            ErrorCode::Ddp1008NotRecipient,
224            "wrap is for another peer",
225        ));
226    }
227    let eph = XPublic::from(wrap.eph_pk);
228    let shared = identity.x25519_secret().diffie_hellman(&eph);
229    let key = wrap_derive(shared.as_bytes(), eph.as_bytes())?;
230    let p = DefaultProvider;
231    let mut aad = Vec::from(&b"ddp-wrap-aad-v2"[..]);
232    aad.extend_from_slice(identity.peer_id.as_bytes());
233    let cek = p.aead_decrypt(&key, &wrap.nonce, &aad, &wrap.wrapped_key)?;
234    if cek.len() != 32 {
235        return Err(DdError::crypto("wrapped key length"));
236    }
237    let mut out = [0u8; 32];
238    out.copy_from_slice(&cek);
239    Ok(out)
240}
241
242pub fn encrypt_payload(
243    cek: &[u8; 32],
244    nonce: &[u8; NONCE_LEN],
245    aad: &[u8],
246    pt: &[u8],
247) -> Result<Vec<u8>> {
248    DefaultProvider.aead_encrypt(cek, nonce, aad, pt)
249}
250
251pub fn decrypt_payload(
252    cek: &[u8; 32],
253    nonce: &[u8; NONCE_LEN],
254    aad: &[u8],
255    ct: &[u8],
256) -> Result<Vec<u8>> {
257    DefaultProvider.aead_decrypt(cek, nonce, aad, ct)
258}
259
260/// Session keys from ephemeral X25519 (forward secrecy). Replay: transcript hash bound into HKDF.
261pub struct SessionKeys {
262    pub send: [u8; 32],
263    pub recv: [u8; 32],
264    pub transcript: [u8; DIGEST_LEN],
265}
266
267pub fn session_keys(
268    initiator: bool,
269    local_eph_sk: &StaticSecret,
270    remote_eph_pk: &XPublic,
271    transcript: &[u8],
272) -> Result<SessionKeys> {
273    let shared = local_eph_sk.diffie_hellman(remote_eph_pk);
274    let hk = Hkdf::<Sha256>::new(Some(transcript), shared.as_bytes());
275    let mut okm = [0u8; 64];
276    hk.expand(b"ddp-session-v2", &mut okm)
277        .map_err(|_| DdError::crypto("hkdf session"))?;
278    let mut a = [0u8; 32];
279    let mut b = [0u8; 32];
280    a.copy_from_slice(&okm[..32]);
281    b.copy_from_slice(&okm[32..]);
282    let transcript_d = DefaultProvider.hash(HashAlgorithm::Blake3, transcript).0;
283    Ok(if initiator {
284        SessionKeys {
285            send: a,
286            recv: b,
287            transcript: transcript_d,
288        }
289    } else {
290        SessionKeys {
291            send: b,
292            recv: a,
293            transcript: transcript_d,
294        }
295    })
296}
297
298pub fn hmac_tag(key: &[u8], data: &[u8]) -> [u8; 16] {
299    let mut mac = <HmacSha256 as Mac>::new_from_slice(key).expect("hmac");
300    mac.update(data);
301    let out = mac.finalize().into_bytes();
302    let mut tag = [0u8; 16];
303    tag.copy_from_slice(&out[..16]);
304    tag
305}
306
307pub fn u64_be(n: u64) -> [u8; 8] {
308    n.to_be_bytes()
309}
310
311pub fn push_lp_str(buf: &mut Vec<u8>, s: &str) {
312    let bytes = s.as_bytes();
313    buf.extend_from_slice(&(bytes.len() as u16).to_be_bytes());
314    buf.extend_from_slice(bytes);
315}
316
317pub fn x25519_ephemeral() -> (StaticSecret, XPublic) {
318    let sk = StaticSecret::random_from_rng(OsRng);
319    let pk = XPublic::from(&sk);
320    (sk, pk)
321}
322
323pub use x25519_dalek::{PublicKey as X25519Public, StaticSecret as X25519Secret};
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn identity_and_wrap() {
331        let a = PrivateIdentity::generate();
332        let b = PrivateIdentity::generate();
333        assert_eq!(verify_identity(&a.public).unwrap(), a.peer_id);
334        let cek = generate_cek();
335        let wrap = wrap_cek(&b.public.x25519_pk, b.peer_id, &cek).unwrap();
336        assert_eq!(unwrap_cek(&b, &wrap).unwrap(), cek);
337        assert!(unwrap_cek(&a, &wrap).is_err());
338    }
339
340    #[test]
341    fn decode_encode_hash() {
342        let p = DefaultProvider;
343        let d = p.hash(HashAlgorithm::Blake3, b"abc");
344        assert_eq!(d.0.len(), 32);
345    }
346}