imap_types/
secret.rs

1//! Handling of secret values.
2//!
3//! This module provides a `Secret<T>` ensuring that sensitive values are not
4//! `Debug`-printed by accident.
5
6use std::fmt::{Debug, Formatter};
7
8#[cfg(feature = "arbitrary")]
9use arbitrary::Arbitrary;
10#[cfg(feature = "bounded-static")]
11use bounded_static::ToStatic;
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14
15/// A wrapper to ensure that secrets are redacted during `Debug`-printing.
16#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
17#[cfg_attr(feature = "bounded-static", derive(ToStatic))]
18#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
19// Note: The implementation of these traits does agree:
20//       `PartialEq` is just a thin wrapper that ensures constant-time comparison.
21#[allow(clippy::derived_hash_with_manual_eq)]
22#[derive(Clone, Eq, Hash, PartialEq)]
23pub struct Secret<T>(T);
24
25impl<T> Secret<T> {
26    /// Crate a new secret.
27    pub fn new(inner: T) -> Self {
28        Self(inner)
29    }
30
31    /// Expose the inner secret.
32    pub fn declassify(&self) -> &T {
33        &self.0
34    }
35}
36
37impl<T> From<T> for Secret<T> {
38    fn from(value: T) -> Self {
39        Self::new(value)
40    }
41}
42
43impl<T> Debug for Secret<T>
44where
45    T: Debug,
46{
47    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
48        #[cfg(not(debug_assertions))]
49        return write!(f, "/* REDACTED */");
50        #[cfg(debug_assertions)]
51        return self.0.fmt(f);
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use crate::{
58        command::{Command, CommandBody},
59        core::{AString, Atom, Literal, Quoted},
60    };
61
62    #[test]
63    #[cfg(not(debug_assertions))]
64    #[allow(clippy::redundant_clone)]
65    fn test_that_secret_is_redacted() {
66        use super::Secret;
67        use crate::auth::{AuthMechanism, AuthenticateData};
68
69        let secret = Secret("xyz123");
70        let got = format!("{:?}", secret);
71        println!("{}", got);
72        assert!(!got.contains("xyz123"));
73
74        println!("-----");
75
76        let tests = vec![
77            CommandBody::login("alice", "xyz123")
78                .unwrap()
79                .tag("A")
80                .unwrap(),
81            CommandBody::authenticate_with_ir(AuthMechanism::Plain, b"xyz123".as_ref())
82                .tag("A")
83                .unwrap(),
84        ];
85
86        for test in tests.into_iter() {
87            let got = format!("{:?}", test);
88            println!("Debug: {}", got);
89            assert!(got.contains("/* REDACTED */"));
90            assert!(!got.contains("xyz123"));
91            assert!(!got.contains("eHl6MTIz"));
92
93            println!();
94        }
95
96        println!("-----");
97
98        let tests = [
99            AuthenticateData(Secret::new(b"xyz123".to_vec())),
100            AuthenticateData(Secret::from(b"xyz123".to_vec())),
101        ];
102
103        for test in tests {
104            let got = format!("{:?}", test);
105            println!("Debug: {}", got);
106            assert!(got.contains("/* REDACTED */"));
107            assert!(!got.contains("xyz123"));
108            assert!(!got.contains("eHl6MTIz"));
109        }
110    }
111
112    #[test]
113    fn test_that_secret_has_no_side_effects_on_eq() {
114        assert_ne!(
115            Command::new(
116                "A",
117                CommandBody::login(
118                    AString::from(Atom::try_from("user").unwrap()),
119                    AString::from(Atom::try_from("pass").unwrap()),
120                )
121                .unwrap(),
122            ),
123            Command::new(
124                "A",
125                CommandBody::login(
126                    AString::from(Atom::try_from("user").unwrap()),
127                    AString::from(Quoted::try_from("pass").unwrap()),
128                )
129                .unwrap(),
130            )
131        );
132
133        assert_ne!(
134            Command::new(
135                "A",
136                CommandBody::login(
137                    Literal::try_from("").unwrap(),
138                    Literal::try_from("A").unwrap(),
139                )
140                .unwrap(),
141            ),
142            Command::new(
143                "A",
144                CommandBody::login(
145                    Literal::try_from("").unwrap(),
146                    Literal::try_from("A").unwrap().into_non_sync(),
147                )
148                .unwrap(),
149            )
150        );
151    }
152}