From d0f5c0390abe230028f0e26ebfea07efedbbc651 Mon Sep 17 00:00:00 2001 From: moana Date: Mon, 18 Mar 2024 09:50:10 +0100 Subject: [PATCH] Add encryption and decryption Resolves #236 --- .github/workflows/dusk_ci.yml | 12 +- CHANGELOG.md | 19 ++ Cargo.toml | 30 +- README.md | 43 ++- benches/cipher_encrypt.rs | 112 -------- benches/decrypt.rs | 138 +++++++++ benches/encrypt.rs | 134 +++++++++ benches/hash.rs | 6 +- benches/cipher_decrypt.rs => decrypt.rs | 2 +- src/cipher.rs | 360 ------------------------ src/encryption.rs | 57 ++++ src/encryption/gadget.rs | 55 ++++ src/error.rs | 13 + src/hades/permutation/gadget.rs | 28 +- src/hades/permutation/scalar.rs | 15 + src/lib.rs | 15 +- tests/cipher.rs | 260 ----------------- tests/encryption.rs | 115 ++++++++ tests/encryption_gadget.rs | 217 ++++++++++++++ tests/hash.rs | 2 +- 20 files changed, 854 insertions(+), 779 deletions(-) delete mode 100644 benches/cipher_encrypt.rs create mode 100644 benches/decrypt.rs create mode 100644 benches/encrypt.rs rename benches/cipher_decrypt.rs => decrypt.rs (98%) delete mode 100644 src/cipher.rs create mode 100644 src/encryption.rs create mode 100644 src/encryption/gadget.rs delete mode 100644 tests/cipher.rs create mode 100644 tests/encryption.rs create mode 100644 tests/encryption_gadget.rs diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index f5239eb..0bd3bcc 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -12,7 +12,7 @@ jobs: uses: dusk-network/.github/.github/workflows/code-analysis.yml@main with: clippy_default: false - clippy_args: --features=rkyv/size_32 + clippy_args: --all-features analyze: name: Dusk Analyzer @@ -24,16 +24,16 @@ jobs: steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - - run: cargo bench --features=cipher,zk --no-run + - run: cargo bench --all-features --no-run - check_cipher: - name: Check cipher compiles without zk + check_encryption: + name: Check encryption compiles without zk uses: dusk-network/.github/.github/workflows/run-tests.yml@main with: - test_flags: --features=cipher --no-run + test_flags: --features=encryption --no-run test_all: name: Tests all uses: dusk-network/.github/.github/workflows/run-tests.yml@main with: - test_flags: --features=zk,cipher,rkyv-impl,size_32 + test_flags: --all-features diff --git a/CHANGELOG.md b/CHANGELOG.md index 9029dda..36777af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add variable length encryption and decryption [#236] +- Add variable length encryption and decryption gadgets [#236] +- Add `encryption` feature [#236] + +### Removed + +- Remove `PoseidonCipher` struct as it is replaced by encryption functions [#236] +- Remove `cipher` feature [#236] +- Remove `rkyv` dependency and all related features [#236] +- Remove `bytecheck` dependency [#236] +- Remove `dusk-bytes` dependency [#236] + +### Changed + +- Append the tag as a constant when initializing the gadget state [#236] + ## [0.36.0] - 2024-03-13 ### Added @@ -478,6 +496,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#246]: https://github.com/dusk-network/poseidon252/issues/246 [#243]: https://github.com/dusk-network/poseidon252/issues/243 [#240]: https://github.com/dusk-network/poseidon252/issues/240 +[#236]: https://github.com/dusk-network/poseidon252/issues/236 [#215]: https://github.com/dusk-network/poseidon252/issues/215 [#212]: https://github.com/dusk-network/poseidon252/issues/212 [#206]: https://github.com/dusk-network/poseidon252/issues/206 diff --git a/Cargo.toml b/Cargo.toml index 1455d43..8a3e142 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,35 +10,23 @@ edition = "2021" license = "MPL-2.0" [dependencies] -dusk-bls12_381 = { version = "0.13", default-features = false } +dusk-bls12_381 = { version = "0.13", default-features = false, features = ["zeroize"] } dusk-jubjub = { version = "0.14", default-features = false } -dusk-bytes = "0.1" -dusk-plonk = { version = "0.19", default-features = false, features = ["alloc"], optional = true } -dusk-safe = "0.1" -rkyv = { version = "0.7", optional = true, default-features = false } -bytecheck = { version = "0.6", optional = true, default-features = false } +dusk-plonk = { version = "0.19.2-rc.0", default-features = false, features = ["alloc", "zeroize"], optional = true } +dusk-safe = "0.2.0-rc.0" [dev-dependencies] criterion = "0.3" rand = { version = "0.8", default-features = false, features = ["getrandom", "std_rng"] } ff = { version = "0.13", default-features = false } once_cell = "1" +dusk-bytes = "0.1" [features] zk = [ "dusk-plonk", ] -cipher = [] -size_16 = ["rkyv/size_16"] -size_32 = ["rkyv/size_32"] -size_64 = ["rkyv/size_64"] -rkyv-impl = [ - "rkyv/validation", - "rkyv/alloc", - "rkyv", - "bytecheck", - "dusk-bls12_381/rkyv-impl" -] +encryption = ["dusk-safe/encryption"] [profile.dev] opt-level = 3 @@ -64,11 +52,11 @@ harness = false required-features = ["zk"] [[bench]] -name = "cipher_encrypt" +name = "encrypt" harness = false -required-features = ["cipher", "zk"] +required-features = ["zk", "encryption"] [[bench]] -name = "cipher_decrypt" +name = "decrypt" harness = false -required-features = ["cipher", "zk"] +required-features = ["zk", "encryption"] diff --git a/README.md b/README.md index 2dd5727..fd9b740 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ The library provides the two hashing techniques of Poseidon: - The 'normal' hashing functionalities operating on `BlsScalar`. - The 'gadget' hashing functionalities that build a circuit which outputs the hash. -## Example +## Examples + +### Hash ```rust use rand::rngs::StdRng; @@ -50,13 +52,48 @@ let merkle_hash = Hash::digest(Domain::Merkle4, &input[..4]); assert_ne!(merkle_hash, Hash::digest(Domain::Other, &input[..4])); ``` +### Encryption + +```rust +#![cfg(feature = "encryption")] + +use dusk_bls12_381::BlsScalar; +use dusk_jubjub::{JubJubScalar, GENERATOR_EXTENDED, dhke}; +use dusk_poseidon::{decrypt, encrypt, Error}; +use ff::Field; +use rand::rngs::StdRng; +use rand::SeedableRng; + +// generate the keys and nonce needed for the encryption +let mut rng = StdRng::seed_from_u64(0x42424242); +let alice_secret = JubJubScalar::random(&mut rng); +let alice_public = GENERATOR_EXTENDED * &alice_secret; +let bob_secret = JubJubScalar::random(&mut rng); +let bob_public = GENERATOR_EXTENDED * &bob_secret; +let nonce = BlsScalar::random(&mut rng); + +// Alice encrypts a message of 3 BlsScalar using Diffie-Hellman key exchange +// with Bob's public key +let message = vec![BlsScalar::from(10), BlsScalar::from(20), BlsScalar::from(30)]; +let shared_secret = dhke(&alice_secret, &bob_public); +let cipher = encrypt(&message, &shared_secret, &nonce) + .expect("Encryption should pass"); + +// Bob decrypts the cipher using Diffie-Hellman key exchange with Alice's public key +let shared_secret = dhke(&bob_secret, &alice_public); +let decrypted_message = decrypt(&cipher, &shared_secret, &nonce) + .expect("Decryption should pass"); + +assert_eq!(decrypted_message, message); +``` + ## Benchmarks -There are benchmarks for `sponge` and `cipher` in their native form, operating on `Scalar`, and as a zero-knowledge gadget, using `Witness`. +There are benchmarks for hashing, encrypting and decrypting in their native form, operating on `Scalar`, and for a zero-knowledge circuit proof generation and verification. To run all benchmarks on your machine, run ```shell -cargo bench --features=zk,cipher +cargo bench --features=zk,encryption ``` in the repository. diff --git a/benches/cipher_encrypt.rs b/benches/cipher_encrypt.rs deleted file mode 100644 index 8141a5a..0000000 --- a/benches/cipher_encrypt.rs +++ /dev/null @@ -1,112 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. -// -// Copyright (c) DUSK NETWORK. All rights reserved. - -use dusk_poseidon::{encrypt_gadget, PoseidonCipher}; - -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use dusk_jubjub::GENERATOR; -use dusk_plonk::prelude::*; -use ff::Field; -use rand::rngs::StdRng; -use rand::SeedableRng; - -use std::ops::Mul; - -const CAPACITY: usize = 11; -const MESSAGE_CAPACITY: usize = 2; - -#[derive(Default)] -pub struct CipherEncrypt { - shared: JubJubAffine, - nonce: BlsScalar, - message: [BlsScalar; MESSAGE_CAPACITY], -} - -impl CipherEncrypt { - pub fn random(rng: &mut StdRng) -> Self { - let shared = GENERATOR - .to_niels() - .mul(&JubJubScalar::random(&mut *rng)) - .into(); - let nonce = BlsScalar::random(&mut *rng); - let message = - [BlsScalar::random(&mut *rng), BlsScalar::random(&mut *rng)]; - - Self { - shared, - nonce, - message, - } - } -} - -impl Circuit for CipherEncrypt { - fn circuit(&self, composer: &mut Composer) -> Result<(), Error> { - let shared = composer.append_point(self.shared); - let nonce = composer.append_witness(self.nonce); - - let mut message_circuit = [Composer::ZERO; MESSAGE_CAPACITY]; - self.message - .iter() - .zip(message_circuit.iter_mut()) - .for_each(|(message_scalar, message_witness)| { - *message_witness = composer.append_witness(*message_scalar); - }); - - encrypt_gadget(composer, &shared, nonce, &message_circuit); - - Ok(()) - } -} - -// Benchmark cipher encryption -fn bench_cipher_encryption(c: &mut Criterion) { - // Prepare benchmarks and initialize variables - let label = b"cipher encryption benchmark"; - let mut rng = StdRng::seed_from_u64(0xc001); - let pp = PublicParameters::setup(1 << CAPACITY, &mut rng).unwrap(); - let (prover, verifier) = Compiler::compile::(&pp, label) - .expect("Circuit should compile successfully"); - let mut proof = Proof::default(); - let public_inputs = Vec::new(); - let circuit = CipherEncrypt::random(&mut rng); - - // Benchmark native cipher encryption - c.bench_function("cipher encryption native", |b| { - b.iter(|| { - PoseidonCipher::encrypt( - black_box(&circuit.message), - black_box(&circuit.shared), - black_box(&circuit.nonce), - ); - }) - }); - - // Benchmark proof creation - c.bench_function("cipher encryption proof generation", |b| { - b.iter(|| { - (proof, _) = prover - .prove(&mut rng, black_box(&circuit)) - .expect("Proof generation should succeed"); - }) - }); - - // Benchmark proof verification - c.bench_function("cipher encryption proof verification", |b| { - b.iter(|| { - verifier - .verify(black_box(&proof), &public_inputs) - .expect("Proof verification should succeed"); - }) - }); -} - -criterion_group! { - name = benches; - config = Criterion::default().sample_size(10); - targets = bench_cipher_encryption -} -criterion_main!(benches); diff --git a/benches/decrypt.rs b/benches/decrypt.rs new file mode 100644 index 0000000..25a248c --- /dev/null +++ b/benches/decrypt.rs @@ -0,0 +1,138 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use dusk_bls12_381::BlsScalar; +use dusk_jubjub::{JubJubAffine, JubJubScalar, GENERATOR_EXTENDED}; +use dusk_plonk::prelude::Error as PlonkError; +use dusk_plonk::prelude::*; +use dusk_poseidon::{decrypt, decrypt_gadget, encrypt}; +use ff::Field; +use once_cell::sync::Lazy; +use rand::rngs::StdRng; +use rand::SeedableRng; + +const MESSAGE_LEN: usize = 2; + +static PUB_PARAMS: Lazy = Lazy::new(|| { + let mut rng = StdRng::seed_from_u64(0xfab); + + const CAPACITY: usize = 11; + PublicParameters::setup(1 << CAPACITY, &mut rng) + .expect("Setup of public params should pass") +}); +static LABEL: &[u8] = b"hash-gadget-tester"; + +#[derive(Debug)] +struct DecryptionCircuit { + pub cipher: Vec, + pub shared_secret: JubJubAffine, + pub nonce: BlsScalar, +} + +impl DecryptionCircuit { + pub fn random(rng: &mut StdRng) -> Self { + let mut message = [BlsScalar::zero(); MESSAGE_LEN]; + message + .iter_mut() + .for_each(|s| *s = BlsScalar::random(&mut *rng)); + let shared_secret = + GENERATOR_EXTENDED * &JubJubScalar::random(&mut *rng); + let shared_secret = shared_secret.into(); + let nonce = BlsScalar::random(&mut *rng); + let cipher = encrypt(&message, &shared_secret, &nonce) + .expect("encryption should not fail"); + + Self { + cipher, + shared_secret, + nonce, + } + } +} + +impl Default for DecryptionCircuit { + fn default() -> Self { + let message = [BlsScalar::zero(); MESSAGE_LEN]; + let mut cipher = message.to_vec(); + cipher.push(BlsScalar::zero()); + let shared_secret = JubJubAffine::identity(); + let nonce = BlsScalar::zero(); + + Self { + cipher, + shared_secret, + nonce, + } + } +} + +impl Circuit for DecryptionCircuit { + fn circuit(&self, composer: &mut Composer) -> Result<(), PlonkError> { + // append all variables to the circuit + let mut cipher_wit = Vec::with_capacity(MESSAGE_LEN + 1); + self.cipher + .iter() + .for_each(|c| cipher_wit.push(composer.append_witness(*c))); + let secret_wit = composer.append_point(self.shared_secret); + let nonce_wit = composer.append_witness(self.nonce); + + // decrypt the cipher with the gadget + let _cipher_result = + decrypt_gadget(composer, &cipher_wit, &secret_wit, &nonce_wit) + .expect("decryption should pass"); + + Ok(()) + } +} + +fn bench_decryption(c: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(0x42424242); + + let (prover, verifier) = + Compiler::compile::(&PUB_PARAMS, LABEL) + .expect("compilation should pass"); + + let circuit: DecryptionCircuit = DecryptionCircuit::random(&mut rng); + let public_inputs = Vec::new(); + let mut proof = Proof::default(); + + // Benchmark native cipher decryption + c.bench_function("decrypt 2 BlsScalar", |b| { + b.iter(|| { + _ = decrypt( + black_box(&circuit.cipher), + black_box(&circuit.shared_secret), + black_box(&circuit.nonce), + ); + }) + }); + + // Benchmark proof creation + c.bench_function("decrypt 2 BlsScalar proof generation", |b| { + b.iter(|| { + (proof, _) = prover + .prove(&mut rng, black_box(&circuit)) + .expect("Proof generation should succeed"); + }) + }); + + // Benchmark proof verification + c.bench_function("decrypt 2 BlsScalar proof verification", |b| { + b.iter(|| { + verifier + .verify(black_box(&proof), &public_inputs) + .expect("Proof verification should succeed"); + }) + }); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = bench_decryption +} +criterion_main!(benches); diff --git a/benches/encrypt.rs b/benches/encrypt.rs new file mode 100644 index 0000000..a3324e4 --- /dev/null +++ b/benches/encrypt.rs @@ -0,0 +1,134 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use dusk_bls12_381::BlsScalar; +use dusk_jubjub::{JubJubAffine, JubJubScalar, GENERATOR_EXTENDED}; +use dusk_plonk::prelude::Error as PlonkError; +use dusk_plonk::prelude::*; +use dusk_poseidon::{encrypt, encrypt_gadget}; +use ff::Field; +use once_cell::sync::Lazy; +use rand::rngs::StdRng; +use rand::SeedableRng; + +const MESSAGE_LEN: usize = 2; + +static PUB_PARAMS: Lazy = Lazy::new(|| { + let mut rng = StdRng::seed_from_u64(0xfab); + + const CAPACITY: usize = 11; + PublicParameters::setup(1 << CAPACITY, &mut rng) + .expect("Setup of public params should pass") +}); +static LABEL: &[u8] = b"hash-gadget-tester"; + +#[derive(Debug)] +struct EncryptionCircuit { + pub message: [BlsScalar; MESSAGE_LEN], + pub shared_secret: JubJubAffine, + pub nonce: BlsScalar, +} + +impl EncryptionCircuit { + pub fn random(rng: &mut StdRng) -> Self { + let mut message = [BlsScalar::zero(); MESSAGE_LEN]; + message + .iter_mut() + .for_each(|s| *s = BlsScalar::random(&mut *rng)); + let shared_secret = + GENERATOR_EXTENDED * &JubJubScalar::random(&mut *rng); + let nonce = BlsScalar::random(&mut *rng); + + Self { + message, + shared_secret: shared_secret.into(), + nonce, + } + } +} + +impl Default for EncryptionCircuit { + fn default() -> Self { + let message = [BlsScalar::zero(); MESSAGE_LEN]; + let shared_secret = JubJubAffine::identity(); + let nonce = BlsScalar::zero(); + + Self { + message, + shared_secret, + nonce, + } + } +} + +impl Circuit for EncryptionCircuit { + fn circuit(&self, composer: &mut Composer) -> Result<(), PlonkError> { + // append all variables to the circuit + let mut message_wit = [Composer::ZERO; MESSAGE_LEN]; + message_wit + .iter_mut() + .zip(self.message) + .for_each(|(w, m)| *w = composer.append_witness(m)); + let secret_wit = composer.append_point(self.shared_secret); + let nonce_wit = composer.append_witness(self.nonce); + + // encrypt the message with the gadget + let _cipher_result = + encrypt_gadget(composer, &message_wit, &secret_wit, &nonce_wit) + .expect("encryption should pass"); + + Ok(()) + } +} + +fn bench_encryption(c: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(0x42424242); + + let (prover, verifier) = + Compiler::compile::(&PUB_PARAMS, LABEL) + .expect("compilation should pass"); + + let circuit: EncryptionCircuit = EncryptionCircuit::random(&mut rng); + let public_inputs = Vec::new(); + let mut proof = Proof::default(); + + // Benchmark native cipher decryption + c.bench_function("encrypt 2 BlsScalar", |b| { + b.iter(|| { + let _ = encrypt( + black_box(&circuit.message), + black_box(&circuit.shared_secret), + black_box(&circuit.nonce), + ); + }) + }); + + // Benchmark proof creation + c.bench_function("encrypt 2 BlsScalar proof generation", |b| { + b.iter(|| { + (proof, _) = prover + .prove(&mut rng, black_box(&circuit)) + .expect("Proof generation should succeed"); + }) + }); + + // Benchmark proof verification + c.bench_function("encrypt 2 BlsScalar proof verification", |b| { + b.iter(|| { + verifier + .verify(black_box(&proof), &public_inputs) + .expect("Proof verification should succeed"); + }) + }); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = bench_encryption +} +criterion_main!(benches); diff --git a/benches/hash.rs b/benches/hash.rs index 7afe097..93f3179 100644 --- a/benches/hash.rs +++ b/benches/hash.rs @@ -67,14 +67,14 @@ fn bench_sponge(c: &mut Criterion) { let circuit = SpongeCircuit::new(message, public_inputs[0]); // Benchmark sponge native - c.bench_function("sponge native", |b| { + c.bench_function("hash 4 BlsScalar", |b| { b.iter(|| { let _ = Hash::digest(Domain::Merkle4, black_box(&circuit.message)); }) }); // Benchmark proof creation - c.bench_function("sponge proof generation", |b| { + c.bench_function("hash 4 BlsScalar proof generation", |b| { b.iter(|| { (proof, _) = prover .prove(&mut rng, black_box(&circuit)) @@ -83,7 +83,7 @@ fn bench_sponge(c: &mut Criterion) { }); // Benchmark proof verification - c.bench_function("sponge proof verification", |b| { + c.bench_function("hash 4 BlsScalar proof verification", |b| { b.iter(|| { verifier .verify(black_box(&proof), &public_inputs) diff --git a/benches/cipher_decrypt.rs b/decrypt.rs similarity index 98% rename from benches/cipher_decrypt.rs rename to decrypt.rs index eb7fa38..67338ca 100644 --- a/benches/cipher_decrypt.rs +++ b/decrypt.rs @@ -4,7 +4,7 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use dusk_poseidon::{decrypt_gadget, PoseidonCipher}; +use dusk_poseidon::{decrypt, decrypt_gadget}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use dusk_jubjub::GENERATOR; diff --git a/src/cipher.rs b/src/cipher.rs deleted file mode 100644 index ff8b287..0000000 --- a/src/cipher.rs +++ /dev/null @@ -1,360 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. -// -// Copyright (c) DUSK NETWORK. All rights reserved. - -//! # Dusk-Poseidon Cipher -//! -//! Encryption/decryption implementation with Dusk-Poseidon backend. -//! -//! This implementation is optimized for a message containing 2 scalars. -//! -//! ## Shared secret -//! -//! The shared secret is a point on the JubJub curve on the affine form. -//! -//! This implementation does not cover the shared secret derivation strategy. -//! -//! The suggestion is to use a Diffie-Hellman key exchange, as shown in the example. Check [dusk-jubjub](https://github.com/dusk-network/jubjub) for further reference. -//! -//! ## Example -//! -//! ```rust -//! use core::ops::Mul; -//! use dusk_bls12_381::BlsScalar; -//! use dusk_jubjub::{dhke, JubJubExtended, JubJubScalar, GENERATOR}; -//! use dusk_poseidon::PoseidonCipher; -//! use rand::rngs::OsRng; -//! use ff::Field; -//! -//! fn sender( -//! sender_secret: &JubJubScalar, -//! receiver_public: &JubJubExtended, -//! message: &[BlsScalar], -//! ) -> (PoseidonCipher, BlsScalar) { -//! // Use the Diffie-Hellman protocol to generate a shared secret -//! let shared_secret = dhke(sender_secret, receiver_public); -//! -//! // Generate a random nonce that will be public -//! let nonce = BlsScalar::random(&mut OsRng); -//! -//! // Encrypt the message -//! let cipher = PoseidonCipher::encrypt(&message, &shared_secret, &nonce); -//! -//! (cipher, nonce) -//! } -//! -//! fn receiver( -//! receiver_secret: &JubJubScalar, -//! sender_public: &JubJubExtended, -//! cipher: &PoseidonCipher, -//! nonce: &BlsScalar, -//! ) -> [BlsScalar; PoseidonCipher::capacity()] { -//! // Use the Diffie-Hellman protocol to generate a shared secret -//! let shared_secret = dhke(receiver_secret, sender_public); -//! -//! // Decrypt the message -//! cipher -//! .decrypt(&shared_secret, &nonce) -//! .expect("Failed to decrypt!") -//! } -//! -//! let mut rng = OsRng; -//! -//! // Generate a secret and a public key for Bob -//! let bob_secret = JubJubScalar::random(&mut rng); -//! let bob_public = GENERATOR.to_niels().mul(&bob_secret); -//! -//! // Generate a secret and a public key for Alice -//! let alice_secret = JubJubScalar::random(&mut rng); -//! let alice_public = GENERATOR.to_niels().mul(&alice_secret); -//! -//! // Generate a secret message -//! let a = BlsScalar::random(&mut rng); -//! let b = BlsScalar::random(&mut rng); -//! let message = [a, b]; -//! -//! // Bob's view (sender) -//! // The cipher and nonce are safe to broadcast publicly -//! let (cipher, nonce) = sender(&bob_secret, &alice_public, &message); -//! -//! // Alice's view (receiver) -//! let decrypted_message = receiver(&alice_secret, &bob_public, &cipher, &nonce); -//! -//! // Successful communication -//! assert_eq!(decrypted_message, message); -//! ``` - -use dusk_bls12_381::BlsScalar; -use dusk_bytes::{DeserializableSlice, Error as BytesError, Serializable}; -use dusk_jubjub::JubJubAffine; -use dusk_safe::Safe; - -use crate::hades::{ScalarPermutation, WIDTH}; - -#[cfg(feature = "rkyv-impl")] -use bytecheck::CheckBytes; -#[cfg(feature = "rkyv-impl")] -use rkyv::{Archive, Deserialize, Serialize}; - -const MESSAGE_CAPACITY: usize = 2; -const CIPHER_SIZE: usize = MESSAGE_CAPACITY + 1; -const CIPHER_BYTES_SIZE: usize = CIPHER_SIZE * BlsScalar::SIZE; - -/// Encapsulates an encrypted data -#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Default)] -#[cfg_attr( - feature = "rkyv-impl", - derive(Archive, Deserialize, Serialize), - archive_attr(derive(CheckBytes)) -)] -pub struct PoseidonCipher { - cipher: [BlsScalar; CIPHER_SIZE], -} - -impl Serializable for PoseidonCipher { - type Error = BytesError; - - /// Create an instance from a previous `PoseidonCipher::to_bytes` function - fn from_bytes(bytes: &[u8; Self::SIZE]) -> Result { - let mut cipher: [BlsScalar; CIPHER_SIZE] = - [BlsScalar::zero(); CIPHER_SIZE]; - - for (i, scalar) in cipher.iter_mut().enumerate() { - let idx = i * BlsScalar::SIZE; - let len = idx + BlsScalar::SIZE; - *scalar = BlsScalar::from_slice(&bytes[idx..len])?; - } - - Ok(Self::new(cipher)) - } - - /// Convert the instance to a bytes representation - fn to_bytes(&self) -> [u8; Self::SIZE] { - let mut bytes = [0u8; Self::SIZE]; - - self.cipher.iter().enumerate().for_each(|(i, c)| { - let n = i * BlsScalar::SIZE; - bytes[n..n + BlsScalar::SIZE].copy_from_slice(&c.to_bytes()); - }); - - bytes - } -} - -impl PoseidonCipher { - /// [`PoseidonCipher`] constructor - pub const fn new(cipher: [BlsScalar; CIPHER_SIZE]) -> Self { - Self { cipher } - } - - /// Maximum number of scalars allowed per message - pub const fn capacity() -> usize { - MESSAGE_CAPACITY - } - - /// Number of scalars used in a cipher - pub const fn cipher_size() -> usize { - CIPHER_SIZE - } - - /// Number of bytes used by from/to bytes `PoseidonCipher` function - pub const fn cipher_size_bytes() -> usize { - CIPHER_BYTES_SIZE - } - - /// Returns the initial state of the encryption - pub fn initial_state( - secret: &JubJubAffine, - nonce: BlsScalar, - ) -> [BlsScalar; WIDTH] { - [ - // Domain - Maximum plaintext length of the elements of Fq, as - // defined in the paper - BlsScalar::from_raw([0x100000000u64, 0, 0, 0]), - // The size of the message is constant because any absent input is - // replaced by zero - BlsScalar::from_raw([MESSAGE_CAPACITY as u64, 0, 0, 0]), - secret.get_u(), - secret.get_v(), - nonce, - ] - } - - /// Getter for the cipher - pub const fn cipher(&self) -> &[BlsScalar; CIPHER_SIZE] { - &self.cipher - } - - /// Encrypt a slice of scalars into an internal cipher representation - /// - /// The message size will be truncated to [`PoseidonCipher::capacity()`] - /// bits - pub fn encrypt( - message: &[BlsScalar], - secret: &JubJubAffine, - nonce: &BlsScalar, - ) -> Self { - let zero = BlsScalar::zero(); - let mut cipher = [zero; CIPHER_SIZE]; - let mut state = PoseidonCipher::initial_state(secret, *nonce); - - ScalarPermutation::new().permute(&mut state); - - (0..MESSAGE_CAPACITY).for_each(|i| { - state[i + 1] += if i < message.len() { - message[i] - } else { - BlsScalar::zero() - }; - - cipher[i] = state[i + 1]; - }); - - ScalarPermutation::new().permute(&mut state); - cipher[MESSAGE_CAPACITY] = state[1]; - - PoseidonCipher::new(cipher) - } - - /// Perform the decrypt of a previously encrypted message. - /// - /// Will return `None` if the decryption fails. - pub fn decrypt( - &self, - secret: &JubJubAffine, - nonce: &BlsScalar, - ) -> Option<[BlsScalar; MESSAGE_CAPACITY]> { - let zero = BlsScalar::zero(); - let mut message = [zero; MESSAGE_CAPACITY]; - let mut state = PoseidonCipher::initial_state(secret, *nonce); - - ScalarPermutation::new().permute(&mut state); - - (0..MESSAGE_CAPACITY).for_each(|i| { - message[i] = self.cipher[i] - state[i + 1]; - state[i + 1] = self.cipher[i]; - }); - - ScalarPermutation::new().permute(&mut state); - - if self.cipher[MESSAGE_CAPACITY] != state[1] { - return None; - } - - Some(message) - } -} - -#[cfg(feature = "zk")] -pub mod zk { - use super::PoseidonCipher; - use crate::hades::{GadgetPermutation, WIDTH}; - - use dusk_plonk::prelude::*; - use dusk_safe::Safe; - - impl PoseidonCipher { - /// Returns the initial state of the encryption within a composer - /// circuit - pub fn initial_state_circuit( - composer: &mut Composer, - ks0: Witness, - ks1: Witness, - nonce: Witness, - ) -> [Witness; WIDTH] { - let domain = BlsScalar::from_raw([0x100000000u64, 0, 0, 0]); - let domain = composer.append_constant(domain); - - let length = BlsScalar::from_raw([ - PoseidonCipher::capacity() as u64, - 0, - 0, - 0, - ]); - let length = composer.append_constant(length); - - [domain, length, ks0, ks1, nonce] - } - } - - /// Given a shared secret calculated using any key protocol compatible with - /// bls and jubjub, perform the encryption of the message. - /// - /// The returned set of variables is the cipher text - pub fn encrypt( - composer: &mut Composer, - shared_secret: &WitnessPoint, - nonce: Witness, - message: &[Witness], - ) -> [Witness; PoseidonCipher::cipher_size()] { - let ks0 = *shared_secret.x(); - let ks1 = *shared_secret.y(); - - let mut cipher = [Composer::ZERO; PoseidonCipher::cipher_size()]; - - let mut state = - PoseidonCipher::initial_state_circuit(composer, ks0, ks1, nonce); - - GadgetPermutation::new(composer).permute(&mut state); - - (0..PoseidonCipher::capacity()).for_each(|i| { - let x = if i < message.len() { - message[i] - } else { - Composer::ZERO - }; - - let constraint = - Constraint::new().left(1).a(state[i + 1]).right(1).b(x); - - state[i + 1] = composer.gate_add(constraint); - - cipher[i] = state[i + 1]; - }); - - GadgetPermutation::new(composer).permute(&mut state); - cipher[PoseidonCipher::capacity()] = state[1]; - - cipher - } - - /// Given a shared secret calculated using any key protocol compatible with - /// bls and jubjub, perform the decryption of the cipher. - /// - /// The returned set of variables is the original message - pub fn decrypt( - composer: &mut Composer, - shared_secret: &WitnessPoint, - nonce: Witness, - cipher: &[Witness], - ) -> [Witness; PoseidonCipher::capacity()] { - let ks0 = *shared_secret.x(); - let ks1 = *shared_secret.y(); - - let mut message = [Composer::ZERO; PoseidonCipher::capacity()]; - let mut state = - PoseidonCipher::initial_state_circuit(composer, ks0, ks1, nonce); - - GadgetPermutation::new(composer).permute(&mut state); - - (0..PoseidonCipher::capacity()).for_each(|i| { - let constraint = Constraint::new() - .left(1) - .a(cipher[i]) - .right(-BlsScalar::one()) - .b(state[i + 1]); - - message[i] = composer.gate_add(constraint); - - state[i + 1] = cipher[i]; - }); - - GadgetPermutation::new(composer).permute(&mut state); - - composer.assert_equal(cipher[PoseidonCipher::capacity()], state[1]); - - message - } -} diff --git a/src/encryption.rs b/src/encryption.rs new file mode 100644 index 0000000..7f10e7f --- /dev/null +++ b/src/encryption.rs @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +#[cfg(feature = "zk")] +pub(crate) mod gadget; + +use alloc::vec::Vec; + +use dusk_bls12_381::BlsScalar; +use dusk_jubjub::JubJubAffine; + +use crate::hades::ScalarPermutation; +use crate::{Domain, Error}; + +/// This function encrypts a given message with a shared secret point on the +/// jubjub-curve and a bls-scalar nonce using the poseidon hash function. +/// +/// The shared secret is expected to be a valid point on the jubjub-curve. +/// +/// The cipher-text will always yield exactly one element more than the message. +pub fn encrypt( + message: impl AsRef<[BlsScalar]>, + shared_secret: &JubJubAffine, + nonce: &BlsScalar, +) -> Result, Error> { + Ok(dusk_safe::encrypt( + ScalarPermutation::new(), + Domain::Encryption, + message, + &[shared_secret.get_u(), shared_secret.get_v()], + nonce, + )?) +} + +/// This function decrypts a message from a given cipher-text with a shared +/// secret point on the jubjub-curve and a bls-scalar nonce using the poseidon +/// hash function. +/// +/// The shared secret is expected to be a valid point on the jubjub-curve. +/// +/// The cipher-text will always yield exactly one element more than the message. +pub fn decrypt( + cipher: impl AsRef<[BlsScalar]>, + shared_secret: &JubJubAffine, + nonce: &BlsScalar, +) -> Result, Error> { + Ok(dusk_safe::decrypt( + ScalarPermutation::new(), + Domain::Encryption, + cipher, + &[shared_secret.get_u(), shared_secret.get_v()], + nonce, + )?) +} diff --git a/src/encryption/gadget.rs b/src/encryption/gadget.rs new file mode 100644 index 0000000..45ee9a6 --- /dev/null +++ b/src/encryption/gadget.rs @@ -0,0 +1,55 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use alloc::vec::Vec; + +use dusk_plonk::prelude::{Composer, Witness, WitnessPoint}; + +use crate::hades::GadgetPermutation; +use crate::{Domain, Error}; + +/// This function encrypts a given message with a shared secret point on the +/// jubjub-curve and a bls-scalar nonce using the poseidon hash function. +/// +/// The shared secret is expected to be a valid point on the jubjub-curve. +/// +/// The cipher-text will always yield exactly one element more than the message. +pub fn encrypt_gadget( + composer: &mut Composer, + message: impl AsRef<[Witness]>, + shared_secret: &WitnessPoint, + nonce: &Witness, +) -> Result, Error> { + Ok(dusk_safe::encrypt( + GadgetPermutation::new(composer), + Domain::Encryption, + message, + &[*shared_secret.x(), *shared_secret.y()], + nonce, + )?) +} + +/// This function decrypts a message from a given cipher-text with a shared +/// secret point on the jubjub-curve and a bls-scalar nonce using the poseidon +/// hash function. +/// +/// The shared secret is expected to be a valid point on the jubjub-curve. +/// +/// The cipher-text will always yield exactly one element more than the message. +pub fn decrypt_gadget( + composer: &mut Composer, + cipher: impl AsRef<[Witness]>, + shared_secret: &WitnessPoint, + nonce: &Witness, +) -> Result, Error> { + Ok(dusk_safe::decrypt( + GadgetPermutation::new(composer), + Domain::Encryption, + cipher, + &[*shared_secret.x(), *shared_secret.y()], + nonce, + )?) +} diff --git a/src/error.rs b/src/error.rs index fe78480..2b0f235 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,6 +18,17 @@ pub enum Error { /// The input doesn't yield enough input elements. TooFewInputElements, + + /// Failed to encrypt the message into the cipher with the provided secret + /// and nonce. + EncryptionFailed, + + /// Failed to decrypt the message from the cipher with the provided secret + /// and nonce. + DecryptionFailed, + + /// Invalid point on the jubjub-curve + InvalidPoint, } impl From for Error { @@ -26,6 +37,8 @@ impl From for Error { SafeError::IOPatternViolation => Self::IOPatternViolation, SafeError::InvalidIOPattern => Self::InvalidIOPattern, SafeError::TooFewInputElements => Self::TooFewInputElements, + SafeError::EncryptionFailed => Self::EncryptionFailed, + SafeError::DecryptionFailed => Self::DecryptionFailed, } } } diff --git a/src/hades/permutation/gadget.rs b/src/hades/permutation/gadget.rs index 4ce9f33..46bfb72 100644 --- a/src/hades/permutation/gadget.rs +++ b/src/hades/permutation/gadget.rs @@ -12,8 +12,8 @@ use crate::hades::{MDS_MATRIX, ROUND_CONSTANTS, WIDTH}; use super::Hades; -/// An implementation for the [`Hades`]permutation operating on [`Witness`]es. -/// Requires a reference to a `ConstraintSystem`. +/// An implementation for the [`Hades`] permutation operating on [`Witness`]es. +/// Requires a reference to a plonk circuit [`Composer`]. pub(crate) struct GadgetPermutation<'a> { /// A reference to the constraint system used by the gadgets composer: &'a mut Composer, @@ -33,7 +33,8 @@ impl<'a> Safe for GadgetPermutation<'a> { fn tag(&mut self, input: &[u8]) -> Witness { let tag = BlsScalar::hash_to_scalar(input.as_ref()); - self.composer.append_witness(tag) + // append the tag as a constant + self.composer.append_constant(tag) } fn add(&mut self, right: &Witness, left: &Witness) -> Witness { @@ -48,7 +49,7 @@ impl<'a> Hades for GadgetPermutation<'a> { round: usize, state: &mut [Witness; WIDTH], ) { - // To safe constraints we only add the constants here in the first + // To save constraints we only add the constants here in the first // round. The remaining constants will be added in the matrix // multiplication. if round == 0 { @@ -131,6 +132,25 @@ impl<'a> Hades for GadgetPermutation<'a> { } } +#[cfg(feature = "encryption")] +impl dusk_safe::Encryption for GadgetPermutation<'_> { + fn subtract(&mut self, minuend: &Witness, subtrahend: &Witness) -> Witness { + let constraint = Constraint::new() + .left(1) + .a(*minuend) + .right(-BlsScalar::one()) + .b(*subtrahend); + self.composer.gate_add(constraint) + } + + fn is_equal(&mut self, lhs: &Witness, rhs: &Witness) -> bool { + self.composer.assert_equal(*lhs, *rhs); + // for the encryption to work we need to return true here, the proof + // creation will fail at a later point if the above assertion isn't met + true + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/hades/permutation/scalar.rs b/src/hades/permutation/scalar.rs index 806643a..79e10c7 100644 --- a/src/hades/permutation/scalar.rs +++ b/src/hades/permutation/scalar.rs @@ -64,6 +64,21 @@ impl Hades for ScalarPermutation { } } +#[cfg(feature = "encryption")] +impl dusk_safe::Encryption for ScalarPermutation { + fn subtract( + &mut self, + minuend: &BlsScalar, + subtrahend: &BlsScalar, + ) -> BlsScalar { + minuend - subtrahend + } + + fn is_equal(&mut self, lhs: &BlsScalar, rhs: &BlsScalar) -> bool { + lhs == rhs + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 8139e53..fd19333 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,12 +21,11 @@ mod hash; pub use hash::gadget::HashGadget; pub use hash::{Domain, Hash}; -#[cfg(feature = "cipher")] -mod cipher; -#[cfg(feature = "cipher")] -pub use cipher::PoseidonCipher; -#[cfg(feature = "cipher")] +#[cfg(feature = "encryption")] +mod encryption; + +#[cfg(feature = "encryption")] #[cfg(feature = "zk")] -pub use cipher::{ - zk::decrypt as decrypt_gadget, zk::encrypt as encrypt_gadget, -}; +pub use encryption::gadget::{decrypt_gadget, encrypt_gadget}; +#[cfg(feature = "encryption")] +pub use encryption::{decrypt, encrypt}; diff --git a/tests/cipher.rs b/tests/cipher.rs deleted file mode 100644 index 847a364..0000000 --- a/tests/cipher.rs +++ /dev/null @@ -1,260 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. -// -// Copyright (c) DUSK NETWORK. All rights reserved. - -#![cfg(feature = "cipher")] - -use core::ops::Mul; -use dusk_bls12_381::BlsScalar; -use dusk_bytes::Serializable; -use dusk_jubjub::{JubJubAffine, JubJubScalar, GENERATOR}; -use dusk_poseidon::PoseidonCipher; -use ff::Field; -use rand::rngs::OsRng; -use rand::RngCore; - -fn gen() -> ( - [BlsScalar; PoseidonCipher::capacity()], - JubJubAffine, - BlsScalar, -) { - let mut rng = OsRng; - let mut message = [BlsScalar::zero(); PoseidonCipher::capacity()]; - message - .iter_mut() - .for_each(|m| *m = BlsScalar::random(&mut rng)); - - let mut secret = [0u8; 64]; - rng.fill_bytes(&mut secret); - let secret = JubJubScalar::from_bytes_wide(&secret); - let secret = GENERATOR.to_niels().mul(&secret).into(); - - let nonce = BlsScalar::random(&mut OsRng); - - (message, secret, nonce) -} - -#[test] -fn sanity() { - // The secret is always a pair with nonce, so the message capacity should be - // at least 2 - assert!(PoseidonCipher::capacity() > 1); - - // The cipher size only makes sense to be `capacity + 1` - assert_eq!( - PoseidonCipher::cipher_size(), - PoseidonCipher::capacity() + 1 - ); - - // The hades permutation cannot be performed if the cipher is bigger than - // hades width - assert!(dusk_poseidon::HADES_WIDTH >= PoseidonCipher::cipher_size()); -} - -#[test] -fn encrypt() { - let (message, secret, nonce) = gen(); - - let cipher = PoseidonCipher::encrypt(&message, &secret, &nonce); - let decrypt = cipher - .decrypt(&secret, &nonce) - .expect("decryption should succeed"); - - assert_eq!(message, decrypt); -} - -#[test] -fn single_bit() { - let (_, secret, nonce) = gen(); - let message = BlsScalar::random(&mut OsRng); - - let cipher = PoseidonCipher::encrypt(&[message], &secret, &nonce); - let decrypt = cipher - .decrypt(&secret, &nonce) - .expect("decryption should succeed"); - - assert_eq!(message, decrypt[0]); -} - -#[test] -fn overflow() { - let (_, secret, nonce) = gen(); - let message = - [BlsScalar::random(&mut OsRng); PoseidonCipher::capacity() + 1]; - - let cipher = PoseidonCipher::encrypt(&message, &secret, &nonce); - let decrypt = cipher - .decrypt(&secret, &nonce) - .expect("decryption should succeed"); - - assert_eq!(message[0..PoseidonCipher::capacity()], decrypt); -} - -#[test] -fn wrong_key_fail() { - let (message, secret, nonce) = gen(); - let (_, wrong_secret, _) = gen(); - - let cipher = PoseidonCipher::encrypt(&message, &secret, &nonce); - assert!(cipher.decrypt(&wrong_secret, &nonce).is_none()); -} - -#[test] -fn bytes() { - let (message, secret, nonce) = gen(); - - let cipher = PoseidonCipher::encrypt(&message, &secret, &nonce); - - let bytes = cipher.to_bytes(); - let restored_cipher = PoseidonCipher::from_bytes(&bytes).unwrap(); - - assert_eq!(cipher, restored_cipher); - - let decrypt = restored_cipher - .decrypt(&secret, &nonce) - .expect("decryption should succeed"); - - assert_eq!(message, decrypt); -} - -#[cfg(feature = "zk")] -mod zk { - use super::*; - - use dusk_jubjub::{dhke, JubJubExtended, GENERATOR_EXTENDED}; - use dusk_plonk::prelude::Error as PlonkError; - use dusk_plonk::prelude::*; - use dusk_poseidon::{decrypt_gadget, encrypt_gadget}; - use rand::rngs::StdRng; - use rand::SeedableRng; - - #[derive(Debug)] - pub struct TestCipherCircuit<'a> { - secret: JubJubScalar, - public: JubJubExtended, - nonce: BlsScalar, - message: &'a [BlsScalar], - cipher: &'a [BlsScalar], - } - - impl<'a> TestCipherCircuit<'a> { - pub const fn new( - secret: JubJubScalar, - public: JubJubExtended, - nonce: BlsScalar, - message: &'a [BlsScalar], - cipher: &'a [BlsScalar], - ) -> Self { - Self { - secret, - public, - nonce, - message, - cipher, - } - } - } - - impl<'a> Default for TestCipherCircuit<'a> { - fn default() -> Self { - let secret = Default::default(); - let public = Default::default(); - let nonce = Default::default(); - - const MESSAGE: [BlsScalar; PoseidonCipher::capacity()] = - [BlsScalar::zero(); PoseidonCipher::capacity()]; - const CIPHER: [BlsScalar; PoseidonCipher::cipher_size()] = - [BlsScalar::zero(); PoseidonCipher::cipher_size()]; - - Self::new(secret, public, nonce, &MESSAGE, &CIPHER) - } - } - - impl<'a> Circuit for TestCipherCircuit<'a> { - fn circuit(&self, composer: &mut Composer) -> Result<(), PlonkError> { - let nonce = composer.append_witness(self.nonce); - - let secret = composer.append_witness(self.secret); - let public = composer.append_point(self.public); - - let shared = composer.component_mul_point(secret, public); - - let mut message_circuit = - [Composer::ZERO; PoseidonCipher::capacity()]; - - self.message - .iter() - .zip(message_circuit.iter_mut()) - .for_each(|(m, v)| { - *v = composer.append_witness(*m); - }); - - let cipher_gadget = - encrypt_gadget(composer, &shared, nonce, &message_circuit); - - self.cipher - .iter() - .zip(cipher_gadget.iter()) - .for_each(|(c, g)| { - let x = composer.append_witness(*c); - composer.assert_equal(x, *g); - }); - - let message_gadget = - decrypt_gadget(composer, &shared, nonce, &cipher_gadget); - - self.message.iter().zip(message_gadget.iter()).for_each( - |(m, g)| { - let x = composer.append_witness(*m); - composer.assert_equal(x, *g); - }, - ); - - Ok(()) - } - } - - #[test] - fn gadget() -> Result<(), PlonkError> { - // Generate a secret and a public key for Bob - let bob_secret = JubJubScalar::random(&mut OsRng); - - // Generate a secret and a public key for Alice - let alice_secret = JubJubScalar::random(&mut OsRng); - let alice_public = GENERATOR_EXTENDED * alice_secret; - - // Generate a shared secret - let shared_secret = dhke(&bob_secret, &alice_public); - - // Generate a secret message - let a = BlsScalar::random(&mut OsRng); - let b = BlsScalar::random(&mut OsRng); - let message = [a, b]; - - // Perform the encryption - let nonce = BlsScalar::random(&mut OsRng); - let cipher = PoseidonCipher::encrypt(&message, &shared_secret, &nonce); - - let label = b"poseidon-cipher"; - let size = 13; - - let pp = PublicParameters::setup(1 << size, &mut OsRng)?; - let (prover, verifier) = - Compiler::compile::(&pp, label)?; - let mut rng = StdRng::seed_from_u64(0xbeef); - - let circuit = TestCipherCircuit::new( - bob_secret, - alice_public, - nonce, - &message, - cipher.cipher(), - ); - - let (proof, public_inputs) = prover.prove(&mut rng, &circuit)?; - - verifier.verify(&proof, &public_inputs) - } -} diff --git a/tests/encryption.rs b/tests/encryption.rs new file mode 100644 index 0000000..238eb25 --- /dev/null +++ b/tests/encryption.rs @@ -0,0 +1,115 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +#![cfg(feature = "encryption")] + +use dusk_bls12_381::BlsScalar; +use dusk_jubjub::{JubJubAffine, JubJubScalar, GENERATOR_EXTENDED}; +use dusk_poseidon::{decrypt, encrypt, Error}; +use ff::Field; +use rand::rngs::StdRng; +use rand::SeedableRng; + +fn encryption_variables( + rng: &mut StdRng, + message_len: usize, +) -> (Vec, JubJubAffine, BlsScalar) { + let mut message = Vec::with_capacity(message_len); + for _ in 0..message_len { + message.push(BlsScalar::random(&mut *rng)); + } + let shared_secret = GENERATOR_EXTENDED * &JubJubScalar::random(&mut *rng); + let nonce = BlsScalar::random(&mut *rng); + + (message, shared_secret.into(), nonce) +} + +#[test] +fn encrypt_decrypt() -> Result<(), Error> { + let mut rng = StdRng::seed_from_u64(0x42424242); + let message_len = 42usize; + + let (message, shared_secret, nonce) = + encryption_variables(&mut rng, message_len); + + let cipher = encrypt(&message, &shared_secret, &nonce)?; + + let decrypted_message = decrypt(&cipher, &shared_secret, &nonce)?; + + assert_eq!(decrypted_message, message); + + Ok(()) +} + +#[test] +fn incorrect_shared_secret_fails() -> Result<(), Error> { + let mut rng = StdRng::seed_from_u64(0x42424242); + let message_len = 21usize; + + let (message, shared_secret, nonce) = + encryption_variables(&mut rng, message_len); + + let cipher = encrypt(&message, &shared_secret, &nonce)?; + + let wrong_shared_secret = + GENERATOR_EXTENDED * &JubJubScalar::random(&mut rng); + assert_ne!(shared_secret, wrong_shared_secret.into()); + + assert_eq!( + decrypt(&cipher, &wrong_shared_secret.into(), &nonce,).unwrap_err(), + Error::DecryptionFailed + ); + + Ok(()) +} + +#[test] +fn incorrect_nonce_fails() -> Result<(), Error> { + let mut rng = StdRng::seed_from_u64(0x42424242); + let message_len = 21usize; + + let (message, shared_secret, nonce) = + encryption_variables(&mut rng, message_len); + + let cipher = encrypt(&message, &shared_secret, &nonce)?; + + let wrong_nonce = BlsScalar::random(&mut rng); + assert_ne!(nonce, wrong_nonce); + + assert_eq!( + decrypt(&cipher, &shared_secret, &wrong_nonce,).unwrap_err(), + Error::DecryptionFailed + ); + + Ok(()) +} + +#[test] +fn incorrect_cipher_fails() -> Result<(), Error> { + let mut rng = StdRng::seed_from_u64(0x42424242); + let message_len = 21usize; + + let (message, shared_secret, nonce) = + encryption_variables(&mut rng, message_len); + + let cipher = encrypt(&message, &shared_secret, &nonce)?; + + let mut wrong_cipher = cipher.clone(); + wrong_cipher[message_len] += BlsScalar::from(42); + assert_eq!( + decrypt(&wrong_cipher, &shared_secret, &nonce,).unwrap_err(), + Error::DecryptionFailed + ); + + let mut wrong_cipher = cipher.clone(); + wrong_cipher[0] += BlsScalar::from(42); + assert_eq!( + decrypt(&wrong_cipher, &shared_secret, &nonce,).unwrap_err(), + Error::DecryptionFailed + ); + + Ok(()) +} diff --git a/tests/encryption_gadget.rs b/tests/encryption_gadget.rs new file mode 100644 index 0000000..4c91890 --- /dev/null +++ b/tests/encryption_gadget.rs @@ -0,0 +1,217 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +#![cfg(feature = "encryption")] +#![cfg(feature = "zk")] + +use dusk_bls12_381::BlsScalar; +use dusk_jubjub::{JubJubAffine, JubJubScalar, GENERATOR_EXTENDED}; +use dusk_plonk::prelude::Error as PlonkError; +use dusk_plonk::prelude::*; +use dusk_poseidon::{decrypt_gadget, encrypt, encrypt_gadget}; +use ff::Field; +use once_cell::sync::Lazy; +use rand::rngs::StdRng; +use rand::SeedableRng; + +static PUB_PARAMS: Lazy = Lazy::new(|| { + let mut rng = StdRng::seed_from_u64(0xfab); + + const CAPACITY: usize = 13; + PublicParameters::setup(1 << CAPACITY, &mut rng) + .expect("Setup of public params should pass") +}); +static LABEL: &[u8] = b"hash-gadget-tester"; + +#[derive(Debug)] +struct EncryptionCircuit { + pub message: [BlsScalar; L], + pub cipher: Vec, + pub shared_secret: JubJubAffine, + pub nonce: BlsScalar, +} + +impl EncryptionCircuit { + pub fn random(rng: &mut StdRng) -> Self { + let mut message = [BlsScalar::zero(); L]; + message + .iter_mut() + .for_each(|s| *s = BlsScalar::random(&mut *rng)); + let shared_secret = + GENERATOR_EXTENDED * &JubJubScalar::random(&mut *rng); + let nonce = BlsScalar::random(&mut *rng); + let cipher = encrypt(&message, &shared_secret.into(), &nonce) + .expect("encryption should pass"); + assert_eq!(message.len() + 1, cipher.len()); + + Self { + message, + cipher, + shared_secret: shared_secret.into(), + nonce, + } + } +} + +impl Default for EncryptionCircuit { + fn default() -> Self { + let message = [BlsScalar::zero(); L]; + let mut cipher = message.to_vec(); + cipher.push(BlsScalar::zero()); + let shared_secret = JubJubAffine::identity(); + let nonce = BlsScalar::zero(); + + Self { + message, + cipher, + shared_secret, + nonce, + } + } +} + +impl Circuit for EncryptionCircuit { + fn circuit(&self, composer: &mut Composer) -> Result<(), PlonkError> { + // append all variables to the circuit + let mut message_wit = [Composer::ZERO; L]; + message_wit + .iter_mut() + .zip(self.message) + .for_each(|(w, m)| *w = composer.append_witness(m)); + let secret_wit = composer.append_point(self.shared_secret); + let nonce_wit = composer.append_witness(self.nonce); + + // encrypt the message with the gadget + let cipher_result = + encrypt_gadget(composer, &message_wit, &secret_wit, &nonce_wit) + .expect("encryption should pass"); + + // ensure that the resulting cipher-text is correct + assert_eq!(cipher_result.len(), self.cipher.len()); + cipher_result + .iter() + .zip(&self.cipher) + .for_each(|(r, c)| composer.assert_equal_constant(*r, 0, Some(*c))); + + // decrypt the cipher result with the gadget + let message_result = + decrypt_gadget(composer, &cipher_result, &secret_wit, &nonce_wit) + .expect("decryption should pass"); + + // assert that the decrypted message is the same as in the beginning + assert_eq!(message_result.len(), L); + message_result + .iter() + .zip(message_wit) + .for_each(|(r, w)| composer.assert_equal(*r, w)); + + Ok(()) + } +} + +#[test] +fn encrypt_decrypt() -> Result<(), PlonkError> { + let mut rng = StdRng::seed_from_u64(0x42424242); + const MESSAGE_LEN: usize = 4; + + let (prover, verifier) = Compiler::compile::>( + &PUB_PARAMS, + LABEL, + )?; + + let circuit: EncryptionCircuit = + EncryptionCircuit::random(&mut rng); + + let (proof, _public_inputs) = prover.prove(&mut rng, &circuit)?; + + let public_inputs = &circuit.cipher; + verifier.verify(&proof, public_inputs) +} + +#[test] +fn incorrect_shared_secret_fails() -> Result<(), Error> { + let mut rng = StdRng::seed_from_u64(0x42424242); + const MESSAGE_LEN: usize = 5; + + let (prover, _verifier) = Compiler::compile::< + EncryptionCircuit, + >(&PUB_PARAMS, LABEL)?; + + let mut circuit: EncryptionCircuit = + EncryptionCircuit::random(&mut rng); + + let wrong_shared_secret = + GENERATOR_EXTENDED * &JubJubScalar::random(&mut rng); + circuit.shared_secret = wrong_shared_secret.into(); + + assert!(prover.prove(&mut rng, &circuit).is_err()); + + Ok(()) +} + +#[test] +fn incorrect_nonce_fails() -> Result<(), Error> { + let mut rng = StdRng::seed_from_u64(0x42424242); + const MESSAGE_LEN: usize = 6; + + let (prover, _verifier) = Compiler::compile::< + EncryptionCircuit, + >(&PUB_PARAMS, LABEL)?; + + let mut circuit: EncryptionCircuit = + EncryptionCircuit::random(&mut rng); + + let wrong_nonce = BlsScalar::random(&mut rng); + circuit.nonce = wrong_nonce; + + assert!(prover.prove(&mut rng, &circuit).is_err()); + + Ok(()) +} + +#[test] +fn incorrect_cipher_fails() -> Result<(), Error> { + let mut rng = StdRng::seed_from_u64(0x42424242); + const MESSAGE_LEN: usize = 7; + + let (prover, _verifier) = Compiler::compile::< + EncryptionCircuit, + >(&PUB_PARAMS, LABEL)?; + + let mut circuit: EncryptionCircuit = + EncryptionCircuit::random(&mut rng); + + let mut wrong_cipher = circuit.cipher.clone(); + wrong_cipher[2] = BlsScalar::random(&mut rng); + circuit.cipher = wrong_cipher; + + assert!(prover.prove(&mut rng, &circuit).is_err()); + + Ok(()) +} + +#[test] +fn incorrect_public_input_fails() -> Result<(), Error> { + let mut rng = StdRng::seed_from_u64(0x42424242); + const MESSAGE_LEN: usize = 8; + + let (prover, verifier) = Compiler::compile::>( + &PUB_PARAMS, + LABEL, + )?; + + let circuit: EncryptionCircuit = + EncryptionCircuit::random(&mut rng); + + let (proof, _public_inputs) = prover.prove(&mut rng, &circuit)?; + + let mut wrong_cipher = circuit.cipher.clone(); + wrong_cipher[MESSAGE_LEN] = BlsScalar::random(&mut rng); + + assert!(verifier.verify(&proof, &wrong_cipher).is_err()); + + Ok(()) +} diff --git a/tests/hash.rs b/tests/hash.rs index aa0b5fd..fd63ef9 100644 --- a/tests/hash.rs +++ b/tests/hash.rs @@ -20,7 +20,7 @@ static PUB_PARAMS: Lazy = Lazy::new(|| { const CAPACITY: usize = 12; PublicParameters::setup(1 << CAPACITY, &mut rng) - .expect("Cannot initialize Public Parameters") + .expect("Setup of public params should pass") }); fn compile_and_verify(