enforce same input labels for c4 and c6

dont perform OT on the same inputs of c4 and c6
use fetch() built-in download progress monitor
query the notary about fetch() upload progress
renumber circuits (we have new circuit 4)
traverse circuit faster with wasm
This commit is contained in:
themighty1
2022-02-25 10:33:07 +03:00
parent 03084cf6a5
commit 7775e85419
17 changed files with 1531 additions and 341 deletions

View File

@@ -56,8 +56,8 @@ export class FirstTimeSetup{
await window['fs'].init();
}
// start from the last circuits in order to give the user a quicker initial update
for (let n=1; n < 7; n++){
const i = 7-n;
for (let n=1; n < 8; n++){
const i = 8-n;
const text = CASM.parseAndAssemble('c'+i+'.casm');
const newobj = await new Promise(function(resolve) {
worker.onmessage = function(event) {
@@ -68,7 +68,7 @@ export class FirstTimeSetup{
worker.postMessage({'text': text});
});
obj[i] = newobj;
if (pm) pm.update('first_time', {'current': n, 'total': 6});
if (pm) pm.update('first_time', {'current': n, 'total': 7});
await wait(100); // make sure update reaches popup
}
console.timeEnd('time_to_parse');

View File

@@ -1,14 +1,16 @@
/* global chrome*/
// The only way to determine if the server is done sending data is to check that out receiving
// buffer has nothing but complete TLS records i.e. that there is no incomplete TLS records
// However it was observed that in cases when getting e.g. zip files, some servers first send HTTP header as one
// TLS record followed by the body as another record(s)
// That's why after receiving a complete TLS record we wait to get some more data
// This extra waiting must not be done for the handshake messages to avoid adding latency and having the handshake
// dropped by the server
// We do not communicate directly with the server but we send messages to the helper app
// It is the helper app which opens a TCP socket and sends/receives data
// The only way to determine if the server is done sending data is to check
// that the receiving buffer has nothing but complete TLS records i.e. that
// there are no incomplete TLS records. However it was observed that in cases
// when getting e.g. zip files, some servers first send HTTP header as one TLS
// record followed by the body as another record(s). That's why after
// receiving a complete TLS record we wait to get some more data.This extra
// waiting must NOT be done for the handshake messages to avoid causing a
// handshake timeout by the server.
// We do not communicate directly with the server but we send messages to the
// helper app. It is the helper app who opens a TCP socket and sends/receives
// data.
import {globals} from './globals.js';
import {ba2str, b64decode, concatTA, b64encode, str2ba, ba2int} from './utils.js';
@@ -36,6 +38,7 @@ export class Socket {
async connect() {
const that = this;
let timer;
// eslint-disable-next-line no-async-promise-executor
const response = await new Promise(async function(resolve, reject) {
// dont wait for connect for too long
timer = setTimeout(function() {
@@ -105,6 +108,7 @@ export class Socket {
return;
}
var that = this;
// eslint-disable-next-line no-async-promise-executor
var response = await new Promise(async function(resolve){
var msg = {'command': 'recv', 'uid': that.uid};
if (globals.usePythonBackend){
@@ -130,8 +134,8 @@ export class Socket {
}, 100);
}
// fetchLoop has built up the recv buffer
// check if there are complete records in the buffer,return them if yes or wait some more if no
// fetchLoop has built up the recv buffer. Check if there are complete
// records in the buffer, return them if yes or wait some more if no.
recv (is_handshake = false) {
const that = this;
return new Promise(function(resolve, reject) {

View File

@@ -2,8 +2,8 @@ export const globals = {
// defaultNotaryIP/Port is the default IP address/port of the notary server.
// If this IP address becomes unavailable, Pagesigner will query backupUrl for
// a new notary's IP address and will save the new IP address in the preferences.
defaultNotaryIP: '127.0.0.1',
//defaultNotaryIP: '18.207.130.193',
//defaultNotaryIP: '127.0.0.1',
defaultNotaryIP: '100.27.49.242',
defaultNotaryPort: 10011,
// backupUrl is the URL to query to get the IP address of another notary
// server in case if defaultNotaryIP is unreachable
@@ -26,6 +26,6 @@ export const globals = {
// if useNotaryNoSandbox is set to true, then we fetch notary's pubkey by
// querying /getPubKey and trust it. This is only useful when notary runs
// in a non-sandbox environment.
//useNotaryNoSandbox: false
useNotaryNoSandbox: true
useNotaryNoSandbox: false
//useNotaryNoSandbox: true
};

View File

@@ -7,7 +7,8 @@ import {ba2str, b64decode, assert, ba2int, verifyAttestationDoc, ba2hex, eq,
const rootsOfTrust = [
'snap-0ccb00d0e0fb4d4da',
'snap-07eda3ed4836f82fb',
'snap-023d50ee97873a1f0'];
'snap-023d50ee97873a1f0',
'snap-0e50af508006037dc'];
// URLFetcher trusted enclave measurements, see
// https://github.com/tlsnotary/URLFetcher
const URLFetcherPCR0 = 'f70217239e8a1cb0f3c010b842a279e2b8d30d3700d7e4722fef22291763479a13783dc76d5219fabbd7e5aa92a7b255';

View File

@@ -4,8 +4,10 @@ import WorkerPool from './WorkerPool.js';
export class GCWorker extends WorkerPool{
// class CGWorker provides convenience functions to speak to the web worker
constructor(numWorkers, processMonitor){
super(numWorkers, chrome.extension.getURL('core/twopc/webWorkers/gcworker.js'));
constructor(numWorkers, processMonitor, ){
// you can replace gcworker_wasm.js with gcworker_purejs.js on systems where
// WebAssembly is not available
super(numWorkers, chrome.extension.getURL('core/twopc/webWorkers/gcworker_wasm.js'));
// pm is an instance of ProcessMonitor
this.pm = processMonitor;
}
@@ -21,11 +23,10 @@ export class GCWorker extends WorkerPool{
resolve();
}
else {
console.log('unexpected response from worker');
throw('unexpected response from worker');
}
};
const obj = {msg: 'parse', data: circuit_ab};
const obj = {msg: 'parse', circuit: circuit_ab};
worker.postMessage(obj);
}));
}
@@ -44,42 +45,43 @@ export class GCWorker extends WorkerPool{
}
// pm is an instance of ProgressMonitor
async garbleBatch(count, obj){
async garbleBatch(count){
const batch = [];
for (let i=0; i < count; i++){
batch.push(obj);
// we dont pass any args when garbling
batch.push({});
}
const output = await this.workerPool(batch, this.garbleBatchDoWork, this.pm, 'garbling');
return output;
}
// batchItem is empty because we dont pass any args
garbleBatchDoWork(batchItem, worker){
return new Promise(function(resolve) {
worker.onmessage = function(event) {
worker['isResolved'] = true;
resolve(event.data);
};
worker.postMessage( {msg: 'garble', data: batchItem});
worker.postMessage( {msg: 'garble'});
});
}
async evaluateBatch(batch){
//const output = await this.workerPool(batch, this.evaluateBatchDoWork, this.pm, 'evaluating');
const output = await this.workerPool(batch, this.evaluateBatchDoWork);
return output;
return await this.workerPool(batch, this.evaluateBatchDoWork);
}
evaluateBatchDoWork(batchItem, worker){
const ga = batchItem[0];
const il = batchItem[0];
const tt = batchItem[1];
const obj = {msg: 'setTruthTable', data: tt.buffer};
const dt = batchItem[2];
const obj = {msg: 'setTruthTables', tt: tt.buffer};
worker.postMessage(obj);
return new Promise(function(resolve) {
worker.onmessage = function(event) {
worker['isResolved'] = true;
resolve(event.data);
};
const obj = {msg: 'evaluate', data: ga.buffer};
const obj = {msg: 'evaluate', il: il.buffer, dt: dt.buffer};
worker.postMessage(obj);
});
}

View File

@@ -378,9 +378,12 @@ export class GHASH {
allBits = [].concat(allBits, bytesToBits(this.shares[key]).reverse());
}
this.lastChoiceBits = allBits;
// TODO rename requestMaskedOT to createRequest and
// rename the rest to createResponse, parseResponse
return this.otR.createRequest(allBits);
if (allBits.length == 0){
// no block agregation is needed
return new Uint8Array(0);
} else {
return this.otR.createRequest(allBits);
}
}
// add NOtary's XTable to our share of the tag
@@ -400,18 +403,17 @@ export class GHASH {
getPowerShares(hisXTables, powersOfH){
const stratKeys = sortKeys(this.lastStrategy);
// for each strategy we have 128 values for 1st factor and 128 values for 2nd factor
const chunks = splitIntoChunks(hisXTables, 256*16);
const xTablePair = splitIntoChunks(hisXTables, 256*16);
for (let j=0; j < stratKeys.length; j++){
const oddPower = stratKeys[j];
if (oddPower > this.maxOddPowerNeeded){
assert(chunks.length === j);
assert(xTablePair.length === j);
break;
}
let xorSum = new Uint8Array(16).fill(0); // start with 0 sum
// TODO find a better name for subChunks
const subChunks = splitIntoChunks(chunks[j], 16);
const xTableRow = splitIntoChunks(xTablePair[j], 16);
for (let i=0; i < 256; i++){
xorSum = xor(xorSum, subChunks[i]);
xorSum = xor(xorSum, xTableRow[i]);
}
const Cx = powersOfH[this.lastStrategy[oddPower][0]];
const Cy = powersOfH[this.lastStrategy[oddPower][1]];

View File

@@ -5,8 +5,8 @@ export class Garbler{
constructor(parent){
// this.s is the TWOPC class
this.s = parent;
// this.garbledC will contain truth table, output labels and input labels for each
// circuit after it is garbled
// this.garbledC will contain input labels, truth tables and decoding table
// for each circuit after it is garbled
this.garbledC = [];
}
@@ -15,7 +15,7 @@ export class Garbler{
// one by one
const allPromises = [];
for (let cNo = 1; cNo < this.s.cs.length; cNo++){
if (cNo === 5){
if (cNo === 6){
allPromises.push(Promise.resolve('empty'));
continue;
}
@@ -23,42 +23,42 @@ export class Garbler{
allPromises.push(worker.garble());
}
const allTt = [];
const allOl = [];
const allIl = [];
// while non-c5 circuits are being garbled, we saturate the CPU with
// parallel garbling of c5
const outputs = await this.s.workers[5].garbleBatch(this.s.C5Count, {});
for (let i=0; i < this.s.C5Count; i++){
const allTt = [];
const allDt = [];
// while non-c6 circuits are being garbled, we saturate the CPU with
// parallel garbling of c6
const outputs = await this.s.workers[6].garbleBatch(this.s.C6Count, {});
for (let i=0; i < this.s.C6Count; i++){
const out = outputs[i];
allTt.push(new Uint8Array(out.tt));
allOl.push(new Uint8Array(out.ol));
allIl.push(new Uint8Array(out.il));
allTt.push(new Uint8Array(out.tt));
allDt.push(new Uint8Array(out.dt));
}
this.garbledC[5] = {
this.garbledC[6] = {
il: concatTA(...allIl),
tt: concatTA(...allTt),
ol: concatTA(...allOl),
il: concatTA(...allIl)};
dt: concatTA(...allDt)};
// all non-c5 circuits have been garbled by now
// all non-c6 circuits have been garbled by now
const allRv = await Promise.all(allPromises);
for (let cNo=1; cNo < this.s.cs.length; cNo++){
const rv = allRv[cNo-1];
if (cNo === 5){
continue; // c5 already dealt with
if (cNo === 6){
continue; // c6 already dealt with
}
this.garbledC[cNo] = {
il: new Uint8Array(rv.il),
tt: new Uint8Array(rv.tt),
ol: new Uint8Array(rv.ol),
il: new Uint8Array(rv.il)};
dt: new Uint8Array(rv.dt)};
}
}
// Notary's inputs are always the first inputs in the circuit
getNotaryLabels(cNo){
// exeCount is how many executions of this circuit we need
const exeCount = [0, 1, 1, 1, 1, this.s.C5Count, this.s.C6Count][cNo];
const exeCount = [0, 1, 1, 1, 1, 1, this.s.C6Count, 1][cNo];
const il = this.garbledC[cNo].il;
const c = this.s.cs[cNo];
const ilArray = [];
@@ -73,7 +73,7 @@ export class Garbler{
// Client's inputs always come after the Notary's inputs in the circuit
getClientLabels(clientInputs, cNo){
const repeatCount = [0, 1, 1, 1, 1, this.s.C5Count, this.s.C6Count][cNo];
const repeatCount = [0, 1, 1, 1, 1, 1, this.s.C6Count, 1][cNo];
const il = this.garbledC[cNo].il;
const c = this.s.cs[cNo];
const ilArray = [];

View File

@@ -147,7 +147,6 @@ export class OTReceiver extends OTCommon{
}
this.receivedSoFar += choiceBits.length;
this.expectingResponseSize = 0;
console.log('this.receivedSoFar', this.receivedSoFar);
return concatTA(...decodedArr);
}
}

View File

@@ -2,7 +2,7 @@
import {ba2hex, assert, getRandom, bytesToBits, concatTA, int2ba, str2ba,
innerHash, xor, eq, sha256, ba2int, splitIntoChunks, bitsToBytes,
pubkeyPEM2raw, checkExpiration, wait} from '../utils.js';
pubkeyPEM2raw, checkExpiration, AESCTRdecrypt} from '../utils.js';
import {Garbler} from './Garbler.js';
import {Evaluator} from './Evaluator.js';
import {Paillier2PC} from './Paillier2PC.js';
@@ -25,12 +25,9 @@ export class TWOPC {
this.notary = notary;
// number of client request encr counter blocks to produce
const noOfAESBlocks = Math.ceil(plaintextLen/16);
// const noOfGctrBlocks = Math.ceil(noOfAESBlocks /8)
const noOfGctrBlocks = 1;
// C5Count is the number of c5 circuit executions
this.C5Count = noOfAESBlocks;
console.log('need to evaluate ', this.C5Count, ' c5 circuits');
this.C6Count = noOfGctrBlocks;
// C6Count is the number of c6 circuit executions
this.C6Count = noOfAESBlocks;
console.log('need to evaluate ', this.C6Count, ' c6 circuits');
// shares of TLS session keys
this.cwkShare = null;
this.swkShare = null;
@@ -44,7 +41,7 @@ export class TWOPC {
this.uid = Math.random().toString(36).slice(-10);
// cs is an array of circuits, where each circuit is an object with the fields:
// gatesBlob, gatesCount, wiresCount, notaryInputSize, clientInputSize,
// outputSize, andGateCount, outputsSizes
// outputSize, andGateCount, outputsSizes. Count starts at 1
this.cs = Object.keys(circuits).map(k => circuits[k]);
// start count of circuits with 1, push an empty element to index 0
this.cs.splice(0, 0, undefined);
@@ -52,10 +49,11 @@ export class TWOPC {
// to know how many bits each output value has in order to parse the output
this.cs[1]['outputsSizes'] = [256, 256];
this.cs[2]['outputsSizes'] = [256, 256];
this.cs[3]['outputsSizes'] = [128, 128, 32, 32, 128, 128, 128];
this.cs[4]['outputsSizes'] = [128, 128, 128, 96];
this.cs[5]['outputsSizes'] = [128];
this.cs[3]['outputsSizes'] = [128, 128, 32, 32];
this.cs[4]['outputsSizes'] = [128, 128, 128];
this.cs[5]['outputsSizes'] = [128, 128, 128, 96];
this.cs[6]['outputsSizes'] = [128];
this.cs[7]['outputsSizes'] = [128];
// output is the output of the circuit as array of bytes
this.output = Array(this.cs.length);
// Commits are used to ensure malicious security of garbled circuits
@@ -103,7 +101,7 @@ export class TWOPC {
this.otCountSend = 0;
// exeCount is how many executions of each circuit we need. Circuit numbering
// starts from 1
const exeCount = [0, 1, 1, 1, 1, this.C5Count, this.C6Count];
const exeCount = [0, 1, 1, 1, 1, 1, this.C6Count, 1];
for (let i=1; i < this.cs.length; i++){
this.otCountRecv += this.cs[i].clientInputSize * exeCount[i];
this.otCountSend += this.cs[i].notaryInputSize * exeCount[i];
@@ -114,6 +112,14 @@ export class TWOPC {
// otS is the sender of Oblivious Transfer
this.otS = new OTSender(this.otCountSend);
this.ghash.setOTReceiver(this.otR);
this.blobSize = this.getBlobSize();
// decrLabels are decrypted input labels corresponding to client_write_key
// (cwk) and client_write_iv (civ) for circuit 4 (c4) and circuit 6 (c6).
// It is a two-dimensional array [A][B], where A is an array of 160 elements
// (each element corresponding to the bit of [cwk|civ]) and B contains as
// many elements as there are executions of c4 + c6. Each element of B is a
// 16-byte input label. B[0] are labels for c4.
this.decrLabels = [];
}
// destroy de-registers listeners, terminates workers
@@ -174,10 +180,15 @@ export class TWOPC {
// [REF 1] Steps 13,15,17,20,22
const [verify_data, c3_p2inner, c3_p1inner] = await this.phase3(msInnerHashState);
// for readability, indexing of masks starts from 1
const masks3 = [0, 16, 16, 4, 4, 16, 16, 16].map(x => getRandom(x));
const masks3 = [0, 16, 16, 4, 4].map(x => getRandom(x));
const input3 = [c3_p1inner, c3_p2inner, ...masks3.slice(1)];
const c3Out = (await this.runCircuit(input3, 3))[0];
const [encFinished, tag] = await this.phase4(c3Out, masks3, verify_data);
await this.phase4(c3Out, masks3);
const masks4 = [0, 16, 16, 16].map(x => getRandom(x));
const input4 = [this.swkShare, this.cwkShare, this.sivShare, this.civShare,
...masks4.slice(1)];
const c4Out = (await this.runCircuit(input4, 4))[0];
const [encFinished, tag] = await this.phase5(c4Out, masks4, verify_data);
return [encFinished, tag, verify_data];
}
@@ -194,28 +205,28 @@ export class TWOPC {
const a0 = seed;
// [REF 1] Step 25
const a1inner = innerHash(this.innerState_MS, a0);
const a1 = await this.send('c4_pre1', a1inner);
const a1 = await this.send('c5_pre1', a1inner);
// [REF 1] Step 27
const p1inner = innerHash(this.innerState_MS, concatTA(a1, seed));
// for readability, mask indexing starts from 1
const masks4 = [0, 16, 16, 16, 12].map(x => getRandom(x));
const input4 = [p1inner, this.swkShare, this.sivShare, sf_nonce,
...masks4.slice(1)];
const masks5 = [0, 16, 16, 16, 12].map(x => getRandom(x));
const input5 = [p1inner, this.swkShare, this.sivShare, sf_nonce,
...masks5.slice(1)];
// [REF 1] Step 28
const c4out = (await this.runCircuit(input4, 4))[0];
const c5out = (await this.runCircuit(input5, 5))[0];
let o = 0; // offset
// parse outputs
const H1MaskedTwice = c4out.slice(o, o+=16);
const gctrSFMasked = c4out.slice(o, o+=16);
const encCounterMasked = c4out.slice(o, o+=16);
const verify_dataMasked = c4out.slice(o, o+=12);
const H1MaskedTwice = c5out.slice(o, o+=16);
const gctrSFMasked = c5out.slice(o, o+=16);
const encCounterMasked = c5out.slice(o, o+=16);
const verify_dataMasked = c5out.slice(o, o+=12);
// unmask outputs
// H1 xor-masked by notary is our share of H^1, the mask is notary's share of H^1
const H1share = xor(H1MaskedTwice, masks4[1]);
const gctrShare = xor(gctrSFMasked, masks4[2]);
const encCounter = xor(encCounterMasked, masks4[3]);
const verify_data = xor(verify_dataMasked, masks4[4]);
const H1share = xor(H1MaskedTwice, masks5[1]);
const gctrShare = xor(gctrSFMasked, masks5[2]);
const encCounter = xor(encCounterMasked, masks5[3]);
const verify_data = xor(verify_dataMasked, masks5[4]);
const sf = concatTA(new Uint8Array([0x14, 0x00, 0x00, 0x0c]), verify_data);
const encSF = xor(sf, encCounter);
@@ -224,7 +235,7 @@ export class TWOPC {
const ghashSF = new GHASH(1);
ghashSF.setOTReceiver(this.otR);
const otReq = await ghashSF.buildFinRequest(H1share);
const step3resp = await this.send('c4_step3', concatTA(encSF, otReq));
const step3resp = await this.send('c5_step3', concatTA(encSF, otReq));
assert(step3resp.length == 16 + 256*32);
o = 0;
const notaryTagShare = step3resp.slice(o, o+=16);
@@ -239,22 +250,25 @@ export class TWOPC {
assert (eq(sf_tag, tagFromPowersOfH));
}
// runs circuit 5
// runs circuit 6
async getEncryptedCounters() {
const masks5 = [];
let input5 = [];
const masks6 = [];
let input6 = [];
// we only send 1 TLS record with a fixed nonce 2
const fixedNonce = int2ba(2, 2);
for (let i = 0; i < this.C5Count; i++) {
for (let i = 0; i < this.C6Count; i++) {
const counter = int2ba(2 + i, 2);
masks5[i] = getRandom(16);
input5 = [].concat(input5, [this.cwkShare, this.civShare, masks5[i],
masks6[i] = getRandom(16);
input6 = [].concat(input6, [
this.cwkShare,
this.civShare,
masks6[i],
fixedNonce, 10, counter, 10]);
}
const c5out = await this.runCircuit(input5, 5);
const c6out = await this.runCircuit(input6, 6);
const encCounters = [];
for (let i=0; i < this.C5Count; i++){
encCounters.push(xor(c5out[i], masks5[i]));
for (let i=0; i < this.C6Count; i++){
encCounters.push(xor(c6out[i], masks6[i]));
}
return encCounters;
}
@@ -263,23 +277,18 @@ export class TWOPC {
// client's gctr share. The notary's gctr share is already xored into the output
// of getTagFromPowersOfH
async getGctrBlocks(){
const masks6 = [];
let input6 = [];
for (let i = 0; i < this.C6Count; i++) {
const nonce = int2ba(2 + i, 2);
masks6.push(getRandom(16));
input6 = [].concat(input6, [this.cwkShare, this.civShare, masks6[i], nonce]);
}
const c6out = await this.runCircuit(input6, 6);
const c6CommitSalt = await this.send('checkC6Commit', this.myCommit[6]);
// the commit which notary computed on their side must be equal to our commit
const saltedCommit = await sha256(concatTA(this.myCommit[6], c6CommitSalt));
assert (eq(saltedCommit, this.hisSaltedCommit[6]));
const mask7 = getRandom(16);
let input7 = [];
const nonce = int2ba(2, 2);
input7 = [].concat(input7, [this.cwkShare, this.civShare, mask7, nonce]);
const c7out = await this.runCircuit(input7, 7);
const c7CommitSalt = await this.send('checkC7Commit', this.myCommit[7]);
// the commit which notary computed on their side must be equal to our commit
const saltedCommit = await sha256(concatTA(this.myCommit[7], c7CommitSalt));
assert (eq(saltedCommit, this.hisSaltedCommit[7]));
const gctrBlocks = [];
for (let i=0; i < this.C6Count; i++){
gctrBlocks.push(xor(c6out[i], masks6[i]));
}
gctrBlocks.push(xor(c7out[0], mask7));
return gctrBlocks;
}
@@ -324,59 +333,96 @@ export class TWOPC {
}
// send data to the notary
async send(cmd, data = new Uint8Array(0), returnPromise = false, willEncrypt = true){
// send sends a command with data to the notary.
// returnPromise if set to true, will return a promise
// (instead of data resolved from the promise).
// willEncrypt if set to true, will encrypt the request to notary.
// willDecrypt if set to true, will decrypt the response from the notary.
// downloadProgress if set to true, will update the progress monitor with
// the amount of data already downloaded.
async send(cmd, data = new Uint8Array(0), returnPromise = false, willEncrypt = true,
willDecrypt = true, downloadProgress = false){
let to_be_sent;
if (willEncrypt){
if (willEncrypt && data.length > 0){
to_be_sent = await this.encryptToNotary(this.clientKey, data);
}
else {
to_be_sent = data;
}
function timeout(ms, promise) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
reject(new Error('Timed out while while transfering data.'));
}, ms);
promise.then(resolve, reject);
});
}
const fetchPromise = fetch(
'http://'+this.notary.IP+':'+globals.defaultNotaryPort+'/'+cmd+'?'+this.uid,
{
method: 'POST',
mode: 'cors',
cache: 'no-store',
// agent: is only used when running in nodejs
agent: typeof(window) === 'undefined' ? keepAliveAgent : null,
//keepalive: true, // <-- trips up chrome
//keepalive: true, // <-- trips up Chrome
body: to_be_sent.buffer
});
const that = this;
const promise = new Promise((resolve, reject) => {
timeout(20000, fetchPromise).then(function(resp){
resp.arrayBuffer().then(function(ab){
const payload = new Uint8Array(ab);
if (payload.length !== 0){
if (willEncrypt){
that.decryptFromNotary(that.notaryKey, payload).then(function(decrypted){
resolve(decrypted);
});
}
else {
resolve(payload);
}
}
else {
resolve();
}
});
}).catch(err =>{
reject(err);
async function timeout(ms, promise) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
reject(new Error('Timed out while while transfering data.'));
}, ms);
promise.then(resolve, reject);
});
}
const that = this;
// eslint-disable-next-line no-async-promise-executor
const promise = new Promise(async function(resolve) {
const resp = await timeout(20000, fetchPromise);
let payload;
if (downloadProgress) {
const reader = resp.body.getReader();
let soFarBytes = 0;
let chunks = [];
// lastUpdateTime is when we last sent progress update. We don't want
// send the update more often than every 1 sec. Set initial value so
// that the very first update triggers immediately.
let lastUpdateTime = (Date.now()/1000) + 1;
let done2;
do {
const {done, value} = await reader.read();
done2 = done;
if (done) break;
chunks.push(value);
soFarBytes += value.length;
const now = (Date.now()/1000);
if (now - lastUpdateTime > 1){
if (that.pm) that.pm.update('download', {'current': soFarBytes, 'total': that.blobSize});
lastUpdateTime = now;
}
} while (!done2);
payload = new Uint8Array(soFarBytes);
let position = 0;
for(let chunk of chunks) {
payload.set(chunk, position);
position += chunk.length;
}
}
else {
const ab = await resp.arrayBuffer();
payload = new Uint8Array(ab);
}
if (payload.length == 0){
resolve();
return;
}
if (!willDecrypt) {
resolve(payload);
return;
}
const decrypted = await that.decryptFromNotary(that.notaryKey, payload);
resolve(decrypted);
return;
}).catch(err =>{
throw(err);
});
if (returnPromise === true){
return promise;
}
@@ -400,12 +446,11 @@ export class TWOPC {
const [pubBytes, privKey] = await this.generateECKeypair();
const init1Blob = await this.send('init1', concatTA(
pubBytes,
int2ba(this.C5Count, 2),
int2ba(this.C6Count, 2),
int2ba(this.otCountRecv, 4),
int2ba(this.otCountSend, 4),
A,
seedCommit), false, false);
seedCommit), false, false, false);
let o = 0;
this.eValidFrom = init1Blob.slice(o, o+=4);
@@ -438,17 +483,24 @@ export class TWOPC {
const hisReceiverCommit = init1Resp.slice(o, o+=32);
const hisSenderallBs = init1Resp.slice(o, o+=128*32);
const hisSenderSeedShare = init1Resp.slice(o, o+=16);
const totalBytes = ba2int(init1Resp.slice(o, o+=4));
assert(init1Resp.length == o);
// as soon as communication keys are established, start downloading
// and uploading
const that = this;
const getBlobPromise = new Promise((resolve) => {
this.getBlobFromNotary(totalBytes)
.then(function(hisBlob){
that.blob = that.processBlob(hisBlob);
resolve();
});
// eslint-disable-next-line no-async-promise-executor
const blobTransferPromise = new Promise(async function(resolve){
const blob = await that.getBlobFromNotary();
that.blob = that.processBlob(blob);
await garbleAllPromise;
let blobOut = new Uint8Array(0);
// proceed to upload
for (let i=1; i < that.cs.length; i++){
blobOut = concatTA(blobOut, that.g.garbledC[i].tt);
blobOut = concatTA(blobOut, that.g.garbledC[i].dt);
}
await that.sendBlobToNotary(blobOut);
resolve();
});
const [encryptedColumns, receiverSeedShare, x, t] =
@@ -467,58 +519,55 @@ export class TWOPC {
await this.otS.setupStep2(hisReceiverEncryptedColumns, hisReceiverSeedShare,
hisReceiverX, hisReceiverT);
await garbleAllPromise;
let blobOut = [];
for (let i=1; i < this.cs.length; i++){
blobOut.push(this.g.garbledC[i].tt);
blobOut.push(this.g.garbledC[i].ol);
}
// start blob upload as soon as download finishes
await getBlobPromise;
await this.sendBlobToNotary(concatTA(...blobOut));
await blobTransferPromise;
this.preComputed = true;
}
// sendBlobToNotary uploads a blob to the notary and periodically publishes the download progress
// to the UI. Upload is in 1MB chunks.
async sendBlobToNotary(blob){
const that = this;
let sentSoFar = 0;
let oneMB = 1024*1024;
let chunkCount = Math.ceil(blob.length / oneMB);
for (let i=0; i < chunkCount; i++){
if (i % 10 == 0){
// dont monopolize the upload bandwidth, allow preComputeOT to squeeze in some data
await wait(200);
// getBlobSize returns the size of all truth tables and encoding tables for
// all garbled circuits executions
getBlobSize(){
let total = 0;
for (let i=1; i < this.cs.length; i++){
let truthTablesSize = this.cs[i].andGateCount *48;
let decryptionTableSize = Math.ceil(this.cs[i].outputSize/8);
if (i === 6){
truthTablesSize = this.C6Count * truthTablesSize;
decryptionTableSize = this.C6Count * decryptionTableSize;
}
let chunk = blob.slice(sentSoFar, sentSoFar + oneMB);
await that.send('setBlobChunk', chunk);
sentSoFar += chunk.length;
if (that.pm) that.pm.update('upload', {'current': i+1, 'total': chunkCount});
total += ( truthTablesSize + decryptionTableSize);
}
that.send('setBlobChunk', str2ba('magic: no more data'));
return total;
}
// getNotaryBlob downloads a blob from notary in 1 MB chunks publishes the download progress
// to the UI.
async getBlobFromNotary (totalBytes){
const allChunks = [];
console.log('totalBytes is', totalBytes);
async sendBlobToNotary(blob){
const uploadPromise = this.send('setBlob', blob, true, false);
let uploadFinished = false;
const that = this;
let soFarBytes = 0;
while (soFarBytes < totalBytes){
let needBytes = 1024*1024;
if (needBytes + soFarBytes > totalBytes){
needBytes = totalBytes - soFarBytes;
// since fetch() does not implement upload progress, we continuously query
// the server about upload progress
const interval = setInterval(async function(){
if (uploadFinished == true) {
clearInterval(interval);
return;
}
let chunk = await this.send('getBlobChunk', int2ba(needBytes, 4));
allChunks.push(chunk);
soFarBytes += chunk.length;
if (this.pm) this.pm.update('download', {'current': soFarBytes, 'total': totalBytes});
}
const rv = concatTA(...allChunks);
assert(rv.length === totalBytes);
const progress = await that.send('getUploadProgress');
const byteCount = ba2int(progress);
if (that.pm) that.pm.update('upload', {'current': byteCount, 'total': blob.length});
console.log('returned progress ', byteCount, blob.length);
}, 1000);
await uploadPromise;
uploadFinished = true;
if (this.pm) this.pm.update('upload', {'current': blob.length, 'total': blob.length});
}
// getBlobFromNotary downloads truth tables and decoding table from notary
// with a progress monitor
async getBlobFromNotary (){
const rv = await this.send('getBlob', new Uint8Array(0), false, false, false, true);
// set download progress to 100%
if (this.pm) this.pm.update('download', {'current': this.blobSize, 'total': this.blobSize});
return rv;
}
@@ -549,11 +598,11 @@ export class TWOPC {
}
async setupWorkers(){
const maxWorkerCount = 3; // only needed for c5
const workerCount = [0, 1, 1, 1, 1, maxWorkerCount, 1];
const maxWorkerCount = 2; // only needed for circuit 6
const workerCount = [0, 1, 1, 1, 1, 1, maxWorkerCount, 1];
for (let i=1; i < this.cs.length; i++){
this.workers[i] = new GCWorker(workerCount[i], this.pm);
this.workers[i].parse(this.cs[i]);
await this.workers[i].parse(this.cs[i]);
}
}
@@ -628,31 +677,37 @@ export class TWOPC {
return [verify_data, p2inner, p1inner];
}
async phase4(c3out, masks3, verify_data) {
async phase4(c3out, masks3) {
let o = 0; // offset
// parse all outputs
const swkMaskedTwice = c3out.slice(o, o+=16);
const cwkMaskedTwice = c3out.slice(o, o+=16);
const sivMaskedTwice = c3out.slice(o, o+=4);
const civMaskedTwice = c3out.slice(o, o+=4);
const H1MaskedTwice = c3out.slice(o, o+=16);
const gctrMaskedTwice = c3out.slice(o, o+=16);
const encCounterMasked = c3out.slice(o, o+=16);
// unmask all outputs
this.swkShare = xor(swkMaskedTwice, masks3[1]);
this.cwkShare = xor(cwkMaskedTwice, masks3[2]);
this.sivShare = xor(sivMaskedTwice, masks3[3]);
this.civShare = xor(civMaskedTwice, masks3[4]);
const myGctrShare = xor(gctrMaskedTwice, masks3[6]);
const encCounter = xor(encCounterMasked, masks3[7]);
}
async phase5(c4out, masks4, verify_data) {
let o = 0; // offset
// parse all outputs
const H1MaskedTwice = c4out.slice(o, o+=16);
const gctrMaskedTwice = c4out.slice(o, o+=16);
const encCounterMasked = c4out.slice(o, o+=16);
// unmask all outputs
const myGctrShare = xor(gctrMaskedTwice, masks4[2]);
const encCounter = xor(encCounterMasked, masks4[3]);
// H1 xor-masked by notary is our (i.e. the client's) share of H^1,
// notary's mask is his share of H^1.
const H1share = xor(H1MaskedTwice, masks3[5]);
const H1share = xor(H1MaskedTwice, masks4[1]);
const clientFinished = concatTA(new Uint8Array([0x14, 0x00, 0x00, 0x0c]), verify_data);
const encCF = xor(clientFinished, encCounter);
const otReq = await this.ghash.buildFinRequest(H1share);
const step3resp = await this.send('c3_step3', concatTA(encCF, otReq));
const step3resp = await this.send('c4_step3', concatTA(encCF, otReq));
assert(step3resp.length === 16 + 256*32);
o = 0;
const notaryTagShare = step3resp.slice(o, o+=16);
@@ -670,17 +725,17 @@ export class TWOPC {
// getClientFinishedResumed runs a circuit to obtain data needed to construct the
// Client Finished messages for cases when we need TLS session resumption
// TODO: this is wip, not yet fully implemented
async getClientFinishedResumed(hs_hash){
const seed = concatTA(str2ba('client finished'), hs_hash);
const a1inner = innerHash(this.innerState_MS, seed);
const a1 = await this.send('c7_step1', a1inner);
const p1inner = innerHash(this.innerState_MS, a1);
// p1inner is the client's input to c7
const mask1 = getRandom(16);
const mask2 = getRandom(12);
const nonFixedBits = bytesToBits(concatTA(
mask2, mask2, this.civShare, this.cwkShare, p1inner));
}
// async getClientFinishedResumed(hs_hash){
// const seed = concatTA(str2ba('client finished'), hs_hash);
// const a1inner = innerHash(this.innerState_MS, seed);
// const a1 = await this.send('c7_step1', a1inner);
// const p1inner = innerHash(this.innerState_MS, a1);
// // p1inner is the client's input to c7
// const mask1 = getRandom(16);
// const mask2 = getRandom(12);
// const nonFixedBits = bytesToBits(concatTA(
// mask2, mask2, this.civShare, this.cwkShare, p1inner));
// }
// getServerKeyShare returns client's xor share of server_write_key
// and server_write_iv
@@ -698,9 +753,10 @@ export class TWOPC {
// The notary is also evaluating this circuit on his end.
async runCircuit(inputs, cNo) {
console.log('in runCircuit', cNo);
// inputs is an array of inputs in the order in which the inputs appear in the c*.casm files.
// inputBitsE is an array of inputs for Client-the-Evaluator in the order
// in which the inputs appear in the c*.casm files.
// The circuit expects the least bit of the input to be the first bit
let inputBits = [];
let inputBitsE = [];
for (let i=0; i < inputs.length; i++){
let bits = bytesToBits(inputs[i]);
if (typeof(inputs[i+1]) == 'number'){
@@ -709,15 +765,30 @@ export class TWOPC {
bits = bits.slice(0, inputs[i+1]);
i += 1;
}
inputBits = [].concat(inputBits, bits);
inputBitsE = [].concat(inputBitsE, bits);
}
// inputBitsG is an array of inputs for Client-the-Garbler
const inputBitsG = inputBitsE.slice();
// c is circuit
const c = this.cs[cNo];
// how many times to repeat evaluation (> 1 only for circuits 5&7 )
const repeatCount = [0, 1, 1, 1, 1, this.C5Count, this.C6Count][cNo];
// how many times to repeat evaluation (> 1 only for circuit 6 )
const repeatCount = [0, 1, 1, 1, 1, 1, this.C6Count, 1][cNo];
// for circuit 1, there was no previous commit
const prevCommit = cNo > 1 ? this.myCommit[cNo-1] : new Uint8Array(0);
const otReq = this.otR.createRequest(inputBits);
if (cNo==6){
// here we remove inputs this.cwkShare and this.civShare (first 160 bits)
// because we already have active labels for those bits for each circuit 6
// execution. We obtained them when we were running circuit 4.
let newInputBits = [];
const chunkSize = inputBitsE.length/this.C6Count;
for (let i=0; i < this.C6Count; i++){
newInputBits = [].concat(newInputBits, inputBitsE.slice(i*chunkSize+160, (i+1)*chunkSize));
}
inputBitsE = newInputBits;
}
const otReq = this.otR.createRequest(inputBitsE);
const blob1 = await this.send(`c${cNo}_step1`, concatTA(prevCommit, otReq));
let o = 0;
@@ -730,56 +801,53 @@ export class TWOPC {
// all notary's labels
const allNotaryLabels = blob1.slice(o, o += c.notaryInputSize*16*repeatCount);
const hisOtResp = blob1.slice(o, o += inputBits.length*32);
// we may need to drop some bits if their amount is not a multiple of 8
let hisOtResp = blob1.slice(o, o += inputBitsE.length*32);
const hisOtReq = blob1.slice(o, o += 1+ c.notaryInputSize/8*repeatCount);
assert(blob1.length == o);
const allClientLabels = this.otR.parseResponse(inputBits, hisOtResp);
let allClientLabels;
if (cNo == 4){
const encryptedLabels = blob1.slice(o, o += 160*2*16*(1+this.C6Count));
assert(blob1.length == o);
allClientLabels = await this.processC4Labels(inputBitsE, hisOtResp, encryptedLabels);
}
else {
assert(blob1.length == o);
allClientLabels = this.otR.parseResponse(inputBitsE, hisOtResp);
}
if (cNo == 6){
allClientLabels = this.processC6Labels(allClientLabels);
}
const notaryLabels = this.g.getNotaryLabels(cNo);
const otResp = this.otS.processRequest(hisOtReq, notaryLabels);
const clientLabels = this.g.getClientLabels(inputBits, cNo);
const clientLabels = this.g.getClientLabels(inputBitsG, cNo);
const sendPromise = this.send(`c${cNo}_step2`, concatTA(otResp, clientLabels), true);
// collect batch of evaluation
// collect batch for evaluation
const batch = [];
const clBatch = splitIntoChunks(allClientLabels, c.clientInputSize*16);
const nlBatch = splitIntoChunks(allNotaryLabels, c.notaryInputSize*16);
const ttBatch = splitIntoChunks(this.blob[`c${cNo}_tt`], c.andGateCount * 48);
const dtBatch = splitIntoChunks(this.blob[`c${cNo}_dt`], Math.ceil(c.outputSize/8));
for (let r=0; r < repeatCount; r++){
batch.push([concatTA(nlBatch[r], clBatch[r]), ttBatch[r]]);
batch.push([concatTA(nlBatch[r], clBatch[r]), ttBatch[r], dtBatch[r]]);
}
// perform evaluation
console.time('evaluateBatch');
const evalOutputLabelsBatch = await this.e.evaluateBatch(batch, cNo);
const evalPlaintextBatch = await this.e.evaluateBatch(batch, cNo);
console.timeEnd('evaluateBatch');
// process evaluation outputs
const output = [];
// garbOutputLabelsBatch is output labels which the garbler sent to us
assert(this.blob[`c${cNo}_ol`].length === c.outputSize*32*repeatCount);
const garbOutputLabelsBatch = splitIntoChunks(this.blob[`c${cNo}_ol`], c.outputSize*32);
for (let r=0; r < repeatCount; r++){
const evalOL = splitIntoChunks(evalOutputLabelsBatch[r], 16);
const garbOL = splitIntoChunks(garbOutputLabelsBatch[r], 16);
const bits = [];
for (let i = 0; i < c.outputSize; i++) {
if (eq(evalOL[i], garbOL[i*2])) {
bits.push(0);
} else if (eq(evalOL[i], garbOL[i*2+1])) {
bits.push(1);
} else {
console.log('evaluator output does not match the garbled outputs');
}
}
// reverse output bits so that the values of the output be placed in
// the same order as they appear in the *.casm files
// plaintext has a padding in MSB to make it a multiple of 8 bits. We
// decompose into bits and drop the padding
const bits = bytesToBits(evalPlaintextBatch[r]).slice(0, c.outputSize);
const out = this.parseOutputBits(cNo, bits);
this.output[cNo] = out;
output.push(out);
}
console.log('output is', output);
this.myCommit[cNo] = await sha256(concatTA(...output));
const cStep2Out = await sendPromise;
@@ -787,6 +855,68 @@ export class TWOPC {
return output;
}
async processC4Labels(inputBits, hisOtResp, encryptedLabels){
assert(encryptedLabels.length % 320 == 0);
// notary also sends input labels for client_write_key and
// client_write_iv for c4 and c6. Each set of labels corresponding
// to the same input bit is encrypted with a unique key.
// replace the labels for cwk/ciw with zeroes. Then the value returned by
// parseResponse() in places of the zeroes will be the decryption keys
// needed to decrypt each encrypted set of labels.
const newOtResp = concatTA(
hisOtResp.slice(0, 128*32),
new Uint8Array(128*32).fill(0),
hisOtResp.slice(256*32, 288*32),
new Uint8Array(32*32).fill(0),
hisOtResp.slice(320*32));
const allClientLabels = this.otR.parseResponse(inputBits, newOtResp);
// now allClientLabels in cwk/civ position have keys needed to decrypt labels
const decrKeys = splitIntoChunks(concatTA(
allClientLabels.slice(128*16, 256*16),
allClientLabels.slice(288*16, 320*16)), 16);
const encrLabels = splitIntoChunks(encryptedLabels, encryptedLabels.length/320);
// choiceBits for cwk/civ
const choiceBits = [].concat(inputBits.slice(128, 256), inputBits.slice(288, 320));
for (let i=0; i < 160; i++){
// the decryption key only decrypts labels corresponding to our choice bit
let toDecrypt;
if (choiceBits[i] == 0){
toDecrypt = encrLabels[i*2];
} else {
toDecrypt = encrLabels[i*2+1];
}
this.decrLabels[i] = splitIntoChunks(await AESCTRdecrypt(decrKeys[i], toDecrypt), 16);
}
// insert decrypted labels for c4 (they are at index 0),
// the rest of the labels will be used for circuit 6
const newLabels = concatTA(
allClientLabels.slice(0, 128*16),
concatTA(...this.decrLabels.slice(0, 128).map(function(arr){return arr[0];})),
allClientLabels.slice(256*16, 288*16),
concatTA(...this.decrLabels.slice(128, 160).map(function(arr){return arr[0];})),
allClientLabels.slice(320*16));
return newLabels;
}
processC6Labels(allClientLabels){
// insert the labels for client_write_key and client_write_iv which we got
// during circuit 4 execution (see func processC4Labels)
const labelsPerExecution = splitIntoChunks(allClientLabels, allClientLabels.length/this.C6Count);
const updatedLabels = [];
for (let i=0; i < this.C6Count; i++) {
updatedLabels.push(concatTA(
...this.decrLabels.map(function(arr){return arr[i+1];}),
labelsPerExecution[i]));
}
return concatTA(...updatedLabels);
}
// parseOutputBits converts the output bits of the circuit number "cNo" into
// a slice of output values in the same order as they appear in the *.casm files
parseOutputBits(cNo, outBits){
@@ -801,27 +931,26 @@ export class TWOPC {
// eslint-disable-next-line class-methods-use-this
// blob contains decryption table for all consecutive circuit execution followed
// by truth tables for all consecutive circuit execution
processBlob(blob) {
// split up blob into [truth table + output labels] for each circuit
const obj = {};
let offset = 0;
for (let i=1; i < this.cs.length; i++){
let truthTableSize = this.cs[i].andGateCount *48;
let outputLabelsSize = this.cs[i].outputSize *32;
if (i === 5){
truthTableSize = this.C5Count * truthTableSize;
outputLabelsSize = this.C5Count * outputLabelsSize;
let decryptionTableSize = Math.ceil(this.cs[i].outputSize/8);
if (i === 6){
decryptionTableSize = this.C6Count * decryptionTableSize;
}
else if ( i == 6 ){
truthTableSize = this.C6Count * truthTableSize;
outputLabelsSize = this.C6Count * outputLabelsSize;
obj['c'+String(i)+'_dt'] = blob.subarray(offset, offset+decryptionTableSize);
offset += decryptionTableSize;
}
for (let i=1; i < this.cs.length; i++){
let truthTablesSize = this.cs[i].andGateCount *48;
if (i === 6){
truthTablesSize = this.C6Count * truthTablesSize;
}
console.log('truthtable for ', i, ' is size: ', truthTableSize);
obj['c'+String(i)+'_tt'] = blob.subarray(offset, offset+truthTableSize);
offset += truthTableSize;
console.log('ol for ', i, ' is size: ', outputLabelsSize);
obj['c'+String(i)+'_ol'] = blob.subarray(offset, offset+outputLabelsSize);
offset += outputLabelsSize;
obj['c'+String(i)+'_tt'] = blob.subarray(offset, offset+truthTablesSize);
offset += truthTablesSize;
}
assert(blob.length === offset);
return obj;

View File

@@ -0,0 +1,35 @@
This folder contains files used by WebWorkers.
serializeCircuits.js:
Used on first startup to convert Bristol Fashion circuits into a compact
binary format.
gcworker_wasm.js:
Garbler and evaluator which uses garbler.wasm for most operations. We still perform
Salsa20 hashing in JS (because it is more performant than in wasm).
gcworker_purejs.js:
Garbler and evaluator written in pure JS (~5-10x slower than the wasm version).
Not used by default but can serve as a drop-in replacement in cases if wasm
cannot be loaded.
garbler.wasm:
Garbler and evaluator loaded by gcworker_wasm.js. It was compiled from Rust
into WebAssembly.
src/:
Rust source code for garbler.wasm. Not actually in use by the extenstion. It
is here just for convenience.
To compile garbler.wasm from source deterministically:
cd src/
sudo apt update && sudo apt install -y build-essential
# the official way to install rustup is:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
rustup install 1.58.1
rustup +1.58.1 target add wasm32-unknown-unknown --toolchain 1.58.1
RUSTFLAGS='-C target-feature=+simd128' cargo +1.58.1 build --target wasm32-unknown-unknown --release
The result in target/wasm32-unknown-unknown/release/garbler.wasm will be bit
identical to garbler.wasm

Binary file not shown.

View File

@@ -2,15 +2,18 @@
// gcworker.js is a WebWorker which performs garbling and evaluation
// of garbled circuits.
// This is a fixed-key-cipher garbling method from BHKR13 https://eprint.iacr.org/2013/426.pdf
// This is a fixed-key-cipher garbling method from BHKR13
// https://eprint.iacr.org/2013/426.pdf
// eslint-disable-next-line no-undef
var parentPort_;
let circuit = null;
let truthTable = null;
let truthTables = null;
let andGateIdx = null;
// fixedKey is used by randomOracle(). We need a 32-byte key because we use Salsa20. The last 4
// bytes will be filled with the index of the circuit's wire.
// fixedKey is used by randomOracle(). We need a 32-byte key because we use
// Salsa20. The last 4 bytes will be filled with the tweak, i.e. the index of
// the circuit's wire.
const fixedKey = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 0, 0, 0, 0]);
// sigma is Salsa's constant "expand 32-byte k"
@@ -18,9 +21,9 @@ const sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98,
121, 116, 101, 32, 107]);
// randomPool will be filled with data from getRandom
let randomPool;
// randomPoolOffset will be moved after data was read from randomPool
// randomPoolOffset will be shifted after data was read from randomPool
let randomPoolOffset = 0;
let garbledAssigment;
let garblingAssigment, evaluationAssigment;
var crypto_;
if (typeof(importScripts) !== 'undefined') {
@@ -50,46 +53,46 @@ if (typeof(importScripts) !== 'undefined') {
}
function processMessage(obj){
const msg = obj.msg;
const data = obj.data;
if (msg === 'parse'){
circuit = data;
garbledAssigment = new Uint8Array(32*(circuit.wiresCount));
// no need to respond to this message
if (obj.msg === 'parse'){
circuit = obj.circuit;
garblingAssigment = new Uint8Array(32*(circuit.wiresCount));
evaluationAssigment = new Uint8Array(16*(circuit.wiresCount));
postMsg('DONE');
}
else if (msg === 'setTruthTable'){
assert(data.byteLength == circuit.andGateCount*48);
truthTable = new Uint8Array(data);
else if (obj.msg === 'setTruthTables'){
assert(obj.tt.byteLength == circuit.andGateCount*48);
truthTables = new Uint8Array(obj.tt);
// no need to respond to this
}
else if (msg === 'garble'){
else if (obj.msg === 'garble'){
if (circuit == null){
console.log('error: need to parse circuit before garble');
return;
}
console.time('garbling done in');
const reuseLabels = (data == undefined) ? undefined : data.reuseLabels;
const reuseIndexes = (data == undefined) ? undefined : data.reuseIndexes;
const reuseR = (data == undefined) ? undefined : data.reuseR;
const [truthTable, inputLabels, outputLabels, R] = garble(circuit, garbledAssigment, reuseLabels, reuseIndexes, reuseR);
assert (truthTable.length === circuit.andGateCount*48);
console.time('worker_garble');
const [inputLabels, truthTables, decodingTable] = garble(circuit, garblingAssigment);
assert (inputLabels.length === circuit.clientInputSize*32 + circuit.notaryInputSize*32);
assert (outputLabels.length === circuit.outputSize*32);
const obj = {'tt': truthTable.buffer, 'il': inputLabels.buffer, 'ol': outputLabels.buffer, 'R': R};
console.timeEnd('garbling done in');
postMsg(obj, [truthTable.buffer, inputLabels.buffer, outputLabels.buffer]);
assert (truthTables.length === circuit.andGateCount*48);
assert (decodingTable.length === Math.ceil(circuit.outputSize/8));
const obj = {'il': inputLabels.buffer, 'tt': truthTables.buffer, 'dt': decodingTable.buffer};
postMsg(obj, [inputLabels.buffer, truthTables.buffer, decodingTable.buffer]);
console.timeEnd('worker_garble');
}
else if (msg === 'evaluate'){
if (circuit == null || truthTable == null){
else if (obj.msg === 'evaluate'){
if (circuit == null || truthTables == null){
console.log('error: need to parse circuit and set truth table before evaluate');
return;
}
const garbledAssigment = new Uint8Array(16*(circuit.wiresCount));
const inputLabels = new Uint8Array(data);
assert (inputLabels.length === circuit.clientInputSize*16 + circuit.notaryInputSize*16);
const outputLabels = evaluate(circuit, garbledAssigment, truthTable, inputLabels);
assert (outputLabels.length === circuit.outputSize*16);
postMsg(outputLabels.buffer);
console.time('worker_evaluate');
const inputSize = circuit.clientInputSize*16 + circuit.notaryInputSize*16;
const inputLabels = new Uint8Array(obj.il);
const decodingTable = new Uint8Array(obj.dt);
assert (inputLabels.length === inputSize);
assert (decodingTable.length === Math.ceil(circuit.outputSize/8));
const plaintext = evaluate(circuit, evaluationAssigment, inputLabels, truthTables, decodingTable);
assert (plaintext.length === Math.ceil(circuit.outputSize/8));
postMsg(plaintext.buffer);
console.timeEnd('worker_evaluate');
}
else {
console.log('Error: unexpected message in worker');
@@ -125,39 +128,23 @@ function generateInputLabels(count, R){
return newLabels;
}
function garble(circuit, ga, reuseLabels = new Uint8Array(0), reuseIndexes = [], R){
function garble(circuit, ga){
const inputCount = circuit.notaryInputSize + circuit.clientInputSize;
fillRandom((inputCount+1+circuit.andGateCount)*16);
R = R || newR();
const R = newR();
// generate new labels
const newLabels = generateInputLabels(inputCount - reuseIndexes.length, R);
const inputLabels = generateInputLabels(inputCount, R);
ga.set(inputLabels);
const truthTables = new Uint8Array(circuit.andGateCount*48);
// set both new and reused labels into ga
let reusedCount = 0; // how many reused inputs were already put into ga
let newInputsCount = 0; // how many new inputs were already put into ga
for (let i = 0; i < inputCount; i++) {
if (reuseIndexes.includes(i)) {
ga.set(reuseLabels.subarray(reusedCount*32, reusedCount*32+32), i*32);
reusedCount += 1;
}
else {
ga.set(newLabels.subarray(newInputsCount*32, newInputsCount*32+32), i*32);
newInputsCount += 1;
}
}
const truthTable = new Uint8Array(circuit.andGateCount*48);
let andGateIdx = 0;
andGateIdx = 0;
// garble gates
for (let i = 0; i < circuit.gatesCount; i++) {
const gateBlob = circuit.gatesBlob.subarray(i*10, i*10+10);
const op = ['XOR', 'AND', 'INV'][gateBlob[0]];
if (op === 'AND') {
garbleAnd(gateBlob, R, ga, truthTable, andGateIdx, i);
garbleAnd(gateBlob, R, ga, truthTables, andGateIdx, i);
andGateIdx += 1;
} else if (op === 'XOR') {
garbleXor(gateBlob, R, ga);
@@ -168,8 +155,13 @@ function garble(circuit, ga, reuseLabels = new Uint8Array(0), reuseIndexes = [],
}
}
return [truthTable, ga.slice(0, inputCount*32), ga.slice(-circuit.outputSize*32), R];
// get decoding table: LSB of label0 for each output wire
const outLSBs = [];
for (let i = 0; i < circuit.outputSize; i++){
outLSBs.push(ga[ga.length-circuit.outputSize*32+i*32+15] & 1);
}
const decodingTable = bitsToBytes(outLSBs);
return [inputLabels, truthTables, decodingTable];
}
@@ -185,7 +177,7 @@ const garbleAnd = function (gateBlob, R, ga, tt, andGateIdx, id) {
const in2_0 = gaGetIndexG(ga, in2, 0);
const in2_1 = gaGetIndexG(ga, in2, 1);
// rows is a truthtable if wire labels in a canonical order, the third
// rows is a truthtable in a canonical order, the third
// item shows an index of output label
const rows = [
[in1_0, in2_0, 0],
@@ -239,12 +231,11 @@ const garbleXor = function (gateBlob, R, ga) {
const out = threeBytesToInt(gateBlob.subarray(7, 10));
const in1_0 = gaGetIndexG(ga, in1, 0);
const in1_1 = gaGetIndexG(ga, in1, 1);
const in2_0 = gaGetIndexG(ga, in2, 0);
const in2_1 = gaGetIndexG(ga, in2, 1);
gaSetIndexG(ga, out, 0, xor(in1_0, in2_0));
gaSetIndexG(ga, out, 1, xor(xor(in1_1, in2_1), R, true));
const label0 = xor(in1_0, in2_0);
gaSetIndexG(ga, out, 0, label0);
gaSetIndexG(ga, out, 1, xor(label0, R, true));
};
@@ -259,18 +250,17 @@ const garbleNot = function (gateBlob, ga) {
gaSetIndexG(ga, out, 1, in1_0.slice());
};
function evaluate (circuit, ga, tt, inputLabels) {
function evaluate (circuit, ga, inputLabels, truthTables, decodingTable) {
// set input labels
ga.set(inputLabels);
// evaluate one gate at a time
let numberOfANDGates = 0;
console.time('worker_evaluate');
for (let i = 0; i < circuit.gatesCount; i++) {
const gateBlob = circuit.gatesBlob.subarray(i*10, i*10+10);
const op = ['XOR', 'AND', 'INV'][gateBlob[0]];
if (op === 'AND') {
evaluateAnd(ga, tt, numberOfANDGates, gateBlob, i);
evaluateAnd(ga, truthTables, numberOfANDGates, gateBlob, i);
numberOfANDGates += 1;
} else if (op === 'XOR') {
evaluateXor(ga, gateBlob);
@@ -280,9 +270,16 @@ function evaluate (circuit, ga, tt, inputLabels) {
throw new Error(`Unrecognized gate: ${op}`);
}
}
console.timeEnd('worker_evaluate');
return ga.slice((circuit.wiresCount-circuit.outputSize)*16, circuit.wiresCount*16);
//decode output labels
// get decoding table: LSB of label0 for each output wire
const outLSBs = [];
for (let i = 0; i < circuit.outputSize; i++){
outLSBs.push(ga[ga.length-circuit.outputSize*16+i*16+15] & 1);
}
const encodings = bitsToBytes(outLSBs);
const plaintext = xor(decodingTable, encodings);
return plaintext;
}
const evaluateAnd = function (ga, tt, andGateIdx, gateBlob, id) {
@@ -453,6 +450,20 @@ function concatTA (...arr){
return newArray;
}
// convert an array of 0/1 (with least bit at index 0) to Uint8Array
function bitsToBytes(arr){
assert(arr.length % 8 === 0);
const ba = new Uint8Array(arr.length/8);
for (let i=0; i < ba.length; i++){
let sum = 0;
for (let j=0; j < 8; j++){
sum += arr[i*8+j] * (2**j);
}
ba[ba.length-1-i] = sum;
}
return ba;
}
// use Salsa20 as a random permutator. Instead of the nonce, we feed the data that needs
// to be permuted.
function Salsa20(key, data){

View File

@@ -0,0 +1,454 @@
/* global process, global */
// gcworker_wasm.js is a WebWorker which performs garbling and evaluation
// of garbled circuits.
// This is a fixed-key-cipher garbling method from BHKR13 https://eprint.iacr.org/2013/426.pdf
// eslint-disable-next-line no-undef
let parentPort_;
let loadFile;
let module;
let circuit = null;
let truthTables = null;
// offsets are set once for the life of the wasm module
let malloc_ptr;
// cryptographically random data
let randOffset, randSize;
// decoding table
let dtOffset, dtSize;
// output (for garbler this is decoding table, tof evaluator this is plaintext)
let outOffset, outSize;
// truth tables
let ttOffset, ttSize;
// input labels
let ilOffset, ilSize;
let wasm = null;
// fixedKey is used by randomOracle(). We need a 32-byte key because we use Salsa20. The last 4
// bytes will be filled with the index of the circuit's wire.
const fixedKey = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]);
// sigma is Salsa's constant "expand 32-byte k"
const sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98,
121, 116, 101, 32, 107]);
var crypto_;
if (typeof(importScripts) !== 'undefined') {
crypto_ = self.crypto;
crypto_.getRandomValues = function(arr){
return new Uint8Array([...Array(arr.length).keys()]);
};
loadFile = loadFileBrowser;
self.onmessage = function(event) {
processMessage(event.data);
};
} else {
// we are in nodejs
import('module').then((mod) => {
module = mod;
// we cannot use the "import" keyword here because on first pass the browser unconditionaly
// parses this if clause and will error out if "import" is found.
// using process.argv instead of import.meta.url to get the name of this script
const filePath = 'file://' + process.argv[1];
// this workaround allows to require() from ES6 modules, which is not allowed by default
const require = module.createRequire(filePath);
const { parentPort } = require('worker_threads');
parentPort_ = parentPort;
const { Crypto } = require('@peculiar/webcrypto');
crypto_ = new Crypto();
const perf = {'now':function(){return 0;}};
global.performance = perf;
loadFile = loadFileNode;
parentPort.on('message', msg => {
processMessage(msg);
});
});
}
async function loadFileBrowser(path) {
const bytes = await fetch(path);
return bytes.arrayBuffer();
}
async function loadFileNode(file) {
const filePath = 'file://' + process.argv[1];
// this workaround allows to require() from ES6 modules, which is not allowed by default
const require = module.createRequire(filePath);
const path = require('path');
const dir = path.dirname(process.argv[1]);
const fs = require('fs');
const data = fs.readFileSync(path.join(dir, file));
return data;
}
function processMessage(obj){
const msg = obj.msg;
// parse is called only once for the duration of the wasm module, even if
// multiple garbling executions will be made.
if (msg === 'parse'){
circuit = obj.circuit;
loadFile('garbler.wasm').then(async function(ab){
// we don't need to allocate any heap memory
const memory = new WebAssembly.Memory({initial:0, maximum:0});
const module = await WebAssembly.instantiate(ab, { js: {memory: memory}});
wasm = module.instance.exports;
assert(circuit.gatesBlob.length == circuit.gatesCount*10);
malloc_ptr = wasm.initAll(
circuit.gatesCount,
circuit.wiresCount,
circuit.notaryInputSize,
circuit.clientInputSize,
circuit.outputSize,
circuit.andGateCount
);
let offset = malloc_ptr;
// ipc1 and ipc2
offset += (20+48);
const gateSize = circuit.gatesCount*10;
const gatesBytesArr = new Uint8Array(wasm.memory.buffer, offset, gateSize);
offset += gateSize;
gatesBytesArr.set(circuit.gatesBlob);
console.time('parse blob');
wasm.parseGatesBlob();
console.timeEnd('parse blob');
randOffset = offset;
randSize = (circuit.notaryInputSize + circuit.clientInputSize + 1)*16;
offset += randSize;
dtOffset = offset;
dtSize = Math.ceil(circuit.outputSize/8);
offset += dtSize;
outOffset = offset;
outSize = Math.ceil(circuit.outputSize/8);
offset += outSize;
ttOffset = offset;
ttSize = circuit.andGateCount*48;
offset += ttSize;
ilOffset = offset;
ilSize = (circuit.notaryInputSize + circuit.clientInputSize)*32;
offset += ilSize;
postMsg('DONE');
});
}
else if (msg === 'setTruthTables'){
assert(obj.tt.byteLength == circuit.andGateCount*48);
truthTables = new Uint8Array(obj.tt);
}
else if (msg === 'garble'){
if (circuit == null){
console.log('error: need to parse circuit before garble');
return;
}
console.time('garbling done in');
let randArr = new Uint8Array(wasm.memory.buffer, randOffset, randSize);
randArr.set(crypto_.getRandomValues(new Uint8Array(randSize)));
const dataBuf = new Uint8Array(wasm.memory.buffer, malloc_ptr, 16);
const tweak = new Uint8Array(wasm.memory.buffer, malloc_ptr+16, 4);
const in1 = new Uint8Array(wasm.memory.buffer, malloc_ptr+20, 16);
const in2 = new Uint8Array(wasm.memory.buffer, malloc_ptr+20+16, 16);
const in3 = new Uint8Array(wasm.memory.buffer, malloc_ptr+20+32, 16);
wasm.startGarbler();
for (let i=0; i < circuit.andGateCount; i++){
wasm.resumeGarbler_0();
Salsa20(dataBuf, tweak);
wasm.resumeGarbler_1();
Salsa20(in1, tweak);
Salsa20(in2, tweak);
Salsa20(in3, tweak);
}
wasm.finishGarbler();
const inputLabels = new Uint8Array(wasm.memory.buffer, ilOffset, ilSize).slice();
const truthTables = new Uint8Array(wasm.memory.buffer, ttOffset, ttSize).slice();
const decodingTable = new Uint8Array(wasm.memory.buffer, outOffset, outSize).slice();
const obj = {'il': inputLabels.buffer, 'tt': truthTables.buffer, 'dt': decodingTable.buffer};
console.timeEnd('garbling done in');
postMsg(obj, [inputLabels.buffer, truthTables.buffer, decodingTable.buffer]);
}
else if (msg === 'evaluate'){
if (circuit == null || truthTables == null){
console.log('error: need to parse circuit and set truth table before evaluate');
return;
}
const inputLabels = new Uint8Array(obj.il);
const decodingTable = new Uint8Array(obj.dt);
assert (inputLabels.length === circuit.clientInputSize*16 + circuit.notaryInputSize*16);
assert (decodingTable.length === Math.ceil(circuit.outputSize/8));
const ilArr = new Uint8Array(wasm.memory.buffer, ilOffset, inputLabels.length);
ilArr.set(inputLabels);
const ttArr = new Uint8Array(wasm.memory.buffer, ttOffset, ttSize);
ttArr.set(truthTables);
const dtArr = new Uint8Array(wasm.memory.buffer, dtOffset, dtSize);
dtArr.set(decodingTable);
const dataBuf = new Uint8Array(wasm.memory.buffer, malloc_ptr, 16);
const tweak = new Uint8Array(wasm.memory.buffer, malloc_ptr+16, 4);
wasm.startEvaluator();
for (let i=0; i < circuit.andGateCount; i++){
wasm.resumeEvaluator_0();
Salsa20(dataBuf, tweak);
}
wasm.finishEvaluator();
const plaintext = new Uint8Array(wasm.memory.buffer, outOffset, outSize).slice();
postMsg(plaintext.buffer);
truthTables == null;
}
else {
console.log('Error: unexpected message in worker');
}
}
function postMsg(value, transferList){
if (typeof importScripts !== 'function'){
parentPort_.postMessage({data:value}, transferList);
} else {
postMessage(value, transferList);
}
}
function assert(condition, message) {
if (!condition) {
console.trace();
throw message || 'Assertion failed';
}
}
// use Salsa20 as a random permutator. Instead of the nonce, we feed the data that needs
// to be permuted. The output will be placed into data;
function Salsa20(data, t){
core_salsa20(data, fixedKey, t, sigma);
return data;
}
// copied from https://github.com/dchest/tweetnacl-js/blob/master/nacl-fast.js
// and modified to reuse input (p) as output and to output only 16 bytes. Also allows
// to input a 28-byte key (k) and a 4-byte tweak (t).
// modified parts are commented out
// function core_salsa20(o, p, k, c) {
function core_salsa20(p, k, t, c) {
var j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24,
j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24,
j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24,
j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24,
j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24,
j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24,
j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24,
j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24,
j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24,
j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24,
j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24,
j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24,
j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24,
j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24,
//j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24,
// the commented out line was replaced with the line below
j14 = t[0] & 0xff | (t[1] & 0xff)<<8 | (t[2] & 0xff)<<16 | (t[3] & 0xff)<<24,
j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24;
var x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7,
x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14,
x15 = j15, u;
for (var i = 0; i < 20; i += 2) {
u = x0 + x12 | 0;
x4 ^= u<<7 | u>>>(32-7);
u = x4 + x0 | 0;
x8 ^= u<<9 | u>>>(32-9);
u = x8 + x4 | 0;
x12 ^= u<<13 | u>>>(32-13);
u = x12 + x8 | 0;
x0 ^= u<<18 | u>>>(32-18);
u = x5 + x1 | 0;
x9 ^= u<<7 | u>>>(32-7);
u = x9 + x5 | 0;
x13 ^= u<<9 | u>>>(32-9);
u = x13 + x9 | 0;
x1 ^= u<<13 | u>>>(32-13);
u = x1 + x13 | 0;
x5 ^= u<<18 | u>>>(32-18);
u = x10 + x6 | 0;
x14 ^= u<<7 | u>>>(32-7);
u = x14 + x10 | 0;
x2 ^= u<<9 | u>>>(32-9);
u = x2 + x14 | 0;
x6 ^= u<<13 | u>>>(32-13);
u = x6 + x2 | 0;
x10 ^= u<<18 | u>>>(32-18);
u = x15 + x11 | 0;
x3 ^= u<<7 | u>>>(32-7);
u = x3 + x15 | 0;
x7 ^= u<<9 | u>>>(32-9);
u = x7 + x3 | 0;
x11 ^= u<<13 | u>>>(32-13);
u = x11 + x7 | 0;
x15 ^= u<<18 | u>>>(32-18);
u = x0 + x3 | 0;
x1 ^= u<<7 | u>>>(32-7);
u = x1 + x0 | 0;
x2 ^= u<<9 | u>>>(32-9);
u = x2 + x1 | 0;
x3 ^= u<<13 | u>>>(32-13);
u = x3 + x2 | 0;
x0 ^= u<<18 | u>>>(32-18);
u = x5 + x4 | 0;
x6 ^= u<<7 | u>>>(32-7);
u = x6 + x5 | 0;
x7 ^= u<<9 | u>>>(32-9);
u = x7 + x6 | 0;
x4 ^= u<<13 | u>>>(32-13);
u = x4 + x7 | 0;
x5 ^= u<<18 | u>>>(32-18);
u = x10 + x9 | 0;
x11 ^= u<<7 | u>>>(32-7);
u = x11 + x10 | 0;
x8 ^= u<<9 | u>>>(32-9);
u = x8 + x11 | 0;
x9 ^= u<<13 | u>>>(32-13);
u = x9 + x8 | 0;
x10 ^= u<<18 | u>>>(32-18);
u = x15 + x14 | 0;
x12 ^= u<<7 | u>>>(32-7);
u = x12 + x15 | 0;
x13 ^= u<<9 | u>>>(32-9);
u = x13 + x12 | 0;
x14 ^= u<<13 | u>>>(32-13);
u = x14 + x13 | 0;
x15 ^= u<<18 | u>>>(32-18);
}
x0 = x0 + j0 | 0;
x1 = x1 + j1 | 0;
x2 = x2 + j2 | 0;
x3 = x3 + j3 | 0;
// x4 = x4 + j4 | 0;
// x5 = x5 + j5 | 0;
// x6 = x6 + j6 | 0;
// x7 = x7 + j7 | 0;
// x8 = x8 + j8 | 0;
// x9 = x9 + j9 | 0;
// x10 = x10 + j10 | 0;
// x11 = x11 + j11 | 0;
// x12 = x12 + j12 | 0;
// x13 = x13 + j13 | 0;
// x14 = x14 + j14 | 0;
// x15 = x15 + j15 | 0;
p[ 0] = x0 >>> 0 & 0xff;
p[ 1] = x0 >>> 8 & 0xff;
p[ 2] = x0 >>> 16 & 0xff;
p[ 3] = x0 >>> 24 & 0xff;
p[ 4] = x1 >>> 0 & 0xff;
p[ 5] = x1 >>> 8 & 0xff;
p[ 6] = x1 >>> 16 & 0xff;
p[ 7] = x1 >>> 24 & 0xff;
p[ 8] = x2 >>> 0 & 0xff;
p[ 9] = x2 >>> 8 & 0xff;
p[10] = x2 >>> 16 & 0xff;
p[11] = x2 >>> 24 & 0xff;
p[12] = x3 >>> 0 & 0xff;
p[13] = x3 >>> 8 & 0xff;
p[14] = x3 >>> 16 & 0xff;
p[15] = x3 >>> 24 & 0xff;
// we only need 16 bytes of the output
// o[ 0] = x0 >>> 0 & 0xff;
// o[ 1] = x0 >>> 8 & 0xff;
// o[ 2] = x0 >>> 16 & 0xff;
// o[ 3] = x0 >>> 24 & 0xff;
// o[ 4] = x1 >>> 0 & 0xff;
// o[ 5] = x1 >>> 8 & 0xff;
// o[ 6] = x1 >>> 16 & 0xff;
// o[ 7] = x1 >>> 24 & 0xff;
// o[ 8] = x2 >>> 0 & 0xff;
// o[ 9] = x2 >>> 8 & 0xff;
// o[10] = x2 >>> 16 & 0xff;
// o[11] = x2 >>> 24 & 0xff;
// o[12] = x3 >>> 0 & 0xff;
// o[13] = x3 >>> 8 & 0xff;
// o[14] = x3 >>> 16 & 0xff;
// o[15] = x3 >>> 24 & 0xff;
// o[16] = x4 >>> 0 & 0xff;
// o[17] = x4 >>> 8 & 0xff;
// o[18] = x4 >>> 16 & 0xff;
// o[19] = x4 >>> 24 & 0xff;
// o[20] = x5 >>> 0 & 0xff;
// o[21] = x5 >>> 8 & 0xff;
// o[22] = x5 >>> 16 & 0xff;
// o[23] = x5 >>> 24 & 0xff;
// o[24] = x6 >>> 0 & 0xff;
// o[25] = x6 >>> 8 & 0xff;
// o[26] = x6 >>> 16 & 0xff;
// o[27] = x6 >>> 24 & 0xff;
// o[28] = x7 >>> 0 & 0xff;
// o[29] = x7 >>> 8 & 0xff;
// o[30] = x7 >>> 16 & 0xff;
// o[31] = x7 >>> 24 & 0xff;
// o[32] = x8 >>> 0 & 0xff;
// o[33] = x8 >>> 8 & 0xff;
// o[34] = x8 >>> 16 & 0xff;
// o[35] = x8 >>> 24 & 0xff;
// o[36] = x9 >>> 0 & 0xff;
// o[37] = x9 >>> 8 & 0xff;
// o[38] = x9 >>> 16 & 0xff;
// o[39] = x9 >>> 24 & 0xff;
// o[40] = x10 >>> 0 & 0xff;
// o[41] = x10 >>> 8 & 0xff;
// o[42] = x10 >>> 16 & 0xff;
// o[43] = x10 >>> 24 & 0xff;
// o[44] = x11 >>> 0 & 0xff;
// o[45] = x11 >>> 8 & 0xff;
// o[46] = x11 >>> 16 & 0xff;
// o[47] = x11 >>> 24 & 0xff;
// o[48] = x12 >>> 0 & 0xff;
// o[49] = x12 >>> 8 & 0xff;
// o[50] = x12 >>> 16 & 0xff;
// o[51] = x12 >>> 24 & 0xff;
// o[52] = x13 >>> 0 & 0xff;
// o[53] = x13 >>> 8 & 0xff;
// o[54] = x13 >>> 16 & 0xff;
// o[55] = x13 >>> 24 & 0xff;
// o[56] = x14 >>> 0 & 0xff;
// o[57] = x14 >>> 8 & 0xff;
// o[58] = x14 >>> 16 & 0xff;
// o[59] = x14 >>> 24 & 0xff;
// o[60] = x15 >>> 0 & 0xff;
// o[61] = x15 >>> 8 & 0xff;
// o[62] = x15 >>> 16 & 0xff;
// o[63] = x15 >>> 24 & 0xff;
}

View File

@@ -0,0 +1,16 @@
[package]
name = "garbler"
version = "0.1.0"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[profile.release]
lto = true
# [dependencies]
# uncomment the dependencies below when debugging with wasm-bindgen
# wasm-bindgen = "0.2"
# console_error_panic_hook = "0.1.7"
# web-sys = { version = "0.3.56", features = ['console'] }

View File

@@ -0,0 +1,537 @@
#! [allow(non_snake_case)]
#! [allow(non_upper_case_globals)]
use core::slice::{from_raw_parts_mut, from_raw_parts};
// when debugging, uncomment the imports below,
// add #[wasm_bindgen] to the function and insert on 1st line of function:
// panic::set_hook(Box::new(console_error_panic_hook::hook));
//use wasm_bindgen::prelude::*;
//use web_sys::console;
//extern crate console_error_panic_hook;
//use std::panic;
// We are trying to make as many vars global as possible. It sacrifices code readability
// but the performance gains are worth it (~20% gains).
static mut gatesCount: u32 = 0;
static mut wiresCount: u32 = 0;
static mut andGateCount: u32 = 0;
static mut notaryInputSize: u32 = 0;
static mut clientInputSize: u32 = 0;
static mut outputSize: u32 = 0;
// gates is parsed gates
static mut gates: Vec<Gate> = Vec::new();
// ga is garbling assignment: all wires with their labels
static mut ga: Vec<[[u8; 16]; 2]> = Vec::new();
// ea is evaluating assignment: one label per wire
static mut ea: Vec<[u8; 16]> = Vec::new();
// R is circuit's delta (garbler only)
static mut R: [u8; 16] = [0; 16];
// resumeFrom saves the gate number from which we will resume
static mut resumeFrom: usize = 0;
// andIdx is index of the AND gate that was processed so far
static mut andIdx: usize = 0;
// m is malloc()ed pointer to shared memory between wasm and JS
static mut m: u32 = 0;
// ipc1 points to a 20-byte buffer for IPC. wasm puts 16-byte data to be salsaed and a
// 4-byte tweak and reads the salsaed result (the first 16 bytes).
static mut ipc1: &mut [u8] = &mut [0; 0];
// ipc2 points to a 48-byte buffer for IPC. wasm puts data to be salsaed,
// JS puts the result of salsaing.
static mut ipc2: &mut [u8] = &mut [0; 0];
// output points to output (for garbler it is decoding table, for evaluator it
// is plaintext output)
static mut output: &mut [u8] = &mut [0; 0];
// decoding_table point to the decoding table (evaluator's input)
static mut decoding_table: & [u8] = & [0; 0];
// tt points to truth tables
static mut tt: &mut [u8] = &mut [0; 0];
// il points to input labels
static mut il: &mut [u8] = &mut [0; 0];
// gatesBuf points to serialized gates description
static mut gatesBuf: &[u8] = &[0; 0];
// randBuf points to crypto random data supplied by JS
static mut randBuf: &[u8] = &[0; 0];
// g points to a gate currently being processed
static mut g: &Gate = &Gate{op:0, in1:0, in2:0, out:0};
// xor* are values that need to be xored to randomOracle's output
// to get an encrypted row
static mut xor0: [u8; 16] = [0; 16];
static mut xor1: [u8; 16] = [0; 16];
static mut xor2: [u8; 16] = [0; 16];
// rows contains info about rows of 1 specific AND gate
static mut rows: Vec<Row> = Vec::new();
static mut idxToReduce: u8 = 0;
// a2 and b4 used in encrypt();
static mut a2: [u8; 16] = [0; 16];
static mut b4: [u8; 16] = [0; 16];
// a and b a re return values of encrypt()
static mut a: [u8; 16] = [0; 16];
static mut b: [u8; 16] = [0; 16];
#[derive(Default, Clone)]
struct Gate {
// gate operation {'XOR': 0, 'AND': 1, 'INV': 2};
op: u8,
// first input wire number
in1: u32,
// second input wire number
in2: u32,
// output wire number
out: u32,
}
struct Row <'a>{
in1: &'a[u8; 16],
in2: &'a[u8; 16],
outNo: u8,
}
#[no_mangle]
pub fn startEvaluator() {
unsafe {
resumeFrom = 0;
andIdx = 0;
// set input labels
for i in 0..(notaryInputSize + clientInputSize) as usize {
ea[i].copy_from_slice(&il[i*16..i*16+16]);
}
}
return;
}
#[no_mangle]
pub fn startGarbler() {
unsafe {
resumeFrom = 0;
andIdx = 0;
idxToReduce = 99;
R.copy_from_slice(&randBuf[0..16]);
R[15] = R[15] | 0x01;
// generate input labels
for i in 0..(notaryInputSize + clientInputSize) as usize {
let mut label0: [u8; 16] = [0; 16];
label0.copy_from_slice(&randBuf[16 + i * 16..16 + (i + 1) * 16]);
let label1 = xor(&label0, &R);
ga[i] = [label0, label1];
}
}
return;
}
#[no_mangle]
//#[wasm_bindgen]
// resume after AND gate was processed. Back to normal gate-by-gate
pub fn resumeGarbler_0() {
unsafe {
if resumeFrom > 0 || andIdx+1 == andGateCount as usize{
finishLastANDGate();
}
let mut lastIdx = 0;
for i in resumeFrom..gates.len() {
lastIdx = i as u32;
g = &gates[i];
if g.op == 1 {
// garbleAndStage1 will implicitly return values in globals a and b
garbleAndStage1();
ipc1[0..16].copy_from_slice(&a);
ipc1[16..20].copy_from_slice(&i.to_be_bytes());
xor0 = b;
resumeFrom = i;
return;
} else if g.op == 0 {
garbleXor();
} else if g.op == 2 {
garbleNot();
} else {
panic!("unknown gate op");
}
}
assert!(gatesCount == lastIdx+1);
return;
}
}
#[no_mangle]
//#[wasm_bindgen]
// resume after AND gate was processed. Back to normal gate-by-gate
pub fn resumeEvaluator_0() {
unsafe {
// this is the last step of the previous AND gate
if resumeFrom > 0 {
let out = gates[resumeFrom].out;
ea[out as usize] = xor(&ipc1[0..16], &xor0);
andIdx += 1;
resumeFrom += 1;
}
// proceed gate by gate
let mut lastIdx = 0;
for i in resumeFrom..gates.len() {
lastIdx = i as u32;
g = &gates[i];
if g.op == 1 {
// evaluateAndStage1 will implicitly return values in globals a and b
evaluateAndStage1();
ipc1[0..16].copy_from_slice(&a);
ipc1[16..20].copy_from_slice(&i.to_be_bytes());
xor0 = b;
resumeFrom = i;
return;
} else if g.op == 0 {
evaluateXor();
} else if g.op == 2 {
evaluateNot();
} else {
panic!("unknown gate op");
}
}
assert!(gatesCount == lastIdx+1);
return;
}
}
#[no_mangle]
//#[wasm_bindgen]
// resume after 1 Salsa invocation
pub fn resumeGarbler_1() {
unsafe {
let outWire = xor(&ipc1[0..16], &xor0);
let mut outLabels: [[u8; 16]; 2] = [[0;16];2];
if idxToReduce == 3 {
outLabels[0] = xor(&outWire, &R);
outLabels[1] = outWire;
} else {
outLabels[0] = outWire;
outLabels[1] = xor(&outWire, &R);
}
let out = gates[resumeFrom].out;
ga[out as usize][0] = outLabels[0];
ga[out as usize][1] = outLabels[1];
for i in 0..rows.len(){
if i == idxToReduce as usize {
continue;
}
encrypt(&rows[i].in1, &rows[i].in2, &outLabels[rows[i].outNo as usize]);
let point = (2 * getPoint(&rows[i].in1) + getPoint(&rows[i].in2)) as usize;
ipc2[point*16..(point+1)*16].copy_from_slice(&a);
if point == 0 {
xor0 = b;
}
else if point == 1 {
xor1 = b;
}
else if point == 2 {
xor2 = b;
}
else {
panic!("wrong point");
}
}
}
}
#[no_mangle]
// final touches after all salsa invocations:
// xor the data from JS with saved data to finish row encryption
pub fn finishLastANDGate() {
unsafe {
tt[andIdx* 48..andIdx*48+16].copy_from_slice(&xor( &xor0, &ipc2[0..16]));
tt[andIdx*48+16..andIdx*48+32].copy_from_slice(&xor( &xor1, &ipc2[16..32]));
tt[andIdx*48+32..andIdx*48+48].copy_from_slice(&xor( &xor2, &ipc2[32..48]));
andIdx += 1;
resumeFrom += 1;
}
}
#[no_mangle]
//#[wasm_bindgen]
// finishGarbler is called after garbling of all AND gates has been done
// to output the output labels and input labels. (the truth tables are already
// sitting in the output buffer).
pub fn finishGarbler() {
unsafe {
// finish with the remaining non-AND gates
resumeGarbler_0();
let roundedUp = ((outputSize+7)/8) as usize;
let mut bits: Vec<u8> = vec![0; roundedUp*8];
for i in 0..outputSize as usize {
// get decoding table: LSB of label0 for each output wire
bits[i] = ga[ga.len() - outputSize as usize + i][0][15] & 1
}
output.copy_from_slice(&bitsToBytes(&bits));
// input label size: how many input label pairs there are
let ils = (notaryInputSize + clientInputSize) as usize;
for i in 0..ils {
il[i* 32..i*32+16].copy_from_slice(&ga[i][0]);
il[i*32+16..i*32+32].copy_from_slice(&ga[i][1]);
}
}
}
// convert a string of bits with LSB at index 0 into big-endian bytes
fn bitsToBytes(bits: &Vec<u8>) -> Vec<u8> {
let mut outBytes: Vec<u8> = vec![0; bits.len()/8];
let byteLen = outBytes.len();
for i in 0..byteLen {
let eightBits = &bits[i*8..i*8+8];
let mut byte: u8 = 0;
for j in 0..8 {
byte <<= 1;
byte ^= eightBits[7-j]
}
outBytes[byteLen-1 - i] = byte
}
return outBytes
}
#[no_mangle]
//#[wasm_bindgen]
// finishEvaluator is called after garbling of all AND gates has been done
// to output the output labels
pub fn finishEvaluator() {
//panic::set_hook(Box::new(console_error_panic_hook::hook));
unsafe {
// finish with the remaining non-AND gates
resumeEvaluator_0();
let roundedUp = ((outputSize+7)/8) as usize;
let mut bits: Vec<u8> = vec![0; roundedUp*8];
for i in 0..outputSize as usize {
// get decoding table: LSB of label0 for each output wire
bits[i] = ea[ea.len() - outputSize as usize + i][15] & 1
}
let outputBytes = bitsToBytes(&bits);
let plaintext = xorSlices(&outputBytes, &decoding_table);
output.copy_from_slice(&plaintext);
}
}
fn garbleXor() {
unsafe {
ga[g.out as usize][0] = xor(&ga[g.in1 as usize][0], &ga[g.in2 as usize][0]);
ga[g.out as usize][1] = xor(&ga[g.out as usize][0], &R);
}
}
fn evaluateXor() {
unsafe {
let in1 = &ea[g.in1 as usize];
let in2 = &ea[g.in2 as usize];
ea[g.out as usize] = xor(in1, in2);
}
}
fn garbleNot() {
unsafe {
ga[g.out as usize][0] = ga[g.in1 as usize][1];
ga[g.out as usize][1] = ga[g.in1 as usize][0];
}
}
fn evaluateNot() {
unsafe {
ea[g.out as usize] = ea[g.in1 as usize];
}
}
fn garbleAndStage1() {
unsafe {
let in1_0 = &ga[g.in1 as usize][0];
let in1_1 = &ga[g.in1 as usize][1];
let in2_0 = &ga[g.in2 as usize][0];
let in2_1 = &ga[g.in2 as usize][1];
rows = vec![
Row {in1: in1_0, in2: in2_0, outNo: 0,},
Row {in1: in1_0, in2: in2_1, outNo: 0,},
Row {in1: in1_1, in2: in2_0, outNo: 0,},
Row {in1: in1_1, in2: in2_1, outNo: 1,}];
for i in 0..rows.len() {
if getPoint(&rows[i].in1) == 1 && getPoint(&rows[i].in2) == 1 {
idxToReduce = i as u8;
// encrypt implicitly returns in a and b
encrypt(&rows[i].in1, &rows[i].in2, &[0; 16]);
break;
}
}
assert!(idxToReduce != 99);
}
}
fn evaluateAndStage1() {
unsafe {
let label1 = &ea[g.in1 as usize];
let label2 = &ea[g.in2 as usize];
let mut cipher: [u8; 16] = [0; 16]; // if point == 3, we will keep it zeroed
let point = (2 * getPoint(label1) + getPoint(label2)) as usize;
if point != 3 {
cipher.copy_from_slice(&tt[andIdx*48+16*point..andIdx*48+16*point+16]);
}
// decrypt implicitly returns in globals a and b
decrypt(label1, label2, &cipher);
}
}
fn getPoint(label: &[u8; 16]) -> u8 {
return label[15] & 0x01;
}
fn decrypt(a_: &[u8; 16], b_: &[u8; 16], m_: &[u8; 16]){
encrypt(a_, b_, m_);
}
fn encrypt(a_: &[u8; 16], b_: &[u8; 16], m_: &[u8; 16]) {
unsafe {
a2.copy_from_slice(a_);
let leastbyte = a2[0];
a2.copy_within(1..15, 0);
a2[14] = leastbyte;
b4.copy_from_slice(b_);
let leastbyte0 = b4[0];
let leastbyte1 = b4[1];
b4.copy_within(2..15, 0);
b4[13] = leastbyte0;
b4[14] = leastbyte1;
// return mid-state of encryption:
// a must be hashed with Salsa and the result xored with b
a = xor(&a2, &b4);
b = xor(&a, m_);
}
}
fn threeBytesToInt(b_: &[u8]) -> u32 {
return b_[2] as u32 + b_[1] as u32 * 256 + b_[0] as u32 * 65536;
}
// xors 16-byte slices
fn xor(a_: &[u8], b_: &[u8]) -> [u8; 16] {
let mut c: [u8; 16] = [0; 16];
for i in 0..16 {
c[i] = a_[i] ^ b_[i];
}
return c;
}
// xors arbitrary-sized slices
fn xorSlices(a_: &[u8], b_: &[u8]) -> Vec<u8> {
let mut c: Vec<u8> = vec![0; a_.len()];
for i in 0..a_.len() {
c[i] = a_[i] ^ b_[i];
}
return c;
}
#[no_mangle]
//#[wasm_bindgen]
pub fn parseGatesBlob() {
unsafe {
gates = vec![Gate::default(); gatesCount as usize];
for i in 0..gatesCount as usize {
let blob: Vec<u8> = gatesBuf[i * 10..(i + 1) * 10].to_vec();
let gate: Gate = Gate {
op: (blob[0]),
in1: (threeBytesToInt(&blob[1..4])),
in2: (threeBytesToInt(&blob[4..7])),
out: (threeBytesToInt(&blob[7..10])),
};
gates[i] = gate;
}
}
}
#[no_mangle]
//#[wasm_bindgen]
pub fn initAll(
gatesCount_: u32,
wiresCount_: u32,
notaryInputSize_: u32,
clientInputSize_: u32,
outputSize_: u32,
andGateCount_: u32,
) -> u32 {
unsafe {
gatesCount = gatesCount_;
wiresCount = wiresCount_;
notaryInputSize = notaryInputSize_;
clientInputSize = clientInputSize_;
outputSize = outputSize_;
andGateCount = andGateCount_;
// memory needs to be allocated for:
let mut total = 0;
// IPC buffer 1 of 20 bytes
total += 20;
// IPC buffer 2 of 48 bytes (garbler only)
total += 48;
// gates blob
total += gatesCount * 10;
// random blob (used by garbler only)
total += (notaryInputSize + clientInputSize + 1) * 16;
// decoding table
total += (outputSize+7)/8;
// output
total += (outputSize+7)/8;
// truth tables
total += andGateCount * 48;
// input lables
total += (notaryInputSize + clientInputSize) * 32;
m = malloc(total as usize) as u32;
// o is memory offset
let mut o = m;
ipc1 = from_raw_parts_mut(o as *mut u8, 20);
o += 20;
ipc2 = from_raw_parts_mut(o as *mut u8, 48);
o += 48;
gatesBuf = from_raw_parts(o as *mut u8, (gatesCount * 10)as usize);
o += gatesCount * 10;
let randSize = (notaryInputSize + clientInputSize + 1) * 16;
randBuf = from_raw_parts(o as *mut u8, randSize as usize);
o += randSize;
decoding_table = from_raw_parts(o as *mut u8, ((outputSize+7)/8) as usize);
o += (outputSize+7)/8;
output = from_raw_parts_mut(o as *mut u8, ((outputSize+7)/8) as usize);
o += (outputSize+7)/8;
tt = from_raw_parts_mut(o as *mut u8, (andGateCount * 48) as usize);
o += andGateCount * 48;
il = from_raw_parts_mut(o as *mut u8, ((notaryInputSize + clientInputSize)*32) as usize);
ga = vec![[[0; 16]; 2]; wiresCount as usize];
ea = vec![[0; 16]; wiresCount as usize];
return m;
}
}
// allocates memory
fn malloc(size: usize) -> *mut u8 {
let mut buf: Vec<u8> = vec![0; size];
let ptr = buf.as_mut_ptr();
// take ownership of the memory block and
// ensure that its destructor is not
// called when the object goes out of scope
// at the end of the function
std::mem::forget(buf);
return ptr;
}

View File

@@ -451,7 +451,7 @@ export function expandRange(min, max){
}
// split Array or Uint8Array into an array array of chunks
// split Array or Uint8Array into an array of chunks
export function splitIntoChunks(ba, chunkSize) {
assert(ba instanceof Uint8Array);
assert(ba.length % chunkSize === 0);