use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::envelope::{Assurance, Envelope};
pub const SEALED_V0_PASSTHROUGH: u8 = 0;
pub const SEALED_V1_GROUPKEY: u8 = 1;
pub const CURRENT_CONFIDENTIALITY: Confidentiality = Confidentiality::None;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Confidentiality {
None,
GroupKey,
}
impl Confidentiality {
pub const fn as_wire(self) -> &'static str {
match self {
Confidentiality::None => "passthrough",
Confidentiality::GroupKey => "groupkey",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct Sealed {
pub v: u8,
pub envelope: Envelope,
}
impl Sealed {
pub fn passthrough(envelope: Envelope) -> Self {
Self {
v: SEALED_V0_PASSTHROUGH,
envelope,
}
}
pub fn confidentiality(&self) -> Confidentiality {
match self.v {
SEALED_V1_GROUPKEY => Confidentiality::GroupKey,
_ => Confidentiality::None,
}
}
}
#[derive(Clone)]
pub struct Opened {
pub payload: Vec<u8>,
pub assurance: Assurance,
pub confidentiality: Confidentiality,
}
impl std::fmt::Debug for Opened {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Opened")
.field("payload_len", &self.payload.len())
.field("assurance", &self.assurance)
.field("confidentiality", &self.confidentiality)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::envelope::{Envelope, Freshness, ENVELOPE_V1};
fn open_envelope() -> Envelope {
Envelope {
v: ENVELOPE_V1,
payload: "aGk".to_string(),
nonce: "bm9uY2U".to_string(),
ts: 1_700_000_000,
sig: None,
}
}
#[test]
fn passthrough_is_v0_and_not_encrypted() {
let s = Sealed::passthrough(open_envelope());
assert_eq!(s.v, SEALED_V0_PASSTHROUGH);
assert_eq!(s.confidentiality(), Confidentiality::None);
}
#[test]
fn v1_version_reads_as_groupkey() {
let s = Sealed {
v: SEALED_V1_GROUPKEY,
envelope: open_envelope(),
};
assert_eq!(s.confidentiality(), Confidentiality::GroupKey);
}
#[test]
fn unknown_version_is_conservatively_not_encrypted() {
let s = Sealed {
v: 99,
envelope: open_envelope(),
};
assert_eq!(s.confidentiality(), Confidentiality::None);
}
#[test]
fn confidentiality_wire_strings() {
assert_eq!(Confidentiality::None.as_wire(), "passthrough");
assert_eq!(Confidentiality::GroupKey.as_wire(), "groupkey");
assert_eq!(CURRENT_CONFIDENTIALITY.as_wire(), "passthrough");
}
#[test]
fn confidentiality_serializes_snake_case() {
assert_eq!(
serde_json::to_string(&Confidentiality::GroupKey).unwrap(),
r#""group_key""#
);
}
#[test]
fn sealed_round_trips() {
let s = Sealed::passthrough(open_envelope());
let json = serde_json::to_string(&s).unwrap();
let back: Sealed = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn opened_debug_redacts_payload() {
let opened = Opened {
payload: b"super-secret-bytes".to_vec(),
assurance: Assurance::Anonymous {
freshness: Freshness::Fresh,
},
confidentiality: Confidentiality::None,
};
let dbg = format!("{opened:?}");
assert!(dbg.contains("payload_len"));
assert!(
!dbg.contains("super-secret"),
"Debug must not leak the payload"
);
}
}