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