parquet/file/metadata/thrift/
encryption.rs1use 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::{parquet_metadata_from_bytes, read_column_metadata, validate_column_metadata},
28 },
29 },
30 parquet_thrift::{
31 ElementType, FieldType, ReadThrift, ThriftCompactInputProtocol,
32 ThriftCompactOutputProtocol, ThriftSliceInputProtocol, WriteThrift, WriteThriftField,
33 },
34 thrift_struct, thrift_union,
35};
36use std::io::Write;
37use std::sync::Arc;
38
39thrift_struct!(
40pub(crate) struct AesGcmV1 {
41 1: optional binary aad_prefix
43
44 2: optional binary aad_file_unique
46
47 3: optional bool supply_aad_prefix
50}
51);
52
53impl HeapSize for AesGcmV1 {
54 fn heap_size(&self) -> usize {
55 self.aad_prefix.heap_size()
56 + self.aad_file_unique.heap_size()
57 + self.supply_aad_prefix.heap_size()
58 }
59}
60
61thrift_struct!(
62pub(crate) struct AesGcmCtrV1 {
63 1: optional binary aad_prefix
65
66 2: optional binary aad_file_unique
68
69 3: optional bool supply_aad_prefix
72}
73);
74
75impl HeapSize for AesGcmCtrV1 {
76 fn heap_size(&self) -> usize {
77 self.aad_prefix.heap_size()
78 + self.aad_file_unique.heap_size()
79 + self.supply_aad_prefix.heap_size()
80 }
81}
82
83thrift_union!(
84union EncryptionAlgorithm {
85 1: (AesGcmV1) AES_GCM_V1
86 2: (AesGcmCtrV1) AES_GCM_CTR_V1
87}
88);
89
90impl HeapSize for EncryptionAlgorithm {
91 fn heap_size(&self) -> usize {
92 match self {
93 Self::AES_GCM_V1(gcm) => gcm.heap_size(),
94 Self::AES_GCM_CTR_V1(gcm_ctr) => gcm_ctr.heap_size(),
95 }
96 }
97}
98
99thrift_struct!(
100pub(crate) struct FileCryptoMetaData<'a> {
102 1: required EncryptionAlgorithm encryption_algorithm
106
107 2: optional binary<'a> key_metadata
110}
111);
112
113fn row_group_from_encrypted_thrift(
114 mut rg: RowGroupMetaData,
115 decryptor: Option<&FileDecryptor>,
116 options: Option<&ParquetMetaDataOptions>,
117) -> Result<RowGroupMetaData> {
118 let schema_descr = rg.schema_descr;
119
120 if schema_descr.num_columns() != rg.columns.len() {
121 return Err(general_err!(
122 "Column count mismatch. Schema has {} columns while Row Group has {}",
123 schema_descr.num_columns(),
124 rg.columns.len()
125 ));
126 }
127 let total_byte_size = rg.total_byte_size;
128 let num_rows = rg.num_rows;
129 let mut columns = vec![];
130
131 for (i, (mut c, d)) in rg
132 .columns
133 .drain(0..)
134 .zip(schema_descr.columns())
135 .enumerate()
136 {
137 if let (true, Some(decryptor)) = (c.encrypted_column_metadata.is_some(), decryptor) {
139 let column_decryptor = match c.crypto_metadata() {
140 None => {
141 return Err(general_err!(
142 "No crypto_metadata is set for column '{}', which has encrypted metadata",
143 d.path().string()
144 ));
145 }
146 Some(ColumnCryptoMetaData::ENCRYPTION_WITH_COLUMN_KEY(crypto_metadata)) => {
147 let column_name = crypto_metadata.path_in_schema.join(".");
148 match decryptor.get_column_metadata_decryptor(
150 column_name.as_str(),
151 crypto_metadata.key_metadata.as_deref(),
152 ) {
153 Ok(dec) => dec,
154 Err(_) => {
155 columns.push(c);
157 continue;
158 }
159 }
160 }
161 Some(ColumnCryptoMetaData::ENCRYPTION_WITH_FOOTER_KEY) => {
162 decryptor.get_footer_decryptor()?
163 }
164 };
165
166 let rg_ordinal = rg.ordinal.ok_or_else(|| {
171 general_err!(
172 "Row group ordinal is required to decrypt column metadata for \
173 column '{}', but the file's row-group ordinals are inconsistent",
174 d.path().string()
175 )
176 })?;
177 let rg_ordinal = usize::try_from(rg_ordinal).map_err(|_| {
180 general_err!(
181 "Row group ordinal {rg_ordinal} is invalid (must be non-negative) \
182 for decrypting column metadata for column '{}'",
183 d.path().string()
184 )
185 })?;
186 let column_aad = crate::encryption::modules::create_module_aad(
187 decryptor.file_aad(),
188 crate::encryption::modules::ModuleType::ColumnMetaData,
189 rg_ordinal,
190 i,
191 None,
192 )?;
193
194 let encrypted_column_metadata = c.encrypted_column_metadata.take();
196 let buf = encrypted_column_metadata.unwrap();
197 let decrypted_cc_buf = column_decryptor
198 .decrypt(&buf, column_aad.as_ref())
199 .map_err(|_| {
200 general_err!(
201 "Unable to decrypt column '{}', perhaps the column key is wrong?",
202 d.path().string()
203 )
204 })?;
205
206 let mut prot = ThriftSliceInputProtocol::new(&decrypted_cc_buf);
208 let mask = read_column_metadata(&mut prot, &mut c, i, options)?;
209 validate_column_metadata(mask)?;
210
211 columns.push(c);
212 } else {
213 columns.push(c);
214 }
215 }
216
217 let sorting_columns = rg.sorting_columns;
218 let file_offset = rg.file_offset;
219 let ordinal = rg.ordinal;
220
221 Ok(RowGroupMetaData {
222 columns,
223 num_rows,
224 sorting_columns,
225 total_byte_size,
226 schema_descr,
227 file_offset,
228 ordinal,
229 })
230}
231
232pub(crate) fn parquet_metadata_with_encryption(
242 file_decryption_properties: Option<&Arc<FileDecryptionProperties>>,
243 encrypted_footer: bool,
244 buf: &[u8],
245 options: Option<&ParquetMetaDataOptions>,
246) -> Result<ParquetMetaData> {
247 use crate::file::metadata::ParquetMetaDataBuilder;
248
249 let mut buf = buf;
250 let mut file_decryptor = None;
251 let decrypted_fmd_buf;
252
253 if encrypted_footer {
254 let mut prot = ThriftSliceInputProtocol::new(buf);
255 if let Some(file_decryption_properties) = file_decryption_properties {
256 let t_file_crypto_metadata: FileCryptoMetaData =
257 FileCryptoMetaData::read_thrift(&mut prot)
258 .map_err(|e| general_err!("Could not parse crypto metadata: {}", e))?;
259 let supply_aad_prefix = match &t_file_crypto_metadata.encryption_algorithm {
260 EncryptionAlgorithm::AES_GCM_V1(algo) => algo.supply_aad_prefix,
261 _ => Some(false),
262 }
263 .unwrap_or(false);
264 if supply_aad_prefix && file_decryption_properties.aad_prefix().is_none() {
265 return Err(general_err!(
266 "Parquet file was encrypted with an AAD prefix that is not stored in the file, \
267 but no AAD prefix was provided in the file decryption properties"
268 ));
269 }
270 let decryptor = get_file_decryptor(
271 t_file_crypto_metadata.encryption_algorithm,
272 t_file_crypto_metadata.key_metadata,
273 file_decryption_properties,
274 )?;
275 let footer_decryptor = decryptor.get_footer_decryptor();
276 let aad_footer = crate::encryption::modules::create_footer_aad(decryptor.file_aad())?;
277
278 decrypted_fmd_buf = footer_decryptor?
279 .decrypt(prot.as_slice().as_ref(), aad_footer.as_ref())
280 .map_err(|_| {
281 general_err!(
282 "Provided footer key and AAD were unable to decrypt parquet footer"
283 )
284 })?;
285
286 buf = &decrypted_fmd_buf;
287 file_decryptor = Some(decryptor);
288 } else {
289 return Err(general_err!(
290 "Parquet file has an encrypted footer but decryption properties were not provided"
291 ));
292 }
293 }
294
295 let parquet_meta = parquet_metadata_from_bytes(buf, options)
296 .map_err(|e| general_err!("Could not parse metadata: {}", e))?;
297
298 let ParquetMetaData {
299 mut file_metadata,
300 row_groups,
301 column_index: _,
302 offset_index: _,
303 file_decryptor: _,
304 } = parquet_meta;
305
306 if let (Some(algo), Some(file_decryption_properties)) = (
309 file_metadata.encryption_algorithm.take(),
310 file_decryption_properties,
311 ) {
312 let footer_signing_key_metadata = file_metadata.footer_signing_key_metadata.take();
313
314 let file_decryptor_value = get_file_decryptor(
316 *algo,
317 footer_signing_key_metadata.as_deref(),
318 file_decryption_properties,
319 )?;
320 if file_decryption_properties.check_plaintext_footer_integrity() && !encrypted_footer {
321 file_decryptor_value.verify_plaintext_footer_signature(buf)?;
322 }
323 file_decryptor = Some(file_decryptor_value);
324 }
325
326 let row_groups = row_groups
328 .into_iter()
329 .map(|rg| row_group_from_encrypted_thrift(rg, file_decryptor.as_ref(), options))
330 .collect::<Result<Vec<_>>>()?;
331
332 let metadata = ParquetMetaDataBuilder::new(file_metadata)
333 .set_row_groups(row_groups)
334 .set_file_decryptor(file_decryptor)
335 .build();
336
337 Ok(metadata)
338}
339
340fn get_file_decryptor(
341 encryption_algorithm: EncryptionAlgorithm,
342 footer_key_metadata: Option<&[u8]>,
343 file_decryption_properties: &Arc<FileDecryptionProperties>,
344) -> Result<FileDecryptor> {
345 match encryption_algorithm {
346 EncryptionAlgorithm::AES_GCM_V1(algo) => {
347 let aad_file_unique = algo
348 .aad_file_unique
349 .ok_or_else(|| general_err!("AAD unique file identifier is not set"))?;
350 let aad_prefix = if let Some(aad_prefix) = file_decryption_properties.aad_prefix() {
351 aad_prefix.clone()
352 } else {
353 algo.aad_prefix.map(|v| v.to_vec()).unwrap_or_default()
354 };
355 let aad_file_unique = aad_file_unique.to_vec();
356
357 FileDecryptor::new(
358 file_decryption_properties,
359 footer_key_metadata,
360 aad_file_unique,
361 aad_prefix,
362 )
363 }
364 EncryptionAlgorithm::AES_GCM_CTR_V1(_) => Err(nyi_err!(
365 "The AES_GCM_CTR_V1 encryption algorithm is not yet supported"
366 )),
367 }
368}