use core::future::Future;
use vitaminc_protected::OpaqueDebug;
use crate::seal::{AsyncCipher, Cipher, Node};
use crate::typed::{DecodeError, Decrypt};
use crate::walk::DecryptError;
use crate::{Context, ContextPiece, Encrypt, EncryptError, IntoAad, IntoContext};
fn fold_tag_aad<'a, Tag, A>(tag: Tag, extra_aad: A) -> (A, ContextPiece<'a>)
where
Tag: IntoContext<'static>,
{
let tag: ContextPiece<'static> = tag.into_context();
let tag: ContextPiece<'a> = tag;
(extra_aad, tag)
}
#[derive(Clone, OpaqueDebug)]
pub struct ContextTag<Tag, T> {
inner: T,
#[non_sensitive]
tag: Tag,
}
impl<Tag, T> ContextTag<Tag, T> {
pub fn new(inner: T, tag: Tag) -> Self {
ContextTag { inner, tag }
}
pub fn refine<B>(self, tag: B) -> ContextTag<(Tag, B), T> {
ContextTag {
inner: self.inner,
tag: (self.tag, tag),
}
}
pub fn into_parts(self) -> (T, Tag) {
(self.inner, self.tag)
}
}
impl<Tag, T> ContextTag<Tag, T>
where
T: Encrypt,
Tag: IntoAad<'static>,
{
pub fn encrypt<C: Cipher>(self, cipher: &C) -> Result<Node<C::Leaf>, EncryptError<C::Error>> {
self.encrypt_with_aad(cipher, Context::empty())
}
pub fn encrypt_with_aad<'a, C, A>(
self,
cipher: &C,
extra_aad: A,
) -> Result<Node<C::Leaf>, EncryptError<C::Error>>
where
C: Cipher,
A: IntoAad<'a>,
{
let ContextTag { inner, tag } = self;
crate::encrypt(inner, cipher, fold_tag_aad(tag, extra_aad))
}
pub fn encrypt_async<'c, C>(
self,
cipher: &'c C,
) -> impl Future<Output = Result<Node<C::Leaf>, EncryptError<C::Error>>>
+ Send
+ 'c
+ use<'c, Tag, T, C>
where
C: AsyncCipher,
{
self.encrypt_with_aad_async(cipher, Context::empty())
}
pub fn encrypt_with_aad_async<'a, 'c, C, A>(
self,
cipher: &'c C,
extra_aad: A,
) -> impl Future<Output = Result<Node<C::Leaf>, EncryptError<C::Error>>>
+ Send
+ 'c
+ use<'a, 'c, Tag, T, C, A>
where
C: AsyncCipher,
A: IntoAad<'a>,
{
let ContextTag { inner, tag } = self;
crate::encrypt_async(inner, cipher, fold_tag_aad(tag, extra_aad))
}
}
impl<Tag> ContextTag<Tag, ()> {
pub fn aad(tag: Tag) -> (Context<'static>, Tag) {
(Context::empty(), tag)
}
pub fn aad_with<A>(extra_aad: A, tag: Tag) -> (A, Tag) {
(extra_aad, tag)
}
pub fn context(tag: Tag) -> Self {
ContextTag { inner: (), tag }
}
}
impl<Tag> ContextTag<Tag, ()>
where
Tag: IntoAad<'static>,
{
pub fn decrypt<T, C>(
self,
cipher: &C,
node: Node<C::Leaf>,
) -> Result<T, DecryptError<DecodeError, C::Error>>
where
T: Decrypt,
C: Cipher,
{
self.decrypt_with_aad(cipher, node, Context::empty())
}
pub fn decrypt_with_aad<'a, T, C, A>(
self,
cipher: &C,
node: Node<C::Leaf>,
extra_aad: A,
) -> Result<T, DecryptError<DecodeError, C::Error>>
where
T: Decrypt,
C: Cipher,
A: IntoAad<'a>,
{
crate::decrypt(cipher, node, fold_tag_aad(self.tag, extra_aad))
}
pub async fn decrypt_async<T, C>(
self,
cipher: &C,
node: Node<C::Leaf>,
) -> Result<T, DecryptError<DecodeError, C::Error>>
where
T: Decrypt,
C: AsyncCipher,
{
self.decrypt_with_aad_async(cipher, node, Context::empty())
.await
}
pub async fn decrypt_with_aad_async<'a, T, C, A>(
self,
cipher: &C,
node: Node<C::Leaf>,
extra_aad: A,
) -> Result<T, DecryptError<DecodeError, C::Error>>
where
T: Decrypt,
C: AsyncCipher,
A: IntoAad<'a>,
{
crate::typed::decrypt_async(cipher, node, fold_tag_aad(self.tag, extra_aad)).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::seal::mock::{Leaf, Mock, Refused};
use crate::{tags, CipherText};
use std::borrow::Cow;
fn sealed<Tag: IntoAad<'static>>(tagged: ContextTag<Tag, &str>) -> Leaf {
match tagged.encrypt(&Mock).expect("the mock cannot fail") {
CipherText::Single(leaf) => leaf,
other => panic!("a scalar seals as one leaf, got {other:?}"),
}
}
fn sealed_with<'a, Tag: IntoAad<'static>, A: IntoAad<'a>>(
tagged: ContextTag<Tag, &str>,
extra: A,
) -> Leaf {
match tagged
.encrypt_with_aad(&Mock, extra)
.expect("the mock cannot fail")
{
CipherText::Single(leaf) => leaf,
other => panic!("a scalar seals as one leaf, got {other:?}"),
}
}
fn bytes_of<'a>(aad: impl IntoAad<'a>) -> Vec<u8> {
aad.into_aad().as_bytes().to_vec()
}
#[test]
fn the_context_keeps_its_parts() {
let folded = fold_tag_aad(("table:users", "column:email"), "row:99");
let piece = folded.into_context();
assert_eq!(
piece,
ContextPiece::List(vec![
ContextPiece::Text(Cow::Borrowed("row:99")),
ContextPiece::List(vec![
ContextPiece::Text(Cow::Borrowed("table:users")),
ContextPiece::Text(Cow::Borrowed("column:email")),
]),
])
);
let names: Vec<String> = piece.leaves().map(ToString::to_string).collect();
assert_eq!(names, ["\"row:99\"", "\"table:users\"", "\"column:email\""]);
let expected = ("row:99", ("table:users", "column:email")).into_aad();
assert_eq!(piece.into_aad().as_bytes(), expected.as_bytes());
}
#[test]
fn encrypts_inner_value_and_binds_tag() {
let leaf = sealed_with(ContextTag::new("hello world", "tag_aad"), "extra");
assert_eq!(
leaf.plaintext,
[&[tags::STRING][..], b"hello world"].concat()
);
assert_eq!(leaf.aad, bytes_of(("extra", "tag_aad")));
}
#[test]
fn encrypt_with_no_extra_aad_still_binds_tag() {
let leaf = sealed(ContextTag::new("secret", "user:42"));
assert_eq!(leaf.aad, bytes_of(((), "user:42")));
}
#[test]
fn unit_tag_still_binds_nonempty_aad() {
let leaf = sealed(ContextTag::new("secret", ()));
assert_eq!(leaf.aad, bytes_of(((), ())));
assert_ne!(leaf.aad, Context::empty().as_bytes());
}
#[test]
fn refine_nests_the_tag() {
let leaf = sealed_with(
ContextTag::new("secret", "table:users").refine("column:email"),
"extra",
);
assert_eq!(
leaf.aad,
bytes_of(("extra", ("table:users", "column:email")))
);
}
#[test]
fn chained_refine_builds_left_nested_tuple() {
let leaf = sealed(ContextTag::new("data", "a").refine("b").refine("c"));
assert_eq!(leaf.aad, bytes_of(((), (("a", "b"), "c"))));
}
#[test]
fn owned_and_byte_tags_are_accepted() {
let leaf = sealed(ContextTag::new("secret", String::from("owned-tag")));
assert_eq!(leaf.aad, bytes_of(((), "owned-tag")));
let leaf = sealed(ContextTag::new("secret", vec![0xde_u8, 0xad, 0xbe, 0xef]));
assert_eq!(leaf.aad, bytes_of(((), vec![0xde_u8, 0xad, 0xbe, 0xef])));
}
#[test]
fn aad_helpers_match_the_encrypt_binding() {
let leaf = sealed(ContextTag::new("secret", "user:42"));
assert_eq!(leaf.aad, bytes_of(ContextTag::aad("user:42")));
let leaf = sealed_with(ContextTag::new("secret", "table:users"), "row:99");
assert_eq!(
leaf.aad,
bytes_of(ContextTag::aad_with("row:99", "table:users"))
);
}
#[test]
fn different_tags_produce_different_aad() {
let a = sealed(ContextTag::new("secret", "user:42"));
let b = sealed(ContextTag::new("secret", "user:99"));
assert_ne!(a.aad, b.aad);
}
#[test]
fn into_parts_round_trips() {
let tagged = ContextTag::new("secret", "ctx");
let (inner, tag) = tagged.into_parts();
assert_eq!(inner, "secret");
assert_eq!(tag, "ctx");
}
#[test]
fn decrypt_mirrors_encrypt_through_the_same_fold() {
let node = ContextTag::new("secret".to_string(), "table:users")
.refine("column:email")
.encrypt_with_aad(&Mock, "row:99")
.expect("encrypts");
let back: String = ContextTag::context("table:users")
.refine("column:email")
.decrypt_with_aad(&Mock, node, "row:99")
.expect("decrypts");
assert_eq!(back, "secret");
let node = ContextTag::new(vec![1u32, 2], "user:42")
.encrypt(&Mock)
.expect("encrypts");
let back: Vec<u32> = ContextTag::context("user:42")
.decrypt(&Mock, node)
.expect("decrypts");
assert_eq!(back, [1, 2]);
}
#[test]
fn a_different_tag_or_extra_aad_fails_at_the_cipher() {
let seal = || {
ContextTag::new("secret".to_string(), "table:users")
.encrypt_with_aad(&Mock, "row:99")
.expect("encrypts")
};
for attempt in [
ContextTag::context("table:orders").decrypt_with_aad::<String, _, _>(
&Mock,
seal(),
"row:99",
),
ContextTag::context("table:users").decrypt_with_aad::<String, _, _>(
&Mock,
seal(),
"row:100",
),
ContextTag::context("row:99").decrypt_with_aad::<String, _, _>(
&Mock,
seal(),
"table:users",
),
crate::decrypt::<String, _, _>(&Mock, seal(), "table:users"),
] {
assert!(matches!(attempt, Err(DecryptError::Cipher(Refused))));
}
}
#[test]
fn the_async_methods_bind_the_same_aad_as_the_sync_ones() {
use crate::seal::mock::Remote;
use futures_executor::block_on;
let remote = Remote::new(false);
let sync_leaf = sealed_with(ContextTag::new("secret", "user:42"), "row:7");
let async_node =
block_on(ContextTag::new("secret", "user:42").encrypt_with_aad_async(&remote, "row:7"))
.expect("the mock cannot fail");
let CipherText::Single(async_leaf) = async_node else {
panic!("a scalar seals as one leaf");
};
assert_eq!(async_leaf.aad, sync_leaf.aad);
assert_eq!(async_leaf.plaintext, sync_leaf.plaintext);
let back: String = block_on(ContextTag::context("user:42").decrypt_with_aad_async(
&remote,
CipherText::Single(async_leaf),
"row:7",
))
.expect("decrypts");
assert_eq!(back, "secret");
let node = block_on(ContextTag::new("secret", "user:42").encrypt_async(&remote))
.expect("the mock cannot fail");
let back: String = block_on(ContextTag::context("user:42").decrypt_async(&remote, node))
.expect("decrypts");
assert_eq!(back, "secret");
let node = block_on(ContextTag::new("secret", "user:42").encrypt_async(&remote))
.expect("the mock cannot fail");
assert!(
block_on(ContextTag::context("other").decrypt_async::<String, _>(&remote, node))
.is_err(),
"a different tag is a different AAD"
);
}
#[test]
fn the_async_decrypt_futures_are_send() {
use crate::seal::mock::Remote;
use futures_executor::block_on;
fn assert_send<F: Send>(future: F) -> F {
future
}
let remote = Remote::new(false);
let node =
block_on(ContextTag::new("secret", "user:42").encrypt_with_aad_async(&remote, "row:7"))
.expect("the mock cannot fail");
let future = assert_send(
ContextTag::context("user:42")
.decrypt_with_aad_async::<String, _, _>(&remote, node, "row:7"),
);
assert_eq!(block_on(future).expect("decrypts"), "secret");
let node = block_on(ContextTag::new("secret", "user:42").encrypt_async(&remote))
.expect("the mock cannot fail");
let future =
assert_send(ContextTag::context("user:42").decrypt_async::<String, _>(&remote, node));
assert_eq!(block_on(future).expect("decrypts"), "secret");
let node = block_on(crate::encrypt_async("secret", &remote, "user:42"))
.expect("the mock cannot fail");
let future = assert_send(crate::decrypt_async::<String, _, _>(
&remote, node, "user:42",
));
assert_eq!(block_on(future).expect("decrypts"), "secret");
}
#[test]
fn debug_shows_the_tag_and_masks_the_value() {
let shown = format!("{:?}", ContextTag::new("secret message", "user:42"));
assert!(!shown.contains("secret message"), "got {shown}");
assert!(shown.contains("user:42"), "got {shown}");
}
}