stegoeggo 0.2.3

Rights-reservation metadata and AI-training restriction notices for images, with optional steganographic markers
docs.rs failed to build stegoeggo-0.2.3
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: stegoeggo-0.2.1

stegoeggo

Embed rights-reservation metadata and AI-training restriction notices into images, with optional best-effort steganographic markers for redundant evidence.

CI Crates.io Documentation License: MIT MSRV

What stegoeggo is

stegoeggo is:

  • A legal-notice and rights-reservation metadata tool for images.
  • A way to make copyright and AI-training restrictions visible to metadata-aware systems.
  • A best-effort redundant marking system when optional steganographic payloads are enabled.

What stegoeggo is not

stegoeggo is not:

  • A forensic watermarking system.
  • A DRM system.
  • A guarantee that marks survive arbitrary resizing, re-encoding, screenshots, cropping, or metadata stripping.
  • A cryptographic proof that a model trained on a specific image.
  • A data-poisoning tool.

What it does

stegoeggo embeds multiple layers of rights-reservation and AI-training restriction metadata into images:

Layer Description
Metadata Injection Embeds rights-reservation and AI-training restriction markers in image headers using canonical plus:DataMining rights signals, XMP, and EXIF
Steganography Optional hidden payloads embedded in image pixels (LSB) or DCT coefficients (JPEG) for redundant evidence

External Standards

  • PLUS License Data Format - Emits plus:DataMining with official PLUS LDF controlled-vocabulary URIs for machine-readable rights signals (canonical per the PLUS License Data Format specification). Legacy Iptc4xmpExt:DMI-* properties are still parsed for backward compatibility but not emitted by default.
  • ISCC - Computes Immutable Self-Certifying Constituent Content identifiers for content identification

Release 5 Features

Release 5 adds cryptographic provenance, signing, and detached manifests:

Feature Description
Payload v3 TLV extension format with domain-separated authentication for future-proof metadata
Provenance Claims Canonical provenance assertions with digest binding and canonical serialization
Ed25519 Signing Real Ed25519 signing via ed25519-dalek of protection payloads and provenance claims (signatures feature)
Detached Manifests Signed sidecar manifests for distributing provenance outside the image (detached-manifest feature)
Structured Verification VerificationReport with per-channel sub-results replaces the old VerificationStatus enum

Installation

As a Library

Add to your Cargo.toml:

[dependencies]
stegoeggo = "0.2"
image = "0.25"  # Required for DynamicImage

For async support (Tokio-based WAF/CDN deployments):

[dependencies]
stegoeggo = { version = "0.2", features = ["async"] }

For Ed25519 signing (provenance claims, detached manifests):

[dependencies]
stegoeggo = { version = "0.2", features = ["signatures", "detached-manifest"] }

As a CLI Tool

Build the binary from source:

cargo build --release

Or install directly:

cargo install stegoeggo-cli

Quick Start

CLI

# Embed legal-notice metadata with default settings (Standard level)
stegoeggo input.png -o output.png

# With explicit legal metadata (recommended for owned content)
stegoeggo artwork.png -o artwork_protected.png \
  --copyright-holder "Jane Artist" \
  --creator "Jane Artist" \
  --rights-url "https://example.com/rights/artwork" \
  --no-genai-training

# With full legal metadata including new v0.2 fields
stegoeggo photo.jpg --copyright-holder "Acme Corp" --creator "Jane Doe" \
  --credit-line "Photo by Jane Doe / Acme Corp" \
  --copyright-owner "Acme Corp" \
  --licensor-name "Acme Corp" --licensor-email "legal@acme.com" \
  --content-created-at "2024-01-15"

# Quick AI-training restriction
stegoeggo photo.jpg -o protected.jpg --no-ai-training

# Light protection (metadata only, minimal stego)
stegoeggo input.png -o output.png --level light

# Authenticated provenance (optional — requires MAC key)
stegoeggo artwork.png -o artwork_auth.png \
  --profile authenticated-provenance \
  --key deadbeefcafebabe \
  --copyright-holder "Jane Artist" \
  --rights-url "https://example.com/rights/artwork" \
  --no-ai-training

# Verify if an image is protected
stegoeggo protected.png --verify

Library

use stegoeggo::{ProtectionPipeline, ProtectionContext, ProtectionLevel};
use image::DynamicImage;

// Create pipeline and context
let pipeline = ProtectionPipeline::new();
let ctx = ProtectionContext::default();

// Process an image — embeds metadata and optional steganographic markers
let img = DynamicImage::new_rgb8(512, 512);
let protected = pipeline.process(&img, ProtectionLevel::Standard, &ctx).unwrap();

Request-Based API (recommended for new code)

use stegoeggo::{ProtectionRequest, RightsPolicy, RightsNotice, LegalMetadata};

let request = ProtectionRequest::metadata_only(
    RightsNotice::default(),
    RightsPolicy::ProhibitedAiMlTraining,
)
.with_legal_metadata(
    LegalMetadata::new()
        .with_copyright_holder("Example Corp")
        .with_usage_terms("All Rights Reserved"),
);

let protected = stegoeggo::process_request_bytes(&img_bytes, &request)?;

Library Usage

Processing Image Bytes

Process images from files or network sources without loading into DynamicImage:

use stegoeggo::{process_image_bytes, ProtectionContext, ProtectionLevel};

// Read image from file
let img_bytes = std::fs::read("image.png").unwrap();

// Process with automatic format detection. The byte API preserves the detected
// input format unless you set `ProtectionContext::with_format(...)`.
let ctx = ProtectionContext::default();
let protected = process_image_bytes(&img_bytes, ProtectionLevel::Standard, &ctx).unwrap();

Parallel Processing

Process multiple images concurrently using Rayon:

use stegoeggo::{process_images_parallel, ProtectionContext, ProtectionLevel};
use image::DynamicImage;

let images: Vec<DynamicImage> = vec![
    image::open("image1.png").unwrap(),
    image::open("image2.png").unwrap(),
    image::open("image3.png").unwrap(),
];

let ctx = ProtectionContext::default();
let results = process_images_parallel(&images, ProtectionLevel::Standard, &ctx).unwrap();

Or process bytes in parallel:

use stegoeggo::{process_images_bytes_parallel, ProtectionContext, ProtectionLevel};

let image_bytes: Vec<Vec<u8>> = vec![
    std::fs::read("image1.png").unwrap(),
    std::fs::read("image2.png").unwrap(),
];

let protected = process_images_bytes_parallel(&image_bytes, ProtectionLevel::Standard, &ctx).unwrap();

Request-Based API (Recommended)

The request-based API is the canonical way to use stegoeggo. It separates rights policy from processing mechanics:

use stegoeggo::{ProtectionRequest, RightsPolicy, ProtectionPreset, RightsNotice};

// Metadata-only legal notice (fastest path)
let request = ProtectionRequest::metadata_only(
    RightsNotice::default(),
    RightsPolicy::ProhibitedAiMlTraining,
);

// With hidden marker
let request = ProtectionRequest::with_hidden_marker(
    RightsNotice::default(),
    RightsPolicy::ProhibitedAiMlTraining,
);

// Using a preset
let request = ProtectionRequest::from_preset(
    ProtectionPreset::AuthenticatedProvenance,
    RightsNotice::default(),
    RightsPolicy::ProhibitedAiMlTraining,
)
.with_mac_key(b"secret".to_vec());

let (protected, report) = stegoeggo::process_request_bytes_with_report(&img_bytes, &request)?;
println!("Metadata injected: {}", report.metadata_injected);
println!("Stego succeeded: {}", report.stego_succeeded);

Key Generation and Signing (feature: signatures)

use stegoeggo::signing::{SigningKey, VerifyingKey};

// Generate a new signing key (uses ed25519-dalek for real Ed25519)
let signing_key = SigningKey::generate();
let verifying_key: VerifyingKey = signing_key.verifying_key();

// Sign a provenance claim — produces a 64-byte Ed25519 signature
let claim_bytes = b"provenance claim data";
let signature = signing_key.sign(claim_bytes);

// Verify
assert!(verifying_key.verify(claim_bytes, &signature).is_ok());

Note: Signing is experimental (feature-gated behind signatures). A valid signature proves only that the private key holder signed the claim bytes — it does not prove copyright ownership or authorship. See SECURITY.md for details.

Detached Manifests (feature: detached-manifest)

use stegoeggo::detached::{DetachedManifest, ManifestBuilder};

// Create a manifest from protected image bytes
let manifest = ManifestBuilder::new()
    .with_provenance(claim)
    .with_signature(signature)
    .build();

// Serialize to JSON sidecar
let sidecar = manifest.to_json()?;

// Later, verify the sidecar against image bytes
let report = DetachedManifest::verify(&sidecar, &image_bytes)?;

VerificationReport (structured results)

use stegoeggo::verification::VerificationReport;

let report: VerificationReport = stegoeggo::verify_image_bytes_detailed(&image_bytes)?;
println!("Stego: {:?}", report.stego);
println!("Metadata: {:?}", report.metadata);
println!("Signing: {:?}", report.signing);

Protection Levels

The library provides three protection levels:

Level Strategy Latency (512x512) Use Case
Disabled No protection ~20 ns Testing, whitelisted clients
Light Metadata + minimal stego (Q-table seed for JPEG, LSB redundancy=1 for PNG/WebP) ~0.8 ms Metadata-only, low cost
Standard Full stego (DCT F5 + metadata for JPEG, LSB + metadata for PNG/WebP) ~0.8 ms Default for most endpoints
use stegoeggo::ProtectionLevel;

// Use different levels
let level = ProtectionLevel::Light;    // Metadata + minimal stego
let level = ProtectionLevel::Standard; // Stego + Metadata (default)

Evidence Profiles

Evidence profiles control how protection warnings are interpreted and the default evidence posture. While ProtectionLevel controls how much processing occurs, EvidenceProfile answers "what evidence model is the caller trying to express?"

Profile MAC Key Required Stego Primary Use Case
LegalNotice (default) No Optional Standards-aligned metadata notice
LegalNoticeWithStego No Yes Metadata notice plus best-effort hidden marker
AuthenticatedProvenance Yes Yes Cryptographic proof of payload origin
Maximal Optional Yes All available evidence channels
use stegoeggo::{ProtectionContext, EvidenceProfile, LegalMetadata, ProtectionLevel};

// Legal notice only — no MAC key needed
let ctx = ProtectionContext::legal_notice()
    .with_legal_metadata(
        LegalMetadata::new()
            .with_copyright_holder("Jane Artist")
            .with_ai_constraints("No AI training permitted.")
    );

// Authenticated provenance — MAC key expected
let ctx = ProtectionContext::authenticated_provenance()
    .with_mac_key(b"secret-key".to_vec());

// Via builder
let ctx = ProtectionContext::new(0.5, 42)
    .with_evidence_profile(EvidenceProfile::Maximal);

Legal Metadata Injection

Inject real legal metadata (copyright, contact info, usage terms). Only use for content you own.

with_legal_metadata(...) provides the content. When legal metadata is provided, with_legal_claims(true) is auto-enabled — you no longer need to call it explicitly. The explicit call is still supported but no longer required:

use stegoeggo::{process_image_bytes, ProtectionContext, LegalMetadata, ProtectionLevel};

let img_bytes = std::fs::read("image.png").unwrap();
let ctx = ProtectionContext::default()
    .with_legal_metadata(
        LegalMetadata::new()
            .with_copyright_holder("Your Company Name")
            .with_contact_email("legal@company.com")
            .with_usage_terms("All Rights Reserved. No AI training permitted.")
            .with_license_url("https://company.com/license")
    )
    .with_legal_claims(true);

let protected = process_image_bytes(&img_bytes, ProtectionLevel::Standard, &ctx).unwrap();

DMI (Data Mining Inhibitor) Values

Set DMI metadata values for AI-training restrictions. The XMP writer emits canonical plus:DataMining properties with PLUS LDF vocabulary keys. Legacy Iptc4xmpExt:DMI-* properties are still parsed for backward compatibility but not emitted by default:

use stegoeggo::{ProtectionContext, DmiValue, ProtectionLevel};

let ctx = ProtectionContext::default()
    .with_dmi(DmiValue::ProhibitedAiMlTraining);

Available values:

  • Unspecified - No restriction specified
  • Allowed - Content may be used for AI/ML training
  • ProhibitedAiMlTraining - Prohibited for AI/ML training
  • ProhibitedGenAiMlTraining - Prohibited for generative AI training
  • ProhibitedExceptSearchEngineIndexing - Prohibited except for search indexing
  • Prohibited - All uses prohibited
  • ProhibitedSeeConstraints - Prohibited, see constraints for details

Each variant maps to a canonical PLUS vocabulary key via DmiValue::plus_vocab_key() (e.g., DMI-PROHIBITED-AIMLTRAINING). Legacy IPTC keys can be parsed back via DmiValue::from_plus_vocab_key().

TDMRep Status

TDMRep (W3C Text and Data Mining Reservation Protocol) deployment artifacts (HTTP headers, /.well-known/tdmrep.json) are deferred from Release 1. StegoEggo currently emits PLUS image metadata only. Legacy tdm:reserve_tdm image properties are still parsed for backward compatibility diagnostics but are not emitted by default. The CLI --tdm-reserved flag is deprecated and now sets DMI to ProhibitedSeeConstraints.

Optional: Authenticated Stego Provenance (MAC Key)

Provide a hex key for HMAC-SHA256 payload verification. Without a key, steganographic payloads use a non-cryptographic CRC32 checksum suitable for development and testing.

use stegoeggo::{ProtectionContext, ProtectionLevel};

// With MAC key — steganographic payloads are cryptographically verified
let key = vec![0xde, 0xad, 0xbe, 0xef, 0x12, 0x34, 0x56, 0x78];
let ctx = ProtectionContext::new(0.8, 42)
    .with_mac_key(key);

// Without key — same seed produces same output (checksum-based verification)
let ctx = ProtectionContext::new(0.8, 42);

Note: ProtectionContext::default() uses generate_random_seed(), which is backed by the OS CSPRNG via the getrandom crate. The seed is unpredictable by design. For reproducible protection across runs, pass an explicit seed via ProtectionContext::new(intensity, seed). In rare sandboxed environments where getrandom is unavailable, a time-based fallback is used and a warning is logged.

Verification profiles:

  • Without a MAC key (legal-notice mode): Steganographic payload verification uses a non-cryptographic CRC32 checksum with ECC redundancy. Visible metadata markers prove intent and rights reservation. No MAC key is required for the legal-notice use case.
  • With a MAC key (authenticated provenance mode): The library uses HMAC-SHA256 for cryptographic payload verification. This proves the hidden payload was generated by a party with the configured secret. Use this when you need cryptographic integrity for the steganographic channel.

The MAC key affects:

  • Steganography payload verification (HMAC-SHA256 instead of simple checksum)

Migration from ProtectionLevel API

The ProtectionLevel and EvidenceProfile APIs still work but are deprecated. To migrate:

Old API New API
process_image_bytes(&bytes, ProtectionLevel::Standard, &ctx) process_request_bytes(&bytes, &request)
ctx.with_dmi(DmiValue::ProhibitedAiMlTraining) RightsPolicy::ProhibitedAiMlTraining in ProtectionRequest
EvidenceProfile::LegalNotice ProtectionPreset::LegalNotice or ProtectionChannels::metadata_only()
ctx.with_metadata_injection(false) ProtectionChannels { rights_metadata: false, .. }

Granular Control

Control individual protection components:

use stegoeggo::{LegalMetadata, ProtectionContext, ProtectionLevel};

// Minimal - stego only, no metadata
let ctx = ProtectionContext::new(0.5, 42)
    .with_metadata_injection(false);

// Full - metadata + legal claims (for owned content)
// Legal claims are auto-enabled when LegalMetadata is provided,
// but you can still pass `true` explicitly if desired.
let ctx = ProtectionContext::new(0.5, 42)
    .with_legal_metadata(LegalMetadata::new()
        .with_copyright_holder("My Company"));

// Limit maximum image dimension for processing
let ctx = ProtectionContext::new(0.5, 42)
    .with_max_dimension(2048);

Performance Tuning

For latency-sensitive deployments:

use stegoeggo::{
    process_image_bytes_with_warnings, ImageOutputFormat, ProtectionContext, ProtectionLevel,
};

// Optimized context for WAF edge deployment
let seed = 42u64;
let mac_key = b"your-secret-key".to_vec();
let input_bytes = std::fs::read("image.png").unwrap();
let ctx = ProtectionContext::new(0.5, seed)
    .with_format(ImageOutputFormat::Png)      // or Jpeg for smaller files
    .with_mac_key(mac_key)                     // for authenticated provenance
    .with_stego_redundancy(2)                  // 1-10, lower = faster
    .with_jpeg_quality(85)                     // 1-100, lower = faster
    .with_progressive_jpeg(true);              // Progressive rendering for web

// Process and serve directly
let (protected_bytes, warnings) =
    process_image_bytes_with_warnings(&input_bytes, ProtectionLevel::Standard, &ctx).unwrap();

// Reverse proxies should log warnings and may enforce policy before serving.
for warning in &warnings {
    eprintln!("Warning: {warning}");
}

Configuration Guide:

Parameter Default Range Effect on Latency
stego_redundancy derived 1-10 Higher = more robust verification, slower. Default: derived from intensity (1 below 0.3, 2 from 0.3 to 0.7, 3 above)
jpeg_quality 90 1-100 Higher = larger files, same speed
progressive_jpeg false bool Progressive = faster perceived load
output_format PNG PNG/JPEG/WebP JPEG = smallest files

Reverse Proxy Integration Contract

stegoeggo owns steganographic embedding and metadata injection. The reverse proxy should own cache lookup/storage, request byte limits, concurrency limits, timeouts, and serving policy.

Recommended hot-path shape:

use stegoeggo::{
    process_image_bytes_with_warnings, ImageOutputFormat, ProtectionContext, ProtectionLevel,
    ProtectionWarning,
};

let seed = 42u64;
let mac_key = b"your-secret-key".to_vec();
let origin_bytes = std::fs::read("image.png").unwrap();
let ctx = ProtectionContext::new(0.5, seed)
    .with_format(ImageOutputFormat::Png)
    .with_mac_key(mac_key)
    .with_max_dimension(4096)
    .with_stego_redundancy(1);

let (protected, warnings) =
    process_image_bytes_with_warnings(&origin_bytes, ProtectionLevel::Standard, &ctx).unwrap();

if warnings.iter().any(|w| matches!(w, ProtectionWarning::MissingMacKey)) {
    // Production policy should normally reject this configuration.
}

Use severity_for_profile() to determine if a warning is actionable for your evidence model:

use stegoeggo::{process_image_bytes_with_warnings, EvidenceProfile, ProtectionContext, ProtectionLevel};

let profile = EvidenceProfile::AuthenticatedProvenance;
let (protected, warnings) =
    process_image_bytes_with_warnings(&origin_bytes, ProtectionLevel::Standard, &ctx).unwrap();

for w in &warnings {
    match w.severity_for_profile(profile) {
        WarningSeverity::Error => eprintln!("FATAL: {w}"),
        WarningSeverity::Warning => eprintln!("WARN: {w}"),
        WarningSeverity::Info => {} // silently ignored
    }
}

Use process_image_bytes_with_warnings() rather than the DynamicImage API in the proxy path. For JPEG-in/JPEG-out, this keeps protection on the byte/DCT fast path. For PNG/WebP, the library must still decode and re-encode pixels to embed LSB payloads, so cache protected outputs aggressively at the proxy layer.

For verification, prefer verify_legal_notice() for a comprehensive report of all evidence channels. It extracts legal notice fields (copyright, creator, contact, etc.), checks steganographic payload integrity, and returns an EvidenceStrength rating. Use verify_image_bytes_detailed() for lower-level payload-only verification.

CLI Usage

Full Options Reference

stegoeggo [OPTIONS] <INPUT>

Arguments:
  <INPUT>                  Input image file(s)

Options:
  -o, --output <OUTPUT>    Output directory (batch) or file (single)
  --verify                Verify legal-notice report, evidence strength, and channels
  -l, --level <LEVEL>      Protection level: disabled, light, standard
  -p, --profile <PROFILE>  Evidence profile: legal-notice, legal-notice-stego,
                           authenticated-provenance, maximal (default: legal-notice)
  -i, --intensity <FLOAT> Protection intensity 0.0-1.0 (default: 0.5)
  -s, --seed <SEED>        Seed for reproducible results
  -f, --format <FORMAT>   Output format: png, jpg, webp (default: png)
  --stego-redundancy <N>  Stego redundancy 1-10 (default: 2). Higher = robust, lower = fast
  --jpeg-quality <N>       JPEG quality 1-100 (default: 90)
  --progressive            Use progressive JPEG encoding
  -v, --verbose            Print verbose output
  -d, --dmi <DMI>          AI-training restriction metadata (DMI value; emitted as canonical plus:DataMining)
  --metadata               Inject metadata (seed, DMI). Default: true for Light and Standard
  --legal-claims          Inject legal claims (copyright, usage terms)  only for content you own
  --copyright-holder <NAME>  Copyright holder name (e.g., 'Jane Doe' or 'Acme Corp')
  --creator <NAME>        Creator/author name (e.g., 'Jane Doe')
  --contact <EMAIL_OR_URL>  Contact email or URL for rights inquiries
  --rights-url <URL>      URL to full usage terms or license text
  --usage-terms <TEXT>    Brief usage terms summary (e.g., 'All rights reserved')
  --ai-constraints <TEXT>  AI-specific constraints (e.g., 'No training, no generation')
  --no-ai-training        Shorthand: prohibit AI/ML training and set default AI constraints
  --no-genai-training     Shorthand: prohibit generative AI training only
  --tdm-reserved          [DEPRECATED] Sets DMI ProhibitedSeeConstraints (TDMRep deferred)
  -k, --key <KEY>          Optional cryptographic key (hex string) for HMAC-SHA256 verification
  -j, --jobs <N>           Parallel jobs for batch processing (default: 1)
  --strict                 Exit with error if any warnings have error severity for the active profile
  -h, --help               Print help
  --version                Print version

Examples

# Basic protection with default settings
stegoeggo photo.jpg -o photo_protected.png

# Light protection (metadata + minimal stego)
stegoeggo art.png -o art_protected.png --level light

# With custom intensity and seed
stegoeggo image.jpg -o output.png -i 0.8 -s 12345

# Convert format while protecting
stegoeggo image.png -o image.jpg -f jpg

# WAF-optimized: fast processing with progressive JPEG
stegoeggo image.png -o image.jpg -f jpg --stego-redundancy 1 --jpeg-quality 85 --progressive

# WAF-optimized: PNG output, minimal latency
stegoeggo image.png -o protected.png --stego-redundancy 1

# With legal metadata (explicit claims)
stegoeggo my_art.png -o protected.png --legal-claims --level standard

# With full legal metadata — auto-enables legal claims (no --legal-claims needed)
stegoeggo my_art.png -o protected.png \
  --copyright-holder "Jane Doe" \
  --contact "jane@example.com" \
  --rights-url "https://example.com/license" \
  --usage-terms "All rights reserved" \
  --no-ai-training

# Quick AI-training restriction
stegoeggo photo.jpg -o protected.jpg --no-genai-training

# With cryptographic key for authenticated provenance
stegoeggo image.png -o output.png --key a1b2c3d4e5f6

# Verify protection
stegoeggo output.png --verify

Verification

Check if an image has been protected:

stegoeggo image.png --verify

Output examples:

# Protected image
Protected: Yes
Level: standard (id: 2)
Seed: 1234567890
Intensity: 0.50
Version: 2

# Unprotected image
Protected: No
This image does not contain a protection signature.

How It Works

1. Metadata Injection

The library injects rights-reservation and AI-training restriction metadata into image headers:

PNG: tEXt and iTXt chunks

  • X-Protection-Seed: Unique identifier for reproducibility
  • plus:DataMining: Canonical PLUS LDF DMI value (e.g., DMI-PROHIBITED-AIMLTRAINING)
  • Copyright/Contact/License: When legal claims enabled

JPEG: Comment markers and XMP packets

  • COM markers for text metadata
  • APP1 XMP packets with plus:DataMining (canonical) and legacy Iptc4xmpExt:DMI-* (parsed only)

WebP: EXIF and XML chunks

  • Similar metadata injection with XMP-based DMI

2. Steganography (Optional)

Hidden payloads embedded in images for redundant verification evidence:

PNG/WebP: LSB (Least Significant Bit) embedding

  • Payload embedded in the lowest bits of RGB channels
  • Redundant passes for verification robustness
  • Uses pseudo-random pixel selection based on seed

JPEG: DCT-based (F5-style) embedding

  • Seed embedded in quantization tables when those tables are preserved
  • DCT coefficient perturbation using F5-style no-zero variant
  • Pixel-domain JPEG fallback removed; JPEG protection now goes through the DCT fast path

Payload Structure:

The library writes payload v3 by default. Older payload versions (v1, v2) are still extracted for backward compatibility but are never written.

v3 core header (32 bytes):

Offset  Size  Field
0       2     Magic bytes ('S', 'E')
2       1     Version (3)
3       1     Header length (includes extensions and key ID)
4       2     Total payload length
6       8     Seed (little-endian)
14      2     Intensity (0–10000, little-endian)
16      1     DMI policy byte
17      8     Content hash (truncated)
25      1     Key ID length (0–32)
26      1     Auth algorithm (0=CRC32, 1=HMAC-SHA256, 2=Ed25519)
27      1     Auth tag length
28      2     Flags
30      2     Reserved

The v3 format supports TLV extensions for additional metadata and optionally carries an Ed25519 signature or HMAC-SHA256 authentication tag. The signatures feature adds 168 bytes of overhead for embedded Ed25519 signatures (64-byte signature + 36-byte public key extension + 68-byte detached signature extension).

Without an authentication key, the payload uses 3× repetition ECC with majority-vote decoding (src/protected/ecc.rs) for error recovery. With a MAC key, HMAC-SHA256 provides cryptographic integrity. With the signatures feature, Ed25519 provides non-repudiable signing.

Integration Architecture

Architecture Overview

+-----------------+     +-----------------+     +-----------------+
|   Image Source  |---->|   Protection    |---->|   Distribution  |
+-----------------+     |   Pipeline      |     +-----------------+
                        +-----------------+
                                |
                                |  1. Inject metadata markers (primary)
                                |  2. Embed steganographic markers (redundant)
                                |  3. Add legal claims (optional)

Verification

Programmatic Verification

use stegoeggo::{SteganographyProtector, MetadataTrapProtector};
use image::DynamicImage;

let protected_bytes = std::fs::read("protected.png").unwrap();
let img = image::load_from_memory(&protected_bytes).unwrap();

// Method 1: Steganography verification
let stego = SteganographyProtector::new();
if stego.verify_payload(&img) {
    println!("Image is protected by stegoeggo");

    // Extract payload details
    if let Some(payload) = stego.extract_payload(&img) {
        println!("Protection level: {}", payload.protection_level());
        println!("Seed: {}", payload.seed());
        println!("Intensity: {:.2}", payload.intensity());
        println!("Version: {}", payload.version());
    }
}

// Method 2: Extract seed from metadata
let seed = MetadataTrapProtector::extract_seed_from_image(&protected_bytes);
if let Some(seed) = seed {
    println!("Found protection seed: {}", seed);
}

// Method 3: Comprehensive legal notice verification (recommended)
let report = stegoeggo::verify_legal_notice(&protected_bytes, b"my-mac-key");
println!("Copyright holder: {:?}", report.copyright_holder());
println!("Evidence strength: {}", report.evidence_strength());
for channel in report.channels() {
    println!("  Channel: {}", channel);
}

Evidence Strength Levels

Level Meaning
NoNoticeFound No metadata or steganographic markers detected
MetadataNoticeOnly Legal notice metadata found, no stego payload verified
MetadataNoticeAndBestEffortStego Metadata + unauthenticated stego payload verified
MetadataNoticeAndAuthenticatedProvenance Metadata + MAC-authenticated stego payload verified

Evidence Channels

The NoticeVerification report lists which evidence channels were detected: PngText, PngXmp, JpegComment, JpegXmp, JpegIptc, WebPXmp, WebPExif, LsbPayload, DctPayload, QTableSeed.

JPEG Limitations

JPEG's lossy compression can destroy steganography payloads embedded in pixel data. This is an inherent limitation of the JPEG format and cannot be fully avoided.

Current behavior:

  • PNG/WebP: LSB steganography is fully supported and verifiable
  • JPEG: F5-style DCT steganography stores a seed in quantization tables when those tables are preserved and embeds payload bits in coefficients

Recommendations:

  • Use PNG output format for protected images when possible
  • For JPEG, a quantization-table seed is detection only; full verification relies on DCT payload integrity or metadata
  • The library automatically uses the best available extraction method
  • The CLI handles this and reports accordingly

Technical note: The library uses F5-style DCT embedding for JPEG. The quantization-table seed is useful when tables are preserved, but generic JPEG re-encoding can regenerate those tables and lose the seed.

Robustness & Survival

Different protection layers survive different image transformations. The truth, verified by the test suite in tests/robustness.rs and tests/robust_stego_matrix (in tests/robustness.rs):

What survives common transformations

Transformation Visible metadata (DMI, XMP, EXIF, COM) Q-table seed (JPEG) LSB stego payload (PNG/WebP) DCT stego payload (JPEG)
File copy / re-hosting Yes Yes Yes Yes
PNG <-> PNG re-encode Yes n/a Yes (spread-spectrum + ECC + majority vote) n/a
WebP lossless <-> WebP lossless Yes n/a Yes (same as PNG) n/a
WebP lossy (any re-encode) Yes n/a No (lossy codec destroys LSBs) n/a
JPEG -> JPEG via image crate encoder No (encoder strips COM/APP1) No (encoder rebuilds Q-tables) No (decoded to pixels) No
JPEG -> JPEG via stegoeggo fast path Yes (re-injected) Yes (re-injected) n/a Yes (DCT coeffs preserved)
Format conversion (PNG <-> JPEG) via image crate No No No No
Format conversion (WebP <-> JPEG) via image crate No n/a No n/a
Crop No (clipped) No Yes with with_tile_size() (>=1 intact tile) partial (tile-aligned crops without re-encode)
Resize No (resampled) No No No
Naive metadata strip No n/a Yes (still extractable) partial
LSB-preserving noise (e.g. contrast, brightness) Yes n/a Yes n/a
LSB-flipping noise (e.g. random LSB overwrites) Yes n/a No without ECC / partial with ECC n/a

Encoder reality check

The image crate (and most general-purpose JPEG encoders) do not preserve COM or APP1 markers, and rebuild standard Q-tables from scratch on every encode. This means the visible metadata channel and the Q-table seed channel are both single-encoding only when the image passes through a generic encoder. The stegoeggo custom transcoder (JpegTranscoder) preserves DCT coefficients and re-injects metadata, but only when the image is processed through process_image_bytes (not through an external re-encoder).

WebP caveat

stegoeggo uses LSB embedding for WebP, which only survives lossless WebP round-trips. The image crate's WebPEncoder::new_lossless preserves LSBs; lossy WebP re-encoding (the common web delivery path) destroys the LSB payload. If you serve protected WebP, configure your CDN to deliver lossless WebP, or convert protected output to PNG/JPEG-in-WebP-container with a tool that preserves the bitstream.

Recommendations

  • For maximum legal evidence: Use PNG output. The visible metadata + LSB stego payload survive almost everything except cropping, resizing, and re-encoding through a non-stegoeggo JPEG encoder. For crop resistance, add .with_tile_size(64) to the protection context — this embeds the payload in every 64x64 tile so any crop containing at least one full tile is recoverable.
  • For CDN/WAF deployment: Use Standard level with PNG output. JPEG output discards the LSB payload and visible metadata on every re-compression.
  • For authenticated provenance: Set a MAC key via with_mac_key() to cryptographically sign steganographic payloads.
  • For the strongest claims about evidence: Serve the protected image directly and reference its ISCC code. Don't rely on downstream consumers to preserve any of the embedded channels.

Honest threat model

The primary deterrence mechanism is visible metadata injection — canonical plus:DataMining rights signals, copyright, and structured COM markers. These are detectable by PLUS/XMP-aware scrapers and provide the strongest legal evidence when preserved. The steganographic payload is a bonus evidence channel: useful for proving the image was processed by this library at the point of distribution, but it is not designed to survive re-encoding through a general-purpose image pipeline. The library is a deterrent, not a forensic watermark.

Performance

Benchmarked on Apple M4 Pro (12 cores), version 0.2.

In-Memory Processing (DynamicImage path)

Image Size Light Standard
256x256 0.2 ms 0.2 ms
512x512 0.8 ms 0.8 ms
1024x1024 3.2 ms 3.1 ms
2560x2560 (2K) 18 ms 20 ms
3840x3840 (4K) 35 ms 40 ms

Bytes-in/Bytes-out Processing (production path for WAF/CDN)

PNG in / PNG out — the "maximum legal evidence" path:

Image Size Light Standard
512x512 0.7 ms 0.7 ms
2560x2560 (2K) 11 ms 13 ms
3840x3840 (4K) 25 ms 29 ms

JPEG Fast Path

JPEG-in / JPEG-out bypasses pixel decode entirely and operates directly on DCT coefficients:

Image Size Time
256x256 1.3 us
512x512 1.6 ms

Tiled Embedding (crop-resistant mode)

JPEG with with_tile_size(64):

Image Size Embed Extract
256x256 1.5 ms 270 ms
1024x1024 253 ms

Allocations

Standard protection at 512x512: 60 allocations, 5.7 MB peak.

Summary

  • <1 ms for images up to 512x512
  • <5 ms for images up to 1024x1024
  • <30 ms for 4K images (bytes path, Standard level)
  • JPEG fast path is sub-millisecond for small images

Technical Details

Image Format Support

Format Metadata Stego
PNG tEXt/iTXt LSB
JPEG COM/XMP/EXIF DCT (F5)
WebP EXIF/XML LSB

ISCC Computation

The library computes ISCC-like (Immutable Self-Certifying Constituent Content) identifiers for content identification. Note: these identifiers are not guaranteed to be interoperable with the standard ISCC specification — they use a custom DCT-based perceptual hash and SHA-256 instance code. They are suitable for in-application deduplication and provenance tracking, but should not be used for cross-ISCC-tool interoperability:

use stegoeggo::{compute_content_identifiers, Iscc};

let img = image::open("image.png").unwrap();
let iscc = compute_content_identifiers(&img);

println!("Content Code: {}", iscc.content);
println!("Data Code: {}", iscc.data);
println!("Instance Code: {}", iscc.instance);
println!("Full ISCC: {}", iscc.full);

Note: compute_iscc() is deprecated. Use compute_content_identifiers() instead.

The Iscc struct fields:

  • meta — optional metadata code (not set by default)
  • content — content-derived identifier (DCT-based perceptual hash)
  • data — data-derived identifier (raw file hash)
  • instance — identical to data (per-file identifier)
  • full — full ISCC URI (e.g., ISCC:...)

Error Handling

The library uses thiserror for error handling:

use stegoeggo::{Error, Result};

fn process() -> Result<image::DynamicImage> {
    // Operations that may fail
}

Common errors:

  • Error::ImageDecode(String) - Failed to decode image
  • Error::ImageEncode(String) - Failed to encode image
  • Error::Metadata(String) - Metadata injection failure

External References

Legal Notice Model

See docs/legal_notice_model.md for a detailed description of the legal notice and evidence model, including what metadata channels are embedded, what transformations commonly remove notices, and operational recommendations.

External Metadata Conformance

The conformance suite validates that protected images expose correct rights metadata to external tools. It uses a layered approach:

  1. Fixture manifest — machine-readable TOML manifest with SHA-256 digests, expected values, and provenance
  2. Manifest validation — structural checks (duplicate IDs, path traversal, invalid formats/categories, SHA-256 validity) run before any fixtures are processed
  3. Internal extractionverify_legal_notice() parses the image
  4. External extraction — ExifTool extracts metadata independently
  5. Namespace-aware XMP validation — xmllint validates XML structure
  6. Normalized comparison — internal and external results are compared field-by-field
  7. Coverage enforcement — strict mode requires explicit per-category and per-format minimums
  8. Machine-readable report — JSON output with per-check pass/fail/warn

Strict mode requires --manifest and evaluates per-fixture expectations from the manifest. The harness returns stable exit codes (0–5) for scripting.

Running Conformance Checks

# Build the conformance harness
cargo build --release --bin stegoeggo-conformance

# Run all fixtures with manifest verification (requires exiftool + xmllint)
./target/release/stegoeggo-conformance \
  --fixtures tests/fixtures/conformance \
  --manifest tests/fixtures/conformance/manifest.toml \
  --strict \
  --json report.json

# Or use the shell wrapper (checks for all required tools)
./scripts/verify_metadata_conformance.sh --strict --json report.json

Expected Field Visibility by Format

Field PNG (tEXt/XMP) JPEG (COM/XMP) WebP (XMP)
Copyright Copyright Comment: Copyright (c) ... dc:rights
Creator Creator Comment: Creator: ... dc:creator
Usage Terms UsageTerms Comment: UsageTerms: ... xmpRights:UsageTerms
Rights URL WebStatement Comment: WebStatement: ... xmpRights:WebStatement
AI Constraints AIConstraints Comment: AIConstraints: ... stegoeggo:AIConstraints
DMI Policy XMP-plus:DataMining XMP-plus:DataMining XMP-plus:DataMining

Caveats

  • ExifTool is the authoritative external parser. Other tools may not expose all XMP properties depending on namespace support.
  • PNG tEXt XML:com.adobe.xmp requires ExifTool to decode — plain xmllint cannot extract XMP from PNG containers.
  • JPEG COM markers are stegoeggo-specific and may not be visible in all tools.
  • WebP XMP visibility depends on the tool's support for dc:rights, dc:creator, xmpRights:*, and stegoeggo:* namespaces.

What Conformance Does and Does Not Prove

Proves:

  • Protected images expose correct rights metadata to external parsers (ExifTool)
  • XMP is well-formed and namespace-correct
  • Internal extraction matches external extraction field-by-field
  • Metadata survives re-processing (idempotence)
  • Unrelated metadata is preserved through the update path
  • Format writers produce semantically equivalent metadata (PNG vs JPEG vs WebP)

Does not prove:

  • Legal enforceability of embedded rights statements
  • That all external tools will parse every XMP namespace
  • That metadata survives arbitrary transformations (social media re-encoding, screenshots, aggressive cropping)
  • That steganographic payloads survive lossy compression
  • Compliance with any specific legal jurisdiction

Installing External Tools

The conformance suite requires exiftool and xmllint.

macOS (Homebrew):

brew install exiftool libxml2

Ubuntu/Debian:

sudo apt-get install libimage-exiftool-perl libxml2-utils

Fedora/RHEL:

sudo dnf install perl-Image-ExifTool libxml2

Arch Linux:

sudo pacman -S perl-image-exiftool libxml2

Adding Fixtures

  1. Place the image in the appropriate tests/fixtures/conformance/<category>/ directory
  2. Add an entry to tests/fixtures/conformance/manifest.toml with provenance, SHA-256 digest, and expected values
  3. Document provenance in tests/fixtures/conformance/README.md
  4. Verify: cargo run --bin stegoeggo-conformance -- --fixtures tests/fixtures/conformance --manifest tests/fixtures/conformance/manifest.toml --strict

Migration Guide

From v0.2 (legacy metadata)

v0.2 used ProtectionLevel::Light for metadata-only and ProtectionLevel::Standard for stego + metadata. The API is unchanged — process_image_bytes() and ProtectionContext work identically. The main difference is that v0.3 emits canonical PLUS DataMining properties instead of legacy Iptc4xmpExt:DMI-* tags.

Action required: None. v0.3 reads legacy metadata written by v0.2.

From ProtectionLevel to ProtectionRequest

ProtectionLevel still works but is deprecated for new code. The ProtectionRequest API provides finer control:

// Old (still works, deprecated):
use stegoeggo::{process_image_bytes, ProtectionContext, ProtectionLevel};
let ctx = ProtectionContext::new(0.5, 42);
let out = process_image_bytes(&bytes, ProtectionLevel::Standard, &ctx)?;

// New (preferred):
use stegoeggo::{process_request_bytes, ProtectionRequest, RightsPolicy};
let request = ProtectionRequest::metadata_only()
    .with_policy(RightsPolicy::ProhibitedAiMlTraining);
let out = process_request_bytes(&bytes, &request)?;

From compute_iscc() to compute_content_identifiers()

The ISCC API was renamed for accuracy:

// Old (deprecated):
let iscc = compute_iscc(&img);

// New:
let ids = compute_content_identifiers(&img);

From EvidenceProfile to ProtectionPreset

EvidenceProfile is deprecated. Use ProtectionPreset for preset-based configuration:

// Old (deprecated):
use stegoeggo::EvidenceProfile;
let ctx = ProtectionContext::new(0.5, 42)
    .with_evidence_profile(EvidenceProfile::LegalNoticeWithStego);

// New:
use stegoeggo::ProtectionPreset;
let request = ProtectionRequest::from_preset(ProtectionPreset::LegalNoticeWithStego);

From with_dmi() to RightsPolicy

The with_dmi() builder method is deprecated. Use RightsPolicy directly:

// Old (deprecated):
let ctx = ProtectionContext::new(0.5, 42)
    .with_dmi(DmiValue::ProhibitedAiMlTraining);

// New:
let request = ProtectionRequest::metadata_only()
    .with_policy(RightsPolicy::ProhibitedAiMlTraining);

From with_inject_legal_claims() / with_metadata_injection()

These three-state options are deprecated. Use ProtectionChannels:

// Old (deprecated):
let ctx = ProtectionContext::new(0.5, 42)
    .with_inject_legal_claims(true)
    .with_metadata_injection(false);

// New:
use stegoeggo::ProtectionChannels;
let channels = ProtectionChannels {
    rights_metadata: false,
    ..Default::default()
};
let request = ProtectionRequest::metadata_only().with_channels(channels);

For the full deprecation inventory, see DEPRECATIONS.md.

Contributor Checklist

Before submitting a change that affects metadata output:

  • Canonical writer test updated
  • Legacy reader test preserved
  • External fixture added or reviewed (tests/fixtures/conformance/)
  • Namespace-aware validation passes
  • Cross-format matrix passes (PNG, JPEG, WebP)
  • Preservation/idempotence passes
  • Strict external conformance passes (./scripts/verify_metadata_conformance.sh --strict)

Architecture

stegoeggo
+-- ProtectionPipeline        # Main orchestration
+-- Protector trait           # Strategy pattern for protectors
|   +-- PassthroughProtector      # No-op (Disabled level)
|   +-- MetadataTrapProtector     # Metadata injection (always)
|   +-- SteganographyProtector    # LSB/DCT embedding (Light: minimal, Standard: full)
+-- ProtectionLevel          # disabled -> light -> standard
+-- LegalMetadata            # Configurable legal metadata
+-- ProtectionContext        # Configuration for protection
+-- StegoPayload             # Extracted stego data

Steganography intensity by level:

  • Disabled: none
  • Light: minimal — Q-table seed (JPEG) or LSB redundancy=1 (PNG/WebP)
  • Standard: full — DCT F5 (JPEG) or LSB + ECC + spread-spectrum (PNG/WebP)

Safety & Ethics

This library uses #![forbid(unsafe_code)] throughout — no unsafe blocks exist in the library crate. All image processing is built on safe Rust with the image crate.

This library is designed to protect intellectual property from unauthorized AI training. It is intended for:

  • Protecting personal photos from being scraped
  • Defending artist portfolios from model training
  • Securing proprietary images on CDNs
  • Content owners who have not licensed their work for AI training

We do not endorse:

  • Using this library for malicious purposes
  • Circumventing legitimate AI services' terms of service
  • Applying restrictions to images you do not own or have rights to
  • Any use that violates applicable laws

This is a defensive tool for content protection, not an offensive weapon against AI systems.

Disclaimer: stegoeggo provides technical mechanisms for embedding metadata and steganographic markers. It does not provide legal advice. The effectiveness of rights-reservation metadata and AI-training restrictions depends on jurisdiction, applicable law, and the specific use case. Consult a qualified attorney for legal guidance regarding intellectual property protection.

License

MIT License - see LICENSE for details.

Contributing

Contributions are welcome! Please ensure:

  1. Tests pass: cargo test --all-features
  2. Code is formatted: cargo fmt --check
  3. No clippy warnings: cargo clippy --all-targets --all-features -- -D warnings
  4. Package builds: cargo package --workspace