Skip to main content

shardline_protocol/
security.rs

1use std::fmt;
2
3use serde::Deserialize;
4use zeroize::{Zeroize, ZeroizeOnDrop};
5
6/// Zeroizing byte-oriented secret material.
7#[derive(Clone, PartialEq, Eq, Deserialize, Zeroize, ZeroizeOnDrop)]
8pub struct SecretBytes(Vec<u8>);
9
10impl SecretBytes {
11    /// Wraps owned secret bytes.
12    #[must_use]
13    pub const fn new(secret: Vec<u8>) -> Self {
14        Self(secret)
15    }
16
17    /// Copies borrowed secret bytes into zeroizing storage.
18    #[must_use]
19    pub fn from_slice(secret: &[u8]) -> Self {
20        Self(secret.to_vec())
21    }
22
23    /// Returns the secret bytes.
24    #[must_use]
25    pub fn expose_secret(&self) -> &[u8] {
26        &self.0
27    }
28
29    /// Returns the secret length in bytes.
30    #[must_use]
31    pub const fn len(&self) -> usize {
32        self.0.len()
33    }
34
35    /// Returns whether the secret is empty.
36    #[must_use]
37    pub const fn is_empty(&self) -> bool {
38        self.0.is_empty()
39    }
40}
41
42impl AsRef<[u8]> for SecretBytes {
43    fn as_ref(&self) -> &[u8] {
44        self.expose_secret()
45    }
46}
47
48impl fmt::Debug for SecretBytes {
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        formatter.write_str("***")
51    }
52}
53
54/// Zeroizing UTF-8 secret material.
55#[derive(Clone, PartialEq, Eq, Deserialize, Zeroize, ZeroizeOnDrop)]
56pub struct SecretString(String);
57
58impl SecretString {
59    /// Wraps owned secret text.
60    #[must_use]
61    pub const fn new(secret: String) -> Self {
62        Self(secret)
63    }
64
65    /// Copies borrowed secret text into zeroizing storage.
66    #[must_use]
67    pub fn from_secret(secret: &str) -> Self {
68        Self(secret.to_owned())
69    }
70
71    /// Returns the secret text.
72    #[must_use]
73    pub fn expose_secret(&self) -> &str {
74        &self.0
75    }
76
77    /// Returns whether the secret text is empty.
78    #[must_use]
79    pub const fn is_empty(&self) -> bool {
80        self.0.is_empty()
81    }
82}
83
84impl AsRef<str> for SecretString {
85    fn as_ref(&self) -> &str {
86        self.expose_secret()
87    }
88}
89
90impl fmt::Debug for SecretString {
91    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92        formatter.write_str("***")
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::{SecretBytes, SecretString};
99
100    #[test]
101    fn secret_bytes_debug_redacts_contents() {
102        let secret = SecretBytes::from_slice(b"test-signing-key-32-bytes-long!!");
103
104        assert_eq!(format!("{secret:?}"), "***");
105    }
106
107    #[test]
108    fn secret_bytes_exposes_underlying_bytes() {
109        let secret = SecretBytes::from_slice(b"test-signing-key-32-bytes-long!!");
110
111        assert_eq!(secret.expose_secret(), b"test-signing-key-32-bytes-long!!");
112    }
113
114    #[test]
115    fn secret_string_debug_redacts_contents() {
116        let secret = SecretString::from_secret("bootstrap-token");
117
118        assert_eq!(format!("{secret:?}"), "***");
119    }
120
121    #[test]
122    fn secret_string_exposes_underlying_text() {
123        let secret = SecretString::from_secret("bootstrap-token");
124
125        assert_eq!(secret.expose_secret(), "bootstrap-token");
126    }
127}