Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 67 additions & 42 deletions crates/iceberg/public-api.txt

Large diffs are not rendered by default.

56 changes: 6 additions & 50 deletions crates/iceberg/src/catalog/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -61,7 +61,7 @@ pub struct SessionContext {
properties: HashMap<String, String>,

#[builder(default)]
credentials: HashMap<String, Credential>,
credentials: HashMap<String, SensitiveString>,
}

impl SessionContext {
Expand All @@ -88,47 +88,11 @@ impl SessionContext {
}

/// Returns the session's credential map.
pub fn credentials(&self) -> &HashMap<String, Credential> {
pub fn credentials(&self) -> &HashMap<String, SensitiveString> {
&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<String>);

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<String> 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)]
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -271,25 +236,16 @@ 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();

let logged = format!("{:?}", session);
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::<Credential>();
assert_send_sync::<SessionContext>();
assert_send_sync::<dyn SessionCatalog>();

Expand Down
57 changes: 8 additions & 49 deletions crates/iceberg/src/encryption/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<aes_gcm::aes::Aes192, U12>;

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<Box<[u8]>>);

impl SensitiveBytes {
/// Wraps the given bytes as sensitive material.
pub fn new(bytes: impl Into<Box<[u8]>>) -> 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.
Expand Down Expand Up @@ -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"[..]);
}
Comment on lines +489 to +493

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not totally sure this test is adding a lot, we could remove it?

}
3 changes: 2 additions & 1 deletion crates/iceberg/src/encryption/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
4 changes: 3 additions & 1 deletion crates/iceberg/src/encryption/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 2 additions & 0 deletions crates/iceberg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ extern crate core;
mod error;
pub use error::{Error, ErrorKind, Result};

pub mod sensitive;

mod catalog;

pub use catalog::utils::drop_table_data;
Expand Down
168 changes: 168 additions & 0 deletions crates/iceberg/src/sensitive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// 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, PartialEq, Eq)]
pub struct SensitiveString(Zeroizing<String>);

impl SensitiveString {
/// Returns the raw value of the sensitive string.
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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("SensitiveString([REDACTED])")
}
}

impl From<String> for SensitiveString {
fn from(value: String) -> Self {
Self(Zeroizing::new(value))
}
}

/// 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<Box<[u8]>>);

impl SensitiveBytes {
/// Wraps the given bytes as sensitive material.
pub fn new(bytes: impl Into<Box<[u8]>>) -> 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::{SensitiveBytes, SensitiveString};

#[test]
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)
);
}
}
Loading