@reclaimprotocol/zk-symmetric-crypto
Version:
JS Wrappers for Various ZK Snark Circuits
122 lines (121 loc) • 4.41 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateProof = generateProof;
exports.verifyProof = verifyProof;
exports.generateZkWitness = generateZkWitness;
exports.getPublicSignals = getPublicSignals;
const config_1 = require("./config");
const utils_1 = require("./utils");
/**
* Generate ZK proof for CHACHA20-CTR encryption.
* Circuit proves that the ciphertext is a
* valid encryption of the given plaintext.
* The plaintext can be partially redacted.
*/
async function generateProof(opts) {
const { algorithm, operator, logger } = opts;
const { witness, plaintextArray } = await generateZkWitness(opts);
let wtnsSerialised;
if ('mask' in opts) {
wtnsSerialised = await operator.generateWitness({
...witness,
toprf: opts.toprf,
mask: opts.mask,
});
}
else {
// @ts-expect-error
wtnsSerialised = await operator.generateWitness(witness);
}
const { proof } = await operator.groth16Prove(wtnsSerialised, logger);
return { algorithm, proofData: proof, plaintext: plaintextArray };
}
/**
* Verify a ZK proof for CHACHA20-CTR encryption.
*
* @param proofs JSON proof generated by "generateProof"
* @param publicInput
* @param zkey
*/
async function verifyProof(opts) {
const publicSignals = getPublicSignals(opts);
const { proof: { proofData }, operator, logger } = opts;
let verified;
if ('toprf' in opts) {
verified = await operator.groth16Verify({ ...publicSignals, toprf: opts.toprf }, proofData, logger);
}
else {
// serialise to array of numbers for the ZK circuit
verified = await operator.groth16Verify(
// @ts-expect-error
publicSignals, proofData, logger);
}
if (!verified) {
throw new Error('invalid proof');
}
}
/**
* Generate a ZK witness for the symmetric encryption circuit.
* This witness can then be used to generate a ZK proof,
* using the operator's groth16Prove function.
*/
async function generateZkWitness({ algorithm, privateInput: { key }, publicInput: { ciphertext, iv, offsetBytes = 0 }, }) {
const { keySizeBytes, ivSizeBytes, } = config_1.CONFIG[algorithm];
if (key.length !== keySizeBytes) {
throw new Error(`key must be ${keySizeBytes} bytes`);
}
if (iv.length !== ivSizeBytes) {
throw new Error(`iv must be ${ivSizeBytes} bytes`);
}
const startCounter = (0, utils_1.getCounterForByteOffset)(algorithm, offsetBytes);
const ciphertextArray = padCiphertextToChunkSize(algorithm, ciphertext);
const plaintextArray = await decryptCiphertext({
algorithm,
key,
iv,
startOffset: offsetBytes,
ciphertext: ciphertextArray,
});
const witness = {
key,
nonce: iv,
counter: startCounter,
in: ciphertextArray,
out: plaintextArray,
};
return { witness, plaintextArray };
}
function getPublicSignals({ proof: { algorithm, plaintext }, publicInput: { ciphertext, iv, offsetBytes = 0 }, }) {
const startCounter = (0, utils_1.getCounterForByteOffset)(algorithm, offsetBytes);
const ciphertextArray = padCiphertextToChunkSize(algorithm, ciphertext);
if (ciphertextArray.length !== plaintext.length) {
throw new Error('ciphertext and plaintext must be the same length');
}
return {
nonce: iv,
counter: startCounter,
in: ciphertextArray,
out: plaintext,
};
}
function padCiphertextToChunkSize(alg, ciphertext) {
const { chunkSize, bitsPerWord } = config_1.CONFIG[alg];
const expectedSizeBytes = (chunkSize * bitsPerWord) / 8;
if (ciphertext.length > expectedSizeBytes) {
throw new Error(`ciphertext must be <= ${expectedSizeBytes}b`);
}
if (ciphertext.length < expectedSizeBytes) {
const arr = new Uint8Array(expectedSizeBytes).fill(0);
arr.set(ciphertext);
ciphertext = arr;
}
return ciphertext;
}
async function decryptCiphertext({ algorithm, key, iv, startOffset, ciphertext, }) {
const { encrypt } = config_1.CONFIG[algorithm];
// fake the start of the ciphertext (it's irrelevant)
const inp = new Uint8Array(startOffset + ciphertext.length);
inp.set(ciphertext, startOffset);
const out = await encrypt({ key, iv, in: inp });
return out.slice(startOffset);
}