Skip to main content

simploxide_client/crypto/
native.rs

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