use crate::{
fmt::impl_redacted_fmt,
token::{Error, Result, TokenMaterial},
};
#[must_use]
fn compute_blake3_v1_raw(bytes: &[u8], pepper: &[u8]) -> blake3::Hash {
let mut hasher = blake3::Hasher::new();
hasher.update(pepper);
hasher.update(bytes);
hasher.finalize()
}
const ALG_BLAKE3_V1: u8 = 0;
#[derive(Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TokenHash {
Blake3V1(blake3::Hash),
}
impl TokenHash {
#[must_use]
pub fn compute<T: TokenMaterial>(token: &T, pepper: &[u8]) -> Self {
Self::compute_blake3_v1(token, pepper)
}
#[must_use]
pub fn compute_blake3_v1<T: TokenMaterial>(token: &T, pepper: &[u8]) -> Self {
let hash = compute_blake3_v1_raw(token.as_bytes(), pepper);
Self::Blake3V1(hash)
}
#[must_use]
pub fn verify<T: TokenMaterial>(&self, token: &T, pepper: &[u8]) -> bool {
match self {
Self::Blake3V1(a) => {
let b = compute_blake3_v1_raw(token.as_bytes(), pepper);
*a == b
}
}
}
pub fn to_bytes(&self) -> Vec<u8> {
match &self {
Self::Blake3V1(h) => {
let mut res = Vec::with_capacity(1 + blake3::OUT_LEN);
res.push(ALG_BLAKE3_V1);
res.extend_from_slice(h.as_bytes());
res
}
}
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Err(Error::HashAlgorithmMissing);
}
match data[0] {
ALG_BLAKE3_V1 => Ok(Self::blake3_from_bytes(&data[1..])?),
algo => Err(Error::HashAlgorithmInvalid(algo)),
}
}
#[must_use]
pub fn to_base64(&self) -> String {
crate::encoding::base64::encode(self.to_bytes())
}
pub fn from_base64(data: &str) -> Result<Self> {
let decoded = crate::encoding::base64::decode(data)?;
Self::from_bytes(&decoded)
}
fn blake3_from_bytes(data: &[u8]) -> Result<Self> {
let hash = blake3::Hash::from_slice(data).map_err(|_| Error::LengthMismatch {
expected: blake3::OUT_LEN,
provided: data.len(),
})?;
Ok(Self::Blake3V1(hash))
}
}
impl_redacted_fmt!(TokenHash);
#[cfg(feature = "serde")]
impl serde::Serialize for TokenHash {
fn serialize<S>(&self, serializer: S) -> std::prelude::v1::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if serializer.is_human_readable() {
serializer.serialize_str(&self.to_base64())
} else {
serializer.serialize_bytes(&self.to_bytes())
}
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for TokenHash {
fn deserialize<D>(deserializer: D) -> std::prelude::v1::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
if deserializer.is_human_readable() {
let s = String::deserialize(deserializer)?;
Self::from_base64(&s).map_err(serde::de::Error::custom)
} else {
let bytes = <Vec<u8>>::deserialize(deserializer)?;
Self::from_bytes(&bytes).map_err(serde::de::Error::custom)
}
}
}
#[cfg(feature = "sqlx")]
mod sqlx_impl {
use super::*;
use sqlx::{Database, Decode, Encode, Type, encode::IsNull, error::BoxDynError};
#[cfg(feature = "sqlx_postgres")]
impl sqlx::postgres::PgHasArrayType for TokenHash {
fn array_type_info() -> sqlx::postgres::PgTypeInfo {
<Vec<u8> as sqlx::postgres::PgHasArrayType>::array_type_info()
}
}
impl<'a, DB: Database> Type<DB> for TokenHash
where
&'a [u8]: Type<DB>,
{
fn type_info() -> <DB as Database>::TypeInfo {
<&[u8] as Type<DB>>::type_info()
}
fn compatible(ty: &<DB as Database>::TypeInfo) -> bool {
<&[u8] as Type<DB>>::compatible(ty)
}
}
impl<'a, DB: Database> Encode<'a, DB> for TokenHash
where
Vec<u8>: Encode<'a, DB>,
{
fn encode_by_ref(
&self,
buf: &mut <DB as Database>::ArgumentBuffer<'a>,
) -> std::result::Result<IsNull, BoxDynError> {
<Vec<u8> as Encode<'a, DB>>::encode_by_ref(&self.to_bytes(), buf)
}
fn produces(&self) -> Option<<DB as Database>::TypeInfo> {
<Vec<u8> as Encode<'a, DB>>::produces(&self.to_bytes())
}
fn size_hint(&self) -> usize {
<Vec<u8> as Encode<'a, DB>>::size_hint(&self.to_bytes())
}
}
impl<'a, DB: Database> Decode<'a, DB> for TokenHash
where
Vec<u8>: Decode<'a, DB>,
{
fn decode(value: <DB as Database>::ValueRef<'a>) -> std::result::Result<Self, BoxDynError> {
Ok(Self::from_bytes(&<Vec<u8> as Decode<'a, DB>>::decode(
value,
)?)?)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::GenericToken;
#[test]
fn redacted_debug() {
let token = GenericToken::from_bytes(b"abcdef");
let hash = token.to_default_hash(b"12345");
assert_eq!(format!("{:?}", hash), "<redacted>");
}
#[test]
fn redacted_display() {
let token = GenericToken::from_bytes(b"abcdef");
let hash = token.to_default_hash(b"12345");
assert_eq!(format!("{hash}"), "<redacted>");
}
#[test]
#[cfg(feature = "serde")]
fn serde_human_readable_roundtrip() {
let token = GenericToken::from_bytes(b"abcdef");
let hash = token.to_blake3(b"12345");
let json = serde_json::to_string(&hash).unwrap();
let deserialized = serde_json::from_str::<TokenHash>(&json).unwrap();
assert_eq!(hash, deserialized);
}
#[test]
fn verify_success() {
let token = GenericToken::random(32).unwrap();
let pepper = b"secret-pepper";
let hash = token.to_default_hash(pepper);
assert!(hash.verify(&token, pepper));
}
#[test]
fn verify_wrong_pepper_fail() {
let token = GenericToken::random(32).unwrap();
let hash = token.to_default_hash(b"pepper1");
assert!(!hash.verify(&token, b"pepper2"));
}
#[test]
fn verify_wrong_token_fail() {
let token1 = GenericToken::random(32).unwrap();
let token2 = GenericToken::random(32).unwrap();
let hash = token1.to_default_hash(b"pepper");
assert!(!hash.verify(&token2, b"pepper"));
}
#[test]
fn reject_too_short() {
let data = vec![0, 1, 2];
assert!(TokenHash::from_bytes(&data).is_err());
}
#[test]
fn reject_unknown_algorithm() {
let mut data = vec![255];
data.extend([0u8; 32]);
assert!(matches!(
TokenHash::from_bytes(&data),
Err(Error::HashAlgorithmInvalid(255))
));
}
}