1use crate::aead::Algorithm;
56use crate::error::{Error, Result};
57
58pub const MAGIC: &[u8; 8] = b"\x89CRYPTIO";
61
62pub const HEADER_LEN: usize = 24;
64
65pub const NONCE_LEN: usize = 12;
67
68pub const NONCE_PREFIX_LEN: usize = 7;
70
71pub const TAG_LEN: usize = 16;
73
74pub const VERSION: u8 = 0x01;
76
77pub const DEFAULT_CHUNK_SIZE_LOG2: u8 = 16;
79
80pub const MIN_CHUNK_SIZE_LOG2: u8 = 10;
83
84pub const MAX_CHUNK_SIZE_LOG2: u8 = 24;
87
88pub(super) const ALG_CHACHA20_POLY1305: u8 = 0x00;
89pub(super) const ALG_AES_256_GCM: u8 = 0x01;
90
91pub(super) fn encode_algorithm(algorithm: Algorithm) -> u8 {
93 match algorithm {
94 Algorithm::ChaCha20Poly1305 => ALG_CHACHA20_POLY1305,
95 Algorithm::Aes256Gcm => ALG_AES_256_GCM,
96 }
97}
98
99pub(super) fn decode_algorithm(byte: u8) -> Result<Algorithm> {
101 match byte {
102 ALG_CHACHA20_POLY1305 => Ok(Algorithm::ChaCha20Poly1305),
103 ALG_AES_256_GCM => Ok(Algorithm::Aes256Gcm),
104 _ => Err(Error::InvalidCiphertext(alloc::format!(
105 "unknown algorithm byte: 0x{byte:02x}"
106 ))),
107 }
108}
109
110#[must_use]
112pub(super) fn build_header(
113 algorithm: Algorithm,
114 chunk_size_log2: u8,
115 nonce_prefix: &[u8; NONCE_PREFIX_LEN],
116) -> [u8; HEADER_LEN] {
117 let mut h = [0u8; HEADER_LEN];
118 h[0..8].copy_from_slice(MAGIC);
119 h[8] = VERSION;
120 h[9] = encode_algorithm(algorithm);
121 h[10] = chunk_size_log2;
122 h[16..23].copy_from_slice(nonce_prefix);
124 h
126}
127
128#[derive(Debug, Clone, Copy)]
130pub(super) struct ParsedHeader {
131 pub algorithm: Algorithm,
132 pub chunk_size_log2: u8,
133 pub nonce_prefix: [u8; NONCE_PREFIX_LEN],
134 pub raw: [u8; HEADER_LEN],
136}
137
138pub(super) fn parse_header(bytes: &[u8]) -> Result<ParsedHeader> {
140 if bytes.len() < HEADER_LEN {
141 return Err(Error::InvalidCiphertext(alloc::format!(
142 "stream header too short ({} bytes, need {HEADER_LEN})",
143 bytes.len()
144 )));
145 }
146 let raw_slice = &bytes[..HEADER_LEN];
147
148 if &raw_slice[0..8] != MAGIC {
149 return Err(Error::InvalidCiphertext(alloc::string::String::from(
150 "stream magic mismatch (not a crypt-io stream)",
151 )));
152 }
153 if raw_slice[8] != VERSION {
154 return Err(Error::InvalidCiphertext(alloc::format!(
155 "unsupported stream version: 0x{:02x} (this build understands 0x{VERSION:02x})",
156 raw_slice[8],
157 )));
158 }
159 let algorithm = decode_algorithm(raw_slice[9])?;
160 let chunk_size_log2 = raw_slice[10];
161 if !(MIN_CHUNK_SIZE_LOG2..=MAX_CHUNK_SIZE_LOG2).contains(&chunk_size_log2) {
162 return Err(Error::InvalidCiphertext(alloc::format!(
163 "chunk_size_log2 out of range: {chunk_size_log2}"
164 )));
165 }
166 let mut nonce_prefix = [0u8; NONCE_PREFIX_LEN];
167 nonce_prefix.copy_from_slice(&raw_slice[16..23]);
168
169 let mut raw = [0u8; HEADER_LEN];
170 raw.copy_from_slice(raw_slice);
171
172 Ok(ParsedHeader {
173 algorithm,
174 chunk_size_log2,
175 nonce_prefix,
176 raw,
177 })
178}
179
180#[must_use]
183pub(super) fn build_nonce(
184 nonce_prefix: &[u8; NONCE_PREFIX_LEN],
185 counter: u32,
186 is_final: bool,
187) -> [u8; NONCE_LEN] {
188 let mut n = [0u8; NONCE_LEN];
189 n[0..7].copy_from_slice(nonce_prefix);
190 n[7..11].copy_from_slice(&counter.to_be_bytes());
191 n[11] = u8::from(is_final);
192 n
193}
194
195#[must_use]
197pub(super) fn chunk_size_from_log2(log2: u8) -> usize {
198 1usize << log2
199}
200
201#[cfg(test)]
202#[allow(clippy::unwrap_used, clippy::expect_used)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn header_round_trip_chacha() {
208 let prefix = [0xaau8; NONCE_PREFIX_LEN];
209 let h = build_header(Algorithm::ChaCha20Poly1305, 16, &prefix);
210 let p = parse_header(&h).unwrap();
211 assert_eq!(p.algorithm, Algorithm::ChaCha20Poly1305);
212 assert_eq!(p.chunk_size_log2, 16);
213 assert_eq!(p.nonce_prefix, prefix);
214 assert_eq!(p.raw, h);
215 }
216
217 #[test]
218 fn header_round_trip_aes() {
219 let prefix = [0xbbu8; NONCE_PREFIX_LEN];
220 let h = build_header(Algorithm::Aes256Gcm, 12, &prefix);
221 let p = parse_header(&h).unwrap();
222 assert_eq!(p.algorithm, Algorithm::Aes256Gcm);
223 assert_eq!(p.chunk_size_log2, 12);
224 assert_eq!(p.nonce_prefix, prefix);
225 }
226
227 #[test]
228 fn header_rejects_wrong_magic() {
229 let mut h = build_header(Algorithm::ChaCha20Poly1305, 16, &[0u8; 7]);
230 h[0] = b'X';
231 let err = parse_header(&h).unwrap_err();
232 assert!(matches!(err, Error::InvalidCiphertext(_)));
233 }
234
235 #[test]
236 fn header_rejects_unknown_version() {
237 let mut h = build_header(Algorithm::ChaCha20Poly1305, 16, &[0u8; 7]);
238 h[8] = 0xff;
239 let err = parse_header(&h).unwrap_err();
240 assert!(matches!(err, Error::InvalidCiphertext(_)));
241 }
242
243 #[test]
244 fn header_rejects_unknown_algorithm() {
245 let mut h = build_header(Algorithm::ChaCha20Poly1305, 16, &[0u8; 7]);
246 h[9] = 0x42;
247 let err = parse_header(&h).unwrap_err();
248 assert!(matches!(err, Error::InvalidCiphertext(_)));
249 }
250
251 #[test]
252 fn header_rejects_out_of_range_chunk_size_log2() {
253 for bad in [0u8, 9, 25, 64, 255] {
254 let mut h = build_header(Algorithm::ChaCha20Poly1305, 16, &[0u8; 7]);
255 h[10] = bad;
256 let err = parse_header(&h).unwrap_err();
257 assert!(matches!(err, Error::InvalidCiphertext(_)), "bad={bad}");
258 }
259 }
260
261 #[test]
262 fn header_rejects_too_short() {
263 let err = parse_header(&[0u8; HEADER_LEN - 1]).unwrap_err();
264 assert!(matches!(err, Error::InvalidCiphertext(_)));
265 }
266
267 #[test]
268 fn nonce_distinct_per_counter_and_flag() {
269 let prefix = [0xccu8; NONCE_PREFIX_LEN];
270 let n0 = build_nonce(&prefix, 0, false);
271 let n1 = build_nonce(&prefix, 1, false);
272 let n0_final = build_nonce(&prefix, 0, true);
273 assert_ne!(n0, n1);
274 assert_ne!(n0, n0_final);
275 assert_ne!(n1, n0_final);
276 assert_eq!(&n0[..7], &prefix);
278 assert_eq!(n0[7..11], 0u32.to_be_bytes());
279 assert_eq!(n0[11], 0);
280 assert_eq!(n0_final[11], 1);
281 }
282
283 #[test]
284 fn chunk_size_from_log2_matches_pow2() {
285 assert_eq!(chunk_size_from_log2(10), 1024);
286 assert_eq!(chunk_size_from_log2(16), 65_536);
287 assert_eq!(chunk_size_from_log2(20), 1_048_576);
288 }
289}