Skip to main content

gunnar_crypt/
envelope.rs

1//! One encrypted file (blob). Two versions; this code writes v2 and reads both.
2//!
3//! **v2 — chunked, the version every new seal writes:**
4//!
5//! ```text
6//! offset  0   8 bytes  magic        00 47 4E 43 52 59 50 54   ("\0GNCRYPT")
7//!         8   1 byte   version      02
8//!         9   4 bytes  epoch        big-endian
9//!        13   4 bytes  chunk size   big-endian (1 MiB = 00 10 00 00)
10//!        17  32 bytes  file nonce   HMAC-SHA256(nonce key, the whole plaintext)
11//!        49   …        blocks       16-byte SIV tag ‖ ciphertext, one per chunk
12//! ```
13//!
14//! * The plaintext is cut into **fixed chunks** of [`CHUNK`] bytes; the last
15//!   one is shorter, and it is empty only when the whole file is (then it is
16//!   the one block). Each block is sealed alone with **AES-256-SIV** (RFC 5297)
17//!   under the epoch key, with associated data
18//!   `header (49 bytes) ‖ block index (u64, big-endian) ‖ last (1 byte: 1 on
19//!   the last block, else 0)`. That is the STREAM construction (Hoang,
20//!   Reyhanitabar, Rogaway, Vizár, "Online Authenticated-Encryption and its
21//!   Nonce-Reuse Misuse-Resistance", CRYPTO 2015) with a deterministic AEAD:
22//!   blocks cannot be reordered (the index), a dropped tail or an appended
23//!   block does not open (the last flag), and a filter can seal and open while
24//!   it reads, holding one chunk, with no block count up front.
25//! * **The file nonce** is in every block's associated data. It is a keyed MAC
26//!   of the WHOLE plaintext, under a key derived from the epoch key
27//!   (HKDF-SHA256, info [`NONCE_INFO`]). Without it, two files (or two
28//!   versions of one file) that share an aligned 1 MiB block in the same epoch
29//!   would show the same ciphertext block, and an observer would see which
30//!   megabytes of a large file changed between commits. With it, any
31//!   difference anywhere in a file changes every block. What is left is what
32//!   v1 already showed: two EQUAL files in one epoch are equal envelopes. It
33//!   costs one more read of the plaintext before the first block can be
34//!   written; the git filter spools its input anyway (git's protocol hands the
35//!   whole file over before it reads an answer), and MACs it while spooling.
36//! * **Deterministic**, like v1: the same content under the same key gives the
37//!   same bytes, so git's clean filter is stable and the blob id is a KEYED id.
38//! * **Overhead**: 49 bytes per file plus 16 bytes per started MiB.
39//!
40//! **v1 — whole-file, still read:** `MAGIC | 01 | EPOCH (4) | SIV tag (16) |
41//! ciphertext`, AES-256-SIV over the whole content with the 13 header bytes
42//! as associated data. AES-SIV is two-pass, so a v1 envelope is opened in
43//! memory. An unchanged file sealed v1 is never re-sealed (the clean filter
44//! keeps the blob it already is), so no repository-wide diff appears.
45//!
46//! `MAGIC` starts with a NUL byte, so git treats every envelope as binary and
47//! never tries a text diff or a text merge on ciphertext.
48//!
49//! The format checks ([`is_envelope`], [`plaintext_len`], [`epoch_of`]) need
50//! no key and no cipher; a server uses them. Sealing and opening need the
51//! `cipher` feature.
52
53/// The first eight bytes of every envelope.
54pub const MAGIC: [u8; 8] = *b"\0GNCRYPT";
55/// The format version this code writes.
56pub const VERSION: u8 = V2;
57/// The whole-file format (read only).
58pub const V1: u8 = 1;
59/// The chunked format.
60pub const V2: u8 = 2;
61/// v1's header: `MAGIC + VERSION + EPOCH`.
62pub const V1_HEADER_LEN: usize = 13;
63/// v2's header: `MAGIC + VERSION + EPOCH + CHUNK SIZE + FILE NONCE`.
64pub const V2_HEADER_LEN: usize = 49;
65/// The bytes of a v2 header that come before the file nonce.
66const V2_FIXED_LEN: usize = 17;
67/// The file nonce.
68pub const NONCE_LEN: usize = 32;
69/// One SIV tag (per file in v1, per block in v2).
70pub const TAG_LEN: usize = 16;
71/// The smallest valid envelope: v1 of empty content.
72pub const MIN_LEN: usize = V1_HEADER_LEN + TAG_LEN;
73/// The plaintext bytes in one v2 block (the last block may be shorter).
74pub const CHUNK: u32 = 1 << 20;
75/// The largest chunk size a reader accepts: what it holds in memory at once.
76pub const MAX_CHUNK: u32 = 16 << 20;
77/// HKDF-SHA256 `info` for the key the file nonce is a MAC under.
78pub const NONCE_INFO: &[u8] = b"gunnar-crypt envelope v2 file nonce";
79/// Associated data of one v2 block: header ‖ index (8) ‖ last (1).
80#[cfg(feature = "cipher")]
81const AD_LEN: usize = V2_HEADER_LEN + 8 + 1;
82
83/// Does `bytes` carry the envelope magic at all (any version)?
84pub fn has_magic(bytes: &[u8]) -> bool {
85    bytes.len() >= MAGIC.len() && bytes[..MAGIC.len()] == MAGIC
86}
87
88/// The plaintext length of an envelope that is `total` bytes long, read from
89/// its first bytes (`head`: at least 17 for v2, 9 for v1) and its length
90/// alone. `None` when it is not a well-formed envelope of a version this code
91/// knows: no magic, an unknown version, an impossible chunk size, or a length
92/// that no sequence of blocks has (cut inside a tag, or an empty last block
93/// after full ones, which no sealer writes).
94pub fn plaintext_len(head: &[u8], total: u64) -> Option<u64> {
95    if !has_magic(head) || head.len() <= MAGIC.len() {
96        return None;
97    }
98    match head[MAGIC.len()] {
99        V1 => total.checked_sub(MIN_LEN as u64),
100        V2 => {
101            if head.len() < V2_FIXED_LEN {
102                return None;
103            }
104            let chunk = u64::from(chunk_of(head));
105            if chunk == 0 || chunk > u64::from(MAX_CHUNK) {
106                return None;
107            }
108            let body = total.checked_sub(V2_HEADER_LEN as u64)?;
109            let block = chunk + TAG_LEN as u64;
110            let (full, rest) = (body / block, body % block);
111            match rest {
112                0 if full > 0 => Some(full * chunk),
113                r if r < TAG_LEN as u64 => None,
114                r if r == TAG_LEN as u64 && full > 0 => None,
115                r => Some(full * chunk + r - TAG_LEN as u64),
116            }
117        }
118        _ => None,
119    }
120}
121
122/// Is `bytes` a well-formed envelope of a version this code knows (v1, v2)?
123///
124/// A format check only: it cannot tell a real envelope from bytes that merely
125/// look like one. That is enough for a server, whose question is "did the
126/// client run the filter", not "is this authentic" (only a key holder can
127/// answer that, by opening it).
128pub fn is_envelope(bytes: &[u8]) -> bool {
129    plaintext_len(bytes, bytes.len() as u64).is_some()
130}
131
132/// The key epoch an envelope was sealed under, if `bytes` is an envelope.
133pub fn epoch_of(bytes: &[u8]) -> Option<u32> {
134    if !is_envelope(bytes) {
135        return None;
136    }
137    let e = &bytes[MAGIC.len() + 1..V1_HEADER_LEN];
138    Some(u32::from_be_bytes([e[0], e[1], e[2], e[3]]))
139}
140
141/// The format version of an envelope's first bytes (`None` without magic).
142pub fn version_of(head: &[u8]) -> Option<u8> {
143    (has_magic(head) && head.len() > MAGIC.len()).then(|| head[MAGIC.len()])
144}
145
146fn chunk_of(head: &[u8]) -> u32 {
147    u32::from_be_bytes([head[13], head[14], head[15], head[16]])
148}
149
150/// Why an envelope did not open.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum OpenError {
153    /// The bytes are not an envelope (no magic, or too short).
154    NotEnvelope,
155    /// An envelope of a format version this code does not know.
156    UnsupportedVersion(u8),
157    /// Sealed under an epoch the key ring does not hold: either a newer
158    /// rotation this clone has not unlocked yet, or a ring the reader was
159    /// never given.
160    UnknownEpoch(u32),
161    /// A tag did not verify: wrong key, or the bytes were changed, cut,
162    /// reordered or added to.
163    Corrupt,
164}
165
166impl std::fmt::Display for OpenError {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        match self {
169            OpenError::NotEnvelope => write!(f, "not a gunnar-crypt envelope"),
170            OpenError::UnsupportedVersion(v) => {
171                write!(
172                    f,
173                    "gunnar-crypt envelope version {v} is newer than this gunnar"
174                )
175            }
176            OpenError::UnknownEpoch(e) => write!(
177                f,
178                "sealed under key epoch {e}, which this key ring does not hold \
179                 (run `gunnar crypt unlock` after pulling the newest .gunnar-crypt/keys)"
180            ),
181            OpenError::Corrupt => write!(
182                f,
183                "the envelope did not verify: wrong key, or the content was altered"
184            ),
185        }
186    }
187}
188
189impl std::error::Error for OpenError {}
190
191/// Why a streamed open stopped: the envelope, or the reader/writer.
192#[derive(Debug)]
193pub enum StreamError {
194    /// The envelope did not open. Blocks before the failing one may already
195    /// have been written: a caller that must not show unverified output keeps
196    /// it until this returns `Ok`, or reports the failure after it (git's
197    /// filter protocol has `status=error` after content for exactly this).
198    Open(OpenError),
199    /// Reading the envelope or writing the plaintext failed.
200    Io(std::io::Error),
201}
202
203impl std::fmt::Display for StreamError {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        match self {
206            StreamError::Open(e) => e.fmt(f),
207            StreamError::Io(e) => e.fmt(f),
208        }
209    }
210}
211
212impl std::error::Error for StreamError {}
213
214impl From<std::io::Error> for StreamError {
215    fn from(e: std::io::Error) -> Self {
216        StreamError::Io(e)
217    }
218}
219
220impl From<OpenError> for StreamError {
221    fn from(e: OpenError) -> Self {
222        StreamError::Open(e)
223    }
224}
225
226/// Read until `buf` is full or the reader ends; the number of bytes read.
227#[cfg(feature = "cipher")]
228fn read_full(r: &mut impl std::io::Read, buf: &mut [u8]) -> std::io::Result<usize> {
229    let mut n = 0;
230    while n < buf.len() {
231        match r.read(&mut buf[n..]) {
232            Ok(0) => break,
233            Ok(k) => n += k,
234            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
235            Err(e) => return Err(e),
236        }
237    }
238    Ok(n)
239}
240
241#[cfg(feature = "cipher")]
242mod cipher {
243    use std::io::{Read, Write};
244
245    use super::*;
246    use crate::keyring::{DataKey, Keyring};
247    use aes_siv::aead::KeyInit;
248    use aes_siv::siv::Aes256Siv;
249    use aes_siv::Tag;
250    use hmac::{Hmac, Mac};
251    use sha2::Sha256;
252    use zeroize::Zeroizing;
253
254    fn siv(key: &DataKey) -> Aes256Siv {
255        Aes256Siv::new(key.bytes().into())
256    }
257
258    /// The v2 file nonce, computed as the plaintext goes by (one pass, any
259    /// split). Feed it exactly the bytes that are then sealed.
260    #[derive(Clone)]
261    pub struct FileNonce(Hmac<Sha256>);
262
263    impl FileNonce {
264        /// A nonce under `key`'s epoch.
265        pub fn new(key: &DataKey) -> FileNonce {
266            let mut nonce_key = Zeroizing::new([0u8; 32]);
267            hkdf::Hkdf::<Sha256>::new(None, key.bytes())
268                .expand(NONCE_INFO, &mut nonce_key[..])
269                .expect("32 bytes is a valid HKDF-SHA256 length");
270            FileNonce(
271                <Hmac<Sha256> as hmac::KeyInit>::new_from_slice(&nonce_key[..])
272                    .expect("HMAC takes a key of any length"),
273            )
274        }
275
276        /// More plaintext.
277        pub fn update(&mut self, bytes: &[u8]) {
278            self.0.update(bytes);
279        }
280
281        /// The nonce.
282        pub fn finish(self) -> [u8; NONCE_LEN] {
283            self.0.finalize().into_bytes().into()
284        }
285    }
286
287    /// The file nonce of `plaintext` under `key`.
288    pub fn file_nonce(key: &DataKey, plaintext: &[u8]) -> [u8; NONCE_LEN] {
289        let mut n = FileNonce::new(key);
290        n.update(plaintext);
291        n.finish()
292    }
293
294    fn header_v2(epoch: u32, chunk: u32, nonce: &[u8; NONCE_LEN]) -> [u8; V2_HEADER_LEN] {
295        let mut h = [0u8; V2_HEADER_LEN];
296        h[..8].copy_from_slice(&MAGIC);
297        h[8] = V2;
298        h[9..13].copy_from_slice(&epoch.to_be_bytes());
299        h[13..17].copy_from_slice(&chunk.to_be_bytes());
300        h[17..].copy_from_slice(nonce);
301        h
302    }
303
304    fn block_ad(header: &[u8; V2_HEADER_LEN], index: u64, last: bool) -> [u8; AD_LEN] {
305        let mut ad = [0u8; AD_LEN];
306        ad[..V2_HEADER_LEN].copy_from_slice(header);
307        ad[V2_HEADER_LEN..V2_HEADER_LEN + 8].copy_from_slice(&index.to_be_bytes());
308        ad[AD_LEN - 1] = u8::from(last);
309        ad
310    }
311
312    /// The first allocation for a block buffer when the input's length is
313    /// not known: a small file (or a short envelope that declares a large
314    /// chunk) never costs a whole chunk.
315    const FIRST_FILL: usize = 64 << 10;
316
317    /// A block buffer: `size` bytes now, zeroed on drop (it holds plaintext).
318    fn block_buf(size: usize) -> Zeroizing<Vec<u8>> {
319        Zeroizing::new(vec![0u8; size])
320    }
321
322    /// Read into `buf[at..]` until it holds `want` bytes or the reader ends,
323    /// growing `buf` (doubling, never past `want`; each old buffer is zeroed
324    /// as it is dropped) only while the input proves to be that long. The
325    /// bytes now in `buf`.
326    fn fill(
327        r: &mut impl Read,
328        buf: &mut Zeroizing<Vec<u8>>,
329        mut at: usize,
330        want: usize,
331    ) -> std::io::Result<usize> {
332        loop {
333            let end = buf.len().min(want);
334            at += read_full(r, &mut buf[at..end])?;
335            if at < end || end == want {
336                return Ok(at);
337            }
338            let mut bigger = block_buf((buf.len() * 2).min(want));
339            bigger[..at].copy_from_slice(&buf[..at]);
340            *buf = bigger;
341        }
342    }
343
344    /// **Seal a stream** under `key` (v2, [`CHUNK`]-byte blocks), writing the
345    /// envelope to `out` as it reads. `nonce` is the [`FileNonce`] of the very
346    /// bytes `plaintext` yields: compute it on a first pass (the git filter
347    /// does while it spools). Holds one chunk in memory. Returns the bytes
348    /// written.
349    pub fn seal_stream(
350        key: &DataKey,
351        nonce: &[u8; NONCE_LEN],
352        plaintext: &mut impl Read,
353        out: &mut impl Write,
354    ) -> std::io::Result<u64> {
355        seal_stream_chunked(key, CHUNK, nonce, plaintext, out)
356    }
357
358    fn seal_stream_chunked(
359        key: &DataKey,
360        chunk: u32,
361        nonce: &[u8; NONCE_LEN],
362        plaintext: &mut impl Read,
363        out: &mut impl Write,
364    ) -> std::io::Result<u64> {
365        assert!(
366            chunk > 0 && chunk <= MAX_CHUNK,
367            "a chunk size readers accept"
368        );
369        let header = header_v2(key.epoch(), chunk, nonce);
370        out.write_all(&header)?;
371        let mut written = V2_HEADER_LEN as u64;
372        let mut siv = siv(key);
373        let chunk = chunk as usize;
374        // [tag | chunk]: as long as the input proves to be, up to one chunk.
375        let want = TAG_LEN + chunk;
376        let mut buf = block_buf(want.min(TAG_LEN + FIRST_FILL));
377        let mut n = fill(plaintext, &mut buf, TAG_LEN, want)? - TAG_LEN;
378        let mut index: u64 = 0;
379        loop {
380            // A full chunk is the last one only if nothing follows it.
381            let mut next = [0u8; 1];
382            let last = n < chunk || read_full(plaintext, &mut next)? == 0;
383            let ad = block_ad(&header, index, last);
384            let tag = siv
385                .encrypt_inout_detached([&ad[..]], (&mut buf[TAG_LEN..TAG_LEN + n]).into())
386                .expect("AES-SIV takes any length and one header");
387            buf[..TAG_LEN].copy_from_slice(tag.as_slice());
388            out.write_all(&buf[..TAG_LEN + n])?;
389            written += (TAG_LEN + n) as u64;
390            if last {
391                return Ok(written);
392            }
393            index += 1;
394            buf[TAG_LEN] = next[0];
395            n = fill(plaintext, &mut buf, TAG_LEN + 1, want)? - TAG_LEN;
396        }
397    }
398
399    /// Seal `plaintext` under `key` (v2). Deterministic: same key, same
400    /// content, same bytes out.
401    pub fn seal(key: &DataKey, plaintext: &[u8]) -> Vec<u8> {
402        seal_with_chunk(key, CHUNK, plaintext)
403    }
404
405    /// [`seal`] with another chunk size: for tests that want many blocks
406    /// from a few bytes. gunnar itself always seals with [`CHUNK`].
407    #[doc(hidden)]
408    pub fn seal_with_chunk(key: &DataKey, chunk: u32, plaintext: &[u8]) -> Vec<u8> {
409        let nonce = file_nonce(key, plaintext);
410        let mut out = Vec::new();
411        seal_into_chunked(key, chunk, &nonce, plaintext, &mut out);
412        out
413    }
414
415    /// Seal `plaintext` (v2) under `key`, whose [`FileNonce`] is `nonce`,
416    /// APPENDED to `out`: each block is encrypted in place in `out`, so no
417    /// other buffer holds the file. What the filter does with a file small
418    /// enough to stay in memory.
419    pub fn seal_into(key: &DataKey, nonce: &[u8; NONCE_LEN], plaintext: &[u8], out: &mut Vec<u8>) {
420        seal_into_chunked(key, CHUNK, nonce, plaintext, out)
421    }
422
423    fn seal_into_chunked(
424        key: &DataKey,
425        chunk: u32,
426        nonce: &[u8; NONCE_LEN],
427        plaintext: &[u8],
428        out: &mut Vec<u8>,
429    ) {
430        assert!(
431            chunk > 0 && chunk <= MAX_CHUNK,
432            "a chunk size readers accept"
433        );
434        let header = header_v2(key.epoch(), chunk, nonce);
435        let chunk = chunk as usize;
436        let blocks = plaintext.len().div_ceil(chunk).max(1);
437        // Reserved up front: `out` never moves (and leaves no copy of the
438        // plaintext behind) while blocks are encrypted in it.
439        out.reserve(V2_HEADER_LEN + blocks * TAG_LEN + plaintext.len());
440        out.extend_from_slice(&header);
441        let mut siv = siv(key);
442        for i in 0..blocks {
443            let piece = &plaintext
444                [(i * chunk).min(plaintext.len())..((i + 1) * chunk).min(plaintext.len())];
445            let at = out.len();
446            out.extend_from_slice(&[0u8; TAG_LEN]);
447            out.extend_from_slice(piece);
448            let ad = block_ad(&header, i as u64, i + 1 == blocks);
449            let tag = siv
450                .encrypt_inout_detached([&ad[..]], (&mut out[at + TAG_LEN..]).into())
451                .expect("AES-SIV takes any length and one header");
452            out[at..at + TAG_LEN].copy_from_slice(tag.as_slice());
453        }
454    }
455
456    /// Seal `plaintext` as a v1 envelope (whole file). Nothing in gunnar
457    /// seals v1 any more; this stays for the v1 test vectors and for tests
458    /// of the v1 → v2 transition.
459    #[doc(hidden)]
460    pub fn seal_v1(key: &DataKey, plaintext: &[u8]) -> Vec<u8> {
461        let mut h = [0u8; V1_HEADER_LEN];
462        h[..8].copy_from_slice(&MAGIC);
463        h[8] = V1;
464        h[9..].copy_from_slice(&key.epoch().to_be_bytes());
465        let ct = siv(key)
466            .encrypt([&h[..]], plaintext)
467            .expect("AES-SIV takes any length and one header");
468        let mut out = Vec::with_capacity(V1_HEADER_LEN + ct.len());
469        out.extend_from_slice(&h);
470        out.extend_from_slice(&ct);
471        out
472    }
473
474    /// **Open a stream**: read an envelope (v1 or v2) from `envelope` and
475    /// write its plaintext to `out`. A v2 envelope is opened one block at a
476    /// time (one chunk in memory); each block is verified before it is
477    /// written, and a cut, reordered or extended envelope fails at the block
478    /// where it shows — after the blocks before it were written (see
479    /// [`StreamError::Open`]). A v1 envelope is read whole (it was sealed
480    /// whole). Returns the plaintext bytes written.
481    pub fn open_stream(
482        ring: &Keyring,
483        envelope: &mut impl Read,
484        out: &mut impl Write,
485    ) -> Result<u64, StreamError> {
486        let mut head = [0u8; V2_HEADER_LEN];
487        let got = read_full(envelope, &mut head[..MAGIC.len() + 1])?;
488        if got < MAGIC.len() + 1 || !has_magic(&head) {
489            return Err(OpenError::NotEnvelope.into());
490        }
491        match head[MAGIC.len()] {
492            V1 => {
493                let mut all = head[..MAGIC.len() + 1].to_vec();
494                envelope.read_to_end(&mut all)?;
495                let plain = Zeroizing::new(open_v1(ring, &all)?);
496                out.write_all(&plain)?;
497                Ok(plain.len() as u64)
498            }
499            V2 => {
500                let got = read_full(envelope, &mut head[MAGIC.len() + 1..])?;
501                if got < V2_HEADER_LEN - MAGIC.len() - 1 {
502                    return Err(OpenError::NotEnvelope.into());
503                }
504                open_v2_blocks(ring, &head, envelope, out)
505            }
506            v => Err(OpenError::UnsupportedVersion(v).into()),
507        }
508    }
509
510    /// The epoch's key for a header, or why there is none.
511    fn key_for<'r>(ring: &'r Keyring, head: &[u8]) -> Result<&'r DataKey, OpenError> {
512        let epoch = u32::from_be_bytes([head[9], head[10], head[11], head[12]]);
513        ring.get(epoch).ok_or(OpenError::UnknownEpoch(epoch))
514    }
515
516    fn open_v2_blocks(
517        ring: &Keyring,
518        header: &[u8; V2_HEADER_LEN],
519        envelope: &mut impl Read,
520        out: &mut impl Write,
521    ) -> Result<u64, StreamError> {
522        let chunk = chunk_of(header);
523        if chunk == 0 || chunk > MAX_CHUNK {
524            return Err(OpenError::Corrupt.into());
525        }
526        let key = key_for(ring, header)?;
527        let mut siv = siv(key);
528        let block = TAG_LEN + chunk as usize;
529        // One block at most, and only as much as the envelope proves to have.
530        let mut buf = block_buf(block.min(FIRST_FILL));
531        let mut n = fill(envelope, &mut buf, 0, block)?;
532        let mut index: u64 = 0;
533        let mut written: u64 = 0;
534        loop {
535            if n < TAG_LEN {
536                // No block at all, or one cut inside its tag.
537                return Err(OpenError::Corrupt.into());
538            }
539            let mut next = [0u8; 1];
540            let last = n < block || read_full(envelope, &mut next)? == 0;
541            if last && n == TAG_LEN && index > 0 {
542                // An empty last block after full ones: never sealed so.
543                return Err(OpenError::Corrupt.into());
544            }
545            let tag = Tag::try_from(&buf[..TAG_LEN]).expect("16 bytes");
546            let ad = block_ad(header, index, last);
547            siv.decrypt_inout_detached([&ad[..]], (&mut buf[TAG_LEN..n]).into(), &tag)
548                .map_err(|_| OpenError::Corrupt)?;
549            out.write_all(&buf[TAG_LEN..n])?;
550            written += (n - TAG_LEN) as u64;
551            if last {
552                return Ok(written);
553            }
554            index = index.checked_add(1).ok_or(OpenError::Corrupt)?;
555            buf[0] = next[0];
556            n = fill(envelope, &mut buf, 1, block)?;
557        }
558    }
559
560    fn open_v1(ring: &Keyring, envelope: &[u8]) -> Result<Vec<u8>, OpenError> {
561        if envelope.len() < MIN_LEN {
562            return Err(OpenError::NotEnvelope);
563        }
564        let key = key_for(ring, envelope)?;
565        siv(key)
566            .decrypt([&envelope[..V1_HEADER_LEN]], &envelope[V1_HEADER_LEN..])
567            .map_err(|_| OpenError::Corrupt)
568    }
569
570    /// Open an envelope (v1 or v2) in memory with whichever epoch key it
571    /// names.
572    pub fn open(ring: &Keyring, envelope: &[u8]) -> Result<Vec<u8>, OpenError> {
573        let mut out = Vec::new();
574        open_into(ring, envelope, &mut out)?;
575        Ok(out)
576    }
577
578    /// [`open`], APPENDING the plaintext to `out`; each block is decrypted in
579    /// place in `out`, which grows by the plaintext's length at most once.
580    /// On an error nothing is appended (what a failing block left is zeroed).
581    pub fn open_into(ring: &Keyring, envelope: &[u8], out: &mut Vec<u8>) -> Result<(), OpenError> {
582        if !has_magic(envelope) || envelope.len() <= MAGIC.len() {
583            return Err(OpenError::NotEnvelope);
584        }
585        match envelope[MAGIC.len()] {
586            V1 => {
587                let plain = Zeroizing::new(open_v1(ring, envelope)?);
588                out.extend_from_slice(&plain);
589                Ok(())
590            }
591            V2 => {
592                if envelope.len() < V2_HEADER_LEN {
593                    return Err(OpenError::NotEnvelope);
594                }
595                let header: &[u8; V2_HEADER_LEN] =
596                    envelope[..V2_HEADER_LEN].try_into().expect("checked above");
597                // The same order of checks as the streamed opener, so both
598                // name the same error: the chunk size, the epoch, the shape.
599                let chunk = chunk_of(header);
600                if chunk == 0 || chunk > MAX_CHUNK {
601                    return Err(OpenError::Corrupt);
602                }
603                let key = key_for(ring, header)?;
604                let len =
605                    plaintext_len(envelope, envelope.len() as u64).ok_or(OpenError::Corrupt)?;
606                let body = &envelope[V2_HEADER_LEN..];
607                let block = TAG_LEN + chunk as usize;
608                let blocks = body.len().div_ceil(block);
609                let start = out.len();
610                out.reserve(len as usize);
611                let mut siv = siv(key);
612                for i in 0..blocks {
613                    let b = &body[i * block..((i + 1) * block).min(body.len())];
614                    let tag = Tag::try_from(&b[..TAG_LEN]).expect("16 bytes");
615                    let at = out.len();
616                    out.extend_from_slice(&b[TAG_LEN..]);
617                    let ad = block_ad(header, i as u64, i + 1 == blocks);
618                    if siv
619                        .decrypt_inout_detached([&ad[..]], (&mut out[at..]).into(), &tag)
620                        .is_err()
621                    {
622                        zeroize::Zeroize::zeroize(&mut out[start..]);
623                        out.truncate(start);
624                        return Err(OpenError::Corrupt);
625                    }
626                }
627                Ok(())
628            }
629            v => Err(OpenError::UnsupportedVersion(v)),
630        }
631    }
632
633    /// Would sealing `plaintext_len` bytes under `ring`'s newest key give
634    /// exactly the envelope whose header is `head`? True only for a v2
635    /// envelope under the newest epoch with gunnar's chunk size: then a
636    /// re-seal of unchanged content is the same bytes (determinism), and the
637    /// clean filter need not open the old one to keep it.
638    pub fn is_current(ring: &Keyring, head: &[u8]) -> bool {
639        head.len() >= V2_FIXED_LEN
640            && has_magic(head)
641            && head[MAGIC.len()] == V2
642            && chunk_of(head) == CHUNK
643            && u32::from_be_bytes([head[9], head[10], head[11], head[12]]) == ring.newest().epoch()
644    }
645}
646
647#[cfg(feature = "cipher")]
648pub use cipher::{
649    file_nonce, is_current, open, open_into, open_stream, seal, seal_into, seal_stream, seal_v1,
650    seal_with_chunk, FileNonce,
651};