Skip to main content

ecdh_omr/
hint.rs

1// SPDX-FileCopyrightText: 2024-2026 eaon <eaon@posteo.net>
2// SPDX-License-Identifier: EUPL-1.2
3
4use std::marker::PhantomData;
5use std::ops::Add;
6
7use aead::{Aead, AeadCore, KeyInit, Payload};
8#[cfg(feature = "rustcrypto-ec")]
9use elliptic_curve::{
10    Curve, CurveArithmetic,
11    point::PointCompression,
12    sec1::{CompressedPoint, CompressedPointSize, FromSec1Point, ModulusSize, ToSec1Point},
13};
14use hybrid_array::ArraySize;
15use rand_core::CryptoRng;
16use typenum::{Sum, Unsigned};
17
18#[cfg(feature = "dalek-ristretto255")]
19use crate::{
20    DalekRistretto255, blind_dalek_ristretto255, checked_decompress_ristretto255_public_key,
21    dalek_ristretto255, random_blind_dalek_ristretto255,
22};
23#[cfg(feature = "dalek-x25519")]
24use crate::{
25    DalekX25519, blind_dalek_x25519, checked_x25519_public_key, random_blind_dalek_x25519,
26};
27#[cfg(feature = "rustcrypto-ec")]
28use crate::{EllipticCurve, blind_rcec, random_blind_rcec};
29use crate::{error::*, *};
30
31/// Semi generic implementations to create new [`Hint`]s
32pub trait Hinting<K: KeyPair, L: ArraySize>: Sized {
33    /// Byte length of a serialized [`Hint`]: public key + ciphertext + tag.
34    type HintSize: ArraySize;
35
36    /// Create a new [`Hint`].
37    fn new(
38        blinded_public_key: &BlindedPublicKey<K>,
39        message: &Array<u8, L>,
40        context: &[u8],
41        csprng: &mut impl CryptoRng,
42    ) -> Result<Self>;
43
44    /// Create a new [`Hint`] using a blinding factor secret.
45    fn from_blinding_factor_secret(
46        blinding_factor_secret: &K::SecretKey,
47        blinded_public_key: &BlindedPublicKey<K>,
48        message: &Array<u8, L>,
49        context: &[u8],
50    ) -> Result<Self>;
51
52    /// Deserialize from byte slice
53    ///
54    /// # Security
55    ///
56    /// Implementers **must** validate the deserialized blinded blinding factor and reject
57    /// small-order and identity points with [`Error::InvalidPoint`].
58    fn from_bytes(bytes: &[u8]) -> Result<Self>;
59
60    /// Serialize to byte array
61    fn to_bytes(self) -> Array<u8, Self::HintSize>;
62}
63
64type HintCiphertextSize<A, L> = Sum<<A as AeadCore>::TagSize, L>;
65type HintSize<K, A, L> = Sum<<K as KeyPair>::PublicKeySize, HintCiphertextSize<A, L>>;
66
67/// Provides compile-time size information for [`Hint`] serialization.
68///
69/// The associated types give access to the ciphertext and total hint sizes,
70/// computed from the key pair, AEAD algorithm, and message length.
71pub trait HintSized<K: KeyPair, A: AeadCore, L: ArraySize>: Sized {
72    /// Ciphertext size: AEAD tag size plus message length.
73    type CiphertextSize: ArraySize;
74    /// Total hint size: public key size plus ciphertext size.
75    type Size: ArraySize;
76}
77
78impl<K: KeyPair, A: Aead + KeyInit, L: ArraySize> HintSized<K, A, L> for Hint<K, A, L>
79where
80    <A as AeadCore>::TagSize: Add<L>,
81    <<A as AeadCore>::TagSize as Add<L>>::Output: ArraySize,
82    <K as KeyPair>::PublicKeySize: Add<HintCiphertextSize<A, L>>,
83    <<K as KeyPair>::PublicKeySize as Add<HintCiphertextSize<A, L>>>::Output: ArraySize,
84{
85    type CiphertextSize = HintCiphertextSize<A, L>;
86    type Size = HintSize<K, A, L>;
87}
88
89/// Message encrypted by a third party, decryptable by an anonymous recipient that doesn't know
90/// whether it is addressed to them or not.
91#[derive(Debug, Clone)]
92pub struct Hint<K: KeyPair, A: Aead + KeyInit, L: ArraySize>
93where
94    Self: HintSized<K, A, L>,
95{
96    /// Two thirds of a three part secret used to encrypt the hint's underlying message.
97    ///
98    /// The blinding factor of a [`BlindedPublicKey`] is blinded itself, allowing the entire hint
99    /// to be randomized.
100    pub(crate) blinded_blinding_factor: K::PublicKey,
101    /// Ciphertext decryptable by trial decryption.
102    pub(crate) ciphertext: Array<u8, <Self as HintSized<K, A, L>>::CiphertextSize>,
103    /// Marker to allow us to generically use AEAD implementations
104    _aead: PhantomData<A>,
105}
106
107fn encrypt<A: Aead + KeyInit>(
108    shared_secret: &[u8],
109    blinded_blinding_factor: &[u8],
110    message: &[u8],
111) -> Result<Vec<u8>> {
112    let cipher = cipher_from_shared_secret::<A>(shared_secret)?;
113    let nonce = nonce::<A>(blinded_blinding_factor)?;
114    let ciphertext = cipher.encrypt(
115        &nonce,
116        Payload {
117            msg: message,
118            aad: blinded_blinding_factor,
119        },
120    )?;
121
122    Ok(ciphertext)
123}
124
125impl<K: KeyPair, A: Aead + KeyInit, L: ArraySize> TryFrom<&[u8]> for Hint<K, A, L>
126where
127    Self: Hinting<K, L> + HintSized<K, A, L>,
128{
129    type Error = Error;
130
131    fn try_from(bytes: &[u8]) -> Result<Self> {
132        Self::from_bytes(bytes)
133    }
134}
135
136#[cfg(any(feature = "dalek-ristretto255", feature = "dalek-x25519"))]
137macro_rules! dalek_hinting {
138    (
139        $construct:ident,
140        (
141            $curve:ty,
142            $public:ty,
143            $secret:ty,
144            $random_blind:ident,
145            $blind:ident,
146            $checked_public_key:expr,
147            $secret_to_bytes:expr,
148            $public_to_bytes:expr $(,)?
149        ) $(,)?
150    ) => {
151        fn $construct<A: Aead + KeyInit, L: ArraySize>(
152            blinded_blinding_factor: $public,
153            blinding_factor_secret: &$secret,
154            blinded_public_key_inner: &$public,
155            message: &Array<u8, L>,
156            context: &[u8],
157        ) -> Result<Hint<$curve, A, L>>
158        where
159            Hint<$curve, A, L>: HintSized<$curve, A, L>,
160        {
161            let raw_shared_secret = blinding_factor_secret.diffie_hellman(blinded_public_key_inner);
162
163            let blinded_blinding_factor_bytes = $public_to_bytes(&blinded_blinding_factor);
164            let raw_shared_secret_bytes = $secret_to_bytes(&raw_shared_secret);
165
166            let shared_secret = shared_secret(
167                &raw_shared_secret_bytes,
168                &blinded_blinding_factor_bytes,
169                context,
170            );
171
172            let ciphertext = encrypt::<A>(
173                &shared_secret,
174                &blinded_blinding_factor_bytes,
175                message.as_ref(),
176            )?
177            .into_iter()
178            .collect();
179
180            Ok(Hint {
181                blinded_blinding_factor,
182                ciphertext,
183                _aead: PhantomData,
184            })
185        }
186
187        impl<A: Aead + KeyInit, L: ArraySize> Hinting<$curve, L> for Hint<$curve, A, L>
188        where
189            Self: HintSized<$curve, A, L>,
190        {
191            type HintSize = <Self as HintSized<$curve, A, L>>::Size;
192
193            fn new(
194                blinded_public_key: &BlindedPublicKey<$curve>,
195                message: &Array<u8, L>,
196                context: &[u8],
197                csprng: &mut impl CryptoRng,
198            ) -> Result<Self> {
199                let (blinded_blinding_factor, blinding_factor_secret) =
200                    $random_blind(&blinded_public_key.blinding_factor, csprng);
201
202                $construct(
203                    blinded_blinding_factor,
204                    &blinding_factor_secret,
205                    &blinded_public_key.inner,
206                    message,
207                    context,
208                )
209            }
210
211            fn from_blinding_factor_secret(
212                blinding_factor_secret: &$secret,
213                blinded_public_key: &BlindedPublicKey<$curve>,
214                message: &Array<u8, L>,
215                context: &[u8],
216            ) -> Result<Self> {
217                let blinded_blinding_factor =
218                    $blind(&blinded_public_key.blinding_factor, blinding_factor_secret);
219
220                $construct(
221                    blinded_blinding_factor,
222                    blinding_factor_secret,
223                    &blinded_public_key.inner,
224                    message,
225                    context,
226                )
227            }
228
229            fn from_bytes(bytes: &[u8]) -> Result<Self> {
230                if bytes.len() != <Self as HintSized<$curve, A, L>>::Size::USIZE {
231                    return Err(Error::Decoding);
232                }
233
234                let public_size = <$curve as KeyPair>::PublicKeySize::USIZE;
235                let blinded_blinding_factor_bytes = bytes
236                    .iter()
237                    .take(public_size)
238                    .copied()
239                    .collect::<Array<u8, <$curve as KeyPair>::PublicKeySize>>();
240                let blinded_blinding_factor =
241                    $checked_public_key(blinded_blinding_factor_bytes.into())?;
242                let ciphertext = bytes.iter().skip(public_size).copied().collect();
243
244                Ok(Self {
245                    blinded_blinding_factor,
246                    ciphertext,
247                    _aead: PhantomData,
248                })
249            }
250
251            fn to_bytes(self) -> Array<u8, Self::HintSize> {
252                $public_to_bytes(&self.blinded_blinding_factor)
253                    .into_iter()
254                    .chain(self.ciphertext)
255                    .collect()
256            }
257        }
258    };
259}
260
261#[cfg(feature = "dalek-ristretto255")]
262dalek_hinting!(
263    construct_dalek_ristretto255_hint,
264    (
265        DalekRistretto255,
266        dalek_ristretto255::PublicKey,
267        dalek_ristretto255::StaticSecret,
268        random_blind_dalek_ristretto255,
269        blind_dalek_ristretto255,
270        checked_decompress_ristretto255_public_key,
271        |ss: &dalek_ristretto255::SharedSecret| ss.to_bytes(),
272        |pk: &dalek_ristretto255::PublicKey| pk.to_bytes(),
273    ),
274);
275
276#[cfg(feature = "dalek-x25519")]
277dalek_hinting!(
278    construct_dalek_x25519_hint,
279    (
280        DalekX25519,
281        x25519_dalek::PublicKey,
282        x25519_dalek::StaticSecret,
283        random_blind_dalek_x25519,
284        blind_dalek_x25519,
285        checked_x25519_public_key,
286        |ss: &x25519_dalek::SharedSecret| *ss.as_bytes(),
287        |pk: &x25519_dalek::PublicKey| *pk.as_bytes(),
288    ),
289);
290
291#[cfg(feature = "rustcrypto-ec")]
292fn construct_rustcrypto_ec_hint<A, C, L: ArraySize>(
293    blinded_blinding_factor: elliptic_curve::PublicKey<C>,
294    blinding_factor_secret: &elliptic_curve::SecretKey<C>,
295    blinded_public_key_inner: &elliptic_curve::PublicKey<C>,
296    message: &Array<u8, L>,
297    context: &[u8],
298) -> Result<Hint<EllipticCurve<C>, A, L>>
299where
300    A: Aead + KeyInit,
301    C: CurveArithmetic + PointCompression,
302    <C as Curve>::FieldBytesSize: ModulusSize,
303    <C as CurveArithmetic>::AffinePoint: ToSec1Point<C> + FromSec1Point<C>,
304    Hint<EllipticCurve<C>, A, L>: HintSized<EllipticCurve<C>, A, L>,
305{
306    let raw_shared_secret = elliptic_curve::ecdh::diffie_hellman(
307        blinding_factor_secret.to_nonzero_scalar(),
308        blinded_public_key_inner.as_affine(),
309    );
310
311    let blinded_blinding_factor_cp = CompressedPoint::<C>::from(&blinded_blinding_factor);
312
313    let shared_secret = shared_secret(
314        raw_shared_secret.raw_secret_bytes(),
315        blinded_blinding_factor_cp.as_slice(),
316        context,
317    );
318
319    let ciphertext = encrypt::<A>(
320        &shared_secret,
321        blinded_blinding_factor_cp.as_slice(),
322        message,
323    )?
324    .into_iter()
325    .collect();
326
327    Ok(Hint {
328        blinded_blinding_factor,
329        ciphertext,
330        _aead: PhantomData,
331    })
332}
333
334#[cfg(feature = "rustcrypto-ec")]
335impl<A: Aead + KeyInit, C: CurveArithmetic + PointCompression, L: ArraySize>
336    Hinting<EllipticCurve<C>, L> for Hint<EllipticCurve<C>, A, L>
337where
338    <C as Curve>::FieldBytesSize: ModulusSize,
339    <C as CurveArithmetic>::AffinePoint: ToSec1Point<C> + FromSec1Point<C>,
340    Self: HintSized<EllipticCurve<C>, A, L>,
341{
342    type HintSize = <Self as HintSized<EllipticCurve<C>, A, L>>::Size;
343
344    fn new(
345        blinded_public_key: &BlindedPublicKey<EllipticCurve<C>>,
346        message: &Array<u8, L>,
347        context: &[u8],
348        csprng: &mut impl CryptoRng,
349    ) -> Result<Self> {
350        let (blinded_blinding_factor, blinding_factor_secret) =
351            random_blind_rcec(&blinded_public_key.blinding_factor, csprng);
352
353        construct_rustcrypto_ec_hint(
354            blinded_blinding_factor,
355            &blinding_factor_secret,
356            &blinded_public_key.inner,
357            message,
358            context,
359        )
360    }
361
362    fn from_blinding_factor_secret(
363        blinding_factor_secret: &elliptic_curve::SecretKey<C>,
364        blinded_public_key: &BlindedPublicKey<EllipticCurve<C>>,
365        message: &Array<u8, L>,
366        context: &[u8],
367    ) -> Result<Self> {
368        let blinded_blinding_factor =
369            blind_rcec(&blinded_public_key.blinding_factor, blinding_factor_secret);
370
371        construct_rustcrypto_ec_hint(
372            blinded_blinding_factor,
373            blinding_factor_secret,
374            &blinded_public_key.inner,
375            message,
376            context,
377        )
378    }
379
380    fn from_bytes(bytes: &[u8]) -> Result<Self> {
381        if bytes.len() != <Self as HintSized<EllipticCurve<C>, A, L>>::Size::USIZE {
382            return Err(Error::Decoding);
383        }
384
385        let public_size = CompressedPointSize::<C>::USIZE;
386        let blinded_blinding_factor =
387            elliptic_curve::PublicKey::<C>::from_sec1_bytes(&bytes[..public_size])
388                .map_err(|_| Error::Decoding)?;
389        let ciphertext = bytes.iter().skip(public_size).copied().collect();
390
391        Ok(Self {
392            blinded_blinding_factor,
393            ciphertext,
394            _aead: PhantomData,
395        })
396    }
397
398    fn to_bytes(self) -> Array<u8, Self::HintSize> {
399        CompressedPoint::<C>::from(&self.blinded_blinding_factor)
400            .into_iter()
401            .chain(self.ciphertext)
402            .collect()
403    }
404}
405
406/// Pairing of message contents and the [`BlindedPublicKey`] of its recipient.
407///
408/// Hint seeds are used to create [`Hints`](crate::Hints).
409#[derive(Debug, Clone)]
410pub struct HintSeed<K: KeyPair, L: ArraySize> {
411    /// Blinded Public Key
412    pub blinded_public_key: BlindedPublicKey<K>,
413    /// Message
414    pub message: Array<u8, L>,
415}
416
417impl<K: KeyPair, L: ArraySize> HintSeed<K, L> {
418    /// Pair [`BlindedPublicKey`] with message contents.
419    pub fn new(blinded_public_key: BlindedPublicKey<K>, message: Array<u8, L>) -> Self {
420        Self {
421            blinded_public_key,
422            message,
423        }
424    }
425}
426
427impl<K: KeyPair, L: ArraySize> Decoy for HintSeed<K, L> {
428    fn random_decoy(csprng: &mut impl CryptoRng) -> Self {
429        Self {
430            blinded_public_key: BlindedPublicKey {
431                inner: K::PublicKey::random_decoy(csprng),
432                blinding_factor: K::PublicKey::random_decoy(csprng),
433            },
434            message: Array::default(),
435        }
436    }
437}