simploxide_client/crypto/fs/
tokio.rs1use 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
8pub 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 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 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 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
318pub enum TokioMaybeCryptoFile<S: SimplexSecretBox> {
320 Plain(::tokio::fs::File),
321 Encrypted(EncryptedFile<S>),
322}
323
324impl<S: SimplexSecretBox> TokioMaybeCryptoFile<S> {
325 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 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}