use std::time::SystemTime;
use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine as _};
use ring::rand::SecureRandom;
use crate::aead::{self, DerivedKey};
use crate::envelope;
#[allow(dead_code)] pub(crate) fn encode_cookie(
key: &DerivedKey,
rng: &dyn SecureRandom,
issued_at: SystemTime,
payload_json: &[u8],
) -> String {
let layer1 = envelope::encode_envelope(issued_at, payload_json);
let sealed = aead::seal(key, rng, &layer1);
BASE64_URL_SAFE_NO_PAD.encode(&sealed)
}
pub(crate) fn decode_cookie(
keys: &[DerivedKey],
cookie_value: &str,
) -> Option<(SystemTime, Vec<u8>, usize)> {
let sealed = BASE64_URL_SAFE_NO_PAD.decode(cookie_value).ok()?;
let (layer1, key_index) = aead::try_decrypt(keys, &sealed)?;
let (issued_at, payload_json) = envelope::decode_envelope(&layer1).ok()?;
Some((issued_at, payload_json, key_index))
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use proptest::prelude::*;
use ring::rand::SystemRandom;
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
struct Inner {
id: u64,
tag: String,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
enum Role {
Admin,
Member(u32),
Guest,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
struct TestPayload {
user_id: u64,
email: Option<String>,
roles: Vec<Role>,
nested: Option<Inner>,
}
const IKM_PRIMARY: &[u8; 32] = b"primary-ikm-fixed-bytes-32-len!!";
const IKM_OTHER_1: &[u8; 32] = b"o1-fallback-ikm-fixed-bytes-32!!";
const IKM_OTHER_2: &[u8; 32] = b"o2-fallback-ikm-fixed-bytes-32!!";
const IKM_UNKNOWN: &[u8; 32] = b"x-unknown-ikm-not-in-keylist-32!";
fn key_from(ikm: &[u8]) -> DerivedKey {
DerivedKey::derive(ikm)
}
fn fixed_issued_at() -> SystemTime {
SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000)
}
fn sample_payload() -> TestPayload {
TestPayload {
user_id: 42,
email: Some("a@b".into()),
roles: vec![Role::Admin, Role::Member(7), Role::Guest],
nested: Some(Inner {
id: 1,
tag: "x".into(),
}),
}
}
#[test]
fn round_trip_through_single_key_returns_index_zero_ac1_1_ac4_1() {
let rng = SystemRandom::new();
let key = key_from(IKM_PRIMARY);
let payload = sample_payload();
let payload_json = serde_json::to_vec(&payload).expect("payload serializes");
let issued_at = fixed_issued_at();
let cookie = encode_cookie(&key, &rng, issued_at, &payload_json);
let (decoded_time, decoded_bytes, idx) =
decode_cookie(&[key_from(IKM_PRIMARY)], &cookie).expect("round-trip must decode");
assert_eq!(decoded_time, issued_at);
assert_eq!(idx, 0);
let decoded_payload: TestPayload =
serde_json::from_slice(&decoded_bytes).expect("layer-2 JSON must parse");
assert_eq!(decoded_payload, payload);
}
#[test]
fn round_trip_preserves_complex_shapes_ac1_2() {
let rng = SystemRandom::new();
let key = key_from(IKM_PRIMARY);
let payload = TestPayload {
user_id: u64::MAX,
email: Some("user@example.test".into()),
roles: vec![
Role::Admin,
Role::Member(0),
Role::Member(u32::MAX),
Role::Guest,
Role::Admin,
],
nested: Some(Inner {
id: 99_999,
tag: "deeply-nested-tag".into(),
}),
};
let payload_json = serde_json::to_vec(&payload).expect("payload serializes");
let issued_at = fixed_issued_at();
let cookie = encode_cookie(&key, &rng, issued_at, &payload_json);
let (_, decoded_bytes, _) =
decode_cookie(&[key_from(IKM_PRIMARY)], &cookie).expect("round-trip must decode");
let decoded_payload: TestPayload =
serde_json::from_slice(&decoded_bytes).expect("layer-2 JSON must parse");
assert_eq!(decoded_payload, payload);
let payload_with_nones = TestPayload {
user_id: 1,
email: None,
roles: vec![Role::Guest],
nested: None,
};
let payload_json = serde_json::to_vec(&payload_with_nones).expect("payload serializes");
let cookie = encode_cookie(&key, &rng, issued_at, &payload_json);
let (_, decoded_bytes, _) =
decode_cookie(&[key_from(IKM_PRIMARY)], &cookie).expect("round-trip must decode");
let decoded_payload: TestPayload =
serde_json::from_slice(&decoded_bytes).expect("layer-2 JSON must parse");
assert_eq!(decoded_payload, payload_with_nones);
}
#[test]
fn round_trip_large_payload_under_4kb_cookie_ac1_3() {
let rng = SystemRandom::new();
let key = key_from(IKM_PRIMARY);
let target_json_len = 3000usize;
let probe = TestPayload {
user_id: 7,
email: Some(String::new()),
roles: vec![Role::Admin],
nested: Some(Inner {
id: 2,
tag: "n".into(),
}),
};
let probe_json = serde_json::to_vec(&probe).expect("probe serializes");
let pad_len = target_json_len.saturating_sub(probe_json.len());
let payload = TestPayload {
user_id: 7,
email: Some("x".repeat(pad_len)),
roles: vec![Role::Admin],
nested: Some(Inner {
id: 2,
tag: "n".into(),
}),
};
let payload_json = serde_json::to_vec(&payload).expect("payload serializes");
assert!(
(target_json_len.saturating_sub(100)..=target_json_len + 100)
.contains(&payload_json.len()),
"test fixture should produce roughly {target_json_len} bytes of JSON, got {}",
payload_json.len()
);
let issued_at = fixed_issued_at();
let cookie = encode_cookie(&key, &rng, issued_at, &payload_json);
assert!(
cookie.len() < 4096,
"cookie {} bytes exceeds 4 KB browser cap",
cookie.len()
);
let (_, decoded_bytes, _) =
decode_cookie(&[key_from(IKM_PRIMARY)], &cookie).expect("round-trip must decode");
let decoded_payload: TestPayload =
serde_json::from_slice(&decoded_bytes).expect("layer-2 JSON must parse");
assert_eq!(decoded_payload, payload);
}
#[test]
fn tamper_rejects_every_byte_flip_ac1_4() {
let rng = SystemRandom::new();
let key = key_from(IKM_PRIMARY);
let payload = sample_payload();
let payload_json = serde_json::to_vec(&payload).expect("payload serializes");
let issued_at = fixed_issued_at();
let cookie = encode_cookie(&key, &rng, issued_at, &payload_json);
let sealed_bytes = BASE64_URL_SAFE_NO_PAD
.decode(&cookie)
.expect("our own cookie must decode as base64");
let keys = [key_from(IKM_PRIMARY)];
for byte_index in 0..sealed_bytes.len() {
let mut tampered_bytes = sealed_bytes.clone();
tampered_bytes[byte_index] ^= 0x01;
let tampered_cookie = BASE64_URL_SAFE_NO_PAD.encode(&tampered_bytes);
let result = decode_cookie(&keys, &tampered_cookie);
assert!(
result.is_none(),
"tamper at byte {byte_index} unexpectedly authenticated"
);
}
}
#[test]
fn rotation_decrypts_via_first_fallback_ac4_2() {
let rng = SystemRandom::new();
let old_key = key_from(IKM_OTHER_1);
let payload = sample_payload();
let payload_json = serde_json::to_vec(&payload).expect("payload serializes");
let issued_at = fixed_issued_at();
let cookie = encode_cookie(&old_key, &rng, issued_at, &payload_json);
let keys = [key_from(IKM_PRIMARY), key_from(IKM_OTHER_1)];
let (decoded_time, decoded_bytes, idx) =
decode_cookie(&keys, &cookie).expect("fallback must decrypt cookie sealed by O");
assert_eq!(decoded_time, issued_at);
assert_eq!(idx, 1);
let decoded_payload: TestPayload =
serde_json::from_slice(&decoded_bytes).expect("layer-2 JSON must parse");
assert_eq!(decoded_payload, payload);
}
#[test]
fn rotation_decrypts_via_second_fallback_ac4_6() {
let rng = SystemRandom::new();
let o2_key = key_from(IKM_OTHER_2);
let payload = sample_payload();
let payload_json = serde_json::to_vec(&payload).expect("payload serializes");
let issued_at = fixed_issued_at();
let cookie = encode_cookie(&o2_key, &rng, issued_at, &payload_json);
let keys = [
key_from(IKM_PRIMARY),
key_from(IKM_OTHER_1),
key_from(IKM_OTHER_2),
];
let (decoded_time, decoded_bytes, idx) =
decode_cookie(&keys, &cookie).expect("third key must authenticate");
assert_eq!(decoded_time, issued_at);
assert_eq!(idx, 2);
let decoded_payload: TestPayload =
serde_json::from_slice(&decoded_bytes).expect("layer-2 JSON must parse");
assert_eq!(decoded_payload, payload);
}
#[test]
fn decode_returns_none_for_invalid_base64_ac6_1() {
let key = key_from(IKM_PRIMARY);
let result = decode_cookie(&[key], "@@@not-valid-base64@@@");
assert!(result.is_none());
}
#[test]
fn decode_returns_none_for_standard_base64_alphabet_ac6_1() {
let key = key_from(IKM_PRIMARY);
let result = decode_cookie(&[key], "AAAA+AAAA");
assert!(result.is_none());
}
#[test]
fn decode_returns_none_for_empty_string_ac6_1() {
let key = key_from(IKM_PRIMARY);
let result = decode_cookie(&[key], "");
assert!(result.is_none());
}
#[test]
fn decode_returns_none_when_no_key_authenticates_ac6_3() {
let rng = SystemRandom::new();
let unknown_key = key_from(IKM_UNKNOWN);
let payload = sample_payload();
let payload_json = serde_json::to_vec(&payload).expect("payload serializes");
let issued_at = fixed_issued_at();
let cookie = encode_cookie(&unknown_key, &rng, issued_at, &payload_json);
let trial_keys = [key_from(IKM_PRIMARY), key_from(IKM_OTHER_1)];
let result = decode_cookie(&trial_keys, &cookie);
assert!(result.is_none());
}
#[test]
fn decode_returns_bytes_for_schema_mismatch_then_serde_errors_ac6_5() {
let rng = SystemRandom::new();
let key = key_from(IKM_PRIMARY);
let issued_at = fixed_issued_at();
let valid_but_wrong_json =
serde_json::to_vec(&"just a string").expect("string serializes to JSON");
let cookie = encode_cookie(&key, &rng, issued_at, &valid_but_wrong_json);
let (decoded_time, decoded_bytes, _) =
decode_cookie(&[key_from(IKM_PRIMARY)], &cookie).expect("codec layer must succeed");
assert_eq!(decoded_time, issued_at);
assert_eq!(decoded_bytes, valid_but_wrong_json);
let typed_result: Result<TestPayload, _> = serde_json::from_slice(&decoded_bytes);
assert!(typed_result.is_err());
}
proptest! {
#[test]
fn prop_roundtrip_under_any_rotation_order(
payload_bytes in prop::collection::vec(any::<u8>(), 0..3072),
extra_keys in prop::collection::vec(
prop::collection::vec(any::<u8>(), 16..32),
0..4usize,
),
sealing_index in 0usize..5,
) {
let rng = SystemRandom::new();
let primary_ikm = [0x11u8; 16];
let all_ikms: Vec<Vec<u8>> = std::iter::once(primary_ikm.to_vec())
.chain(extra_keys.into_iter())
.collect();
let idx = sealing_index % all_ikms.len();
let keys: Vec<DerivedKey> =
all_ikms.iter().map(|k| DerivedKey::derive(k)).collect();
let issued_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000_000);
let cookie = encode_cookie(&keys[idx], &rng, issued_at, &payload_bytes);
let (t, bytes, returned_idx) =
decode_cookie(&keys, &cookie).expect("encoded cookie must decode");
prop_assert_eq!(t, issued_at);
prop_assert_eq!(bytes, payload_bytes);
prop_assert_eq!(returned_idx, idx);
}
}
}