Skip to main content

polars_io/parquet/
encryption.rs

1//! Configuration for reading and writing files encrypted with Parquet modular encryption.
2
3use std::fmt::Debug;
4use std::hash::Hash;
5use std::sync::Arc;
6
7use polars_error::PolarsResult;
8use polars_parquet::parquet::encryption::decrypt::FileDecryptionProperties;
9use polars_parquet::parquet::encryption::encrypt::{
10    FileEncryptionProperties, validate_encrypted_column_names,
11};
12use polars_parquet::parquet::metadata::SchemaDescriptor;
13
14/// Creates the properties used to encrypt each Parquet file written.
15pub trait EncryptionPropertiesFactory: Send + Sync {
16    /// Create encryption properties for a new file.
17    fn file_encryption_properties(&self) -> PolarsResult<Arc<FileEncryptionProperties>>;
18
19    /// The dot-separated paths of the columns that are encrypted with their own key,
20    /// or none for uniform encryption.
21    ///
22    /// This is used to validate the columns before any file is written.
23    fn encrypted_column_names(&self) -> Vec<String>;
24}
25
26/// Creates the properties used to decrypt each Parquet file read.
27pub trait DecryptionPropertiesFactory: Send + Sync {
28    /// Create decryption properties for a file.
29    fn file_decryption_properties(&self) -> PolarsResult<Arc<FileDecryptionProperties>>;
30}
31
32/// File encryption properties can be specified directly and used as a factory.
33impl EncryptionPropertiesFactory for Arc<FileEncryptionProperties> {
34    fn file_encryption_properties(&self) -> PolarsResult<Arc<FileEncryptionProperties>> {
35        Ok(Arc::clone(self))
36    }
37
38    fn encrypted_column_names(&self) -> Vec<String> {
39        FileEncryptionProperties::encrypted_column_names(self)
40            .map(str::to_string)
41            .collect()
42    }
43}
44
45/// File decryption properties can be specified directly and used as a factory.
46impl DecryptionPropertiesFactory for Arc<FileDecryptionProperties> {
47    fn file_decryption_properties(&self) -> PolarsResult<Arc<FileDecryptionProperties>> {
48        Ok(Arc::clone(self))
49    }
50}
51
52/// Determines how to encrypt Parquet files when writing.
53///
54/// This may hold secret keys, so can't be serialized, and is compared and hashed
55/// by pointer.
56#[derive(Clone)]
57pub struct ParquetEncryption(Arc<dyn EncryptionPropertiesFactory>);
58
59impl ParquetEncryption {
60    /// Create properties for each file using a factory.
61    pub fn new(factory: impl EncryptionPropertiesFactory + 'static) -> Self {
62        Self(Arc::new(factory))
63    }
64
65    /// Get the properties to use for encrypting a single file.
66    pub fn file_properties(&self) -> PolarsResult<Arc<FileEncryptionProperties>> {
67        self.0.file_encryption_properties()
68    }
69
70    /// The dot-separated paths of the columns that are encrypted with their own key,
71    /// or none for uniform encryption.
72    pub fn encrypted_column_names(&self) -> Vec<String> {
73        self.0.encrypted_column_names()
74    }
75
76    /// Checks that each column that is to be encrypted with its own key is present in
77    /// the schema exactly once.
78    pub fn validate_encrypted_column_names(&self, schema: &SchemaDescriptor) -> PolarsResult<()> {
79        let encrypted_columns = self.encrypted_column_names();
80        validate_encrypted_column_names(encrypted_columns.iter().map(String::as_str), schema)?;
81        Ok(())
82    }
83}
84
85impl From<Arc<FileEncryptionProperties>> for ParquetEncryption {
86    fn from(properties: Arc<FileEncryptionProperties>) -> Self {
87        Self::new(properties)
88    }
89}
90
91/// Determines how to decrypt Parquet files when reading.
92///
93/// This may hold secret keys, so can't be serialized, and is compared and hashed
94/// by pointer.
95// The factory is double-boxed to make this a thin pointer, keeping `FileScanIR` small.
96#[derive(Clone)]
97pub struct ParquetDecryption(Arc<Box<dyn DecryptionPropertiesFactory>>);
98
99impl ParquetDecryption {
100    /// Create properties for each file using a factory.
101    pub fn new(factory: impl DecryptionPropertiesFactory + 'static) -> Self {
102        Self(Arc::new(Box::new(factory)))
103    }
104
105    /// Get the properties to use for decrypting a single file.
106    pub fn file_properties(&self) -> PolarsResult<Arc<FileDecryptionProperties>> {
107        self.0.file_decryption_properties()
108    }
109}
110
111impl From<Arc<FileDecryptionProperties>> for ParquetDecryption {
112    fn from(properties: Arc<FileDecryptionProperties>) -> Self {
113        Self::new(properties)
114    }
115}
116
117/// Get the properties to use for decrypting a single file, if decryption is configured.
118pub fn file_decryption_properties(
119    decryption: Option<&ParquetDecryption>,
120) -> PolarsResult<Option<Arc<FileDecryptionProperties>>> {
121    decryption
122        .map(ParquetDecryption::file_properties)
123        .transpose()
124}
125
126/// Implement traits that compare and hash by pointer, and refuse to serialize.
127macro_rules! impl_opaque_traits {
128    ($t:ident, $description:literal) => {
129        impl Debug for $t {
130            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131                f.write_str(stringify!($t))
132            }
133        }
134
135        impl Eq for $t {}
136
137        impl PartialEq for $t {
138            fn eq(&self, other: &Self) -> bool {
139                Arc::ptr_eq(&self.0, &other.0)
140            }
141        }
142
143        impl Hash for $t {
144            fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
145                state.write_usize(Arc::as_ptr(&self.0).cast::<()>() as usize)
146            }
147        }
148
149        #[cfg(feature = "serde")]
150        impl<'de> serde::Deserialize<'de> for $t {
151            fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
152            where
153                D: serde::Deserializer<'de>,
154            {
155                use serde::de::Error;
156                Err(D::Error::custom(concat!(
157                    "cannot deserialize ",
158                    $description
159                )))
160            }
161        }
162
163        #[cfg(feature = "serde")]
164        impl serde::Serialize for $t {
165            fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
166            where
167                S: serde::Serializer,
168            {
169                use serde::ser::Error;
170                Err(S::Error::custom(concat!("cannot serialize ", $description)))
171            }
172        }
173
174        #[cfg(feature = "dsl-schema")]
175        impl schemars::JsonSchema for $t {
176            fn schema_name() -> std::borrow::Cow<'static, str> {
177                stringify!($t).into()
178            }
179
180            fn schema_id() -> std::borrow::Cow<'static, str> {
181                std::borrow::Cow::Borrowed(concat!(module_path!(), "::", stringify!($t)))
182            }
183
184            fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
185                Vec::<u8>::json_schema(generator)
186            }
187        }
188    };
189}
190
191impl_opaque_traits!(ParquetEncryption, "parquet encryption properties");
192impl_opaque_traits!(ParquetDecryption, "parquet decryption properties");
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    struct Factory;
199
200    impl DecryptionPropertiesFactory for Factory {
201        fn file_decryption_properties(&self) -> PolarsResult<Arc<FileDecryptionProperties>> {
202            Ok(FileDecryptionProperties::builder(b"0123456789012345".to_vec()).build()?)
203        }
204    }
205
206    #[test]
207    fn test_pointer_equality() {
208        let properties = FileDecryptionProperties::builder(b"0123456789012345".to_vec())
209            .build()
210            .unwrap();
211        let a = ParquetDecryption::from(Arc::clone(&properties));
212        assert_eq!(a, a.clone());
213        assert_ne!(a, ParquetDecryption::from(properties));
214
215        let b = ParquetDecryption::new(Factory);
216        assert_eq!(b, b.clone());
217        assert_ne!(b, ParquetDecryption::new(Factory));
218    }
219
220    #[test]
221    fn test_file_properties() {
222        let decryption = ParquetDecryption::new(Factory);
223        let a = decryption.file_properties().unwrap();
224        let b = decryption.file_properties().unwrap();
225        assert!(!Arc::ptr_eq(&a, &b));
226
227        let decryption = ParquetDecryption::from(Arc::clone(&a));
228        assert!(Arc::ptr_eq(&decryption.file_properties().unwrap(), &a));
229    }
230}