pub const H_VERSION: &str = "X-SESAME-Version";
pub const H_KEY_ID: &str = "X-SESAME-KeyId";
pub const H_TIMESTAMP: &str = "X-SESAME-Timestamp";
pub const H_NONCE: &str = "X-SESAME-Nonce";
pub const H_SIGNATURE: &str = "X-SESAME-Signature";
pub const H_SCOPE: &str = "X-SESAME-Scope";
pub const H_ENCRYPTED: &str = "X-SESAME-Encrypted";
pub const H_ENC_KEY_ID: &str = "X-SESAME-EncKeyId";
pub const H_IV: &str = "X-SESAME-IV";
pub const PROTOCOL_VERSION: &str = "1.0";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SesameError {
MissingHeaders,
InvalidVersion,
UnknownKey,
ExpiredTimestamp,
ReplayDetected,
SignatureMismatch,
ScopeDenied,
DecryptFailed,
KeyRevoked,
}
impl SesameError {
pub fn code(&self) -> &'static str {
match self {
SesameError::MissingHeaders => "sesame_missing_headers",
SesameError::InvalidVersion => "sesame_invalid_version",
SesameError::UnknownKey => "sesame_unknown_key",
SesameError::ExpiredTimestamp => "sesame_expired_timestamp",
SesameError::ReplayDetected => "sesame_replay_detected",
SesameError::SignatureMismatch => "sesame_signature_mismatch",
SesameError::ScopeDenied => "sesame_scope_denied",
SesameError::DecryptFailed => "sesame_decrypt_failed",
SesameError::KeyRevoked => "sesame_key_revoked",
}
}
pub fn http_status(&self) -> u16 {
match self {
SesameError::InvalidVersion | SesameError::DecryptFailed => 400,
SesameError::ScopeDenied => 403,
_ => 401,
}
}
}
impl core::fmt::Display for SesameError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.code())
}
}
impl std::error::Error for SesameError {}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SesameHeaders {
pub version: Option<String>,
pub key_id: Option<String>,
pub timestamp: Option<String>,
pub nonce: Option<String>,
pub signature: Option<String>,
pub scope: Option<String>,
pub encrypted: bool,
pub enc_key_id: Option<String>,
pub iv: Option<String>,
}
impl SesameHeaders {
pub fn is_absent(&self) -> bool {
self.version.is_none()
&& self.key_id.is_none()
&& self.timestamp.is_none()
&& self.nonce.is_none()
&& self.signature.is_none()
}
pub fn from_lookup<F>(get: F) -> Self
where
F: Fn(&str) -> Option<String>,
{
let encrypted = get(H_ENCRYPTED)
.map(|v| v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
SesameHeaders {
version: get(H_VERSION),
key_id: get(H_KEY_ID),
timestamp: get(H_TIMESTAMP),
nonce: get(H_NONCE),
signature: get(H_SIGNATURE),
scope: get(H_SCOPE),
encrypted,
enc_key_id: get(H_ENC_KEY_ID),
iv: get(H_IV),
}
}
pub fn require_tier1(&self) -> Result<Tier1Fields<'_>, SesameError> {
match (
self.version.as_deref(),
self.key_id.as_deref(),
self.timestamp.as_deref(),
self.nonce.as_deref(),
self.signature.as_deref(),
) {
(Some(version), Some(key_id), Some(timestamp), Some(nonce), Some(signature)) => {
Ok(Tier1Fields {
version,
key_id,
timestamp,
nonce,
signature,
})
}
_ => Err(SesameError::MissingHeaders),
}
}
}
pub struct Tier1Fields<'a> {
pub version: &'a str,
pub key_id: &'a str,
pub timestamp: &'a str,
pub nonce: &'a str,
pub signature: &'a str,
}
pub fn hex_encode(bytes: &[u8]) -> String {
const LUT: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for &b in bytes {
out.push(LUT[(b >> 4) as usize] as char);
out.push(LUT[(b & 0x0f) as usize] as char);
}
out
}
pub fn hex_decode(s: &str) -> Option<Vec<u8>> {
if s.len() % 2 != 0 {
return None;
}
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(s.len() / 2);
let val = |c: u8| -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
};
let mut i = 0;
while i < bytes.len() {
let hi = val(bytes[i])?;
let lo = val(bytes[i + 1])?;
out.push((hi << 4) | lo);
i += 2;
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_roundtrip() {
let data = [0x00u8, 0x0f, 0xa1, 0xff, 0x10];
assert_eq!(hex_encode(&data), "000fa1ff10");
assert_eq!(hex_decode("000fa1ff10").unwrap(), data);
}
#[test]
fn hex_decode_rejects_odd_and_nonhex() {
assert!(hex_decode("abc").is_none());
assert!(hex_decode("zz").is_none());
}
#[test]
fn absent_headers_detected() {
assert!(SesameHeaders::default().is_absent());
}
#[test]
fn error_codes_and_statuses_match_appendix() {
assert_eq!(SesameError::ScopeDenied.code(), "sesame_scope_denied");
assert_eq!(SesameError::ScopeDenied.http_status(), 403);
assert_eq!(SesameError::DecryptFailed.http_status(), 400);
assert_eq!(SesameError::InvalidVersion.http_status(), 400);
assert_eq!(SesameError::ReplayDetected.http_status(), 401);
}
}