use crate::bulletproofs::{Range, RangeProof};
use crate::error::FastCryptoError::{InvalidInput, InvalidProof};
use crate::error::FastCryptoResult;
use crate::groups::ristretto255::{RistrettoPoint, RistrettoScalar, RISTRETTO_POINT_BYTE_LENGTH};
use crate::groups::{Doubling, FiatShamirChallenge, GroupElement, MultiScalarMul, Scalar};
use crate::pedersen::{Blinding, PedersenCommitment, G, H};
use crate::serde_helpers::ToFromByteArray;
use crate::traits::AllowedRng;
use derive_more::{Add, Mul, Sub};
use itertools::{iterate, Itertools};
use serde::{Deserialize, Serialize};
use std::array::from_fn;
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PublicKey(RistrettoPoint);
#[derive(Debug, Serialize, Deserialize)]
pub struct PrivateKey(RistrettoScalar);
#[derive(Debug, Clone, Add, Sub, Mul, Serialize, Deserialize)]
pub struct Ciphertext {
commitment: PedersenCommitment,
decryption_handle: RistrettoPoint,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MultiRecipientCiphertext {
commitment: PedersenCommitment,
decryption_handles: Vec<RistrettoPoint>,
}
pub struct VerifiableKeyEncapsulation<const N: usize> {
pub ciphertexts: [MultiRecipientCiphertext; N],
pub range_proof: RangeProof,
pub consistency_proof: KeyConsistencyProof<N>,
}
pub struct KeyConsistencyProof<const N: usize> {
a1: Vec<RistrettoPoint>,
a2: [RistrettoPoint; N],
a3: RistrettoPoint,
z1: [RistrettoScalar; N],
z2: [RistrettoScalar; N],
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConsistencyProof {
a1: RistrettoPoint,
a2: RistrettoPoint,
z1: RistrettoScalar,
z2: RistrettoScalar,
}
pub fn generate_keypair(rng: &mut impl AllowedRng) -> (PublicKey, PrivateKey) {
let sk = PrivateKey(RistrettoScalar::rand(rng));
(PublicKey::from(&sk), sk)
}
impl From<&PrivateKey> for PublicKey {
fn from(sk: &PrivateKey) -> Self {
PublicKey(*G * sk.0)
}
}
impl Ciphertext {
pub fn encrypt(
encryption_key: &PublicKey,
message: u32,
rng: &mut impl AllowedRng,
) -> (Self, Blinding) {
let blinding = Blinding::rand(rng);
(
Self {
decryption_handle: encryption_key.0 * blinding.0,
commitment: PedersenCommitment::new(
&RistrettoScalar::from(message as u64),
&blinding,
),
},
blinding,
)
}
pub fn encrypt_with_consistency_proof(
encryption_key: &PublicKey,
message: u32,
dst: &[u8],
rng: &mut impl AllowedRng,
) -> FastCryptoResult<(Self, Blinding, ConsistencyProof)> {
let (ciphertext, blinding) = Self::encrypt(encryption_key, message, rng);
let proof = ConsistencyProof::prove(
&RistrettoScalar::from(message as u64),
&ciphertext,
&blinding,
encryption_key,
dst,
rng,
)?;
Ok((ciphertext, blinding, proof))
}
pub fn decrypt(
&self,
private_key: &PrivateKey,
table: &HashMap<[u8; RISTRETTO_POINT_BYTE_LENGTH], u16>,
) -> FastCryptoResult<u32> {
let mut c = self.commitment.0 - (self.decryption_handle / private_key.0)?;
for x_low in 0..1 << 16 {
if let Some(&x_high) = table.get(&c.to_byte_array()) {
return Ok(x_low + ((x_high as u32) << 16));
}
c -= *H;
}
Err(InvalidInput)
}
}
impl ConsistencyProof {
pub fn prove(
message: &RistrettoScalar,
ciphertext: &Ciphertext,
blinding: &Blinding,
encryption_key: &PublicKey,
dst: &[u8],
rng: &mut impl AllowedRng,
) -> FastCryptoResult<Self> {
let r1 = RistrettoScalar::rand(rng);
let r2 = RistrettoScalar::rand(rng);
let a1 = encryption_key.0 * r1;
let a2 = RistrettoPoint::multi_scalar_mul(&[r1, r2], &[*G, *H]).expect("Constant length");
let c = Self::challenge(&a1, &a2, ciphertext, encryption_key, dst);
let z1 = r1 + c * blinding.0;
let z2 = r2 + c * message;
Ok(Self { a1, a2, z1, z2 })
}
pub fn verify(
&self,
ciphertext: &Ciphertext,
encryption_key: &PublicKey,
dst: &[u8],
) -> FastCryptoResult<()> {
let c = Self::challenge(&self.a1, &self.a2, ciphertext, encryption_key, dst);
if self.a1
!= RistrettoPoint::multi_scalar_mul(
&[-c, self.z1],
&[ciphertext.decryption_handle, encryption_key.0],
)
.expect("Constant lengths")
|| self.a2
!= RistrettoPoint::multi_scalar_mul(
&[-c, self.z1, self.z2],
&[ciphertext.commitment.0, *G, *H],
)
.expect("Constant lengths")
{
return Err(InvalidProof);
}
Ok(())
}
fn challenge(
a1: &RistrettoPoint,
a2: &RistrettoPoint,
ciphertext: &Ciphertext,
encryption_key: &PublicKey,
dst: &[u8],
) -> RistrettoScalar {
RistrettoScalar::fiat_shamir_reduction_to_group_element(
&bcs::to_bytes(&vec![
dst.to_vec(),
G.to_byte_array().to_vec(),
H.to_byte_array().to_vec(),
encryption_key.0.to_byte_array().to_vec(),
ciphertext.commitment.0.to_byte_array().to_vec(),
ciphertext.decryption_handle.to_byte_array().to_vec(),
a1.to_byte_array().to_vec(),
a2.to_byte_array().to_vec(),
])
.expect("Serialization succeeds"),
)
}
}
impl MultiRecipientCiphertext {
pub fn encrypt(
encryption_keys: &[PublicKey],
message: u32,
rng: &mut impl AllowedRng,
) -> (Self, Blinding) {
let blinding = Blinding::rand(rng);
(
Self {
decryption_handles: encryption_keys.iter().map(|pk| pk.0 * blinding.0).collect(),
commitment: PedersenCommitment::new(
&RistrettoScalar::from(message as u64),
&blinding,
),
},
blinding,
)
}
pub fn decrypt(
&self,
index: usize,
decryption_key: &PrivateKey,
table: &HashMap<[u8; RISTRETTO_POINT_BYTE_LENGTH], u16>,
) -> FastCryptoResult<u32> {
self.ciphertext(index)?.decrypt(decryption_key, table)
}
pub fn ciphertext(&self, index: usize) -> FastCryptoResult<Ciphertext> {
if index >= self.decryption_handles.len() {
return Err(InvalidInput);
}
Ok(Ciphertext {
commitment: self.commitment.clone(),
decryption_handle: self.decryption_handles[index],
})
}
}
impl<const N: usize> KeyConsistencyProof<N> {
pub fn prove(
sender_private_key_limbs: &[u32; N],
sender_public_key: &PublicKey,
recipient_encryption_keys: &[PublicKey],
ciphertexts: &[MultiRecipientCiphertext; N],
blindings: &[Blinding; N],
dst: &[u8],
rng: &mut impl AllowedRng,
) -> Self {
let a: [_; N] = from_fn(|_| RistrettoScalar::rand(rng));
let b: [_; N] = from_fn(|_| RistrettoScalar::rand(rng));
let a1 = a
.iter()
.flat_map(|ai| recipient_encryption_keys.iter().map(move |pk| pk.0 * ai))
.collect_vec();
let a2 = from_fn(|i| *G * a[i] + *H * b[i]);
let a3_per_limb: [RistrettoPoint; N] = from_fn(|i| *G * b[i]);
let base = RistrettoScalar::from(1u64 << 32);
let a3 = RistrettoPoint::multi_scalar_mul(
&iterate(RistrettoScalar::generator(), |e| e * base)
.take(N)
.collect_vec(),
&a3_per_limb,
)
.expect("Consistent lengths");
let c = Self::challenge(
sender_public_key,
recipient_encryption_keys,
ciphertexts,
&a1,
&a2,
&a3,
dst,
);
let z1 = from_fn(|i| a[i] + c * blindings[i].0);
let z2 = from_fn(|i| b[i] + c * RistrettoScalar::from(sender_private_key_limbs[i] as u64));
Self { a1, a2, a3, z1, z2 }
}
pub fn verify(
&self,
sender_public_key: &PublicKey,
recipient_encryption_keys: &[PublicKey],
ciphertexts: &[MultiRecipientCiphertext; N],
dst: &[u8],
rng: &mut impl AllowedRng,
) -> FastCryptoResult<()> {
let c = Self::challenge(
sender_public_key,
recipient_encryption_keys,
ciphertexts,
&self.a1,
&self.a2,
&self.a3,
dst,
);
let m = recipient_encryption_keys.len();
let mu: Vec<RistrettoScalar> = (0..N * m).map(|_| RistrettoScalar::rand(rng)).collect();
let rho: [RistrettoScalar; N] = from_fn(|_| RistrettoScalar::rand(rng));
let alpha = RistrettoScalar::rand(rng);
let beta = RistrettoScalar::rand(rng);
let rho_z1 = RistrettoScalar::inner_product(rho, self.z1);
let rho_z2 = RistrettoScalar::inner_product(rho, self.z2);
let b = RistrettoScalar::from(1u64 << 32);
let z = RistrettoScalar::inner_product(
iterate(RistrettoScalar::generator(), |e| e * b),
self.z2,
);
let mut scalars: Vec<RistrettoScalar> = vec![alpha * rho_z1 + beta * z, alpha * rho_z2];
let mut points: Vec<RistrettoPoint> = vec![*G, *H];
for j in 0..m {
scalars.push(RistrettoScalar::inner_product(
(0..N).map(|i| mu[i * m + j]),
self.z1,
));
points.push(recipient_encryption_keys[j].0);
}
for (i, (a1_chunk, ci)) in self.a1.chunks(m).zip(ciphertexts).enumerate() {
for (j, (a1ij, dij)) in a1_chunk.iter().zip(&ci.decryption_handles).enumerate() {
scalars.push(-mu[i * m + j]);
points.push(*a1ij);
scalars.push(-(c * mu[i * m + j]));
points.push(*dij);
}
}
for ((rhoi, a2i), ci) in rho.iter().zip(self.a2).zip(ciphertexts) {
scalars.push(-(alpha * *rhoi));
points.push(a2i);
scalars.push(-(c * alpha * *rhoi));
points.push(ci.commitment.0);
}
scalars.push(-(beta * c));
points.push(sender_public_key.0);
scalars.push(-beta);
points.push(self.a3);
if RistrettoPoint::multi_scalar_mul(&scalars, &points).expect("Consistent lengths")
!= RistrettoPoint::zero()
{
return Err(InvalidProof);
}
Ok(())
}
fn challenge(
sender_public_key: &PublicKey,
recipient_encryption_keys: &[PublicKey],
ciphertexts: &[MultiRecipientCiphertext; N],
a1: &[RistrettoPoint],
a2: &[RistrettoPoint],
a3: &RistrettoPoint,
dst: &[u8],
) -> RistrettoScalar {
let chunks: Vec<Vec<u8>> = std::iter::once(dst.to_vec())
.chain(std::iter::once(G.to_byte_array().to_vec()))
.chain(std::iter::once(H.to_byte_array().to_vec()))
.chain(std::iter::once(
sender_public_key.0.to_byte_array().to_vec(),
))
.chain(
recipient_encryption_keys
.iter()
.map(|pk| pk.0.to_byte_array().to_vec()),
)
.chain(ciphertexts.iter().flat_map(|ct| {
std::iter::once(ct.commitment.0.to_byte_array().to_vec()).chain(
ct.decryption_handles
.iter()
.map(|dh| dh.to_byte_array().to_vec()),
)
}))
.chain(a1.iter().map(|p| p.to_byte_array().to_vec()))
.chain(a2.iter().map(|p| p.to_byte_array().to_vec()))
.chain(std::iter::once(a3.to_byte_array().to_vec()))
.collect();
RistrettoScalar::fiat_shamir_reduction_to_group_element(
&bcs::to_bytes(&chunks).expect("Serialization succeeds"),
)
}
}
impl<const N: usize> VerifiableKeyEncapsulation<N> {
pub fn batch_seal(
sender_private_key: &PrivateKey,
recipient_encryption_keys: &[PublicKey],
range_proof_dst: &[u8],
consistency_proof_dst: &[u8],
rng: &mut impl AllowedRng,
) -> VerifiableKeyEncapsulation<N> {
let limbs: [u32; N] = sender_private_key
.0
.to_byte_array()
.chunks(4)
.map(|c| u32::from_le_bytes(c.try_into().unwrap()))
.collect_vec()
.try_into()
.unwrap();
let (ciphertexts, blindings): (Vec<_>, Vec<_>) = limbs
.iter()
.map(|&li| MultiRecipientCiphertext::encrypt(recipient_encryption_keys, li, rng))
.unzip();
let ciphertexts: [MultiRecipientCiphertext; N] = ciphertexts.try_into().unwrap();
let range_proof = RangeProof::prove_batch(
&limbs.map(|m| m as u64),
&blindings,
&Range::Bits32,
range_proof_dst,
rng,
)
.unwrap();
let consistency_proof = KeyConsistencyProof::prove(
&limbs,
&PublicKey::from(sender_private_key),
recipient_encryption_keys,
&ciphertexts,
&blindings.try_into().unwrap(),
consistency_proof_dst,
rng,
);
VerifiableKeyEncapsulation {
ciphertexts,
range_proof,
consistency_proof,
}
}
pub fn seal(
sender_private_key: &PrivateKey,
recipient_encryption_key: &PublicKey,
range_proof_dst: &[u8],
consistency_proof_dst: &[u8],
rng: &mut impl AllowedRng,
) -> VerifiableKeyEncapsulation<N> {
Self::batch_seal(
sender_private_key,
std::slice::from_ref(recipient_encryption_key),
range_proof_dst,
consistency_proof_dst,
rng,
)
}
pub fn verify(
&self,
sender_public_key: &PublicKey,
recipient_encryption_keys: &[PublicKey],
range_proof_dst: &[u8],
consistency_proof_dst: &[u8],
rng: &mut impl AllowedRng,
) -> FastCryptoResult<()> {
let commitments = self
.ciphertexts
.iter()
.map(|c| c.commitment.clone())
.collect::<Vec<_>>();
self.range_proof
.verify_batch(&commitments, &Range::Bits32, range_proof_dst, rng)?;
self.consistency_proof.verify(
sender_public_key,
recipient_encryption_keys,
&self.ciphertexts,
consistency_proof_dst,
rng,
)
}
#[allow(clippy::too_many_arguments)]
pub fn open(
&self,
index: usize,
recipient_decryption_key: &PrivateKey,
recipient_public_keys: &[PublicKey],
sender_public_key: &PublicKey,
table: &HashMap<[u8; RISTRETTO_POINT_BYTE_LENGTH], u16>,
range_proof_dst: &[u8],
consistency_proof_dst: &[u8],
rng: &mut impl AllowedRng,
) -> FastCryptoResult<PrivateKey> {
self.verify(
sender_public_key,
recipient_public_keys,
range_proof_dst,
consistency_proof_dst,
rng,
)?;
let limbs = self
.ciphertexts
.iter()
.map(|c| c.decrypt(index, recipient_decryption_key, table))
.collect::<FastCryptoResult<Vec<_>>>()?;
let private_key_bytes = limbs.iter().flat_map(|l| l.to_le_bytes()).collect_vec();
let private_key = RistrettoScalar::from_byte_array(&private_key_bytes.try_into().unwrap())?;
if PublicKey::from(&PrivateKey(private_key)) != *sender_public_key {
return Err(InvalidInput);
}
Ok(PrivateKey(private_key))
}
}
pub fn precompute_table() -> HashMap<[u8; RISTRETTO_POINT_BYTE_LENGTH], u16> {
let step = H.repeated_doubling(16);
iterate(RistrettoPoint::zero(), |p| p + step)
.enumerate()
.map(|(i, p)| (p.to_byte_array(), i as u16))
.take(1 << 16)
.collect()
}
#[test]
fn test_round_trip() {
let (pk, sk) = generate_keypair(&mut rand::thread_rng());
let message = 1234567890u32;
let (ciphertext, _) = Ciphertext::encrypt(&pk, message, &mut rand::thread_rng());
let table = precompute_table();
assert_eq!(ciphertext.decrypt(&sk, &table).unwrap(), message);
}
#[test]
fn test_round_trip_with_consistency_proof() {
let dst = b"test";
let (pk, sk) = generate_keypair(&mut rand::thread_rng());
let message = 1234567890u32;
let (ciphertext, _, proof) =
Ciphertext::encrypt_with_consistency_proof(&pk, message, dst, &mut rand::thread_rng())
.unwrap();
assert!(proof.verify(&ciphertext, &pk, dst).is_ok());
assert!(proof.verify(&ciphertext, &pk, b"other").is_err());
let (other_pk, _) = generate_keypair(&mut rand::thread_rng());
assert!(proof.verify(&ciphertext, &other_pk, dst).is_err());
let table = precompute_table();
assert_eq!(ciphertext.decrypt(&sk, &table).unwrap(), message);
}
#[test]
fn encrypt_and_range_proof() {
let value = 1234u32;
let range = crate::bulletproofs::Range::Bits32;
let mut rng = rand::thread_rng();
let (pk, sk) = generate_keypair(&mut rng);
let (ciphertext, blinding) = Ciphertext::encrypt(&pk, value, &mut rng);
let range_proof =
crate::bulletproofs::RangeProof::prove(value as u64, &blinding, &range, b"test", &mut rng)
.unwrap();
assert!(range_proof
.verify(&ciphertext.commitment, &range, b"test", &mut rng)
.is_ok());
assert_eq!(ciphertext.decrypt(&sk, &precompute_table()).unwrap(), value);
}
#[test]
fn linear_encryptions() {
let value_1 = 12u32;
let value_2 = 34u32;
let s = 7u32;
let (pk, sk) = generate_keypair(&mut rand::thread_rng());
let (ciphertext_1, _) = Ciphertext::encrypt(&pk, value_1, &mut rand::thread_rng());
let (ciphertext_2, _) = Ciphertext::encrypt(&pk, value_2, &mut rand::thread_rng());
let ciphertext_3 = ciphertext_1 + ciphertext_2 * RistrettoScalar::from(s as u64);
assert_eq!(
ciphertext_3.decrypt(&sk, &precompute_table()).unwrap(),
value_1 + value_2 * s
);
}
#[test]
fn test_key_consistency_proof() {
const N: usize = 8;
let mut rng = rand::thread_rng();
let (pk_snd, sk_snd) = generate_keypair(&mut rng);
let (pk_rcv, _sk_rcv) = generate_keypair(&mut rng);
let sk_bytes = sk_snd.0.to_byte_array();
let limbs: [u32; N] = std::array::from_fn(|i| {
u32::from_le_bytes(sk_bytes[4 * i..4 * (i + 1)].try_into().unwrap())
});
let (ciphertexts, blindings): (Vec<_>, Vec<_>) = limbs
.iter()
.map(|&limb| {
MultiRecipientCiphertext::encrypt(std::slice::from_ref(&pk_rcv), limb, &mut rng)
})
.unzip();
let ciphertexts: [MultiRecipientCiphertext; N] = ciphertexts.try_into().unwrap();
let blindings: [Blinding; N] = blindings.try_into().unwrap();
let dst = b"test";
let proof = KeyConsistencyProof::<N>::prove(
&limbs,
&pk_snd,
std::slice::from_ref(&pk_rcv),
&ciphertexts,
&blindings,
dst,
&mut rng,
);
assert!(proof
.verify(
&pk_snd,
std::slice::from_ref(&pk_rcv),
&ciphertexts,
dst,
&mut rng
)
.is_ok());
assert!(proof
.verify(
&pk_snd,
std::slice::from_ref(&pk_rcv),
&ciphertexts,
b"other",
&mut rng
)
.is_err());
let (other_pk_snd, _) = generate_keypair(&mut rng);
assert!(proof
.verify(&other_pk_snd, &[pk_rcv], &ciphertexts, dst, &mut rng)
.is_err());
}
#[test]
fn test_verifiable_key_encapsulation() {
const N: usize = 8;
let range_dst = b"range";
let consistency_dst = b"consistency";
let mut rng = rand::thread_rng();
let table = precompute_table();
let (pk_snd, sk_snd) = generate_keypair(&mut rng);
let (pk_rcv_0, sk_rcv_0) = generate_keypair(&mut rng);
let (pk_rcv_1, sk_rcv_1) = generate_keypair(&mut rng);
let (pk_rcv_2, sk_rcv_2) = generate_keypair(&mut rng);
let recipient_keys = [pk_rcv_0.clone(), pk_rcv_1.clone(), pk_rcv_2.clone()];
let encapsulation = VerifiableKeyEncapsulation::<N>::batch_seal(
&sk_snd,
&recipient_keys,
range_dst,
consistency_dst,
&mut rng,
);
assert!(encapsulation
.verify(
&pk_snd,
&recipient_keys,
range_dst,
consistency_dst,
&mut rng
)
.is_ok());
assert!(encapsulation
.verify(
&pk_snd,
&recipient_keys,
b"other",
consistency_dst,
&mut rng
)
.is_err());
assert!(encapsulation
.verify(&pk_snd, &recipient_keys, range_dst, b"other", &mut rng)
.is_err());
let (other_pk, _) = generate_keypair(&mut rng);
assert!(encapsulation
.verify(
&other_pk,
&recipient_keys,
range_dst,
consistency_dst,
&mut rng
)
.is_err());
let recovered_0 = encapsulation
.open(
0,
&sk_rcv_0,
&recipient_keys,
&pk_snd,
&table,
range_dst,
consistency_dst,
&mut rng,
)
.unwrap();
let recovered_1 = encapsulation
.open(
1,
&sk_rcv_1,
&recipient_keys,
&pk_snd,
&table,
range_dst,
consistency_dst,
&mut rng,
)
.unwrap();
let recovered_2 = encapsulation
.open(
2,
&sk_rcv_2,
&recipient_keys,
&pk_snd,
&table,
range_dst,
consistency_dst,
&mut rng,
)
.unwrap();
assert_eq!(recovered_0.0, sk_snd.0);
assert_eq!(recovered_1.0, sk_snd.0);
assert_eq!(recovered_2.0, sk_snd.0);
assert!(encapsulation
.open(
1,
&sk_rcv_0,
&recipient_keys,
&pk_snd,
&table,
range_dst,
consistency_dst,
&mut rng
)
.is_err());
}
#[test]
fn test_fiat_shamir_regression() {
use crate::encoding::{Encoding, Hex};
let challenge_input = (
"Accept the challenges",
"so that you can feel the exhilaration of victory",
7u64,
);
let expected = RistrettoScalar::from_byte_array(
&Hex::decode("1e8fa9e453ab773a8ac1dd02d9602f45962c3d2061c543d7b9a33de8f51c4000")
.unwrap()
.try_into()
.unwrap(),
)
.unwrap();
let msg = bcs::to_bytes(&challenge_input).unwrap();
let actual = RistrettoScalar::fiat_shamir_reduction_to_group_element(
&bcs::to_bytes(&vec![b"".to_vec(), msg.clone()]).unwrap(),
);
assert_eq!(actual, expected);
assert_ne!(
RistrettoScalar::fiat_shamir_reduction_to_group_element(
&bcs::to_bytes(&vec![b"dst".to_vec(), msg.clone()]).unwrap(),
),
expected
);
}