Skip to main content

simploxide_client/crypto/
native.rs

1//! Native Rust implementation of SimpleX crypto
2
3use poly1305::{
4    Block as MacBlock, Poly1305,
5    universal_hash::{KeyInit, UniversalHash},
6};
7use salsa20::{
8    XSalsa20,
9    cipher::{Array, KeyIvInit, StreamCipher, typenum::U10},
10    hsalsa,
11};
12use subtle::ConstantTimeEq as _;
13use zeroize::{Zeroize as _, Zeroizing};
14
15use super::{Poly1305Tag, SimplexSecretBox, XSalsa20Key, XSalsa20Nonce};
16
17pub struct SecretBox {
18    cipher: XSalsa20,
19    mac: Poly1305,
20    // Partial bytes of Poly1305 block
21    mac_tail: Zeroizing<[u8; 16]>,
22    mac_tail_len: usize,
23}
24
25impl SecretBox {
26    /// `poly1305::update` requires fully-filled 16-byte blocks. This helper buffers incomplete
27    /// blocks and feeds only complete 16-byte blocks to `update`
28    fn update_mac(&mut self, data: &[u8]) {
29        let mut pos = 0;
30
31        // Try to fill the existing partial block first.
32        if self.mac_tail_len > 0 {
33            let rem = std::cmp::min(16 - self.mac_tail_len, data.len());
34            self.mac_tail[self.mac_tail_len..self.mac_tail_len + rem].copy_from_slice(&data[..rem]);
35
36            self.mac_tail_len += rem;
37            pos = rem;
38
39            if self.mac_tail_len == 16 {
40                self.mac
41                    .update(&[MacBlock::try_from(self.mac_tail.as_ref())
42                        .expect("mac len is checked above")]);
43
44                self.mac_tail_len = 0;
45            }
46        }
47
48        // Feed remaining complete 16-byte blocks directly.
49        let mut chunks = data[pos..].chunks_exact(16);
50
51        for chunk in &mut chunks {
52            // chunk len is checked by chunks_exact
53            self.mac.update(&[
54                MacBlock::try_from(chunk).expect("chunk len is guaranteed by chunks_exact")
55            ]);
56        }
57
58        let tail = chunks.remainder();
59
60        if !tail.is_empty() {
61            self.mac_tail[..tail.len()].copy_from_slice(tail);
62            self.mac_tail_len += tail.len();
63        }
64    }
65}
66
67impl SimplexSecretBox for SecretBox {
68    fn init(key: &XSalsa20Key, nonce: &XSalsa20Nonce) -> Self {
69        let mut intermediate = hsalsa::<U10>(&Array(*key), &Array([0u8; 16]));
70        let mut cipher = XSalsa20::new(&intermediate, &Array(*nonce));
71        let mut poly_key = [0u8; 32];
72
73        cipher.apply_keystream(&mut poly_key);
74        let mac = Poly1305::new_from_slice(&poly_key).unwrap();
75
76        intermediate.zeroize();
77        poly_key.zeroize();
78
79        Self {
80            cipher,
81            mac,
82            mac_tail: Zeroizing::new([0u8; 16]),
83            mac_tail_len: 0,
84        }
85    }
86
87    fn encrypt_chunk(&mut self, chunk: impl AsRef<[u8]>, mut output: impl AsMut<[u8]>) {
88        let chunk = chunk.as_ref();
89        let output = output.as_mut();
90
91        output[..chunk.len()].copy_from_slice(chunk);
92        self.cipher.apply_keystream(&mut output[..chunk.len()]);
93        self.update_mac(&output[..chunk.len()]);
94    }
95
96    fn decrypt_chunk(&mut self, chunk: impl AsRef<[u8]>, mut output: impl AsMut<[u8]>) {
97        let chunk = chunk.as_ref();
98        let output = output.as_mut();
99
100        output[..chunk.len()].copy_from_slice(chunk);
101        self.cipher.apply_keystream(&mut output[..chunk.len()]);
102        self.update_mac(chunk); // MAC over original ciphertext
103    }
104
105    fn auth_tag(&mut self) -> Poly1305Tag {
106        self.mac
107            .clone()
108            .compute_unpadded(&self.mac_tail[..self.mac_tail_len])
109            .into()
110    }
111
112    fn verify_tag(&mut self, in_tag: &Poly1305Tag) -> bool {
113        let tag = self.auth_tag();
114        tag.as_slice().ct_eq(in_tag.as_slice()).into()
115    }
116}