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}