use base64::engine::general_purpose::STANDARD_NO_PAD;
use base64::Engine as _;
pub const TAG: &str = "gunnar-crypt user-key wrap v2";
pub const INFO: &[u8] = b"gunnar-crypt user-key wrap v2";
pub const SALT_BYTES: usize = 32;
pub const NONCE_BYTES: usize = 24;
pub const TAG_BYTES: usize = 16;
pub const MAX_SEALED: usize = 4 << 10;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Parts {
pub salt: [u8; SALT_BYTES],
pub nonce: [u8; NONCE_BYTES],
pub sealed: Vec<u8>,
}
pub fn is_v2(text: &str) -> bool {
text.trim_start().starts_with(TAG)
}
pub fn parse(text: &str) -> Option<Parts> {
let rest = text.trim().strip_prefix(TAG)?.strip_prefix(' ')?;
let words: Vec<&str> = rest.split(' ').collect();
let [salt, nonce, sealed] = words.as_slice() else {
return None;
};
let salt: [u8; SALT_BYTES] = STANDARD_NO_PAD.decode(salt).ok()?.try_into().ok()?;
let nonce: [u8; NONCE_BYTES] = STANDARD_NO_PAD.decode(nonce).ok()?.try_into().ok()?;
if sealed.len() > MAX_SEALED * 2 {
return None;
}
let sealed = STANDARD_NO_PAD.decode(sealed).ok()?;
(sealed.len() > TAG_BYTES && sealed.len() <= MAX_SEALED).then_some(Parts {
salt,
nonce,
sealed,
})
}
pub fn format(parts: &Parts) -> String {
format!(
"{TAG} {} {} {}\n",
STANDARD_NO_PAD.encode(parts.salt),
STANDARD_NO_PAD.encode(parts.nonce),
STANDARD_NO_PAD.encode(&parts.sealed)
)
}
pub fn aad(credential_id: &[u8]) -> Vec<u8> {
let mut a = Vec::with_capacity(INFO.len() + 1 + credential_id.len());
a.extend_from_slice(INFO);
a.push(0);
a.extend_from_slice(credential_id);
a
}