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}