use cratefield_oauth_client::{SealContext, SealError, TokenSealer as _, XChaChaSealer};
use zeroize::Zeroizing;
pub(crate) const KEY_LEN: usize = cratefield_oauth_client::KEY_LEN;
const NAMESPACE: &str = "connections";
pub(crate) struct SealKey {
inner: XChaChaSealer,
}
impl SealKey {
pub(crate) fn from_config(encoded: &str, id: u8) -> Result<Self, String> {
XChaChaSealer::from_base64_key(encoded, id)
.map(|inner| Self { inner })
.map_err(|error| match error {
SealError::Key => {
format!("CONNECTIONS_TOKEN_KEY is not {KEY_LEN} bytes of base64")
}
other => other.to_string(),
})
}
}
pub(crate) fn context<'a>(table: &'a str, row_id: &'a str, column: &'a str) -> SealContext<'a> {
SealContext {
namespace: NAMESPACE,
table,
row_id,
column,
}
}
pub(crate) fn seal(
key: &SealKey,
aad: &SealContext<'_>,
plaintext: &str,
) -> Result<String, SealError> {
key.inner.seal(plaintext, aad)
}
pub(crate) fn open(
key: &SealKey,
aad: &SealContext<'_>,
sealed: &str,
) -> Result<Zeroizing<String>, SealError> {
key.inner.open(sealed, aad)
}
#[cfg(test)]
mod tests {
use super::*;
const KEY: &str = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=";
#[test]
fn round_trips_through_the_crate_sealer() {
let key = SealKey::from_config(KEY, 1).expect("32-byte key");
let aad = context("connection", "conn_1", "access_token_sealed");
let sealed = seal(&key, &aad, "AQXNnd2kXITHELmWblJigb").expect("seals");
assert_eq!(
&*open(&key, &aad, &sealed).expect("opens"),
"AQXNnd2kXITHELmWblJigb"
);
let other = SealKey::from_config(KEY, 2).expect("32-byte key");
assert_eq!(open(&other, &aad, &sealed), Err(SealError::UnknownKeyId(1)));
}
#[test]
fn a_ciphertext_cannot_move_between_rows_or_columns() {
let key = SealKey::from_config(KEY, 1).expect("32-byte key");
let sealed = seal(
&key,
&context("connection", "conn_1", "access_token_sealed"),
"secret",
)
.expect("seals");
for aad in [
context("connection", "conn_2", "access_token_sealed"),
context("connection", "conn_1", "refresh_token_sealed"),
context("connection_state", "conn_1", "access_token_sealed"),
] {
assert_eq!(open(&key, &aad, &sealed), Err(SealError::Authentication));
}
}
#[test]
fn a_bad_key_is_named_for_the_operator() {
let Err(error) = SealKey::from_config("too short", 1) else {
panic!("a short key must not parse");
};
assert_eq!(
error,
format!("CONNECTIONS_TOKEN_KEY is not {KEY_LEN} bytes of base64")
);
}
}