ecdh_omr/hints.rs
1// SPDX-FileCopyrightText: 2024-2026 eaon <eaon@posteo.net>
2// SPDX-License-Identifier: EUPL-1.2
3
4use aead::{Aead, KeyInit};
5use hybrid_array::ArraySize;
6use rand::seq::SliceRandom;
7use rand_core::CryptoRng;
8use typenum::Unsigned;
9
10use crate::{Decoy, Hint, HintSeed, HintSized, Hinting, KeyPair, RandomSecretKey, error::*};
11
12/// Batch of [`Hint`]s that enforces inner vector size as well as shuffling, and also mitigates
13/// potential timing leaks at creation time.
14///
15/// 1. If fewer `HintSeed` items than `S` are supplied during creation, decoy `HintSeed` will fill
16/// the remaining slots.
17/// 2. A new temporary `K::SecretKey` is generated, which is used as a one-off contribution to the
18/// group secrets used to encrypt the respective `Hint`'s messages.
19/// 3. In order to make sure passive attackers don't know which `Hint` to brute force, the `Hints`
20/// order should not be deterministic, so decoy and real `Hint`s get shuffled before the result
21/// is returned.
22///
23/// In aggregate, this ensures that even if the same `HintSeed` is used to create multiple `Hints`
24/// instances, they are indistinguishable to passive observers that want to infer communication
25/// patterns by repeatedly polling a server or other intermediary.
26#[derive(Debug, Clone)]
27pub struct Hints<H, const S: usize> {
28 inner: Vec<H>,
29}
30
31impl<H, const S: usize> FromIterator<H> for Hints<H, S> {
32 fn from_iter<I: IntoIterator<Item = H>>(iter: I) -> Self {
33 let inner = iter.into_iter().collect();
34
35 Self { inner }
36 }
37}
38
39impl<K: KeyPair, A: Aead + KeyInit, L: ArraySize, const S: usize> Hints<Hint<K, A, L>, S>
40where
41 Hint<K, A, L>: HintSized<K, A, L> + Hinting<K, L>,
42{
43 /// Build shuffled batch of [`Hint`] instances from a [`HintSeed`] slice, context, and an RNG,
44 /// resulting in a total of `S` items.
45 ///
46 /// **Note**: Although this associated function attempts to account for it, timing leaks MAY
47 /// happen here. The mitigations' effectiveness has not yet been independently verified.
48 pub fn new(
49 hint_seeds: &[HintSeed<K, L>],
50 context: &[u8],
51 csprng: &mut impl CryptoRng,
52 ) -> Result<Self>
53 where
54 HintSeed<K, L>: Decoy,
55 {
56 let hint_seeds_len = hint_seeds.len();
57 if hint_seeds_len > S {
58 return Err(Error::HintsLength);
59 }
60
61 let hints_secret = K::SecretKey::random_secret_key(csprng);
62
63 // To mitigate timing leaks, we always generate as many decoy HintSeeds we would serve
64 // hints.
65 let decoys: Vec<_> = (0..S)
66 .map(|_| HintSeed::<K, L>::random_decoy(csprng))
67 .collect();
68
69 // Shuffle vector as we're building it to make brute forcing individual items pointless.
70 let mut indices: Vec<usize> = (0..S).collect();
71 indices.shuffle(csprng);
72
73 // We combine the real hint_seeds with as many decoys as we need, and then encrypt all of
74 // them, ensuring that all items take equal time to be created, provided that the underlying
75 // primitives have constant time implementations.
76 indices
77 .into_iter()
78 .map(|i| {
79 let hint_seed = if i < hint_seeds_len {
80 &hint_seeds[i]
81 } else {
82 &decoys[i - hint_seeds_len]
83 };
84
85 Hint::<K, A, L>::from_blinding_factor_secret(
86 &hints_secret,
87 &hint_seed.blinded_public_key,
88 &hint_seed.message,
89 context,
90 )
91 })
92 .collect::<Result<_>>()
93 }
94
95 /// View as slice.
96 pub fn as_slice(&self) -> &[Hint<K, A, L>] {
97 self.inner.as_slice()
98 }
99
100 /// Deserialize from byte slice.
101 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
102 let hint_bytes_length = <Hint<K, A, L> as HintSized<K, A, L>>::Size::USIZE;
103
104 if bytes.len() != hint_bytes_length * S {
105 return Err(Error::HintsLength);
106 }
107
108 Ok(Self {
109 inner: bytes
110 .chunks_exact(hint_bytes_length)
111 .map(|c| Hint::<K, A, L>::from_bytes(c))
112 .collect::<Result<Vec<_>>>()?,
113 })
114 }
115
116 /// Serialize to byte vector.
117 pub fn to_bytes(self) -> Vec<u8> {
118 let mut bytes = Vec::with_capacity(<Hint<K, A, L> as HintSized<K, A, L>>::Size::USIZE * S);
119
120 self.inner
121 .into_iter()
122 .for_each(|hint| bytes.extend(hint.to_bytes()));
123
124 bytes
125 }
126}