Skip to main content

vitaminc_aead/ciphertext/
mod.rs

1mod read_monads;
2mod write_monads;
3use bytes::Bytes;
4use read_monads::CipherTextReader;
5use serde::{Deserialize, Serialize};
6pub use write_monads::CipherTextBuilder;
7
8/// First byte of every [`LocalCipherText`] — the wire-format version.
9///
10/// The leaf is the only byte-format commitment the crate makes (the
11/// container tree has no canonical encoding), so the version discriminator
12/// lives here and rides inside every persisted leaf for free. Bump it on
13/// any break to the leaf layout *or* to the AAD derivation rules; decrypt
14/// rejects versions it does not know how to parse.
15///
16/// The byte is authenticated, not merely parsed: every leaf's effective
17/// AAD binds it via [`Context::for_leaf`](crate::Context::for_leaf), so relabeling
18/// a stored leaf's version fails tag verification instead of selecting a
19/// different (perhaps weaker) set of derivation rules — a downgrade is
20/// foreclosed by construction, not by parser luck.
21pub const WIRE_VERSION: u8 = 1;
22
23/// A sealed leaf: `version(1) ‖ nonce ‖ ciphertext ‖ tag`.
24#[derive(Debug, Serialize, Deserialize)]
25#[serde(transparent)]
26pub struct LocalCipherText(Bytes);
27
28impl LocalCipherText {
29    pub fn into_inner(self) -> Bytes {
30        self.0
31    }
32
33    pub fn into_reader(self) -> CipherTextReader {
34        CipherTextReader::new(self.0)
35    }
36
37    /// The stored wire-format version, readable without the key — for
38    /// diagnostics and migration tooling (an operator can tell "old
39    /// format" from "current format" on a decrypt failure). `None` means
40    /// the buffer is empty. This is an *unauthenticated peek*: trust it
41    /// for triage, never for parsing decisions outside the reader, which
42    /// re-checks it under the AEAD tag.
43    pub fn wire_version(&self) -> Option<u8> {
44        self.0.first().copied()
45    }
46}
47
48impl AsRef<[u8]> for LocalCipherText {
49    fn as_ref(&self) -> &[u8] {
50        self.0.as_ref()
51    }
52}
53
54impl From<Vec<u8>> for LocalCipherText {
55    fn from(bytes: Vec<u8>) -> Self {
56        LocalCipherText(Bytes::from(bytes))
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use crate::Nonce;
64    use vitaminc_protected::{Controlled, Protected};
65
66    #[test]
67    fn test_ciphertext_builder_with_plaintext_in_place() -> Result<(), ()> {
68        let nonce = Nonce::new([1u8; 12]);
69        let plaintext = vec![0u8; 10];
70        let ciphertext = CipherTextBuilder::new()
71            .append_nonce(nonce)
72            .append_target_plaintext(plaintext)
73            .accepts_ciphertext_and_tag_ok(|mut ciphertext| {
74                ciphertext.copy_from_slice(&[2u8; 10]);
75                ciphertext.extend([3u8; 16]);
76                Ok(ciphertext)
77            })
78            .build()?;
79
80        // version(1) ‖ nonce(12) ‖ ciphertext(10) ‖ tag(16)
81        assert_eq!(ciphertext.0.len(), 39);
82        assert_eq!(ciphertext.0[0], WIRE_VERSION);
83        assert_eq!(ciphertext.wire_version(), Some(WIRE_VERSION));
84        assert_eq!(&ciphertext.0[1..13], &[1u8; 12]);
85        assert_eq!(&ciphertext.0[13..23], &[2u8; 10]);
86        assert_eq!(&ciphertext.0[23..], &[3u8; 16]);
87
88        Ok(())
89    }
90
91    #[test]
92    fn test_ciphertext_reader() -> Result<(), ()> {
93        let nonce = Nonce::new([1u8; 12]);
94        let plaintext: Protected<Vec<u8>> = Protected::new(vec![0u8; 10]);
95
96        let ciphertext = CipherTextBuilder::new()
97            .append_nonce(nonce)
98            .append_target_plaintext(plaintext)
99            .accepts_ciphertext_and_tag_ok(|mut ciphertext| {
100                ciphertext.copy_from_slice(&[2u8; 10]);
101                ciphertext.extend([3u8; 16]);
102                Ok(ciphertext)
103            })
104            .build()?;
105
106        let (nonce, reader) = ciphertext
107            .into_reader()
108            .read_version()
109            .map_err(|_| ())?
110            .read_nonce::<12>()
111            .map_err(|_| ())?;
112
113        let plaintext = reader
114            .accepts_plaintext_ok(|data| {
115                assert_eq!(data.len(), 26);
116                assert_eq!(&data[..10], [2u8; 10]);
117                assert_eq!(&data[10..], [3u8; 16]);
118                // Write in the same way as AWS-LC/Ring does
119                data[..10].copy_from_slice(&[0u8; 10]);
120                Ok(10)
121            })
122            .read()?;
123
124        assert_eq!(nonce.into_inner(), [1u8; 12]);
125        assert_eq!(plaintext.risky_unwrap()[..10], vec![0u8; 10]);
126
127        Ok(())
128    }
129
130    #[test]
131    fn wire_version_reads_the_stored_first_byte() {
132        // A byte other than WIRE_VERSION proves the peek reads the buffer
133        // rather than returning a constant.
134        assert_eq!(
135            LocalCipherText::from(vec![7u8, 0, 0]).wire_version(),
136            Some(7)
137        );
138        assert_eq!(
139            LocalCipherText::from(vec![WIRE_VERSION]).wire_version(),
140            Some(WIRE_VERSION)
141        );
142        assert_eq!(LocalCipherText::from(Vec::new()).wire_version(), None);
143    }
144}