Skip to main content

simploxide_client/crypto/fs/
tokio.rs

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