arnaucube
arnaucube/plonky2-crypto-gadgetsPublic Self-hosted repository
Merge branch 'main' into aes-xor-lookup
Ahmad root@ahmadafuni.com
Fri, 27 Feb 2026 02:16:40 +1000
commit

8649703f2365e2096a939eb071c22ccab00be6a9

parent

57cb5a4c921cc9a0be0d14a853596b97cfad164d

M Cargo.toml → Cargo.toml

@@ -1,8 +1,14 @@

[workspace] members = [ - "aes-gcm" + "aes-gcm", + "ecgfp5", + "feistel", + "poseidon-cipher" ] resolver = "2" [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,5 +1,12 @@

# plonky2-crypto-gadgets +> 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 - 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): 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/Cargo.toml → aes-gcm/Cargo.toml

@@ -4,8 +4,8 @@ version = "0.1.0"

edition = "2024" [dependencies] -plonky2 = { git = "https://github.com/0xPARC/plonky2.git", rev = "109d517d09c210ae4c2cee381d3e3fbc04aa3812" } -anyhow = "1.0.56" +plonky2 = { workspace = true } +anyhow = { workspace = true } [dev-dependencies] rand = "0.10.0"
A aes-gcm/README.md

@@ -0,0 +1,25 @@

+# plonky2-aes + +- [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 + - AES-GCM (Galois Counter Mode): https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf + + +> Warning: this code is unaudited. + +Full example of usage at [examples](https://github.com/0xPARC/plonky2-aes/blob/main/examples) directory. +```rust +// 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 data = builder.build::<PoseidonGoldilocksConfig>(); + +// set values to circuit +let mut pw = PartialWitness::<F>::new(); +aes_targets.set_targets(&mut pw, key, nonce, pt, ct, tag)?; + +let proof = data.prove(pw)?; +data.verify(proof) +```
A aes-gcm/examples/aes_gcm_128.rs

@@ -0,0 +1,54 @@

+//! Run: `cargo run --release --example aes_gcm_128` +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] + +use anyhow::Result; +use plonky2::{ + field::goldilocks_field::GoldilocksField as F, + iop::witness::PartialWitness, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, +}; +use plonky2_aes::{AesGcm128Target, D, KEY_LEN_128}; + +fn main() -> Result<()> { + // max size (bytes) of plaintext supported by the instantiation of the circuit + const L: usize = 42; + + let key: &[u8; KEY_LEN_128] = &[123; KEY_LEN_128]; + let nonce: &[u8; 12] = &[111; 12]; + let pt: &[u8; L] = &[231u8; L]; // plaintext + + // use external rust library to compute the ciphertext & tag + let nonce_ext = aes_gcm::Nonce::from_slice(nonce); + use aes_gcm::{KeyInit, aead::Aead}; // needed traits + let cipher = aes_gcm::Aes128Gcm::new_from_slice(key.as_slice()).unwrap(); + let encrypt_res = cipher.encrypt(nonce_ext, pt.as_ref()).unwrap(); + let ct = &encrypt_res[..L]; // ciphertext + let tag = &encrypt_res[L..]; + + // alternatively we could use this repo's rust implementation (only for tests) + // let (ct, tag) = plonky2_aes::native_gcm::encrypt::<4, 4, 10>(key, nonce, pt); + + // circuit declaration + let config = CircuitConfig::standard_recursion_zk_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + let aes_targets = AesGcm128Target::<L>::build(&mut builder); + + println!( + "AES-GCM-128 circuit (L:{}) num_gates: {}", + L, + builder.num_gates() + ); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + aes_targets.set_targets(&mut pw, key, nonce, pt, ct, tag)?; + + let proof = data.prove(pw)?; + data.verify(proof) +}
A aes-gcm/examples/aes_gcm_256.rs

@@ -0,0 +1,54 @@

+//! Run: `cargo run --release --example aes_gcm_256` +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] + +use anyhow::Result; +use plonky2::{ + field::goldilocks_field::GoldilocksField as F, + iop::witness::PartialWitness, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, +}; +use plonky2_aes::{AesGcm256Target, D, KEY_LEN_256}; + +fn main() -> Result<()> { + // max size (bytes) of plaintext supported by the instantiation of the circuit + const L: usize = 42; + + let key: &[u8; KEY_LEN_256] = &[123; KEY_LEN_256]; + let nonce: &[u8; 12] = &[111; 12]; + let pt: &[u8; L] = &[231u8; L]; // plaintext + + // use external rust library to compute the ciphertext & tag + let nonce_ext = aes_gcm::Nonce::from_slice(nonce); + use aes_gcm::{KeyInit, aead::Aead}; // needed traits + let cipher = aes_gcm::Aes256Gcm::new_from_slice(key.as_slice()).unwrap(); + let encrypt_res = cipher.encrypt(nonce_ext, pt.as_ref()).unwrap(); + let ct = &encrypt_res[..L]; // ciphertext + let tag = &encrypt_res[L..]; + + // alternatively we could use this repo's rust implementation (only for tests) + // let (ct, tag) = plonky2_aes::native_gcm::encrypt::<8, 4, 14>(key, nonce, pt); + + // circuit declaration + let config = CircuitConfig::standard_recursion_zk_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + let aes_targets = AesGcm256Target::<L>::build(&mut builder); + + println!( + "AES-GCM-256 circuit (L:{}) num_gates: {}", + L, + builder.num_gates() + ); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + aes_targets.set_targets(&mut pw, key, nonce, pt, ct, tag)?; + + let proof = data.prove(pw)?; + data.verify(proof) +}
M aes-gcm/src/circuit_aes.rs → aes-gcm/src/circuit_aes.rs

@@ -1,4 +1,7 @@

-//! For LICENSE check out https://github.com/0xPARC/plonky2-aes/blob/main/LICENSE +//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE +//! +//! Plonky2 circuit implementation of +//! [AES](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf). use std::{array, sync::Arc};

@@ -19,10 +22,20 @@ native_aes::{State, gf_2_8_mul, rot_word, shift_rows},

}; #[derive(Debug, Copy, Clone)] -pub struct StateTarget(pub [[ByteArrayTarget; 4]; 4]); +pub struct StateTarget(pub [[ByteTarget; 4]; 4]); + +impl StateTarget { + pub(crate) fn flatten(self) -> [ByteTarget; 16] { + array::from_fn(|i| self.0[i % 4][i / 4]) + } + + pub(crate) fn from_flat(b: [ByteTarget; 16]) -> Self { + StateTarget(array::from_fn(|i| array::from_fn(|j| b[j * 4 + i]))) + } +} -// TODO: Normalise terminology! -pub type ByteArrayTarget = Target; +/// Type alias for byte targets +pub type ByteTarget = Target; pub trait CircuitBuilderAESState<F: RichField + Extendable<D>, const D: usize> { /// Adds state target.

@@ -34,9 +47,9 @@ &mut self,

xor_lut_idx: usize, gf_2_8_mul_lut_idx: usize, sbox_lut_idx: usize, - mix_matrix: [[ByteArrayTarget; 4]; 4], // constant mix_matrix + mix_matrix: [[ByteTarget; 4]; 4], // constant mix_matrix s: StateTarget, - w: [[ByteArrayTarget; 4]; 4 * (NR + 1)], // expanded key + w: [[ByteTarget; 4]; 4 * (NR + 1)], // expanded key ) -> StateTarget { let mut s = s; s = self.state_add_round_key(xor_lut_idx, &w[0..4], s);

@@ -59,18 +72,14 @@ }))

} /// SubWord - fn state_sub_word( - &mut self, - sbox_lut_idx: usize, - word: [ByteArrayTarget; 4], - ) -> [ByteArrayTarget; 4]; + fn state_sub_word(&mut self, sbox_lut_idx: usize, word: [ByteTarget; 4]) -> [ByteTarget; 4]; /// MixColumns fn state_mix_columns( &mut self, xor_lut_idx: usize, gf_2_8_mul_lut_idx: usize, - mix_matrix: [[ByteArrayTarget; 4]; 4], + mix_matrix: [[ByteTarget; 4]; 4], s: StateTarget, ) -> StateTarget { let cols: [_; 4] = array::from_fn(|i| array::from_fn(|j| s.0[j][i]));

@@ -84,7 +93,7 @@ /// AddRoundKey

fn state_add_round_key( &mut self, xor_lut_idx: usize, - round_key: &[[ByteArrayTarget; 4]], + round_key: &[[ByteTarget; 4]], s: StateTarget, ) -> StateTarget { StateTarget(array::from_fn(|i| {

@@ -97,8 +106,8 @@ fn key_expansion<const NK: usize, const NB: usize, const NR: usize>(

&mut self, xor_lut_idx: usize, sbox_lut_idx: usize, - key: [ByteArrayTarget; NK * NB], - ) -> [[ByteArrayTarget; 4]; 4 * (NR + 1)]; + key: [ByteTarget; NK * NB], + ) -> [[ByteTarget; 4]; 4 * (NR + 1)]; /// GF(2^8) addition fn gf_2_8_add(&mut self, xor_lut_idx: usize, x: Target, y: Target) -> Target;

@@ -111,20 +120,25 @@ fn bytearray_ip_bits<const N: usize>(

&mut self, xor_lut_idx: usize, gf_2_8_mul_lut_idx: usize, - x: [ByteArrayTarget; N], - y: [ByteArrayTarget; N], - ) -> ByteArrayTarget; + x: [ByteTarget; N], + y: [ByteTarget; N], + ) -> ByteTarget; /// Bytearray matrix application. fn bytearray_matrix_apply_bits<const M: usize, const N: usize>( &mut self, xor_lut_idx: usize, gf_2_8_mul_lut_idx: usize, - a: [[ByteArrayTarget; N]; M], - x: [ByteArrayTarget; N], - ) -> [ByteArrayTarget; M] { + a: [[ByteTarget; N]; M], + x: [ByteTarget; N], + ) -> [ByteTarget; M] { std::array::from_fn(|i| self.bytearray_ip_bits(xor_lut_idx, gf_2_8_mul_lut_idx, a[i], x)) } + + /// returns a 0u8 in the shape of a ByteTarget. + fn zero_byte(&mut self) -> ByteTarget; + + fn empty_state(&mut self) -> StateTarget; } impl CircuitBuilderAESState<F, D> for CircuitBuilder<F, D> {

@@ -134,11 +148,7 @@ array::from_fn(|_| self.add_virtual_target())

})) } - fn state_sub_word( - &mut self, - sbox_lut_idx: usize, - word: [ByteArrayTarget; 4], - ) -> [ByteArrayTarget; 4] { + fn state_sub_word(&mut self, sbox_lut_idx: usize, word: [ByteTarget; 4]) -> [ByteTarget; 4] { array::from_fn(|i| { let byte_target = word[i];

@@ -150,9 +160,9 @@ fn key_expansion<const NK: usize, const NB: usize, const NR: usize>(

&mut self, xor_lut_idx: usize, sbox_lut_idx: usize, - key: [ByteArrayTarget; NK * NB], - ) -> [[ByteArrayTarget; 4]; 4 * (NR + 1)] { - let rcon: [ByteArrayTarget; 11] = + key: [ByteTarget; NK * NB], + ) -> [[ByteTarget; 4]; 4 * (NR + 1)] { + let rcon: [ByteTarget; 11] = array::from_fn(|i| self.constant(F::from_canonical_u8(RCON[i]))); let key: [[_; 4]; NK] = array::from_fn(|i| array::from_fn(|j| key[4 * i + j]));

@@ -202,23 +212,32 @@ fn bytearray_ip_bits<const N: usize>(

&mut self, xor_lut_idx: usize, gf_2_8_mul_lut_idx: usize, - x: [ByteArrayTarget; N], - y: [ByteArrayTarget; N], - ) -> ByteArrayTarget { + x: [ByteTarget; N], + y: [ByteTarget; N], + ) -> ByteTarget { let zero = self.zero(); std::iter::zip(x, y).fold(zero, |acc, (a, b)| { let prod = self.gf_2_8_mul(gf_2_8_mul_lut_idx, a, b); self.gf_2_8_add(xor_lut_idx, acc, prod) }) } + + fn zero_byte(&mut self) -> ByteTarget { + self.zero() + } + + fn empty_state(&mut self) -> StateTarget { + let zero_byte: ByteTarget = self.zero_byte(); + StateTarget(array::from_fn(|_| array::from_fn(|_| zero_byte))) + } } pub trait PartialWitnessByteArray { - fn set_byte_array_target(&mut self, target: ByteArrayTarget, value: u8) -> anyhow::Result<()>; + fn set_byte_array_target(&mut self, target: ByteTarget, value: u8) -> anyhow::Result<()>; } impl<F: Field> PartialWitnessByteArray for PartialWitness<F> { - fn set_byte_array_target(&mut self, target: ByteArrayTarget, value: u8) -> anyhow::Result<()> { + fn set_byte_array_target(&mut self, target: ByteTarget, value: u8) -> anyhow::Result<()> { self.set_target(target, F::from_canonical_u8(value)) } }

@@ -273,7 +292,7 @@ .collect();

builder.add_lookup_table_from_pairs(Arc::new(gf_2_8_mul_table)) } -pub fn state_mix_matrix_bits(builder: &mut CircuitBuilder<F, D>) -> [[ByteArrayTarget; 4]; 4] { +pub fn state_mix_matrix_bits(builder: &mut CircuitBuilder<F, D>) -> [[ByteTarget; 4]; 4] { let one = builder.one(); let two = builder.two(); let three = builder.constant(F::from_canonical_u64(3));

@@ -289,9 +308,9 @@

pub fn byte_xor( builder: &mut CircuitBuilder<F, D>, xor_lut_idx: usize, - x: ByteArrayTarget, - y: ByteArrayTarget, -) -> ByteArrayTarget { + x: ByteTarget, + y: ByteTarget, +) -> ByteTarget { let lookup_idx = builder.mul_const_add(F::from_canonical_u64(1 << 8), x, y); builder.add_lookup_from_index(lookup_idx, xor_lut_idx) }

@@ -311,7 +330,10 @@ },

}; use rand::RngExt; - use super::*; + use super::{ + ByteTarget, CircuitBuilderAESState, D, PartialWitnessAESState, PartialWitnessByteArray, + byte_xor_lut, gf_2_8_mul_lut, sbox_lut, state_mix_matrix_bits, + }; use crate::native_aes::{State, encrypt_block, key_expansion, mix_columns, sub_bytes}; #[test]

@@ -501,8 +523,7 @@ // Circuit declaration

let config = CircuitConfig::standard_recursion_config(); let mut builder = CircuitBuilder::<F, D>::new(config); - let key_target: [ByteArrayTarget; NK * NB] = - array::from_fn(|_| builder.add_virtual_target()); + let key_target: [ByteTarget; NK * NB] = array::from_fn(|_| builder.add_virtual_target()); let xor_lut_idx = byte_xor_lut(&mut builder); let sbox_lut_idx = sbox_lut(&mut builder); let expanded_key_target =

@@ -585,13 +606,12 @@ // Circuit declaration

let config = CircuitConfig::standard_recursion_config(); let mut builder = CircuitBuilder::<F, D>::new(config); - let key_target: [ByteArrayTarget; NK * NB] = - array::from_fn(|_| builder.add_virtual_target()); + let key_target: [ByteTarget; NK * NB] = array::from_fn(|_| builder.add_virtual_target()); let xor_lut_idx = byte_xor_lut(&mut builder); let gf_2_8_mul_lut_idx = gf_2_8_mul_lut(&mut builder); let sbox_lut_idx = sbox_lut(&mut builder); let mix_matrix = state_mix_matrix_bits(&mut builder); - let expanded_key_target: [[ByteArrayTarget; 4]; 4 * (NR + 1)] = + let expanded_key_target: [[ByteTarget; 4]; 4 * (NR + 1)] = builder.key_expansion::<NK, NB, NR>(xor_lut_idx, sbox_lut_idx, key_target); let input_state_target = builder.add_virtual_state();
A aes-gcm/src/circuit_gcm.rs

@@ -0,0 +1,779 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE +//! +//! Plonky2 circuit implementation of +//! [AES-GCM (Galois Counter Mode)](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf). + +use std::{array, sync::Arc}; + +use anyhow::Result; +use plonky2::{ + field::{goldilocks_field::GoldilocksField as F, types::Field}, + iop::{target::BoolTarget, witness::PartialWitness}, + plonk::circuit_builder::CircuitBuilder, +}; + +use crate::{ + D, + circuit_aes::{ + ByteTarget, CircuitBuilderAESState, PartialWitnessByteArray, StateTarget, byte_xor, + byte_xor_lut, gf_2_8_mul_lut, sbox_lut, state_mix_matrix_bits, + }, + constants::TAG_LEN, +}; + +pub struct AesGcmTarget< + const NK: usize, + const NB: usize, + const NR: usize, + // L: max size of plaintext to cipher. + const L: usize, + const TAG: bool, +> where + [(); NK * NB]:, + [(); 4 * (NR + 1)]:, +{ + key: [ByteTarget; NK * NB], + nonce: [ByteTarget; 12], + pt: [ByteTarget; L], + + ct: [ByteTarget; L], + tag: [ByteTarget; TAG_LEN / 8], +} + +impl<const NK: usize, const NB: usize, const NR: usize, const L: usize, const TAG: bool> + AesGcmTarget<NK, NB, NR, L, TAG> +where + [(); NK * NB]:, + [(); 4 * (NR + 1)]:, +{ + pub fn build(builder: &mut CircuitBuilder<F, D>) -> Self { + // add targets + let key: [ByteTarget; NK * NB] = array::from_fn(|_| builder.add_virtual_target()); + let nonce: [ByteTarget; 12] = array::from_fn(|_| builder.add_virtual_target()); + let pt: [ByteTarget; L] = array::from_fn(|_| builder.add_virtual_target()); + + let tag: [ByteTarget; TAG_LEN / 8] = array::from_fn(|_| builder.add_virtual_target()); + + let sbox_lut = sbox_lut(builder); + let xor_lut = byte_xor_lut(builder); + let gf_2_8_mul_lut = gf_2_8_mul_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>(xor_lut, sbox_lut, key); + + let empty_state = builder.empty_state(); + + // 1. CIPH_K + let h = builder + .encrypt_block::<NR>( + xor_lut, + gf_2_8_mul_lut, + sbox_lut, + mix_matrix, + empty_state, + expanded_key, + ) + .flatten(); + + // 2. J_0 + let zero_byte = builder.zero_byte(); + let one_byte = builder.one(); + 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(nonce.as_slice()); + out[12..16].copy_from_slice(&[zero_byte, zero_byte, zero_byte, one_byte]); + out + } else { + panic!("unsupported at initial version; nonce.len()=12 (96 bits)"); + }; + + // 3. C=GCTR() + let inc32_j0 = inc32_target(builder, j0); + let ct = gctr_target::<NR, L>( + builder, + (xor_lut, gf_2_8_mul_lut, 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 Self { + key, + nonce, + pt, + ct, + tag, + }; + } + + // 4. u, v + let u: usize = 16 * (L as f64 / 16_f64).ceil() as usize - L; // L=x.len() + 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] = (ct.len() * 8).to_be_bytes(); // const + let a_len: [ByteTarget; 8] = + array::from_fn(|byte_i| builder.constant(F::from_canonical_u8(const_a_len_u8[byte_i]))); + let c_len: [ByteTarget; 8] = + array::from_fn(|byte_i| builder.constant(F::from_canonical_u8(const_c_len_u8[byte_i]))); + let ghash_input_vec = [ + a.to_vec(), + vec![zero_byte; v], + ct.to_vec(), + vec![zero_byte; u], + a_len.to_vec(), + c_len.to_vec(), + ] + .concat(); + + let u8_unit_right_shift_lut_idx = u8_unit_right_shift_lut(builder); + let u8_bitref_lut_idx = u8_bitref_lut(builder); + let s = ghash_target( + builder, + xor_lut, + u8_unit_right_shift_lut_idx, + u8_bitref_lut_idx, + h, + ghash_input_vec, + ); + + // 6. T=MSB(GCTR())) + let msb_input = gctr_target( + builder, + (xor_lut, gf_2_8_mul_lut, sbox_lut), + mix_matrix, + expanded_key, + &j0, + &s, + ); + let t_vec = msb_t_target(TAG_LEN / 8, &msb_input); + + std::iter::zip(tag, t_vec).for_each(|(a, b)| builder.connect(a, b)); + + Self { + key, + nonce, + pt, + ct, + tag, + } + } + + pub fn set_targets( + &self, + pw: &mut PartialWitness<F>, + key: &[u8; NK * NB], + nonce: &[u8; 12], + pt: &[u8], + ct: &[u8], + tag: &[u8], + ) -> Result<()> { + assert!(pt.len() <= L); + assert!(ct.len() <= L); + assert!(tag.len() <= TAG_LEN / 8); + + // extend to expected sizes + let mut pt_arr: [u8; L] = [0u8; L]; + pt_arr.copy_from_slice(pt); + let mut ct_arr: [u8; L] = [0u8; L]; + ct_arr.copy_from_slice(ct); + let mut tag_arr: [u8; TAG_LEN / 8] = [0u8; TAG_LEN / 8]; + tag_arr.copy_from_slice(tag); + + std::iter::zip(self.key, key).try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + 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))?; + 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(()) + } +} + +/// L: max size of plaintext to cipher. +fn gctr_target<const NR: usize, const L: usize>( + builder: &mut CircuitBuilder<F, D>, + lut_indices: (usize, usize, usize), + mix_matrix: [[ByteTarget; 4]; 4], + key: [[ByteTarget; 4]; 4 * (NR + 1)], + icb: &[ByteTarget; 16], + x: &[ByteTarget; L], +) -> [ByteTarget; L] { + let (xor_lut_idx, gf_2_8_mul_lut_idx, sbox_lut_idx) = lut_indices; + let n = ((L * 8) as f64 / 128_f64).ceil() as usize; + + let mut y: [ByteTarget; L] = *x; + let mut cb_i = *icb; + + let zero_byte = builder.zero_byte(); + for (i, x_i_raw) in x.chunks(16).enumerate() { + if i > 0 { + cb_i = inc32_target(builder, cb_i); + } + + let l = x_i_raw.len().min(16); + let mut x_i = [zero_byte; 16]; + x_i[..l].copy_from_slice(x_i_raw); + + let ciph_cb_i = builder + .encrypt_block::<NR>( + xor_lut_idx, + gf_2_8_mul_lut_idx, + sbox_lut_idx, + mix_matrix, + StateTarget::from_flat(cb_i), + key, + ) + .flatten(); + + let (y_i, n_bytes) = if i < n && x_i_raw.len() == 16 { + (xor_blocks(builder, xor_lut_idx, x_i, ciph_cb_i), 16) + } else { + // last chunk, might be smaller than 16 bytes (L%16 bytes) + let msb_res = msb_t_target(L % 16, ciph_cb_i.as_slice()); + let l = msb_res.len().min(16); + let mut m: [ByteTarget; 16] = [zero_byte; 16]; + m[..l].copy_from_slice(&msb_res); + (xor_blocks(builder, xor_lut_idx, x_i, m), l) + }; + y[i * 16..i * 16 + n_bytes].clone_from_slice(&y_i[..n_bytes]); + } + y +} + +fn ghash_target( + builder: &mut CircuitBuilder<F, D>, + xor_lut_idx: usize, + u8_unit_right_shift_lut_idx: usize, + u8_bitref_lut_idx: usize, + h: [ByteTarget; 16], + x: Vec<ByteTarget>, +) -> [ByteTarget; 16] { + assert!(x.len().is_multiple_of(16)); // multiple of 128 bits + let m = x.len() / 16; + + let zero_byte = builder.zero_byte(); + let mut y = [zero_byte; 16]; // (128 bits) + for i in 0..m { + let mut xi = [zero_byte; 16]; + xi.clone_from_slice(&x[i * 16..i * 16 + 16]); + let y_xi = xor_blocks(builder, xor_lut_idx, y, xi); + y = gf_2_128_mul_target( + builder, + xor_lut_idx, + u8_unit_right_shift_lut_idx, + u8_bitref_lut_idx, + y_xi, + h, + ); + } + y +} +fn gf_2_128_mul_target( + builder: &mut CircuitBuilder<F, D>, + xor_lut_idx: usize, + u8_unit_right_shift_lut_idx: usize, + u8_bitref_lut_idx: usize, + x: [ByteTarget; 16], + y: [ByteTarget; 16], +) -> [ByteTarget; 16] { + let zero = builder.zero(); + + // R: 10000111 (= 225) || 0^120 (in little-endian) + let r_first = builder.constant(F::from_canonical_u8(225)); + + let mut z = [zero; 16]; + let mut v = y; + for i in 0..128 { + let byte_index = i / 8; + let bit_index = 7 - (i % 8); + let bit_idx_target = builder.constant(F::from_canonical_usize(bit_index)); + let xi = BoolTarget::new_unsafe(u8_bitref( + builder, + u8_bitref_lut_idx, + x[byte_index], + bit_idx_target, + )); + + // set z = if xi==1: z^v, else: z + for b in 0..16 { + let z_xor_v = byte_xor(builder, xor_lut_idx, z[b], v[b]); + z[b] = builder.select(xi, z_xor_v, z[b]); + } + + let lsb = BoolTarget::new_unsafe(u8_bitref(builder, u8_bitref_lut_idx, v[15], zero)); // (little-endian) + + v = right_shift_one_target(builder, u8_unit_right_shift_lut_idx, &v); + + // if lsb==1: v=v^R, else: v + let v_xor_r = byte_xor(builder, xor_lut_idx, v[0], r_first); + v[0] = builder.select(lsb, v_xor_r, v[0]); + } + z +} +pub fn right_shift_one_target( + builder: &mut CircuitBuilder<F, D>, + u8_unit_right_shift_lut_idx: usize, + v: &[ByteTarget; 16], +) -> [ByteTarget; 16] { + let mut r: [ByteTarget; 16] = *v; + + let mut carry = builder.zero(); + for i in 0..16 { + let current = v[i]; + let mut shifted = u8_unit_right_shift(builder, u8_unit_right_shift_lut_idx, current); + let next_carry = + builder.mul_const_add(F::from_canonical_u64(2) * F::NEG_ONE, shifted, current); + + shifted = builder.mul_const_add(F::from_canonical_u64(1 << 7), carry, shifted); + + r[i] = shifted; + carry = next_carry; + } + r +} + +pub fn inc32_target( + builder: &mut CircuitBuilder<F, D>, + block: [ByteTarget; 16], +) -> [ByteTarget; 16] { + let mut r = block; + let zero = builder.zero(); + let u8_max = builder.constant(F::from_canonical_u8(u8::MAX)); + + let mut carry = builder.one(); + for byte_index in (12..16).rev() { + let a = block[byte_index]; + let sum = builder.add(a, carry); + let a_is_u8_max = builder.is_equal(a, u8_max); + let carry_out = builder.mul(carry, a_is_u8_max.target); + r[byte_index] = builder.select(a_is_u8_max, zero, sum); + carry = carry_out; + } + r +} + +fn xor_blocks( + builder: &mut CircuitBuilder<F, D>, + xor_lut_idx: usize, + b1: [ByteTarget; 16], + b2: [ByteTarget; 16], +) -> [ByteTarget; 16] { + array::from_fn(|i| byte_xor(builder, xor_lut_idx, b1[i], b2[i])) +} + +pub fn msb_t_target(t_bytes: usize, block: &[ByteTarget]) -> Vec<ByteTarget> { + // Always dealing with full bytes in circuit. + let full_bytes = t_bytes; + + let mut out: Vec<ByteTarget> = Vec::new(); + for block_i in block.iter().take(full_bytes) { + out.push(*block_i); + } + + out +} + +/// Lookup table for unit right-shift of elements of u8. +pub fn u8_unit_right_shift_lut(builder: &mut CircuitBuilder<F, D>) -> usize { + let u8_unit_right_shift_table: Vec<(u16, u16)> = (0..=u8::MAX as usize) + .map(|x| (x as u16, (x as u16) >> 1)) + .collect(); + builder.add_lookup_table_from_pairs(Arc::new(u8_unit_right_shift_table)) +} +pub fn u8_unit_right_shift( + builder: &mut CircuitBuilder<F, D>, + u8_unit_right_shift_lut_idx: usize, + x: ByteTarget, +) -> ByteTarget { + builder.add_lookup_from_index(x, u8_unit_right_shift_lut_idx) +} + +/// Lookup table for u8 bit referencing. +pub fn u8_bitref_lut(builder: &mut CircuitBuilder<F, D>) -> usize { + let u8_bitref_table: Vec<(u16, u16)> = (0..=u8::MAX as usize) + .flat_map(|x| { + (0..8) + .map(|i| (((x as u16) << 3) + i as u16, ((x as u16) >> i) & 1)) + .collect::<Vec<_>>() + }) + .collect(); + builder.add_lookup_table_from_pairs(Arc::new(u8_bitref_table)) +} +pub fn u8_bitref( + builder: &mut CircuitBuilder<F, D>, + u8_bitref_lut_idx: usize, + x: ByteTarget, + i: ByteTarget, +) -> ByteTarget { + let lookup_idx = builder.mul_const_add(F::from_canonical_u64(8), x, i); + builder.add_lookup_from_index(lookup_idx, u8_bitref_lut_idx) +} + +fn le_bits_from_byte(v: u8) -> [bool; 8] { + let v_bits = (0..8) + .scan(v, |st, _| { + let cur_bit = (*st % 2) != 0; + *st >>= 1; + Some(cur_bit) + }) + .collect::<Vec<_>>(); + array::from_fn(|i| v_bits[i]) +} + +#[cfg(test)] +mod tests { + use std::array; + + use anyhow::Result; + use plonky2::{ + field::goldilocks_field::GoldilocksField as F, + iop::witness::PartialWitness, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + + use super::{D, *}; + use crate::{ + circuit_aes::{PartialWitnessByteArray, byte_xor_lut, gf_2_8_mul_lut}, + native_aes::key_expansion, + native_gcm::{gctr, gf_2_128_mul, ghash, right_shift_one}, + }; + + #[test] + fn test_gctr() -> Result<()> { + // AES-GCM-128 + test_gctr_opt::<4, 4, 10, 13>()?; + test_gctr_opt::<4, 4, 10, 17>()?; + + // AES-GCM-256 + test_gctr_opt::<8, 4, 14, 13>()?; + + Ok(()) + } + fn test_gctr_opt<const NK: usize, const NB: usize, const NR: usize, const L: usize>() + -> Result<()> + where + [(); 4 * (NR + 1)]:, + [(); NK * NB]:, + { + let key: &[u8; NK * NB] = &[42; NK * NB]; + let expanded_key = key_expansion::<NK, NB, NR>(key); + let nonce: &[u8; 12] = &[111; 12]; + let icb: &[u8; 16] = &[222; 16]; + let pt: &[u8; L] = &[42u8; L]; + + let expected = gctr(expanded_key, icb, pt); + + // Circuit declaration + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let key_target: [ByteTarget; NK * NB] = array::from_fn(|_| builder.add_virtual_target()); + let nonce_target: [ByteTarget; 12] = array::from_fn(|_| builder.add_virtual_target()); + let icb_target: [ByteTarget; 16] = array::from_fn(|_| builder.add_virtual_target()); + let pt_target: [ByteTarget; L] = array::from_fn(|_| builder.add_virtual_target()); + let sbox_lut = sbox_lut(&mut builder); + let xor_lut = byte_xor_lut(&mut builder); + let gf_2_8_mul_lut = gf_2_8_mul_lut(&mut builder); + let mix_matrix = state_mix_matrix_bits(&mut builder); + + let key_expanded_target = + builder.key_expansion::<NK, NB, NR>(xor_lut, sbox_lut, key_target); + let gctr_out_target = gctr_target::<NR, L>( + &mut builder, + (xor_lut, gf_2_8_mul_lut, sbox_lut), + mix_matrix, + key_expanded_target, + &icb_target, + &pt_target, + ); + + println!( + "gctr (NK:{}, NB:{}, NR:{}, L:{}) num_gates: {}", + NK, + NB, + NR, + L, + builder.num_gates() + ); + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + std::iter::zip(key_target, key).try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + std::iter::zip(nonce_target, nonce) + .try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + std::iter::zip(icb_target, icb).try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + std::iter::zip(pt_target, pt).try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + + std::iter::zip(gctr_out_target, expected) + .try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + let proof = data.prove(pw)?; + data.verify(proof) + } + + #[test] + fn test_right_shift_one() -> Result<()> { + let x: [u8; 16] = [111; 16]; + + let mut expected = x; + right_shift_one(&mut expected); + + // Circuit declaration + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let u8_unit_right_shift_lut_idx = u8_unit_right_shift_lut(&mut builder); + + let x_target: [ByteTarget; 16] = array::from_fn(|_| builder.add_virtual_target()); + + let out_target: [ByteTarget; 16] = + right_shift_one_target(&mut builder, u8_unit_right_shift_lut_idx, &x_target); + + println!("right_shift_one num_gates: {}", builder.num_gates()); + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + std::iter::zip(x_target, x).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + std::iter::zip(out_target, expected) + .try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + let proof = data.prove(pw)?; + data.verify(proof)?; + Ok(()) + } + + #[test] + fn test_gf_mul() -> Result<()> { + // AES-GCM-128 + test_gf_mul_opt::<4, 4, 10, 16>()?; + test_gf_mul_opt::<4, 4, 10, 32>()?; + + // AES-GCM-256 + test_gf_mul_opt::<8, 4, 14, 16>()?; + + Ok(()) + } + fn test_gf_mul_opt<const NK: usize, const NB: usize, const NR: usize, const L: usize>() + -> Result<()> + where + [(); 4 * (NR + 1)]:, + [(); NK * NB]:, + { + let x: [u8; 16] = [111; 16]; + let y: [u8; 16] = [222; 16]; + + let expected = gf_2_128_mul(x, y); + let x_bits: [[bool; 8]; 16] = array::from_fn(|b| super::le_bits_from_byte(x[b])); + let y_bits: [[bool; 8]; 16] = array::from_fn(|b| super::le_bits_from_byte(y[b])); + + // sanity check + let expected2 = crate::native_gcm::gf_2_128_mul_circuit_version(x_bits, y_bits); + let expected_bits: [[bool; 8]; 16] = + array::from_fn(|b| super::le_bits_from_byte(expected[b])); + assert_eq!(expected2, expected_bits); + + // Circuit declaration + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + let xor_lut_idx = byte_xor_lut(&mut builder); + let u8_unit_right_shift_lut_idx = u8_unit_right_shift_lut(&mut builder); + let u8_bitref_lut_idx = u8_bitref_lut(&mut builder); + + let x_target: [ByteTarget; 16] = array::from_fn(|_| builder.add_virtual_target()); + let y_target: [ByteTarget; 16] = array::from_fn(|_| builder.add_virtual_target()); + + let out_target = gf_2_128_mul_target( + &mut builder, + xor_lut_idx, + u8_unit_right_shift_lut_idx, + u8_bitref_lut_idx, + x_target, + y_target, + ); + + println!("gf_2_128_mul (L:{}) num_gates: {}", L, builder.num_gates()); + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + std::iter::zip(x_target, x).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + std::iter::zip(y_target, y).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + std::iter::zip(out_target, expected) + .try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + let proof = data.prove(pw)?; + data.verify(proof) + } + + #[test] + fn test_ghash() -> Result<()> { + // AES-GCM-128 + test_ghash_opt::<4, 4, 10, 16>()?; + test_ghash_opt::<4, 4, 10, 32>()?; + + // AES-GCM-256 + test_ghash_opt::<8, 4, 14, 16>()?; + + Ok(()) + } + fn test_ghash_opt<const NK: usize, const NB: usize, const NR: usize, const L: usize>() + -> Result<()> + where + [(); 4 * (NR + 1)]:, + [(); NK * NB]:, + { + let h: [u8; 16] = [222; 16]; + let x: &[u8; L] = &[42u8; L]; + + let expected = ghash(h, x); + + // Circuit declaration + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let xor_lut_idx = byte_xor_lut(&mut builder); + let u8_unit_right_shift_lut_idx = u8_unit_right_shift_lut(&mut builder); + let u8_bitref_lut_idx = u8_bitref_lut(&mut builder); + + let h_target: [ByteTarget; 16] = array::from_fn(|_| builder.add_virtual_target()); + let x_target: [ByteTarget; L] = array::from_fn(|_| builder.add_virtual_target()); + + // let ghash_out_target = ghash_target::<L>(&mut builder, h_target, &x_target); + let ghash_out_target = ghash_target( + &mut builder, + xor_lut_idx, + u8_unit_right_shift_lut_idx, + u8_bitref_lut_idx, + h_target, + x_target.to_vec(), + ); + + println!("ghash (L:{}) num_gates: {}", L, builder.num_gates()); + let data = builder.build::<PoseidonGoldilocksConfig>(); + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + std::iter::zip(h_target, h).try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + std::iter::zip(x_target, x).try_for_each(|(t, v)| pw.set_byte_array_target(t, *v))?; + + std::iter::zip(ghash_out_target, expected) + .try_for_each(|(t, v)| pw.set_byte_array_target(t, v))?; + + let proof = data.prove(pw)?; + data.verify(proof) + } + + #[test] + fn test_encrypt() -> Result<()> { + // AES-GCM-128 + test_encrypt_opt::<4, 4, 10, 13, false>(false)?; + test_encrypt_opt::<4, 4, 10, 13, true>(false)?; // with tag + 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(()) + } + // run it with: cargo test -- --ignored + #[ignore] + #[test] + fn test_encrypt_report_sizes() -> Result<()> { + // AES-GCM-128 + test_encrypt_opt::<4, 4, 10, 16, false>(true)?; + test_encrypt_opt::<4, 4, 10, 17, false>(true)?; + test_encrypt_opt::<4, 4, 10, 32, false>(true)?; + test_encrypt_opt::<4, 4, 10, 33, false>(true)?; + test_encrypt_opt::<4, 4, 10, 64, false>(true)?; + test_encrypt_opt::<4, 4, 10, 128, false>(true)?; + test_encrypt_opt::<4, 4, 10, 256, false>(true)?; + test_encrypt_opt::<4, 4, 10, 512, false>(true)?; + test_encrypt_opt::<4, 4, 10, 1024, false>(true)?; + test_encrypt_opt::<4, 4, 10, 2048, false>(true)?; + + // AES-GCM-256 + test_encrypt_opt::<8, 4, 14, 16, false>(true)?; + test_encrypt_opt::<8, 4, 14, 17, false>(true)?; + test_encrypt_opt::<8, 4, 14, 32, false>(true)?; + test_encrypt_opt::<8, 4, 14, 33, false>(true)?; + test_encrypt_opt::<8, 4, 14, 64, false>(true)?; + test_encrypt_opt::<8, 4, 14, 128, false>(true)?; + test_encrypt_opt::<8, 4, 14, 256, false>(true)?; + test_encrypt_opt::<8, 4, 14, 512, false>(true)?; + test_encrypt_opt::<8, 4, 14, 1024, false>(true)?; + test_encrypt_opt::<8, 4, 14, 2048, false>(true)?; + + Ok(()) + } + fn test_encrypt_opt< + const NK: usize, + const NB: usize, + const NR: usize, + const L: usize, + const TAG: bool, + >( + only_build: bool, + ) -> Result<()> + where + [(); 4 * (NR + 1)]:, + [(); NK * NB]:, + { + let key: &[u8; NK * NB] = &[42; NK * NB]; + let nonce: &[u8; 12] = &[111; 12]; + let pt: &[u8; L] = &[42u8; L]; + + let (ct, tag) = crate::native_gcm::encrypt::<NK, NB, NR>(key, nonce, pt); + + // 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>::build(&mut builder); + + println!( + "encrypt (NK:{}, NB:{}, NR:{}, L:{}, TAG:{}) num_gates: {}", + NK, + NB, + NR, + L, + TAG, + builder.num_gates() + ); + let data = builder.build::<PoseidonGoldilocksConfig>(); + + if only_build { + return Ok(()); + } + + // set values to circuit + let mut pw = PartialWitness::<F>::new(); + aes_targets.set_targets(&mut pw, key, nonce, pt, &ct, &tag)?; + + let proof = data.prove(pw)?; + data.verify(proof) + } +}
M aes-gcm/src/constants.rs → aes-gcm/src/constants.rs

@@ -1,7 +1,10 @@

-//! For LICENSE check out https://github.com/0xPARC/plonky2-aes/blob/main/LICENSE +//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE //! //! Constants for the implementation of AES following the specification from //! https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf. + +/// supported tag length +pub const TAG_LEN: usize = 128; /// table 4, page 14 pub(crate) const SBOX: [u8; 256] = [
M aes-gcm/src/lib.rs → aes-gcm/src/lib.rs

@@ -1,12 +1,26 @@

-//! For LICENSE check out https://github.com/0xPARC/plonky2-aes/blob/main/LICENSE +//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE #![allow(incomplete_features)] #![feature(generic_const_exprs)] #![allow(dead_code)] // TMP -mod circuit_aes; -mod constants; -mod native_aes; -mod native_gcm; +pub mod circuit_aes; +pub mod circuit_gcm; +pub mod constants; +pub mod native_aes; +pub mod native_gcm; /// D defines the extension degree of the field used in the Plonky2 proofs (quadratic extension). pub const D: usize = 2; + +pub use circuit_gcm::AesGcmTarget; + +// expose pre-defined configurations (AES-128, AES-192, AES-256): + +pub type AesGcm128Target<const L: usize> = AesGcmTarget<4, 4, 10, L, false>; +pub const KEY_LEN_128: usize = 4 * 4; + +pub const KEY_LEN_192: usize = 6 * 4; +pub type AesGcm192Target<const L: usize> = AesGcmTarget<6, 4, 12, L, false>; + +pub const KEY_LEN_256: usize = 8 * 4; +pub type AesGcm256Target<const L: usize> = AesGcmTarget<8, 4, 14, L, false>;
M aes-gcm/src/native_aes.rs → aes-gcm/src/native_aes.rs

@@ -1,4 +1,4 @@

-//! For LICENSE check out https://github.com/0xPARC/plonky2-aes/blob/main/LICENSE +//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE //! //! Rust native implementation of //! [AES](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf) having

@@ -33,7 +33,6 @@

let mut s: State = [[0; 4]; 4]; for i in 0..4 { for j in 0..4 { - // TODO s[i][j] = input[i + 4 * j]; } }

@@ -152,6 +151,14 @@

#[cfg(test)] mod tests { use super::*; + + #[test] + fn test_flatten_state() { + let s: [[u8; 4]; 4] = array::from_fn(|i| array::from_fn(|j| (i * 4 + j) as u8)); + + // the array::from_fn version is what we use later in the circuit + assert_eq!(flatten_state(s), array::from_fn(|i| s[i % 4][i / 4])); + } /// test against test vectors from Appendix A #[test]
M aes-gcm/src/native_gcm.rs → aes-gcm/src/native_gcm.rs

@@ -1,4 +1,4 @@

-//! For LICENSE check out https://github.com/0xPARC/plonky2-aes/blob/main/LICENSE +//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE //! //! Rust native implementation of [AES-GCM (Galois Counter //! Mode)](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf)

@@ -7,10 +7,10 @@ //! is not written in the most rust idiomatic way, nor memory-efficient, but

//! simulating the behavior that we will do later inside the circuit. //! -use crate::native_aes::{encrypt_block, flatten_state, key_expansion}; - -// supported tag length -const TAG_LEN: usize = 128; +use crate::{ + constants::TAG_LEN, + native_aes::{encrypt_block, flatten_state, key_expansion}, +}; /// Section 7.1 from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf pub fn encrypt<const NK: usize, const NB: usize, const NR: usize>(

@@ -21,7 +21,7 @@ ) -> (Vec<u8>, Vec<u8>)

where [(); 4 * (NR + 1)]:, { - let a: &[u8] = &[]; // additional authenticated data // TODO maybe as input + let a: &[u8] = &[]; // additional authenticated data let expanded_key: [[u8; 4]; 4 * (NR + 1)] = key_expansion::<NK, NB, NR>(key); // 1. CIPH_K

@@ -43,6 +43,7 @@ // 3. C=GCTR()

let c = gctr(expanded_key, &inc32(j0), pt); // 4. u, v + // note: c.len() will be x.len(), "L" in the circuit let u: usize = 16 * (c.len() as f64 / 16_f64).ceil() as usize - c.len(); let v: usize = 16 * (a.len() as f64 / 16_f64).ceil() as usize - a.len();

@@ -67,7 +68,11 @@ (c, t)

} /// GCM-Ctr, Section 6.5, Algorithm 3 -fn gctr<const NR: usize>(key: [[u8; 4]; 4 * (NR + 1)], icb: &[u8; 16], x: &[u8]) -> Vec<u8> { +pub(crate) fn gctr<const NR: usize>( + key: [[u8; 4]; 4 * (NR + 1)], + icb: &[u8; 16], + x: &[u8], +) -> Vec<u8> { if x.is_empty() { return x.to_vec(); }

@@ -103,7 +108,7 @@ y.concat()

} /// Section 6.4, Algorithm 2 -fn ghash(h: [u8; 16], x: &[u8]) -> [u8; 16] { +pub(crate) fn ghash(h: [u8; 16], x: &[u8]) -> [u8; 16] { assert!(x.len().is_multiple_of(16)); // multiple of 128 bits let m = x.len() / 16;

@@ -151,7 +156,71 @@ }

} z } -fn right_shift_one(block: &mut [u8; 16]) { + +// auxiliary version for testing the circuit logic +pub(crate) fn gf_2_128_mul_circuit_version( + x: [[bool; 8]; 16], + y: [[bool; 8]; 16], +) -> [[bool; 8]; 16] { + let zero_byte = [false; 8]; + + // R: 10000111 || 0^120 (in little-endian) + let mut r = [zero_byte; 16]; + r[0][0] = true; + r[0][5] = true; + r[0][6] = true; + r[0][7] = true; + + let mut z = [zero_byte; 16]; + let mut v = y; + for i in 0..128 { + let byte_index = i / 8; + let bit_index = 7 - (i % 8); + let xi = x[byte_index][bit_index]; + + // set z = if xi==1: z^v, else: z + for b in 0..16 { + for k in 0..8 { + let z_xor_v = z[b][k] ^ v[b][k]; + z[b][k] = if xi { z_xor_v } else { z[b][k] }; + } + } + + let lsb = v[15][0]; // (little-endian) + v = right_shift_one_circuit_version(&v); + + // if lsb==1: v=v^R, else: v + for b in 0..16 { + for k in 0..8 { + let v_xor_r = v[b][k] ^ r[b][k]; + v[b][k] = if lsb { v_xor_r } else { v[b][k] }; + } + } + } + z +} +// method for debugging +fn bools_to_u8_le(bits: [bool; 8]) -> u8 { + bits.iter() + .enumerate() + .fold(0u8, |acc, (i, &bit)| if bit { acc | (1 << i) } else { acc }) +} +fn right_shift_one_circuit_version(v: &[[bool; 8]; 16]) -> [[bool; 8]; 16] { + let mut r: [[bool; 8]; 16] = *v; + let mut carry = false; + for i in 0..16 { + let current = v[i]; + let next_carry = current[0]; + let mut shifted = [false; 8]; + shifted[..7].copy_from_slice(&current[1..(7 + 1)]); + shifted[7] = carry; + r[i] = shifted; + carry = next_carry; + } + r +} + +pub(crate) fn right_shift_one(block: &mut [u8; 16]) { let mut carry = 0u8; for byte in block.iter_mut() {

@@ -162,7 +231,7 @@ }

} /// increment the right-most 32 bits of the given block (128 bits). Section 2. -pub fn inc32(b: [u8; 16]) -> [u8; 16] { +pub(crate) fn inc32(b: [u8; 16]) -> [u8; 16] { let mut r = b; // counter = last 32bits let counter = u32::from_be_bytes([r[12], r[13], r[14], r[15]]);

@@ -173,7 +242,7 @@ r

} /// returns the t left-most (most-significant in BE) bits of the block -pub fn msb_t(t_bytes: usize, block: &[u8]) -> Vec<u8> { +pub(crate) fn msb_t(t_bytes: usize, block: &[u8]) -> Vec<u8> { let t = t_bytes * 8; let mut out = Vec::with_capacity(t.div_ceil(8));
A ecgfp5/Cargo.toml

@@ -0,0 +1,12 @@

+[package] +name = "plonky2-ecgfp5" +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"
A ecgfp5/src/circuit.rs

@@ -0,0 +1,101 @@

+//! 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, +}; +use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::{ + bits::{BigUInt320Target, CircuitBuilderBits}, + curve::{CircuitBuilderElliptic, Point, PointTarget, WitnessWriteCurve}, + }, +}; + +use crate::ECGFP5SecretKey; + +/// EcGFp5 secret key target type +pub struct ECGFP5SecretKeyTarget(pub BigUInt320Target); + +/// EcGFp5 public key (point) type alias +pub type PublicKeyTarget = PointTarget; + +pub trait CircuitBuilderECGFP5PublicKey<F: RichField + Extendable<D>, const D: usize> { + /// Adds secret key target + fn add_secret_key(&mut self) -> ECGFP5SecretKeyTarget; + + /// Derives public key from secret key + fn public_key(&mut self, sk: &ECGFP5SecretKeyTarget) -> PublicKeyTarget; +} + +impl CircuitBuilderECGFP5PublicKey<F, D> for CircuitBuilder<F, D> { + fn add_secret_key(&mut self) -> ECGFP5SecretKeyTarget { + ECGFP5SecretKeyTarget(self.add_virtual_biguint320_target()) + } + + fn public_key(&mut self, sk: &ECGFP5SecretKeyTarget) -> PublicKeyTarget { + let generator = self.constant_point(Point::generator()); + self.multiply_point(&sk.0.bits, &generator) + } +} + +pub trait PartialWitnessECGFP5PublicKey { + /// Sets EcGFp5 secret key witnesses + fn set_secret_key_target( + &mut self, + target: &ECGFP5SecretKeyTarget, + value: &ECGFP5SecretKey, + ) -> anyhow::Result<()>; +} + +impl PartialWitnessECGFP5PublicKey for PartialWitness<F> { + fn set_secret_key_target( + &mut self, + target: &ECGFP5SecretKeyTarget, + value: &ECGFP5SecretKey, + ) -> anyhow::Result<()> { + self.set_biguint320_target(&target.0, &value.0) + } +} + +#[cfg(test)] +mod tests { + use plonky2::{ + iop::witness::PartialWitness, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::curve::WitnessWriteCurve, + }; + + use crate::{ + ECGFP5SecretKey, + circuit::{CircuitBuilderECGFP5PublicKey, PartialWitnessECGFP5PublicKey}, + }; + + #[test] + fn public_key_calculation() -> anyhow::Result<()> { + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let sk_target = builder.add_secret_key(); + let pk_target = builder.public_key(&sk_target); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + (0..10).try_for_each(|_| { + let sk = ECGFP5SecretKey::rand(); + let pk = sk.public_key(); + + let mut pw = PartialWitness::new(); + pw.set_secret_key_target(&sk_target, &sk)?; + pw.set_point_target(&pk_target, &pk)?; + + let proof = data.prove(pw)?; + data.verify(proof) + }) + } +}
A ecgfp5/src/elgamal.rs

@@ -0,0 +1,47 @@

+//! 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}; + +use crate::ECGFP5SecretKey; + +pub mod circuit; + +/// ElGamal encryption on EcGFp5 +pub fn elgamal_encrypt(pk: Point, nonce: &BigUint, msg: Point) -> (Point, Point) { + assert!(nonce < &GROUP_ORDER); + let nonce_times_gen = nonce * Point::generator(); + let nonce_times_pk = nonce * pk; + (nonce_times_gen, msg + nonce_times_pk) +} + +/// ElGamal decryption on EcGFp5 +pub fn elgamal_decrypt(sk: ECGFP5SecretKey, ct: (Point, Point)) -> Point { + let nonce_times_pk = &sk.0 * ct.0; + ct.1 + nonce_times_pk.inverse() +} + +#[cfg(test)] +mod tests { + use num_bigint::RandBigInt; + use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point}; + use rand::rngs::OsRng; + + use crate::elgamal::{ECGFP5SecretKey, elgamal_decrypt, elgamal_encrypt}; + + #[test] + fn round_trip_encryption() { + (0..100).for_each(|_| { + let sk = ECGFP5SecretKey::rand(); + let pk = sk.public_key(); + + let msg = Point::new_rand_from_subgroup(); + let nonce = OsRng.gen_biguint_below(&GROUP_ORDER); + + let encrypted_msg = elgamal_encrypt(pk, &nonce, msg); + let decrypted_msg = elgamal_decrypt(sk, encrypted_msg); + + assert_eq!(msg, decrypted_msg); + }) + } +}
A ecgfp5/src/elgamal/circuit.rs

@@ -0,0 +1,98 @@

+//! 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, +}; +use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::{ + bits::BigUInt320Target, + curve::{CircuitBuilderElliptic, Point, PointTarget}, + }, +}; + +use crate::circuit::PublicKeyTarget; + +pub trait CircuitBuilderElGamal<F: RichField + Extendable<D>, const D: usize> { + /// Encrypts an EcGFp5 point according to the ElGamal scheme. + fn elgamal_encrypt( + &mut self, + pk: &PublicKeyTarget, + nonce: &BigUInt320Target, + msg: &PointTarget, + ) -> (PointTarget, PointTarget); +} + +impl CircuitBuilderElGamal<F, D> for CircuitBuilder<F, D> { + fn elgamal_encrypt( + &mut self, + pk: &PublicKeyTarget, + nonce: &BigUInt320Target, + msg: &PointTarget, + ) -> (PointTarget, PointTarget) { + let generator = self.constant_point(Point::generator()); + let nonce_times_gen = self.multiply_point(&nonce.bits, &generator); + let nonce_times_pk = self.multiply_point(&nonce.bits, pk); + (nonce_times_gen, self.add_point(msg, &nonce_times_pk)) + } +} + +#[cfg(test)] +mod tests { + use num_bigint::RandBigInt; + use plonky2::{ + iop::witness::PartialWitness, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::{ + bits::CircuitBuilderBits, + curve::{CircuitBuilderElliptic, GROUP_ORDER, Point, WitnessWriteCurve}, + }, + }; + use rand::rngs::OsRng; + + use crate::{ + ECGFP5SecretKey, + elgamal::{circuit::CircuitBuilderElGamal, elgamal_encrypt}, + }; + + #[test] + fn elgamal_encryption() -> anyhow::Result<()> { + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let pk_target = builder.add_virtual_point_target(); + let nonce_target = builder.add_virtual_biguint320_target(); + let msg_target = builder.add_virtual_point_target(); + + let ct_target = builder.elgamal_encrypt(&pk_target, &nonce_target, &msg_target); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + + (0..10).try_for_each(|_| { + let sk = ECGFP5SecretKey::rand(); + let pk = sk.public_key(); + + let msg = Point::new_rand_from_subgroup(); + let nonce = OsRng.gen_biguint_below(&GROUP_ORDER); + + let encrypted_msg = elgamal_encrypt(pk, &nonce, msg); + + let mut pw = PartialWitness::new(); + pw.set_point_target(&pk_target, &pk)?; + pw.set_point_target(&msg_target, &msg)?; + pw.set_biguint320_target(&nonce_target, &nonce)?; + pw.set_point_target(&ct_target.0, &encrypted_msg.0)?; + pw.set_point_target(&ct_target.1, &encrypted_msg.1)?; + + let proof = data.prove(pw)?; + data.verify(proof) + }) + } +}
A ecgfp5/src/hashed_elgamal.rs

@@ -0,0 +1,64 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + +use std::array; + +use num::bigint::BigUint; +use plonky2::{ + field::goldilocks_field::GoldilocksField as F, + hash::{hashing::hash_n_to_m_no_pad, poseidon::PoseidonPermutation}, +}; +use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point}; + +use crate::ECGFP5SecretKey; + +pub mod circuit; + +/// A variant of 'hashed ElGamal' with addition in place of +/// XORing. Assumes a message of 5 field elements and uses the +/// Poseidon hash function outputting 5 field elements. +pub fn hashed_elgamal_encrypt(pk: Point, nonce: &BigUint, msg: [F; 5]) -> (Point, [F; 5]) { + assert!(nonce < &GROUP_ORDER); + let nonce_times_gen = nonce * Point::generator(); + let nonce_times_pk = nonce * pk; + let h = hash_n_to_m_no_pad::<F, PoseidonPermutation<_>>(&nonce_times_pk.as_fields(), 5); + (nonce_times_gen, array::from_fn(|i| msg[i] + h[i])) +} + +/// Inverse to `hashed_elgamal_encrypt`. +pub fn hashed_elgamal_decrypt(sk: ECGFP5SecretKey, ct: (Point, [F; 5])) -> [F; 5] { + let nonce_times_pk = &sk.0 * ct.0; + let h = hash_n_to_m_no_pad::<F, PoseidonPermutation<_>>(&nonce_times_pk.as_fields(), 5); + array::from_fn(|i| ct.1[i] - h[i]) +} + +#[cfg(test)] +mod tests { + use num_bigint::RandBigInt; + use plonky2::field::{ + goldilocks_field::GoldilocksField as F, + types::{Field, Field64}, + }; + use pod2::backends::plonky2::primitives::ec::curve::GROUP_ORDER; + use rand::{Rng, rngs::OsRng}; + + use crate::{ + ECGFP5SecretKey, + hashed_elgamal::{hashed_elgamal_decrypt, hashed_elgamal_encrypt}, + }; + + #[test] + fn round_trip_encryption() { + (0..100).for_each(|_| { + let sk = ECGFP5SecretKey::rand(); + let pk = sk.public_key(); + + let msg = std::array::from_fn(|_| F::from_canonical_u64(OsRng.gen_range(0..F::ORDER))); + let nonce = OsRng.gen_biguint_below(&GROUP_ORDER); + + let encrypted_msg = hashed_elgamal_encrypt(pk, &nonce, msg); + let decrypted_msg = hashed_elgamal_decrypt(sk, encrypted_msg); + + assert_eq!(msg, decrypted_msg); + }) + } +}
A ecgfp5/src/hashed_elgamal/circuit.rs

@@ -0,0 +1,108 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + +use std::array; + +use plonky2::{ + field::extension::Extendable, + hash::{hash_types::RichField, poseidon::PoseidonHash}, + iop::target::Target, + plonk::circuit_builder::CircuitBuilder, +}; +use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::{ + bits::BigUInt320Target, + curve::{CircuitBuilderElliptic, Point, PointTarget}, + }, +}; + +use crate::circuit::PublicKeyTarget; + +pub trait CircuitBuilderHashedElGamal<F: RichField + Extendable<D>, const D: usize> { + /// Encrypts an EcGFp5 point according to the 'hashed ElGamal ' + /// scheme described in `super::hashed_elgamal_encrypt`. + fn hashed_elgamal_encrypt( + &mut self, + pk: &PublicKeyTarget, + nonce: &BigUInt320Target, + msg: [Target; 5], + ) -> (PointTarget, [Target; 5]); +} + +impl CircuitBuilderHashedElGamal<F, D> for CircuitBuilder<F, D> { + fn hashed_elgamal_encrypt( + &mut self, + pk: &PublicKeyTarget, + nonce: &BigUInt320Target, + msg: [Target; 5], + ) -> (PointTarget, [Target; 5]) { + let generator = self.constant_point(Point::generator()); + let nonce_times_gen = self.multiply_point(&nonce.bits, &generator); + let nonce_times_pk = self.multiply_point(&nonce.bits, pk); + let h = self.hash_n_to_m_no_pad::<PoseidonHash>( + [nonce_times_pk.x.components, nonce_times_pk.u.components].concat(), + 5, + ); + (nonce_times_gen, array::from_fn(|i| self.add(msg[i], h[i]))) + } +} + +#[cfg(test)] +mod tests { + use num_bigint::RandBigInt; + use plonky2::{ + field::types::{Field, Field64}, + iop::witness::{PartialWitness, WitnessWrite}, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + use pod2::backends::plonky2::{ + basetypes::{D, F}, + primitives::ec::{ + bits::CircuitBuilderBits, + curve::{CircuitBuilderElliptic, GROUP_ORDER, WitnessWriteCurve}, + }, + }; + use rand::{Rng, rngs::OsRng}; + + use crate::{ + ECGFP5SecretKey, + hashed_elgamal::{circuit::CircuitBuilderHashedElGamal, hashed_elgamal_encrypt}, + }; + + #[test] + fn hashed_elgamal_encryption() -> anyhow::Result<()> { + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let pk_target = builder.add_virtual_point_target(); + let nonce_target = builder.add_virtual_biguint320_target(); + let msg_target = builder.add_virtual_target_arr(); + + let ct_target = builder.hashed_elgamal_encrypt(&pk_target, &nonce_target, msg_target); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + + (0..10).try_for_each(|_| { + let sk = ECGFP5SecretKey::rand(); + let pk = sk.public_key(); + + let msg = std::array::from_fn(|_| F::from_canonical_u64(OsRng.gen_range(0..F::ORDER))); + let nonce = OsRng.gen_biguint_below(&GROUP_ORDER); + + let encrypted_msg = hashed_elgamal_encrypt(pk, &nonce, msg); + + let mut pw = PartialWitness::new(); + pw.set_point_target(&pk_target, &pk)?; + pw.set_target_arr(&msg_target, &msg)?; + pw.set_biguint320_target(&nonce_target, &nonce)?; + pw.set_point_target(&ct_target.0, &encrypted_msg.0)?; + pw.set_target_arr(&ct_target.1, &encrypted_msg.1)?; + + let proof = data.prove(pw)?; + data.verify(proof) + }) + } +}
A ecgfp5/src/lib.rs

@@ -0,0 +1,114 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + +use std::array; + +use num::BigUint; +use num_bigint::RandBigInt; +use plonky2::field::{ + extension::quintic::QuinticExtension, + goldilocks_field::GoldilocksField as F, + types::{Field, Field64}, +}; +use pod2::backends::plonky2::primitives::ec::curve::{GROUP_ORDER, Point}; +use rand::{Rng, rngs::OsRng}; + +pub mod circuit; +pub mod elgamal; +pub mod hashed_elgamal; + +// Message encoding constants +pub const BITS_PER_LIMB: usize = 32; +pub const TOTAL_BITS: usize = 5 * BITS_PER_LIMB; + +/// EcGFp5 secret key type +pub struct ECGFP5SecretKey(pub BigUint); + +impl ECGFP5SecretKey { + /// EcGFp5 secret key constructor + pub fn new(s: BigUint) -> Self { + assert!(&s < &GROUP_ORDER); + Self(s) + } + + /// Random EcGFp5 secret key generator + pub fn rand() -> Self { + Self(OsRng.gen_biguint_below(&GROUP_ORDER)) + } + + /// Derives public key from secret key + pub fn public_key(&self) -> Point { + &self.0 * Point::generator() + } +} + +/// Convert `TOTAL_BITS`-bit biguint to an EcGFp5 point by padding +/// with random bits. This is an adaptation of the probabilistic +/// algorithm described in §3 of Koblitz's *Elliptic Curve +/// Cryptosystems* (1987). +pub fn encode_binary(x: &BigUint) -> Point { + let num_bits = x.bits() as usize; + assert!(num_bits <= TOTAL_BITS); + + let bs_bits = [x.to_radix_le(2), vec![0; TOTAL_BITS - num_bits]].concat(); + let bs_fields: Vec<F> = bs_bits + .chunks(BITS_PER_LIMB) + .map(|bits| { + F::from_noncanonical_biguint( + BigUint::from_radix_le(bits, 2).expect("Should be a valid bit decomposition"), + ) + }) + .collect(); + + std::iter::repeat_with(|| { + let ec_fields: [_; 5] = array::from_fn(|i| { + let random_element: u64 = OsRng.gen_range(0..F::ORDER - bs_fields[i].0); + F::from_canonical_u64( + bs_fields[i].0 + ((random_element >> BITS_PER_LIMB) << BITS_PER_LIMB), + ) + }); + QuinticExtension(ec_fields) + }) + .find_map(|w| Point::decompress_into_subgroup(&w).ok()) + .expect("Should be a match!") +} + +/// Convert an EcGFp5 point to a `TOTAL_BITS`-bit biguint. This is the +/// inverse to `encode_binary`. +pub fn decode_binary(p: Point) -> BigUint { + let msg_bits: Vec<_> = p + .compress_from_subgroup() + .unwrap() + .0 + .into_iter() + .flat_map(|f| { + BigUint::from(f.0) + .to_radix_le(2) + .into_iter() + .take(BITS_PER_LIMB) + .collect::<Vec<_>>() + }) + .collect(); + BigUint::from_bytes_le( + &BigUint::from_radix_le(&msg_bits, 2) + .expect("Should be a valid bit slice.") + .to_radix_le(256), + ) +} +#[cfg(test)] +mod tests { + use num::BigUint; + use num_bigint::RandBigInt; + use rand::rngs::OsRng; + + use crate::{TOTAL_BITS, decode_binary, encode_binary}; + + #[test] + fn msg_encoding_roundtrip() { + (0..100).for_each(|_| { + let test_val = OsRng.gen_biguint_below(&BigUint::from(2u32).pow(TOTAL_BITS as u32)); + let pt = encode_binary(&test_val); + let maybe_test_val = decode_binary(pt); + assert_eq!(test_val, maybe_test_val); + }) + } +}
A feistel/Cargo.toml

@@ -0,0 +1,9 @@

+[package] +name = "plonky2-feistel" +version = "0.1.0" +edition = "2024" + +[dependencies] +plonky2 = { workspace = true } +anyhow = { workspace = true } +rand = "0.8.5"
A feistel/src/circuit.rs

@@ -0,0 +1,155 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + +use std::array; + +use plonky2::{ + field::{extension::Extendable, goldilocks_field::GoldilocksField as F}, + hash::hash_types::RichField, + iop::{ + target::Target, + witness::{PartialWitness, WitnessWrite}, + }, + plonk::circuit_builder::CircuitBuilder, +}; + +pub(crate) const D: usize = 2; + +/// State on which the Feistel-like cipher operates +#[derive(Debug, Copy, Clone)] +pub struct FeistelStateTarget<const STATE_LEN: usize>([Target; STATE_LEN]); + +pub trait CircuitBuilderFeistel<F: RichField + Extendable<D>, const D: usize> { + /// Feistel state target constructor + fn add_feistel_state_target<const STATE_LEN: usize>(&mut self) + -> FeistelStateTarget<STATE_LEN>; + + /// Generates Feistel-like cipher as in `crate::feistel_cipher` + fn feistel_cipher<const STATE_HALF_LEN: usize, const KEY_LEN: usize>( + &mut self, + state: FeistelStateTarget<{ 2 * STATE_HALF_LEN }>, + key_schedule: &[[Target; KEY_LEN]], + f: &impl Fn(&mut Self, Vec<Target>) -> [Target; STATE_HALF_LEN], + ) -> FeistelStateTarget<{ 2 * STATE_HALF_LEN }>; +} + +impl CircuitBuilderFeistel<F, D> for CircuitBuilder<F, D> { + fn add_feistel_state_target<const STATE_LEN: usize>( + &mut self, + ) -> FeistelStateTarget<STATE_LEN> { + FeistelStateTarget(self.add_virtual_target_arr()) + } + + fn feistel_cipher<const STATE_HALF_LEN: usize, const KEY_LEN: usize>( + &mut self, + state: FeistelStateTarget<{ 2 * STATE_HALF_LEN }>, + key_schedule: &[[Target; KEY_LEN]], + f: &impl Fn(&mut Self, Vec<Target>) -> [Target; STATE_HALF_LEN], + ) -> FeistelStateTarget<{ 2 * STATE_HALF_LEN }> { + if key_schedule.is_empty() { + state + } else { + let k = key_schedule[0]; + let l = &state.0[..STATE_HALF_LEN]; + let r = &state.0[STATE_HALF_LEN..]; + + let new_l = r; + let round_offset = f(self, [r, &k].concat()); + let new_r: [_; STATE_HALF_LEN] = array::from_fn(|i| self.add(l[i], round_offset[i])); + self.feistel_cipher( + FeistelStateTarget(array::from_fn(|i| { + if i < STATE_HALF_LEN { + new_l[i] + } else { + new_r[i - STATE_HALF_LEN] + } + })), + &key_schedule[1..], + f, + ) + } + } +} + +pub trait PartialWitnessFeistel { + /// Sets witnesses for a Feistel state target + fn set_feistel_state_target<const STATE_SIZE: usize>( + &mut self, + target: &FeistelStateTarget<STATE_SIZE>, + value: &[F; STATE_SIZE], + ) -> anyhow::Result<()>; +} + +impl PartialWitnessFeistel for PartialWitness<F> { + fn set_feistel_state_target<const STATE_SIZE: usize>( + &mut self, + target: &FeistelStateTarget<STATE_SIZE>, + value: &[F; STATE_SIZE], + ) -> anyhow::Result<()> { + self.set_target_arr(&target.0, value) + } +} + +#[cfg(test)] +mod tests { + use plonky2::{ + field::goldilocks_field::GoldilocksField as F, + hash::{ + hashing::hash_n_to_hash_no_pad, + poseidon::{PoseidonHash, PoseidonPermutation}, + }, + iop::witness::{PartialWitness, WitnessWrite}, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + + use super::D; + use crate::{ + circuit::{CircuitBuilderFeistel, FeistelStateTarget, PartialWitnessFeistel}, + feistel_cipher, + tests::random_fields, + }; + + #[test] + fn feistel_poseidon_check() -> anyhow::Result<()> { + const NR: usize = 32; + const STATE_HALF_LEN: usize = 4; + const KEY_LEN: usize = 4; + + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let state_target: FeistelStateTarget<{ 2 * STATE_HALF_LEN }> = + builder.add_feistel_state_target(); + let key_schedule_target: [[_; KEY_LEN]; NR] = + std::array::from_fn(|_| builder.add_virtual_target_arr()); + + let f = |builder: &mut CircuitBuilder<F, D>, targets: Vec<_>| { + builder + .hash_n_to_hash_no_pad::<PoseidonHash>(targets) + .elements + }; + + let out_state_target = builder.feistel_cipher(state_target, &key_schedule_target, &f); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + (0..10).try_for_each(|_| { + let state: [_; 2 * STATE_HALF_LEN] = random_fields(); + let key_schedule: [[_; KEY_LEN]; NR] = std::array::from_fn(|_| random_fields()); + let eff = |fields: Vec<_>| { + hash_n_to_hash_no_pad::<F, PoseidonPermutation<_>>(&fields).elements + }; + let out_state = feistel_cipher(state, &key_schedule, &eff); + + let mut pw = PartialWitness::new(); + pw.set_feistel_state_target(&state_target, &state)?; + std::iter::zip(key_schedule_target, key_schedule) + .try_for_each(|(t, v)| pw.set_target_arr(&t, &v))?; + pw.set_feistel_state_target(&out_state_target, &out_state)?; + + let proof = data.prove(pw)?; + data.verify(proof) + }) + } +}
A feistel/src/lib.rs

@@ -0,0 +1,121 @@

+//! For LICENSE check out https://github.com/0xPARC/plonky2-crypto-gadgets/blob/main/LICENSE + +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] + +pub mod circuit; + +use std::array; + +use plonky2::field::types::Field; + +/// Feistel-type cipher for finite fields, where XORing is replaced +/// with addition, cf. https://en.wikipedia.org/wiki/Feistel_cipher or +/// §3.5.1 of Stinson's *Cryptography* (3rd edition). +pub fn feistel_cipher<const STATE_HALF_LEN: usize, const KEY_LEN: usize, F: Field>( + state: [F; 2 * STATE_HALF_LEN], + key_schedule: &[[F; KEY_LEN]], + f: &impl Fn(Vec<F>) -> [F; STATE_HALF_LEN], +) -> [F; 2 * STATE_HALF_LEN] { + if key_schedule.is_empty() { + state + } else { + let k = key_schedule[0]; + let l = &state[..STATE_HALF_LEN]; + let r = &state[STATE_HALF_LEN..]; + + let new_l = r; + let round_offset = f([r, &k].concat()); + let new_r: [_; STATE_HALF_LEN] = array::from_fn(|i| l[i] + round_offset[i]); + + feistel_cipher( + array::from_fn(|i| { + if i < STATE_HALF_LEN { + new_l[i] + } else { + new_r[i - STATE_HALF_LEN] + } + }), + &key_schedule[1..], + f, + ) + } +} + +/// Feistel-type inverse cipher for finite fields, where XORing is +/// replaced with subtraction. +pub fn feistel_inv_cipher<const STATE_HALF_LEN: usize, const KEY_LEN: usize, F: Field>( + state: [F; 2 * STATE_HALF_LEN], + reverse_key_schedule: &[[F; KEY_LEN]], + f: &impl Fn(Vec<F>) -> [F; STATE_HALF_LEN], +) -> [F; 2 * STATE_HALF_LEN] { + if reverse_key_schedule.is_empty() { + state + } else { + let k = reverse_key_schedule[0]; + let l = &state[..STATE_HALF_LEN]; + let r = &state[STATE_HALF_LEN..]; + + let new_r = l; + let round_offset = f([l, &k].concat()); + let new_l: [_; STATE_HALF_LEN] = array::from_fn(|i| r[i] - round_offset[i]); + + feistel_inv_cipher( + array::from_fn(|i| { + if i < STATE_HALF_LEN { + new_l[i] + } else { + new_r[i - STATE_HALF_LEN] + } + }), + &reverse_key_schedule[1..], + f, + ) + } +} + +#[cfg(test)] +mod tests { + use plonky2::{ + field::{ + goldilocks_field::GoldilocksField as F, + types::{Field, Field64}, + }, + hash::{ + hashing::hash_n_to_hash_no_pad, keccak::KeccakPermutation, + poseidon::PoseidonPermutation, + }, + }; + use rand::{Rng, rngs::OsRng}; + + use crate::{feistel_cipher, feistel_inv_cipher}; + + pub(crate) fn random_fields<const N: usize>() -> [F; N] { + std::array::from_fn(|_| F::from_canonical_u64(OsRng.gen_range(0..F::ORDER))) + } + + #[test] + fn feistel_keccak_poseidon_roundtrip() { + const NR: usize = 32; + const STATE_HALF_LEN: usize = 4; + const KEY_LEN: usize = 4; + + let f_keccak = + |fields: Vec<_>| hash_n_to_hash_no_pad::<F, KeccakPermutation<_>>(&fields).elements; + let f_poseidon = + |fields: Vec<_>| hash_n_to_hash_no_pad::<F, PoseidonPermutation<_>>(&fields).elements; + + [f_keccak, f_poseidon].into_iter().for_each(|f| { + (0..10).for_each(|_| { + let state: [_; 2 * STATE_HALF_LEN] = random_fields(); + let key_schedule: [[_; KEY_LEN]; NR] = std::array::from_fn(|_| random_fields()); + + let out_state = feistel_cipher(state, &key_schedule, &f); + let reverse_key_schedule = key_schedule.into_iter().rev().collect::<Vec<_>>(); + let maybe_original_state = feistel_inv_cipher(out_state, &reverse_key_schedule, &f); + + assert_eq!(state, maybe_original_state); + }) + }) + } +}
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]); + } +}