aranya_crypto/groupkey.rs
1#![forbid(unsafe_code)]
2
3use core::{cell::OnceCell, iter, marker::PhantomData, result::Result};
4
5use derive_where::derive_where;
6use spideroak_crypto::{
7 aead::{Aead, BufferTooSmallError, KeyData, OpenError, SealError, Tag},
8 csprng::{Csprng, Random},
9 ctutils::{Choice, CtEq},
10 hash::{Digest, Hash},
11 import::Import,
12 typenum::U64,
13 zeroize::{Zeroize as _, ZeroizeOnDrop},
14};
15
16use crate::{
17 aranya::VerifyingKey,
18 ciphersuite::{CipherSuite, CipherSuiteExt as _},
19 engine::unwrapped,
20 error::Error,
21 hybrid_array::Array,
22 id::{IdError, Identified, custom_id},
23 policy::CmdId,
24};
25
26/// Key material used to derive per-event encryption keys.
27pub struct GroupKey<CS> {
28 seed: [u8; 64],
29 id: OnceCell<Result<GroupKeyId, IdError>>,
30 _cs: PhantomData<CS>,
31}
32
33impl<CS> ZeroizeOnDrop for GroupKey<CS> {}
34impl<CS> Drop for GroupKey<CS> {
35 fn drop(&mut self) {
36 self.seed.zeroize();
37 }
38}
39
40impl<CS> Clone for GroupKey<CS> {
41 fn clone(&self) -> Self {
42 Self {
43 seed: self.seed,
44 id: OnceCell::new(),
45 _cs: PhantomData,
46 }
47 }
48}
49
50impl<CS: CipherSuite> GroupKey<CS> {
51 /// Creates a new, random `GroupKey`.
52 pub fn new<R: Csprng>(rng: R) -> Self {
53 Self::from_seed(Random::random(rng))
54 }
55
56 /// Uniquely identifies the [`GroupKey`].
57 ///
58 /// Two keys with the same ID are the same key.
59 #[inline]
60 pub fn id(&self) -> Result<GroupKeyId, IdError> {
61 self.id
62 .get_or_init(|| {
63 // prk = LabeledExtract(
64 // "GroupKeyId-v1",
65 // {0}^n,
66 // "prk",
67 // seed,
68 // )
69 // GroupKey = LabeledExpand(
70 // "GroupKeyId-v1",
71 // prk,
72 // "id",
73 // {0}^0,
74 // )
75 const DOMAIN: &[u8] = b"GroupKeyId-v1";
76 let prk = CS::labeled_extract(DOMAIN, &[], b"prk", iter::once::<&[u8]>(&self.seed));
77 CS::labeled_expand(DOMAIN, &prk, b"id", [])
78 .map_err(|_| IdError::new("unable to expand PRK"))
79 .map(GroupKeyId::from_bytes)
80 })
81 .clone()
82 }
83
84 /// The size in bytes of the overhead added to plaintexts
85 /// encrypted with [`seal`][Self::seal].
86 pub const OVERHEAD: usize = CS::Aead::NONCE_SIZE + CS::Aead::OVERHEAD;
87
88 /// Returns the size in bytes of the overhead added to
89 /// plaintexts encrypted with [`seal`][Self::seal].
90 ///
91 /// Same as [`OVERHEAD`][Self::OVERHEAD].
92 pub const fn overhead(&self) -> usize {
93 Self::OVERHEAD
94 }
95
96 /// Encrypts and authenticates `plaintext` in a particular
97 /// context.
98 ///
99 /// The resulting ciphertext is written to `dst`, which must
100 /// be at least [`overhead`][Self::overhead] bytes longer
101 /// than `plaintext.len()`.
102 ///
103 /// # Example
104 ///
105 /// ```rust
106 /// # #[cfg(all(feature = "alloc", not(feature = "trng")))]
107 /// # {
108 /// use aranya_crypto::{
109 /// Context, GroupKey, Rng, SigningKey,
110 /// default::{DefaultCipherSuite, DefaultEngine},
111 /// policy::CmdId,
112 /// };
113 ///
114 /// const MESSAGE: &[u8] = b"hello, world!";
115 /// const LABEL: &str = "doc test";
116 /// const PARENT: CmdId = CmdId::default();
117 /// let author = SigningKey::<DefaultCipherSuite>::new(Rng)
118 /// .public()
119 /// .expect("signing key should be valid");
120 ///
121 /// let key = GroupKey::new(Rng);
122 ///
123 /// let ciphertext = {
124 /// let mut dst = vec![0u8; MESSAGE.len() + key.overhead()];
125 /// key.seal(
126 /// Rng,
127 /// &mut dst,
128 /// MESSAGE,
129 /// Context {
130 /// label: LABEL,
131 /// parent: PARENT,
132 /// author_sign_pk: &author,
133 /// },
134 /// )
135 /// .expect("should not fail");
136 /// dst
137 /// };
138 /// let plaintext = {
139 /// let mut dst = vec![0u8; ciphertext.len() - key.overhead()];
140 /// key.open(
141 /// &mut dst,
142 /// &ciphertext,
143 /// Context {
144 /// label: LABEL,
145 /// parent: PARENT,
146 /// author_sign_pk: &author,
147 /// },
148 /// )
149 /// .expect("should not fail");
150 /// dst
151 /// };
152 /// assert_eq!(&plaintext, MESSAGE);
153 /// # }
154 /// ```
155 pub fn seal<R: Csprng>(
156 &self,
157 rng: R,
158 dst: &mut [u8],
159 plaintext: &[u8],
160 ctx: Context<'_, CS>,
161 ) -> Result<(), Error> {
162 if dst.len() < self.overhead() {
163 // Not enough room in `dst`.
164 return Err(Error::Seal(SealError::BufferTooSmall(BufferTooSmallError(
165 self.overhead().checked_add(plaintext.len()),
166 ))));
167 }
168 let (nonce, out) = dst.split_at_mut(CS::Aead::NONCE_SIZE);
169 rng.fill_bytes(nonce);
170 let info = ctx.to_bytes()?;
171 let key = self.derive_key(&info)?;
172 Ok(CS::Aead::new(&key).seal(out, nonce, plaintext, &info)?)
173 }
174
175 /// Decrypts and authenticates `ciphertext` in a particular
176 /// context.
177 ///
178 /// The resulting plaintext is written to `dst`, which must
179 /// be at least as long as the original plaintext (i.e.,
180 /// `ciphertext.len()` - [`overhead`][Self::overhead] bytes
181 /// long).
182 pub fn open(
183 &self,
184 dst: &mut [u8],
185 ciphertext: &[u8],
186 ctx: Context<'_, CS>,
187 ) -> Result<(), Error> {
188 if ciphertext.len() < self.overhead() {
189 // Can't find the nonce and/or tag, so it's obviously
190 // invalid.
191 return Err(OpenError::Authentication.into());
192 }
193 let (nonce, ciphertext) = ciphertext.split_at(CS::Aead::NONCE_SIZE);
194 let info = ctx.to_bytes()?;
195 let key = self.derive_key(&info)?;
196 Ok(CS::Aead::new(&key).open(dst, nonce, ciphertext, &info)?)
197 }
198
199 /// Derives a key for [`Self::open`] and [`Self::seal`].
200 fn derive_key(&self, info: &[u8]) -> Result<<CS::Aead as Aead>::Key, Error> {
201 // prk = LabeledExtract(
202 // "kdf-ext-v1",
203 // {0}^n,
204 // "EventKey_prk",
205 // seed,
206 // )
207 // GroupKey = LabeledExpand(
208 // "kdf-exp-v1",
209 // prk,
210 // "EventKey_key",
211 // info,
212 // )
213 let prk = CS::labeled_extract(
214 b"kdf-ext-v1",
215 &[],
216 b"EventKey_prk",
217 iter::once::<&[u8]>(&self.seed),
218 );
219 let key: KeyData<CS::Aead> =
220 CS::labeled_expand(b"kdr-exp-v1", &prk, b"EventKey_key", [info])?;
221 Ok(<<CS::Aead as Aead>::Key as Import<_>>::import(
222 key.as_bytes(),
223 )?)
224 }
225
226 // Utility routines for other modules.
227
228 /// Returns the underlying seed.
229 pub(crate) const fn raw_seed(&self) -> &[u8; 64] {
230 &self.seed
231 }
232
233 /// Creates itself from the seed.
234 pub(crate) const fn from_seed(seed: [u8; 64]) -> Self {
235 Self {
236 seed,
237 id: OnceCell::new(),
238 _cs: PhantomData,
239 }
240 }
241}
242
243unwrapped! {
244 name: GroupKey;
245 type: Seed;
246 into: |key: Self| { key.seed };
247 from: |seed: [u8;64] | { Self::from_seed(seed) };
248}
249
250impl<CS: CipherSuite> Identified for GroupKey<CS> {
251 type Id = GroupKeyId;
252
253 #[inline]
254 fn id(&self) -> Result<Self::Id, IdError> {
255 self.id()
256 }
257}
258
259impl<CS: CipherSuite> CtEq for GroupKey<CS> {
260 #[inline]
261 fn ct_eq(&self, other: &Self) -> Choice {
262 self.seed.ct_eq(&other.seed)
263 }
264}
265
266/// Contextual binding for [`GroupKey::seal`] and
267/// [`GroupKey::open`].
268pub struct Context<'a, CS: CipherSuite> {
269 /// Describes what is being encrypted.
270 ///
271 /// For example, it could be an event name.
272 pub label: &'a str,
273 /// The stable ID of the parent event.
274 pub parent: CmdId,
275 /// The public key of the author of the encrypted data.
276 pub author_sign_pk: &'a VerifyingKey<CS>,
277}
278
279impl<CS: CipherSuite> Context<'_, CS> {
280 /// Converts the [`Context`] to its byte representation.
281 fn to_bytes(&self) -> Result<Digest<<CS::Hash as Hash>::DigestSize>, Error> {
282 // Ideally, this would simple be the actual concatenation
283 // of `Context`'s fields. However, we need to be
284 // `no_alloc` and without `const_generic_exprs` it's
285 // quite difficult to concatenate the fields into
286 // a fixed-size buffer.
287 //
288 // So, we instead hash the fields into a fixed-size
289 // buffer. We use `tuple_hash` out of paranoia, but
290 // a regular hash should also suffice.
291 Ok(CS::tuple_hash(
292 b"GroupKey",
293 [
294 self.label.as_bytes(),
295 self.parent.as_ref(),
296 self.author_sign_pk.id()?.as_bytes(),
297 ],
298 ))
299 }
300}
301
302custom_id! {
303 /// Uniquely identifies a [`GroupKey`].
304 pub struct GroupKeyId;
305}
306
307/// An encrypted [`GroupKey`].
308#[derive_where(Clone, Debug, Serialize, Deserialize)]
309pub struct EncryptedGroupKey<CS: CipherSuite> {
310 pub(crate) ciphertext: Array<u8, U64>,
311 pub(crate) tag: Tag<CS::Aead>,
312}
313
314#[cfg(test)]
315mod tests {
316 #![allow(clippy::arithmetic_side_effects)]
317
318 use super::*;
319 use crate::{Rng, SigningKey, default::DefaultCipherSuite};
320
321 type CS = DefaultCipherSuite;
322
323 fn author() -> VerifyingKey<CS> {
324 SigningKey::<CS>::new(Rng)
325 .public()
326 .expect("signing key should be valid")
327 }
328
329 fn ctx<'a>(author: &'a VerifyingKey<CS>) -> Context<'a, CS> {
330 Context {
331 label: "test",
332 parent: CmdId::default(),
333 author_sign_pk: author,
334 }
335 }
336
337 /// Exercises both the happy path and the "`dst` too small"
338 /// error path of [`GroupKey::seal`].
339 #[test]
340 fn test_seal_dst_too_small() {
341 let author = author();
342 let key = GroupKey::<CS>::new(Rng);
343 const MESSAGE: &[u8] = b"hello, world!";
344
345 // Happy path: `dst` is large enough.
346 let mut dst = vec![0u8; MESSAGE.len() + key.overhead()];
347 key.seal(Rng, &mut dst, MESSAGE, ctx(&author))
348 .expect("`seal` should succeed");
349
350 // Error path: `dst` is shorter than `overhead()`.
351 let mut small = [0u8; 1];
352 let err = key
353 .seal(Rng, &mut small, MESSAGE, ctx(&author))
354 .expect_err("`seal` should fail when `dst` is too small");
355 assert!(matches!(err, Error::Seal(_)), "got {err:?}");
356 }
357
358 /// Exercises both the happy path and the "`ciphertext` too
359 /// short" error path of [`GroupKey::open`].
360 #[test]
361 fn test_open_ciphertext_too_short() {
362 let author = author();
363 let key = GroupKey::<CS>::new(Rng);
364 const MESSAGE: &[u8] = b"hello, world!";
365
366 let mut ciphertext = vec![0u8; MESSAGE.len() + key.overhead()];
367 key.seal(Rng, &mut ciphertext, MESSAGE, ctx(&author))
368 .expect("`seal` should succeed");
369
370 // Happy path: decrypt the full ciphertext.
371 let mut plaintext = vec![0u8; ciphertext.len() - key.overhead()];
372 key.open(&mut plaintext, &ciphertext, ctx(&author))
373 .expect("`open` should succeed");
374 assert_eq!(plaintext, MESSAGE);
375
376 // Error path: `ciphertext` is shorter than `overhead()`.
377 let err = key
378 .open(&mut plaintext, b"short", ctx(&author))
379 .expect_err("`open` should fail when `ciphertext` is too short");
380 assert!(matches!(err, Error::Open(_)), "got {err:?}");
381 }
382}