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::{
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 1: optional binary aad_prefix
46
47 2: optional binary aad_file_unique
49
50 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 1: optional binary aad_prefix
68
69 2: optional binary aad_file_unique
71
72 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!(
103pub(crate) struct FileCryptoMetaData<'a> {
105 1: required EncryptionAlgorithm encryption_algorithm
109
110 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 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 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 columns.push(c);
160 continue;
161 }
162 }
163 }
164 Some(ColumnCryptoMetaData::ENCRYPTION_WITH_FOOTER_KEY) => {
165 decryptor.get_footer_decryptor()?
166 }
167 };
168
169 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 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 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 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
235pub(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 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 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 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}