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