crates/gunnar-crypt/src/envelope.rs
//! One encrypted file (blob). Two versions; this code writes v2 and reads both.
//!
//! **v2 — chunked, the version every new seal writes:**
//!
//! ```text
//! offset 0 8 bytes magic 00 47 4E 43 52 59 50 54 ("\0GNCRYPT")
//! 8 1 byte version 02
//! 9 4 bytes epoch big-endian
//! 13 4 bytes chunk size big-endian (1 MiB = 00 10 00 00)
//! 17 32 bytes file nonce HMAC-SHA256(nonce key, the whole plaintext)
//! 49 … blocks 16-byte SIV tag ‖ ciphertext, one per chunk
//! ```
//!
//! * The plaintext is cut into **fixed chunks** of [`CHUNK`] bytes; the last
//! one is shorter, and it is empty only when the whole file is (then it is
//! the one block). Each block is sealed alone with **AES-256-SIV** (RFC 5297)
//! under the epoch key, with associated data
//! `header (49 bytes) ‖ block index (u64, big-endian) ‖ last (1 byte: 1 on
//! the last block, else 0)`. That is the STREAM construction (Hoang,
//! Reyhanitabar, Rogaway, Vizár, "Online Authenticated-Encryption and its
//! Nonce-Reuse Misuse-Resistance", CRYPTO 2015) with a deterministic AEAD:
//! blocks cannot be reordered (the index), a dropped tail or an appended
//! block does not open (the last flag), and a filter can seal and open while
//! it reads, holding one chunk, with no block count up front.
//! * **The file nonce** is in every block's associated data. It is a keyed MAC
//! of the WHOLE plaintext, under a key derived from the epoch key
//! (HKDF-SHA256, info [`NONCE_INFO`]). Without it, two files (or two
//! versions of one file) that share an aligned 1 MiB block in the same epoch
//! would show the same ciphertext block, and an observer would see which
//! megabytes of a large file changed between commits. With it, any
//! difference anywhere in a file changes every block. What is left is what
//! v1 already showed: two EQUAL files in one epoch are equal envelopes. It
//! costs one more read of the plaintext before the first block can be
//! written; the git filter spools its input anyway (git's protocol hands the
//! whole file over before it reads an answer), and MACs it while spooling.
//! * **Deterministic**, like v1: the same content under the same key gives the
//! same bytes, so git's clean filter is stable and the blob id is a KEYED id.
//! * **Overhead**: 49 bytes per file plus 16 bytes per started MiB.
//!
//! **v1 — whole-file, still read:** `MAGIC | 01 | EPOCH (4) | SIV tag (16) |
//! ciphertext`, AES-256-SIV over the whole content with the 13 header bytes
//! as associated data. AES-SIV is two-pass, so a v1 envelope is opened in
//! memory. An unchanged file sealed v1 is never re-sealed (the clean filter
//! keeps the blob it already is), so no repository-wide diff appears.
//!
//! `MAGIC` starts with a NUL byte, so git treats every envelope as binary and
//! never tries a text diff or a text merge on ciphertext.
//!
//! The format checks ([`is_envelope`], [`plaintext_len`], [`epoch_of`]) need
//! no key and no cipher; a server uses them. Sealing and opening need the
//! `cipher` feature.
/// The first eight bytes of every envelope.
pub const MAGIC: [u8; 8] = *b"\0GNCRYPT";
/// The format version this code writes.
pub const VERSION: u8 = V2;
/// The whole-file format (read only).
pub const V1: u8 = 1;
/// The chunked format.
pub const V2: u8 = 2;
/// v1's header: `MAGIC + VERSION + EPOCH`.
pub const V1_HEADER_LEN: usize = 13;
/// v2's header: `MAGIC + VERSION + EPOCH + CHUNK SIZE + FILE NONCE`.
pub const V2_HEADER_LEN: usize = 49;
/// The bytes of a v2 header that come before the file nonce.
const V2_FIXED_LEN: usize = 17;
/// The file nonce.
pub const NONCE_LEN: usize = 32;
/// One SIV tag (per file in v1, per block in v2).
pub const TAG_LEN: usize = 16;
/// The smallest valid envelope: v1 of empty content.
pub const MIN_LEN: usize = V1_HEADER_LEN + TAG_LEN;
/// The plaintext bytes in one v2 block (the last block may be shorter).
pub const CHUNK: u32 = 1 << 20;
/// The largest chunk size a reader accepts: what it holds in memory at once.
pub const MAX_CHUNK: u32 = 16 << 20;
/// HKDF-SHA256 `info` for the key the file nonce is a MAC under.
pub const NONCE_INFO: &[u8] = b"gunnar-crypt envelope v2 file nonce";
/// Associated data of one v2 block: header ‖ index (8) ‖ last (1).
#[cfg(feature = "cipher")]
const AD_LEN: usize = V2_HEADER_LEN + 8 + 1;
/// Does `bytes` carry the envelope magic at all (any version)?
pub fn has_magic(bytes: &[u8]) -> bool {
bytes.len() >= MAGIC.len() && bytes[..MAGIC.len()] == MAGIC
}
/// The plaintext length of an envelope that is `total` bytes long, read from
/// its first bytes (`head`: at least 17 for v2, 9 for v1) and its length
/// alone. `None` when it is not a well-formed envelope of a version this code
/// knows: no magic, an unknown version, an impossible chunk size, or a length
/// that no sequence of blocks has (cut inside a tag, or an empty last block
/// after full ones, which no sealer writes).
pub fn plaintext_len(head: &[u8], total: u64) -> Option<u64> {
if !has_magic(head) || head.len() <= MAGIC.len() {
return None;
}
match head[MAGIC.len()] {
V1 => total.checked_sub(MIN_LEN as u64),
V2 => {
if head.len() < V2_FIXED_LEN {
return None;
}
let chunk = u64::from(chunk_of(head));
if chunk == 0 || chunk > u64::from(MAX_CHUNK) {
return None;
}
let body = total.checked_sub(V2_HEADER_LEN as u64)?;
let block = chunk + TAG_LEN as u64;
let (full, rest) = (body / block, body % block);
match rest {
0 if full > 0 => Some(full * chunk),
r if r < TAG_LEN as u64 => None,
r if r == TAG_LEN as u64 && full > 0 => None,
r => Some(full * chunk + r - TAG_LEN as u64),
}
}
_ => None,
}
}
/// Is `bytes` a well-formed envelope of a version this code knows (v1, v2)?
///
/// A format check only: it cannot tell a real envelope from bytes that merely
/// look like one. That is enough for a server, whose question is "did the
/// client run the filter", not "is this authentic" (only a key holder can
/// answer that, by opening it).
pub fn is_envelope(bytes: &[u8]) -> bool {
plaintext_len(bytes, bytes.len() as u64).is_some()
}
/// The key epoch an envelope was sealed under, if `bytes` is an envelope.
pub fn epoch_of(bytes: &[u8]) -> Option<u32> {
if !is_envelope(bytes) {
return None;
}
let e = &bytes[MAGIC.len() + 1..V1_HEADER_LEN];
Some(u32::from_be_bytes([e[0], e[1], e[2], e[3]]))
}
/// The format version of an envelope's first bytes (`None` without magic).
pub fn version_of(head: &[u8]) -> Option<u8> {
(has_magic(head) && head.len() > MAGIC.len()).then(|| head[MAGIC.len()])
}
fn chunk_of(head: &[u8]) -> u32 {
u32::from_be_bytes([head[13], head[14], head[15], head[16]])
}
/// Why an envelope did not open.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OpenError {
/// The bytes are not an envelope (no magic, or too short).
NotEnvelope,
/// An envelope of a format version this code does not know.
UnsupportedVersion(u8),
/// Sealed under an epoch the key ring does not hold: either a newer
/// rotation this clone has not unlocked yet, or a ring the reader was
/// never given.
UnknownEpoch(u32),
/// A tag did not verify: wrong key, or the bytes were changed, cut,
/// reordered or added to.
Corrupt,
}
impl std::fmt::Display for OpenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OpenError::NotEnvelope => write!(f, "not a gunnar-crypt envelope"),
OpenError::UnsupportedVersion(v) => {
write!(
f,
"gunnar-crypt envelope version {v} is newer than this gunnar"
)
}
OpenError::UnknownEpoch(e) => write!(
f,
"sealed under key epoch {e}, which this key ring does not hold \
(run `gunnar crypt unlock` after pulling the newest .gunnar-crypt/keys)"
),
OpenError::Corrupt => write!(
f,
"the envelope did not verify: wrong key, or the content was altered"
),
}
}
}
impl std::error::Error for OpenError {}
/// Why a streamed open stopped: the envelope, or the reader/writer.
#[derive(Debug)]
pub enum StreamError {
/// The envelope did not open. Blocks before the failing one may already
/// have been written: a caller that must not show unverified output keeps
/// it until this returns `Ok`, or reports the failure after it (git's
/// filter protocol has `status=error` after content for exactly this).
Open(OpenError),
/// Reading the envelope or writing the plaintext failed.
Io(std::io::Error),
}
impl std::fmt::Display for StreamError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StreamError::Open(e) => e.fmt(f),
StreamError::Io(e) => e.fmt(f),
}
}
}
impl std::error::Error for StreamError {}
impl From<std::io::Error> for StreamError {
fn from(e: std::io::Error) -> Self {
StreamError::Io(e)
}
}
impl From<OpenError> for StreamError {
fn from(e: OpenError) -> Self {
StreamError::Open(e)
}
}
/// Read until `buf` is full or the reader ends; the number of bytes read.
#[cfg(feature = "cipher")]
fn read_full(r: &mut impl std::io::Read, buf: &mut [u8]) -> std::io::Result<usize> {
let mut n = 0;
while n < buf.len() {
match r.read(&mut buf[n..]) {
Ok(0) => break,
Ok(k) => n += k,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(n)
}
#[cfg(feature = "cipher")]
mod cipher {
use std::io::{Read, Write};
use super::*;
use crate::keyring::{DataKey, Keyring};
use aes_siv::aead::KeyInit;
use aes_siv::siv::Aes256Siv;
use aes_siv::Tag;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use zeroize::Zeroizing;
fn siv(key: &DataKey) -> Aes256Siv {
Aes256Siv::new(key.bytes().into())
}
/// The v2 file nonce, computed as the plaintext goes by (one pass, any
/// split). Feed it exactly the bytes that are then sealed.
#[derive(Clone)]
pub struct FileNonce(Hmac<Sha256>);
impl FileNonce {
/// A nonce under `key`'s epoch.
pub fn new(key: &DataKey) -> FileNonce {
let mut nonce_key = Zeroizing::new([0u8; 32]);
hkdf::Hkdf::<Sha256>::new(None, key.bytes())
.expand(NONCE_INFO, &mut nonce_key[..])
.expect("32 bytes is a valid HKDF-SHA256 length");
FileNonce(
<Hmac<Sha256> as hmac::KeyInit>::new_from_slice(&nonce_key[..])
.expect("HMAC takes a key of any length"),
)
}
/// More plaintext.
pub fn update(&mut self, bytes: &[u8]) {
self.0.update(bytes);
}
/// The nonce.
pub fn finish(self) -> [u8; NONCE_LEN] {
self.0.finalize().into_bytes().into()
}
}
/// The file nonce of `plaintext` under `key`.
pub fn file_nonce(key: &DataKey, plaintext: &[u8]) -> [u8; NONCE_LEN] {
let mut n = FileNonce::new(key);
n.update(plaintext);
n.finish()
}
fn header_v2(epoch: u32, chunk: u32, nonce: &[u8; NONCE_LEN]) -> [u8; V2_HEADER_LEN] {
let mut h = [0u8; V2_HEADER_LEN];
h[..8].copy_from_slice(&MAGIC);
h[8] = V2;
h[9..13].copy_from_slice(&epoch.to_be_bytes());
h[13..17].copy_from_slice(&chunk.to_be_bytes());
h[17..].copy_from_slice(nonce);
h
}
fn block_ad(header: &[u8; V2_HEADER_LEN], index: u64, last: bool) -> [u8; AD_LEN] {
let mut ad = [0u8; AD_LEN];
ad[..V2_HEADER_LEN].copy_from_slice(header);
ad[V2_HEADER_LEN..V2_HEADER_LEN + 8].copy_from_slice(&index.to_be_bytes());
ad[AD_LEN - 1] = u8::from(last);
ad
}
/// The first allocation for a block buffer when the input's length is
/// not known: a small file (or a short envelope that declares a large
/// chunk) never costs a whole chunk.
const FIRST_FILL: usize = 64 << 10;
/// A block buffer: `size` bytes now, zeroed on drop (it holds plaintext).
fn block_buf(size: usize) -> Zeroizing<Vec<u8>> {
Zeroizing::new(vec![0u8; size])
}
/// Read into `buf[at..]` until it holds `want` bytes or the reader ends,
/// growing `buf` (doubling, never past `want`; each old buffer is zeroed
/// as it is dropped) only while the input proves to be that long. The
/// bytes now in `buf`.
fn fill(
r: &mut impl Read,
buf: &mut Zeroizing<Vec<u8>>,
mut at: usize,
want: usize,
) -> std::io::Result<usize> {
loop {
let end = buf.len().min(want);
at += read_full(r, &mut buf[at..end])?;
if at < end || end == want {
return Ok(at);
}
let mut bigger = block_buf((buf.len() * 2).min(want));
bigger[..at].copy_from_slice(&buf[..at]);
*buf = bigger;
}
}
/// **Seal a stream** under `key` (v2, [`CHUNK`]-byte blocks), writing the
/// envelope to `out` as it reads. `nonce` is the [`FileNonce`] of the very
/// bytes `plaintext` yields: compute it on a first pass (the git filter
/// does while it spools). Holds one chunk in memory. Returns the bytes
/// written.
pub fn seal_stream(
key: &DataKey,
nonce: &[u8; NONCE_LEN],
plaintext: &mut impl Read,
out: &mut impl Write,
) -> std::io::Result<u64> {
seal_stream_chunked(key, CHUNK, nonce, plaintext, out)
}
fn seal_stream_chunked(
key: &DataKey,
chunk: u32,
nonce: &[u8; NONCE_LEN],
plaintext: &mut impl Read,
out: &mut impl Write,
) -> std::io::Result<u64> {
assert!(
chunk > 0 && chunk <= MAX_CHUNK,
"a chunk size readers accept"
);
let header = header_v2(key.epoch(), chunk, nonce);
out.write_all(&header)?;
let mut written = V2_HEADER_LEN as u64;
let mut siv = siv(key);
let chunk = chunk as usize;
// [tag | chunk]: as long as the input proves to be, up to one chunk.
let want = TAG_LEN + chunk;
let mut buf = block_buf(want.min(TAG_LEN + FIRST_FILL));
let mut n = fill(plaintext, &mut buf, TAG_LEN, want)? - TAG_LEN;
let mut index: u64 = 0;
loop {
// A full chunk is the last one only if nothing follows it.
let mut next = [0u8; 1];
let last = n < chunk || read_full(plaintext, &mut next)? == 0;
let ad = block_ad(&header, index, last);
let tag = siv
.encrypt_inout_detached([&ad[..]], (&mut buf[TAG_LEN..TAG_LEN + n]).into())
.expect("AES-SIV takes any length and one header");
buf[..TAG_LEN].copy_from_slice(tag.as_slice());
out.write_all(&buf[..TAG_LEN + n])?;
written += (TAG_LEN + n) as u64;
if last {
return Ok(written);
}
index += 1;
buf[TAG_LEN] = next[0];
n = fill(plaintext, &mut buf, TAG_LEN + 1, want)? - TAG_LEN;
}
}
/// Seal `plaintext` under `key` (v2). Deterministic: same key, same
/// content, same bytes out.
pub fn seal(key: &DataKey, plaintext: &[u8]) -> Vec<u8> {
seal_with_chunk(key, CHUNK, plaintext)
}
/// [`seal`] with another chunk size: for tests that want many blocks
/// from a few bytes. gunnar itself always seals with [`CHUNK`].
#[doc(hidden)]
pub fn seal_with_chunk(key: &DataKey, chunk: u32, plaintext: &[u8]) -> Vec<u8> {
let nonce = file_nonce(key, plaintext);
let mut out = Vec::new();
seal_into_chunked(key, chunk, &nonce, plaintext, &mut out);
out
}
/// Seal `plaintext` (v2) under `key`, whose [`FileNonce`] is `nonce`,
/// APPENDED to `out`: each block is encrypted in place in `out`, so no
/// other buffer holds the file. What the filter does with a file small
/// enough to stay in memory.
pub fn seal_into(key: &DataKey, nonce: &[u8; NONCE_LEN], plaintext: &[u8], out: &mut Vec<u8>) {
seal_into_chunked(key, CHUNK, nonce, plaintext, out)
}
fn seal_into_chunked(
key: &DataKey,
chunk: u32,
nonce: &[u8; NONCE_LEN],
plaintext: &[u8],
out: &mut Vec<u8>,
) {
assert!(
chunk > 0 && chunk <= MAX_CHUNK,
"a chunk size readers accept"
);
let header = header_v2(key.epoch(), chunk, nonce);
let chunk = chunk as usize;
let blocks = plaintext.len().div_ceil(chunk).max(1);
// Reserved up front: `out` never moves (and leaves no copy of the
// plaintext behind) while blocks are encrypted in it.
out.reserve(V2_HEADER_LEN + blocks * TAG_LEN + plaintext.len());
out.extend_from_slice(&header);
let mut siv = siv(key);
for i in 0..blocks {
let piece = &plaintext
[(i * chunk).min(plaintext.len())..((i + 1) * chunk).min(plaintext.len())];
let at = out.len();
out.extend_from_slice(&[0u8; TAG_LEN]);
out.extend_from_slice(piece);
let ad = block_ad(&header, i as u64, i + 1 == blocks);
let tag = siv
.encrypt_inout_detached([&ad[..]], (&mut out[at + TAG_LEN..]).into())
.expect("AES-SIV takes any length and one header");
out[at..at + TAG_LEN].copy_from_slice(tag.as_slice());
}
}
/// Seal `plaintext` as a v1 envelope (whole file). Nothing in gunnar
/// seals v1 any more; this stays for the v1 test vectors and for tests
/// of the v1 → v2 transition.
#[doc(hidden)]
pub fn seal_v1(key: &DataKey, plaintext: &[u8]) -> Vec<u8> {
let mut h = [0u8; V1_HEADER_LEN];
h[..8].copy_from_slice(&MAGIC);
h[8] = V1;
h[9..].copy_from_slice(&key.epoch().to_be_bytes());
let ct = siv(key)
.encrypt([&h[..]], plaintext)
.expect("AES-SIV takes any length and one header");
let mut out = Vec::with_capacity(V1_HEADER_LEN + ct.len());
out.extend_from_slice(&h);
out.extend_from_slice(&ct);
out
}
/// **Open a stream**: read an envelope (v1 or v2) from `envelope` and
/// write its plaintext to `out`. A v2 envelope is opened one block at a
/// time (one chunk in memory); each block is verified before it is
/// written, and a cut, reordered or extended envelope fails at the block
/// where it shows — after the blocks before it were written (see
/// [`StreamError::Open`]). A v1 envelope is read whole (it was sealed
/// whole). Returns the plaintext bytes written.
pub fn open_stream(
ring: &Keyring,
envelope: &mut impl Read,
out: &mut impl Write,
) -> Result<u64, StreamError> {
let mut head = [0u8; V2_HEADER_LEN];
let got = read_full(envelope, &mut head[..MAGIC.len() + 1])?;
if got < MAGIC.len() + 1 || !has_magic(&head) {
return Err(OpenError::NotEnvelope.into());
}
match head[MAGIC.len()] {
V1 => {
let mut all = head[..MAGIC.len() + 1].to_vec();
envelope.read_to_end(&mut all)?;
let plain = Zeroizing::new(open_v1(ring, &all)?);
out.write_all(&plain)?;
Ok(plain.len() as u64)
}
V2 => {
let got = read_full(envelope, &mut head[MAGIC.len() + 1..])?;
if got < V2_HEADER_LEN - MAGIC.len() - 1 {
return Err(OpenError::NotEnvelope.into());
}
open_v2_blocks(ring, &head, envelope, out)
}
v => Err(OpenError::UnsupportedVersion(v).into()),
}
}
/// The epoch's key for a header, or why there is none.
fn key_for<'r>(ring: &'r Keyring, head: &[u8]) -> Result<&'r DataKey, OpenError> {
let epoch = u32::from_be_bytes([head[9], head[10], head[11], head[12]]);
ring.get(epoch).ok_or(OpenError::UnknownEpoch(epoch))
}
fn open_v2_blocks(
ring: &Keyring,
header: &[u8; V2_HEADER_LEN],
envelope: &mut impl Read,
out: &mut impl Write,
) -> Result<u64, StreamError> {
let chunk = chunk_of(header);
if chunk == 0 || chunk > MAX_CHUNK {
return Err(OpenError::Corrupt.into());
}
let key = key_for(ring, header)?;
let mut siv = siv(key);
let block = TAG_LEN + chunk as usize;
// One block at most, and only as much as the envelope proves to have.
let mut buf = block_buf(block.min(FIRST_FILL));
let mut n = fill(envelope, &mut buf, 0, block)?;
let mut index: u64 = 0;
let mut written: u64 = 0;
loop {
if n < TAG_LEN {
// No block at all, or one cut inside its tag.
return Err(OpenError::Corrupt.into());
}
let mut next = [0u8; 1];
let last = n < block || read_full(envelope, &mut next)? == 0;
if last && n == TAG_LEN && index > 0 {
// An empty last block after full ones: never sealed so.
return Err(OpenError::Corrupt.into());
}
let tag = Tag::try_from(&buf[..TAG_LEN]).expect("16 bytes");
let ad = block_ad(header, index, last);
siv.decrypt_inout_detached([&ad[..]], (&mut buf[TAG_LEN..n]).into(), &tag)
.map_err(|_| OpenError::Corrupt)?;
out.write_all(&buf[TAG_LEN..n])?;
written += (n - TAG_LEN) as u64;
if last {
return Ok(written);
}
index = index.checked_add(1).ok_or(OpenError::Corrupt)?;
buf[0] = next[0];
n = fill(envelope, &mut buf, 1, block)?;
}
}
fn open_v1(ring: &Keyring, envelope: &[u8]) -> Result<Vec<u8>, OpenError> {
if envelope.len() < MIN_LEN {
return Err(OpenError::NotEnvelope);
}
let key = key_for(ring, envelope)?;
siv(key)
.decrypt([&envelope[..V1_HEADER_LEN]], &envelope[V1_HEADER_LEN..])
.map_err(|_| OpenError::Corrupt)
}
/// Open an envelope (v1 or v2) in memory with whichever epoch key it
/// names.
pub fn open(ring: &Keyring, envelope: &[u8]) -> Result<Vec<u8>, OpenError> {
let mut out = Vec::new();
open_into(ring, envelope, &mut out)?;
Ok(out)
}
/// [`open`], APPENDING the plaintext to `out`; each block is decrypted in
/// place in `out`, which grows by the plaintext's length at most once.
/// On an error nothing is appended (what a failing block left is zeroed).
pub fn open_into(ring: &Keyring, envelope: &[u8], out: &mut Vec<u8>) -> Result<(), OpenError> {
if !has_magic(envelope) || envelope.len() <= MAGIC.len() {
return Err(OpenError::NotEnvelope);
}
match envelope[MAGIC.len()] {
V1 => {
let plain = Zeroizing::new(open_v1(ring, envelope)?);
out.extend_from_slice(&plain);
Ok(())
}
V2 => {
if envelope.len() < V2_HEADER_LEN {
return Err(OpenError::NotEnvelope);
}
let header: &[u8; V2_HEADER_LEN] =
envelope[..V2_HEADER_LEN].try_into().expect("checked above");
// The same order of checks as the streamed opener, so both
// name the same error: the chunk size, the epoch, the shape.
let chunk = chunk_of(header);
if chunk == 0 || chunk > MAX_CHUNK {
return Err(OpenError::Corrupt);
}
let key = key_for(ring, header)?;
let len =
plaintext_len(envelope, envelope.len() as u64).ok_or(OpenError::Corrupt)?;
let body = &envelope[V2_HEADER_LEN..];
let block = TAG_LEN + chunk as usize;
let blocks = body.len().div_ceil(block);
let start = out.len();
out.reserve(len as usize);
let mut siv = siv(key);
for i in 0..blocks {
let b = &body[i * block..((i + 1) * block).min(body.len())];
let tag = Tag::try_from(&b[..TAG_LEN]).expect("16 bytes");
let at = out.len();
out.extend_from_slice(&b[TAG_LEN..]);
let ad = block_ad(header, i as u64, i + 1 == blocks);
if siv
.decrypt_inout_detached([&ad[..]], (&mut out[at..]).into(), &tag)
.is_err()
{
zeroize::Zeroize::zeroize(&mut out[start..]);
out.truncate(start);
return Err(OpenError::Corrupt);
}
}
Ok(())
}
v => Err(OpenError::UnsupportedVersion(v)),
}
}
/// Would sealing `plaintext_len` bytes under `ring`'s newest key give
/// exactly the envelope whose header is `head`? True only for a v2
/// envelope under the newest epoch with gunnar's chunk size: then a
/// re-seal of unchanged content is the same bytes (determinism), and the
/// clean filter need not open the old one to keep it.
pub fn is_current(ring: &Keyring, head: &[u8]) -> bool {
head.len() >= V2_FIXED_LEN
&& has_magic(head)
&& head[MAGIC.len()] == V2
&& chunk_of(head) == CHUNK
&& u32::from_be_bytes([head[9], head[10], head[11], head[12]]) == ring.newest().epoch()
}
}
#[cfg(feature = "cipher")]
pub use cipher::{
file_nonce, is_current, open, open_into, open_stream, seal, seal_into, seal_stream, seal_v1,
seal_with_chunk, FileNonce,
};