Skip to main content

iceberg/
sensitive.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! This module contains types to keep sensitive data in memory.
19
20use std::fmt;
21
22use zeroize::Zeroizing;
23
24/// A string-like type containing sensitive information such as passwords or tokens.
25///
26/// It is redacted from debug logs and automatically zeroized.
27///
28/// # Example
29/// ```
30/// use iceberg::sensitive::SensitiveString;
31///
32/// let sensitive_value = "my-pw-12345";
33/// let sensitive_string = SensitiveString::from(sensitive_value.to_string());
34///
35/// // Not contained in debug logs.
36/// assert!(!format!("{:?}", sensitive_string).contains(sensitive_value));
37/// ```
38///
39/// # Display
40/// [`SensitiveString`] does **not** implement [`Display`] to prevent bugs like:
41///
42/// ```compile_fail
43/// # use iceberg::sensitive::SensitiveString;
44/// // We don't want to send a redacted `Bearer: *****`.
45/// let auth_header = format!("Bearer {}", SensitiveString::from("token".to_string()));
46/// ```
47///
48/// Instead use an explicit [`SensitiveString::expose`] when you need it:
49///
50/// ```
51/// # use iceberg::sensitive::SensitiveString;
52/// let auth_header = format!(
53///     "Bearer: {}",
54///     SensitiveString::from("token".to_string()).expose()
55/// );
56/// ```
57#[derive(Clone, PartialEq, Eq)]
58pub struct SensitiveString(Zeroizing<String>);
59
60impl SensitiveString {
61    /// Returns the raw value of the sensitive string.
62    pub fn expose(&self) -> &str {
63        &self.0
64    }
65
66    /// Returns `true` if the string value is empty.
67    pub fn is_empty(&self) -> bool {
68        self.0.is_empty()
69    }
70}
71
72impl fmt::Debug for SensitiveString {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.write_str("SensitiveString([REDACTED])")
75    }
76}
77
78impl From<String> for SensitiveString {
79    fn from(value: String) -> Self {
80        Self(Zeroizing::new(value))
81    }
82}
83
84/// Wrapper for sensitive byte data (encryption keys, DEKs, etc.) that:
85/// - Zeroizes memory on drop
86/// - Redacts content in [`Debug`] and [`Display`] output
87/// - Provides only `&[u8]` access via [`as_bytes()`](Self::as_bytes)
88/// - Uses `Box<[u8]>` (immutable boxed slice) since key bytes never grow
89///
90/// Use this type for any struct field that holds plaintext key material.
91/// Because its [`Debug`] impl always prints `[N bytes REDACTED]`, structs
92/// containing `SensitiveBytes` can safely derive or implement `Debug`
93/// without risk of leaking key material.
94#[derive(Clone, PartialEq, Eq)]
95pub struct SensitiveBytes(Zeroizing<Box<[u8]>>);
96
97impl SensitiveBytes {
98    /// Wraps the given bytes as sensitive material.
99    pub fn new(bytes: impl Into<Box<[u8]>>) -> Self {
100        Self(Zeroizing::new(bytes.into()))
101    }
102
103    /// Returns the underlying bytes.
104    pub fn as_bytes(&self) -> &[u8] {
105        &self.0
106    }
107
108    /// Returns the number of bytes.
109    pub fn len(&self) -> usize {
110        self.0.len()
111    }
112
113    /// Returns `true` if the byte slice is empty.
114    pub fn is_empty(&self) -> bool {
115        self.0.is_empty()
116    }
117}
118
119impl fmt::Debug for SensitiveBytes {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        write!(f, "[{} bytes REDACTED]", self.0.len())
122    }
123}
124
125impl fmt::Display for SensitiveBytes {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        write!(f, "[{} bytes REDACTED]", self.0.len())
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use crate::sensitive::{SensitiveBytes, SensitiveString};
134
135    #[test]
136    fn test_sensitive_string_redacts_debug_value() {
137        let sensitive_value = "my-pw-12346";
138
139        let logged = format!("{:?}", SensitiveString::from(sensitive_value.to_string()));
140        assert!(!logged.contains(sensitive_value));
141    }
142
143    #[test]
144    fn test_sensitive_bytes_redacts_debug_value() {
145        let sensitive_value = b"my-secret-bytes";
146
147        let logged = format!("{:?}", SensitiveBytes::new(&sensitive_value[..]));
148        assert!(
149            !logged
150                .as_bytes()
151                .windows(sensitive_value.len())
152                .any(|window| window == sensitive_value)
153        );
154    }
155
156    #[test]
157    fn test_sensitive_bytes_redacts_display_value() {
158        let sensitive_value = b"my-secret-bytes";
159
160        let logged = format!("{}", SensitiveBytes::new(&sensitive_value[..]));
161        assert!(
162            !logged
163                .as_bytes()
164                .windows(sensitive_value.len())
165                .any(|window| window == sensitive_value)
166        );
167    }
168}