Skip to content

Commit

Permalink
hash chain chacha20
Browse files Browse the repository at this point in the history
  • Loading branch information
0xJepsen committed Nov 18, 2024
1 parent 9cddda0 commit 5910e7a
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 5 deletions.
5 changes: 5 additions & 0 deletions builds/target_1024b/chacha20_nivc_1024.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pragma circom 2.1.9;

include "../../circuits/chacha20/chacha20.circom";

component main = ChaCha20_NIVC(256);
5 changes: 5 additions & 0 deletions builds/target_512b/chacha20_nivc_512b.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pragma circom 2.1.9;

include "../../circuits/chacha20/nivc/chacha20_nivc.circom";

component main = ChaCha20_NIVC(128);
7 changes: 2 additions & 5 deletions circuits/chacha20/chacha20.circom
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,9 @@ template ChaCha20(N) {

// the below can be both ciphertext or plaintext depending on the direction
// in => N 32-bit words => N 4 byte words
signal input plainText[N][32];
signal input in[N][32];
// out => N 32-bit words => N 4 byte words
signal input cipherText[N][32];

signal input step_in[1];
signal output step_out[1];
signal output out[N][32];

var tmp[16][32] = [
[
Expand Down
140 changes: 140 additions & 0 deletions circuits/chacha20/nivc/chacha20_nivc.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// initially from https://github.com/reclaimprotocol/zk-symmetric-crypto
// modified for our needs
pragma circom 2.1.9;

include "../chacha-round.circom";
include "../chacha-qr.circom";
include "../../utils/generics-bits.circom";
include "../../utils/hash.circom";
include "../../utils/array.circom";


/** ChaCha20 in counter mode */
// Chacha20 opperates a 4x4 matrix of 32-bit words where the first 4 words are constants: C
// and the next 8 words are the 256 bit key: K. The next 2 words are the block counter: #
// and the last 2 words are the nonce: N.
// +---+---+---+---+
// | C | C | C | C |
// +---+---+---+---+
// | K | K | K | K |
// +---+---+---+---+
// | K | K | K | K |
// +---+---+---+---+
// | # | N | N | N |
// +---+---+---+---+
// paramaterized by n which is the number of 32-bit words to encrypt
template ChaCha20_NIVC(N) {
// key => 8 32-bit words = 32 bytes
signal input key[8][32];
// nonce => 3 32-bit words = 12 bytes
signal input nonce[3][32];
// counter => 32-bit word to apply w nonce
signal input counter[32];

// the below can be both ciphertext or plaintext depending on the direction
// in => N 32-bit words => N 4 byte words
signal input plainText[N][32];
// out => N 32-bit words => N 4 byte words
signal input cipherText[N][32];

signal input step_in[1];
signal output step_out[1];

var tmp[16][32] = [
[
// constant 0x61707865
0, 1, 1, 0, 0, 0, 0, 1, 0,
1, 1, 1, 0, 0, 0, 0, 0, 1,
1, 1, 1, 0, 0, 0, 0, 1, 1,
0, 0, 1, 0, 1
],
[
// constant 0x3320646e
0, 0, 1, 1, 0, 0, 1, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 1, 0, 0, 0, 1, 1,
0, 1, 1, 1, 0
],
[
// constant 0x79622d32
0, 1, 1, 1, 1, 0, 0, 1, 0,
1, 1, 0, 0, 0, 1, 0, 0, 0,
1, 0, 1, 1, 0, 1, 0, 0, 1,
1, 0, 0, 1, 0
],
[
// constant 0x6b206574
0, 1, 1, 0, 1, 0, 1, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 1, 0, 1, 0, 1, 1,
1, 0, 1, 0, 0
],
key[0], key[1], key[2], key[3],
key[4], key[5], key[6], key[7],
counter, nonce[0], nonce[1], nonce[2]
];

// 1 in 32-bit words
signal one[32];
one <== [
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1
];

var i = 0;
var j = 0;

// do the ChaCha20 rounds
// rounds opperates on 4 words at a time
component rounds[N/16];
component xors[N];
component counter_adder[N/16 - 1];

signal computedCipherText[N][32];

for(i = 0; i < N/16; i++) {
rounds[i] = Round();
rounds[i].in <== tmp;
// XOR block with input
for(j = 0; j < 16; j++) {
xors[i*16 + j] = XorBits(32);
xors[i*16 + j].a <== plainText[i*16 + j];
xors[i*16 + j].b <== rounds[i].out[j];
computedCipherText[i*16 + j] <== xors[i*16 + j].out;
}

if(i < N/16 - 1) {
counter_adder[i] = AddBits(32);
counter_adder[i].a <== tmp[12];
counter_adder[i].b <== one;

// increment the counter
tmp[12] = counter_adder[i].out;
}
}

signal ciphertext_equal_check[N][32];
for(var i = 0 ; i < N; i++) {
for(var j = 0 ; j < 32 ; j++) {
ciphertext_equal_check[i][j] <== IsEqual()([computedCipherText[i][j], cipherText[i][j]]);
ciphertext_equal_check[i][j] === 1;
}
}

var packedPlaintext[N]; // Each element will be a 32-bit word
for(var i = 0; i < N; i++) {
packedPlaintext[i] = 0;
for(var j = 0; j < 32; j++) { // Loop through all 32 bits
packedPlaintext[i] += plainText[i][j] * 2**j; // Now we shift by single bits
}
}

signal hash[N];
hash[0] <== PoseidonChainer()([step_in[0], packedPlaintext[0]]);
for(var i = 1 ; i < N ; i++) {
hash[i] <== PoseidonChainer()([hash[i-1], packedPlaintext[i]]);
}
step_out[0] <== hash[N-1];
}
94 changes: 94 additions & 0 deletions circuits/test/chacha20/chacha20-nivc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { WitnessTester } from "circomkit";
import { circomkit, bytesToBigInt, toUint32Array, uintArray32ToBits, bitsToBytes } from "../common";
import { PoseidonModular } from "../common/poseidon";
import { assert } from "chai";


describe("chacha20-nivc", () => {
// this is failing right now
describe("2 block test", () => {
let circuit: WitnessTester<["key", "nonce", "counter", "plainText", "cipherText", "step_in"], ["step_out"]>;
it("should perform encryption", async () => {
circuit = await circomkit.WitnessTester(`ChaCha20`, {
file: "chacha20/nivc/chacha20_nivc",
template: "ChaCha20_NIVC",
params: [16] // number of 32-bit words in the key, 512 / 32 = 16
});
// Test case from RCF https://www.rfc-editor.org/rfc/rfc7539.html#section-2.4.2
// the input encoding here is not the most intuitive. inputs are serialized as little endian.
// i.e. "e4e7f110" is serialized as "10 f1 e7 e4". So the way i am reading in inputs is
// to ensure that every 32 bit word is byte reversed before being turned into bits.
// i think this should be easy when we compute witness in rust.
let test = {
keyBytes: Buffer.from(
[
0x00, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b,
0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13,
0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1a, 0x1b,
0x1c, 0x1d, 0x1e, 0x1f
]
),
nonceBytes: Buffer.from(
[
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x4a,
0x00, 0x00, 0x00, 0x00
]
),
counter: 1,
plaintextBytes: Buffer.from(
[
0x4c, 0x61, 0x64, 0x69, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x47, 0x65, 0x6e, 0x74, 0x6c,
0x65, 0x6d, 0x65, 0x6e, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x61, 0x73,
0x73, 0x20, 0x6f, 0x66, 0x20, 0x27, 0x39, 0x39, 0x3a, 0x20, 0x49, 0x66, 0x20, 0x49, 0x20, 0x63,
0x6f, 0x75, 0x6c, 0x64, 0x20, 0x6f, 0x66, 0x66, 0x65, 0x72, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x6f,
]
),
ciphertextBytes: Buffer.from(
[
0x6e, 0x2e, 0x35, 0x9a, 0x25, 0x68, 0xf9, 0x80, 0x41, 0xba, 0x07, 0x28, 0xdd, 0x0d, 0x69, 0x81,
0xe9, 0x7e, 0x7a, 0xec, 0x1d, 0x43, 0x60, 0xc2, 0x0a, 0x27, 0xaf, 0xcc, 0xfd, 0x9f, 0xae, 0x0b,
0xf9, 0x1b, 0x65, 0xc5, 0x52, 0x47, 0x33, 0xab, 0x8f, 0x59, 0x3d, 0xab, 0xcd, 0x62, 0xb3, 0x57,
0x16, 0x39, 0xd6, 0x24, 0xe6, 0x51, 0x52, 0xab, 0x8f, 0x53, 0x0c, 0x35, 0x9f, 0x08, 0x61, 0xd8
]
)}
const ciphertextBits = uintArray32ToBits(toUint32Array(test.ciphertextBytes))
const plaintextBits = uintArray32ToBits(toUint32Array(test.plaintextBytes))
const counterBits = uintArray32ToBits([test.counter])[0]
let w = await circuit.compute({
key: uintArray32ToBits(toUint32Array(test.keyBytes)),
nonce: uintArray32ToBits(toUint32Array(test.nonceBytes)),
counter: counterBits,
cipherText: ciphertextBits,
plainText: plaintextBits,
step_in: 0
}, (["step_out"]));
assert.deepEqual(w.step_out, testing(uintArray32ToBits(toUint32Array(test.plaintextBytes))));
});
});
});


function testing(Bytes: number[][]): bigint {
let hashes: bigint[] = [BigInt(0)]; // Initialize first hash as 0

for (let i = 0; i < Bytes.length; i++) {
let packedInput = BigInt(0);
for (let j = 0; j < 32; j++) {
packedInput += BigInt(Bytes[i][j]) * BigInt(Math.pow(2, j));
}
// Compute next hash using previous hash and packed input, but if packed input is zero, don't hash it.
if (packedInput == BigInt(0)) {
hashes.push(hashes[i]);
} else {
let hash = PoseidonModular([hashes[i], packedInput]);
hashes.push(hash);
}
}
// Return the last hash
return hashes[Bytes.length];
}
12 changes: 12 additions & 0 deletions circuits/test/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,18 @@ export function bitsToHex(bits: number[]): string {
return hex;
}

export function bitsToBytes(bits: number[]): number[] {
const bytes: number[] = [];
for (let i = 0; i < bits.length; i += 8) {
let byte = 0;
for (let j = 0; j < 8; j++) {
byte = (byte << 1) | (bits[i + j] || 0);
}
bytes.push(byte);
}
return bytes;
}

export function BytesToInput(bytes: number[]): number[] {
const output: number[][] = [];
let counter = 1;
Expand Down
74 changes: 74 additions & 0 deletions circuits/utils/generics-bits.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// initially from https://github.com/reclaimprotocol/zk-symmetric-crypto
// modified for our needs
pragma circom 2.1.9;

/**
* Add N bit numbers together
* copied in from: https://github.com/iden3/circomlib/blob/master/circuits/binsum.circom
* but rewritten slightly to reduce the final number of wires & labels
* and possibly look at reducing the number of constraints
*/
template AddBits(BITS) {
signal input a[BITS];
signal input b[BITS];
signal output out[BITS];
signal carrybit;

var lin = 0;
var lout = 0;

var k;
var j = 0;

var e2;

// create e2 which
// is the numerical sum of 2^k
e2 = 1;
for (k = BITS - 1; k >= 0; k--) {
lin += (a[k] + b[k]) * e2;
e2 *= 2;
}

e2 = 1;
for (k = BITS - 1; k >= 0; k--) {
out[k] <-- (lin >> j) & 1;
// Ensure out is binary
out[k] * (out[k] - 1) === 0;
lout += out[k] * e2;
e2 *= 2;
j += 1;
}

carrybit <-- (lin >> j) & 1;
// Ensure out is binary
carrybit * (carrybit - 1) === 0;
lout += carrybit * e2;

// Ensure the sum matches
lin === lout;
}

/**
* Rotate left a BITS bit integer L bits
*/
template RotateLeftBits(BITS, L) {
signal input in[BITS];
signal output out[BITS];
for (var i = 0; i < BITS; i++) {
out[i] <== in[(i + L) % BITS];
}
}

/**
* XOR BITS-bit words
*/
template XorBits(BITS) {
signal input a[BITS];
signal input b[BITS];
signal output out[BITS];

for (var k=0; k<BITS; k++) {
out[k] <== a[k] + b[k] - 2*a[k]*b[k];
}
}

0 comments on commit 5910e7a

Please sign in to comment.