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