Skip to main content

simploxide_client/crypto/fs/
mod.rs

1//! Types that handle SimpleX client-side file encryption
2
3use 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}