Skip to main content

coven_crypto/
cipher.rs

1//! One XChaCha20-Poly1305 implementation for objects, chunks and custody.
2
3use crate::{randomness, CryptoError};
4use chacha20poly1305::{
5    aead::{array::typenum::Unsigned, consts::U24, Aead, AeadCore, AeadInOut, Payload},
6    KeyInit, XChaCha20Poly1305,
7};
8use zeroize::Zeroizing;
9
10/// Bytes added to a file chunk: its authentication tag, without a stored nonce.
11pub const FILE_CHUNK_TAG_LEN: usize = <XChaCha20Poly1305 as AeadCore>::TagSize::USIZE;
12
13/// A random nonce, stored before the ciphertext it seals.
14pub(crate) const NONCE_LEN: usize = <XChaCha20Poly1305 as AeadCore>::NonceSize::USIZE;
15
16/// Bytes added to each sealed object chunk: its stored nonce and authentication tag.
17pub const SEALED_OBJECT_CHUNK_OVERHEAD: usize = NONCE_LEN + FILE_CHUNK_TAG_LEN;
18
19pub(crate) fn storage_path(path: &str) -> &[u8] {
20    assert!(!path.is_empty(), "storage paths must be nonempty");
21    path.as_bytes()
22}
23pub(crate) fn context(parts: &[&[u8]]) -> Vec<u8> {
24    let mut aad = Vec::new();
25    for part in parts {
26        aad.extend_from_slice(&(part.len() as u64).to_be_bytes());
27        aad.extend_from_slice(part);
28    }
29    aad
30}
31
32pub(crate) fn seal(key: &[u8; 32], nonce: &[u8; 24], aad: &[u8], plaintext: &[u8]) -> Vec<u8> {
33    encrypt(&XChaCha20Poly1305::new(key.into()), nonce, aad, plaintext)
34}
35
36fn encrypt(
37    cipher: &impl Aead<NonceSize = U24>,
38    nonce: &[u8; 24],
39    aad: &[u8],
40    plaintext: &[u8],
41) -> Vec<u8> {
42    cipher
43        .encrypt(nonce.into(), Payload { msg: plaintext, aad })
44        .expect("XChaCha20-Poly1305 plaintext must fit its block counter and lengths must fit u64")
45}
46
47pub(crate) fn open(
48    key: &[u8; 32],
49    nonce: &[u8; 24],
50    aad: &[u8],
51    sealed: &[u8],
52) -> Result<Vec<u8>, CryptoError> {
53    if sealed.len() < FILE_CHUNK_TAG_LEN {
54        return Err(CryptoError::Malformed);
55    }
56    XChaCha20Poly1305::new(key.into())
57        .decrypt(nonce.into(), Payload { msg: sealed, aad })
58        // AEAD's error is a unit: authentication failed, and nothing more.
59        .map_err(|chacha20poly1305::aead::Error| CryptoError::Authentication)
60}
61
62pub(crate) fn seal_random(
63    key: &[u8; 32],
64    aad: &[u8],
65    plaintext: &[u8],
66) -> Result<Vec<u8>, CryptoError> {
67    let mut nonce = [0; NONCE_LEN];
68    randomness::fill(&mut nonce)?;
69    let mut sealed = nonce.to_vec();
70    sealed.extend(seal(key, &nonce, aad, plaintext));
71    Ok(sealed)
72}
73
74pub(crate) fn open_random(
75    key: &[u8; 32],
76    aad: &[u8],
77    sealed: &[u8],
78) -> Result<Vec<u8>, CryptoError> {
79    let (nonce, ciphertext) =
80        sealed.split_first_chunk::<NONCE_LEN>().ok_or(CryptoError::Malformed)?;
81    open(key, nonce, aad, ciphertext)
82}
83
84/// Open a ciphertext whose length the caller fixed into a plaintext of the
85/// same length, erased on drop.
86pub(crate) fn open_exact<const N: usize>(
87    key: &[u8; 32],
88    nonce: &[u8; NONCE_LEN],
89    aad: &[u8],
90    ciphertext: &[u8; N],
91    tag: &[u8; FILE_CHUNK_TAG_LEN],
92) -> Result<Zeroizing<[u8; N]>, CryptoError> {
93    let mut plaintext = Zeroizing::new(*ciphertext);
94    XChaCha20Poly1305::new(key.into())
95        .decrypt_inout_detached(nonce.into(), aad, plaintext.as_mut_slice().into(), tag.into())
96        // AEAD's error is a unit: authentication failed, and nothing more.
97        .map_err(|chacha20poly1305::aead::Error| CryptoError::Authentication)?;
98    Ok(plaintext)
99}
100
101#[cfg(test)]
102#[path = "cipher_tests.rs"]
103mod tests;