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