palisade_errors/models.rs
1//! Dual-context error handling for honeypot systems with type-enforced trust boundaries.
2//!
3//! # Architecture
4//!
5//! This module separates public-facing error messages from internal diagnostic data
6//! using distinct types that cannot be confused at compile time:
7//!
8//! - `PublicContext`: Contains data safe for external display (truthful or deceptive)
9//! - `InternalContext`: Contains diagnostic data restricted to authenticated SOC access
10//! - `DualContextError`: Pairs these contexts with enforced consistency rules
11//!
12//! # Trust Boundary Enforcement
13//!
14//! The type system prevents accidental cross-boundary leakage:
15//! - `PublicContext` implements `Display` for external rendering
16//! - `InternalContext` implements `Display` as redacted placeholder only
17//! - No implicit conversions exist between these types
18//!
19//! # Feature Gates
20//!
21//! When `external_signaling` is disabled, `PublicTruth` variant is unavailable at
22//! compile time. This forces all external outputs to use `DeceptiveLie`, ensuring
23//! honeypot deployments cannot accidentally expose truthful diagnostic information.
24//!
25//! # Memory Safety Strategy
26//!
27//! Sensitive data receives best-effort clearing from memory on drop:
28//!
29//! 1. **Owned strings**: Cleared via `zeroize` crate (handles heap buffers)
30//! 2. **Compiler optimization**: Volatile writes prevent LLVM dead-store elimination
31//! 3. **Instruction ordering**: Compiler fences prevent reordering across security boundaries
32//!
33//! ## What This Does NOT Guarantee
34//!
35//! - **Hardware cache visibility**: Compiler fences do not flush CPU caches
36//! - **Cross-thread guarantees**: Other threads may observe old values in cache
37//! - **Allocator-level security**: Memory may be reallocated before physical clearing
38//! - **DMA or swap**: OS/hardware may have copied data before zeroization
39//!
40//! This protects against compiler optimizations and casual memory inspection.
41//! It does NOT provide HSM-grade secure memory wiping. For that, use platform-specific
42//! APIs (mlock, SecureZeroMemory, etc.) and dedicated secure allocators.
43
44use std::borrow::Cow;
45use std::fmt;
46use std::ptr;
47use std::sync::atomic::{compiler_fence, Ordering};
48use zeroize::{Zeroize, ZeroizeOnDrop};
49
50// ============================================================================
51// Capability Token for Sensitive Access Control
52// ============================================================================
53
54/// Capability token for accessing sensitive internal context data.
55///
56/// # Purpose
57///
58/// This zero-sized type serves as a proof-of-authority for accessing sensitive
59/// information via `InternalContext::expose_sensitive()`. Requiring this token:
60///
61/// 1. Forces explicit privilege acquisition (cannot call accidentally)
62/// 2. Makes sensitive access grep-able in codebase
63/// 3. Enables future RBAC or audit hook integration
64/// 4. Documents authority requirement in the type system
65///
66/// # Construction
67///
68/// Only constructible via `SocAccess::acquire()`, which should be called only in
69/// controlled contexts (authenticated logging pipelines, SOC-exclusive endpoints, etc.).
70///
71/// # Security Model
72///
73/// This is not cryptographic. An attacker with code execution can trivially construct
74/// this type. The purpose is **organizational process safety**: preventing accidental
75/// misuse by well-meaning developers, not preventing malicious actors.
76///
77/// # Example
78///
79/// ```ignore
80/// // In SOC-restricted logging code:
81/// let access = SocAccess::acquire();
82/// if let Some(sensitive) = context.expose_sensitive(&access) {
83/// secure_log_to_encrypted_siem(sensitive);
84/// }
85/// ```
86pub struct SocAccess(());
87
88impl SocAccess {
89 /// Acquire SOC access capability for sensitive data exposure.
90 ///
91 /// # Security Contract
92 ///
93 /// Caller must ensure this is invoked only in contexts where sensitive data
94 /// disclosure is authorized:
95 /// - Authenticated SOC dashboards with RBAC
96 /// - Encrypted internal logging pipelines
97 /// - Forensic analysis tools with access controls
98 ///
99 /// # Audit Recommendation
100 ///
101 /// Calls to this method should be logged separately for compliance auditing.
102 /// Consider wrapping this in a macro that logs the caller's location:
103 ///
104 /// ```ignore
105 /// macro_rules! acquire_soc_access {
106 /// () => {{
107 /// audit_log!("SOC access acquired at {}:{}", file!(), line!());
108 /// SocAccess::acquire()
109 /// }}
110 /// }
111 /// ```
112 #[inline]
113 pub fn acquire() -> Self {
114 Self(())
115 }
116}
117
118// ============================================================================
119// Core Context Classification
120// ============================================================================
121
122/// Internal context field classification for SOC-visible error data.
123///
124/// This type is wrapped in `InternalContext` newtype and never directly exposed
125/// to external systems. The wrapper prevents accidental leakage via Display trait.
126///
127/// # Variants
128///
129/// - `Diagnostic`: Standard internal error information for SOC analysis
130/// - `Sensitive`: Contains PII, credentials, file paths, or other high-value data
131/// - `Lie`: Deceptive content tracked internally (marked to prevent analyst confusion)
132///
133/// # Memory Model
134///
135/// Uses `Cow<'static, str>` to allow both:
136/// - Zero-allocation storage of compile-time string literals (`Cow::Borrowed`)
137/// - Runtime-allocated sensitive data that can be zeroized (`Cow::Owned`)
138///
139/// ## Assumption About Borrowed Data
140///
141/// By convention, `Cow::Borrowed` is assumed to contain non-sensitive data
142/// (typically string literals embedded in the binary). This assumption can be
143/// violated if someone uses `Box::leak()` to create 'static references to sensitive
144/// data, but this is not a supported pattern.
145///
146/// Only `Cow::Owned` variants receive zeroization. Borrowed static data cannot
147/// and need not be cleared (it's in the binary).
148///
149/// # No Clone/Copy Policy
150///
151/// Prevents unintended duplication of potentially sensitive diagnostic data.
152/// All access requires borrowing from the single owner.
153enum InternalContextField {
154 Diagnostic(Cow<'static, str>),
155 Sensitive(Cow<'static, str>),
156 Lie(Cow<'static, str>),
157}
158
159impl Zeroize for InternalContextField {
160 fn zeroize(&mut self) {
161 match self {
162 Self::Diagnostic(cow) | Self::Sensitive(cow) | Self::Lie(cow) => {
163 if let Cow::Owned(s) = cow {
164 s.zeroize();
165 }
166 }
167 }
168 }
169}
170
171impl ZeroizeOnDrop for InternalContextField {}
172
173impl Drop for InternalContextField {
174 fn drop(&mut self) {
175 // For sensitive variants with owned data, perform volatile write to prevent
176 // compiler from eliding the zeroization as a "dead store" optimization.
177 //
178 // GUARANTEES PROVIDED:
179 // - Prevents LLVM from removing the write as dead code
180 // - Ensures write completes before subsequent drop logic
181 //
182 // GUARANTEES NOT PROVIDED:
183 // - Does NOT guarantee CPU cache flushes
184 // - Does NOT prevent other threads from seeing old values
185 // - Does NOT prevent allocator from reusing memory before physical clear
186 // - Does NOT protect against swap, DMA, or hardware memory copies
187 //
188 // This is best-effort memory clearing for defense against casual inspection
189 // and compiler optimizations. Not suitable for cryptographic key material
190 // that requires HSM-grade wiping.
191 if let Self::Sensitive(cow) = &mut *self {
192 if let Cow::Owned(s) = cow {
193 // SAFETY:
194 // - We own this String and are in its Drop implementation
195 // - as_mut_ptr() returns valid pointer to the String's buffer
196 // - len() is correct and bounds-checked by Rust
197 // - We write only within allocated bounds (0..len)
198 // - Volatile writes prevent compiler optimization
199 unsafe {
200 let ptr = s.as_mut_ptr();
201 let len = s.len();
202 for i in 0..len {
203 ptr::write_volatile(ptr.add(i), 0u8);
204 }
205 }
206 };
207 }
208
209 // High-level zeroization via zeroize crate
210 self.zeroize();
211
212 // Compiler fence prevents reordering of instructions across this boundary.
213 // Ensures zeroization completes before any subsequent destructor logic.
214 //
215 // GUARANTEES PROVIDED:
216 // - Prevents compiler from reordering instructions
217 // - Sequential consistency for this thread's view
218 //
219 // GUARANTEES NOT PROVIDED:
220 // - Does NOT imply hardware memory barriers
221 // - Does NOT force cache coherence across CPU cores
222 // - Other threads may still observe old values in their caches
223 compiler_fence(Ordering::SeqCst);
224 }
225}
226
227/// Public context field classification for externally-visible error data.
228///
229/// This type is wrapped in `PublicContext` newtype and can only display data
230/// explicitly marked as safe or intentionally deceptive for external consumption.
231///
232/// # Variants
233///
234/// - `Truth`: Minimal truthful message safe for external display (feature-gated)
235/// - `Lie`: Intentionally false narrative for misleading attackers
236///
237/// # Feature Gate Behavior
238///
239/// `Truth` variant only exists when `external_signaling` feature is enabled.
240/// Without this feature, attempting to construct `PublicContext::truth()` will
241/// fail at compile time, forcing all external outputs to be deceptive.
242///
243/// # Memory Model
244///
245/// Same as `InternalContextField` - uses `Cow<'static, str>` for efficient
246/// storage of both static and dynamic strings. Borrowed data assumed non-sensitive.
247///
248/// # No Clone/Copy Policy
249///
250/// Prevents accidental propagation of deceptive narratives across system boundaries.
251enum PublicContextField {
252 #[cfg(feature = "external_signaling")]
253 Truth(Cow<'static, str>),
254 Lie(Cow<'static, str>),
255}
256
257impl Zeroize for PublicContextField {
258 fn zeroize(&mut self) {
259 match self {
260 #[cfg(feature = "external_signaling")]
261 Self::Truth(cow) => {
262 if let Cow::Owned(s) = cow {
263 s.zeroize();
264 }
265 }
266 Self::Lie(cow) => {
267 if let Cow::Owned(s) = cow {
268 s.zeroize();
269 }
270 }
271 }
272 }
273}
274
275impl ZeroizeOnDrop for PublicContextField {}
276
277// Note: No custom Drop implementation here. Public contexts rarely contain
278// sensitive data, and if they did, they should be in InternalContext instead.
279// ZeroizeOnDrop provides sufficient cleanup for owned strings.
280
281// ============================================================================
282// Type-Safe Context Wrappers
283// ============================================================================
284
285/// Type-safe wrapper for public-facing error contexts.
286///
287/// # Trust Boundary Enforcement
288///
289/// This newtype prevents `InternalContextField` from being accidentally displayed
290/// externally. The type system ensures only `PublicContextField` variants can be
291/// wrapped here, and the `Display` implementation is the sole external rendering path.
292///
293/// # Construction
294///
295/// - `lie()`: Always available for deceptive public messages
296/// - `truth()`: Only available with `external_signaling` feature enabled
297///
298/// # Safety Properties
299///
300/// 1. Cannot be constructed from `InternalContext`
301/// 2. Cannot implicitly convert to string (must use `as_str()` or `Display`)
302/// 3. Implements `ZeroizeOnDrop` for owned string data
303///
304/// # No Clone/Copy Policy
305///
306/// Single-owner semantics prevent duplicate public messages from existing
307/// simultaneously, reducing risk of inconsistent external responses.
308pub struct PublicContext(PublicContextField);
309
310impl PublicContext {
311 /// Create a deceptive public context for external display.
312 ///
313 /// # Use Case
314 ///
315 /// Default constructor for honeypot deployments. Deceptive messages are
316 /// explicitly labeled and auditable in internal logs.
317 ///
318 /// # Performance
319 ///
320 /// Accepts `Cow<'static, str>` to allow zero-allocation when passed string
321 /// literals: `PublicContext::lie("error")` allocates nothing.
322 #[inline]
323 pub fn lie(message: impl Into<Cow<'static, str>>) -> Self {
324 Self(PublicContextField::Lie(message.into()))
325 }
326
327 /// Create a truthful public context for external display.
328 ///
329 /// # Availability
330 ///
331 /// This method only exists when `external_signaling` feature is enabled.
332 /// Without this feature, all public contexts must be deceptive, enforcing
333 /// operational security at compile time rather than runtime configuration.
334 ///
335 /// # Use Case
336 ///
337 /// For honeypots that intentionally signal some authentic errors to appear
338 /// more legitimate (e.g., benign input validation failures).
339 #[cfg(feature = "external_signaling")]
340 #[inline]
341 pub fn truth(message: impl Into<Cow<'static, str>>) -> Self {
342 Self(PublicContextField::Truth(message.into()))
343 }
344
345 /// Get the external-safe string representation.
346 ///
347 /// # Returns
348 ///
349 /// Borrowed string slice suitable for HTTP responses, external APIs, or any
350 /// untrusted display context. This string may be deceptive.
351 ///
352 /// # Lifetime
353 ///
354 /// Returned reference borrows from self, valid until this context is dropped.
355 #[inline]
356 pub fn as_str(&self) -> &str {
357 match &self.0 {
358 #[cfg(feature = "external_signaling")]
359 PublicContextField::Truth(c) => c.as_ref(),
360 PublicContextField::Lie(c) => c.as_ref(),
361 }
362 }
363
364 /// Get classification label for internal audit trails.
365 ///
366 /// # Returns
367 ///
368 /// Static string identifying context type without exposing payload.
369 /// Useful for metrics, SOC dashboards, and audit log indexing.
370 ///
371 /// # Values
372 ///
373 /// - `"PublicTruth"`: Authentic message (requires feature flag)
374 /// - `"DeceptiveLie"`: Intentionally false message
375 #[inline]
376 pub const fn classification(&self) -> &'static str {
377 match &self.0 {
378 #[cfg(feature = "external_signaling")]
379 PublicContextField::Truth(_) => "PublicTruth",
380 PublicContextField::Lie(_) => "DeceptiveLie",
381 }
382 }
383}
384
385impl Zeroize for PublicContext {
386 fn zeroize(&mut self) {
387 self.0.zeroize();
388 }
389}
390
391impl ZeroizeOnDrop for PublicContext {}
392
393impl fmt::Display for PublicContext {
394 /// Render public context for external display.
395 ///
396 /// This is the primary interface for converting error contexts into
397 /// externally-visible strings (HTTP responses, external APIs, etc.).
398 ///
399 /// # Security Note
400 ///
401 /// This implementation is intentionally simple and does not check context
402 /// classification. The type system guarantees only `PublicContextField`
403 /// variants can be wrapped in this type, so all outputs are safe by construction.
404 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405 f.write_str(self.as_str())
406 }
407}
408
409impl fmt::Debug for PublicContext {
410 /// Debug representation for internal logging and diagnostics.
411 ///
412 /// # Redaction Strategy
413 ///
414 /// Deceptive payloads are redacted in debug output to prevent lies from
415 /// being aggregated as factual data in log analysis systems that may:
416 /// - Export logs to external SIEMs
417 /// - Send logs to cloud providers
418 /// - Aggregate metrics across trust boundaries
419 ///
420 /// This prevents deceptive error messages from polluting statistical analysis.
421 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422 match &self.0 {
423 #[cfg(feature = "external_signaling")]
424 PublicContextField::Truth(c) => write!(f, "PublicTruth({:?})", c),
425 PublicContextField::Lie(_) => write!(f, "DeceptiveLie([REDACTED])"),
426 }
427 }
428}
429
430/// Type-safe wrapper for internal-only error contexts.
431///
432/// # Trust Boundary Enforcement
433///
434/// This newtype ensures internal diagnostic data cannot be accidentally exposed
435/// externally. The `Display` implementation returns a redacted placeholder, not
436/// actual content, preventing misuse in external-facing code paths.
437///
438/// # Access Patterns
439///
440/// - `payload()`: Returns structured data for SOC logging (zero allocation)
441/// - `expose_sensitive()`: Returns raw sensitive content (requires `SocAccess` capability)
442///
443/// Both methods require conscious choice and cannot be used accidentally via
444/// generic string formatting.
445///
446/// # Memory Safety
447///
448/// Implements `ZeroizeOnDrop` to clear owned string data. Sensitive variants
449/// receive additional volatile write treatment in `InternalContextField::drop()`
450/// to prevent compiler optimization of the clearing operation.
451///
452/// # No Clone/Copy Policy
453///
454/// Single-owner semantics prevent sensitive diagnostic data from being duplicated
455/// across memory regions, reducing attack surface for memory inspection.
456pub struct InternalContext(InternalContextField);
457
458impl InternalContext {
459 /// Create a standard diagnostic internal context.
460 ///
461 /// # Use Case
462 ///
463 /// For typical internal error diagnostics that SOC analysts need but that
464 /// should never be exposed externally (stack traces, internal state, etc.).
465 ///
466 /// # Example
467 ///
468 /// ```ignore
469 /// InternalContext::diagnostic("SQL injection detected in /api/users?id=1' OR '1'='1")
470 /// ```
471 #[inline]
472 pub fn diagnostic(message: impl Into<Cow<'static, str>>) -> Self {
473 Self(InternalContextField::Diagnostic(message.into()))
474 }
475
476 /// Create a sensitive internal context with best-effort memory clearing.
477 ///
478 /// # Use Case
479 ///
480 /// For internal diagnostics containing:
481 /// - Personally identifiable information (PII)
482 /// - Credentials or API keys
483 /// - Filesystem paths that reveal system topology
484 /// - Database connection strings
485 /// - Any data that could aid an attacker
486 ///
487 /// # Memory Clearing Strategy
488 ///
489 /// When this context is dropped:
490 /// 1. Owned string data is cleared via `zeroize` crate
491 /// 2. Volatile writes prevent LLVM dead-store elimination
492 /// 3. Compiler fence prevents instruction reordering
493 ///
494 /// This provides best-effort clearing against casual memory inspection and
495 /// compiler optimizations. It does NOT provide guarantees against:
496 /// - Hardware cache persistence
497 /// - Allocator-level memory reuse
498 /// - Swap file or DMA copies
499 ///
500 /// For cryptographic key material, use dedicated secure allocators.
501 ///
502 /// # Example
503 ///
504 /// ```ignore
505 /// InternalContext::sensitive(format!("Failed login for username: {}", username))
506 /// ```
507 #[inline]
508 pub fn sensitive(message: impl Into<Cow<'static, str>>) -> Self {
509 Self(InternalContextField::Sensitive(message.into()))
510 }
511
512 /// Create an internal context marked as deceptive.
513 ///
514 /// # Use Case
515 ///
516 /// When internal logs themselves may be exfiltrated and you need to track
517 /// deceptive narratives without exposing them externally. The `payload()`
518 /// method will return this with a `Lie` marker to prevent SOC analysts from
519 /// treating it as authentic diagnostic data.
520 ///
521 /// # Distinction from PublicContext::lie()
522 ///
523 /// - `PublicContext::lie()`: For external consumption
524 /// - `InternalContext::lie()`: For internal tracking of deception operations
525 ///
526 /// # Example
527 ///
528 /// ```ignore
529 /// InternalContext::lie("Normal database query executed successfully")
530 /// ```
531 #[inline]
532 pub fn lie(message: impl Into<Cow<'static, str>>) -> Self {
533 Self(InternalContextField::Lie(message.into()))
534 }
535
536 /// Get classification label for logging and metrics.
537 ///
538 /// # Returns
539 ///
540 /// Static string identifying the context type:
541 /// - `"InternalDiagnostic"`: Standard internal error information
542 /// - `"Sensitive"`: Contains high-value data requiring special handling
543 /// - `"InternalLie"`: Deceptive content tracked internally
544 ///
545 /// # Use Case
546 ///
547 /// For indexing in log aggregation systems, metrics collection, or
548 /// routing different context types to different storage backends.
549 #[inline]
550 pub const fn classification(&self) -> &'static str {
551 match &self.0 {
552 InternalContextField::Diagnostic(_) => "InternalDiagnostic",
553 InternalContextField::Sensitive(_) => "Sensitive",
554 InternalContextField::Lie(_) => "InternalLie",
555 }
556 }
557
558 /// Get structured payload for internal logging without heap allocation.
559 ///
560 /// # Returns
561 ///
562 /// - `Some(InternalPayload::Truth(_))`: For diagnostic contexts
563 /// - `Some(InternalPayload::Lie(_))`: For lie contexts (marked for SOC awareness)
564 /// - `None`: For sensitive contexts (use `expose_sensitive()` instead)
565 ///
566 /// # Performance
567 ///
568 /// Zero allocation. The returned `InternalPayload` borrows directly from the
569 /// underlying `Cow<'static, str>`. Loggers can format this without heap use:
570 ///
571 /// ```ignore
572 /// match context.payload() {
573 /// Some(payload) => println!("{}", payload), // No allocation
574 /// None => println!("[SENSITIVE REDACTED]"),
575 /// }
576 /// ```
577 ///
578 /// # Rationale
579 ///
580 /// Previous design allocated `format!("[LIE] {}", msg)` on every access.
581 /// This approach defers formatting to the logger, allowing:
582 /// - Zero-copy access to underlying data
583 /// - Logger-controlled formatting policy
584 /// - Better performance under high error rates
585 #[inline]
586 pub fn payload(&self) -> Option<InternalPayload<'_>> {
587 match &self.0 {
588 InternalContextField::Diagnostic(c) => Some(InternalPayload::Truth(c.as_ref())),
589 InternalContextField::Sensitive(_) => None,
590 InternalContextField::Lie(c) => Some(InternalPayload::Lie(c.as_ref())),
591 }
592 }
593
594 /// Expose raw sensitive content with capability-based access control.
595 ///
596 /// # Access Control
597 ///
598 /// Requires `SocAccess` capability token, which forces:
599 /// 1. Explicit privilege acquisition (cannot call accidentally)
600 /// 2. Grep-able sensitive data access points
601 /// 3. Future integration with RBAC or audit systems
602 ///
603 /// # Security Contract
604 ///
605 /// Caller must ensure returned data is sent ONLY to:
606 /// - Authenticated, encrypted, access-controlled endpoints
607 /// - SOC-exclusive dashboards with strict RBAC
608 /// - Encrypted internal logging with key rotation
609 /// - Forensic analysis workstations with air-gapped storage
610 ///
611 /// Never send to:
612 /// - External SIEMs or cloud logging services
613 /// - Unencrypted log files
614 /// - Monitoring services that aggregate across trust boundaries
615 ///
616 /// # Returns
617 ///
618 /// - `Some(&str)`: Raw sensitive content (if this is a Sensitive context)
619 /// - `None`: If this is not a Sensitive context
620 ///
621 /// # Why #[must_use]
622 ///
623 /// Forces caller to explicitly handle the returned sensitive data rather than
624 /// accidentally discarding it (which might indicate a logic error).
625 ///
626 /// # Example
627 ///
628 /// ```ignore
629 /// let access = SocAccess::acquire();
630 /// if let Some(sensitive) = context.expose_sensitive(&access) {
631 /// send_to_encrypted_soc_siem(sensitive);
632 /// }
633 /// ```
634 #[must_use]
635 #[inline]
636 pub fn expose_sensitive(&self, _access: &SocAccess) -> Option<&str> {
637 match &self.0 {
638 InternalContextField::Sensitive(c) => Some(c.as_ref()),
639 _ => None,
640 }
641 }
642}
643
644impl Zeroize for InternalContext {
645 fn zeroize(&mut self) {
646 self.0.zeroize();
647 }
648}
649
650impl ZeroizeOnDrop for InternalContext {}
651
652impl fmt::Display for InternalContext {
653 /// Display implementation for internal contexts.
654 ///
655 /// # Security Policy
656 ///
657 /// This ALWAYS returns a redacted placeholder, never actual content.
658 /// Internal contexts should not be formatted for external display under
659 /// any circumstances. This implementation exists only to satisfy trait
660 /// bounds in generic code.
661 ///
662 /// # Correct Usage
663 ///
664 /// - Use `payload()` for SOC logging
665 /// - Use `expose_sensitive()` for controlled sensitive access
666 /// - Do NOT use `Display` or `ToString` on internal contexts
667 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
668 f.write_str("[INTERNAL CONTEXT REDACTED]")
669 }
670}
671
672impl fmt::Debug for InternalContext {
673 /// Debug representation for internal development and diagnostics.
674 ///
675 /// # Redaction Policy
676 ///
677 /// - Diagnostic: Shows full content (for debugging)
678 /// - Sensitive: Redacted (to prevent accidental logging)
679 /// - Lie: Redacted (to prevent aggregation as factual data)
680 ///
681 /// # Use Case
682 ///
683 /// Primarily for unit tests and local development. Production logging should
684 /// use `payload()` or `expose_sensitive()` for explicit control.
685 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
686 match &self.0 {
687 InternalContextField::Diagnostic(c) => write!(f, "InternalDiagnostic({:?})", c),
688 InternalContextField::Sensitive(_) => write!(f, "Sensitive([REDACTED])"),
689 InternalContextField::Lie(_) => write!(f, "InternalLie([REDACTED])"),
690 }
691 }
692}
693
694/// Zero-allocation internal payload for SOC logging.
695///
696/// Returned by `InternalContext::payload()`. This type borrows from the
697/// underlying context and allows loggers to format output without heap allocation.
698///
699/// # Variants
700///
701/// - `Truth(&str)`: Authentic diagnostic message
702/// - `Lie(&str)`: Deceptive message (should be prefixed in logs)
703///
704/// # Performance
705///
706/// The borrowed lifetime ties this to the parent `InternalContext`, preventing
707/// accidental persistence of sensitive data beyond the logging operation.
708///
709/// # No Copy Policy
710///
711/// While this type only contains `&str` (which is Copy), we deliberately do not
712/// derive Copy to maintain consistency with the module's no-duplication philosophy.
713/// Use `Clone` if you need to store the payload temporarily.
714///
715/// # Usage Pattern
716///
717/// ```ignore
718/// match context.payload() {
719/// Some(InternalPayload::Truth(msg)) => soc_log!("DIAG: {}", msg),
720/// Some(InternalPayload::Lie(msg)) => soc_log!("LIE: {}", msg),
721/// None => {
722/// // Sensitive - requires explicit access
723/// let access = SocAccess::acquire();
724/// if let Some(sensitive) = context.expose_sensitive(&access) {
725/// secure_log_encrypted(sensitive);
726/// }
727/// }
728/// }
729/// ```
730#[derive(Debug, Clone)]
731pub enum InternalPayload<'a> {
732 Truth(&'a str),
733 Lie(&'a str),
734}
735
736impl<'a> InternalPayload<'a> {
737 /// Get the raw message content without classification prefix.
738 ///
739 /// # Returns
740 ///
741 /// Borrowed string slice from the parent context. Valid until the
742 /// `InternalContext` is dropped.
743 ///
744 /// # Note
745 ///
746 /// This does NOT include `[LIE]` prefix. Use the `Display` implementation
747 /// if you want formatted output with classification markers.
748 #[inline]
749 pub const fn as_str(&self) -> &'a str {
750 match self {
751 Self::Truth(s) | Self::Lie(s) => s,
752 }
753 }
754
755 /// Check if this payload represents deceptive content.
756 ///
757 /// # Returns
758 ///
759 /// - `true`: This is a lie, should be marked in logs
760 /// - `false`: This is authentic diagnostic data
761 ///
762 /// # Use Case
763 ///
764 /// For conditional log routing or metrics collection based on deception status.
765 #[inline]
766 pub const fn is_lie(&self) -> bool {
767 matches!(self, Self::Lie(_))
768 }
769}
770
771impl<'a> fmt::Display for InternalPayload<'a> {
772 /// Format payload with classification prefix for logging.
773 ///
774 /// # Output Format
775 ///
776 /// - Truth: Raw message (no prefix)
777 /// - Lie: `[LIE] {message}`
778 ///
779 /// # Rationale
780 ///
781 /// The `[LIE]` prefix prevents SOC analysts from mistaking deceptive content
782 /// for authentic diagnostic data when reviewing logs. This is critical when
783 /// logs may be exported to systems that lack context classification.
784 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
785 match self {
786 Self::Truth(s) => f.write_str(s),
787 Self::Lie(s) => write!(f, "[LIE] {}", s),
788 }
789 }
790}
791
792// ============================================================================
793// Operation Category
794// ============================================================================
795
796/// Operation category for contextualizing errors without revealing architecture.
797///
798/// Categories are intentionally broad to provide SOC operators with sufficient
799/// signal for triage and response while preventing attackers from mapping internal
800/// system topology through error analysis.
801///
802/// # Design Principle
803///
804/// Each category represents a functional domain rather than a specific component.
805/// This prevents errors from revealing:
806/// - Service boundaries
807/// - Technology stack details
808/// - Deployment architecture
809/// - Internal naming conventions
810///
811/// # Copy Semantics Exception
812///
813/// This is the only type in this module that implements Copy. Rationale:
814/// - Small fieldless enum (single byte in practice)
815/// - No owned or sensitive data
816/// - Copying is cheaper than reference passing (no indirection)
817/// - Frequently passed by value in error construction
818///
819/// # Deception Support
820///
821/// Honeypot-specific categories (Deception, Detection, Containment) have
822/// deceptive display names that mask defensive operations as routine work.
823#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
824pub enum OperationCategory {
825 /// Configuration parsing, validation, or application
826 Configuration,
827 /// Artifact deployment, management, or versioning
828 Deployment,
829 /// Event collection, monitoring, or observability
830 Monitoring,
831 /// Rule evaluation, scoring, or decision logic
832 Analysis,
833 /// Action execution, remediation, or enforcement
834 Response,
835 /// Logging, audit trail, or compliance recording
836 Audit,
837 /// System-level operations (process, memory, OS interaction)
838 System,
839 /// File system or network I/O operations
840 IO,
841 /// Honeypot luring, facade maintenance, or attacker engagement
842 Deception,
843 /// Intrusion detection, anomaly identification, or threat recognition
844 Detection,
845 /// Attack isolation, sandboxing, or containment operations
846 Containment,
847}
848
849impl OperationCategory {
850 /// Get the authentic display name for this category.
851 ///
852 /// # Returns
853 ///
854 /// Static string describing the operation domain. No allocation, no Unicode
855 /// edge cases. Safe for all internal logging and SOC dashboards.
856 ///
857 /// # Performance
858 ///
859 /// Const function compiled to direct pointer return. Zero runtime cost.
860 #[inline]
861 pub const fn display_name(&self) -> &'static str {
862 match self {
863 Self::Configuration => "Configuration",
864 Self::Deployment => "Deployment",
865 Self::Monitoring => "Monitoring",
866 Self::Analysis => "Analysis",
867 Self::Response => "Response",
868 Self::Audit => "Audit",
869 Self::System => "System",
870 Self::IO => "I/O",
871 Self::Deception => "Deception",
872 Self::Detection => "Detection",
873 Self::Containment => "Containment",
874 }
875 }
876
877 /// Get deceptive display name for external contexts.
878 ///
879 /// # Returns
880 ///
881 /// Static string that masks honeypot-specific operations as routine work:
882 /// - Deception → "Routine Operation"
883 /// - Detection → "Routine Operation"
884 /// - Containment → "Routine Operation"
885 /// - All others → Same as `display_name()`
886 ///
887 /// # Use Case
888 ///
889 /// For external error messages where revealing defensive operations would
890 /// compromise the honeypot's effectiveness. Generic labels prevent attackers
891 /// from identifying which interactions triggered defensive responses.
892 ///
893 /// # Performance
894 ///
895 /// Const function, zero runtime cost. Compiled to direct pointer return.
896 #[inline]
897 pub const fn deceptive_name(&self) -> &'static str {
898 match self {
899 Self::Deception | Self::Detection | Self::Containment => "Routine Operation",
900 _ => self.display_name(),
901 }
902 }
903}
904
905// ============================================================================
906// Dual-Context Error with Invariant Enforcement
907// ============================================================================
908
909/// Dual-context error model for honeypot systems with constructor-enforced invariants.
910///
911/// # Type Safety Guarantees
912///
913/// 1. Public and internal contexts use distinct wrapper types (cannot be confused)
914/// 2. Fields are private (all construction goes through validated constructors)
915/// 3. Constructors enforce semantic consistency rules at creation time
916///
917/// # Enforced Invariants
918///
919/// - Public truth requires internal truth (no internal lies when external truth)
920/// - Public lie allows any internal context (deception is flexible)
921/// - Sensitive data flows only through InternalContext (type system prevents external leakage)
922///
923/// # Constructor Selection
924///
925/// - `with_lie()`: Public deception + internal diagnostic (most common)
926/// - `with_lie_and_sensitive()`: Public deception + best-effort cleared sensitive internal
927/// - `with_truth()`: Public truth + internal truth (feature-gated, enforces consistency)
928/// - `with_double_lie()`: Public deception + internal deception (for log exfiltration scenarios)
929///
930/// # Memory Management
931///
932/// Implements `ZeroizeOnDrop` to clear all owned string data. Sensitive contexts
933/// receive additional volatile write treatment in `InternalContextField::drop()`
934/// to prevent LLVM from eliding the zeroization as a dead-store optimization.
935///
936/// This provides best-effort memory clearing but does not guarantee:
937/// - Hardware cache flushes
938/// - Cross-thread memory visibility
939/// - Protection against allocator reuse before physical clear
940///
941/// # No Clone/Copy Policy
942///
943/// Single-owner semantics prevent:
944/// - Duplicate error contexts in memory (reduced attack surface)
945/// - Inconsistent public/internal message pairs
946/// - Accidental persistence of sensitive data across scopes
947pub struct DualContextError {
948 public: PublicContext,
949 internal: InternalContext,
950 category: OperationCategory,
951}
952
953impl DualContextError {
954 /// Internal constructor from pre-built contexts.
955 ///
956 /// This is crate-private to preserve external API invariants.
957 #[inline]
958 pub(crate) fn new(
959 public: PublicContext,
960 internal: InternalContext,
961 category: OperationCategory,
962 ) -> Self {
963 Self {
964 public,
965 internal,
966 category,
967 }
968 }
969
970 /// Create error with public deception and internal diagnostic.
971 ///
972 /// # Use Case
973 ///
974 /// Standard constructor for honeypot deployments. External attackers see
975 /// deceptive error message while SOC analysts see actual diagnostic data.
976 ///
977 /// # Invariant
978 ///
979 /// Public message is explicitly marked as `DeceptiveLie`. Internal message
980 /// is authentic diagnostic data for SOC analysis.
981 ///
982 /// # Example
983 ///
984 /// ```ignore
985 /// DualContextError::with_lie(
986 /// "Permission denied", // Attacker sees generic error
987 /// "Blocked SQL injection attempt: UNION SELECT detected in query parameter 'id'",
988 /// OperationCategory::Detection,
989 /// )
990 /// ```
991 ///
992 /// # Performance
993 ///
994 /// Zero allocation if string literals are passed. `Into<Cow<'static, str>>`
995 /// allows both literals and owned strings without forcing allocation.
996 #[inline]
997 pub fn with_lie(
998 public_lie: impl Into<Cow<'static, str>>,
999 internal_diagnostic: impl Into<Cow<'static, str>>,
1000 category: OperationCategory,
1001 ) -> Self {
1002 Self {
1003 public: PublicContext::lie(public_lie),
1004 internal: InternalContext::diagnostic(internal_diagnostic),
1005 category,
1006 }
1007 }
1008
1009 /// Create error with public deception and sensitive internal data.
1010 ///
1011 /// # Use Case
1012 ///
1013 /// When internal diagnostic contains PII, credentials, file paths, or other
1014 /// high-value data requiring best-effort memory clearing on drop.
1015 ///
1016 /// # Memory Clearing Strategy
1017 ///
1018 /// When this error is dropped, sensitive data receives:
1019 /// 1. High-level clearing via `zeroize` crate
1020 /// 2. Volatile writes to prevent compiler optimization
1021 /// 3. Compiler fence to prevent instruction reordering
1022 ///
1023 /// This provides best-effort defense against casual memory inspection and
1024 /// compiler optimizations. See module-level docs for limitations.
1025 ///
1026 /// # Example
1027 ///
1028 /// ```ignore
1029 /// DualContextError::with_lie_and_sensitive(
1030 /// "Resource not found",
1031 /// format!("Attempted access to restricted path: /var/secrets/api_keys.txt by user {}", username),
1032 /// OperationCategory::IO,
1033 /// )
1034 /// ```
1035 ///
1036 /// # Invariant
1037 ///
1038 /// Public message is deceptive. Internal message is marked sensitive and
1039 /// will be redacted in most logging contexts (requires explicit access via
1040 /// `expose_sensitive()` with `SocAccess` capability).
1041 #[inline]
1042 pub fn with_lie_and_sensitive(
1043 public_lie: impl Into<Cow<'static, str>>,
1044 internal_sensitive: impl Into<Cow<'static, str>>,
1045 category: OperationCategory,
1046 ) -> Self {
1047 Self {
1048 public: PublicContext::lie(public_lie),
1049 internal: InternalContext::sensitive(internal_sensitive),
1050 category,
1051 }
1052 }
1053
1054 /// Create error with public truth and internal diagnostic.
1055 ///
1056 /// # Availability
1057 ///
1058 /// Only available when `external_signaling` feature is enabled. Without this
1059 /// feature, all public contexts must be deceptive (compile-time enforcement).
1060 ///
1061 /// # Invariant Enforcement
1062 ///
1063 /// When telling truth externally, internal context must also be truthful.
1064 /// This constructor enforces semantic consistency - you cannot lie internally
1065 /// while being honest externally.
1066 ///
1067 /// # Use Case
1068 ///
1069 /// For honeypots that intentionally signal some authentic errors to appear
1070 /// more legitimate (e.g., benign validation failures that don't reveal
1071 /// defensive posture).
1072 ///
1073 /// # Example
1074 ///
1075 /// ```ignore
1076 /// DualContextError::with_truth(
1077 /// "Invalid JSON format",
1078 /// "JSON parse error at line 42, column 15: expected closing brace",
1079 /// OperationCategory::Configuration,
1080 /// )
1081 /// ```
1082 #[cfg(feature = "external_signaling")]
1083 #[inline]
1084 pub fn with_truth(
1085 public_truth: impl Into<Cow<'static, str>>,
1086 internal_diagnostic: impl Into<Cow<'static, str>>,
1087 category: OperationCategory,
1088 ) -> Self {
1089 Self {
1090 public: PublicContext::truth(public_truth),
1091 internal: InternalContext::diagnostic(internal_diagnostic),
1092 category,
1093 }
1094 }
1095
1096 /// Create error where both public and internal contexts are deceptive.
1097 ///
1098 /// # Use Case
1099 ///
1100 /// Advanced deception scenarios where even internal logs may be exfiltrated
1101 /// by sophisticated attackers. Both contexts contain lies, but the internal
1102 /// lie is marked to prevent SOC analysts from treating it as authentic.
1103 ///
1104 /// # Behavior
1105 ///
1106 /// - External systems see the public lie (generic error)
1107 /// - Internal logs show internal lie prefixed with `[LIE]` marker
1108 /// - SOC analysts are warned not to trust this diagnostic data
1109 ///
1110 /// # Example
1111 ///
1112 /// ```ignore
1113 /// DualContextError::with_double_lie(
1114 /// "Service temporarily unavailable",
1115 /// "Routine maintenance window in progress",
1116 /// OperationCategory::System,
1117 /// )
1118 /// ```
1119 ///
1120 /// # Rationale
1121 ///
1122 /// In environments where log exfiltration is a threat (compromised SIEM,
1123 /// malicious cloud provider, etc.), truthful internal logs become a liability.
1124 /// This constructor allows full deception while maintaining analyst awareness.
1125 #[inline]
1126 pub fn with_double_lie(
1127 public_lie: impl Into<Cow<'static, str>>,
1128 internal_lie: impl Into<Cow<'static, str>>,
1129 category: OperationCategory,
1130 ) -> Self {
1131 Self {
1132 public: PublicContext::lie(public_lie),
1133 internal: InternalContext::lie(internal_lie),
1134 category,
1135 }
1136 }
1137
1138 /// Get the public context (safe for external display).
1139 ///
1140 /// # Returns
1141 ///
1142 /// Borrowed reference to `PublicContext`, which can be used with `Display`
1143 /// trait to render external error messages.
1144 ///
1145 /// # Lifetime
1146 ///
1147 /// Reference borrows from self, valid until this error is dropped.
1148 #[inline]
1149 pub const fn public(&self) -> &PublicContext {
1150 &self.public
1151 }
1152
1153 /// Get the internal context (SOC-only visibility).
1154 ///
1155 /// # Returns
1156 ///
1157 /// Borrowed reference to `InternalContext`. Use `payload()` or
1158 /// `expose_sensitive()` methods for accessing content.
1159 ///
1160 /// # Security Note
1161 ///
1162 /// Do NOT use `Display` or `ToString` on this reference. Those implementations
1163 /// return redacted placeholders. Use explicit accessor methods instead.
1164 #[inline]
1165 pub const fn internal(&self) -> &InternalContext {
1166 &self.internal
1167 }
1168
1169 /// Get the operation category.
1170 ///
1171 /// # Returns
1172 ///
1173 /// Copy of the `OperationCategory` enum. Can be used with `display_name()`
1174 /// for internal logging or `deceptive_name()` for external contexts.
1175 #[inline]
1176 pub const fn category(&self) -> OperationCategory {
1177 self.category
1178 }
1179
1180 /// Get the external-facing error message as a string.
1181 ///
1182 /// # Returns
1183 ///
1184 /// Borrowed string slice suitable for HTTP responses, external APIs, or any
1185 /// untrusted context. May be deceptive depending on constructor used.
1186 ///
1187 /// # Use Case
1188 ///
1189 /// Primary method for rendering errors to external clients:
1190 /// ```ignore
1191 /// let error = DualContextError::with_lie(...);
1192 /// http_response.body(error.external_message());
1193 /// ```
1194 ///
1195 /// # Performance
1196 ///
1197 /// Returns borrowed reference, no allocation. Delegates to
1198 /// `PublicContext::as_str()` which in turn delegates to `Cow::as_ref()`.
1199 #[inline]
1200 pub fn external_message(&self) -> &str {
1201 self.public.as_str()
1202 }
1203
1204 /// Get the deceptive category name for external display.
1205 ///
1206 /// # Returns
1207 ///
1208 /// Static string that masks honeypot operations as generic activity.
1209 /// Honeypot-specific categories return "Routine Operation".
1210 ///
1211 /// # Use Case
1212 ///
1213 /// For structured error responses where category field is included:
1214 /// ```ignore
1215 /// json!({
1216 /// "error": error.external_message(),
1217 /// "category": error.external_category(),
1218 /// })
1219 /// ```
1220 ///
1221 /// # Performance
1222 ///
1223 /// Const function, compiles to direct pointer return. Zero runtime cost.
1224 #[inline]
1225 pub fn external_category(&self) -> &'static str {
1226 self.category.deceptive_name()
1227 }
1228}
1229
1230impl Zeroize for DualContextError {
1231 fn zeroize(&mut self) {
1232 self.public.zeroize();
1233 self.internal.zeroize();
1234 // category is Copy, contains no sensitive data, no zeroization needed
1235 }
1236}
1237
1238impl ZeroizeOnDrop for DualContextError {}
1239
1240// Note: No custom Drop implementation here. Zeroization is handled authoritatively
1241// in InternalContextField::drop() for sensitive data. This layer just delegates
1242// via ZeroizeOnDrop trait. Consolidating the volatile writes and fences to a single
1243// location (the base field type) reduces complexity and prevents redundant operations.
1244
1245impl fmt::Display for DualContextError {
1246 /// Render error for external display.
1247 ///
1248 /// # Behavior
1249 ///
1250 /// Displays only the public context. Internal diagnostic data is never
1251 /// included in this output.
1252 ///
1253 /// # Use Case
1254 ///
1255 /// For generic error handling where you want automatic string conversion:
1256 /// ```ignore
1257 /// println!("Error occurred: {}", error);
1258 /// ```
1259 ///
1260 /// # Security Note
1261 ///
1262 /// This is safe for external use. Only public context is rendered, which
1263 /// can only contain `PublicTruth` or `DeceptiveLie` - never internal diagnostics.
1264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1265 write!(f, "{}", self.public)
1266 }
1267}
1268
1269impl fmt::Debug for DualContextError {
1270 /// Debug representation for development and internal diagnostics.
1271 ///
1272 /// # Output Format
1273 ///
1274 /// Structured representation showing all three fields:
1275 /// - public: May be redacted if deceptive
1276 /// - internal: May be redacted if sensitive
1277 /// - category: Always shown
1278 ///
1279 /// # Use Case
1280 ///
1281 /// For unit tests, local development logging, and internal debugging.
1282 /// Not intended for production SOC logging (use explicit accessors instead).
1283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1284 f.debug_struct("DualContextError")
1285 .field("public", &self.public)
1286 .field("internal", &self.internal)
1287 .field("category", &self.category)
1288 .finish()
1289 }
1290}
1291
1292// ============================================================================
1293// Tests
1294// ============================================================================
1295
1296#[cfg(test)]
1297mod tests {
1298 use super::*;
1299
1300 #[test]
1301 fn test_public_context_lie() {
1302 let ctx = PublicContext::lie("Permission denied");
1303 assert_eq!(ctx.as_str(), "Permission denied");
1304 assert_eq!(ctx.classification(), "DeceptiveLie");
1305 }
1306
1307 #[cfg(feature = "external_signaling")]
1308 #[test]
1309 fn test_public_context_truth() {
1310 let ctx = PublicContext::truth("Invalid input format");
1311 assert_eq!(ctx.as_str(), "Invalid input format");
1312 assert_eq!(ctx.classification(), "PublicTruth");
1313 }
1314
1315 #[test]
1316 fn test_internal_context_diagnostic() {
1317 let ctx = InternalContext::diagnostic("SQL injection detected in /api/users");
1318 assert_eq!(ctx.classification(), "InternalDiagnostic");
1319
1320 match ctx.payload() {
1321 Some(InternalPayload::Truth(msg)) => {
1322 assert_eq!(msg, "SQL injection detected in /api/users");
1323 }
1324 _ => panic!("Expected truth payload"),
1325 }
1326 }
1327
1328 #[test]
1329 fn test_internal_context_sensitive() {
1330 let ctx = InternalContext::sensitive("/etc/passwd accessed by user:admin");
1331 assert_eq!(ctx.classification(), "Sensitive");
1332 assert!(ctx.payload().is_none());
1333
1334 let access = SocAccess::acquire();
1335 let exposed = ctx.expose_sensitive(&access);
1336 assert_eq!(exposed, Some("/etc/passwd accessed by user:admin"));
1337 }
1338
1339 #[test]
1340 fn test_internal_context_lie() {
1341 let ctx = InternalContext::lie("Normal database query");
1342 assert_eq!(ctx.classification(), "InternalLie");
1343
1344 match ctx.payload() {
1345 Some(InternalPayload::Lie(msg)) => {
1346 assert_eq!(msg, "Normal database query");
1347 }
1348 _ => panic!("Expected lie payload"),
1349 }
1350 }
1351
1352 #[test]
1353 fn test_internal_payload_display() {
1354 let truth = InternalPayload::Truth("test message");
1355 assert_eq!(format!("{}", truth), "test message");
1356
1357 let lie = InternalPayload::Lie("test message");
1358 assert_eq!(format!("{}", lie), "[LIE] test message");
1359 }
1360
1361 #[test]
1362 fn test_dual_context_with_lie() {
1363 let err = DualContextError::with_lie(
1364 "Access forbidden",
1365 "Honeypot triggered: multiple failed auth attempts",
1366 OperationCategory::Detection,
1367 );
1368
1369 assert_eq!(err.external_message(), "Access forbidden");
1370 assert_eq!(err.external_category(), "Routine Operation");
1371 }
1372
1373 #[test]
1374 fn test_dual_context_with_sensitive() {
1375 let err = DualContextError::with_lie_and_sensitive(
1376 "Resource not found",
1377 "File path: /var/secrets/api_keys.txt",
1378 OperationCategory::IO,
1379 );
1380
1381 assert_eq!(err.external_message(), "Resource not found");
1382 assert!(err.internal().payload().is_none());
1383 }
1384
1385 #[cfg(feature = "external_signaling")]
1386 #[test]
1387 fn test_dual_context_with_truth() {
1388 let err = DualContextError::with_truth(
1389 "Invalid JSON format",
1390 "JSON parse error at line 42: unexpected token",
1391 OperationCategory::Configuration,
1392 );
1393
1394 assert_eq!(err.external_message(), "Invalid JSON format");
1395 }
1396
1397 #[test]
1398 fn test_operation_category_deceptive_names() {
1399 assert_eq!(OperationCategory::Deception.deceptive_name(), "Routine Operation");
1400 assert_eq!(OperationCategory::Detection.deceptive_name(), "Routine Operation");
1401 assert_eq!(OperationCategory::Containment.deceptive_name(), "Routine Operation");
1402 assert_eq!(OperationCategory::Configuration.deceptive_name(), "Configuration");
1403 }
1404
1405 #[test]
1406 fn test_soc_access_capability() {
1407 let ctx = InternalContext::sensitive("secret data".to_string());
1408
1409 // Without capability, cannot access
1410 // (This is enforced by requiring SocAccess parameter)
1411
1412 // With capability, can access
1413 let access = SocAccess::acquire();
1414 assert_eq!(ctx.expose_sensitive(&access), Some("secret data"));
1415 }
1416
1417 #[test]
1418 fn test_zeroization() {
1419 let mut ctx = InternalContext::sensitive("secret data".to_string());
1420
1421 // Verify data exists before zeroization
1422 let access = SocAccess::acquire();
1423 assert_eq!(ctx.expose_sensitive(&access), Some("secret data"));
1424
1425 // Explicit zeroize call
1426 ctx.zeroize();
1427
1428 // Note: Actual verification of memory clearing would require unsafe inspection.
1429 // This test demonstrates the API contract. In production, zeroization happens
1430 // automatically on drop via ZeroizeOnDrop trait and InternalContextField::drop().
1431 }
1432
1433 #[test]
1434 fn test_volatile_write_on_drop() {
1435 // This test verifies the drop behavior exists and compiles correctly.
1436 // Actual verification of volatile writes would require memory inspection tools.
1437 let ctx = InternalContext::sensitive("highly sensitive data".to_string());
1438 drop(ctx);
1439 // If this compiles and runs without panicking, the Drop impl is correct
1440 }
1441
1442 #[test]
1443 fn test_display_never_leaks_internal() {
1444 let err = DualContextError::with_lie(
1445 "Generic error",
1446 "Actual internal diagnostic with sensitive details",
1447 OperationCategory::System,
1448 );
1449
1450 let display_output = format!("{}", err);
1451 assert_eq!(display_output, "Generic error");
1452 assert!(!display_output.contains("internal"));
1453 assert!(!display_output.contains("sensitive"));
1454 }
1455
1456 #[test]
1457 fn test_internal_display_always_redacts() {
1458 let ctx = InternalContext::diagnostic("secret diagnostic info");
1459 let display_output = format!("{}", ctx);
1460 assert_eq!(display_output, "[INTERNAL CONTEXT REDACTED]");
1461 assert!(!display_output.contains("secret"));
1462 }
1463
1464 #[test]
1465 fn test_internal_payload_not_copy() {
1466 // This test verifies InternalPayload does not implement Copy
1467 // If it did, this would compile with just moving instead of cloning
1468 let payload = InternalPayload::Truth("test");
1469 let _payload2 = payload.clone();
1470 // If we try to use payload again without Clone, it would fail to compile
1471 // (proving it's not Copy)
1472 }
1473}