arnaucube
arnaucube/plonky2-crypto-gadgetsPublic Self-hosted repository
Add poseidon encryption (rust native & plonky2 circuits) (#8)

* implment poseidon-cipher https://drive.google.com/file/d/1EVrP3DzoGbmzkRmYnyEDcIQcXVU7GlOd/view natively

* implement plonky2 circuit for poseidon-cipher

* aes: circuit_gcm.rs group new_targets & build_circuit into just build for easier usage

* Modify decryption and state hash

* adapt circuit to new hash_state logic

* extend nonce from F to [F;2] (<2^128)

* two128 constant

* replace old nonce & L approach by [n[0], n[1], L, 0, 0] to bind nonce&L into the initial state

---------

Co-authored-by: Ahmad <root@ahmadafuni.com>
arnaucube git@arnaucube.com
Wed, 25 Feb 2026 15:02:25 +0100
commit

89e89451149d8d9d234ca78db4f8d05604be3221

parent

336cc02ae2edfe3469d9294472d4b60e23609171

M Cargo.toml → Cargo.toml

@@ -2,7 +2,8 @@ [workspace]

members = [ "aes-gcm", "ecgfp5", - "feistel" + "feistel", + "poseidon-cipher" ] resolver = "2"

@@ -10,3 +11,4 @@

[workspace.dependencies] plonky2 = { git = "https://github.com/0xPARC/plonky2.git", rev = "109d517d09c210ae4c2cee381d3e3fbc04aa3812" } anyhow = "1.0.56" +pod2 = { git = "https://github.com/0xPARC/pod2", rev = "cdf227e3530247e07e375daed007cae7d443b4ae" }
M README.md → README.md

@@ -1,6 +1,6 @@

# plonky2-crypto-gadgets -> Warning: this code is unaudited. +> Warning: unaudited code. - [aes-gcm](/aes-gcm): Plonky2 circuits for AES & AES-GCM encryption system as described at - AES: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf

@@ -8,4 +8,5 @@ - AES-GCM (Galois Counter Mode): https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf

- [ecgfp5](/ecgfp5): Plonky2 circuits for ElGamal and (a finite field analogue of) hashed ElGamal cryptosystems as described at - ElGamal: §1.4.3 of Menezes - *Elliptic Curve Public Key Cryptosystems* (1993) - hashed ElGamal: Abdalla, Bellare & Rogaway - *The oracle Diffie-Hellman assumptions and an analysis of DHIES* (2001) -- [feistel](/feistel): a Plonky2 library for a finite field analogue of the Feistel cipher as described [here](https://en.wikipedia.org/wiki/Feistel_cipher). +- [feistel](/feistel): Plonky2 library for a finite field analogue of the Feistel cipher as described [here](https://en.wikipedia.org/wiki/Feistel_cipher). +- [poseidon-cipher](/poseidon-cipher): Plonky2 library & circuits for Poseidon encryption as described at [*"Encryption with Poseidon"* by Dmitry Khovratovich (2019)](https://drive.google.com/file/d/1EVrP3DzoGbmzkRmYnyEDcIQcXVU7GlOd/view).
M aes-gcm/examples/aes_gcm_128.rs → aes-gcm/examples/aes_gcm_128.rs

@@ -35,8 +35,7 @@

// circuit declaration let config = CircuitConfig::standard_recursion_zk_config(); let mut builder = CircuitBuilder::<F, D>::new(config); - let aes_targets = AesGcm128Target::<L>::new_virtual(&mut builder); - aes_targets.build_circuit(&mut builder); + let aes_targets = AesGcm128Target::<L>::build(&mut builder); println!( "AES-GCM-128 circuit (L:{}) num_gates: {}",
M aes-gcm/examples/aes_gcm_256.rs → aes-gcm/examples/aes_gcm_256.rs

@@ -35,8 +35,7 @@

// circuit declaration let config = CircuitConfig::standard_recursion_zk_config(); let mut builder = CircuitBuilder::<F, D>::new(config); - let aes_targets = AesGcm256Target::<L>::new_virtual(&mut builder); - aes_targets.build_circuit(&mut builder); + let aes_targets = AesGcm256Target::<L>::build(&mut builder); println!( "AES-GCM-256 circuit (L:{}) num_gates: {}",
M aes-gcm/src/circuit_gcm.rs → aes-gcm/src/circuit_gcm.rs

@@ -46,7 +46,8 @@ where

[(); NK * NB]:, [(); 4 * (NR + 1)]:, { - pub fn new_virtual(builder: &mut CircuitBuilder<F, D>) -> Self { + pub fn build(builder: &mut CircuitBuilder<F, D>) -> Self { + // add targets let key: [ByteTarget; NK * NB] = array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); let nonce: [ByteTarget; 12] =

@@ -54,27 +55,15 @@ array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe()));

let pt: [ByteTarget; L] = array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); - let ct: [ByteTarget; L] = - array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); let tag: [ByteTarget; TAG_LEN / 8] = array::from_fn(|_| array::from_fn(|_| builder.add_virtual_bool_target_safe())); - Self { - key, - nonce, - pt, - ct, - tag, - } - } - - pub fn build_circuit(&self, builder: &mut CircuitBuilder<F, D>) { let sbox_lut = sbox_lut(builder); let mix_matrix = state_mix_matrix_bits(builder); let a: &[ByteTarget] = &[]; // additional authenticated data - let expanded_key = builder.key_expansion::<NK, NB, NR>(sbox_lut, self.key); + let expanded_key = builder.key_expansion::<NK, NB, NR>(sbox_lut, key); let empty_state = builder.empty_state();

@@ -93,10 +82,10 @@ } else {

builder._false() } }); - let j0: [ByteTarget; 16] = if self.nonce.len() * 8 == 96 { + let j0: [ByteTarget; 16] = if nonce.len() * 8 == 96 { // J_0 = IV || 0^31 || 1 let mut out = [zero_byte; 16]; - out[..12].copy_from_slice(self.nonce.as_slice()); + out[..12].copy_from_slice(nonce.as_slice()); out[12..16].copy_from_slice(&[zero_byte, zero_byte, zero_byte, one_byte]); out } else {

@@ -105,27 +94,18 @@ };

// 3. C=GCTR() let inc32_j0 = inc32_target(builder, j0); - let c = gctr_target::<NR, L>( - builder, - sbox_lut, - mix_matrix, - expanded_key, - &inc32_j0, - &self.pt, - ); - - // connect the ct value to the external ct value - #[allow(clippy::needless_range_loop)] - for byte in 0..L { - for bit in 0..8 { - builder.connect(self.ct[byte][bit].target, c[byte][bit].target); - } - } + let ct = gctr_target::<NR, L>(builder, sbox_lut, mix_matrix, expanded_key, &inc32_j0, &pt); // the rest of the logic is for the tag; for some use cases we might skip it // (saving a notable amount of gates, about double) if !TAG { - return; + return Self { + key, + nonce, + pt, + ct, + tag, + }; } // 4. u, v

@@ -134,7 +114,7 @@ let v: usize = 16 * (a.len() as f64 / 16_f64).ceil() as usize - a.len();

// 5. S = GHASH() let const_a_len_u8: [u8; 8] = (a.len() * 8).to_be_bytes(); // const - let const_c_len_u8: [u8; 8] = (c.len() * 8).to_be_bytes(); // const + let const_c_len_u8: [u8; 8] = (ct.len() * 8).to_be_bytes(); // const let a_len: [ByteTarget; 8] = array::from_fn(|byte_i| { let const_bits = le_bits_from_byte(const_a_len_u8[byte_i]); let byte: ByteTarget = array::from_fn(|bit_i| {

@@ -154,7 +134,7 @@ });

let ghash_input_vec = [ a.to_vec(), vec![zero_byte; v], - c.to_vec(), + ct.to_vec(), vec![zero_byte; u], a_len.to_vec(), c_len.to_vec(),

@@ -165,15 +145,20 @@

// 6. T=MSB(GCTR())) let msb_input = gctr_target(builder, sbox_lut, mix_matrix, expanded_key, &j0, &s); let t_vec = msb_t_target(builder, TAG_LEN / 8, &msb_input); - let mut t: [ByteTarget; TAG_LEN / 8] = [zero_byte; TAG_LEN / 8]; - t.copy_from_slice(&t_vec); - // connect the computed tag value to the external tag value #[allow(clippy::needless_range_loop)] for byte in 0..TAG_LEN / 8 { for bit in 0..8 { - builder.connect(self.tag[byte][bit].target, t[byte][bit].target); + builder.connect(tag[byte][bit].target, t_vec[byte][bit].target); } + } + + Self { + key, + nonce, + pt, + ct, + tag, } }

@@ -203,7 +188,14 @@ std::iter::zip(self.nonce, nonce).try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?;

std::iter::zip(self.pt, pt_arr).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; std::iter::zip(self.ct, ct_arr).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; - std::iter::zip(self.tag, tag_arr).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + if TAG { + std::iter::zip(self.tag, tag_arr) + .try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + } else { + self.tag + .iter() + .try_for_each(|t| pw.set_byte_array_target(*t, 0u8))?; + } Ok(()) } }

@@ -643,6 +635,7 @@ test_encrypt_opt::<4, 4, 10, 17, false>(false)?;

// AES-GCM-256 test_encrypt_opt::<8, 4, 14, 13, false>(false)?; + test_encrypt_opt::<8, 4, 14, 13, true>(false)?; // with tag Ok(()) }

@@ -699,8 +692,7 @@ // Circuit declaration

let config = CircuitConfig::standard_recursion_config(); let mut builder = CircuitBuilder::<F, D>::new(config); - let aes_targets = AesGcmTarget::<NK, NB, NR, L, TAG>::new_virtual(&mut builder); - aes_targets.build_circuit(&mut builder); + let aes_targets = AesGcmTarget::<NK, NB, NR, L, TAG>::build(&mut builder); println!( "encrypt (NK:{}, NB:{}, NR:{}, L:{}, TAG:{}) num_gates: {}",
M ecgfp5/Cargo.toml → ecgfp5/Cargo.toml

@@ -7,6 +7,6 @@ [dependencies]

plonky2 = { workspace = true } anyhow = { workspace = true } num = { version = "0.4.3", features = ["num-bigint"] } -pod2 = { git = "https://github.com/0xPARC/pod2", rev = "cdf227e3530247e07e375daed007cae7d443b4ae" } +pod2 = { workspace = true } # for ecgfp5 num-bigint = { version = "0.4.6", features = ["rand"] } -rand = "0.8.5"+rand = "0.8.5"
M ecgfp5/src/circuit.rs → ecgfp5/src/circuit.rs

@@ -1,3 +1,5 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + use plonky2::{ field::extension::Extendable, hash::hash_types::RichField, iop::witness::PartialWitness, plonk::circuit_builder::CircuitBuilder,
M ecgfp5/src/elgamal.rs → ecgfp5/src/elgamal.rs

@@ -1,3 +1,5 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + use num::bigint::BigUint; use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point};
M ecgfp5/src/elgamal/circuit.rs → ecgfp5/src/elgamal/circuit.rs

@@ -1,3 +1,5 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + use plonky2::{ field::extension::Extendable, hash::hash_types::RichField, plonk::circuit_builder::CircuitBuilder,
M ecgfp5/src/hashed_elgamal.rs → ecgfp5/src/hashed_elgamal.rs

@@ -1,3 +1,5 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + use std::array; use num::bigint::BigUint;
M ecgfp5/src/hashed_elgamal/circuit.rs → ecgfp5/src/hashed_elgamal/circuit.rs

@@ -1,3 +1,5 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + use std::array; use plonky2::{
M ecgfp5/src/lib.rs → ecgfp5/src/lib.rs

@@ -1,3 +1,5 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + use std::array; use num::BigUint;
M feistel/src/circuit.rs → feistel/src/circuit.rs

@@ -1,3 +1,5 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + use std::array; use plonky2::{
M feistel/src/lib.rs → feistel/src/lib.rs

@@ -1,3 +1,5 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + #![allow(incomplete_features)] #![feature(generic_const_exprs)]
A poseidon-cipher/Cargo.toml

@@ -0,0 +1,14 @@

+[package] +name = "plonky2-poseidon-cipher" +version = "0.1.0" +edition = "2024" + +[dependencies] +plonky2 = { workspace = true } +anyhow = { workspace = true } +num = { version = "0.4.3", features = ["num-bigint"] } +pod2 = { workspace = true } # for ecgfp5 +num-bigint = { version = "0.4.6", features = ["rand"] } +rand = "0.8.5" + +[dev-dependencies]
A poseidon-cipher/src/circuit.rs

@@ -0,0 +1,191 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE +//! +//! Implements the Plonky2 circuits for Poseidon encryption as described at +//! https://drive.google.com/file/d/1EVrP3DzoGbmzkRmYnyEDcIQcXVU7GlOd/view . + +use std::array; + +use anyhow::Result; +use plonky2::{ + field::{ + extension::quintic::QuinticExtension, + goldilocks_field::GoldilocksField as F, + types::{Field, Field64}, + }, + hash::poseidon::PoseidonHash, + iop::{ + target::Target, + witness::{PartialWitness, WitnessWrite}, + }, + plonk::circuit_builder::CircuitBuilder, +}; +use pod2::backends::plonky2::{ + basetypes::D, + primitives::ec::{ + curve::{CircuitBuilderElliptic, Point, PointTarget, WitnessWriteCurve}, + field::{CircuitBuilderNNF, OEFTarget}, + }, +}; + +use crate::Fq; + +// Fq Target +type FqT = OEFTarget<5, QuinticExtension<F>>; + +pub struct PoseidonEncryptTarget<const L: usize> +where + [(); L + 1]:, +{ + ks: PointTarget, + m: [FqT; L], + nonce: [Target; 2], + ct: [FqT; L + 1], +} + +impl<const L: usize> PoseidonEncryptTarget<L> +where + [(); L + 1]:, +{ + pub fn build(builder: &mut CircuitBuilder<F, D>) -> Self { + assert!(L.is_multiple_of(3)); + assert!(L < F::ORDER as usize); + let fqt_zero = const_fqt_zero(builder); + + // add targets + let ks: PointTarget = builder.add_virtual_point_target(); + let m: [FqT; L] = array::from_fn(|_| builder.add_virtual_nnf_target()); + let nonce: [Target; 2] = builder.add_virtual_target_arr::<2>(); + let mut ct: [FqT; L + 1] = array::from_fn(|_| fqt_zero.clone()); + + // build the circuit logic + let f_zero = builder.constant(F::ZERO); + let n_l = FqT::new([ + nonce[0], + nonce[1], + builder.constant(F::from_canonical_u64(L as u64)), + f_zero, + f_zero, + ]); + let fq_zero: FqT = FqT::new([f_zero, f_zero, f_zero, f_zero, f_zero]); + let mut s: [FqT; 4] = [fq_zero, ks.x.clone(), ks.u.clone(), n_l]; + + for i in 0..L / 3 { + s = hash_state_target(builder, s); + + // absorb + s[1] = builder.nnf_add(&s[1], &m[i * 3]); + s[2] = builder.nnf_add(&s[2], &m[i * 3 + 1]); + s[3] = builder.nnf_add(&s[3], &m[i * 3 + 2]); + + // release + ct[i * 3] = s[1].clone(); + ct[i * 3 + 1] = s[2].clone(); + ct[i * 3 + 2] = s[3].clone(); + } + s = hash_state_target(builder, s); + ct[L] = s[1].clone(); + + Self { ks, m, nonce, ct } + } + + pub fn set_targets( + &self, + pw: &mut PartialWitness<F>, + ks: Point, + m: &[Fq], + nonce: [F; 2], + ct: &[Fq], + ) -> Result<()> { + assert_eq!(m.len(), L); + assert_eq!(m.len() + 1, ct.len()); + + pw.set_point_target(&self.ks, &ks)?; + #[allow(clippy::needless_range_loop)] + for i in 0..L { + for j in 0..5 { + pw.set_target(self.m[i].components[j], m[i].0[j])?; + } + } + pw.set_target_arr(&self.nonce, &nonce)?; + #[allow(clippy::needless_range_loop)] + for i in 0..L + 1 { + for j in 0..5 { + pw.set_target(self.ct[i].components[j], ct[i].0[j])?; + } + } + + Ok(()) + } +} + +fn hash_state_target(builder: &mut CircuitBuilder<F, D>, s: [FqT; 4]) -> [FqT; 4] { + let elems: [Target; 4 * 5] = array::from_fn(|i| s[i / 5].components[i % 5]); + let h = builder.hash_n_to_m_no_pad::<PoseidonHash>(elems.to_vec(), 4 * 5); + array::from_fn(|i| FqT::new(array::from_fn::<_, 5, _>(|j| h[j + 5 * i]))) +} + +fn const_fqt_from_fq(builder: &mut CircuitBuilder<F, D>, fq: Fq) -> FqT { + let t: [Target; 5] = array::from_fn(|i| builder.constant(fq.0[i])); + FqT::new(t) +} +fn const_fqt_from_f(builder: &mut CircuitBuilder<F, D>, f: F) -> FqT { + let v = builder.constant(f); + let zero = builder.constant(F::ZERO); + FqT::new([v, zero, zero, zero, zero]) +} +fn const_fqt_zero(builder: &mut CircuitBuilder<F, D>) -> FqT { + let zero = builder.constant(F::ZERO); + FqT::new([zero, zero, zero, zero, zero]) +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use plonky2::{ + field::{goldilocks_field::GoldilocksField as F, types::Sample}, + iop::witness::PartialWitness, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + + use super::*; + + #[test] + fn test_encrypt() -> Result<()> { + let mut rng = rand::thread_rng(); + const L: usize = 129; // (multiple of 3) + + let (_k, K) = crate::new_key(); + let ks = crate::expanded_key(K); + let nonce: [F; 2] = [F::rand(), F::rand()]; + let msg: Vec<Fq> = (0..L).map(|_| Fq::sample(&mut rng)).collect(); + let l = msg.len(); + + let ct = crate::encrypt(ks, &msg, nonce); + + let m = crate::decrypt(ks, &ct, nonce, l); + assert_eq!(m, msg); + + // circuit declaration + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let cipher_targets = PoseidonEncryptTarget::<L>::build(&mut builder); + + println!( + "PoseidonEncrypt circuit (L:{}) num_gates: {}", + L, + builder.num_gates() + ); + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + cipher_targets.set_targets(&mut pw, ks, &msg, nonce, &ct)?; + + let proof = data.prove(pw)?; + data.verify(proof) + } +}
A poseidon-cipher/src/lib.rs

@@ -0,0 +1,153 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE +//! +//! Implements Poseidon encryption as described at +//! https://drive.google.com/file/d/1EVrP3DzoGbmzkRmYnyEDcIQcXVU7GlOd/view . + +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] +#![allow(dead_code)] // TMP +#![allow(non_snake_case)] + +use std::array; + +use num::bigint::BigUint; +use num_bigint::RandBigInt; +use plonky2::{ + field::{ + extension::{FieldExtension, quintic::QuinticExtension}, + goldilocks_field::GoldilocksField as F, + types::{Field, Field64}, + }, + hash::{hashing::hash_n_to_m_no_pad, poseidon::PoseidonPermutation}, +}; +use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point}; +use rand::rngs::OsRng; + +pub mod circuit; + +// ECField +type Fq = QuinticExtension<F>; + +pub fn new_key() -> (BigUint, Point) { + let k: BigUint = OsRng.gen_biguint_below(&GROUP_ORDER); + let K = &k * Point::generator(); + (k, K) +} +pub fn expanded_key(K: Point) -> Point { + let r: BigUint = OsRng.gen_biguint_below(&GROUP_ORDER); + &r * K +} + +pub fn encrypt(ks: Point, msg: &[Fq], nonce: [F; 2]) -> Vec<Fq> { + let mut m = msg.to_vec(); // pad m + m.resize(m.len().next_multiple_of(3), Fq::ZERO); + assert!(m.len() < F::ORDER as usize); + + // bind the msg length and nonce into the initial state + let n_l = Fq::from_basefield_array([ + nonce[0], + nonce[1], + F::from_canonical_u64(msg.len() as u64), + F::ZERO, + F::ZERO, + ]); + let mut s: [Fq; 4] = [Fq::ZERO, ks.x, ks.u, n_l]; + + let mut ct: Vec<Fq> = vec![Fq::ZERO; m.len() + 1]; + for i in 0..m.len() / 3 { + s = hash_state(s); + + // absorb + s[1] += m[i * 3]; + s[2] += m[i * 3 + 1]; + s[3] += m[i * 3 + 2]; + + // release + ct[i * 3] = s[1]; + ct[i * 3 + 1] = s[2]; + ct[i * 3 + 2] = s[3]; + } + s = hash_state(s); + ct[m.len()] = s[1]; + ct +} + +pub fn decrypt(ks: Point, ct: &[Fq], nonce: [F; 2], l: usize) -> Vec<Fq> { + let n_l = Fq::from_basefield_array([ + nonce[0], + nonce[1], + F::from_canonical_u64(l as u64), + F::ZERO, + F::ZERO, + ]); + let mut s: [Fq; 4] = [Fq::ZERO, ks.x, ks.u, n_l]; + + let mut m: Vec<Fq> = vec![Fq::ZERO; ct.len() - 1]; + for i in 0..ct.len() / 3 { + s = hash_state(s); + + // release + m[3 * i] = ct[3 * i] - s[1]; + m[3 * i + 1] = ct[3 * i + 1] - s[2]; + m[3 * i + 2] = ct[3 * i + 2] - s[3]; + + // modify state + s[1] = ct[3 * i]; + s[2] = ct[3 * i + 1]; + s[3] = ct[3 * i + 2]; + } + + if l > 3 { + if l % 3 == 2 { + assert_eq!(m[m.len() - 1], Fq::ZERO); + } else if l % 3 == 1 { + assert_eq!(m[m.len() - 1], Fq::ZERO); + assert_eq!(m[m.len() - 2], Fq::ZERO); + } + } + s = hash_state(s); + assert_eq!(ct[ct.len() - 1], s[1]); + m[0..l].to_vec() +} + +fn hash_state(s: [Fq; 4]) -> [Fq; 4] { + let elems: [F; 4 * 5] = array::from_fn(|i| s[i / 5].0[i % 5]); + // Note: here we're using plonky2's normal poseidon, not duplex-sponge + let h = hash_n_to_m_no_pad::<F, PoseidonPermutation<_>>(&elems, 4 * 5); + array::from_fn(|i| Fq::from_basefield_array(array::from_fn::<_, 5, _>(|j| h[j + 5 * i]))) +} + +#[cfg(test)] +mod tests { + use plonky2::field::types::Sample; + + use super::*; + + #[test] + fn test_encrypt_decrypt() { + test_encrypt_decrypt_op(9); + test_encrypt_decrypt_op(10); + test_encrypt_decrypt_op(11); + test_encrypt_decrypt_op(12); + test_encrypt_decrypt_op(128); + test_encrypt_decrypt_op(129); + test_encrypt_decrypt_op(1024); + test_encrypt_decrypt_op(1023); + test_encrypt_decrypt_op(1025); + } + fn test_encrypt_decrypt_op(msg_len: usize) { + let mut rng = rand::thread_rng(); + + let (_k, K) = new_key(); + let ks = expanded_key(K); + let nonce: [F; 2] = [F::rand(), F::rand()]; + let msg: Vec<Fq> = (0..msg_len).map(|_| Fq::sample(&mut rng)).collect(); + let l = msg.len(); + + let ct = encrypt(ks, &msg, nonce); + + let m = decrypt(ks, &ct, nonce, l); + assert_eq!(m, msg); + assert_ne!(&msg, &ct[..msg_len]); + } +}