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}