1pub const H_VERSION: &str = "X-SESAME-Version";
15pub const H_KEY_ID: &str = "X-SESAME-KeyId";
16pub const H_TIMESTAMP: &str = "X-SESAME-Timestamp";
17pub const H_NONCE: &str = "X-SESAME-Nonce";
18pub const H_SIGNATURE: &str = "X-SESAME-Signature";
19pub const H_SCOPE: &str = "X-SESAME-Scope";
20pub const H_ENCRYPTED: &str = "X-SESAME-Encrypted";
21pub const H_ENC_KEY_ID: &str = "X-SESAME-EncKeyId";
22pub const H_IV: &str = "X-SESAME-IV";
23
24pub const PROTOCOL_VERSION: &str = "1.0";
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum SesameError {
41 MissingHeaders,
42 InvalidVersion,
43 UnknownKey,
44 ExpiredTimestamp,
45 ReplayDetected,
46 SignatureMismatch,
47 ScopeDenied,
48 DecryptFailed,
49 KeyRevoked,
50}
51
52impl SesameError {
53 pub fn code(&self) -> &'static str {
55 match self {
56 SesameError::MissingHeaders => "sesame_missing_headers",
57 SesameError::InvalidVersion => "sesame_invalid_version",
58 SesameError::UnknownKey => "sesame_unknown_key",
59 SesameError::ExpiredTimestamp => "sesame_expired_timestamp",
60 SesameError::ReplayDetected => "sesame_replay_detected",
61 SesameError::SignatureMismatch => "sesame_signature_mismatch",
62 SesameError::ScopeDenied => "sesame_scope_denied",
63 SesameError::DecryptFailed => "sesame_decrypt_failed",
64 SesameError::KeyRevoked => "sesame_key_revoked",
65 }
66 }
67
68 pub fn http_status(&self) -> u16 {
70 match self {
71 SesameError::InvalidVersion | SesameError::DecryptFailed => 400,
72 SesameError::ScopeDenied => 403,
73 _ => 401,
74 }
75 }
76}
77
78impl core::fmt::Display for SesameError {
79 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
80 write!(f, "{}", self.code())
81 }
82}
83
84impl std::error::Error for SesameError {}
85
86#[derive(Debug, Clone, Default, PartialEq, Eq)]
94pub struct SesameHeaders {
95 pub version: Option<String>,
96 pub key_id: Option<String>,
97 pub timestamp: Option<String>,
98 pub nonce: Option<String>,
99 pub signature: Option<String>,
100 pub scope: Option<String>,
101 pub encrypted: bool,
102 pub enc_key_id: Option<String>,
103 pub iv: Option<String>,
104}
105
106impl SesameHeaders {
107 pub fn is_absent(&self) -> bool {
110 self.version.is_none()
111 && self.key_id.is_none()
112 && self.timestamp.is_none()
113 && self.nonce.is_none()
114 && self.signature.is_none()
115 }
116
117 pub fn from_lookup<F>(get: F) -> Self
120 where
121 F: Fn(&str) -> Option<String>,
122 {
123 let encrypted = get(H_ENCRYPTED)
124 .map(|v| v.eq_ignore_ascii_case("true"))
125 .unwrap_or(false);
126 SesameHeaders {
127 version: get(H_VERSION),
128 key_id: get(H_KEY_ID),
129 timestamp: get(H_TIMESTAMP),
130 nonce: get(H_NONCE),
131 signature: get(H_SIGNATURE),
132 scope: get(H_SCOPE),
133 encrypted,
134 enc_key_id: get(H_ENC_KEY_ID),
135 iv: get(H_IV),
136 }
137 }
138
139 pub fn require_tier1(&self) -> Result<Tier1Fields<'_>, SesameError> {
142 match (
143 self.version.as_deref(),
144 self.key_id.as_deref(),
145 self.timestamp.as_deref(),
146 self.nonce.as_deref(),
147 self.signature.as_deref(),
148 ) {
149 (Some(version), Some(key_id), Some(timestamp), Some(nonce), Some(signature)) => {
150 Ok(Tier1Fields {
151 version,
152 key_id,
153 timestamp,
154 nonce,
155 signature,
156 })
157 }
158 _ => Err(SesameError::MissingHeaders),
159 }
160 }
161}
162
163pub struct Tier1Fields<'a> {
165 pub version: &'a str,
166 pub key_id: &'a str,
167 pub timestamp: &'a str,
168 pub nonce: &'a str,
169 pub signature: &'a str,
170}
171
172pub fn hex_encode(bytes: &[u8]) -> String {
177 const LUT: &[u8; 16] = b"0123456789abcdef";
178 let mut out = String::with_capacity(bytes.len() * 2);
179 for &b in bytes {
180 out.push(LUT[(b >> 4) as usize] as char);
181 out.push(LUT[(b & 0x0f) as usize] as char);
182 }
183 out
184}
185
186pub fn hex_decode(s: &str) -> Option<Vec<u8>> {
187 if s.len() % 2 != 0 {
188 return None;
189 }
190 let bytes = s.as_bytes();
191 let mut out = Vec::with_capacity(s.len() / 2);
192 let val = |c: u8| -> Option<u8> {
193 match c {
194 b'0'..=b'9' => Some(c - b'0'),
195 b'a'..=b'f' => Some(c - b'a' + 10),
196 b'A'..=b'F' => Some(c - b'A' + 10),
197 _ => None,
198 }
199 };
200 let mut i = 0;
201 while i < bytes.len() {
202 let hi = val(bytes[i])?;
203 let lo = val(bytes[i + 1])?;
204 out.push((hi << 4) | lo);
205 i += 2;
206 }
207 Some(out)
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn hex_roundtrip() {
216 let data = [0x00u8, 0x0f, 0xa1, 0xff, 0x10];
217 assert_eq!(hex_encode(&data), "000fa1ff10");
218 assert_eq!(hex_decode("000fa1ff10").unwrap(), data);
219 }
220
221 #[test]
222 fn hex_decode_rejects_odd_and_nonhex() {
223 assert!(hex_decode("abc").is_none());
224 assert!(hex_decode("zz").is_none());
225 }
226
227 #[test]
228 fn absent_headers_detected() {
229 assert!(SesameHeaders::default().is_absent());
230 }
231
232 #[test]
233 fn error_codes_and_statuses_match_appendix() {
234 assert_eq!(SesameError::ScopeDenied.code(), "sesame_scope_denied");
235 assert_eq!(SesameError::ScopeDenied.http_status(), 403);
236 assert_eq!(SesameError::DecryptFailed.http_status(), 400);
237 assert_eq!(SesameError::InvalidVersion.http_status(), 400);
238 assert_eq!(SesameError::ReplayDetected.http_status(), 401);
239 }
240}