1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
//! Functions for creating and parsing signed & encrypted cookies.
//!
//! Functions for creating and parsing signed & encrypted cookies.
//!
//! The [cookie](https://crates.io/crates/cookie) crate is the de-facto secure cookie library in Rust.
//! It is Way Too Complicated (TM) for what I need. (And, in my opinion, for what most people need.)
//! This is the 80% solution for 20% of the effort.
//!
//! This library has only two goals:
//! - A simple, easily auditable implementation of singing, encrypting, decrypting & verifying cookies.
//! - Clear comments pointing out security issues and describing how to avoid them.
//!
//! The goals of this library are *not*:
//! - Automatically detecting when a new Set-Cookie header is required.
//! - Tracking changes to cookies.
//! - Validating cookie name compliance with [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265). (Just don't use any weird cookie es.)
//! - Any kind of cookie "jar" functionality.
//! - Literally anything else.
//!
//! ## Examples
//!
//! Basic use:
//!
//! ```
//! use simple_cookie::{generate_signing_key, encode_cookie, decode_cookie};
//!
//! let signing_key = generate_signing_key();
//! let encoded = encode_cookie(&signing_key, "account_id", &[56]);
//! let decoded = decode_cookie(&signing_key, "account_id", encoded);
//!
//! assert_eq!(decoded, Some(vec![56]));
//! ```
//!
//! You probably want an actual Set-Cookie header. You can build one pretty easily:
//!
//! ```
//! use simple_cookie::{generate_signing_key, encode_cookie};
//!
//! let signing_key = generate_signing_key();
//! let encoded = encode_cookie(&signing_key, "account_id", &[56]);
//! let header = format!("Set-Cookie: session={}; Max-Age=604800; Secure; HttpOnly; SameSite=Strict", encoded);
//! ```
//!
//! Then, to decrypt a header:
//!
//! ```
//! use simple_cookie::{parse_cookie_header_value, decode_cookie};
//!
//! // You can create your own key or load it from somewhere.
//! // Don't use all zeros like this though. See the documentation for SigningKey for more info.
//! let signing_key = [0; 32];
//!
//! // This is a standard HTTP Cookie header, pretty much exactly what the browser sends to your server.
//! let header = b"Cookie: session=gNm1wQ6lTTgAxLxfD2ntNS2nIBVcnjSmI+7FdFk; another-cookie=another-value";
//!
//! // parse_cookie_header_value doesn't expect the header name.
//! // you don't normally need this step since HTTP libraries typically automatically parse
//! // the header name & value into separate parts of a tuple or struct or something.
//! let header = &header[8..];
//!
//! // parse_cookie_header_value returns an iterator, so you can use it in a for loop or something.
//! // I'll just find the cookie we're interested in here.
//! let (name, encoded_value) = parse_cookie_header_value(header).find(|(name, _value)| *name == "session").unwrap();
//! let value = decode_cookie(&signing_key, name, encoded_value);
//!
//! assert!(value.is_some())
//! ```



/// A bit of cryptographically secure random data is attached to every encoded cookie so that
/// identical values don't have identical encoded representations. This prevents attackers
/// from determining the value of an encoded cookie by comparing it to the encoded value of
/// a known cookie.
const NONCE_LENGTH: usize = 12;



/// Key used to sign, encrypt, decrypt & verify your cookies
///
/// The signing key should be cryptographically secure random data.
/// You can use [generate_signing_key] to safely make a signing key,
/// or you can generate it yourself as long as you make sure the randomness is cryptographically secure.
/// This signing key may be stored in a secure location and loaded at startup if you like. You might want to store & load if:
/// - Cookie based sessions should out-last server restarts
/// - The same cookie needs to be read by separate instances of the server in horizontal scaling situations
/// - The cookie needs to be read by an entirely separate unrelated server (say, a caching server or something)
pub type SigningKey = [u8; 32];



/// Generate a new signing key for use with the [encode_cookie] and [decode_cookie] functions.
///
/// This uses the thread-local random number generator, which is guaranteed by the rand crate
/// to produce cryptographically secure random data.
pub fn generate_signing_key() -> SigningKey {
    use rand::RngCore;
    let mut data = [0; 32];
    rand::thread_rng().fill_bytes(&mut data);
    data
}



/// Build an iterator from the value part of a Cookie: header that will yield a name/value tuple for each cookie.
///
/// Certain characters are not permitted in cookie names, and different characters are not permitted
/// in cookie values. See [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265) for details. This function makes no attempt to validate the name
/// or value of the cookie headers.
///
/// Cookie values may or may not be quoted. (Like this: session="38h29onuf20138t")
/// This iterator will never include the quotes in the emitted value.
/// In the above example, the pair will be: ("session", "38h29onuf20138t") instead of ("session", "\"38h29onuf20138t\"")
/// Note that according to [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265), using quotes is optional and never necessary
/// because the characters permitted inside a quoted value are the exact same characters
/// permitted outside the quoted value.
///
/// Cookie values may not necessarily be valid UTF-8.
/// As such, this function emits values of type &[u8]
pub fn parse_cookie_header_value(header: &[u8]) -> impl Iterator<Item = (&str, &[u8])> {
    header
        .split(|c| *c == b';')
        .map(|x| trim_ascii_whitespace(x))
        .filter_map(|x| {
            let mut key_value_iterator = x.split(|c| *c == b'=').into_iter();

            let key: &[u8] = key_value_iterator.next()?;
            let key: &[u8] = trim_ascii_whitespace(key);
            let key: &str = std::str::from_utf8(key).ok()?;

            let value: &[u8] = trim_ascii_whitespace(key_value_iterator.next()?);
            let value: &[u8] = value.strip_prefix(&[b'"']).unwrap_or(value);
            let value: &[u8] = value.strip_suffix(&[b'"']).unwrap_or(value);

            Some((key, value))
        })
}



// Trims ascii whitespace from either end of a slice.
// Calls should be replaced with &[u8]::trim_ascii() when it stabilizes
fn trim_ascii_whitespace(slice: &[u8]) -> &[u8] {
    let mut start_index = 0;
    for (index, character) in slice.iter().enumerate() {
        start_index = index;
        if *character != b' ' && *character != b'\t' {
            break;
        }
    }

    let mut end_index = slice.len();
    for (index, character) in slice.iter().enumerate().rev() {
        end_index = index;
        if *character != b' ' && *character != b'\t' {
            break;
        }
    }

    &slice[start_index..=end_index]
}



/// Encrypt & sign a cookie value.
///
/// ## Cookie Name
/// The name of the cookie is required to prevent attackers
/// from swapping the encrypted value of one cookie with the encrypted value of another cookie.
///
/// For example, say you have two cookies:
///
/// ```txt
/// session-account-id=2381
/// last-cache-reload=3193
/// ```
///
/// When encrypted, the cookies might look like:
///
/// ```txt
/// session-account=LfwFJ8N0YR5f4U8dWFc5vARKQL7GvRJI
/// last-cache-reload=NyOwR3npVm0gn8xlm89qcPMzQHjLZLs99
/// ```
///
/// If the name of the cookie wasn't included in the encrypted value it would be possible for
/// an attacker to swap the values of the two cookies and make your server think that the
/// session-account-id cookie value was 3193, effectively impersonating another user.
///
/// The name will be included in the encrypted value and verified against the name you provide
/// when calling [decode_cookie] later.
///
/// ## Other Notes
/// [RFC6265](https://datatracker.ietf.org/doc/html/rfc6265) restricts the characters valid in cookie names. This function does *not* validate the name you provide.
///
/// Inspired by the [cookie](https://crates.io/crates/cookie) crate.
pub fn encode_cookie<Name: AsRef<str>, Value: AsRef<[u8]>>(key: &SigningKey, name: Name, value: Value) -> String {
    let value: &[u8] = value.as_ref();
    let name: &str = name.as_ref();

    // Final message will be [nonce, encrypted_value, signature]
    let mut data = vec![0; NONCE_LENGTH + value.len() + 16];

    // Split the data vec apart into mutable slices for each component
    let (nonce_slot, message_and_tag) = data.split_at_mut(NONCE_LENGTH);
    let (encrypted_slot, signature_slot) = message_and_tag.split_at_mut(value.len());

    // Generate some random data for the nonce
    use rand::RngCore;
    rand::thread_rng().fill_bytes(nonce_slot);

    // Copy the unencrypted message into the slot for the encrypted message
    // (It will be encrypted in-place.)
    encrypted_slot.copy_from_slice(value);

    // Encrypt the message
    // This encryption method has a convenient associated data option that will be part
    // of the signature, so we'll drop the cookie name into that rather than doing something
    // more complex like concatenating the message and name ourselves.
    use aes_gcm::{AeadInPlace, KeyInit};
    let key_array = aes_gcm::aead::generic_array::GenericArray::from_slice(key);
    let nonce_array = aes_gcm::aead::generic_array::GenericArray::from_slice(nonce_slot);
    let encryptor = aes_gcm::Aes256Gcm::new(key_array);
    let signature = encryptor
        .encrypt_in_place_detached(&nonce_array, name.as_bytes(), encrypted_slot)
        .expect("failed to encrypt");

    // Copy the signature into the final message
    signature_slot.copy_from_slice(&signature);

    use base64::Engine;
    base64::engine::general_purpose::STANDARD_NO_PAD.encode(&data)
}



/// Decrypt & verify the signature of a cookie value.
///
/// The name of the cookie is included in the signed content generated by
/// encode_cookie, and is cross-referenced with the value you provide here to
/// guarantee that the cookie's encrypted content was not swapped with the
/// encrypted content of another cookie. For security purposes (e.g. to
/// prevent side-channel attacks) no details about a decoding failure are
/// returned.
///
/// Inspired by the [cookie](https://crates.io/crates/cookie) crate.
pub fn decode_cookie<Name: AsRef<str>, Value: AsRef<[u8]>>(key: &SigningKey, name: Name, value: Value) -> Option<Vec<u8>> {
    use aes_gcm::KeyInit;
    use aes_gcm::aead::Aead;
    use base64::Engine;

    // The binary cipher is base64 encoded
    let message = base64::engine::general_purpose::STANDARD_NO_PAD.decode(value.as_ref()).ok()?;

    // The binary cipher is constructed as [ nonce, encrypted_value_with_signature ]
    // so we need to split it into it's individual parts
    let (nonce, cipher) = message.split_at(NONCE_LENGTH);

    /*
    The API we should have is
       aes256gcm::decrypt(key: &[u8], nonce: &[u8], expected_associated_data: &[u8], cipher: &[u8]) -> Option<Vec<u8>>

    Instead we have to wrap the first two arguments in GenericArray structs,
    construct a decryptor object with the wrapped signing key, build a struct containing
    the cipher text and expected associated data, then call decrypt on the decryptor
    object passing in the struct and wrapped nonce. I really hope there's a good reason
    for this API, because if not it's really stupid.
    */

    // Wrap the slices up in GenericArrays because that's what aes_gcm expects
    let key_array = aes_gcm::aead::generic_array::GenericArray::from_slice(key);
    let nonce_array = aes_gcm::aead::generic_array::GenericArray::from_slice(nonce);

    // Wrap the cipher and expected associated data in a struct because that's what aes_gcm expects
    let payload = aes_gcm::aead::Payload {
        msg: cipher,
        aad: name.as_ref().as_bytes(),
    };

    // Build the decryptor object which we'll use to decrypt the cipher text
    let cipher = aes_gcm::Aes256Gcm::new(key_array);

    // Actually decrypt the value!
    // For security reasons aes_gcm returns no details about the error, just an empty struct.
    // This prevents side-channel leakage.
    cipher.decrypt(nonce_array, payload).ok()
}




#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn encode_decode_succeeds() {
        let key = &generate_signing_key();
        let name = "session";
        let data = r#"{"id":5}"#;
        let encoded = encode_cookie(key, name, data);
        let decoded = decode_cookie(key, name, encoded);
        assert_eq!(decoded.unwrap(), data.as_bytes());
    }

    #[test]
    fn different_keys_fails() {
        let key_a = generate_signing_key();
        let name = "session";
        let data = r#"{"id":5}"#;
        let encoded = encode_cookie(&key_a, name, data);

        let key_b = generate_signing_key();
        let decoded = decode_cookie(&key_b, name, encoded);

        assert_eq!(decoded, None);
    }

    #[test]
    fn different_names_fails() {
        let key = &generate_signing_key();
        let name_a = "session";
        let data = r#"{"id":5}"#;
        let encoded = encode_cookie(key, name_a, data);

        let name_b = "laskdjf";
        let decoded = decode_cookie(key, name_b, encoded);

        assert_eq!(decoded, None);
    }

    #[test]
    fn identical_values_have_different_ciphers() {
        let key = &generate_signing_key();
        let name = "session";
        let data = "which wolf do you feed?";
        let encoded_1 = encode_cookie(key, name, data);
        let encoded_2 = encode_cookie(key, name, data);
        assert_ne!(encoded_1, encoded_2);
    }

    #[test]
    fn parses_spaceless_header() {
        let header = b"session=213lkj1;another=3829";
        let mut iterator = parse_cookie_header_value(header);

        let (name, value) = iterator.next().unwrap();
        assert_eq!(name, "session");
        assert_eq!(value, b"213lkj1");

        let (name, value) = iterator.next().unwrap();
        assert_eq!(name, "another");
        assert_eq!(value, b"3829");
    }

    #[test]
    fn parses_spaced_header() {
        let header = b"session = 123kj; sakjdf = klsjdf23";
        let mut iterator = parse_cookie_header_value(header);

        let (name, value) = iterator.next().unwrap();
        assert_eq!(name, "session");
        assert_eq!(value, b"123kj");

        let (name, value) = iterator.next().unwrap();
        assert_eq!(name, "sakjdf");
        assert_eq!(value, b"klsjdf23");
    }

    #[test]
    fn strips_value_quotes() {
        let header = b"session=\"alkjs\"";
        let mut iterator = parse_cookie_header_value(header);
        let (name, value) = iterator.next().unwrap();
        assert_eq!(name, "session");
        assert_eq!(value, b"alkjs");
    }

    #[test]
    fn ignores_name_quotes() {
        let header = b"\"session\"=asdf";
        let mut iterator = parse_cookie_header_value(header);
        let (name, value) = iterator.next().unwrap();
        assert_eq!(name, "\"session\"");
        assert_eq!(value, b"asdf");
    }
}