Skip to main content

iceberg/encryption/
crypto.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//! Core cryptographic operations for Iceberg encryption.
19
20use std::fmt;
21use std::str::FromStr;
22
23use aes_gcm::aead::generic_array::typenum::U12;
24use aes_gcm::aead::rand_core::RngCore;
25use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng, Payload};
26use aes_gcm::{Aes128Gcm, Aes256Gcm, AesGcm, Nonce};
27use zeroize::Zeroizing;
28
29/// AES-192-GCM with 96-bit nonce. Not provided by `aes-gcm` but constructible
30/// from the underlying primitives, same as `Aes128Gcm` and `Aes256Gcm`.
31type Aes192Gcm = AesGcm<aes_gcm::aes::Aes192, U12>;
32
33use crate::{Error, ErrorKind, Result};
34
35/// Wrapper for sensitive byte data (encryption keys, DEKs, etc.) that:
36/// - Zeroizes memory on drop
37/// - Redacts content in [`Debug`] and [`Display`] output
38/// - Provides only `&[u8]` access via [`as_bytes()`](Self::as_bytes)
39/// - Uses `Box<[u8]>` (immutable boxed slice) since key bytes never grow
40///
41/// Use this type for any struct field that holds plaintext key material.
42/// Because its [`Debug`] impl always prints `[N bytes REDACTED]`, structs
43/// containing `SensitiveBytes` can safely derive or implement `Debug`
44/// without risk of leaking key material.
45#[derive(Clone, PartialEq, Eq)]
46pub struct SensitiveBytes(Zeroizing<Box<[u8]>>);
47
48impl SensitiveBytes {
49    /// Wraps the given bytes as sensitive material.
50    pub fn new(bytes: impl Into<Box<[u8]>>) -> Self {
51        Self(Zeroizing::new(bytes.into()))
52    }
53
54    /// Returns the underlying bytes.
55    pub fn as_bytes(&self) -> &[u8] {
56        &self.0
57    }
58
59    /// Returns the number of bytes.
60    pub fn len(&self) -> usize {
61        self.0.len()
62    }
63
64    /// Returns `true` if the byte slice is empty.
65    pub fn is_empty(&self) -> bool {
66        self.0.is_empty()
67    }
68}
69
70impl fmt::Debug for SensitiveBytes {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(f, "[{} bytes REDACTED]", self.0.len())
73    }
74}
75
76impl fmt::Display for SensitiveBytes {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(f, "[{} bytes REDACTED]", self.0.len())
79    }
80}
81
82/// Supported AES key sizes for AES-GCM encryption.
83///
84/// The Iceberg spec supports 128, 192, and 256-bit keys for AES-GCM.
85/// See: <https://iceberg.apache.org/gcm-stream-spec/#goals>
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
87pub enum AesKeySize {
88    /// 128-bit AES key (16 bytes). Default per the Iceberg spec.
89    #[default]
90    Bits128 = 128,
91    /// 192-bit AES key (24 bytes)
92    Bits192 = 192,
93    /// 256-bit AES key (32 bytes)
94    Bits256 = 256,
95}
96
97impl AesKeySize {
98    /// Returns the key length in bytes for this key size.
99    pub fn key_length(&self) -> usize {
100        match self {
101            Self::Bits128 => 16,
102            Self::Bits192 => 24,
103            Self::Bits256 => 32,
104        }
105    }
106
107    /// Returns the key size for a given DEK length in bytes.
108    ///
109    /// Matches Java's `encryption.data-key-length` property semantics:
110    /// 16 → 128-bit, 24 → 192-bit, 32 → 256-bit.
111    pub fn from_key_length(len: usize) -> Result<Self> {
112        match len {
113            16 => Ok(Self::Bits128),
114            24 => Ok(Self::Bits192),
115            32 => Ok(Self::Bits256),
116            _ => Err(Error::new(
117                ErrorKind::FeatureUnsupported,
118                format!("Unsupported data key length: {len} (must be 16, 24, or 32)"),
119            )),
120        }
121    }
122}
123
124impl FromStr for AesKeySize {
125    type Err = Error;
126
127    fn from_str(s: &str) -> Result<Self> {
128        match s {
129            "128" | "AES_GCM_128" | "AES128_GCM" => Ok(Self::Bits128),
130            "192" | "AES_GCM_192" | "AES192_GCM" => Ok(Self::Bits192),
131            "256" | "AES_GCM_256" | "AES256_GCM" => Ok(Self::Bits256),
132            _ => Err(Error::new(
133                ErrorKind::FeatureUnsupported,
134                format!("Unsupported AES key size: {s}"),
135            )),
136        }
137    }
138}
139
140/// A secure encryption key that zeroes its memory on drop.
141///
142/// The `Debug` impl is safe to expose: the inner [`SensitiveBytes`] redacts
143/// the key material, printing only its length.
144#[derive(Clone, Debug, PartialEq, Eq)]
145pub struct SecureKey {
146    key: SensitiveBytes,
147    key_size: AesKeySize,
148}
149
150impl SecureKey {
151    /// Creates a new secure key with the specified key size.
152    ///
153    /// # Errors
154    /// Returns an error if the key length doesn't match the key size requirements.
155    pub fn new(key: &[u8]) -> Result<Self> {
156        let key_size = AesKeySize::from_key_length(key.len())?;
157        Ok(Self {
158            key: SensitiveBytes::new(key),
159            key_size,
160        })
161    }
162
163    /// Generates a new random key for the specified key size.
164    pub fn generate(key_size: AesKeySize) -> Self {
165        let mut key = vec![0u8; key_size.key_length()];
166        OsRng.fill_bytes(&mut key);
167        Self {
168            key: SensitiveBytes::new(key),
169            key_size,
170        }
171    }
172
173    /// Returns the AES key size.
174    pub fn key_size(&self) -> AesKeySize {
175        self.key_size
176    }
177
178    /// Returns the key bytes.
179    pub fn as_bytes(&self) -> &[u8] {
180        self.key.as_bytes()
181    }
182}
183
184impl TryFrom<SensitiveBytes> for SecureKey {
185    type Error = Error;
186
187    fn try_from(key: SensitiveBytes) -> Result<Self> {
188        let key_size = AesKeySize::from_key_length(key.len())?;
189        Ok(Self { key, key_size })
190    }
191}
192
193/// AES-GCM cipher for encrypting and decrypting data.
194pub struct AesGcmCipher {
195    key: SensitiveBytes,
196    key_size: AesKeySize,
197}
198
199impl AesGcmCipher {
200    /// AES-GCM nonce length in bytes (96 bits).
201    pub const NONCE_LEN: usize = 12;
202    /// AES-GCM authentication tag length in bytes (128 bits).
203    pub const TAG_LEN: usize = 16;
204
205    /// Creates a new cipher with the specified key.
206    pub fn new(key: SecureKey) -> Self {
207        Self {
208            key: SensitiveBytes::new(key.as_bytes()),
209            key_size: key.key_size(),
210        }
211    }
212
213    /// Encrypts data using AES-GCM.
214    ///
215    /// # Arguments
216    /// * `plaintext` - The data to encrypt
217    /// * `aad` - Additional authenticated data (optional)
218    ///
219    /// # Returns
220    /// The encrypted data in the format: [12-byte nonce][ciphertext][16-byte auth tag]
221    /// This matches the Java implementation format for compatibility.
222    pub fn encrypt(&self, plaintext: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>> {
223        match self.key_size {
224            AesKeySize::Bits128 => {
225                encrypt_aes_gcm::<Aes128Gcm>(self.key.as_bytes(), plaintext, aad)
226            }
227            AesKeySize::Bits192 => {
228                encrypt_aes_gcm::<Aes192Gcm>(self.key.as_bytes(), plaintext, aad)
229            }
230            AesKeySize::Bits256 => {
231                encrypt_aes_gcm::<Aes256Gcm>(self.key.as_bytes(), plaintext, aad)
232            }
233        }
234    }
235
236    /// Decrypts data using AES-GCM.
237    ///
238    /// # Arguments
239    /// * `ciphertext` - The encrypted data with format: [12-byte nonce][encrypted data][16-byte auth tag]
240    /// * `aad` - Additional authenticated data (must match encryption)
241    ///
242    /// # Returns
243    /// The decrypted plaintext.
244    pub fn decrypt(&self, ciphertext: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>> {
245        if ciphertext.len() < Self::NONCE_LEN + Self::TAG_LEN {
246            return Err(Error::new(
247                ErrorKind::DataInvalid,
248                format!(
249                    "Ciphertext too short: expected at least {} bytes, got {}",
250                    Self::NONCE_LEN + Self::TAG_LEN,
251                    ciphertext.len()
252                ),
253            ));
254        }
255
256        match self.key_size {
257            AesKeySize::Bits128 => {
258                decrypt_aes_gcm::<Aes128Gcm>(self.key.as_bytes(), ciphertext, aad)
259            }
260            AesKeySize::Bits192 => {
261                decrypt_aes_gcm::<Aes192Gcm>(self.key.as_bytes(), ciphertext, aad)
262            }
263            AesKeySize::Bits256 => {
264                decrypt_aes_gcm::<Aes256Gcm>(self.key.as_bytes(), ciphertext, aad)
265            }
266        }
267    }
268}
269
270fn encrypt_aes_gcm<C>(key_bytes: &[u8], plaintext: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>>
271where C: Aead + AeadCore + KeyInit {
272    let cipher = C::new_from_slice(key_bytes).map_err(|e| {
273        Error::new(ErrorKind::DataInvalid, "Invalid AES key").with_source(anyhow::anyhow!(e))
274    })?;
275    let nonce = C::generate_nonce(&mut OsRng);
276
277    let ciphertext = if let Some(aad) = aad {
278        cipher.encrypt(&nonce, Payload {
279            msg: plaintext,
280            aad,
281        })
282    } else {
283        cipher.encrypt(&nonce, plaintext.as_ref())
284    }
285    .map_err(|e| {
286        Error::new(ErrorKind::Unexpected, "AES-GCM encryption failed")
287            .with_source(anyhow::anyhow!(e))
288    })?;
289
290    // Prepend nonce to ciphertext (Java compatible format)
291    let mut result = Vec::with_capacity(nonce.len() + ciphertext.len());
292    result.extend_from_slice(&nonce);
293    result.extend_from_slice(&ciphertext);
294    Ok(result)
295}
296
297fn decrypt_aes_gcm<C>(key_bytes: &[u8], ciphertext: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>>
298where C: Aead + AeadCore + KeyInit {
299    let cipher = C::new_from_slice(key_bytes).map_err(|e| {
300        Error::new(ErrorKind::DataInvalid, "Invalid AES key").with_source(anyhow::anyhow!(e))
301    })?;
302
303    let nonce = Nonce::from_slice(&ciphertext[..AesGcmCipher::NONCE_LEN]);
304    let encrypted_data = &ciphertext[AesGcmCipher::NONCE_LEN..];
305
306    let plaintext = if let Some(aad) = aad {
307        cipher.decrypt(nonce, Payload {
308            msg: encrypted_data,
309            aad,
310        })
311    } else {
312        cipher.decrypt(nonce, encrypted_data)
313    }
314    .map_err(|e| {
315        Error::new(ErrorKind::Unexpected, "AES-GCM decryption failed")
316            .with_source(anyhow::anyhow!(e))
317    })?;
318
319    Ok(plaintext)
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_aes_key_size() {
328        assert_eq!(AesKeySize::Bits128.key_length(), 16);
329        assert_eq!(AesKeySize::Bits192.key_length(), 24);
330        assert_eq!(AesKeySize::Bits256.key_length(), 32);
331
332        assert_eq!(
333            AesKeySize::from_key_length(16).unwrap(),
334            AesKeySize::Bits128
335        );
336        assert_eq!(
337            AesKeySize::from_key_length(24).unwrap(),
338            AesKeySize::Bits192
339        );
340        assert_eq!(
341            AesKeySize::from_key_length(32).unwrap(),
342            AesKeySize::Bits256
343        );
344        assert!(AesKeySize::from_key_length(8).is_err());
345
346        assert_eq!(AesKeySize::from_str("128").unwrap(), AesKeySize::Bits128);
347        assert_eq!(
348            AesKeySize::from_str("AES_GCM_128").unwrap(),
349            AesKeySize::Bits128
350        );
351        assert_eq!(
352            AesKeySize::from_str("AES_GCM_256").unwrap(),
353            AesKeySize::Bits256
354        );
355        assert!(AesKeySize::from_str("INVALID").is_err());
356    }
357
358    #[test]
359    fn test_secure_key() {
360        // Test key generation
361        let key1 = SecureKey::generate(AesKeySize::Bits128);
362        assert_eq!(key1.as_bytes().len(), 16);
363        assert_eq!(key1.key_size(), AesKeySize::Bits128);
364
365        // Test key creation with validation
366        let valid_key = [0u8; 16];
367        assert!(SecureKey::new(valid_key.as_slice()).is_ok());
368
369        let invalid_key = [0u8; 33];
370        assert!(SecureKey::new(invalid_key.as_slice()).is_err());
371    }
372
373    #[test]
374    fn test_aes128_gcm_encryption_roundtrip() {
375        let key = SecureKey::generate(AesKeySize::Bits128);
376        let cipher = AesGcmCipher::new(key);
377
378        let plaintext = b"Hello, Iceberg encryption!";
379        let aad = b"additional authenticated data";
380
381        // Test without AAD
382        let ciphertext = cipher.encrypt(plaintext, None).unwrap();
383        assert!(ciphertext.len() > plaintext.len() + 12); // nonce + tag
384        assert_ne!(&ciphertext[12..], plaintext); // encrypted portion differs
385
386        let decrypted = cipher.decrypt(&ciphertext, None).unwrap();
387        assert_eq!(decrypted, plaintext);
388
389        // Test with AAD
390        let ciphertext = cipher.encrypt(plaintext, Some(aad)).unwrap();
391        let decrypted = cipher.decrypt(&ciphertext, Some(aad)).unwrap();
392        assert_eq!(decrypted, plaintext);
393
394        // Test with wrong AAD fails
395        assert!(cipher.decrypt(&ciphertext, Some(b"wrong aad")).is_err());
396    }
397
398    #[test]
399    fn test_aes192_gcm_encryption_roundtrip() {
400        let key = SecureKey::generate(AesKeySize::Bits192);
401        let cipher = AesGcmCipher::new(key);
402
403        let plaintext = b"Hello, Iceberg encryption!";
404        let aad = b"additional authenticated data";
405
406        // Test without AAD
407        let ciphertext = cipher.encrypt(plaintext, None).unwrap();
408        let decrypted = cipher.decrypt(&ciphertext, None).unwrap();
409        assert_eq!(decrypted, plaintext);
410
411        // Test with AAD
412        let ciphertext = cipher.encrypt(plaintext, Some(aad)).unwrap();
413        let decrypted = cipher.decrypt(&ciphertext, Some(aad)).unwrap();
414        assert_eq!(decrypted, plaintext);
415
416        // Test with wrong AAD fails
417        assert!(cipher.decrypt(&ciphertext, Some(b"wrong aad")).is_err());
418    }
419
420    #[test]
421    fn test_aes256_gcm_encryption_roundtrip() {
422        let key = SecureKey::generate(AesKeySize::Bits256);
423        let cipher = AesGcmCipher::new(key);
424
425        let plaintext = b"Hello, Iceberg encryption!";
426        let aad = b"additional authenticated data";
427
428        // Test without AAD
429        let ciphertext = cipher.encrypt(plaintext, None).unwrap();
430        let decrypted = cipher.decrypt(&ciphertext, None).unwrap();
431        assert_eq!(decrypted, plaintext);
432
433        // Test with AAD
434        let ciphertext = cipher.encrypt(plaintext, Some(aad)).unwrap();
435        let decrypted = cipher.decrypt(&ciphertext, Some(aad)).unwrap();
436        assert_eq!(decrypted, plaintext);
437
438        // Test with wrong AAD fails
439        assert!(cipher.decrypt(&ciphertext, Some(b"wrong aad")).is_err());
440    }
441
442    #[test]
443    fn test_cross_key_size_incompatibility() {
444        let plaintext = b"Cross-key test";
445
446        let key128 = SecureKey::generate(AesKeySize::Bits128);
447        let key256 = SecureKey::generate(AesKeySize::Bits256);
448
449        let cipher128 = AesGcmCipher::new(key128);
450        let cipher256 = AesGcmCipher::new(key256);
451
452        // Ciphertext from 128-bit key should not decrypt with 256-bit key
453        let ciphertext = cipher128.encrypt(plaintext, None).unwrap();
454        assert!(cipher256.decrypt(&ciphertext, None).is_err());
455    }
456
457    #[test]
458    fn test_encryption_with_empty_plaintext() {
459        let key = SecureKey::generate(AesKeySize::Bits128);
460        let cipher = AesGcmCipher::new(key);
461
462        let plaintext = b"";
463        let ciphertext = cipher.encrypt(plaintext, None).unwrap();
464
465        // Even empty plaintext produces nonce + tag
466        assert_eq!(ciphertext.len(), 12 + 16); // 12-byte nonce + 16-byte tag
467
468        let decrypted = cipher.decrypt(&ciphertext, None).unwrap();
469        assert_eq!(decrypted, plaintext);
470    }
471
472    #[test]
473    fn test_decryption_with_tampered_ciphertext() {
474        let key = SecureKey::generate(AesKeySize::Bits128);
475        let cipher = AesGcmCipher::new(key);
476
477        let plaintext = b"Sensitive data";
478        let mut ciphertext = cipher.encrypt(plaintext, None).unwrap();
479
480        // Tamper with the encrypted portion (after the nonce)
481        if ciphertext.len() > 12 {
482            ciphertext[12] ^= 0xFF;
483        }
484
485        // Decryption should fail due to authentication tag mismatch
486        assert!(cipher.decrypt(&ciphertext, None).is_err());
487    }
488
489    #[test]
490    fn test_different_keys_produce_different_ciphertexts() {
491        let key1 = SecureKey::generate(AesKeySize::Bits128);
492        let key2 = SecureKey::generate(AesKeySize::Bits128);
493
494        let cipher1 = AesGcmCipher::new(key1);
495        let cipher2 = AesGcmCipher::new(key2);
496
497        let plaintext = b"Same plaintext";
498
499        let ciphertext1 = cipher1.encrypt(plaintext, None).unwrap();
500        let ciphertext2 = cipher2.encrypt(plaintext, None).unwrap();
501
502        // Different keys should produce different ciphertexts (comparing the encrypted portion)
503        // Note: The nonces will also be different, but we're mainly interested in the encrypted data
504        assert_ne!(&ciphertext1[12..], &ciphertext2[12..]);
505    }
506
507    #[test]
508    fn test_ciphertext_format_java_compatible() {
509        // Test that our ciphertext format matches Java's: [12-byte nonce][ciphertext][16-byte tag]
510        let key = SecureKey::generate(AesKeySize::Bits128);
511        let cipher = AesGcmCipher::new(key);
512
513        let plaintext = b"Test data";
514        let ciphertext = cipher.encrypt(plaintext, None).unwrap();
515
516        // Format should be: [12-byte nonce][encrypted_data + 16-byte GCM tag]
517        assert_eq!(
518            ciphertext.len(),
519            12 + plaintext.len() + 16,
520            "Ciphertext should be nonce + plaintext + tag length"
521        );
522
523        // Verify we can decrypt by extracting nonce from the beginning
524        let nonce = &ciphertext[..12];
525        assert_eq!(nonce.len(), 12, "Nonce should be 12 bytes");
526
527        // The rest is encrypted data + tag
528        let encrypted_with_tag = &ciphertext[12..];
529        assert_eq!(
530            encrypted_with_tag.len(),
531            plaintext.len() + 16,
532            "Encrypted portion should be plaintext length + 16-byte tag"
533        );
534    }
535}