use std::collections::HashMap;
use std::path::PathBuf;
use bytes::Bytes;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct Payload(Vec<u8>);
impl Payload {
#[must_use]
pub fn from_bytes(data: impl Into<Vec<u8>>) -> Self {
Self(data.into())
}
pub fn from_str_utf8(s: &str) -> Result<Self, std::convert::Infallible> {
Ok(Self(s.as_bytes().to_vec()))
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
#[must_use]
pub const fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl std::fmt::Debug for Payload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Payload")
.field("len", &self.0.len())
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CoverMediaKind {
PngImage,
BmpImage,
JpegImage,
GifImage,
WavAudio,
PdfDocument,
PlainText,
}
#[derive(Debug, Clone)]
pub struct CoverMedia {
pub kind: CoverMediaKind,
pub data: Bytes,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum StegoTechnique {
LsbImage,
DctJpeg,
Palette,
LsbAudio,
PhaseEncoding,
EchoHiding,
ZeroWidthText,
PdfContentStream,
PdfMetadata,
CorpusSelection,
DualPayload,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ChromaSubsampling {
Yuv420,
Yuv422,
Yuv444,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum PlatformProfile {
Instagram,
Twitter,
Imgur,
WhatsApp,
Telegram,
Custom {
quality: u8,
subsampling: ChromaSubsampling,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EmbeddingProfile {
Standard,
Adaptive {
max_detectability_db: f64,
},
CompressionSurvivable {
platform: PlatformProfile,
},
CorpusBased,
}
pub const DEFAULT_ADAPTIVE_DETECTABILITY_DB: f64 = -12.0;
impl EmbeddingProfile {
#[must_use]
pub const fn default_adaptive() -> Self {
Self::Adaptive {
max_detectability_db: DEFAULT_ADAPTIVE_DETECTABILITY_DB,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ManyToManyMode {
Replicate,
Stripe,
Diagonal,
Random,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DistributionPattern {
OneToOne,
OneToMany {
data_shards: u8,
parity_shards: u8,
},
ManyToOne,
ManyToMany {
mode: ManyToManyMode,
},
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct Shard {
pub index: u8,
pub total: u8,
pub data: Vec<u8>,
pub hmac_tag: [u8; 32],
}
impl std::fmt::Debug for Shard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Shard")
.field("index", &self.index)
.field("total", &self.total)
.field("data_len", &self.data.len())
.field("hmac_tag", &"[redacted]")
.finish()
}
}
impl Serialize for Shard {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut st = s.serialize_struct("Shard", 4)?;
st.serialize_field("index", &self.index)?;
st.serialize_field("total", &self.total)?;
st.serialize_field("data", &self.data)?;
st.serialize_field("hmac_tag", &self.hmac_tag)?;
st.end()
}
}
impl<'de> Deserialize<'de> for Shard {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct ShardHelper {
index: u8,
total: u8,
data: Vec<u8>,
hmac_tag: [u8; 32],
}
let h = ShardHelper::deserialize(d)?;
Ok(Self {
index: h.index,
total: h.total,
data: h.data,
hmac_tag: h.hmac_tag,
})
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct KeyPair {
pub public_key: Vec<u8>,
pub secret_key: Vec<u8>,
}
impl std::fmt::Debug for KeyPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KeyPair")
.field("public_key_len", &self.public_key.len())
.field("secret_key", &"[redacted]")
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signature(pub Bytes);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PqcAlgorithm {
MlKem1024,
MlDsa87,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Capacity {
pub bytes: u64,
pub technique: StegoTechnique,
}
impl Capacity {
#[must_use]
pub const fn is_sufficient_for(&self, payload: &Payload) -> bool {
self.bytes >= payload.len() as u64
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DetectabilityRisk {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpectralScore {
pub phase_coherence_drop: f64,
pub carrier_snr_drop_db: f64,
pub sample_pair_asymmetry: f64,
pub combined_risk: DetectabilityRisk,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisReport {
pub technique: StegoTechnique,
pub cover_capacity: Capacity,
pub chi_square_score: f64,
pub detectability_risk: DetectabilityRisk,
pub recommended_max_payload_bytes: u64,
pub ai_watermark: Option<AiWatermarkAssessment>,
pub spectral_score: Option<SpectralScore>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AiWatermarkAssessment {
pub detected: bool,
pub model_id: Option<String>,
pub confidence: f64,
pub matched_strong_bins: usize,
pub total_strong_bins: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatermarkReceipt {
pub recipient: String,
pub algorithm: String,
pub shards: Vec<u8>,
pub created_at: DateTime<Utc>,
}
impl std::fmt::Display for WatermarkReceipt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "# Watermark Receipt")?;
writeln!(f, "- **Recipient**: {}", self.recipient)?;
writeln!(f, "- **Algorithm**: {}", self.algorithm)?;
writeln!(f, "- **Shards**: {:?}", self.shards)?;
writeln!(f, "- **Created**: {}", self.created_at)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ArchiveFormat {
Zip,
Tar,
TarGz,
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct DeniableKeySet {
pub primary_key: Vec<u8>,
pub decoy_key: Vec<u8>,
}
impl std::fmt::Debug for DeniableKeySet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeniableKeySet")
.field("primary_key", &"[redacted]")
.field("decoy_key", &"[redacted]")
.finish()
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct DeniablePayloadPair {
pub real_payload: Vec<u8>,
pub decoy_payload: Vec<u8>,
}
impl std::fmt::Debug for DeniablePayloadPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeniablePayloadPair")
.field("real_len", &self.real_payload.len())
.field("decoy_len", &self.decoy_payload.len())
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanaryShard {
pub shard: Shard,
pub canary_id: Uuid,
pub notify_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetrievalManifest {
pub platform: PlatformProfile,
pub retrieval_url: String,
pub technique: StegoTechnique,
pub stego_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeographicManifest {
pub shards: Vec<GeoShardEntry>,
pub minimum_jurisdictions: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeoShardEntry {
pub shard_index: u8,
pub jurisdiction: String,
pub holder_description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeLockPuzzle {
pub ciphertext: Bytes,
pub modulus: Vec<u8>,
pub start_value: Vec<u8>,
pub squarings_required: u64,
pub created_at: DateTime<Utc>,
pub unlock_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SpectralKey {
pub model_id: String,
pub resolution: (u32, u32),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorpusEntry {
pub file_hash: [u8; 32],
pub path: String,
pub cover_kind: CoverMediaKind,
pub precomputed_bit_pattern: Bytes,
pub spectral_key: Option<SpectralKey>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StyloProfile {
pub target_vocab_size: usize,
pub target_avg_sentence_len: f64,
pub normalize_punctuation: bool,
}
#[derive(Debug, Clone)]
pub struct PanicWipeConfig {
pub key_paths: Vec<PathBuf>,
pub config_paths: Vec<PathBuf>,
pub temp_dirs: Vec<PathBuf>,
}
pub struct WatermarkTripwireTag {
pub recipient_id: Uuid,
pub embedding_seed: Vec<u8>,
}
impl Zeroize for WatermarkTripwireTag {
fn zeroize(&mut self) {
self.embedding_seed.zeroize();
}
}
impl Drop for WatermarkTripwireTag {
fn drop(&mut self) {
self.zeroize();
}
}
impl Clone for WatermarkTripwireTag {
fn clone(&self) -> Self {
Self {
recipient_id: self.recipient_id,
embedding_seed: self.embedding_seed.clone(),
}
}
}
impl std::fmt::Debug for WatermarkTripwireTag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WatermarkTripwireTag")
.field("recipient_id", &self.recipient_id)
.field("embedding_seed", &"[redacted]")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn capacity_insufficient_when_payload_exceeds_limit() {
let cap = Capacity {
bytes: 10,
technique: StegoTechnique::LsbImage,
};
let large = Payload::from_bytes(vec![0u8; 11]);
assert!(!cap.is_sufficient_for(&large));
}
#[test]
fn capacity_sufficient_when_payload_fits_exactly() {
let cap = Capacity {
bytes: 10,
technique: StegoTechnique::LsbImage,
};
let exact = Payload::from_bytes(vec![0u8; 10]);
assert!(cap.is_sufficient_for(&exact));
}
#[test]
fn payload_from_str_utf8_round_trips() -> TestResult {
let p = Payload::from_str_utf8("hello")?;
assert_eq!(p.as_bytes(), b"hello");
Ok(())
}
#[test]
fn shard_round_trips_through_serde_json() -> TestResult {
let original = Shard {
index: 2,
total: 5,
data: vec![1, 2, 3, 4],
hmac_tag: [0xAB; 32],
};
let json = serde_json::to_string(&original)?;
let decoded: Shard = serde_json::from_str(&json)?;
assert_eq!(decoded.index, 2);
assert_eq!(decoded.total, 5);
assert_eq!(decoded.data, &[1, 2, 3, 4]);
assert_eq!(decoded.hmac_tag, [0xAB; 32]);
Ok(())
}
#[test]
fn watermark_receipt_display_contains_heading() -> TestResult {
let receipt = WatermarkReceipt {
recipient: "alice".into(),
algorithm: "lsb".into(),
shards: vec![0, 1, 2],
created_at: DateTime::from_timestamp(0, 0).ok_or("invalid timestamp")?,
};
let s = receipt.to_string();
assert!(s.contains("# Watermark Receipt"), "missing heading in: {s}");
Ok(())
}
#[test]
fn geographic_manifest_serialises_roundtrip() -> TestResult {
let manifest = GeographicManifest {
shards: vec![
GeoShardEntry {
shard_index: 0,
jurisdiction: "DE".into(),
holder_description: "Journalist A".into(),
},
GeoShardEntry {
shard_index: 1,
jurisdiction: "CH".into(),
holder_description: "Journalist B".into(),
},
],
minimum_jurisdictions: 2,
};
let json = serde_json::to_string(&manifest)?;
let decoded: GeographicManifest = serde_json::from_str(&json)?;
assert_eq!(decoded.minimum_jurisdictions, 2);
assert_eq!(decoded.shards.len(), 2);
assert_eq!(
decoded.shards.first().ok_or("no shards")?.jurisdiction,
"DE"
);
Ok(())
}
#[test]
fn deniable_key_set_both_keys_zeroized_on_explicit_call() {
let mut ks = DeniableKeySet {
primary_key: vec![0xFF; 32],
decoy_key: vec![0xAA; 32],
};
ks.zeroize();
assert!(ks.primary_key.iter().all(|&b| b == 0));
assert!(ks.decoy_key.iter().all(|&b| b == 0));
}
#[test]
fn watermark_tripwire_tag_seed_zeroized_on_explicit_call() {
let mut tag = WatermarkTripwireTag {
recipient_id: Uuid::new_v4(),
embedding_seed: vec![0xDE; 32],
};
tag.zeroize();
assert!(tag.embedding_seed.iter().all(|&b| b == 0));
}
#[test]
fn distribution_pattern_copy_clone() {
let p = DistributionPattern::OneToMany {
data_shards: 3,
parity_shards: 2,
};
let q = p;
assert_eq!(p, q);
}
#[test]
fn analysis_report_serialises() -> TestResult {
let report = AnalysisReport {
technique: StegoTechnique::DctJpeg,
cover_capacity: Capacity {
bytes: 1024,
technique: StegoTechnique::DctJpeg,
},
chi_square_score: 0.42,
detectability_risk: DetectabilityRisk::Low,
recommended_max_payload_bytes: 512,
ai_watermark: None,
spectral_score: None,
};
let json = serde_json::to_string(&report)?;
assert!(json.contains("DctJpeg"));
assert!(json.contains("Low"));
Ok(())
}
#[test]
fn payload_debug_redacted() {
let p = Payload::from_bytes(b"secret".to_vec());
let dbg = format!("{p:?}");
assert!(dbg.contains("len"));
assert!(!dbg.contains("secret"));
}
#[test]
fn payload_empty() {
let p = Payload::from_bytes(Vec::new());
assert!(p.is_empty());
assert_eq!(p.len(), 0);
}
#[test]
fn keypair_debug_redacted() {
let kp = KeyPair {
public_key: vec![1u8; 32],
secret_key: vec![2u8; 32],
};
let dbg = format!("{kp:?}");
assert!(dbg.contains("[redacted]"));
assert!(dbg.contains("public_key_len"));
}
#[test]
fn shard_debug_redacted() {
let s = Shard {
index: 0,
total: 5,
data: vec![0u8; 100],
hmac_tag: [0u8; 32],
};
let dbg = format!("{s:?}");
assert!(dbg.contains("[redacted]"));
assert!(dbg.contains("data_len"));
}
#[test]
fn deniable_payload_pair_debug_redacted() {
let pair = DeniablePayloadPair {
real_payload: vec![1u8; 50],
decoy_payload: vec![2u8; 30],
};
let dbg = format!("{pair:?}");
assert!(dbg.contains("real_len"));
assert!(dbg.contains("decoy_len"));
}
#[test]
fn deniable_key_set_debug_redacted() {
let ks = DeniableKeySet {
primary_key: vec![1u8; 32],
decoy_key: vec![2u8; 32],
};
let dbg = format!("{ks:?}");
assert!(dbg.contains("[redacted]"));
assert!(!dbg.contains("\\x01"));
}
#[test]
fn watermark_tripwire_tag_debug_redacted() {
let tag = WatermarkTripwireTag {
recipient_id: Uuid::nil(),
embedding_seed: vec![0xFF; 16],
};
let dbg = format!("{tag:?}");
assert!(dbg.contains("[redacted]"));
}
#[test]
fn watermark_tripwire_tag_clone() {
let tag = WatermarkTripwireTag {
recipient_id: Uuid::new_v4(),
embedding_seed: vec![0xAA; 32],
};
let cloned = tag.clone();
assert_eq!(tag.recipient_id, cloned.recipient_id);
assert_eq!(tag.embedding_seed, cloned.embedding_seed);
}
#[test]
fn time_lock_puzzle_serialises() -> TestResult {
let puzzle = TimeLockPuzzle {
ciphertext: Bytes::from(vec![1u8; 32]),
modulus: vec![2u8; 16],
start_value: vec![3u8; 16],
squarings_required: 1000,
created_at: Utc::now(),
unlock_at: Utc::now(),
};
let json = serde_json::to_string(&puzzle)?;
assert!(json.contains("squarings_required"));
Ok(())
}
#[test]
fn retrieval_manifest_serialises() -> TestResult {
let manifest = RetrievalManifest {
platform: PlatformProfile::Instagram,
retrieval_url: "https://example.com/post/123".into(),
technique: StegoTechnique::LsbImage,
stego_hash: "abcdef123456".into(),
};
let json = serde_json::to_string(&manifest)?;
let decoded: RetrievalManifest = serde_json::from_str(&json)?;
assert_eq!(decoded.retrieval_url, "https://example.com/post/123");
Ok(())
}
#[test]
fn corpus_entry_serialises() -> TestResult {
let entry = CorpusEntry {
file_hash: [0xAB; 32],
path: "images/cover.png".into(),
cover_kind: CoverMediaKind::PngImage,
precomputed_bit_pattern: Bytes::from(vec![0u8; 16]),
spectral_key: None,
};
let json = serde_json::to_string(&entry)?;
assert!(json.contains("images/cover.png"));
Ok(())
}
#[test]
fn stylo_profile_default_values() {
let profile = StyloProfile {
target_vocab_size: 500,
target_avg_sentence_len: 15.0,
normalize_punctuation: true,
};
assert!(profile.normalize_punctuation);
assert_eq!(profile.target_vocab_size, 500);
}
#[test]
fn archive_format_variants_are_distinct() {
assert_ne!(ArchiveFormat::Zip, ArchiveFormat::Tar);
assert_ne!(ArchiveFormat::Tar, ArchiveFormat::TarGz);
assert_ne!(ArchiveFormat::Zip, ArchiveFormat::TarGz);
}
#[test]
fn canary_shard_serialises() -> TestResult {
let shard = CanaryShard {
shard: Shard {
index: 0,
total: 3,
data: vec![1, 2, 3],
hmac_tag: [0u8; 32],
},
canary_id: Uuid::new_v4(),
notify_url: Some("https://example.com/canary".into()),
};
let json = serde_json::to_string(&shard)?;
assert!(json.contains("canary_id"));
Ok(())
}
#[test]
fn panic_wipe_config_debug() {
let config = PanicWipeConfig {
key_paths: vec![std::path::PathBuf::from("/tmp/key")],
config_paths: vec![],
temp_dirs: vec![],
};
let dbg = format!("{config:?}");
assert!(dbg.contains("key_paths"));
}
#[test]
fn spectral_key_round_trips_through_serde_json() -> TestResult {
let key = SpectralKey {
model_id: "gemini".to_string(),
resolution: (1024, 1024),
};
let json = serde_json::to_string(&key)?;
let decoded: SpectralKey = serde_json::from_str(&json)?;
assert_eq!(decoded.model_id, "gemini");
assert_eq!(decoded.resolution, (1024, 1024));
Ok(())
}
#[test]
fn spectral_key_usable_as_hashmap_key() {
let mut map = std::collections::HashMap::new();
let key = SpectralKey {
model_id: "gemini".to_string(),
resolution: (1024, 1024),
};
map.insert(key.clone(), 42usize);
assert_eq!(map.get(&key), Some(&42));
}
#[test]
fn corpus_entry_with_spectral_key_serialises() -> TestResult {
let entry = CorpusEntry {
file_hash: [0xCC; 32],
path: "ai/image.png".into(),
cover_kind: CoverMediaKind::PngImage,
precomputed_bit_pattern: Bytes::from(vec![0u8; 8]),
spectral_key: Some(SpectralKey {
model_id: "gemini".to_string(),
resolution: (1024, 1024),
}),
};
let json = serde_json::to_string(&entry)?;
assert!(json.contains("gemini"));
let decoded: CorpusEntry = serde_json::from_str(&json)?;
assert!(decoded.spectral_key.is_some());
Ok(())
}
#[test]
fn corpus_entry_without_spectral_key_serialises() -> TestResult {
let entry = CorpusEntry {
file_hash: [0xDD; 32],
path: "camera/photo.jpg".into(),
cover_kind: CoverMediaKind::JpegImage,
precomputed_bit_pattern: Bytes::from(vec![0u8; 8]),
spectral_key: None,
};
let json = serde_json::to_string(&entry)?;
let decoded: CorpusEntry = serde_json::from_str(&json)?;
assert!(decoded.spectral_key.is_none());
Ok(())
}
#[test]
fn default_adaptive_uses_named_constant() {
let profile = EmbeddingProfile::default_adaptive();
assert!(matches!(
profile,
EmbeddingProfile::Adaptive {
max_detectability_db
} if (max_detectability_db - DEFAULT_ADAPTIVE_DETECTABILITY_DB).abs() < f64::EPSILON
));
}
}