Skip to main content

simploxide_client/crypto/fs/
std.rs

1use simploxide_api_types::CryptoFile as SxcCryptoFile;
2
3use std::{
4    io::{Read, Seek as _, SeekFrom, Write},
5    path::Path,
6};
7
8use super::{EncryptedFileState, FileCryptoArgs, InvalidAuthTag, Mode, SimplexSecretBox};
9
10/// Sync wrapper over a file with SimpleX-SecretBox encryption.
11///
12/// # Security
13///
14/// - All bytes returned from `read()` are unauthenticated until the file is fully read. The caller
15///   must never act on streamed content until `read()` has returned `Ok(0)`. If reading a file
16///   returns Err() all previously read data cannot be trusted and must be discarded.
17///
18/// - The caller is responsible to call [`Self::put_auth_tag`] manually. The `Drop` implementation does
19///   its best to write the authentication tag but it can silently fail leaving the file
20///   unauthenticated.
21pub struct EncryptedFile<S: SimplexSecretBox> {
22    file: ::std::fs::File,
23    state: Box<EncryptedFileState<S>>,
24}
25
26impl<S: SimplexSecretBox> EncryptedFile<S> {
27    pub fn create<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
28        Ok(Self {
29            file: std::fs::File::create(path)?,
30            state: Box::new(EncryptedFileState::new()),
31        })
32    }
33
34    pub fn create_with_args<P: AsRef<Path>>(
35        path: P,
36        crypto_args: FileCryptoArgs,
37    ) -> std::io::Result<Self> {
38        Ok(Self {
39            file: std::fs::File::create(path)?,
40            state: Box::new(EncryptedFileState::from_args(crypto_args)),
41        })
42    }
43
44    /// Note: this call requires write permissions on the file system for
45    /// [`Self::prepare_for_overwrite`] to work. Use [`Self::open_read_only`] when write access is
46    /// not needed or not available.
47    pub fn open<P: AsRef<Path>>(path: P, crypto_args: FileCryptoArgs) -> std::io::Result<Self> {
48        let mut file = std::fs::OpenOptions::new()
49            .write(true)
50            .read(true)
51            .create(false)
52            .open(path)?;
53
54        let size = size_hint(&mut file)?;
55
56        Ok(Self {
57            file,
58            state: Box::new(EncryptedFileState::from_size_and_args(size, crypto_args)?),
59        })
60    }
61
62    /// Opens file in a read-only mode. [`Self::prepare_for_overwrite`] will return an IO error.
63    pub fn open_read_only<P: AsRef<Path>>(
64        path: P,
65        crypto_args: FileCryptoArgs,
66    ) -> std::io::Result<Self> {
67        let mut file = std::fs::OpenOptions::new()
68            .write(false)
69            .read(true)
70            .create(false)
71            .open(path)?;
72
73        let size = size_hint(&mut file)?;
74
75        Ok(Self {
76            file,
77            state: Box::new(EncryptedFileState::from_size_and_args(size, crypto_args)?),
78        })
79    }
80
81    pub fn prepare_for_overwrite(&mut self) -> std::io::Result<()> {
82        self.file.seek(SeekFrom::Start(0))?;
83        self.file.set_len(0)?;
84        self.state.reset();
85        self.state.mode = Mode::Write;
86
87        Ok(())
88    }
89
90    pub fn crypto_args(&self) -> &FileCryptoArgs {
91        self.state.crypto_args()
92    }
93
94    pub fn optimal_buf_size(&self) -> usize {
95        self.state.optimal_buf_size()
96    }
97
98    pub fn plaintext_size_hint(&self) -> usize {
99        self.state.plaintext_size_hint()
100    }
101
102    /// Does nothing if auth tag was already written
103    pub fn put_auth_tag(mut self) -> std::io::Result<()> {
104        if self.state.mode == Mode::Read {
105            return self.state.assert_writable();
106        } else if self.state.mode == Mode::Write {
107            self.state.mode = Mode::Auth;
108            let tag = self.state.secret_box.auth_tag();
109            self.file.write_all(&tag)?;
110        } else if self.state.mode == Mode::AuthFailure {
111            return Err(InvalidAuthTag::io_error());
112        }
113
114        Ok(())
115    }
116}
117
118impl<S: SimplexSecretBox> Write for EncryptedFile<S> {
119    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
120        self.state.assert_writable()?;
121        let encrypted = self.state.encrypt_chunk(buf);
122        self.file.write_all(encrypted)?;
123        Ok(buf.len())
124    }
125
126    fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
127        self.write(buf).map(drop)
128    }
129
130    fn flush(&mut self) -> std::io::Result<()> {
131        self.file.flush()
132    }
133}
134
135impl<S: SimplexSecretBox> Read for EncryptedFile<S> {
136    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
137        if self.state.mode == Mode::AuthFailure {
138            return Err(InvalidAuthTag::io_error());
139        }
140
141        if self.state.mode == Mode::Auth {
142            if self.state.is_all_data_read() {
143                return Ok(0);
144            } else {
145                self.file
146                    .read_exact(self.state.auth_tag_buf())
147                    .map_err(|_| InvalidAuthTag::io_error())?;
148                self.state.authenticate()?;
149                return Ok(0);
150            }
151        }
152
153        self.state.assert_readable()?;
154
155        if buf.is_empty() {
156            return Err(std::io::Error::new(
157                std::io::ErrorKind::InvalidInput,
158                "reader got exhausted before EOF: the data cannot be authenticated",
159            ));
160        }
161
162        let read_buf = self.state.prep_read_buf(buf.len());
163        let bytes_read = self.file.read(read_buf)?;
164
165        self.state.decrypt_read_buf(bytes_read, buf);
166
167        if self.state.is_all_data_read() {
168            self.state.switch_to_auth_mode();
169        } else if bytes_read == 0 {
170            return Err(std::io::Error::new(
171                std::io::ErrorKind::UnexpectedEof,
172                "file truncated before ciphertext end",
173            ));
174        }
175
176        Ok(bytes_read)
177    }
178}
179
180impl<S: SimplexSecretBox> Drop for EncryptedFile<S> {
181    fn drop(&mut self) {
182        if self.state.mode == Mode::Write {
183            let tag = self.state.secret_box.auth_tag();
184            if let Err(e) = self.file.write_all(&tag) {
185                log::error!("Failed to authenticate a file: {e}");
186            }
187        }
188    }
189}
190
191/// A sync file that is either plaintext or SimpleX-SecretBox encrypted.
192pub enum StdMaybeCryptoFile<S: SimplexSecretBox> {
193    Plain(::std::fs::File),
194    Encrypted(EncryptedFile<S>),
195}
196
197impl<S: SimplexSecretBox> StdMaybeCryptoFile<S> {
198    /// Opens the file read+write so that [`Self::prepare_for_overwrite`] works.
199    /// Use [`Self::open_read_only`] when write access is not needed or not available.
200    pub fn open<P: AsRef<Path>>(
201        path: P,
202        crypto_args: Option<FileCryptoArgs>,
203    ) -> std::io::Result<Self> {
204        match crypto_args {
205            Some(args) => Ok(Self::Encrypted(EncryptedFile::open(path, args)?)),
206            None => Ok(Self::Plain(
207                std::fs::OpenOptions::new()
208                    .write(true)
209                    .read(true)
210                    .create(false)
211                    .open(path)?,
212            )),
213        }
214    }
215
216    pub fn open_read_only<P: AsRef<Path>>(
217        path: P,
218        crypto_args: Option<FileCryptoArgs>,
219    ) -> std::io::Result<Self> {
220        match crypto_args {
221            Some(args) => Ok(Self::Encrypted(EncryptedFile::open_read_only(path, args)?)),
222            None => Ok(Self::Plain(
223                std::fs::OpenOptions::new()
224                    .write(false)
225                    .read(true)
226                    .create(false)
227                    .open(path)?,
228            )),
229        }
230    }
231
232    pub fn create<P: AsRef<Path>>(
233        path: P,
234        crypto_args: Option<FileCryptoArgs>,
235    ) -> std::io::Result<Self> {
236        match crypto_args {
237            Some(args) => Ok(Self::Encrypted(EncryptedFile::create_with_args(
238                path, args,
239            )?)),
240            None => Ok(Self::Plain(
241                std::fs::OpenOptions::new()
242                    .write(true)
243                    .create(true)
244                    .truncate(true)
245                    .open(path)?,
246            )),
247        }
248    }
249
250    pub fn from_crypto_file(crypto_file: SxcCryptoFile) -> std::io::Result<Self> {
251        match crypto_file.crypto_args {
252            Some(args) => {
253                let crypto_args = FileCryptoArgs::try_from(args)?;
254                Self::open(&crypto_file.file_path, Some(crypto_args))
255            }
256            None => Self::open(&crypto_file.file_path, None),
257        }
258    }
259
260    pub fn from_crypto_file_read_only(crypto_file: SxcCryptoFile) -> std::io::Result<Self> {
261        match crypto_file.crypto_args {
262            Some(args) => {
263                let crypto_args = FileCryptoArgs::try_from(args)?;
264                Self::open_read_only(&crypto_file.file_path, Some(crypto_args))
265            }
266            None => Self::open_read_only(&crypto_file.file_path, None),
267        }
268    }
269
270    pub fn size_hint(&mut self) -> std::io::Result<usize> {
271        match self {
272            Self::Plain(f) => size_hint(f),
273            Self::Encrypted(f) => Ok(f.plaintext_size_hint()),
274        }
275    }
276
277    pub fn crypto_args(&self) -> Option<&FileCryptoArgs> {
278        match self {
279            Self::Plain(_) => None,
280            Self::Encrypted(f) => Some(f.crypto_args()),
281        }
282    }
283
284    pub fn prepare_for_overwrite(&mut self) -> std::io::Result<()> {
285        match self {
286            Self::Plain(f) => {
287                f.seek(SeekFrom::Start(0))?;
288                f.set_len(0)?;
289                Ok(())
290            }
291            Self::Encrypted(f) => f.prepare_for_overwrite(),
292        }
293    }
294
295    /// Writes the AEAD auth tag for encrypted files; no-op for plain files.
296    pub fn put_auth_tag(self) -> std::io::Result<()> {
297        match self {
298            Self::Plain(_) => Ok(()),
299            Self::Encrypted(f) => f.put_auth_tag(),
300        }
301    }
302}
303
304impl<S: SimplexSecretBox> Read for StdMaybeCryptoFile<S> {
305    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
306        match self {
307            Self::Plain(f) => f.read(buf),
308            Self::Encrypted(e) => e.read(buf),
309        }
310    }
311}
312
313impl<S: SimplexSecretBox> Write for StdMaybeCryptoFile<S> {
314    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
315        match self {
316            Self::Plain(f) => f.write(buf),
317            Self::Encrypted(e) => e.write(buf),
318        }
319    }
320
321    fn flush(&mut self) -> std::io::Result<()> {
322        match self {
323            Self::Plain(f) => f.flush(),
324            Self::Encrypted(e) => e.flush(),
325        }
326    }
327
328    fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
329        match self {
330            Self::Plain(f) => f.write_all(buf),
331            Self::Encrypted(e) => e.write_all(buf),
332        }
333    }
334}
335
336fn size_hint(file: &mut ::std::fs::File) -> ::std::io::Result<usize> {
337    let size = file.seek(SeekFrom::End(0))?;
338    file.seek(SeekFrom::Start(0))?;
339
340    crate::util::cast_file_size(size)
341}