1pub const MAGIC: [u8; 8] = *b"\0GNCRYPT";
55pub const VERSION: u8 = V2;
57pub const V1: u8 = 1;
59pub const V2: u8 = 2;
61pub const V1_HEADER_LEN: usize = 13;
63pub const V2_HEADER_LEN: usize = 49;
65const V2_FIXED_LEN: usize = 17;
67pub const NONCE_LEN: usize = 32;
69pub const TAG_LEN: usize = 16;
71pub const MIN_LEN: usize = V1_HEADER_LEN + TAG_LEN;
73pub const CHUNK: u32 = 1 << 20;
75pub const MAX_CHUNK: u32 = 16 << 20;
77pub const NONCE_INFO: &[u8] = b"gunnar-crypt envelope v2 file nonce";
79#[cfg(feature = "cipher")]
81const AD_LEN: usize = V2_HEADER_LEN + 8 + 1;
82
83pub fn has_magic(bytes: &[u8]) -> bool {
85 bytes.len() >= MAGIC.len() && bytes[..MAGIC.len()] == MAGIC
86}
87
88pub 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
122pub fn is_envelope(bytes: &[u8]) -> bool {
129 plaintext_len(bytes, bytes.len() as u64).is_some()
130}
131
132pub 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
141pub 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#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum OpenError {
153 NotEnvelope,
155 UnsupportedVersion(u8),
157 UnknownEpoch(u32),
161 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#[derive(Debug)]
193pub enum StreamError {
194 Open(OpenError),
199 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#[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 #[derive(Clone)]
261 pub struct FileNonce(Hmac<Sha256>);
262
263 impl FileNonce {
264 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 pub fn update(&mut self, bytes: &[u8]) {
278 self.0.update(bytes);
279 }
280
281 pub fn finish(self) -> [u8; NONCE_LEN] {
283 self.0.finalize().into_bytes().into()
284 }
285 }
286
287 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 const FIRST_FILL: usize = 64 << 10;
316
317 fn block_buf(size: usize) -> Zeroizing<Vec<u8>> {
319 Zeroizing::new(vec![0u8; size])
320 }
321
322 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 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 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 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 pub fn seal(key: &DataKey, plaintext: &[u8]) -> Vec<u8> {
402 seal_with_chunk(key, CHUNK, plaintext)
403 }
404
405 #[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 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 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 #[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 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 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 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 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 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 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 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 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 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};