simple_cookie/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(feature="std"), no_std)]
3
4
5
6/// Key used to sign, encrypt, decrypt & verify your cookies
7///
8/// The signing key should be cryptographically secure random data.
9/// You can use [generate_signing_key] to safely make a signing key,
10/// or you can generate it yourself as long as you make sure the randomness is cryptographically secure.
11/// This signing key may be stored in a secure location and loaded at startup if you like. You might want to store & load if:
12/// - Cookie based sessions should out-last server restarts
13/// - The same cookie needs to be read by separate instances of the server in horizontal scaling situations
14/// - The cookie needs to be read by an entirely separate unrelated server (say, a caching server or something)
15pub type SigningKey = [u8; 32];
16
17/// A bit of random data attached to every cookie before encrypting to avoid the same cookie
18/// value being encrypted into the same bits.
19///
20/// Use [generate_nonce] to create one, or make your own from **cryptographically secure** random data.
21pub type Nonce = [u8; 12];
22
23const NONCE_LENGTH: usize = core::mem::size_of::<Nonce>();
24
25const SIGNATURE_LENGTH: usize = 16;
26
27
28
29/// Generate a new signing key for use with the [encode_cookie] and [decode_cookie] functions.
30///
31/// This uses the thread-local random number generator, which is guaranteed by the rand crate
32/// to produce cryptographically secure random data.
33#[cfg(feature="rand")]
34pub fn generate_signing_key() -> SigningKey {
35	let mut data = [0; 32];
36	rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut data);
37	data
38}
39
40
41
42/// Generate a new nonce for encrypting a cookie with the [encode_cookie] function
43///
44/// This uses the thread-local random number generator, which is guaranteed by the rand crate
45/// to produce cryptographically secure random data.
46#[cfg(feature="rand")]
47pub fn generate_nonce() -> Nonce {
48	let mut data = [0; 12];
49	rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut data);
50	data
51}
52
53
54
55/**
56Build an iterator from the value part of a Cookie: header that will yield a name/value tuple for each cookie.
57
58Certain characters are not permitted in cookie names, and different characters are not permitted
59in cookie values. See [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265) for details. This function makes no attempt to validate the name
60or value of the cookie headers.
61
62Cookie values may or may not be quoted. (Like this: session="38h29onuf20138t")
63This iterator will never include the quotes in the emitted value.
64In the above example, the pair will be: ("session", "38h29onuf20138t") instead of ("session", "\"38h29onuf20138t\"")
65Note that according to [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265), using quotes is optional and never necessary
66because the characters permitted inside a quoted value are the exact same characters
67permitted outside the quoted value.
68
69Cookie values may not necessarily be valid UTF-8.
70As such, this function emits values of type [&\[u8\]](slice).
71*/
72pub fn parse_cookie_header_value(header: &[u8]) -> impl Iterator<Item = (&str, &[u8])> {
73	header
74		.split(|c| *c == b';')
75		.map(|x| x.trim_ascii())
76		.filter_map(|x| {
77			let mut key_value_iterator = x.split(|c| *c == b'=').into_iter();
78
79			let key: &[u8] = key_value_iterator.next()?;
80			let key: &[u8] = key.trim_ascii();
81			let key: &str = core::str::from_utf8(key).ok()?;
82
83			let value: &[u8] = key_value_iterator.next()?.trim_ascii();
84			let value: &[u8] = value.strip_prefix(&[b'"']).unwrap_or(value);
85			let value: &[u8] = value.strip_suffix(&[b'"']).unwrap_or(value);
86
87			Some((key, value))
88		})
89}
90
91
92
93
94/**
95Encrypt & sign a cookie value.
96
97You may be interested in [encode_cookie_advanced] for no_std support.
98
99## Cookie Name
100The name of the cookie is required to prevent attackers
101from swapping the encrypted value of one cookie with the encrypted value of another cookie.
102
103For example, say you have two cookies:
104
105```txt
106session-account-id=2381
107last-cache-reload=3193
108```
109
110When encrypted, the cookies might look like:
111
112```txt
113session-account=LfwFJ8N0YR5f4U8dWFc5vARKQL7GvRJI
114last-cache-reload=NyOwR3npVm0gn8xlm89qcPMzQHjLZLs99
115```
116
117If the name of the cookie wasn't included in the encrypted value it would be possible for
118an attacker to swap the values of the two cookies and make your server think that the
119session-account-id cookie value was 3193, effectively impersonating another user.
120
121The name will be included in the encrypted value and verified against the name you provide
122when calling [decode_cookie] later.
123
124## Other Notes
125[RFC6265](https://datatracker.ietf.org/doc/html/rfc6265) restricts the characters valid in cookie names. This function does *not* validate the name you provide.
126
127Inspired by the [cookie](https://crates.io/crates/cookie) crate.
128*/
129#[cfg(all(feature="std", feature="rand"))]
130pub fn encode_cookie(key: SigningKey, name: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> String {
131	let nonce = generate_nonce();
132	let mut output = vec![0; encoded_buffer_size(value.as_ref().len()).expect("unreachable, len comes from a slice and no slice can be large enough to make this operation overflow")];
133	encode_cookie_advanced(key, nonce, name, value, &mut output).expect("unreachable, the buffer should always be correctly sized");
134	String::from_utf8(output).expect("unreachable, encode_cookie_advanced should always produce ascii data")
135}
136
137
138
139/**
140Just like [encode_cookie], but advanced.
141
142This function supports running in a no_std environment without the rand crate. To securely produce
143cookies you must guarantee that the provided [Nonce] is filled with cryptographically secure random
144data and the signing key you provide abides by the requirements documented on the [SigningKey] type.
145
146You can use the [encoded_buffer_size] function to get the required size of the output buffer.
147*/
148pub fn encode_cookie_advanced<'a>(
149	key: SigningKey,
150	nonce: Nonce,
151	name: impl AsRef<[u8]>,
152	value: impl AsRef<[u8]>,
153	output: &'a mut [u8],
154) -> Result<(), OutputBufferWrongSize> {
155	let value: &[u8] = value.as_ref();
156
157	let expected_size =
158		match encoded_buffer_size(value.len()) {
159			None => return Err(OutputBufferWrongSize { expected_size: None, was: value.len(), }),
160			Some(x) if output.len() < x => return Err(OutputBufferWrongSize { expected_size: Some(x), was: value.len(), }),
161			Some(x) if x < output.len() => return Err(OutputBufferWrongSize { expected_size: Some(x), was: value.len(), }),
162			Some(x) => x,
163		};
164
165	// Final message will be [nonce, encrypted_value, signature]
166	// Split the output buffer apart into mutable slices for each component
167	let (nonce_slot, rest_of_output) = output.split_at_mut(NONCE_LENGTH);
168	let (encrypted_slot, rest_of_output) = rest_of_output.split_at_mut(value.len());
169	let (signature_slot, _rest_of_output) = rest_of_output.split_at_mut(SIGNATURE_LENGTH);
170
171	// Copy unencrypted data into the encrypted buffer.
172	// The message will will be encrypted in-place.
173	nonce_slot.copy_from_slice(&nonce);
174	encrypted_slot.copy_from_slice(value);
175
176	// Encrypt the message
177	// This encryption method has a convenient associated output option that will be part
178	// of the signature, so we'll drop the cookie name into that rather than doing something
179	// more complex like concatenating the message and name ourselves.
180	use aes_gcm::{AeadInPlace, KeyInit};
181	let key_array = aes_gcm::aead::generic_array::GenericArray::from_slice(&key);
182	let nonce_array = aes_gcm::aead::generic_array::GenericArray::from_slice(nonce_slot);
183	let encryptor = aes_gcm::Aes256Gcm::new(key_array);
184	let signature = encryptor
185		.encrypt_in_place_detached(&nonce_array, name.as_ref(), encrypted_slot)
186		.expect("failed to encrypt");
187
188	// The signature is returned from aes_gcm rather than being written into the output buffer,
189	// so we need to write it in ourselves.
190	signature_slot.copy_from_slice(&signature);
191
192	let total_length = NONCE_LENGTH + value.len() + SIGNATURE_LENGTH;
193
194	// Cookie values must be in the ASCII printable range
195	// unwrap: encoded data guaranteed to fit, output size checked at start of function
196	encode_bytes_as_ascii(&mut output[..expected_size], total_length).unwrap();
197
198	Ok(())
199}
200
201/// Unable to encode the cookie. Returned from [encode_cookie_advanced].
202#[derive(Debug, Eq, PartialEq, Copy, Clone)]
203pub struct OutputBufferWrongSize {
204	/// Expected size of the output buffer, or None if the required size would overflow a usize.
205	pub expected_size: Option<usize>,
206	pub was: usize,
207}
208
209
210
211/**
212Get the required size of the output buffer passed to encode_cookie for the given input buffer length
213
214This will always be larger than the size of the decoded buffer.
215
216Returns None if the calculation would overflow a usize. This happens somewhere around
217usize::MAX/2, so shouldn't happen on 64 bit platforms unless you have more RAM than the CIA.
218*/
219pub const fn encoded_buffer_size(value_length: usize) -> Option<usize> {
220	if (usize::MAX / 2) - NONCE_LENGTH - SIGNATURE_LENGTH < value_length {
221		None
222	} else {
223		Some((NONCE_LENGTH + value_length + SIGNATURE_LENGTH) * 2)
224	}
225}
226
227
228
229/**
230Get the length of the data inside an encrypted buffer of the given length.
231
232This is the length of the output buffer passed to [decode_cookie_advanced].
233
234This will always be smaller than the length of the encoded data.
235
236Note that the encrypted value includes a constant amount of non-message data and will therefore
237have a minimum length. If the length passed to this function is too small to contain the required
238constant data, this function will return None.
239*/
240pub const fn decode_buffer_size(value_length: usize) -> Option<usize> {
241	if value_length < (NONCE_LENGTH + SIGNATURE_LENGTH) * 2 {
242		None
243	} else {
244		Some((value_length / 2) - NONCE_LENGTH - SIGNATURE_LENGTH)
245	}
246}
247
248
249
250/**
251Decrypt & verify the signature of a cookie value.
252
253See [decode_cookie_advanced] for no_std support.
254
255The name of the cookie is included in the signed content generated by
256[encode_cookie], and is cross-referenced with the value you provide here to
257guarantee that the cookie's encrypted content was not swapped with the
258encrypted content of another cookie. For security purposes (e.g. to
259prevent side-channel attacks) no details about a decoding failure are
260returned.
261
262Returns `Err(DecodeCookieError)` if the value argument is empty.
263
264Inspired by the [cookie](https://crates.io/crates/cookie) crate.
265*/
266#[cfg(feature="std")]
267pub fn decode_cookie(key: SigningKey, name: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
268	let Some(output_buffer_length) = decode_buffer_size(value.as_ref().len()) else {
269		return Err(DecodeError);
270	};
271
272	let mut output = vec![0; output_buffer_length];
273
274	match decode_cookie_advanced(key, name, value, &mut output) {
275		Ok(_) => Ok(output),
276		Err(reason) => Err(reason),
277	}
278}
279
280
281
282/**
283Just like [decode_cookie], but advanced.
284
285This function supports running in a no_std environment by taking an output buffer to write into
286rather than allocating a Vec. It otherwise behaves identically to [decode_cookie].
287
288Use [decode_buffer_size] to determine the required length of the output buffer.
289
290Returns `Err(DecodeCookieError)` if the output buffer is too small.
291*/
292pub fn decode_cookie_advanced(key: SigningKey, name: impl AsRef<[u8]>, value: impl AsRef<[u8]>, output: &mut [u8]) -> Result<(), DecodeError> {
293	let value = value.as_ref();
294
295	if value.len() == 0 { return Err(DecodeError); }
296
297	if output.len() != decode_buffer_size(value.len()).ok_or(DecodeError)? {
298		return Err(DecodeError);
299	}
300
301	let merged_values_length = value.len() / 2;
302
303	// The binary cipher is base64 encoded as [ nonce, encrypted_value, signature ]
304	let mut nonce = [0u8; NONCE_LENGTH];
305	decode_ascii_as_bytes(value, &mut nonce, 0, NONCE_LENGTH);
306
307	let mut signature = [0u8; SIGNATURE_LENGTH];
308	decode_ascii_as_bytes(value, &mut signature, merged_values_length - SIGNATURE_LENGTH, merged_values_length);
309
310	decode_ascii_as_bytes(value, output, NONCE_LENGTH, merged_values_length - SIGNATURE_LENGTH);
311
312	// Wrap the slices up in GenericArrays because that's what aes_gcm requires
313	let key_array = aes_gcm::aead::generic_array::GenericArray::from_slice(&key);
314	let nonce_array = aes_gcm::aead::generic_array::GenericArray::from_slice(&nonce);
315	let signature = aes_gcm::aead::generic_array::GenericArray::from_slice(&signature);
316
317	// Actually decrypt the value!
318	use aes_gcm::KeyInit;
319	use aes_gcm::AeadInPlace;
320	aes_gcm::Aes256Gcm::new(key_array)
321		.decrypt_in_place_detached(
322			nonce_array,
323			name.as_ref(),
324			output,
325			signature,
326		)
327		.or(Err(DecodeError))
328}
329
330/// The given cookie is not valid.
331///
332/// To avoid side-channel leakage no further information can be returned about the error.
333#[derive(Copy, Clone, Debug, Eq, PartialEq)]
334pub struct DecodeError;
335
336
337
338/**
339Encode arbitrary bytes into just letters in the ASCII range.
340
341Returns None if the input buffer is not large enough to encode the given length of data.
342
343## Rationale
344
345Only letters, numbers, and some symbols are permitted in cookie values.
346To store the arbitrary bytes output by the encryption algorithm, we need
347to encode it into just the permitted characters.
348
349Base64 is a common solution to this. I used this custom encoding instead for two reasons:
350
3511. To support no_alloc I needed to be able to encode into the same buffer that the data
352	being encoded is in. This is possible in base64 (I've written the code to do it before
353	actually) but I wasn't able to find a base64 library on crates.io that could do it,
354	meaning I'd need to write the code myself anyway.
355
3562. To support no_alloc I needed to be able to decode starting from an arbitrary position in the
357	input data. This is also possible in base64 (I've written that function too) but I've never
358	seen that feature in *any* base64 library regardless of language, much less on crates.io.
359
360I could have implemented the appropriate base64 functions, but this encoding is much,
361much simpler to write and an explicit goal of this library is simplicity.
362
363Downside is that the encoding is not as efficient as base64, resulting in larger encoded text.
364
365base64 length = (4n + 2) / 3
366This method = 2n
367
368So this is somewhere around 1.5x larger
369That's not too much larger, so it's worth the trade-off in my opinion.
370*/
371fn encode_bytes_as_ascii<'a>(input: &'a mut [u8], length: usize) -> Option<&'a mut str> {
372	if input.len() < length * 2 {
373		return None;
374	}
375
376	let mut read_index = length;
377	let mut write_index = length * 2;
378
379	while 0 < read_index {
380		read_index -= 1;
381		write_index -= 2;
382		let byte = input[read_index];
383		let high = byte >> 4;
384		let low = byte & 0b1111;
385		input[write_index + 0] = high + b'a';
386		input[write_index + 1] = low + b'a';
387	}
388
389	let string =
390		core::str::from_utf8_mut(&mut input[..length * 2])
391		.expect("unreachable: code can only generate valid ascii");
392
393	Some(string)
394}
395
396
397
398/**
399Decode bytes encoded with [encode_bytes_as_ascii].
400
401See the docs on that function for rationale.
402
403- Does not error on invalid bytes - output will contain whatever data the algorithm happens to decode the invalid bytes to.
404- Returns an empty slice if to < from.
405- If the length indicated by from..to is larger than output, will decode as much as possible and return.
406*/
407fn decode_ascii_as_bytes<'a>(input: &[u8], output: &'a mut [u8], from: usize, to: usize) -> &'a mut [u8] {
408	if to < from {
409		return &mut output[..0];
410	}
411
412	let length = (to - from).min(output.len());
413
414	for (index, chunk) in input.chunks_exact(2).skip(from).take(length).enumerate() {
415		let [high, low] = chunk else { unreachable!() };
416
417		output[index] =
418			((high.saturating_sub(b'a')) & 0b1111) << 4
419			| ((low.saturating_sub(b'a')) & 0b1111);
420	}
421
422	&mut output[..length]
423}
424
425
426
427#[cfg(test)]
428mod test {
429	use super::*;
430
431	pub fn init_random() -> oorandom::Rand64 {
432		let seed = rand::Rng::gen_range(&mut rand::thread_rng(), 100_000_000..999_999_999);
433		println!("Seed: {}", seed);
434		oorandom::Rand64::new(seed)
435	}
436
437	pub fn random_bytes(random: &mut oorandom::Rand64) -> Vec<u8> {
438		let length = random.rand_range(0..50);
439		let mut data = vec![0; length as usize];
440		for entry in data.iter_mut() {
441			*entry = random.rand_u64() as u8;
442		}
443
444		data
445	}
446
447	pub const fn const_unwrap(input: Option<usize>) -> usize {
448		match input {
449			None => panic!("Tried to unwrap a None value"),
450			Some(t) => t,
451		}
452	}
453
454	#[test]
455	fn test_ascii_encode() {
456		let mut random = test::init_random();
457
458		for _ in 0..100 {
459			let raw_data = test::random_bytes(&mut random);
460
461			if raw_data.len() == 0 { continue; }
462
463			let mut encoded_buffer = vec![0u8; raw_data.len() * 2];
464			encoded_buffer[..raw_data.len()].copy_from_slice(&raw_data);
465			encode_bytes_as_ascii(&mut encoded_buffer, raw_data.len()).unwrap();
466
467			for _ in 0..10 {
468				let from = random.rand_range(0..raw_data.len() as u64) as usize;
469				let to = random.rand_range(from as u64..raw_data.len() as u64) as usize;
470				let mut decoded_buffer = vec![0u8; to - from];
471				decode_ascii_as_bytes(&encoded_buffer, &mut decoded_buffer, from, to);
472
473				assert_eq!(&raw_data[from..to], &decoded_buffer);
474			}
475		}
476	}
477
478	#[test]
479	fn encode_decode_succeeds() {
480		let key = generate_signing_key();
481		let nonce = generate_nonce();
482		let name = "session";
483		let data = r#"{"id":5}"#;
484
485		let mut encoded = [0u8; const_unwrap(encoded_buffer_size(8))];
486		encode_cookie_advanced(key, nonce, name, data, &mut encoded).unwrap();
487
488		let mut decoded = [0u8; 8];
489		decode_cookie_advanced(key, name, encoded, &mut decoded).unwrap();
490
491		assert_eq!(decoded, data.as_bytes());
492	}
493
494	#[test]
495	fn returns_error_for_invalid_buffer_lengths() {
496		let key = generate_signing_key();
497
498		assert_eq!(Err(DecodeError), decode_cookie_advanced(key, "", "", &mut []));
499		assert_eq!(Err(DecodeError), decode_cookie_advanced(key, "", "a", &mut []));
500		assert_eq!(Err(DecodeError), decode_cookie_advanced(key, "", "asdklfjaskdf", &mut []));
501		assert_eq!(Err(DecodeError), decode_cookie_advanced(key, "", "asdklfjaskdf", &mut [0u8]));
502		assert_eq!(Err(DecodeError), decode_cookie_advanced(key, "", "asdklfjaskdf", &mut [0u8; 5]));
503	}
504
505	#[test]
506	fn different_keys_fails() {
507		let key_a = generate_signing_key();
508		let nonce = generate_nonce();
509		let name = "session";
510		let data = r#"{"id":5}"#;
511
512		let mut encoded = [0u8; const_unwrap(encoded_buffer_size(8))];
513		encode_cookie_advanced(key_a, nonce, name, data, &mut encoded).unwrap();
514
515		let key_b = generate_signing_key();
516		let mut decoded = [0u8; 8];
517		let decode_result = decode_cookie_advanced(key_b, name, encoded, &mut decoded);
518
519		assert_eq!(decode_result, Err(DecodeError));
520	}
521
522	#[test]
523	fn different_names_fails() {
524		let key = generate_signing_key();
525		let nonce = generate_nonce();
526		let name_a = "session";
527		let data = r#"{"id":5}"#;
528
529		let mut encoded = [0u8; const_unwrap(encoded_buffer_size(8))];
530		encode_cookie_advanced(key, nonce, name_a, data, &mut encoded).unwrap();
531
532		let name_b = "laskdjf";
533		let mut decoded = [0u8; 8];
534		let decode_result = decode_cookie_advanced(key, name_b, encoded, &mut decoded);
535
536		assert_eq!(decode_result, Err(DecodeError));
537	}
538
539	#[test]
540	fn identical_values_have_different_ciphers() {
541		let key = generate_signing_key();
542		let name = "session";
543		let data = "which wolf do you feed?";
544
545		let mut encoded_1 = [0u8; const_unwrap(encoded_buffer_size(23))];
546		encode_cookie_advanced(key, generate_nonce(), name, data, &mut encoded_1).unwrap();
547
548		let mut encoded_2 = [0u8; const_unwrap(encoded_buffer_size(23))];
549		encode_cookie_advanced(key, generate_nonce(), name, data, &mut encoded_2).unwrap();
550
551		assert_ne!(encoded_1, encoded_2);
552	}
553
554	#[test]
555	fn parses_spaceless_header() {
556		let header = b"session=213lkj1;another=3829";
557		let mut iterator = parse_cookie_header_value(header);
558
559		let (name, value) = iterator.next().unwrap();
560		assert_eq!(name, "session");
561		assert_eq!(value, b"213lkj1");
562
563		let (name, value) = iterator.next().unwrap();
564		assert_eq!(name, "another");
565		assert_eq!(value, b"3829");
566	}
567
568	#[test]
569	fn parses_spaced_header() {
570		let header = b"session = 123kj; sakjdf = klsjdf23";
571		let mut iterator = parse_cookie_header_value(header);
572
573		let (name, value) = iterator.next().unwrap();
574		assert_eq!(name, "session");
575		assert_eq!(value, b"123kj");
576
577		let (name, value) = iterator.next().unwrap();
578		assert_eq!(name, "sakjdf");
579		assert_eq!(value, b"klsjdf23");
580	}
581
582	#[test]
583	fn strips_value_quotes() {
584		let header = b"session=\"alkjs\"";
585		let mut iterator = parse_cookie_header_value(header);
586		let (name, value) = iterator.next().unwrap();
587		assert_eq!(name, "session");
588		assert_eq!(value, b"alkjs");
589	}
590
591	#[test]
592	fn includes_name_quotes() {
593		let header = b"\"session\"=asdf";
594		let mut iterator = parse_cookie_header_value(header);
595		let (name, value) = iterator.next().unwrap();
596		assert_eq!(name, "\"session\"");
597		assert_eq!(value, b"asdf");
598	}
599}