use base64::Engine;
use serde_json::Value;
use sha2::{Digest, Sha256};
use rand::Rng;
use super::error::DisclosureError;
pub const SD_HASH_ALG: &str = "sha-256";
const SALT_BYTES: usize = 16;
const RESERVED: [&str; 2] = ["_sd", "..."];
fn b64() -> base64::engine::general_purpose::GeneralPurpose {
base64::engine::general_purpose::URL_SAFE_NO_PAD
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Disclosure {
salt: String,
claim_name: String,
claim_value: Value,
encoded: String,
}
impl Disclosure {
pub fn new(claim_name: impl Into<String>, claim_value: Value) -> Result<Self, DisclosureError> {
let mut salt_bytes = [0u8; SALT_BYTES];
crate::os_rng().fill_bytes(&mut salt_bytes);
Self::with_salt(b64().encode(salt_bytes), claim_name, claim_value)
}
pub fn with_salt(
salt: String,
claim_name: impl Into<String>,
claim_value: Value,
) -> Result<Self, DisclosureError> {
let claim_name = claim_name.into();
if RESERVED.contains(&claim_name.as_str()) {
return Err(DisclosureError::ReservedClaimName);
}
let encoded = b64().encode(
Value::Array(vec![
Value::String(salt.clone()),
Value::String(claim_name.clone()),
claim_value.clone(),
])
.to_string(),
);
Ok(Self {
salt,
claim_name,
claim_value,
encoded,
})
}
pub fn parse(encoded: &str) -> Result<Self, DisclosureError> {
let bytes = b64()
.decode(encoded)
.map_err(|_| DisclosureError::NotBase64)?;
let parsed: Value = serde_json::from_slice(&bytes).map_err(|_| DisclosureError::NotJson)?;
let Some(items) = parsed.as_array() else {
return Err(DisclosureError::NotATriple);
};
let [salt, name, value] = items.as_slice() else {
return Err(DisclosureError::NotATriple);
};
let (Some(salt), Some(name)) = (salt.as_str(), name.as_str()) else {
return Err(DisclosureError::NotATriple);
};
if RESERVED.contains(&name) {
return Err(DisclosureError::ReservedClaimName);
}
Ok(Self {
salt: salt.to_owned(),
claim_name: name.to_owned(),
claim_value: value.clone(),
encoded: encoded.to_owned(),
})
}
pub fn encoded(&self) -> &str {
&self.encoded
}
pub fn salt(&self) -> &str {
&self.salt
}
pub fn claim_name(&self) -> &str {
&self.claim_name
}
pub fn claim_value(&self) -> &Value {
&self.claim_value
}
pub fn digest(&self) -> String {
digest_of(&self.encoded)
}
}
pub fn digest_of(encoded: &str) -> String {
b64().encode(Sha256::digest(encoded.as_bytes()))
}