passring/
payload.rs

1// Copyright (C) 2024 Stanislav Zhevachevskyi
2// 
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU Affero General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7// 
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11// GNU Affero General Public License for more details.
12// 
13// You should have received a copy of the GNU Affero General Public License
14// along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16//! Passring payloads
17
18use chacha20poly1305::{AeadCore, ChaCha20Poly1305, KeyInit};
19use chacha20poly1305::aead::Aead;
20use chacha20poly1305::aead::generic_array::GenericArray;
21use rand_core::CryptoRngCore;
22use serde::{Deserialize, Serialize};
23use crate::choices::VotingChoice;
24use crate::errors::PassringError::{InvalidPayload, SymmetricError};
25use crate::Result;
26
27/// Encrypted payload
28/// 
29/// Only encrypted payload can be used for signing, and transferring over the network.
30/// The payload is encrypted using the `ChaCha20Poly1305` algorithm.
31#[allow(clippy::module_name_repetitions)]
32#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
33pub struct Payload {
34    /// Voting ID
35    pub voting_id: uuid::Uuid,
36    /// Encrypted payload
37    pub encrypted: Vec<u8>,
38    /// Nonce
39    pub nonce: Vec<u8>,
40}
41
42/// Clear payload
43/// 
44/// Clear payload is the decrypted version of the payload.
45/// Agency will decrypt the payload using the key, received from voter after the ballot is published.
46#[allow(clippy::module_name_repetitions)]
47#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
48pub struct ClearPayload {
49    /// Voting ID
50    pub voting_id: uuid::Uuid,
51    /// Choice
52    pub choice: VotingChoice,
53    /// Randomness. It is used for mitigating brute force attacks.
54    pub randomness: Vec<u8>,
55}
56
57
58impl Payload {
59    /// Create a new Payload
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use passring::payload::Payload;
65    ///
66    /// let voting_id = uuid::Uuid::new_v4();
67    /// let encrypted = vec![0u8; 32];
68    /// let nonce = vec![0u8; 12];
69    ///
70    /// let payload = Payload::new(voting_id, encrypted, nonce);
71    /// ```
72    #[must_use]
73    pub fn new(voting_id: uuid::Uuid, encrypted: Vec<u8>, nonce: Vec<u8>) -> Self {
74        Payload {
75            voting_id,
76            encrypted,
77            nonce,
78        }
79    }
80
81    /// Decrypt the payload
82    ///
83    /// # Errors
84    ///
85    /// * [`SymmetricError`]: Symmetric decryption error
86    /// * [`InvalidPayload`]: Payload is invalid
87    /// 
88    /// # Examples
89    ///
90    /// ```
91    /// use passring::payload::Payload;
92    ///
93    /// let voting_id = uuid::Uuid::new_v4();
94    /// let encrypted = vec![0u8; 32];
95    /// let nonce = vec![0u8; 12];
96    ///
97    /// let payload = Payload::new(voting_id, encrypted, nonce);
98    ///
99    /// let key = vec![0u8; 32];
100    ///
101    /// let decrypted = payload.decrypt(&key);
102    ///
103    /// // We used zeroed values for the payload and key, so the decryption will fail
104    /// assert!(decrypted.is_err());
105    /// ```
106    pub fn decrypt(&self, key: &[u8]) -> Result<ClearPayload> {
107        let Ok(cipher) = ChaCha20Poly1305::new_from_slice(key) else {
108            return Err(SymmetricError);
109        };
110
111        let nonce = GenericArray::clone_from_slice(&self.nonce);
112
113        let Ok(d) = cipher.decrypt(&nonce, self.encrypted.as_slice()) else {
114            return Err(SymmetricError);
115        };
116        match serde_json::from_slice::<ClearPayload>(&d) {
117            Ok(payload) => Ok(payload),
118            Err(_) => Err(InvalidPayload),
119        }
120    }
121}
122
123impl ClearPayload {
124
125    /// Create a new `ClearPayload`
126    ///
127    /// This function creates a new `ClearPayload` with the given `voting_id`, `choice` and `randomness`.
128    /// Randomness is a random vector of bytes, and primarily used for mitigating brute force attacks.
129    /// For production use, the randomness should be generated using a secure random number generator.
130    /// See [`new_random`](ClearPayload::new_random) for generating `ClearPayload` with prefilled randomness.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use passring::payload::ClearPayload;
136    /// use passring::choices::{BasicVotingChoice, VotingChoice};
137    ///
138    /// let voting_id = uuid::Uuid::new_v4();
139    /// let choice = VotingChoice::Basic { choice: BasicVotingChoice::For };
140    /// let randomness = vec![0u8; 32]; // must be random bytes
141    ///
142    /// let payload = ClearPayload::new(voting_id, choice, randomness);
143    /// ```
144    #[must_use]
145    pub fn new(voting_id: uuid::Uuid, choice: VotingChoice, randomness: Vec<u8>) -> Self {
146        ClearPayload {
147            voting_id,
148            choice,
149            randomness,
150        }
151    }
152
153    /// Create a new `ClearPayload` with random values
154    ///
155    /// This function creates a new `ClearPayload` with the given `voting_id`, `choice` and random `randomness`.
156    /// Randomness is a random vector of bytes, and primarily used for mitigating brute force attacks.
157    /// In this function, the randomness is generated using the given `rng`.
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// use passring::payload::ClearPayload;
163    /// use rand_core::OsRng;
164    /// use passring::choices::{BasicVotingChoice, VotingChoice};
165    ///
166    /// let voting_id = uuid::Uuid::new_v4();
167    /// let choice = VotingChoice::Basic { choice: BasicVotingChoice::For };
168    ///
169    /// let payload = ClearPayload::new_random(voting_id, choice, &mut OsRng);
170    /// ```
171    pub fn new_random(voting_id: uuid::Uuid, choice: VotingChoice, rng: &mut impl CryptoRngCore) -> Self {
172        let mut randomness = vec![0u8; 32];
173        rng.fill_bytes(&mut randomness);
174        ClearPayload::new(voting_id, choice, randomness)
175    }
176
177    /// Encrypt the payload
178    /// 
179    /// This function encrypts the payload using the given `key` and `rng`.
180    /// The encryption is done using the `ChaCha20Poly1305` algorithm.
181    /// Nonce is generated using the given `rng`.
182    /// 
183    /// # Errors
184    /// 
185    /// * [`SymmetricError`]: Symmetric encryption error
186    /// * [`InvalidPayload`]: Payload is invalid
187    /// 
188    /// # Examples 
189    /// 
190    /// ```
191    /// use passring::payload::ClearPayload;
192    /// use chacha20poly1305::{ChaCha20Poly1305, KeyInit};
193    /// use rand_core::OsRng;
194    /// use passring::choices::{BasicVotingChoice, VotingChoice};
195    ///
196    /// let voting_id = uuid::Uuid::new_v4();
197    /// let choice = VotingChoice::Basic { choice: BasicVotingChoice::For };
198    ///
199    /// let clear_payload = ClearPayload::new_random(voting_id, choice, &mut OsRng);
200    /// let key = ChaCha20Poly1305::generate_key(&mut OsRng);
201    ///
202    /// let payload = clear_payload.encrypt(&key, &mut OsRng).unwrap();
203    /// ```
204    pub fn encrypt<R: CryptoRngCore>(&self, key: &[u8], rng: &mut R) -> Result<Payload> {
205        let Ok(cipher) = ChaCha20Poly1305::new_from_slice(key) else {
206            return Err(SymmetricError);
207        };
208
209        let nonce = ChaCha20Poly1305::generate_nonce(rng);
210
211        let Ok(message) = serde_json::to_vec(&self) else {
212            return Err(InvalidPayload);
213        };
214
215        let Ok(e) = cipher.encrypt(&nonce, message.as_slice()) else {
216            return Err(SymmetricError);
217        };
218
219        Ok(Payload {
220            voting_id: self.voting_id,
221            encrypted: e,
222            nonce: nonce.to_vec(),
223        })
224    }
225}
226
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use rand_core::OsRng;
232    use crate::choices::BasicVotingChoice;
233
234    #[test]
235    fn test_payload() {
236        let voting_id = uuid::Uuid::new_v4();
237        let choice = VotingChoice::Basic { choice: BasicVotingChoice::For };
238        let clear_payload = ClearPayload::new(voting_id, choice, vec![0u8; 32]);
239        let key = ChaCha20Poly1305::generate_key(&mut OsRng);
240        let payload = clear_payload.encrypt(&key, &mut OsRng).unwrap();
241        let decrypted = payload.decrypt(&key).unwrap();
242        assert_eq!(clear_payload, decrypted);
243    }
244
245    #[test]
246    fn test_payload_random() {
247        let voting_id = uuid::Uuid::new_v4();
248        let choice = VotingChoice::Basic { choice: BasicVotingChoice::For };
249        let clear_payload = ClearPayload::new_random(voting_id, choice, &mut OsRng);
250        let key = ChaCha20Poly1305::generate_key(&mut OsRng);
251        let payload = clear_payload.encrypt(&key, &mut OsRng).unwrap();
252        let decrypted = payload.decrypt(&key).unwrap();
253        assert_eq!(clear_payload, decrypted);
254    }
255}