Skip to main content

simploxide_client/crypto/fs/
std.rs

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