simploxide_client/crypto/fs/
mod.rs1use 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}