Skip to main content

simploxide_client/crypto/fs/
tokio.rs

1use simploxide_api_types::CryptoFile as SxcCryptoFile;
2use tokio::io::{AsyncRead, AsyncSeekExt as _, AsyncWrite, AsyncWriteExt as _};
3
4use std::{io::SeekFrom, path::Path, pin::Pin, task::Poll};
5
6use super::{EncryptedFileState, FileCryptoArgs, InvalidAuthTag, Mode, SimplexSecretBox};
7
8/// Async wrapper over a file with SimpleX-SecretBox encryption.
9///
10/// # Security
11///
12/// - All bytes returned from `read()` are unauthenticated until the file is fully read. The caller
13///   must never act on streamed content until `read()` has returned `Ok(0)`. If reading a file
14///   returns Err() all previously read data cannot be trusted and must be discarded.
15///
16/// - The caller is responsible to call [`Self::put_auth_tag`] manually. The `AsyncWrite` implementation
17///   does its best to write the authentication tag but it can silently fail leaving the file
18///   unauthenticated.
19pub struct EncryptedFile<S: SimplexSecretBox> {
20    file: ::tokio::fs::File,
21    state: Box<EncryptedFileState<S>>,
22}
23
24impl<S: SimplexSecretBox> EncryptedFile<S> {
25    pub async fn create<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
26        let file = tokio::fs::File::create(path).await?;
27
28        Ok(Self {
29            file,
30            state: Box::new(EncryptedFileState::new()),
31        })
32    }
33
34    pub async fn create_with_args<P: AsRef<Path>>(
35        path: P,
36        crypto_args: FileCryptoArgs,
37    ) -> std::io::Result<Self> {
38        let file = tokio::fs::File::create(path).await?;
39
40        Ok(Self {
41            file,
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 async fn open<P: AsRef<Path>>(
50        path: P,
51        crypto_args: FileCryptoArgs,
52    ) -> std::io::Result<Self> {
53        let mut file = tokio::fs::OpenOptions::new()
54            .write(true)
55            .read(true)
56            .create(false)
57            .open(path)
58            .await?;
59
60        let size = size_hint(&mut file).await?;
61
62        Ok(Self {
63            file,
64            state: Box::new(EncryptedFileState::from_size_and_args(size, crypto_args)?),
65        })
66    }
67
68    /// Opens file in a read-only mode. [`Self::prepare_for_overwrite`] will return an IO error.
69    pub async fn open_read_only<P: AsRef<Path>>(
70        path: P,
71        crypto_args: FileCryptoArgs,
72    ) -> std::io::Result<Self> {
73        let mut file = tokio::fs::OpenOptions::new()
74            .write(false)
75            .read(true)
76            .create(false)
77            .open(path)
78            .await?;
79
80        let size = size_hint(&mut file).await?;
81
82        Ok(Self {
83            file,
84            state: Box::new(EncryptedFileState::from_size_and_args(size, crypto_args)?),
85        })
86    }
87
88    pub async fn prepare_for_overwrite(&mut self) -> std::io::Result<()> {
89        self.file.seek(SeekFrom::Start(0)).await?;
90        self.file.set_len(0).await?;
91        self.state.reset();
92        self.state.mode = Mode::Write;
93
94        Ok(())
95    }
96
97    pub fn crypto_args(&self) -> &FileCryptoArgs {
98        self.state.crypto_args()
99    }
100
101    pub fn optimal_buf_size(&self) -> usize {
102        self.state.optimal_buf_size()
103    }
104
105    pub fn plaintext_size_hint(&self) -> usize {
106        self.state.plaintext_size_hint()
107    }
108
109    /// Does nothing if auth tag was already written
110    pub async fn put_auth_tag(mut self) -> std::io::Result<()> {
111        if self.state.mode == Mode::Read {
112            return self.state.assert_writable();
113        } else if self.state.mode == Mode::Write {
114            self.state.mode = Mode::Auth;
115            let tag = self.state.secret_box.auth_tag();
116            self.file.write_all(&tag).await?;
117        } else if self.state.mode == Mode::AuthFailure {
118            return Err(InvalidAuthTag::io_error());
119        }
120
121        Ok(())
122    }
123}
124
125macro_rules! poll_throw {
126    ($e:expr) => {
127        match $e {
128            Ok(res) => res,
129            Err(err) => {
130                return ::std::task::Poll::Ready(Err(err));
131            }
132        }
133    };
134}
135
136impl<S: SimplexSecretBox> AsyncWrite for EncryptedFile<S> {
137    fn poll_write(
138        self: std::pin::Pin<&mut Self>,
139        cx: &mut std::task::Context<'_>,
140        buf: &[u8],
141    ) -> Poll<std::io::Result<usize>> {
142        let this = self.get_mut();
143        let mut file = Pin::new(&mut this.file);
144
145        poll_throw!(this.state.assert_writable());
146
147        if this.state.is_encrypted_buf_consumed() {
148            this.state.encrypt_chunk(buf);
149        }
150
151        while !this.state.is_encrypted_buf_consumed() {
152            let encrypted_buf = this.state.encrypted_buf();
153
154            match file.as_mut().poll_write(cx, encrypted_buf) {
155                Poll::Ready(res) => {
156                    let bytes_written = poll_throw!(res);
157
158                    if bytes_written == 0 {
159                        return Poll::Ready(Err(std::io::Error::new(
160                            std::io::ErrorKind::WriteZero,
161                            "underlying writer accepted 0 bytes",
162                        )));
163                    }
164
165                    this.state.consume_encrypted_bytes(bytes_written);
166                }
167                Poll::Pending => return Poll::Pending,
168            }
169        }
170
171        Poll::Ready(Ok(buf.len()))
172    }
173
174    fn poll_flush(
175        self: std::pin::Pin<&mut Self>,
176        cx: &mut std::task::Context<'_>,
177    ) -> Poll<std::io::Result<()>> {
178        let this = self.get_mut();
179        let file = Pin::new(&mut this.file);
180
181        file.poll_flush(cx)
182    }
183
184    fn poll_shutdown(
185        self: std::pin::Pin<&mut Self>,
186        cx: &mut std::task::Context<'_>,
187    ) -> Poll<std::io::Result<()>> {
188        let this = self.get_mut();
189        let mut file = Pin::new(&mut this.file);
190
191        if this.state.mode == Mode::AuthFailure {
192            return Poll::Ready(Err(InvalidAuthTag::io_error()));
193        }
194
195        if this.state.mode == Mode::Read {
196            return Poll::Ready(this.state.assert_writable());
197        }
198
199        if this.state.mode == Mode::Write {
200            this.state.write_auth_tag_in_buf();
201        }
202
203        if this.state.mode == Mode::Auth {
204            while !this.state.is_encrypted_buf_consumed() {
205                let encrypted_buf = this.state.encrypted_buf();
206
207                match file.as_mut().poll_write(cx, encrypted_buf) {
208                    Poll::Ready(res) => {
209                        let bytes_written = poll_throw!(res);
210
211                        if bytes_written == 0 {
212                            return Poll::Ready(Err(std::io::Error::new(
213                                std::io::ErrorKind::WriteZero,
214                                "underlying writer accepted 0 bytes",
215                            )));
216                        }
217
218                        this.state.consume_encrypted_bytes(bytes_written);
219                    }
220                    Poll::Pending => return Poll::Pending,
221                }
222            }
223
224            this.state.mode = Mode::Shutdown;
225        }
226
227        if this.state.mode == Mode::Shutdown {
228            return file.poll_shutdown(cx);
229        }
230
231        unreachable!()
232    }
233}
234
235impl<S: SimplexSecretBox> AsyncRead for EncryptedFile<S> {
236    fn poll_read(
237        self: std::pin::Pin<&mut Self>,
238        cx: &mut std::task::Context<'_>,
239        buf: &mut tokio::io::ReadBuf<'_>,
240    ) -> Poll<std::io::Result<()>> {
241        let this = self.get_mut();
242        let mut file = Pin::new(&mut this.file);
243
244        if this.state.mode == Mode::AuthFailure {
245            return Poll::Ready(Err(InvalidAuthTag::io_error()));
246        }
247
248        if this.state.mode == Mode::Auth {
249            if this.state.is_all_data_read() {
250                return Poll::Ready(Ok(()));
251            } else {
252                while !this.state.is_all_data_read() {
253                    let mut auth_buf = tokio::io::ReadBuf::new(this.state.auth_tag_buf());
254
255                    match file.as_mut().poll_read(cx, &mut auth_buf) {
256                        Poll::Ready(res) => {
257                            poll_throw!(res.map_err(|_| InvalidAuthTag::io_error()));
258
259                            let bytes_read = auth_buf.filled().len();
260                            if bytes_read == 0 {
261                                return Poll::Ready(Err(InvalidAuthTag::io_error()));
262                            }
263
264                            this.state.consume_auth_tag_bytes(bytes_read);
265                        }
266                        Poll::Pending => return Poll::Pending,
267                    }
268                }
269
270                poll_throw!(this.state.authenticate());
271                return Poll::Ready(Ok(()));
272            }
273        }
274
275        poll_throw!(this.state.assert_readable());
276
277        if buf.remaining() == 0 {
278            return Poll::Ready(Err(std::io::Error::new(
279                std::io::ErrorKind::InvalidInput,
280                "reader got exhausted before EOF: the data cannot be authenticated",
281            )));
282        }
283
284        let mut read_buf = tokio::io::ReadBuf::new(this.state.prep_read_buf(buf.remaining()));
285        match file.poll_read(cx, &mut read_buf) {
286            Poll::Ready(res) => {
287                poll_throw!(res);
288                let bytes_read = read_buf.filled().len();
289
290                let out = buf.initialize_unfilled_to(bytes_read);
291                this.state.decrypt_read_buf(bytes_read, out);
292                buf.advance(bytes_read);
293
294                if this.state.is_all_data_read() {
295                    this.state.switch_to_auth_mode();
296                } else if bytes_read == 0 {
297                    return Poll::Ready(Err(std::io::Error::new(
298                        std::io::ErrorKind::UnexpectedEof,
299                        "file truncated before ciphertext end",
300                    )));
301                }
302
303                Poll::Ready(Ok(()))
304            }
305            Poll::Pending => Poll::Pending,
306        }
307    }
308}
309
310impl<S: SimplexSecretBox> Drop for EncryptedFile<S> {
311    fn drop(&mut self) {
312        if self.state.mode == Mode::Write {
313            log::error!("The file was not authenticated after write");
314        }
315    }
316}
317
318/// An async file that is either plaintext or SimpleX-SecretBox encrypted.
319pub enum TokioMaybeCryptoFile<S: SimplexSecretBox> {
320    Plain(::tokio::fs::File),
321    Encrypted(EncryptedFile<S>),
322}
323
324impl<S: SimplexSecretBox> TokioMaybeCryptoFile<S> {
325    /// Opens the file read+write so that [`Self::prepare_for_overwrite`] works.
326    /// Use [`Self::open_read_only`] when write access is not needed or not available.
327    pub async fn open<P: AsRef<Path>>(
328        path: P,
329        crypto_args: Option<FileCryptoArgs>,
330    ) -> std::io::Result<Self> {
331        match crypto_args {
332            Some(args) => Ok(Self::Encrypted(EncryptedFile::open(path, args).await?)),
333            None => Ok(Self::Plain(
334                tokio::fs::OpenOptions::new()
335                    .write(true)
336                    .read(true)
337                    .create(false)
338                    .open(path)
339                    .await?,
340            )),
341        }
342    }
343
344    pub async fn open_read_only<P: AsRef<Path>>(
345        path: P,
346        crypto_args: Option<FileCryptoArgs>,
347    ) -> std::io::Result<Self> {
348        match crypto_args {
349            Some(args) => Ok(Self::Encrypted(
350                EncryptedFile::open_read_only(path, args).await?,
351            )),
352            None => Ok(Self::Plain(
353                tokio::fs::OpenOptions::new()
354                    .write(false)
355                    .read(true)
356                    .create(false)
357                    .open(path)
358                    .await?,
359            )),
360        }
361    }
362
363    pub async fn create<P: AsRef<Path>>(
364        path: P,
365        crypto_args: Option<FileCryptoArgs>,
366    ) -> std::io::Result<Self> {
367        match crypto_args {
368            Some(args) => Ok(Self::Encrypted(
369                EncryptedFile::create_with_args(path, args).await?,
370            )),
371            None => Ok(Self::Plain(
372                tokio::fs::OpenOptions::new()
373                    .write(true)
374                    .create(true)
375                    .truncate(true)
376                    .open(path)
377                    .await?,
378            )),
379        }
380    }
381
382    pub async fn from_crypto_file(crypto_file: SxcCryptoFile) -> std::io::Result<Self> {
383        match crypto_file.crypto_args {
384            Some(args) => {
385                let crypto_args = FileCryptoArgs::try_from(args)?;
386                Self::open(&crypto_file.file_path, Some(crypto_args)).await
387            }
388            None => Self::open(&crypto_file.file_path, None).await,
389        }
390    }
391
392    pub async fn from_crypto_file_read_only(crypto_file: SxcCryptoFile) -> std::io::Result<Self> {
393        match crypto_file.crypto_args {
394            Some(args) => {
395                let crypto_args = FileCryptoArgs::try_from(args)?;
396                Self::open_read_only(&crypto_file.file_path, Some(crypto_args)).await
397            }
398            None => Self::open_read_only(&crypto_file.file_path, None).await,
399        }
400    }
401
402    pub async fn size_hint(&mut self) -> std::io::Result<usize> {
403        match self {
404            Self::Plain(f) => size_hint(f).await,
405            Self::Encrypted(f) => Ok(f.plaintext_size_hint()),
406        }
407    }
408
409    pub fn crypto_args(&self) -> Option<&FileCryptoArgs> {
410        match self {
411            Self::Plain(_) => None,
412            Self::Encrypted(f) => Some(f.crypto_args()),
413        }
414    }
415
416    pub async fn prepare_for_overwrite(&mut self) -> std::io::Result<()> {
417        match self {
418            Self::Plain(f) => {
419                f.seek(SeekFrom::Start(0)).await?;
420                f.set_len(0).await?;
421                Ok(())
422            }
423            Self::Encrypted(f) => f.prepare_for_overwrite().await,
424        }
425    }
426
427    /// Writes the AEAD auth tag for encrypted files; no-op for plain files.
428    pub async fn put_auth_tag(self) -> std::io::Result<()> {
429        match self {
430            Self::Plain(_) => Ok(()),
431            Self::Encrypted(f) => f.put_auth_tag().await,
432        }
433    }
434}
435
436impl<S: SimplexSecretBox> AsyncRead for TokioMaybeCryptoFile<S> {
437    fn poll_read(
438        self: Pin<&mut Self>,
439        cx: &mut std::task::Context<'_>,
440        buf: &mut tokio::io::ReadBuf<'_>,
441    ) -> Poll<std::io::Result<()>> {
442        let this = self.get_mut();
443        match this {
444            Self::Plain(f) => Pin::new(f).poll_read(cx, buf),
445            Self::Encrypted(e) => Pin::new(e).poll_read(cx, buf),
446        }
447    }
448}
449
450impl<S: SimplexSecretBox> AsyncWrite for TokioMaybeCryptoFile<S> {
451    fn poll_write(
452        self: Pin<&mut Self>,
453        cx: &mut std::task::Context<'_>,
454        buf: &[u8],
455    ) -> Poll<std::io::Result<usize>> {
456        let this = self.get_mut();
457        match this {
458            Self::Plain(f) => Pin::new(f).poll_write(cx, buf),
459            Self::Encrypted(e) => Pin::new(e).poll_write(cx, buf),
460        }
461    }
462
463    fn poll_flush(
464        self: Pin<&mut Self>,
465        cx: &mut std::task::Context<'_>,
466    ) -> Poll<std::io::Result<()>> {
467        let this = self.get_mut();
468        match this {
469            Self::Plain(f) => Pin::new(f).poll_flush(cx),
470            Self::Encrypted(e) => Pin::new(e).poll_flush(cx),
471        }
472    }
473
474    fn poll_shutdown(
475        self: Pin<&mut Self>,
476        cx: &mut std::task::Context<'_>,
477    ) -> Poll<std::io::Result<()>> {
478        let this = self.get_mut();
479        match this {
480            Self::Plain(f) => Pin::new(f).poll_shutdown(cx),
481            Self::Encrypted(e) => Pin::new(e).poll_shutdown(cx),
482        }
483    }
484}
485
486async fn size_hint(file: &mut ::tokio::fs::File) -> ::std::io::Result<usize> {
487    let size = file.seek(SeekFrom::End(0)).await?;
488    file.seek(SeekFrom::Start(0)).await?;
489
490    crate::util::cast_file_size(size)
491}