ecdh-omr 0.2.0

ECDH based Oblivious Message Retrieval
Documentation
// SPDX-FileCopyrightText: 2024-2025 eaon <eaon@posteo.net>
// SPDX-License-Identifier: EUPL-1.2

use aead::{Aead, AeadCore, KeyInit, generic_array::typenum::marker_traits::Unsigned};
#[cfg(feature = "rustcrypto-ec")]
use elliptic_curve::{
    Curve, CurveArithmetic,
    point::PointCompression,
    sec1::{CompressedPoint, FromEncodedPoint, ModulusSize, ToEncodedPoint},
};
use sha3::{Digest, Sha3_256};

use crate::{
    Hint, Hinting, Hints, cipher_from_shared_secret, curves,
    curves::{KeyPair, sealed},
    error::*,
};
#[cfg(feature = "rustcrypto-ec")]
use curves::{EllipticCurve, rcec};
#[cfg(feature = "dalek")]
use curves::{X25519, dalek};

/// Decrypt [`Hint`] and [`Hints`]. Also a silly pun.
pub trait TakeTheHint<K: KeyPair> {
    /// Trial decryption of an individual [`Hint`].
    fn take_the<A: Aead + KeyInit, const L: usize>(
        &self,
        hint: &Hint<K, A, L>,
        salt: &[u8],
    ) -> Result<[u8; L]>;

    /// Trial decryption for a batch of [`Hints`].
    fn take_all_the<A: Aead + KeyInit, const L: usize, const S: usize>(
        &self,
        hints: &Hints<Hint<K, A, L>, S>,
        salt: &[u8],
    ) -> Vec<[u8; L]>
    where
        Hint<K, A, L>: Hinting<K, L>,
        K::SecretKey: sealed::RandomSecretKey,
    {
        hints
            .as_slice()
            .iter()
            .filter_map(|hint| self.take_the(hint, salt).ok())
            .collect()
    }
}

fn decrypt<A: Aead + KeyInit>(
    nonce: &[u8],
    shared_secret: impl AsRef<[u8]>,
    ciphertext: &[u8],
) -> Result<Vec<u8>> {
    let cipher: A = cipher_from_shared_secret(shared_secret);
    let nonce_size = <A as AeadCore>::NonceSize::to_usize();
    let nonce = aead::Nonce::<A>::from_slice(&nonce[..nonce_size]);

    Ok(cipher.decrypt(nonce, ciphertext)?)
}

#[cfg(feature = "dalek")]
impl TakeTheHint<X25519> for dalek::StaticSecret {
    fn take_the<A: Aead + KeyInit, const L: usize>(
        &self,
        hint: &Hint<X25519, A, L>,
        salt: &[u8],
    ) -> Result<[u8; L]> {
        let raw_shared_secret = self.diffie_hellman(&hint.blinded_blinding_factor);

        let mut hasher = <Sha3_256 as Digest>::new();
        hasher.update(raw_shared_secret.as_bytes());
        hasher.update(hint.blinded_blinding_factor.as_bytes());
        hasher.update(salt);

        let shared_secret = hasher.finalize();

        <[u8; L]>::try_from(
            decrypt::<A>(
                hint.blinded_blinding_factor.as_bytes(),
                shared_secret,
                hint.ciphertext.as_slice(),
            )?
            .as_slice(),
        )
        .map_err(|_| Error::MessageLength)
    }
}

#[cfg(feature = "rustcrypto-ec")]
impl<C: CurveArithmetic> TakeTheHint<EllipticCurve<C>> for rcec::SecretKey<C>
where
    C: CurveArithmetic + PointCompression,
    <C as Curve>::FieldBytesSize: ModulusSize,
    <C as CurveArithmetic>::AffinePoint: ToEncodedPoint<C> + FromEncodedPoint<C>,
{
    fn take_the<A: Aead + KeyInit, const L: usize>(
        &self,
        hint: &Hint<EllipticCurve<C>, A, L>,
        salt: &[u8],
    ) -> Result<[u8; L]> {
        let raw_shared_secret = elliptic_curve::ecdh::diffie_hellman(
            self.to_nonzero_scalar(),
            hint.blinded_blinding_factor.as_affine(),
        );
        let blinded_blinding_factor_cp = CompressedPoint::<C>::from(hint.blinded_blinding_factor);

        let mut hasher = <Sha3_256 as Digest>::new();
        hasher.update(raw_shared_secret.raw_secret_bytes());
        hasher.update(blinded_blinding_factor_cp.as_slice());
        hasher.update(salt);

        let shared_secret = hasher.finalize();

        <[u8; L]>::try_from(
            decrypt::<A>(
                blinded_blinding_factor_cp.as_slice(),
                shared_secret,
                hint.ciphertext.as_slice(),
            )?
            .as_slice(),
        )
        .map_err(|_| Error::MessageLength)
    }
}