From eb72d6242b2a57085418c137f15c4a701307e45b Mon Sep 17 00:00:00 2001 From: Jannik Steinmann Date: Thu, 6 Aug 2026 10:14:30 +0200 Subject: [PATCH 1/6] Add SensitiveString Signed-off-by: Jannik Steinmann --- crates/iceberg/public-api.txt | 10 ++++ crates/iceberg/src/lib.rs | 2 + crates/iceberg/src/sensitive.rs | 90 +++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 crates/iceberg/src/sensitive.rs diff --git a/crates/iceberg/public-api.txt b/crates/iceberg/public-api.txt index 24e276db56..899793e0ea 100644 --- a/crates/iceberg/public-api.txt +++ b/crates/iceberg/public-api.txt @@ -1364,6 +1364,16 @@ pub fn iceberg::scan::TableScanBuilder<'a>::with_row_group_filtering_enabled(sel pub fn iceberg::scan::TableScanBuilder<'a>::with_row_selection_enabled(self, row_selection_enabled: bool) -> Self pub type iceberg::scan::ArrowRecordBatchStream = futures_core::stream::BoxStream<'static, iceberg::Result> pub type iceberg::scan::FileScanTaskStream = futures_core::stream::BoxStream<'static, iceberg::Result> +pub mod iceberg::sensitive +pub struct iceberg::sensitive::SensitiveString(_) +impl iceberg::sensitive::SensitiveString +pub fn iceberg::sensitive::SensitiveString::expose(&self) -> &str +impl core::clone::Clone for iceberg::sensitive::SensitiveString +pub fn iceberg::sensitive::SensitiveString::clone(&self) -> iceberg::sensitive::SensitiveString +impl core::convert::From for iceberg::sensitive::SensitiveString +pub fn iceberg::sensitive::SensitiveString::from(value: alloc::string::String) -> Self +impl core::fmt::Debug for iceberg::sensitive::SensitiveString +pub fn iceberg::sensitive::SensitiveString::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub mod iceberg::spec pub use iceberg::spec::ByteBuf pub enum iceberg::spec::DataContentType diff --git a/crates/iceberg/src/lib.rs b/crates/iceberg/src/lib.rs index b3a409fa8c..d1c3670efa 100644 --- a/crates/iceberg/src/lib.rs +++ b/crates/iceberg/src/lib.rs @@ -71,6 +71,8 @@ extern crate self as iceberg; mod error; pub use error::{Error, ErrorKind, Result}; +pub mod sensitive; + mod catalog; pub use catalog::utils::drop_table_data; diff --git a/crates/iceberg/src/sensitive.rs b/crates/iceberg/src/sensitive.rs new file mode 100644 index 0000000000..2e452bc951 --- /dev/null +++ b/crates/iceberg/src/sensitive.rs @@ -0,0 +1,90 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! This module contains types to keep sensitive data in memory. + +use std::fmt; + +use zeroize::Zeroizing; + +/// A string-like type containing sensitive information such as passwords or tokens. +/// +/// It is redacted from debug logs and automatically zeroized. +/// +/// # Example +/// ``` +/// use iceberg::sensitive::SensitiveString; +/// +/// let sensitive_value = "my-pw-12345"; +/// let sensitive_string = SensitiveString::from(sensitive_value.to_string()); +/// +/// // Not contained in debug logs. +/// assert!(!format!("{:?}", sensitive_string).contains(sensitive_value)); +/// ``` +/// +/// # Display +/// [`SensitiveString`] does **not** implement [`Display`] to prevent bugs like: +/// +/// ```compile_fail +/// # use iceberg::sensitive::SensitiveString; +/// // We don't want to send a redacted `Bearer: *****`. +/// let auth_header = format!("Bearer {}", SensitiveString::from("token".to_string())); +/// ``` +/// +/// Instead use an explicit [`SensitiveString::expose`] when you need it: +/// +/// ``` +/// # use iceberg::sensitive::SensitiveString; +/// let auth_header = format!( +/// "Bearer: {}", +/// SensitiveString::from("token".to_string()).expose() +/// ); +/// ``` +#[derive(Clone)] +pub struct SensitiveString(Zeroizing); + +impl SensitiveString { + /// Returns the raw value of the sensitive string. + pub fn expose(&self) -> &str { + &self.0 + } +} + +impl fmt::Debug for SensitiveString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("SensitiveString([REDACTED])") + } +} + +impl From for SensitiveString { + fn from(value: String) -> Self { + Self(Zeroizing::new(value)) + } +} + +#[cfg(test)] +mod tests { + use crate::sensitive::SensitiveString; + + #[test] + fn test_sensitive_string_redacts_value() { + let sensitive_value = "my-pw-12346"; + + let logged = format!("{:?}", SensitiveString::from(sensitive_value.to_string())); + assert!(!logged.contains(sensitive_value)); + } +} From 9d890bffbbf5de73a183fb593d90aeb1b94c3a00 Mon Sep 17 00:00:00 2001 From: Jannik Steinmann Date: Thu, 6 Aug 2026 10:16:49 +0200 Subject: [PATCH 2/6] Replace session::Credential with SensitiveString Signed-off-by: Jannik Steinmann --- crates/catalog/rest/src/auth/oauth2.rs | 22 +++++----- crates/iceberg/public-api.txt | 11 +---- crates/iceberg/src/catalog/session.rs | 56 +++----------------------- 3 files changed, 19 insertions(+), 70 deletions(-) diff --git a/crates/catalog/rest/src/auth/oauth2.rs b/crates/catalog/rest/src/auth/oauth2.rs index 897fcbb853..5907f2721f 100644 --- a/crates/catalog/rest/src/auth/oauth2.rs +++ b/crates/catalog/rest/src/auth/oauth2.rs @@ -21,7 +21,8 @@ use std::sync::Arc; use async_trait::async_trait; use http::StatusCode; -use iceberg::{Credential, Error, ErrorKind, Result}; +use iceberg::sensitive::SensitiveString; +use iceberg::{Error, ErrorKind, Result}; use reqwest::header::HeaderMap; use tokio::sync::Mutex; @@ -37,7 +38,7 @@ use crate::types::{ErrorResponse, TokenResponse}; struct OAuth2Params { extra_headers: HeaderMap, token_endpoint: String, - credential: Option<(Option, Credential)>, + credential: Option<(Option, SensitiveString)>, extra_oauth_params: HashMap, } @@ -48,7 +49,7 @@ struct OAuth2Params { /// for a token at the token endpoint and cached. The cached token is shared /// across sessions so it survives the config handshake. pub struct OAuth2Manager { - token: Arc>>, + token: Arc>>, init_params: OAuth2Params, /// True when the token endpoint was derived from the catalog URI (not /// explicitly configured): it is then recomputed from the merged URI in @@ -80,7 +81,7 @@ impl OAuth2Manager { /// Sets a bearer token used directly (takes precedence over `credential`). pub fn with_token(mut self, token: impl Into) -> Self { - self.token = Arc::new(Mutex::new(Some(Credential::from(token.into())))); + self.token = Arc::new(Mutex::new(Some(SensitiveString::from(token.into())))); self } @@ -106,7 +107,7 @@ impl OAuth2Manager { pub(crate) fn from_config(cfg: &RestCatalogConfig) -> Result { Ok(Self { - token: Arc::new(Mutex::new(cfg.token().map(Credential::from))), + token: Arc::new(Mutex::new(cfg.token().map(SensitiveString::from))), init_params: OAuth2Params { extra_headers: cfg.extra_headers()?, token_endpoint: cfg.get_token_endpoint(), @@ -157,7 +158,7 @@ impl OAuth2Manager { ) -> Result { // The properties may carry a new token (or restate the user's). if let Some(token) = props.get("token") { - *self.token.lock().await = Some(Credential::from(token.clone())); + *self.token.lock().await = Some(SensitiveString::from(token.clone())); } let mut extra_headers = self.init_params.extra_headers.clone(); @@ -208,7 +209,7 @@ impl OAuth2Manager { /// Attaches `token` as a `Authorization: Bearer ` header, marked /// sensitive so `Debug`-formatted requests redact it. -fn attach_bearer(req: &mut HttpRequest, token: &Credential) -> Result<()> { +fn attach_bearer(req: &mut HttpRequest, token: &SensitiveString) -> Result<()> { let mut value: http::HeaderValue = format!("Bearer {}", token.expose()).parse().map_err(|e| { Error::new( @@ -231,7 +232,7 @@ fn attach_bearer(req: &mut HttpRequest, token: &Credential) -> Result<()> { /// /// # TODO: Support automatic token refreshing. struct OAuth2Session { - token: Arc>>, + token: Arc>>, token_source: TokenSource, } @@ -246,7 +247,7 @@ enum TokenSource { struct ClientCredentialsConfig { client: HttpClient, - credential: (Option, Credential), + credential: (Option, SensitiveString), token_endpoint: String, extra_headers: HeaderMap, extra_oauth_params: HashMap, @@ -323,7 +324,8 @@ impl AuthSession for OAuth2Session { (Some(token), _) => Some(token.clone()), (None, TokenSource::StaticToken) => None, (None, TokenSource::ClientCredentials(config)) => { - let new_token = Credential::from(config.exchange_credential_for_token().await?); + let new_token = + SensitiveString::from(config.exchange_credential_for_token().await?); *token = Some(new_token.clone()); Some(new_token) } diff --git a/crates/iceberg/public-api.txt b/crates/iceberg/public-api.txt index 899793e0ea..b26e9d6fd6 100644 --- a/crates/iceberg/public-api.txt +++ b/crates/iceberg/public-api.txt @@ -3573,15 +3573,6 @@ impl serde_core::ser::Serialize for iceberg::ViewUpdate pub fn iceberg::ViewUpdate::serialize<__S>(&self, __serializer: __S) -> core::result::Result<<__S as serde_core::ser::Serializer>::Ok, <__S as serde_core::ser::Serializer>::Error> where __S: serde_core::ser::Serializer impl<'de> serde_core::de::Deserialize<'de> for iceberg::ViewUpdate pub fn iceberg::ViewUpdate::deserialize<__D>(__deserializer: __D) -> core::result::Result::Error> where __D: serde_core::de::Deserializer<'de> -pub struct iceberg::Credential(_) -impl iceberg::Credential -pub fn iceberg::Credential::expose(&self) -> &str -impl core::clone::Clone for iceberg::Credential -pub fn iceberg::Credential::clone(&self) -> iceberg::Credential -impl core::convert::From for iceberg::Credential -pub fn iceberg::Credential::from(value: alloc::string::String) -> Self -impl core::fmt::Debug for iceberg::Credential -pub fn iceberg::Credential::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct iceberg::Error impl iceberg::Error pub fn iceberg::Error::backtrace(&self) -> &std::backtrace::Backtrace @@ -3732,7 +3723,7 @@ impl core::fmt::Debug for iceberg::RuntimeHandle pub fn iceberg::RuntimeHandle::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct iceberg::SessionContext impl iceberg::SessionContext -pub fn iceberg::SessionContext::credentials(&self) -> &std::collections::hash::map::HashMap +pub fn iceberg::SessionContext::credentials(&self) -> &std::collections::hash::map::HashMap pub fn iceberg::SessionContext::empty() -> Self pub fn iceberg::SessionContext::identity(&self) -> core::option::Option<&str> pub fn iceberg::SessionContext::properties(&self) -> &std::collections::hash::map::HashMap diff --git a/crates/iceberg/src/catalog/session.rs b/crates/iceberg/src/catalog/session.rs index 1790f4978a..e506820983 100644 --- a/crates/iceberg/src/catalog/session.rs +++ b/crates/iceberg/src/catalog/session.rs @@ -25,8 +25,8 @@ use async_trait::async_trait; use mockall::automock; use typed_builder::TypedBuilder; use uuid::Uuid; -use zeroize::Zeroizing; +use crate::sensitive::SensitiveString; use crate::table::Table; use crate::{Namespace, NamespaceIdent, Result, TableCommit, TableCreation, TableIdent}; @@ -61,7 +61,7 @@ pub struct SessionContext { properties: HashMap, #[builder(default)] - credentials: HashMap, + credentials: HashMap, } impl SessionContext { @@ -88,47 +88,11 @@ impl SessionContext { } /// Returns the session's credential map. - pub fn credentials(&self) -> &HashMap { + pub fn credentials(&self) -> &HashMap { &self.credentials } } -/// A string-like type containing sensitive information such as passwords or tokens. -/// -/// It is redacted from logs and automatically zeroized. -/// -/// # Example -/// ```rust -/// use iceberg::Credential; -/// -/// let sensitive_value = "my-pw-12345"; -/// let credential = Credential::from(sensitive_value.to_string()); -/// -/// // Not contained in debug logs. -/// assert!(!format!("{:?}", credential).contains(sensitive_value)); -/// ``` -#[derive(Clone)] -pub struct Credential(Zeroizing); - -impl Credential { - /// Returns the raw value of the credential. - pub fn expose(&self) -> &str { - &self.0 - } -} - -impl Debug for Credential { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Credential([REDACTED])") - } -} - -impl From for Credential { - fn from(value: String) -> Self { - Self(Zeroizing::new(value)) - } -} - /// The catalog API for Iceberg Rust that includes session handling. #[async_trait] #[cfg_attr(test, automock)] @@ -239,7 +203,8 @@ mod tests { use uuid::Uuid; - use crate::{Credential, SessionCatalog, SessionContext}; + use crate::sensitive::SensitiveString; + use crate::{SessionCatalog, SessionContext}; #[test] fn test_empty_session_context_has_uuid_session_id() { @@ -271,7 +236,7 @@ mod tests { let session = SessionContext::builder() .credentials(HashMap::from([( "key".to_string(), - Credential::from(sensitive_value.to_string()), + SensitiveString::from(sensitive_value.to_string()), )])) .build(); @@ -279,17 +244,8 @@ mod tests { assert!(!logged.contains(sensitive_value)) } - #[test] - fn test_credential_redacts_value() { - let sensitive_value = "my-pw-12346"; - - let logged = format!("{:?}", Credential::from(sensitive_value.to_string())); - assert!(!logged.contains(sensitive_value)); - } - #[test] fn test_types_are_send_sync() { - assert_send_sync::(); assert_send_sync::(); assert_send_sync::(); From 56f8578042ed806b58255fea4dc9c0fb0079eb35 Mon Sep 17 00:00:00 2001 From: Jannik Steinmann Date: Thu, 6 Aug 2026 12:38:20 +0200 Subject: [PATCH 3/6] Move SensitiveBytes to sensitive mod Signed-off-by: Jannik Steinmann --- crates/iceberg/public-api.txt | 83 +++++++++++++++--------- crates/iceberg/src/encryption/crypto.rs | 57 +++------------- crates/iceberg/src/encryption/manager.rs | 3 +- crates/iceberg/src/encryption/mod.rs | 4 +- crates/iceberg/src/sensitive.rs | 47 ++++++++++++++ 5 files changed, 111 insertions(+), 83 deletions(-) diff --git a/crates/iceberg/public-api.txt b/crates/iceberg/public-api.txt index b26e9d6fd6..99112b9462 100644 --- a/crates/iceberg/public-api.txt +++ b/crates/iceberg/public-api.txt @@ -178,13 +178,13 @@ pub mod iceberg::encryption pub mod iceberg::encryption::kms pub struct iceberg::encryption::kms::GeneratedKey impl iceberg::encryption::GeneratedKey -pub fn iceberg::encryption::GeneratedKey::key(&self) -> &iceberg::encryption::SensitiveBytes -pub fn iceberg::encryption::GeneratedKey::new(key: iceberg::encryption::SensitiveBytes, wrapped_key: alloc::vec::Vec) -> Self +pub fn iceberg::encryption::GeneratedKey::key(&self) -> &iceberg::sensitive::SensitiveBytes +pub fn iceberg::encryption::GeneratedKey::new(key: iceberg::sensitive::SensitiveBytes, wrapped_key: alloc::vec::Vec) -> Self pub fn iceberg::encryption::GeneratedKey::wrapped_key(&self) -> &[u8] pub struct iceberg::encryption::kms::MemoryKeyManagementClient impl iceberg::encryption::kms::MemoryKeyManagementClient pub fn iceberg::encryption::kms::MemoryKeyManagementClient::add_master_key(&self, key_id: impl core::convert::Into) -> iceberg::Result<()> -pub fn iceberg::encryption::kms::MemoryKeyManagementClient::add_master_key_bytes(&self, key_id: impl core::convert::Into, key_bytes: iceberg::encryption::SensitiveBytes) -> iceberg::Result<()> +pub fn iceberg::encryption::kms::MemoryKeyManagementClient::add_master_key_bytes(&self, key_id: impl core::convert::Into, key_bytes: iceberg::sensitive::SensitiveBytes) -> iceberg::Result<()> pub fn iceberg::encryption::kms::MemoryKeyManagementClient::has_key(&self, key_id: &str) -> bool pub fn iceberg::encryption::kms::MemoryKeyManagementClient::key_count(&self) -> usize pub fn iceberg::encryption::kms::MemoryKeyManagementClient::new() -> Self @@ -198,12 +198,12 @@ pub fn iceberg::encryption::kms::MemoryKeyManagementClient::fmt(&self, f: &mut c impl iceberg::encryption::KeyManagementClient for iceberg::encryption::kms::MemoryKeyManagementClient pub fn iceberg::encryption::kms::MemoryKeyManagementClient::generate_key<'life0, 'life1, 'async_trait>(&'life0 self, _wrapping_key_id: &'life1 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait pub fn iceberg::encryption::kms::MemoryKeyManagementClient::supports_key_generation(&self) -> bool -pub fn iceberg::encryption::kms::MemoryKeyManagementClient::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait +pub fn iceberg::encryption::kms::MemoryKeyManagementClient::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait pub fn iceberg::encryption::kms::MemoryKeyManagementClient::wrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait pub struct iceberg::encryption::kms::MemoryKmsClientFactory impl iceberg::encryption::kms::MemoryKmsClientFactory pub fn iceberg::encryption::kms::MemoryKmsClientFactory::add_master_key(&self, key_id: impl core::convert::Into) -> iceberg::Result<()> -pub fn iceberg::encryption::kms::MemoryKmsClientFactory::add_master_key_bytes(&self, key_id: impl core::convert::Into, key_bytes: iceberg::encryption::SensitiveBytes) -> iceberg::Result<()> +pub fn iceberg::encryption::kms::MemoryKmsClientFactory::add_master_key_bytes(&self, key_id: impl core::convert::Into, key_bytes: iceberg::sensitive::SensitiveBytes) -> iceberg::Result<()> pub fn iceberg::encryption::kms::MemoryKmsClientFactory::new() -> Self pub fn iceberg::encryption::kms::MemoryKmsClientFactory::with_master_key_size(master_key_size: iceberg::encryption::AesKeySize) -> Self impl core::clone::Clone for iceberg::encryption::kms::MemoryKmsClientFactory @@ -217,17 +217,17 @@ pub fn iceberg::encryption::kms::MemoryKmsClientFactory::create_kms_client<'life pub trait iceberg::encryption::kms::KeyManagementClient: core::marker::Send + core::marker::Sync + core::fmt::Debug pub fn iceberg::encryption::kms::KeyManagementClient::generate_key<'life0, 'life1, 'async_trait>(&'life0 self, wrapping_key_id: &'life1 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait pub fn iceberg::encryption::kms::KeyManagementClient::supports_key_generation(&self) -> bool -pub fn iceberg::encryption::kms::KeyManagementClient::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait +pub fn iceberg::encryption::kms::KeyManagementClient::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait pub fn iceberg::encryption::kms::KeyManagementClient::wrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait impl iceberg::encryption::KeyManagementClient for iceberg::encryption::kms::MemoryKeyManagementClient pub fn iceberg::encryption::kms::MemoryKeyManagementClient::generate_key<'life0, 'life1, 'async_trait>(&'life0 self, _wrapping_key_id: &'life1 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait pub fn iceberg::encryption::kms::MemoryKeyManagementClient::supports_key_generation(&self) -> bool -pub fn iceberg::encryption::kms::MemoryKeyManagementClient::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait +pub fn iceberg::encryption::kms::MemoryKeyManagementClient::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait pub fn iceberg::encryption::kms::MemoryKeyManagementClient::wrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait impl + core::marker::Send + core::marker::Sync + core::fmt::Debug> iceberg::encryption::KeyManagementClient for T pub fn T::generate_key<'life0, 'life1, 'async_trait>(&'life0 self, wrapping_key_id: &'life1 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait pub fn T::supports_key_generation(&self) -> bool -pub fn T::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait +pub fn T::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait pub fn T::wrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait pub trait iceberg::encryption::kms::KmsClientFactory: core::fmt::Debug + core::marker::Send + core::marker::Sync pub fn iceberg::encryption::kms::KmsClientFactory::create_kms_client<'life0, 'life1, 'async_trait>(&'life0 self, properties: &'life1 std::collections::hash::map::HashMap) -> core::pin::Pin>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait @@ -309,8 +309,8 @@ impl iceberg::encryption::EncryptionManager pub fn iceberg::encryption::EncryptionManager::builder() -> EncryptionManagerBuilder<((), (), (), (std::sync::poison::rwlock::RwLock>))> pub struct iceberg::encryption::GeneratedKey impl iceberg::encryption::GeneratedKey -pub fn iceberg::encryption::GeneratedKey::key(&self) -> &iceberg::encryption::SensitiveBytes -pub fn iceberg::encryption::GeneratedKey::new(key: iceberg::encryption::SensitiveBytes, wrapped_key: alloc::vec::Vec) -> Self +pub fn iceberg::encryption::GeneratedKey::key(&self) -> &iceberg::sensitive::SensitiveBytes +pub fn iceberg::encryption::GeneratedKey::new(key: iceberg::sensitive::SensitiveBytes, wrapped_key: alloc::vec::Vec) -> Self pub fn iceberg::encryption::GeneratedKey::wrapped_key(&self) -> &[u8] pub struct iceberg::encryption::SecureKey impl iceberg::encryption::SecureKey @@ -325,31 +325,31 @@ impl core::cmp::PartialEq for iceberg::encryption::SecureKey pub fn iceberg::encryption::SecureKey::eq(&self, other: &iceberg::encryption::SecureKey) -> bool impl core::convert::From for iceberg::encryption::StandardKeyMetadata pub fn iceberg::encryption::StandardKeyMetadata::from(encryption_key: iceberg::encryption::SecureKey) -> Self -impl core::convert::TryFrom for iceberg::encryption::SecureKey +impl core::convert::TryFrom for iceberg::encryption::SecureKey pub type iceberg::encryption::SecureKey::Error = iceberg::Error -pub fn iceberg::encryption::SecureKey::try_from(key: iceberg::encryption::SensitiveBytes) -> iceberg::Result +pub fn iceberg::encryption::SecureKey::try_from(key: iceberg::sensitive::SensitiveBytes) -> iceberg::Result impl core::fmt::Debug for iceberg::encryption::SecureKey pub fn iceberg::encryption::SecureKey::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::StructuralPartialEq for iceberg::encryption::SecureKey pub struct iceberg::encryption::SensitiveBytes(_) -impl iceberg::encryption::SensitiveBytes -pub fn iceberg::encryption::SensitiveBytes::as_bytes(&self) -> &[u8] -pub fn iceberg::encryption::SensitiveBytes::is_empty(&self) -> bool -pub fn iceberg::encryption::SensitiveBytes::len(&self) -> usize -pub fn iceberg::encryption::SensitiveBytes::new(bytes: impl core::convert::Into>) -> Self -impl core::clone::Clone for iceberg::encryption::SensitiveBytes -pub fn iceberg::encryption::SensitiveBytes::clone(&self) -> iceberg::encryption::SensitiveBytes -impl core::cmp::Eq for iceberg::encryption::SensitiveBytes -impl core::cmp::PartialEq for iceberg::encryption::SensitiveBytes -pub fn iceberg::encryption::SensitiveBytes::eq(&self, other: &iceberg::encryption::SensitiveBytes) -> bool -impl core::convert::TryFrom for iceberg::encryption::SecureKey +impl iceberg::sensitive::SensitiveBytes +pub fn iceberg::sensitive::SensitiveBytes::as_bytes(&self) -> &[u8] +pub fn iceberg::sensitive::SensitiveBytes::is_empty(&self) -> bool +pub fn iceberg::sensitive::SensitiveBytes::len(&self) -> usize +pub fn iceberg::sensitive::SensitiveBytes::new(bytes: impl core::convert::Into>) -> Self +impl core::clone::Clone for iceberg::sensitive::SensitiveBytes +pub fn iceberg::sensitive::SensitiveBytes::clone(&self) -> iceberg::sensitive::SensitiveBytes +impl core::cmp::Eq for iceberg::sensitive::SensitiveBytes +impl core::cmp::PartialEq for iceberg::sensitive::SensitiveBytes +pub fn iceberg::sensitive::SensitiveBytes::eq(&self, other: &iceberg::sensitive::SensitiveBytes) -> bool +impl core::convert::TryFrom for iceberg::encryption::SecureKey pub type iceberg::encryption::SecureKey::Error = iceberg::Error -pub fn iceberg::encryption::SecureKey::try_from(key: iceberg::encryption::SensitiveBytes) -> iceberg::Result -impl core::fmt::Debug for iceberg::encryption::SensitiveBytes -pub fn iceberg::encryption::SensitiveBytes::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::fmt::Display for iceberg::encryption::SensitiveBytes -pub fn iceberg::encryption::SensitiveBytes::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for iceberg::encryption::SensitiveBytes +pub fn iceberg::encryption::SecureKey::try_from(key: iceberg::sensitive::SensitiveBytes) -> iceberg::Result +impl core::fmt::Debug for iceberg::sensitive::SensitiveBytes +pub fn iceberg::sensitive::SensitiveBytes::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for iceberg::sensitive::SensitiveBytes +pub fn iceberg::sensitive::SensitiveBytes::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for iceberg::sensitive::SensitiveBytes pub struct iceberg::encryption::StandardKeyMetadata impl iceberg::encryption::StandardKeyMetadata pub fn iceberg::encryption::StandardKeyMetadata::aad_prefix(&self) -> core::option::Option<&[u8]> @@ -373,17 +373,17 @@ impl core::marker::StructuralPartialEq for iceberg::encryption::StandardKeyMetad pub trait iceberg::encryption::KeyManagementClient: core::marker::Send + core::marker::Sync + core::fmt::Debug pub fn iceberg::encryption::KeyManagementClient::generate_key<'life0, 'life1, 'async_trait>(&'life0 self, wrapping_key_id: &'life1 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait pub fn iceberg::encryption::KeyManagementClient::supports_key_generation(&self) -> bool -pub fn iceberg::encryption::KeyManagementClient::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait +pub fn iceberg::encryption::KeyManagementClient::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait pub fn iceberg::encryption::KeyManagementClient::wrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait impl iceberg::encryption::KeyManagementClient for iceberg::encryption::kms::MemoryKeyManagementClient pub fn iceberg::encryption::kms::MemoryKeyManagementClient::generate_key<'life0, 'life1, 'async_trait>(&'life0 self, _wrapping_key_id: &'life1 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait pub fn iceberg::encryption::kms::MemoryKeyManagementClient::supports_key_generation(&self) -> bool -pub fn iceberg::encryption::kms::MemoryKeyManagementClient::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait +pub fn iceberg::encryption::kms::MemoryKeyManagementClient::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait pub fn iceberg::encryption::kms::MemoryKeyManagementClient::wrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait impl + core::marker::Send + core::marker::Sync + core::fmt::Debug> iceberg::encryption::KeyManagementClient for T pub fn T::generate_key<'life0, 'life1, 'async_trait>(&'life0 self, wrapping_key_id: &'life1 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait pub fn T::supports_key_generation(&self) -> bool -pub fn T::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait +pub fn T::unwrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, wrapped_key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait pub fn T::wrap_key<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, key: &'life1 [u8], wrapping_key_id: &'life2 str) -> core::pin::Pin>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait pub mod iceberg::expr pub enum iceberg::expr::BoundPredicate @@ -1365,6 +1365,25 @@ pub fn iceberg::scan::TableScanBuilder<'a>::with_row_selection_enabled(self, row pub type iceberg::scan::ArrowRecordBatchStream = futures_core::stream::BoxStream<'static, iceberg::Result> pub type iceberg::scan::FileScanTaskStream = futures_core::stream::BoxStream<'static, iceberg::Result> pub mod iceberg::sensitive +pub struct iceberg::sensitive::SensitiveBytes(_) +impl iceberg::sensitive::SensitiveBytes +pub fn iceberg::sensitive::SensitiveBytes::as_bytes(&self) -> &[u8] +pub fn iceberg::sensitive::SensitiveBytes::is_empty(&self) -> bool +pub fn iceberg::sensitive::SensitiveBytes::len(&self) -> usize +pub fn iceberg::sensitive::SensitiveBytes::new(bytes: impl core::convert::Into>) -> Self +impl core::clone::Clone for iceberg::sensitive::SensitiveBytes +pub fn iceberg::sensitive::SensitiveBytes::clone(&self) -> iceberg::sensitive::SensitiveBytes +impl core::cmp::Eq for iceberg::sensitive::SensitiveBytes +impl core::cmp::PartialEq for iceberg::sensitive::SensitiveBytes +pub fn iceberg::sensitive::SensitiveBytes::eq(&self, other: &iceberg::sensitive::SensitiveBytes) -> bool +impl core::convert::TryFrom for iceberg::encryption::SecureKey +pub type iceberg::encryption::SecureKey::Error = iceberg::Error +pub fn iceberg::encryption::SecureKey::try_from(key: iceberg::sensitive::SensitiveBytes) -> iceberg::Result +impl core::fmt::Debug for iceberg::sensitive::SensitiveBytes +pub fn iceberg::sensitive::SensitiveBytes::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for iceberg::sensitive::SensitiveBytes +pub fn iceberg::sensitive::SensitiveBytes::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for iceberg::sensitive::SensitiveBytes pub struct iceberg::sensitive::SensitiveString(_) impl iceberg::sensitive::SensitiveString pub fn iceberg::sensitive::SensitiveString::expose(&self) -> &str diff --git a/crates/iceberg/src/encryption/crypto.rs b/crates/iceberg/src/encryption/crypto.rs index 5c7549d9ba..4c33da019e 100644 --- a/crates/iceberg/src/encryption/crypto.rs +++ b/crates/iceberg/src/encryption/crypto.rs @@ -17,68 +17,20 @@ //! Core cryptographic operations for Iceberg encryption. -use std::fmt; use std::str::FromStr; use aes_gcm::aead::generic_array::typenum::U12; use aes_gcm::aead::rand_core::RngCore; use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng, Payload}; use aes_gcm::{Aes128Gcm, Aes256Gcm, AesGcm, Nonce}; -use zeroize::Zeroizing; /// AES-192-GCM with 96-bit nonce. Not provided by `aes-gcm` but constructible /// from the underlying primitives, same as `Aes128Gcm` and `Aes256Gcm`. type Aes192Gcm = AesGcm; +use crate::sensitive::SensitiveBytes; use crate::{Error, ErrorKind, Result}; -/// Wrapper for sensitive byte data (encryption keys, DEKs, etc.) that: -/// - Zeroizes memory on drop -/// - Redacts content in [`Debug`] and [`Display`] output -/// - Provides only `&[u8]` access via [`as_bytes()`](Self::as_bytes) -/// - Uses `Box<[u8]>` (immutable boxed slice) since key bytes never grow -/// -/// Use this type for any struct field that holds plaintext key material. -/// Because its [`Debug`] impl always prints `[N bytes REDACTED]`, structs -/// containing `SensitiveBytes` can safely derive or implement `Debug` -/// without risk of leaking key material. -#[derive(Clone, PartialEq, Eq)] -pub struct SensitiveBytes(Zeroizing>); - -impl SensitiveBytes { - /// Wraps the given bytes as sensitive material. - pub fn new(bytes: impl Into>) -> Self { - Self(Zeroizing::new(bytes.into())) - } - - /// Returns the underlying bytes. - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } - - /// Returns the number of bytes. - pub fn len(&self) -> usize { - self.0.len() - } - - /// Returns `true` if the byte slice is empty. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -impl fmt::Debug for SensitiveBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[{} bytes REDACTED]", self.0.len()) - } -} - -impl fmt::Display for SensitiveBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[{} bytes REDACTED]", self.0.len()) - } -} - /// Supported AES key sizes for AES-GCM encryption. /// /// The Iceberg spec supports 128, 192, and 256-bit keys for AES-GCM. @@ -532,4 +484,11 @@ mod tests { "Encrypted portion should be plaintext length + 16-byte tag" ); } + + #[test] + fn test_backwards_compatible_sensitive_bytes_import() { + use crate::encryption::SensitiveBytes; + + let _ = SensitiveBytes::new(&b"123"[..]); + } } diff --git a/crates/iceberg/src/encryption/manager.rs b/crates/iceberg/src/encryption/manager.rs index e2294c2f2c..3f5848a289 100644 --- a/crates/iceberg/src/encryption/manager.rs +++ b/crates/iceberg/src/encryption/manager.rs @@ -36,11 +36,12 @@ use uuid::Uuid; const MILLIS_IN_DAY: i64 = 24 * 60 * 60 * 1000; -use super::crypto::{AesGcmCipher, AesKeySize, SecureKey, SensitiveBytes}; +use super::crypto::{AesGcmCipher, AesKeySize, SecureKey}; use super::io::EncryptedOutputFile; use super::key_metadata::StandardKeyMetadata; use super::kms::KeyManagementClient; use crate::io::OutputFile; +use crate::sensitive::SensitiveBytes; use crate::spec::{EncryptedKey, FormatVersion, TableMetadataRef}; use crate::{Error, ErrorKind, Result}; diff --git a/crates/iceberg/src/encryption/mod.rs b/crates/iceberg/src/encryption/mod.rs index 12ee76e5e0..73e7527ce4 100644 --- a/crates/iceberg/src/encryption/mod.rs +++ b/crates/iceberg/src/encryption/mod.rs @@ -27,9 +27,11 @@ pub mod kms; mod manager; mod stream; -pub use crypto::{AesGcmCipher, AesKeySize, SecureKey, SensitiveBytes}; +pub use crypto::{AesGcmCipher, AesKeySize, SecureKey}; pub use io::{EncryptedInputFile, EncryptedOutputFile}; pub use key_metadata::StandardKeyMetadata; pub use kms::{GeneratedKey, KeyManagementClient}; pub use manager::EncryptionManager; pub use stream::{AesGcmFileRead, AesGcmFileWrite}; + +pub use crate::sensitive::SensitiveBytes; diff --git a/crates/iceberg/src/sensitive.rs b/crates/iceberg/src/sensitive.rs index 2e452bc951..a6326515ff 100644 --- a/crates/iceberg/src/sensitive.rs +++ b/crates/iceberg/src/sensitive.rs @@ -76,6 +76,53 @@ impl From for SensitiveString { } } +/// Wrapper for sensitive byte data (encryption keys, DEKs, etc.) that: +/// - Zeroizes memory on drop +/// - Redacts content in [`Debug`] and [`Display`] output +/// - Provides only `&[u8]` access via [`as_bytes()`](Self::as_bytes) +/// - Uses `Box<[u8]>` (immutable boxed slice) since key bytes never grow +/// +/// Use this type for any struct field that holds plaintext key material. +/// Because its [`Debug`] impl always prints `[N bytes REDACTED]`, structs +/// containing `SensitiveBytes` can safely derive or implement `Debug` +/// without risk of leaking key material. +#[derive(Clone, PartialEq, Eq)] +pub struct SensitiveBytes(Zeroizing>); + +impl SensitiveBytes { + /// Wraps the given bytes as sensitive material. + pub fn new(bytes: impl Into>) -> Self { + Self(Zeroizing::new(bytes.into())) + } + + /// Returns the underlying bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Returns the number of bytes. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if the byte slice is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl fmt::Debug for SensitiveBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{} bytes REDACTED]", self.0.len()) + } +} + +impl fmt::Display for SensitiveBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{} bytes REDACTED]", self.0.len()) + } +} + #[cfg(test)] mod tests { use crate::sensitive::SensitiveString; From 085d66ade0718d79106800388fc531d7a6d9cedd Mon Sep 17 00:00:00 2001 From: Jannik Steinmann Date: Thu, 6 Aug 2026 12:54:04 +0200 Subject: [PATCH 4/6] Add redaction tests for SensitiveBytes Signed-off-by: Jannik Steinmann --- crates/iceberg/src/sensitive.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/iceberg/src/sensitive.rs b/crates/iceberg/src/sensitive.rs index a6326515ff..58bd8aa7b6 100644 --- a/crates/iceberg/src/sensitive.rs +++ b/crates/iceberg/src/sensitive.rs @@ -125,13 +125,39 @@ impl fmt::Display for SensitiveBytes { #[cfg(test)] mod tests { - use crate::sensitive::SensitiveString; + use crate::sensitive::{SensitiveBytes, SensitiveString}; #[test] - fn test_sensitive_string_redacts_value() { + fn test_sensitive_string_redacts_debug_value() { let sensitive_value = "my-pw-12346"; let logged = format!("{:?}", SensitiveString::from(sensitive_value.to_string())); assert!(!logged.contains(sensitive_value)); } + + #[test] + fn test_sensitive_bytes_redacts_debug_value() { + let sensitive_value = b"my-secret-bytes"; + + let logged = format!("{:?}", SensitiveBytes::new(&sensitive_value[..])); + assert!( + !logged + .as_bytes() + .windows(sensitive_value.len()) + .any(|window| window == sensitive_value) + ); + } + + #[test] + fn test_sensitive_bytes_redacts_display_value() { + let sensitive_value = b"my-secret-bytes"; + + let logged = format!("{}", SensitiveBytes::new(&sensitive_value[..])); + assert!( + !logged + .as_bytes() + .windows(sensitive_value.len()) + .any(|window| window == sensitive_value) + ); + } } From 971761eaec0c2a0a88059f1c0cf6c2dbd207a65f Mon Sep 17 00:00:00 2001 From: Jannik Steinmann Date: Thu, 6 Aug 2026 20:12:53 +0200 Subject: [PATCH 5/6] Add additional SensitiveBytes API to SensitiveString Signed-off-by: Jannik Steinmann Re-generate public API --- crates/iceberg/public-api.txt | 5 +++++ crates/iceberg/src/sensitive.rs | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/iceberg/public-api.txt b/crates/iceberg/public-api.txt index 99112b9462..bb34d0d620 100644 --- a/crates/iceberg/public-api.txt +++ b/crates/iceberg/public-api.txt @@ -1387,12 +1387,17 @@ impl core::marker::StructuralPartialEq for iceberg::sensitive::SensitiveBytes pub struct iceberg::sensitive::SensitiveString(_) impl iceberg::sensitive::SensitiveString pub fn iceberg::sensitive::SensitiveString::expose(&self) -> &str +pub fn iceberg::sensitive::SensitiveString::is_empty(&self) -> bool impl core::clone::Clone for iceberg::sensitive::SensitiveString pub fn iceberg::sensitive::SensitiveString::clone(&self) -> iceberg::sensitive::SensitiveString +impl core::cmp::Eq for iceberg::sensitive::SensitiveString +impl core::cmp::PartialEq for iceberg::sensitive::SensitiveString +pub fn iceberg::sensitive::SensitiveString::eq(&self, other: &iceberg::sensitive::SensitiveString) -> bool impl core::convert::From for iceberg::sensitive::SensitiveString pub fn iceberg::sensitive::SensitiveString::from(value: alloc::string::String) -> Self impl core::fmt::Debug for iceberg::sensitive::SensitiveString pub fn iceberg::sensitive::SensitiveString::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for iceberg::sensitive::SensitiveString pub mod iceberg::spec pub use iceberg::spec::ByteBuf pub enum iceberg::spec::DataContentType diff --git a/crates/iceberg/src/sensitive.rs b/crates/iceberg/src/sensitive.rs index 58bd8aa7b6..de1c609008 100644 --- a/crates/iceberg/src/sensitive.rs +++ b/crates/iceberg/src/sensitive.rs @@ -54,7 +54,7 @@ use zeroize::Zeroizing; /// SensitiveString::from("token".to_string()).expose() /// ); /// ``` -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub struct SensitiveString(Zeroizing); impl SensitiveString { @@ -62,6 +62,11 @@ impl SensitiveString { pub fn expose(&self) -> &str { &self.0 } + + /// Returns `true` if the string value is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } impl fmt::Debug for SensitiveString { From 6bb1ffe5e69de1e19f475d1e39668c1e726229aa Mon Sep 17 00:00:00 2001 From: Jannik Steinmann Date: Fri, 14 Aug 2026 10:33:43 +0200 Subject: [PATCH 6/6] Remove export test --- crates/iceberg/src/encryption/crypto.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/iceberg/src/encryption/crypto.rs b/crates/iceberg/src/encryption/crypto.rs index 4c33da019e..74badd286e 100644 --- a/crates/iceberg/src/encryption/crypto.rs +++ b/crates/iceberg/src/encryption/crypto.rs @@ -484,11 +484,4 @@ mod tests { "Encrypted portion should be plaintext length + 16-byte tag" ); } - - #[test] - fn test_backwards_compatible_sensitive_bytes_import() { - use crate::encryption::SensitiveBytes; - - let _ = SensitiveBytes::new(&b"123"[..]); - } }