use std::sync::OnceLock;
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use chacha20poly1305::{
KeyInit, XChaCha20Poly1305, XNonce,
aead::{Aead, Payload},
};
use rand::RngCore;
const NONCE_LEN: usize = 24;
fn cipher() -> &'static XChaCha20Poly1305 {
static CIPHER: OnceLock<XChaCha20Poly1305> = OnceLock::new();
CIPHER.get_or_init(|| {
let mut key = [0_u8; 32];
rand::rng().fill_bytes(&mut key);
XChaCha20Poly1305::new(&key.into())
})
}
#[must_use]
pub fn seal(position: i64, stream: &str) -> Option<String> {
let mut nonce = [0_u8; NONCE_LEN];
rand::rng().fill_bytes(&mut nonce);
let sealed = cipher()
.encrypt(
XNonce::from_slice(&nonce),
Payload {
msg: &position.to_be_bytes(),
aad: stream.as_bytes(),
},
)
.ok()?;
let mut token = nonce.to_vec();
token.extend_from_slice(&sealed);
Some(URL_SAFE_NO_PAD.encode(token))
}
#[must_use]
pub fn open(token: &str, stream: &str) -> Option<i64> {
let bytes = URL_SAFE_NO_PAD.decode(token).ok()?;
if bytes.len() <= NONCE_LEN {
return None;
}
let (nonce, sealed) = bytes.split_at(NONCE_LEN);
let position = cipher()
.decrypt(
XNonce::from_slice(nonce),
Payload {
msg: sealed,
aad: stream.as_bytes(),
},
)
.ok()?;
Some(i64::from_be_bytes(position.try_into().ok()?))
}