iceberg/encryption/
io.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//! Encrypted file wrappers for InputFile / OutputFile.
19
20use std::sync::Arc;
21
22use bytes::Bytes;
23
24use super::crypto::{AesGcmCipher, SecureKey};
25use super::key_metadata::StandardKeyMetadata;
26use super::stream::{AesGcmFileRead, AesGcmFileWrite};
27use crate::Result;
28use crate::io::{FileMetadata, FileRead, FileWrite, InputFile, OutputFile};
29
30/// An AGS1 stream-encrypted input file wrapping a plain [`InputFile`].
31///
32/// Transparently decrypts on read.
33pub struct EncryptedInputFile {
34    inner: InputFile,
35    key_metadata: StandardKeyMetadata,
36}
37
38impl EncryptedInputFile {
39    /// Creates a new encrypted input file.
40    pub fn new(inner: InputFile, key_metadata: StandardKeyMetadata) -> Self {
41        Self {
42            inner,
43            key_metadata,
44        }
45    }
46
47    /// Absolute path of the file.
48    pub fn location(&self) -> &str {
49        self.inner.location()
50    }
51
52    /// Check if file exists.
53    pub async fn exists(&self) -> Result<bool> {
54        self.inner.exists().await
55    }
56
57    /// Fetch and returns metadata of file.
58    ///
59    /// The returned size is the **plaintext** size.
60    pub async fn metadata(&self) -> Result<FileMetadata> {
61        let raw_meta = self.inner.metadata().await?;
62        let plaintext_size = AesGcmFileRead::calculate_plaintext_length(raw_meta.size)?;
63        Ok(FileMetadata {
64            size: plaintext_size,
65        })
66    }
67
68    /// Read and returns whole content of file (decrypted plaintext).
69    pub async fn read(&self) -> Result<Bytes> {
70        let meta = self.metadata().await?;
71        let reader = self.reader().await?;
72        reader.read(0..meta.size).await
73    }
74
75    /// Creates a reader that transparently decrypts on each read.
76    pub async fn reader(&self) -> Result<Box<dyn FileRead>> {
77        let raw_meta = self.inner.metadata().await?;
78        let raw_reader = self.inner.reader().await?;
79        let cipher = build_cipher(&self.key_metadata)?;
80        let aad_prefix: Box<[u8]> = self.key_metadata.aad_prefix().unwrap_or_default().into();
81        let decrypting = AesGcmFileRead::new(raw_reader, cipher, aad_prefix, raw_meta.size)?;
82        Ok(Box::new(decrypting))
83    }
84
85    /// Returns a reference to the file's key metadata.
86    pub fn key_metadata(&self) -> &StandardKeyMetadata {
87        &self.key_metadata
88    }
89
90    /// Consumes self and returns the underlying plain input file.
91    pub fn into_inner(self) -> InputFile {
92        self.inner
93    }
94}
95
96impl std::fmt::Debug for EncryptedInputFile {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.debug_struct("EncryptedInputFile")
99            .field("path", &self.inner.location())
100            .finish_non_exhaustive()
101    }
102}
103
104/// An AGS1 stream-encrypted output file wrapping a plain [`OutputFile`].
105///
106/// Transparently encrypts on write.
107pub struct EncryptedOutputFile {
108    inner: OutputFile,
109    key_metadata: StandardKeyMetadata,
110}
111
112impl EncryptedOutputFile {
113    /// Creates a new encrypted output file.
114    pub fn new(inner: OutputFile, key_metadata: StandardKeyMetadata) -> Self {
115        Self {
116            inner,
117            key_metadata,
118        }
119    }
120
121    /// Returns a reference to the file's key metadata.
122    pub fn key_metadata(&self) -> &StandardKeyMetadata {
123        &self.key_metadata
124    }
125
126    /// Absolute path of the file.
127    pub fn location(&self) -> &str {
128        self.inner.location()
129    }
130
131    /// Creates a writer that transparently encrypts on each write.
132    pub async fn writer(&self) -> Result<Box<dyn FileWrite>> {
133        let raw_writer = self.inner.writer().await?;
134        let cipher = build_cipher(&self.key_metadata)?;
135        let aad_prefix: Box<[u8]> = self.key_metadata.aad_prefix().unwrap_or_default().into();
136        Ok(Box::new(AesGcmFileWrite::new(
137            raw_writer, cipher, aad_prefix,
138        )))
139    }
140
141    /// Write bytes to file (transparently encrypted).
142    pub async fn write(&self, bs: Bytes) -> Result<()> {
143        let mut writer = self.writer().await?;
144        writer.write(bs).await?;
145        writer.close().await
146    }
147
148    /// Deletes the underlying file.
149    pub async fn delete(&self) -> Result<()> {
150        self.inner.delete().await
151    }
152
153    /// Consumes self and returns the underlying plain output file.
154    pub fn into_inner(self) -> OutputFile {
155        self.inner
156    }
157}
158
159impl std::fmt::Debug for EncryptedOutputFile {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        f.debug_struct("EncryptedOutputFile")
162            .field("path", &self.inner.location())
163            .finish_non_exhaustive()
164    }
165}
166
167fn build_cipher(metadata: &StandardKeyMetadata) -> Result<Arc<AesGcmCipher>> {
168    let key = SecureKey::new(metadata.encryption_key().as_bytes())?;
169    Ok(Arc::new(AesGcmCipher::new(key)))
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::io::FileIO;
176
177    fn key_metadata() -> StandardKeyMetadata {
178        StandardKeyMetadata::new(b"0123456789abcdef").with_aad_prefix(b"test-aad-prefix!")
179    }
180
181    #[tokio::test]
182    async fn test_write_read_roundtrip() {
183        let fileio = FileIO::new_with_memory();
184        let path = "memory:///test/io_roundtrip.bin";
185        let plaintext = b"Hello from EncryptedInputFile/EncryptedOutputFile!";
186
187        let output = EncryptedOutputFile::new(fileio.new_output(path).unwrap(), key_metadata());
188        output.write(Bytes::from(plaintext.to_vec())).await.unwrap();
189
190        let input = EncryptedInputFile::new(fileio.new_input(path).unwrap(), key_metadata());
191        let content = input.read().await.unwrap();
192        assert_eq!(&content[..], plaintext);
193    }
194
195    #[tokio::test]
196    async fn test_metadata_returns_plaintext_size() {
197        let fileio = FileIO::new_with_memory();
198        let path = "memory:///test/io_metadata.bin";
199        let plaintext = b"some bytes to measure";
200
201        let output = EncryptedOutputFile::new(fileio.new_output(path).unwrap(), key_metadata());
202        output.write(Bytes::from(plaintext.to_vec())).await.unwrap();
203
204        let raw_size = fileio
205            .new_input(path)
206            .unwrap()
207            .metadata()
208            .await
209            .unwrap()
210            .size;
211        assert!(
212            raw_size > plaintext.len() as u64,
213            "encrypted file should be larger than plaintext (header + nonce + tag)"
214        );
215
216        let input = EncryptedInputFile::new(fileio.new_input(path).unwrap(), key_metadata());
217        let meta = input.metadata().await.unwrap();
218        assert_eq!(meta.size, plaintext.len() as u64);
219    }
220}