use crate::crypto_secretstream::{Key, PullState, PushState, SecretstreamError, Tag};
use crate::hazmat::dstu9041::curve256::{base_point, is_valid_scalar, point_from_x, Point};
use crate::hazmat::dstu9041::encryption::{
decrypt as dstu9041_decrypt, encrypt as dstu9041_encrypt,
};
use crate::hazmat::dstu9041::fp256::from_candidate_bytes;
use crate::hazmat::dstu9041::message::L_MAX_P;
use crate::hazmat::kupyna_kdf::Kupyna256Kdf;
use crate::randombytes::{randombytes_buf, RandomError};
use core::fmt;
use zeroize::Zeroize;
const SEED_LEN: usize = L_MAX_P / 8;
const KEM_CIPHERTEXT_LEN: usize = 128;
const HEADER_LEN: usize = 32;
const TAG_LEN: usize = 16;
const KDF_CONTEXT: &[u8; 8] = b"cryptbox";
#[derive(Debug)]
pub enum SealError {
Random(RandomError),
}
impl fmt::Display for SealError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SealError::Random(e) => write!(f, "{e}"),
}
}
}
impl core::error::Error for SealError {}
impl From<RandomError> for SealError {
fn from(e: RandomError) -> Self {
SealError::Random(e)
}
}
#[derive(Debug)]
pub enum OpenError {
Truncated,
InvalidCiphertext,
}
impl fmt::Display for OpenError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OpenError::Truncated => write!(f, "input too short to contain a sealed message"),
OpenError::InvalidCiphertext => write!(f, "authentication failed"),
}
}
}
impl core::error::Error for OpenError {}
fn random_valid_scalar() -> Result<[u8; 32], RandomError> {
loop {
let mut candidate = [0u8; 32];
randombytes_buf(&mut candidate)?;
if is_valid_scalar(&candidate) {
return Ok(candidate);
}
candidate.zeroize();
}
}
fn embed_seed(seed: &[u8; SEED_LEN]) -> [u8; 32] {
let mut embedded = [0u8; 32];
embedded[32 - SEED_LEN..].copy_from_slice(seed);
embedded
}
pub struct SecretKey([u8; 32]);
impl Drop for SecretKey {
fn drop(&mut self) {
self.0.zeroize();
}
}
impl SecretKey {
#[must_use]
pub fn from_bytes(e: &[u8; 32]) -> Option<Self> {
if is_valid_scalar(e) {
Some(SecretKey(*e))
} else {
None
}
}
#[cfg(any(feature = "std", feature = "getrandom"))]
pub fn generate() -> Result<Self, RandomError> {
random_valid_scalar().map(SecretKey)
}
#[must_use]
pub fn to_bytes(&self) -> [u8; 32] {
self.0
}
#[must_use]
pub fn public_key(&self) -> PublicKey {
PublicKey(base_point().scalar_multiply(&self.0))
}
}
#[derive(Clone, Copy)]
pub struct PublicKey(Point);
impl PublicKey {
#[must_use]
pub fn from_bytes(bytes: &[u8; 32]) -> Option<Self> {
from_candidate_bytes(bytes)
.and_then(point_from_x)
.map(PublicKey)
}
#[must_use]
pub fn to_bytes(&self) -> [u8; 32] {
self.0.x.to_be_bytes()
}
}
pub fn seal(message: &[u8], recipient: &PublicKey) -> Result<Vec<u8>, SealError> {
let mut seed = [0u8; SEED_LEN];
randombytes_buf(&mut seed)?;
let mut epsilon = random_valid_scalar()?;
let Ok(kem_ciphertext) = dstu9041_encrypt(&seed, L_MAX_P, recipient.0, &epsilon) else {
unreachable!("seed/epsilon are always valid by construction")
};
epsilon.zeroize();
let mut embedded = embed_seed(&seed);
seed.zeroize();
let mut stream_key_bytes = Kupyna256Kdf::derive_subkey(&embedded, 0, KDF_CONTEXT);
embedded.zeroize();
let key = Key::from_bytes(stream_key_bytes);
stream_key_bytes.zeroize();
let (mut push, header) = match PushState::init(&key) {
Ok(ok) => ok,
Err(SecretstreamError::Random(e)) => return Err(SealError::Random(e)),
Err(_) => unreachable!("PushState::init only ever fails via SecretstreamError::Random"),
};
let mut ciphertext = vec![0u8; message.len()];
let Ok(tag) = push.push(Tag::Final, message, &mut ciphertext) else {
unreachable!(
"ciphertext.len() == message.len() by construction; stream freshly initialized, \
never finalized yet"
)
};
let mut out = Vec::with_capacity(KEM_CIPHERTEXT_LEN + HEADER_LEN + ciphertext.len() + TAG_LEN);
out.extend_from_slice(&kem_ciphertext);
out.extend_from_slice(&header);
out.extend_from_slice(&ciphertext);
out.extend_from_slice(&tag);
Ok(out)
}
pub fn open(sealed: &[u8], secret: &SecretKey) -> Result<Vec<u8>, OpenError> {
const MIN_LEN: usize = KEM_CIPHERTEXT_LEN + HEADER_LEN + TAG_LEN;
if sealed.len() < MIN_LEN {
return Err(OpenError::Truncated);
}
let mut kem_ciphertext = [0u8; KEM_CIPHERTEXT_LEN];
kem_ciphertext.copy_from_slice(&sealed[..KEM_CIPHERTEXT_LEN]);
let mut header = [0u8; HEADER_LEN];
header.copy_from_slice(&sealed[KEM_CIPHERTEXT_LEN..KEM_CIPHERTEXT_LEN + HEADER_LEN]);
let ciphertext_len = sealed.len() - MIN_LEN;
let ciphertext_start = KEM_CIPHERTEXT_LEN + HEADER_LEN;
let ciphertext = &sealed[ciphertext_start..ciphertext_start + ciphertext_len];
let tag = &sealed[ciphertext_start + ciphertext_len..];
let (mut seed_padded, bit_len) =
dstu9041_decrypt(&kem_ciphertext, &secret.0).map_err(|_| OpenError::InvalidCiphertext)?;
if bit_len != L_MAX_P {
seed_padded.zeroize();
return Err(OpenError::InvalidCiphertext);
}
let mut embedded = embed_seed(&seed_padded);
seed_padded.zeroize();
let mut stream_key_bytes = Kupyna256Kdf::derive_subkey(&embedded, 0, KDF_CONTEXT);
embedded.zeroize();
let key = Key::from_bytes(stream_key_bytes);
stream_key_bytes.zeroize();
let mut pull = PullState::init(&key, &header);
let mut plaintext = vec![0u8; ciphertext_len];
pull.pull(Tag::Final.to_byte(), ciphertext, tag, &mut plaintext)
.map_err(|_| OpenError::InvalidCiphertext)?;
Ok(plaintext)
}