use std::fmt;
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OpaqueId {
pub(crate) id: String,
}
impl OpaqueId {
#[must_use]
pub fn new(db_id: impl Into<String>) -> Self {
let id_str = db_id.into();
let encoded = URL_SAFE_NO_PAD.encode(id_str.as_bytes());
Self { id: encoded }
}
#[must_use]
pub fn with_signature(db_id: impl Into<String>, secret: &[u8]) -> Self {
let id_str = db_id.into();
let mut hasher = Sha256::new();
hasher.update(id_str.as_bytes());
hasher.update(secret);
let signature = URL_SAFE_NO_PAD.encode(hasher.finalize());
let opaque = format!("{}|{}", id_str, signature);
let encoded = URL_SAFE_NO_PAD.encode(opaque.as_bytes());
Self { id: encoded }
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.id
}
#[must_use]
pub fn decode(&self) -> Option<String> {
URL_SAFE_NO_PAD
.decode(&self.id)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
}
#[must_use]
pub fn verify_signature(&self, secret: &[u8]) -> bool {
let Some(decoded) = self.decode() else {
return false;
};
let Some((db_id, provided_sig)) = decoded.split_once('|') else {
return false;
};
let mut hasher = Sha256::new();
hasher.update(db_id.as_bytes());
hasher.update(secret);
let expected_sig = URL_SAFE_NO_PAD.encode(hasher.finalize());
constant_time_eq(provided_sig.as_bytes(), expected_sig.as_bytes())
}
}
impl fmt::Display for OpaqueId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.id)
}
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
result |= x ^ y;
}
result == 0
}