use aion_core::AssistantSessionId;
use sha2::{Digest, Sha256};
pub(crate) const SESSION_TOKEN_SCHEME: &str = "Bearer";
pub(crate) const SESSION_ID_HEADER: &str = "x-aion-assistant-session";
pub(crate) struct MintedSessionToken {
secret: String,
digest: String,
}
impl MintedSessionToken {
pub(crate) fn mint() -> Self {
let secret = format!(
"{}{}",
uuid::Uuid::new_v4().simple(),
uuid::Uuid::new_v4().simple()
);
let digest = digest_of(&secret);
Self { secret, digest }
}
pub(crate) fn secret(&self) -> &str {
&self.secret
}
pub(crate) fn digest(&self) -> &str {
&self.digest
}
}
impl std::fmt::Debug for MintedSessionToken {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("MintedSessionToken")
.field("secret", &"<redacted>")
.field("digest", &self.digest)
.finish()
}
}
pub(crate) fn digest_of(secret: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(secret.as_bytes());
let digest = hasher.finalize();
let mut hex = String::with_capacity(digest.len() * 2);
for byte in digest {
use std::fmt::Write as _;
if write!(hex, "{byte:02x}").is_err() {
return String::new();
}
}
hex
}
#[must_use]
pub(crate) fn matches(presented: &str, stored_digest: &str) -> bool {
let presented = digest_of(presented);
if presented.len() != stored_digest.len() {
return false;
}
presented
.bytes()
.zip(stored_digest.bytes())
.fold(0_u8, |difference, (left, right)| {
difference | (left ^ right)
})
== 0
}
#[must_use]
pub(crate) fn authorization_value(secret: &str) -> String {
format!("{SESSION_TOKEN_SCHEME} {secret}")
}
#[must_use]
pub(crate) fn session_header_value(session_id: AssistantSessionId) -> String {
session_id.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_minted_token_verifies_against_its_own_digest_and_nothing_else() {
let first = MintedSessionToken::mint();
let second = MintedSessionToken::mint();
assert!(matches(first.secret(), first.digest()));
assert!(
!matches(first.secret(), second.digest()),
"one session's token must not verify against another's digest"
);
assert_ne!(
first.secret(),
second.secret(),
"two mints must not collide"
);
}
#[test]
fn the_debug_rendering_carries_the_digest_and_never_the_secret() {
let token = MintedSessionToken::mint();
let rendered = format!("{token:?}");
assert!(
!rendered.contains(token.secret()),
"the token must never reach a log line through Debug"
);
assert!(rendered.contains(token.digest()));
assert!(rendered.contains("<redacted>"));
}
#[test]
fn a_digest_is_sixty_four_lowercase_hex_characters() {
let digest = digest_of("anything");
assert_eq!(digest.len(), 64);
assert!(
digest
.chars()
.all(|character| character.is_ascii_hexdigit())
);
assert!(digest.chars().all(|character| !character.is_uppercase()));
}
#[test]
fn a_mismatched_length_is_refused_rather_than_compared() {
assert!(!matches("token", "short"));
}
}