Skip to main content

parquet/file/metadata/thrift/
encryption.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//! Encryption support for Thrift serialization
19
20use crate::{
21    encryption::decrypt::{FileDecryptionProperties, FileDecryptor},
22    errors::{ParquetError, Result},
23    file::{
24        column_crypto_metadata::ColumnCryptoMetaData,
25        metadata::{
26            HeapSize, ParquetMetaData, ParquetMetaDataOptions, RowGroupMetaData,
27            thrift::{
28                file_metadata_and_row_groups_from_bytes, read_column_metadata,
29                validate_column_metadata,
30            },
31        },
32    },
33    parquet_thrift::{
34        ElementType, FieldType, ReadThrift, ThriftCompactInputProtocol,
35        ThriftCompactOutputProtocol, ThriftSliceInputProtocol, WriteThrift, WriteThriftField,
36    },
37    thrift_struct, thrift_union,
38};
39use std::io::Write;
40use std::sync::Arc;
41
42thrift_struct!(
43pub(crate) struct AesGcmV1 {
44  /// AAD prefix
45  1: optional binary aad_prefix
46
47  /// Unique file identifier part of AAD suffix
48  2: optional binary aad_file_unique
49
50  /// In files encrypted with AAD prefix without storing it,
51  /// readers must supply the prefix
52  3: optional bool supply_aad_prefix
53}
54);
55
56impl HeapSize for AesGcmV1 {
57    fn heap_size(&self) -> usize {
58        self.aad_prefix.heap_size()
59            + self.aad_file_unique.heap_size()
60            + self.supply_aad_prefix.heap_size()
61    }
62}
63
64thrift_struct!(
65pub(crate) struct AesGcmCtrV1 {
66  /// AAD prefix
67  1: optional binary aad_prefix
68
69  /// Unique file identifier part of AAD suffix
70  2: optional binary aad_file_unique
71
72  /// In files encrypted with AAD prefix without storing it,
73  /// readers must supply the prefix
74  3: optional bool supply_aad_prefix
75}
76);
77
78impl HeapSize for AesGcmCtrV1 {
79    fn heap_size(&self) -> usize {
80        self.aad_prefix.heap_size()
81            + self.aad_file_unique.heap_size()
82            + self.supply_aad_prefix.heap_size()
83    }
84}
85
86thrift_union!(
87union EncryptionAlgorithm {
88  1: (AesGcmV1) AES_GCM_V1
89  2: (AesGcmCtrV1) AES_GCM_CTR_V1
90}
91);
92
93impl HeapSize for EncryptionAlgorithm {
94    fn heap_size(&self) -> usize {
95        match self {
96            Self::AES_GCM_V1(gcm) => gcm.heap_size(),
97            Self::AES_GCM_CTR_V1(gcm_ctr) => gcm_ctr.heap_size(),
98        }
99    }
100}
101
102thrift_struct!(
103/// Crypto metadata for files with encrypted footer
104pub(crate) struct FileCryptoMetaData<'a> {
105  /// Encryption algorithm. This field is only used for files
106  /// with encrypted footer. Files with plaintext footer store algorithm id
107  /// inside footer (FileMetaData structure).
108  1: required EncryptionAlgorithm encryption_algorithm
109
110  /// Retrieval metadata of key used for encryption of footer,
111  /// and (possibly) columns.
112  2: optional binary<'a> key_metadata
113}
114);
115
116fn row_group_from_encrypted_thrift(
117    mut rg: RowGroupMetaData,
118    decryptor: Option<&FileDecryptor>,
119    options: Option<&ParquetMetaDataOptions>,
120) -> Result<RowGroupMetaData> {
121    let schema_descr = rg.schema_descr;
122
123    if schema_descr.num_columns() != rg.columns.len() {
124        return Err(general_err!(
125            "Column count mismatch. Schema has {} columns while Row Group has {}",
126            schema_descr.num_columns(),
127            rg.columns.len()
128        ));
129    }
130    let total_byte_size = rg.total_byte_size;
131    let num_rows = rg.num_rows;
132    let mut columns = vec![];
133
134    for (i, (mut c, d)) in rg
135        .columns
136        .drain(0..)
137        .zip(schema_descr.columns())
138        .enumerate()
139    {
140        // Read encrypted metadata if it's present and we have a decryptor.
141        if let (true, Some(decryptor)) = (c.encrypted_column_metadata.is_some(), decryptor) {
142            let column_decryptor = match c.crypto_metadata() {
143                None => {
144                    return Err(general_err!(
145                        "No crypto_metadata is set for column '{}', which has encrypted metadata",
146                        d.path().string()
147                    ));
148                }
149                Some(ColumnCryptoMetaData::ENCRYPTION_WITH_COLUMN_KEY(crypto_metadata)) => {
150                    let column_name = crypto_metadata.path_in_schema.join(".");
151                    // Try to get the decryptor - if it fails, we don't have the key
152                    match decryptor.get_column_metadata_decryptor(
153                        column_name.as_str(),
154                        crypto_metadata.key_metadata.as_deref(),
155                    ) {
156                        Ok(dec) => dec,
157                        Err(_) => {
158                            // We don't have the key for this column, so we can't decrypt its metadata.
159                            columns.push(c);
160                            continue;
161                        }
162                    }
163                }
164                Some(ColumnCryptoMetaData::ENCRYPTION_WITH_FOOTER_KEY) => {
165                    decryptor.get_footer_decryptor()?
166                }
167            };
168
169            // The ordinal is part of the AAD for encrypted column metadata.
170            // It can be missing here only for files with *mixed* row-group
171            // ordinals, which decode leaves untouched — fail cleanly rather
172            // than panic (see `ensure_row_group_ordinals`).
173            let rg_ordinal = rg.ordinal.ok_or_else(|| {
174                general_err!(
175                    "Row group ordinal is required to decrypt column metadata for \
176                     column '{}', but the file's row-group ordinals are inconsistent",
177                    d.path().string()
178                )
179            })?;
180            // Reject a negative ordinal cleanly rather than sign-extending it into
181            // a bogus AAD via `as usize`.
182            let rg_ordinal = usize::try_from(rg_ordinal).map_err(|_| {
183                general_err!(
184                    "Row group ordinal {rg_ordinal} is invalid (must be non-negative) \
185                     for decrypting column metadata for column '{}'",
186                    d.path().string()
187                )
188            })?;
189            let column_aad = crate::encryption::modules::create_module_aad(
190                decryptor.file_aad(),
191                crate::encryption::modules::ModuleType::ColumnMetaData,
192                rg_ordinal,
193                i,
194                None,
195            )?;
196
197            // Take the encrypted column metadata as it is no longer needed.
198            let encrypted_column_metadata = c.encrypted_column_metadata.take();
199            let buf = encrypted_column_metadata.unwrap();
200            let decrypted_cc_buf = column_decryptor
201                .decrypt(&buf, column_aad.as_ref())
202                .map_err(|_| {
203                    general_err!(
204                        "Unable to decrypt column '{}', perhaps the column key is wrong?",
205                        d.path().string()
206                    )
207                })?;
208
209            // parse decrypted buffer and then replace fields in 'c'
210            let mut prot = ThriftSliceInputProtocol::new(&decrypted_cc_buf);
211            let mask = read_column_metadata(&mut prot, &mut c, i, options)?;
212            validate_column_metadata(mask)?;
213
214            columns.push(c);
215        } else {
216            columns.push(c);
217        }
218    }
219
220    let sorting_columns = rg.sorting_columns;
221    let file_offset = rg.file_offset;
222    let ordinal = rg.ordinal;
223
224    Ok(RowGroupMetaData {
225        columns,
226        num_rows,
227        sorting_columns,
228        total_byte_size,
229        schema_descr,
230        file_offset,
231        ordinal,
232    })
233}
234
235/// Decodes [`ParquetMetaData`] from the provided bytes, handling metadata that may be encrypted.
236///
237/// Typically this is used to decode the metadata from the end of a parquet
238/// file. The format of `buf` is the Thrift compact binary protocol, as specified
239/// by the [Parquet Spec]. Buffer can be encrypted with AES GCM or AES CTR
240/// ciphers as specified in the [Parquet Encryption Spec].
241///
242/// [Parquet Spec]: https://github.com/apache/parquet-format#metadata
243/// [Parquet Encryption Spec]: https://parquet.apache.org/docs/file-format/data-pages/encryption/
244pub(crate) fn parquet_metadata_with_encryption(
245    file_decryption_properties: Option<&Arc<FileDecryptionProperties>>,
246    encrypted_footer: bool,
247    buf: &[u8],
248    options: Option<&ParquetMetaDataOptions>,
249) -> Result<ParquetMetaData> {
250    use crate::file::metadata::ParquetMetaDataBuilder;
251
252    let mut buf = buf;
253    let mut file_decryptor = None;
254    let decrypted_fmd_buf;
255
256    if encrypted_footer {
257        let mut prot = ThriftSliceInputProtocol::new(buf);
258        if let Some(file_decryption_properties) = file_decryption_properties {
259            let t_file_crypto_metadata: FileCryptoMetaData =
260                FileCryptoMetaData::read_thrift(&mut prot)
261                    .map_err(|e| general_err!("Could not parse crypto metadata: {}", e))?;
262            let supply_aad_prefix = match &t_file_crypto_metadata.encryption_algorithm {
263                EncryptionAlgorithm::AES_GCM_V1(algo) => algo.supply_aad_prefix,
264                EncryptionAlgorithm::AES_GCM_CTR_V1(_) => Some(false),
265            }
266            .unwrap_or(false);
267            if supply_aad_prefix && file_decryption_properties.aad_prefix().is_none() {
268                return Err(general_err!(
269                    "Parquet file was encrypted with an AAD prefix that is not stored in the file, \
270                        but no AAD prefix was provided in the file decryption properties"
271                ));
272            }
273            let decryptor = get_file_decryptor(
274                t_file_crypto_metadata.encryption_algorithm,
275                t_file_crypto_metadata.key_metadata,
276                file_decryption_properties,
277            )?;
278            let footer_decryptor = decryptor.get_footer_decryptor();
279            let aad_footer = crate::encryption::modules::create_footer_aad(decryptor.file_aad())?;
280
281            decrypted_fmd_buf = footer_decryptor?
282                .decrypt(prot.as_slice().as_ref(), aad_footer.as_ref())
283                .map_err(|_| {
284                    general_err!(
285                        "Provided footer key and AAD were unable to decrypt parquet footer"
286                    )
287                })?;
288
289            buf = &decrypted_fmd_buf;
290            file_decryptor = Some(decryptor);
291        } else {
292            return Err(general_err!(
293                "Parquet file has an encrypted footer but decryption properties were not provided"
294            ));
295        }
296    }
297
298    let (mut file_metadata, row_groups) = file_metadata_and_row_groups_from_bytes(buf, options)
299        .map_err(|e| general_err!("Could not parse metadata: {}", e))?;
300
301    // Take the encryption algorithm and footer signing key metadata as they are no longer
302    // needed after this.
303    if let (Some(algo), Some(file_decryption_properties)) = (
304        file_metadata.encryption_algorithm.take(),
305        file_decryption_properties,
306    ) {
307        let footer_signing_key_metadata = file_metadata.footer_signing_key_metadata.take();
308
309        // File has a plaintext footer but encryption algorithm is set
310        let file_decryptor_value = get_file_decryptor(
311            *algo,
312            footer_signing_key_metadata.as_deref(),
313            file_decryption_properties,
314        )?;
315        if file_decryption_properties.check_plaintext_footer_integrity() && !encrypted_footer {
316            file_decryptor_value.verify_plaintext_footer_signature(buf)?;
317        }
318        file_decryptor = Some(file_decryptor_value);
319    }
320
321    // decrypt column chunk info
322    let row_groups = row_groups
323        .into_iter()
324        .map(|rg| row_group_from_encrypted_thrift(rg, file_decryptor.as_ref(), options))
325        .collect::<Result<Vec<_>>>()?;
326
327    let metadata = ParquetMetaDataBuilder::new(file_metadata)
328        .set_row_groups(row_groups)
329        .set_file_decryptor(file_decryptor)
330        .build();
331
332    Ok(metadata)
333}
334
335fn get_file_decryptor(
336    encryption_algorithm: EncryptionAlgorithm,
337    footer_key_metadata: Option<&[u8]>,
338    file_decryption_properties: &Arc<FileDecryptionProperties>,
339) -> Result<FileDecryptor> {
340    match encryption_algorithm {
341        EncryptionAlgorithm::AES_GCM_V1(algo) => {
342            let aad_file_unique = algo
343                .aad_file_unique
344                .ok_or_else(|| general_err!("AAD unique file identifier is not set"))?;
345            let aad_prefix = if let Some(aad_prefix) = file_decryption_properties.aad_prefix() {
346                aad_prefix.clone()
347            } else {
348                algo.aad_prefix.unwrap_or_default()
349            };
350
351            FileDecryptor::new(
352                file_decryption_properties,
353                footer_key_metadata,
354                aad_file_unique,
355                aad_prefix,
356            )
357        }
358        EncryptionAlgorithm::AES_GCM_CTR_V1(_) => Err(nyi_err!(
359            "The AES_GCM_CTR_V1 encryption algorithm is not yet supported"
360        )),
361    }
362}