use liminal_protocol::wire::{DetachRequest, EnrollmentToken, LeaveRequest};
pub type Digest = [u8; 32];
#[derive(Debug, thiserror::Error)]
pub enum FactsError {
#[error("failed to read entropy from {ENTROPY_SOURCE}: {0}")]
Entropy(std::io::Error),
#[error("system clock is outside the representable millisecond domain")]
Clock,
}
const ENTROPY_SOURCE: &str = "/dev/urandom";
pub fn mint_secret_bytes() -> Result<[u8; 32], FactsError> {
use std::io::Read;
let mut bytes = [0_u8; 32];
let mut source = std::fs::File::open(ENTROPY_SOURCE).map_err(FactsError::Entropy)?;
source.read_exact(&mut bytes).map_err(FactsError::Entropy)?;
Ok(bytes)
}
pub fn now_unix_millis() -> Result<u64, FactsError> {
use std::time::{SystemTime, UNIX_EPOCH};
let elapsed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| FactsError::Clock)?;
u64::try_from(elapsed.as_millis()).map_err(|_| FactsError::Clock)
}
#[must_use]
pub fn enrollment_fingerprint(token: EnrollmentToken) -> Digest {
let mut digest = [0_u8; 32];
let bytes = token.into_bytes();
if let Some(prefix) = digest.get_mut(..bytes.len()) {
prefix.copy_from_slice(&bytes);
}
digest
}
#[must_use]
pub fn detach_request_verifier(request: &DetachRequest) -> Digest {
let mut digest = [0_u8; 32];
let token = request.detach_attempt_token.into_bytes();
let generation = request.capability_generation.get().to_be_bytes();
let participant = request.participant_id.to_be_bytes();
for (slot, byte) in digest.iter_mut().zip(
token
.iter()
.chain(generation.iter())
.chain(participant.iter()),
) {
*slot = *byte;
}
digest
}
#[must_use]
pub fn leave_request_verifier(request: &LeaveRequest) -> Digest {
let mut hasher = blake3::Hasher::new();
hasher.update(b"liminal/leave/secret-verifier/v2");
hasher.update(&request.attach_secret.into_bytes());
*hasher.finalize().as_bytes()
}
#[must_use]
pub fn leave_fingerprint(request: &LeaveRequest) -> Digest {
leave_digest(b"liminal/leave/fingerprint/v2", request)
}
fn leave_digest(domain: &[u8], request: &LeaveRequest) -> Digest {
let mut hasher = blake3::Hasher::new();
hasher.update(domain);
hasher.update(&request.conversation_id.to_be_bytes());
hasher.update(&request.participant_id.to_be_bytes());
hasher.update(&request.capability_generation.get().to_be_bytes());
hasher.update(&request.attach_secret.into_bytes());
hasher.update(&request.leave_attempt_token.into_bytes());
*hasher.finalize().as_bytes()
}
#[must_use]
pub fn constant_time_eq(expected: &[u8], candidate: &[u8]) -> bool {
let mut difference = u8::from(expected.len() != candidate.len());
for (left, right) in expected.iter().zip(candidate.iter()) {
difference |= left ^ right;
}
difference == 0
}