use std::fmt;
use std::num::NonZeroU32;
use qso_graph_trust_awslc::{aes256_gcm_open, aes256_gcm_seal, pbkdf2_sha256, random};
use zeroize::Zeroizing;
const MAGIC: &[u8; 8] = b"QGT-PART";
const VERSION: u8 = 1;
const HEADER: usize = 41;
const ALPHABET: &[u8; 62] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
pub const ITERATIONS: u32 = 600_000;
const MIN_ITERATIONS: u32 = ITERATIONS;
const MAX_ITERATIONS: u32 = ITERATIONS;
pub const MAX_PLAINTEXT_LEN: usize = 40 * 1024 * 1024;
pub const LOTW_PART: &str = "lotw";
pub struct Password {
secret: Zeroizing<String>,
generated: bool,
}
impl fmt::Debug for Password {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Password(..)")
}
}
impl Password {
pub fn generate() -> Result<Self, ExportError> {
let mut secret = Zeroizing::new(String::with_capacity(20));
let mut bytes = Zeroizing::new(vec![0; 32]);
while secret.len() < 20 {
random(&mut bytes).map_err(|_| ExportError::Random)?;
for &byte in bytes.iter() {
if byte < 248 {
secret.push(char::from(ALPHABET[usize::from(byte % 62)]));
if secret.len() == 20 {
break;
}
}
}
}
Ok(Self {
secret,
generated: true,
})
}
pub fn from_typed(text: &str) -> Result<Self, ExportError> {
if text.len() < 20 || !text.bytes().all(|b| b.is_ascii_alphanumeric()) {
return Err(ExportError::Password);
}
Ok(Self {
secret: Zeroizing::new(text.to_owned()),
generated: false,
})
}
pub fn expose_secret(&self) -> &str {
&self.secret
}
pub(crate) fn require_generated(&self) -> Result<(), ExportError> {
if self.generated {
Ok(())
} else {
Err(ExportError::Password)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExportError {
Open,
Password,
Random,
Seal,
}
impl fmt::Display for ExportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Open => "the export part could not be opened",
Self::Password => "a valid generated export password is required for sealing; opening requires at least 20 ASCII letters or digits",
Self::Random => "secure randomness is unavailable",
Self::Seal => "the export part could not be sealed",
})
}
}
impl std::error::Error for ExportError {}
fn aad(part_name: &str, header: &[u8]) -> Vec<u8> {
let mut aad = Vec::with_capacity(29 + part_name.len());
aad.extend_from_slice(&header[..9]);
aad.extend_from_slice(part_name.as_bytes());
aad.extend_from_slice(&header[9..29]);
aad
}
pub(crate) fn seal(
part_name: &str,
plaintext: &[u8],
password: &Password,
) -> Result<Vec<u8>, ExportError> {
crate::memory::scrub_after(|| seal_inner(part_name, plaintext, password))
}
#[inline(never)]
fn seal_inner(
part_name: &str,
plaintext: &[u8],
password: &Password,
) -> Result<Vec<u8>, ExportError> {
password.require_generated()?;
if plaintext.len() > MAX_PLAINTEXT_LEN {
return Err(ExportError::Seal);
}
let mut header = [0; HEADER];
header[..8].copy_from_slice(MAGIC);
header[8] = VERSION;
header[9..13].copy_from_slice(&ITERATIONS.to_be_bytes());
random(&mut header[13..]).map_err(|_| ExportError::Random)?;
let key = pbkdf2_sha256(
password.secret.as_bytes(),
&header[13..29],
NonZeroU32::new(ITERATIONS).expect("nonzero count"),
);
let ciphertext = aes256_gcm_seal(
&key,
header[29..41].try_into().expect("nonce"),
&aad(part_name, &header),
plaintext,
)
.map_err(|_| ExportError::Seal)?;
let mut out = Vec::with_capacity(HEADER + ciphertext.len());
out.extend_from_slice(&header);
out.extend_from_slice(&ciphertext);
Ok(out)
}
pub(crate) fn open(
part_name: &str,
bytes: &[u8],
password: &Password,
) -> Result<Zeroizing<Vec<u8>>, ExportError> {
crate::memory::scrub_after(|| open_with(part_name, bytes, password, pbkdf2_sha256))
}
#[inline(never)]
fn open_with(
part_name: &str,
bytes: &[u8],
password: &Password,
derive: impl FnOnce(&[u8], &[u8], NonZeroU32) -> Zeroizing<Vec<u8>>,
) -> Result<Zeroizing<Vec<u8>>, ExportError> {
if bytes.len() < HEADER + 16
|| bytes.len() > HEADER + 16 + MAX_PLAINTEXT_LEN
|| &bytes[..8] != MAGIC
|| bytes[8] != VERSION
{
return Err(ExportError::Open);
}
let iterations = u32::from_be_bytes(bytes[9..13].try_into().expect("count"));
if !(MIN_ITERATIONS..=MAX_ITERATIONS).contains(&iterations) {
return Err(ExportError::Open);
}
let key = derive(
password.secret.as_bytes(),
&bytes[13..29],
NonZeroU32::new(iterations).ok_or(ExportError::Open)?,
);
aes256_gcm_open(
&key,
bytes[29..41].try_into().expect("nonce"),
&aad(part_name, bytes),
&bytes[HEADER..],
)
.map_err(|_| ExportError::Open)
}
#[cfg(test)]
mod tests;