Skip to main content

simploxide_client/crypto/fs/
mod.rs

1use base64::{Engine as _, engine::general_purpose::URL_SAFE};
2use rand::Rng as _;
3use simploxide_api_types::CryptoFileArgs as SxcCryptoFileArgs;
4use zeroize::{Zeroize as _, ZeroizeOnDrop, Zeroizing};
5
6use crate::crypto::InvalidCryptoArgs;
7
8use super::{InvalidAuthTag, Poly1305Tag, SimplexSecretBox, XSalsa20Key, XSalsa20Nonce};
9
10pub mod std;
11pub mod tokio;
12
13#[cfg(feature = "native_crypto")]
14pub type StdEncryptedFile = std::EncryptedFile<super::native::SecretBox>;
15
16#[cfg(feature = "native_crypto")]
17pub type TokioEncryptedFile = tokio::EncryptedFile<super::native::SecretBox>;
18
19#[cfg(feature = "native_crypto")]
20pub type StdMaybeCryptoFile = std::StdMaybeCryptoFile<super::native::SecretBox>;
21
22#[cfg(feature = "native_crypto")]
23pub type TokioMaybeCryptoFile = tokio::TokioMaybeCryptoFile<super::native::SecretBox>;
24
25#[derive(ZeroizeOnDrop)]
26pub struct FileCryptoArgs {
27    key: XSalsa20Key,
28    nonce: XSalsa20Nonce,
29}
30
31impl FileCryptoArgs {
32    fn new(key: &XSalsa20Key, nonce: &XSalsa20Nonce) -> Self {
33        Self {
34            key: *key,
35            nonce: *nonce,
36        }
37    }
38
39    pub fn try_from_base64(mut key: String, mut nonce: String) -> Result<Self, InvalidCryptoArgs> {
40        fn try_decode(key_str: &str, nonce_str: &str) -> Result<FileCryptoArgs, InvalidCryptoArgs> {
41            let mut key = Zeroizing::new([0u8; ::std::mem::size_of::<XSalsa20Key>()]);
42            let mut nonce = Zeroizing::new([0u8; ::std::mem::size_of::<XSalsa20Nonce>()]);
43
44            decode_base64_arg(key_str, key.as_mut())?;
45            decode_base64_arg(nonce_str, nonce.as_mut())?;
46
47            Ok(FileCryptoArgs::new(&key, &nonce))
48        }
49
50        let result = try_decode(&key, &nonce);
51
52        key.zeroize();
53        nonce.zeroize();
54
55        result
56    }
57
58    pub fn expose(&self) -> SxcCryptoFileArgs {
59        SxcCryptoFileArgs {
60            file_key: URL_SAFE.encode(self.key),
61            file_nonce: URL_SAFE.encode(self.nonce),
62            undocumented: Default::default(),
63        }
64    }
65}
66
67impl TryFrom<SxcCryptoFileArgs> for FileCryptoArgs {
68    type Error = InvalidCryptoArgs;
69
70    fn try_from(args: SxcCryptoFileArgs) -> Result<Self, Self::Error> {
71        Self::try_from_base64(args.file_key, args.file_nonce)
72    }
73}
74
75struct EncryptedFileState<S> {
76    crypto_args: FileCryptoArgs,
77    secret_box: S,
78    buf: Zeroizing<Vec<u8>>,
79    mode: Mode,
80    remaining_data_len: usize,
81}
82
83impl<S> EncryptedFileState<S> {
84    const DEFAULT_BUFSIZE: usize = 65536;
85}
86
87impl<S: SimplexSecretBox> EncryptedFileState<S> {
88    fn new() -> Self {
89        let mut rng = rand::rng();
90
91        let mut key = [0u8; ::std::mem::size_of::<XSalsa20Key>()];
92        let mut nonce = [0u8; ::std::mem::size_of::<XSalsa20Nonce>()];
93
94        rng.fill_bytes(&mut key);
95        rng.fill_bytes(&mut nonce);
96
97        let crypto_args = FileCryptoArgs::new(&key, &nonce);
98        let secret_box = SimplexSecretBox::init(&key, &nonce);
99
100        key.zeroize();
101        nonce.zeroize();
102
103        Self {
104            crypto_args,
105            secret_box,
106            buf: Zeroizing::new(Vec::new()),
107            mode: Mode::Write,
108            remaining_data_len: 0,
109        }
110    }
111
112    fn from_args(crypto_args: FileCryptoArgs) -> Self {
113        let secret_box = SimplexSecretBox::init(&crypto_args.key, &crypto_args.nonce);
114
115        Self {
116            crypto_args,
117            secret_box,
118            buf: Zeroizing::new(Vec::new()),
119            mode: Mode::Write,
120            remaining_data_len: 0,
121        }
122    }
123
124    fn from_size_and_args(
125        file_size: usize,
126        crypto_args: FileCryptoArgs,
127    ) -> ::std::io::Result<Self> {
128        let mut state = Self::from_args(crypto_args);
129        if file_size < ::std::mem::size_of::<Poly1305Tag>() {
130            return Err(InvalidAuthTag::io_error());
131        } else if file_size == ::std::mem::size_of::<Poly1305Tag>() {
132            state.switch_to_auth_mode();
133        } else {
134            state.remaining_data_len = file_size - ::std::mem::size_of::<Poly1305Tag>();
135            state.mode = Mode::Read;
136        }
137
138        Ok(state)
139    }
140
141    fn crypto_args(&self) -> &FileCryptoArgs {
142        &self.crypto_args
143    }
144
145    fn reset(&mut self) {
146        let mut rng = rand::rng();
147        let mut key = [0u8; ::std::mem::size_of::<XSalsa20Key>()];
148        let mut nonce = [0u8; ::std::mem::size_of::<XSalsa20Nonce>()];
149
150        rng.fill_bytes(&mut key);
151        rng.fill_bytes(&mut nonce);
152
153        self.crypto_args = FileCryptoArgs::new(&key, &nonce);
154        self.secret_box = SimplexSecretBox::init(&key, &nonce);
155        self.remaining_data_len = 0;
156
157        key.zeroize();
158        nonce.zeroize();
159    }
160
161    fn encrypt_chunk(&mut self, chunk: &[u8]) -> &[u8] {
162        self.buf
163            .reserve_exact(::std::cmp::max(Self::DEFAULT_BUFSIZE, chunk.len()));
164        self.buf.resize(chunk.len(), 0);
165
166        self.secret_box.encrypt_chunk(chunk, &mut self.buf);
167        self.remaining_data_len += chunk.len();
168        &self.buf
169    }
170
171    fn encrypted_buf(&self) -> &[u8] {
172        let offset = self
173            .buf
174            .len()
175            .checked_sub(self.remaining_data_len)
176            .expect("encrypted_buf: no overflows");
177
178        &self.buf[offset..]
179    }
180
181    fn consume_encrypted_bytes(&mut self, bytes: usize) {
182        self.remaining_data_len = self
183            .remaining_data_len
184            .checked_sub(bytes)
185            .expect("consume_encrypted_bytes: no overflows");
186    }
187
188    fn is_encrypted_buf_consumed(&self) -> bool {
189        self.is_all_data_read()
190    }
191
192    fn prep_read_buf(&mut self, bytes: usize) -> &mut [u8] {
193        let corrected_bytes = ::std::cmp::min(bytes, self.remaining_data_len);
194        let buf_size = ::std::cmp::max(self.optimal_buf_size(), corrected_bytes);
195        self.buf.reserve_exact(buf_size);
196
197        self.buf.resize(corrected_bytes, 0);
198        &mut self.buf
199    }
200
201    fn decrypt_read_buf(&mut self, bytes_read: usize, out_chunk: &mut [u8]) {
202        self.remaining_data_len = self
203            .remaining_data_len
204            .checked_sub(bytes_read)
205            .expect("decrypt_read_buf: no overflows");
206
207        self.secret_box
208            .decrypt_chunk(&mut self.buf[..bytes_read], out_chunk);
209    }
210
211    fn is_all_data_read(&self) -> bool {
212        self.remaining_data_len == 0
213    }
214
215    fn optimal_buf_size(&self) -> usize {
216        if self.mode == Mode::Auth {
217            ::std::mem::size_of::<Poly1305Tag>()
218        } else if self.mode == Mode::Read && self.remaining_data_len < Self::DEFAULT_BUFSIZE {
219            self.remaining_data_len
220        } else {
221            Self::DEFAULT_BUFSIZE
222        }
223    }
224
225    fn plaintext_size_hint(&self) -> usize {
226        match self.mode {
227            Mode::Read => self.remaining_data_len,
228            Mode::Write => EncryptedFileState::<S>::DEFAULT_BUFSIZE,
229            Mode::Auth | Mode::Shutdown | Mode::AuthFailure => 0,
230        }
231    }
232
233    fn assert_writable(&self) -> ::std::io::Result<()> {
234        if self.mode == Mode::Write {
235            Ok(())
236        } else {
237            Err(::std::io::Error::other("Trying to write non-writable file"))
238        }
239    }
240
241    fn assert_readable(&self) -> ::std::io::Result<()> {
242        if self.mode == Mode::Read {
243            Ok(())
244        } else {
245            Err(::std::io::Error::other("Trying to read non-readable file"))
246        }
247    }
248
249    fn switch_to_auth_mode(&mut self) {
250        self.mode = Mode::Auth;
251        self.buf.resize(::std::mem::size_of::<Poly1305Tag>(), 0);
252        self.remaining_data_len = ::std::mem::size_of::<Poly1305Tag>();
253    }
254
255    fn write_auth_tag_in_buf(&mut self) {
256        self.switch_to_auth_mode();
257        let file_tag = self.secret_box.auth_tag();
258        self.buf.copy_from_slice(&file_tag);
259    }
260
261    fn auth_tag_buf(&mut self) -> &mut [u8] {
262        let offset = self
263            .buf
264            .len()
265            .checked_sub(self.remaining_data_len)
266            .expect("auth_tag_buf: no overflows");
267
268        self.buf[offset..].as_mut()
269    }
270
271    fn consume_auth_tag_bytes(&mut self, bytes: usize) {
272        self.consume_encrypted_bytes(bytes);
273    }
274
275    fn authenticate(&mut self) -> ::std::io::Result<()> {
276        let file_tag: &Poly1305Tag = self
277            .buf
278            .as_slice()
279            .try_into()
280            .map_err(|_| InvalidAuthTag::io_error())?;
281
282        let result = if self.secret_box.verify_tag(file_tag) {
283            Ok(())
284        } else {
285            self.mode = Mode::AuthFailure;
286            Err(InvalidAuthTag::io_error())
287        };
288
289        self.buf.truncate(0);
290        self.remaining_data_len = 0;
291
292        result
293    }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297enum Mode {
298    Read,
299    Write,
300    Auth,
301    Shutdown,
302    AuthFailure,
303}
304
305fn decode_base64_arg(b64str: &str, buf: &mut [u8]) -> Result<(), InvalidCryptoArgs> {
306    let len = URL_SAFE
307        .decode_slice(b64str, buf)
308        .map_err(|_| InvalidCryptoArgs)?;
309
310    if len == buf.len() {
311        Ok(())
312    } else {
313        Err(InvalidCryptoArgs)
314    }
315}