aven-protocol 0.1.0

Storage-independent encrypted sync protocol for Aven
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
//! Sequence-zero membership and one-vault claim, without transport.
//!
//! The fixed genesis profile uses strict Ed25519 and an HPKE base-mode self grant.
//! Operator setup authority, not the self signature, admits the first device.
//! Public validation cannot prove the encrypted self grant is usable. Hosts must
//! verify it and persist exact authority outside replaceable databases before use.
//! A fixed signed publication successor preserves genesis authority. Neither
//! preparation nor server acceptance enables client sync or advances a local
//! protected checkpoint.

use std::fmt;

use anyhow::{Context, Result, ensure};
use chacha20::ChaCha20Rng;
use ed25519_dalek::{Signer, SigningKey};
use hpke::{Deserializable, OpModeR, OpModeS, Serializable};
use rand_core::SeedableRng;
use subtle::ConstantTimeEq;
use zeroize::Zeroizing;

use crate::context::{LocalSharedStatePackageContext, LocalSharedStatePackageKey};
pub mod codec;

use codec::{MEMBERSHIP_SIGN, Reader, bytes, cce, hash, membership_core, valid};

type Kem = hpke::kem::X25519HkdfSha256;
type Aead = hpke::aead::ChaCha20Poly1305;
type Kdf = hpke::kdf::HkdfSha256;
type HpkePrivate = <Kem as hpke::Kem>::PrivateKey;
type Enc = <Kem as hpke::Kem>::EncappedKey;

pub const GENESIS_BYTES: usize = 902;
pub const CLAIM_BYTES: usize = 912;
/// Signing seed, HPKE private key, token and exact public genesis. Host-only storage.
pub const SEED_STORAGE_BYTES: usize = 96 + GENESIS_BYTES;

/// A high-entropy secret with redacted diagnostics. Never serialize into task data.
pub struct Secret(Zeroizing<[u8; 32]>);

impl Secret {
    pub fn new(bytes: [u8; 32]) -> Self {
        Self(Zeroizing::new(bytes))
    }

    pub fn generate() -> Result<Self> {
        let mut bytes = Zeroizing::new([0; 32]);
        getrandom::fill(bytes.as_mut()).context("error seed-claim-entropy-unavailable")?;
        Ok(Self(bytes))
    }

    /// For protected persistence or authenticated transport only, never logs.
    pub fn expose(&self) -> &[u8; 32] {
        &self.0
    }
}

impl fmt::Debug for Secret {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("Secret([REDACTED])")
    }
}

/// Immutable, publicly verified sequence-zero record. Not proof of admission.
#[derive(Clone, PartialEq, Eq)]
pub struct Genesis {
    record: [u8; GENESIS_BYTES],
    context: LocalSharedStatePackageContext,
    setup: [u8; 32],
    claim: [u8; 32],
    device: [u8; 32],
    signing_public: [u8; 32],
    hpke_public: [u8; 32],
    verifier: [u8; 32],
    generation_commitment: [u8; 32],
}

impl fmt::Debug for Genesis {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Genesis")
            .field("commitment", &self.commitment())
            .finish()
    }
}

impl Genesis {
    pub fn from_claim(bytes: &[u8]) -> Result<Self> {
        Self::from_record(codec::claim_record(bytes)?)
    }

    pub fn from_record(record: &[u8]) -> Result<Self> {
        codec::parse(record)
    }

    pub fn record(&self) -> &[u8; GENESIS_BYTES] {
        &self.record
    }

    pub fn commitment(&self) -> [u8; 32] {
        hash(&self.record)
    }

    pub fn authorizes_bearer(&self, token: &Secret) -> bool {
        bool::from(
            credential_verifier(self.context.vault_id, self.device, token).ct_eq(&self.verifier),
        )
    }

    pub fn context(&self) -> LocalSharedStatePackageContext {
        self.context
    }

    pub fn setup_id(&self) -> [u8; 32] {
        self.setup
    }

    pub fn claim_id(&self) -> [u8; 32] {
        self.claim
    }

    pub fn device_id(&self) -> [u8; 32] {
        self.device
    }

    pub fn signing_public(&self) -> [u8; 32] {
        self.signing_public
    }

    pub fn hpke_public(&self) -> [u8; 32] {
        self.hpke_public
    }

    pub fn verifier(&self) -> [u8; 32] {
        self.verifier
    }

    pub fn generation_commitment(&self) -> [u8; 32] {
        self.generation_commitment
    }

    pub fn claim_bytes(&self) -> Vec<u8> {
        let mut out = b"AVCL\0\x01".to_vec();
        bytes(&mut out, &self.record);
        out
    }

    fn info(&self) -> Vec<u8> {
        cce(
            "aven-e2ee/v1/genesis/self",
            &[
                &1_u16.to_be_bytes(),
                &[1],
                &self.context.vault_id,
                &self.setup,
                &self.claim,
                &self.device,
                &self.hpke_public,
                &self.context.generation_id,
            ],
        )
    }

    fn state(&self) -> Vec<u8> {
        let mut out = b"AVGS\0\x01\x01".to_vec();
        out.extend(self.context.vault_id);
        out.extend([0, 0, 0, 1]);
        out.extend(self.device);
        out.extend(self.signing_public);
        out.extend(self.hpke_public);
        out.extend(self.verifier);
        out.extend(1_u32.to_be_bytes());
        out.extend(self.claim);
        out.push(1);
        out.extend(self.context.generation_id);
        out.extend(self.generation_commitment);
        out.extend(0_u64.to_be_bytes());
        out
    }

    fn core(&self, state: &[u8]) -> Vec<u8> {
        let mut body = b"AVGC\0\x01".to_vec();
        body.extend(self.setup);
        body.extend(self.claim);
        membership_core(
            &self.context.vault_id,
            0,
            &[0; 32],
            &self.device,
            1,
            &body,
            state,
        )
    }
}

/// Private seed material and pinned exact genesis, validated against package authority.
/// Construction alone is not durable. The host must save before making a request.
pub struct SeedAuthority {
    signing: Secret,
    recipient: Secret,
    bearer: Secret,
    genesis: Genesis,
}

impl fmt::Debug for SeedAuthority {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("SeedAuthority([REDACTED])")
    }
}

impl SeedAuthority {
    pub fn generate(
        context: LocalSharedStatePackageContext,
        key: &LocalSharedStatePackageKey,
        setup: [u8; 32],
    ) -> Result<Self> {
        let signing = Secret::generate()?;
        let ikm = Secret::generate()?;
        let (recipient, _) = <Kem as hpke::Kem>::derive_keypair(ikm.expose());
        let bearer = Secret::generate()?;
        let claim = *Secret::generate()?.expose();
        let device = *Secret::generate()?.expose();
        let entropy = Secret::generate()?;
        let mut rng = ChaCha20Rng::from_seed(*entropy.expose());
        Self::build(
            context, key, setup, claim, device, signing, recipient, bearer, &mut rng,
        )
    }

    #[allow(clippy::too_many_arguments)]
    fn build(
        context: LocalSharedStatePackageContext,
        key: &LocalSharedStatePackageKey,
        setup: [u8; 32],
        claim: [u8; 32],
        device: [u8; 32],
        signing: Secret,
        recipient: HpkePrivate,
        bearer: Secret,
        rng: &mut impl rand_core::CryptoRng,
    ) -> Result<Self> {
        let signer = SigningKey::from_bytes(signing.expose());
        let hpke_public = <Kem as hpke::Kem>::sk_to_pk(&recipient);
        let mut genesis = Genesis {
            record: [0; GENESIS_BYTES],
            context,
            setup,
            claim,
            device,
            signing_public: signer.verifying_key().to_bytes(),
            hpke_public: hpke_public.to_bytes().into(),
            verifier: credential_verifier(context.vault_id, device, &bearer),
            generation_commitment: generation_commitment(context, key.protected_storage_bytes()),
        };
        let state = genesis.state();
        let core = genesis.core(&state);
        let plaintext = self_plaintext(&genesis, key.protected_storage_bytes());
        let (enc, ciphertext) = hpke::single_shot_seal_with_rng::<Aead, Kdf, Kem>(
            &OpModeS::Base,
            &hpke_public,
            &genesis.info(),
            &plaintext,
            &core,
            rng,
        )
        .map_err(|_| anyhow::anyhow!("error seed-claim-self-seal"))?;
        let mut attachments = b"AVGA\0\x01\x01\x01\x01".to_vec();
        bytes(&mut attachments, &device);
        bytes(&mut attachments, &genesis.hpke_public);
        bytes(&mut attachments, &enc.to_bytes());
        bytes(&mut attachments, &ciphertext);
        let signature = signer.sign(&cce(MEMBERSHIP_SIGN, &[&core, &attachments]));
        let mut record = vec![1];
        for component in [&core[..], &state, &attachments, &signature.to_bytes()] {
            bytes(&mut record, component);
        }
        genesis = Genesis::from_record(&record)?;
        let authority = Self {
            signing,
            recipient: Secret::new(recipient.to_bytes().into()),
            bearer,
            genesis,
        };
        authority.validate(context, key)?;
        Ok(authority)
    }

    pub fn genesis(&self) -> &Genesis {
        &self.genesis
    }

    /// Borrows signing material only for authenticated device operations or protected storage.
    /// Never place these bytes in logs, task data or ordinary backups.
    pub fn protected_signing_seed(&self) -> &Secret {
        &self.signing
    }

    /// Borrows recipient material only for authenticated device operations or protected storage.
    /// Never place these bytes in logs, task data or ordinary backups.
    pub fn protected_recipient_key(&self) -> &Secret {
        &self.recipient
    }

    pub fn bearer(&self) -> &Secret {
        &self.bearer
    }

    /// Exact protected-storage representation, never SQLite or ordinary backups.
    pub fn protected_storage_bytes(&self) -> Zeroizing<Vec<u8>> {
        let mut out = Zeroizing::new(Vec::with_capacity(SEED_STORAGE_BYTES));
        out.extend_from_slice(self.signing.expose());
        out.extend_from_slice(self.recipient.expose());
        out.extend_from_slice(self.bearer.expose());
        out.extend_from_slice(self.genesis.record());
        out
    }

    pub fn from_protected_storage(
        raw: &[u8],
        context: LocalSharedStatePackageContext,
        key: &LocalSharedStatePackageKey,
    ) -> Result<Self> {
        valid(raw.len() == SEED_STORAGE_BYTES)?;
        let mut reader = Reader(raw);
        let signing = Secret::new(reader.array()?);
        let recipient = Secret::new(reader.array()?);
        let bearer = Secret::new(reader.array()?);
        let genesis = Genesis::from_record(reader.take(GENESIS_BYTES)?)?;
        reader.end()?;
        let authority = Self {
            signing,
            recipient,
            bearer,
            genesis,
        };
        authority.validate(context, key)?;
        Ok(authority)
    }

    pub fn validate(
        &self,
        context: LocalSharedStatePackageContext,
        key: &LocalSharedStatePackageKey,
    ) -> Result<()> {
        let g = &self.genesis;
        valid(g.context == context)?;
        valid(
            SigningKey::from_bytes(self.signing.expose())
                .verifying_key()
                .to_bytes()
                == g.signing_public,
        )?;
        valid(bool::from(
            credential_verifier(context.vault_id, g.device, &self.bearer).ct_eq(&g.verifier),
        ))?;
        valid(
            generation_commitment(context, key.protected_storage_bytes())
                == g.generation_commitment,
        )?;
        let private = HpkePrivate::from_bytes(self.recipient.expose())
            .map_err(|_| anyhow::anyhow!("error seed-claim-recipient"))?;
        valid(<Kem as hpke::Kem>::sk_to_pk(&private).to_bytes().as_slice() == g.hpke_public)?;
        let (core, _, attachments, _) = codec::components(&g.record)?;
        let mut reader = Reader(attachments);
        reader.take(9)?;
        reader.blob(32)?;
        reader.blob(32)?;
        let enc = Enc::from_bytes(reader.blob(32)?)
            .map_err(|_| anyhow::anyhow!("error seed-claim-encapsulation"))?;
        let ciphertext = reader.blob(191)?;
        reader.end()?;
        let plaintext = Zeroizing::new(
            hpke::single_shot_open::<Aead, Kdf, Kem>(
                &OpModeR::Base,
                &private,
                &enc,
                &g.info(),
                ciphertext,
                core,
            )
            .map_err(|_| anyhow::anyhow!("error seed-claim-self-open"))?,
        );
        valid(bool::from(
            plaintext
                .as_slice()
                .ct_eq(&self_plaintext(g, key.protected_storage_bytes())),
        ))
    }
}

fn self_plaintext(g: &Genesis, secret: &[u8; 32]) -> Zeroizing<Vec<u8>> {
    let mut out = Zeroizing::new(b"AVGK\0\x01\x01".to_vec());
    for value in [
        &g.context.vault_id,
        &g.claim,
        &g.device,
        &g.context.generation_id,
        secret,
    ] {
        out.extend_from_slice(value);
    }
    out.extend_from_slice(&0_u64.to_be_bytes());
    out
}

pub fn generation_commitment(
    context: LocalSharedStatePackageContext,
    secret: &[u8; 32],
) -> [u8; 32] {
    let message = Zeroizing::new(cce(
        "aven-e2ee/v1/generation/commit",
        &[&context.vault_id, &context.generation_id, secret],
    ));
    hash(&message)
}

pub fn credential_verifier(vault: [u8; 32], device: [u8; 32], token: &Secret) -> [u8; 32] {
    let message = Zeroizing::new(cce(
        "aven-e2ee/v1/credential/verifier",
        &[&vault, &device, &1_u32.to_be_bytes(), token.expose()],
    ));
    hash(&message)
}

/// Operator-configured public verifier. Never accept this value from a claimant.
#[derive(Clone, Debug)]
pub struct SetupAuthority {
    id: [u8; 32],
    verifier: [u8; 32],
}

impl SetupAuthority {
    pub fn from_verifier(id: [u8; 32], verifier: [u8; 32]) -> Self {
        Self { id, verifier }
    }

    pub fn id(&self) -> [u8; 32] {
        self.id
    }

    pub fn verifier(id: [u8; 32], secret: &Secret) -> [u8; 32] {
        let message = Zeroizing::new(cce("aven-e2ee/v1/setup/verifier", &[&id, secret.expose()]));
        hash(&message)
    }

    pub fn authorizes(&self, id: [u8; 32], secret: &Secret) -> bool {
        self.id == id && bool::from(Self::verifier(id, secret).ct_eq(&self.verifier))
    }
}

#[derive(Debug)]
pub enum ClaimAuthentication<'a> {
    SetupSecret(&'a Secret),
    SeedBearer(&'a Secret),
}

/// A claim refusal decided inside the claim transaction. Any other claim
/// error leaves the outcome unknown to the claimant.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ClaimRefusal {
    /// The credential does not authorize this claim; `claimed` records
    /// whether storage already held a claim.
    Unauthorized { claimed: bool },
    /// Unclaimed storage whose setup verifier expired, refusing the exact
    /// secret that verifier was issued for.
    Expired,
    /// Storage holds a different claim.
    Conflict,
    /// The stored claim was retired by publication.
    Retired,
}

impl fmt::Display for ClaimRefusal {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Self::Unauthorized { .. } => "error seed-claim-unauthorized",
            Self::Expired => "error seed-claim-setup-expired",
            Self::Conflict => "error seed-claim-conflict",
            Self::Retired => "error seed-claim-retired",
        })
    }
}

impl std::error::Error for ClaimRefusal {}

/// Server storage must not contain local domain data or sync history.
#[derive(Debug)]
pub struct StorageNotEmpty;

impl fmt::Display for StorageNotEmpty {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("error e2ee-server-storage-not-empty")
    }
}

impl std::error::Error for StorageNotEmpty {}

/// Equality result, not proof of READY, active membership or physical durability.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClaimResult {
    pub vault_id: [u8; 32],
    pub claim_id: [u8; 32],
    pub genesis_commitment: [u8; 32],
}

impl ClaimResult {
    pub fn from_genesis(g: &Genesis) -> Self {
        Self {
            vault_id: g.context.vault_id,
            claim_id: g.claim,
            genesis_commitment: g.commitment(),
        }
    }

    pub fn validate_pinned(&self, pinned: &Genesis) -> Result<()> {
        ensure!(
            *self == Self::from_genesis(pinned),
            "error seed-claim-result-mismatch"
        );
        Ok(())
    }
}

#[cfg(test)]
mod tests;

pub mod membership;
pub mod peer;
pub mod publication;
pub use publication::{PUBLICATION_BYTES, Publication, PublicationBinding, PublicationOutcome};