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.
21///
22/// Version 2 is the tagged leaf of ADR 0001: the plaintext is a type tag
23/// followed by the scalar's bytes. Version 1 leaves have the same layout,
24/// but what they hold depends on who sealed them: the pre-ADR 0001 `Value`
25/// path (`vitaminc-aead-value` 0.5 and the Go binding) sealed tagged
26/// scalars, while `vitaminc-encrypt` 0.5's `Encrypt` path sealed raw,
27/// untagged bytes. Nothing in a version 1 leaf tells the two apart, so a
28/// reader that reads tags opens version 1 only where it knows the leaf came
29/// from the `Value` path, by passing it to `read_version_in` on the reader
30/// from [`LocalCipherText::into_reader`].
31pub const WIRE_VERSION: u8 = 2;
32
33/// A sealed leaf: `version(1) ‖ nonce ‖ ciphertext ‖ tag`.
34#[derive(Debug, Serialize, Deserialize)]
35#[serde(transparent)]
36pub struct LocalCipherText(Bytes);
37
38impl LocalCipherText {
39    pub fn into_inner(self) -> Bytes {
40        self.0
41    }
42
43    pub fn into_reader(self) -> CipherTextReader {
44        CipherTextReader::new(self.0)
45    }
46
47    /// The stored wire-format version, readable without the key — for
48    /// diagnostics and migration tooling (an operator can tell "old
49    /// format" from "current format" on a decrypt failure). `None` means
50    /// the buffer is empty. This is an *unauthenticated peek*: trust it
51    /// for triage, never for parsing decisions outside the reader, which
52    /// re-checks it under the AEAD tag.
53    pub fn wire_version(&self) -> Option<u8> {
54        self.0.first().copied()
55    }
56}
57
58impl AsRef<[u8]> for LocalCipherText {
59    fn as_ref(&self) -> &[u8] {
60        self.0.as_ref()
61    }
62}
63
64impl From<Vec<u8>> for LocalCipherText {
65    fn from(bytes: Vec<u8>) -> Self {
66        LocalCipherText(Bytes::from(bytes))
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::Nonce;
74    use vitaminc_protected::{Controlled, Protected};
75
76    #[test]
77    fn test_ciphertext_builder_with_plaintext_in_place() -> Result<(), ()> {
78        let nonce = Nonce::new([1u8; 12]);
79        let plaintext = vec![0u8; 10];
80        let ciphertext = CipherTextBuilder::new()
81            .append_nonce(nonce)
82            .append_target_plaintext(plaintext)
83            .accepts_ciphertext_and_tag_ok(|mut ciphertext| {
84                ciphertext.copy_from_slice(&[2u8; 10]);
85                ciphertext.extend([3u8; 16]);
86                Ok(ciphertext)
87            })
88            .build()?;
89
90        // version(1) ‖ nonce(12) ‖ ciphertext(10) ‖ tag(16)
91        assert_eq!(ciphertext.0.len(), 39);
92        assert_eq!(ciphertext.0[0], WIRE_VERSION);
93        assert_eq!(ciphertext.wire_version(), Some(WIRE_VERSION));
94        assert_eq!(&ciphertext.0[1..13], &[1u8; 12]);
95        assert_eq!(&ciphertext.0[13..23], &[2u8; 10]);
96        assert_eq!(&ciphertext.0[23..], &[3u8; 16]);
97
98        Ok(())
99    }
100
101    #[test]
102    fn test_ciphertext_reader() -> Result<(), ()> {
103        let nonce = Nonce::new([1u8; 12]);
104        let plaintext: Protected<Vec<u8>> = Protected::new(vec![0u8; 10]);
105
106        let ciphertext = CipherTextBuilder::new()
107            .append_nonce(nonce)
108            .append_target_plaintext(plaintext)
109            .accepts_ciphertext_and_tag_ok(|mut ciphertext| {
110                ciphertext.copy_from_slice(&[2u8; 10]);
111                ciphertext.extend([3u8; 16]);
112                Ok(ciphertext)
113            })
114            .build()?;
115
116        let (nonce, reader) = ciphertext
117            .into_reader()
118            .read_version()
119            .map_err(|_| ())?
120            .read_nonce::<12>()
121            .map_err(|_| ())?;
122
123        let plaintext = reader
124            .accepts_plaintext_ok(|data| {
125                assert_eq!(data.len(), 26);
126                assert_eq!(&data[..10], [2u8; 10]);
127                assert_eq!(&data[10..], [3u8; 16]);
128                // Write in the same way as AWS-LC/Ring does
129                data[..10].copy_from_slice(&[0u8; 10]);
130                Ok(10)
131            })
132            .read()?;
133
134        assert_eq!(nonce.into_inner(), [1u8; 12]);
135        assert_eq!(plaintext.risky_unwrap()[..10], vec![0u8; 10]);
136
137        Ok(())
138    }
139
140    #[test]
141    fn wire_version_reads_the_stored_first_byte() {
142        // A byte other than WIRE_VERSION proves the peek reads the buffer
143        // rather than returning a constant.
144        assert_eq!(
145            LocalCipherText::from(vec![7u8, 0, 0]).wire_version(),
146            Some(7)
147        );
148        assert_eq!(
149            LocalCipherText::from(vec![WIRE_VERSION]).wire_version(),
150            Some(WIRE_VERSION)
151        );
152        assert_eq!(LocalCipherText::from(Vec::new()).wire_version(), None);
153    }
154}