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