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