1 | #![allow (clippy::duplicate_mod)] |
2 | |
3 | use crate::error::Error; |
4 | use crate::rand::GetRandomFailed; |
5 | use crate::server::ProducesTickets; |
6 | |
7 | use super::ring_like::aead; |
8 | use super::ring_like::rand::{SecureRandom, SystemRandom}; |
9 | |
10 | use alloc::boxed::Box; |
11 | use alloc::sync::Arc; |
12 | use alloc::vec::Vec; |
13 | use core::fmt; |
14 | use core::fmt::{Debug, Formatter}; |
15 | |
16 | /// A concrete, safe ticket creation mechanism. |
17 | pub struct Ticketer {} |
18 | |
19 | impl Ticketer { |
20 | /// Make the recommended Ticketer. This produces tickets |
21 | /// with a 12 hour life and randomly generated keys. |
22 | /// |
23 | /// The encryption mechanism used is Chacha20Poly1305. |
24 | pub fn new() -> Result<Arc<dyn ProducesTickets>, Error> { |
25 | Ok(Arc::new(data:crate::ticketer::TicketSwitcher::new( |
26 | lifetime:6 * 60 * 60, |
27 | make_ticket_generator, |
28 | )?)) |
29 | } |
30 | } |
31 | |
32 | fn make_ticket_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> { |
33 | let mut key: [u8; 32] = [0u8; 32]; |
34 | SystemRandom::new() |
35 | .fill(&mut key) |
36 | .map_err(|_| GetRandomFailed)?; |
37 | |
38 | let alg: &Algorithm = &aead::CHACHA20_POLY1305; |
39 | let key: UnboundKey = aead::UnboundKey::new(algorithm:alg, &key).unwrap(); |
40 | |
41 | Ok(Box::new(AeadTicketer { |
42 | alg, |
43 | key: aead::LessSafeKey::new(key), |
44 | lifetime: 60 * 60 * 12, |
45 | })) |
46 | } |
47 | |
48 | /// This is a `ProducesTickets` implementation which uses |
49 | /// any *ring* `aead::Algorithm` to encrypt and authentication |
50 | /// the ticket payload. It does not enforce any lifetime |
51 | /// constraint. |
52 | struct AeadTicketer { |
53 | alg: &'static aead::Algorithm, |
54 | key: aead::LessSafeKey, |
55 | lifetime: u32, |
56 | } |
57 | |
58 | impl ProducesTickets for AeadTicketer { |
59 | fn enabled(&self) -> bool { |
60 | true |
61 | } |
62 | fn lifetime(&self) -> u32 { |
63 | self.lifetime |
64 | } |
65 | |
66 | /// Encrypt `message` and return the ciphertext. |
67 | fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> { |
68 | // Random nonce, because a counter is a privacy leak. |
69 | let mut nonce_buf = [0u8; 12]; |
70 | SystemRandom::new() |
71 | .fill(&mut nonce_buf) |
72 | .ok()?; |
73 | let nonce = aead::Nonce::assume_unique_for_key(nonce_buf); |
74 | let aad = aead::Aad::empty(); |
75 | |
76 | let mut ciphertext = |
77 | Vec::with_capacity(nonce_buf.len() + message.len() + self.key.algorithm().tag_len()); |
78 | ciphertext.extend(nonce_buf); |
79 | ciphertext.extend(message); |
80 | self.key |
81 | .seal_in_place_separate_tag(nonce, aad, &mut ciphertext[nonce_buf.len()..]) |
82 | .map(|tag| { |
83 | ciphertext.extend(tag.as_ref()); |
84 | ciphertext |
85 | }) |
86 | .ok() |
87 | } |
88 | |
89 | /// Decrypt `ciphertext` and recover the original message. |
90 | fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> { |
91 | // Non-panicking `let (nonce, ciphertext) = ciphertext.split_at(...)`. |
92 | let nonce = ciphertext.get(..self.alg.nonce_len())?; |
93 | let ciphertext = ciphertext.get(nonce.len()..)?; |
94 | |
95 | // This won't fail since `nonce` has the required length. |
96 | let nonce = aead::Nonce::try_assume_unique_for_key(nonce).ok()?; |
97 | |
98 | let mut out = Vec::from(ciphertext); |
99 | |
100 | let plain_len = self |
101 | .key |
102 | .open_in_place(nonce, aead::Aad::empty(), &mut out) |
103 | .ok()? |
104 | .len(); |
105 | out.truncate(plain_len); |
106 | |
107 | Some(out) |
108 | } |
109 | } |
110 | |
111 | impl Debug for AeadTicketer { |
112 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { |
113 | // Note: we deliberately omit the key from the debug output. |
114 | f&mut DebugStruct<'_, '_>.debug_struct("AeadTicketer" ) |
115 | .field("alg" , &self.alg) |
116 | .field(name:"lifetime" , &self.lifetime) |
117 | .finish() |
118 | } |
119 | } |
120 | |
121 | #[cfg (test)] |
122 | mod tests { |
123 | use super::*; |
124 | |
125 | use core::time::Duration; |
126 | use pki_types::UnixTime; |
127 | |
128 | #[test ] |
129 | fn basic_pairwise_test() { |
130 | let t = Ticketer::new().unwrap(); |
131 | assert!(t.enabled()); |
132 | let cipher = t.encrypt(b"hello world" ).unwrap(); |
133 | let plain = t.decrypt(&cipher).unwrap(); |
134 | assert_eq!(plain, b"hello world" ); |
135 | } |
136 | |
137 | #[test ] |
138 | fn ticketswitcher_switching_test() { |
139 | let t = Arc::new(crate::ticketer::TicketSwitcher::new(1, make_ticket_generator).unwrap()); |
140 | let now = UnixTime::now(); |
141 | let cipher1 = t.encrypt(b"ticket 1" ).unwrap(); |
142 | assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1" ); |
143 | { |
144 | // Trigger new ticketer |
145 | t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs( |
146 | now.as_secs() + 10, |
147 | ))); |
148 | } |
149 | let cipher2 = t.encrypt(b"ticket 2" ).unwrap(); |
150 | assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1" ); |
151 | assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2" ); |
152 | { |
153 | // Trigger new ticketer |
154 | t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs( |
155 | now.as_secs() + 20, |
156 | ))); |
157 | } |
158 | let cipher3 = t.encrypt(b"ticket 3" ).unwrap(); |
159 | assert!(t.decrypt(&cipher1).is_none()); |
160 | assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2" ); |
161 | assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3" ); |
162 | } |
163 | |
164 | #[cfg (test)] |
165 | fn fail_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> { |
166 | Err(GetRandomFailed) |
167 | } |
168 | |
169 | #[test ] |
170 | fn ticketswitcher_recover_test() { |
171 | let mut t = crate::ticketer::TicketSwitcher::new(1, make_ticket_generator).unwrap(); |
172 | let now = UnixTime::now(); |
173 | let cipher1 = t.encrypt(b"ticket 1" ).unwrap(); |
174 | assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1" ); |
175 | t.generator = fail_generator; |
176 | { |
177 | // Failed new ticketer |
178 | t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs( |
179 | now.as_secs() + 10, |
180 | ))); |
181 | } |
182 | t.generator = make_ticket_generator; |
183 | let cipher2 = t.encrypt(b"ticket 2" ).unwrap(); |
184 | assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1" ); |
185 | assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2" ); |
186 | { |
187 | // recover |
188 | t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs( |
189 | now.as_secs() + 20, |
190 | ))); |
191 | } |
192 | let cipher3 = t.encrypt(b"ticket 3" ).unwrap(); |
193 | assert!(t.decrypt(&cipher1).is_none()); |
194 | assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2" ); |
195 | assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3" ); |
196 | } |
197 | |
198 | #[test ] |
199 | fn aeadticketer_is_debug_and_producestickets() { |
200 | use super::*; |
201 | use alloc::format; |
202 | |
203 | let t = make_ticket_generator().unwrap(); |
204 | |
205 | assert_eq!( |
206 | format!("{:?}" , t), |
207 | "AeadTicketer { alg: CHACHA20_POLY1305, lifetime: 43200 }" |
208 | ); |
209 | assert!(t.enabled()); |
210 | assert_eq!(t.lifetime(), 43200); |
211 | } |
212 | } |
213 | |