mirror of
https://github.com/tlsnotary/PageSigner.git
synced 2026-01-08 22:27:57 -05:00
- use Salsa20 instead of tweetnacl-js's secretbox for a 30% garbling speedup in the browser.
- implement garbled row reduction GRR3 for 25% bandwidth saving - implement KOS15 OT extension - linting
This commit is contained in:
@@ -4,12 +4,11 @@
|
||||
<body>
|
||||
<script src="core/third-party/simple-js-ec-math.js"></script>
|
||||
<script src="core/third-party/sodium.js"></script> <!--dont put this any lower in the list or youll get require() not defined -->
|
||||
<script src="core/third-party/nacl-fast.js"></script>
|
||||
<script src="core/third-party/fastsha256.js"></script>
|
||||
<script src="core/third-party/cbor.js"></script>
|
||||
<script src="core/third-party/cose.js"></script>
|
||||
<script src="core/twopc/circuits/casmbundle.js"></script>
|
||||
<script type="module" src="core/Main.js"></script>
|
||||
<!-- <script type="module" src="core/test.js"></script> -->
|
||||
<script type="module" src="core/internal.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,5 @@
|
||||
/* global chrome, CASM*/
|
||||
|
||||
import {wait} from './utils.js';
|
||||
|
||||
// class FakeFS imitates node.js's fs.readFileSync() by
|
||||
@@ -43,7 +45,7 @@ class FakeFS{
|
||||
// All future invocations of Pagesigner use these serialized cached circuits.
|
||||
export class FirstTimeSetup{
|
||||
async start(pm){
|
||||
const url = chrome.extension.getURL('core/twopc/webWorkers/serializeCircuits.js')
|
||||
const url = chrome.extension.getURL('core/twopc/webWorkers/serializeCircuits.js');
|
||||
const worker = new Worker(url);
|
||||
console.log('Parsing circuits. This is done only once on first launch and will take ~30 secs');
|
||||
console.time('time_to_parse');
|
||||
|
||||
24
core/Main.js
24
core/Main.js
@@ -1,10 +1,13 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
/* eslint-disable no-case-declarations */
|
||||
import {parse_certs, verifyChain, getCommonName, getAltNames} from './verifychain.js';
|
||||
import {ba2str, b64decode, concatTA, int2ba, sha256, b64encode, verifySig, assert,
|
||||
ba2int, xor, eq, wait, AESECBencrypt, wildcardTest, pubkeyPEM2raw, import_resource} from './utils.js';
|
||||
import {getPref, getSessionBlob, getSession, getAllSessions, saveNewSession, init_db,
|
||||
addNewPreference, setPref, renameSession, deleteSession} from './indexeddb.js';
|
||||
/* global chrome, browser */
|
||||
|
||||
import {parse_certs, verifyChain, getCommonName, getAltNames} from
|
||||
'./verifychain.js';
|
||||
import {ba2str, b64decode, concatTA, int2ba, sha256, b64encode, verifySig,
|
||||
assert, ba2int, xor, eq, wait, AESECBencrypt, wildcardTest, pubkeyPEM2raw,
|
||||
import_resource} from './utils.js';
|
||||
import {getPref, getSessionBlob, getSession, getAllSessions, saveNewSession,
|
||||
init_db, addNewPreference, setPref, renameSession, deleteSession}
|
||||
from './indexeddb.js';
|
||||
import {globals} from './globals.js';
|
||||
import {Socket} from './Socket.js';
|
||||
import {TLS, getExpandedKeys, decrypt_tls_responseV6} from './TLS.js';
|
||||
@@ -145,6 +148,7 @@ export class Main{
|
||||
mode: 'no-cors'
|
||||
});
|
||||
const out = await Promise.race([fProm, wait(5000)])
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
.catch(err => {
|
||||
// fetch got 404; do nothing, just prevent exception from propagating
|
||||
});
|
||||
@@ -249,6 +253,7 @@ export class Main{
|
||||
// opened BEFORE we initiated the opening of "tab"
|
||||
// we abort if tab doesn't open after 5 secs.
|
||||
checkIfTabOpened(tab, property, openTabs){
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
openTabs = openTabs || [];
|
||||
let isTimeoutTriggered = false;
|
||||
setTimeout(function(){
|
||||
@@ -534,9 +539,8 @@ export class Main{
|
||||
this.importPgsgAndShow(new Uint8Array(data.args.data));
|
||||
break;
|
||||
case 'export':
|
||||
const pgsg = await this.getPGSG(data.args.dir);
|
||||
const value = await getSession(data.args.dir);
|
||||
this.sendToManager({'pgsg': JSON.stringify(pgsg), 'name': value.sessionName}, 'export');
|
||||
this.sendToManager({'pgsg': JSON.stringify(await this.getPGSG(data.args.dir)),
|
||||
'name': await getSession(data.args.dir).sessionName}, 'export');
|
||||
break;
|
||||
case 'notarize':
|
||||
this.prepareNotarization(false);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* global chrome */
|
||||
|
||||
// class ProgressMonitor receives progress information about client's garbling,
|
||||
// evaluation, blob upload, blob download. It dispatches progress status messages
|
||||
// periodically or when queried.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* 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
|
||||
@@ -251,9 +253,3 @@ export class Socket {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (typeof module !== 'undefined'){ // we are in node.js environment
|
||||
module.exports={
|
||||
};
|
||||
}
|
||||
95
core/TLS.js
95
core/TLS.js
@@ -1,3 +1,5 @@
|
||||
/* global SocketNode */
|
||||
|
||||
import {concatTA, int2ba, sha256, str2ba, assert, ba2int, getRandom, sigDER2p1363,
|
||||
pubkeyPEM2raw, eq, xor, AESECBencrypt, b64urlencode} from './utils.js';
|
||||
import {verifyChain, checkCertSubjName} from './verifychain.js';
|
||||
@@ -5,33 +7,6 @@ import {Socket} from './Socket.js';
|
||||
|
||||
|
||||
export class TLS {
|
||||
// allHandshakes is a concatenation of all handshake messages up to this point.
|
||||
// This is only data visible at the handshake layer and does not include record layer headers
|
||||
allHandshakes;
|
||||
// certPath is an array of certificates from the server arranged by pkijs in the ascending
|
||||
// order from leaf to root
|
||||
certPath;
|
||||
// cke is TLS handshake's Client Key Exchange message
|
||||
cke;
|
||||
clientRandom;
|
||||
commPrivkey;
|
||||
commSymmetricKey;
|
||||
headers;
|
||||
isMhm; // multiple handshake messages
|
||||
mustVerifyCert;
|
||||
notaryWillEncryptRequest;
|
||||
options;
|
||||
port;
|
||||
rsaSig;
|
||||
serverRandom;
|
||||
sckt;
|
||||
serverEcPubkey;
|
||||
secret; // for debug purposes only
|
||||
// sid is id for this notarization session
|
||||
sid;
|
||||
serverName;
|
||||
useMaxFragmentLength;
|
||||
|
||||
constructor (serverName, port, headers, options){
|
||||
this.serverName = serverName;
|
||||
this.port = port;
|
||||
@@ -41,6 +16,34 @@ export class TLS {
|
||||
this.useMaxFragmentLength = options.useMaxFragmentLength;
|
||||
this.notaryWillEncryptRequest = options.notaryWillEncryptRequest;
|
||||
this.mustVerifyCert = options.mustVerifyCert;
|
||||
// allHandshakes is a concatenation of all handshake messages up to this point.
|
||||
// This is only data visible at the handshake layer and does not include record layer headers
|
||||
this.allHandshakes;
|
||||
// certPath is an array of certificates from the server arranged by pkijs in the ascending
|
||||
// order from leaf to root
|
||||
this.certPath;
|
||||
// cke is TLS handshake's Client Key Exchange message
|
||||
this.cke;
|
||||
this.clientRandom;
|
||||
this.commPrivkey;
|
||||
this.commSymmetricKey;
|
||||
this.headers;
|
||||
this.isMhm; // multiple handshake messages
|
||||
this.mustVerifyCert;
|
||||
this.notaryWillEncryptRequest;
|
||||
this.options;
|
||||
this.port;
|
||||
this.rsaSig;
|
||||
this.serverRandom;
|
||||
this.sckt;
|
||||
this.serverEcPubkey;
|
||||
this.secret; // for debug purposes only
|
||||
// sid is id for this notarization session
|
||||
this.sid;
|
||||
this.serverName;
|
||||
this.useMaxFragmentLength;
|
||||
|
||||
|
||||
if (typeof(window) !== 'undefined'){
|
||||
this.sckt = new Socket(serverName, port);
|
||||
} else {
|
||||
@@ -130,43 +133,6 @@ export class TLS {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
async parse_commpk_commpksig_cpk(dataEnc){
|
||||
// Notary's EC pubkey (to derive ECDH secret for communication) is not encrypted
|
||||
const comm_pk = dataEnc.slice(0,65);
|
||||
// for debug purposes only
|
||||
this.secret = dataEnc.slice(65,97);
|
||||
|
||||
this.commSymmetricKey = await getECDHSecret(comm_pk, this.commPrivkey);
|
||||
const data = await this.decryptFromNotary(
|
||||
this.commSymmetricKey,
|
||||
dataEnc.slice(65+this.secret.length));
|
||||
|
||||
let o = 0; // offset
|
||||
// get signature over communication pubkey
|
||||
const ssrvLen = ba2int(data.slice(o,o+=1));
|
||||
const signingServerRetval = data.slice(o, o+=ssrvLen);
|
||||
const cpk = data.slice(o,o+=65); // Client's pubkey for ECDH
|
||||
// parse signing server's return value
|
||||
o = 0;
|
||||
const sessionSigLen = ba2int(signingServerRetval.slice(o, o+=1));
|
||||
const sessionSig = signingServerRetval.slice(o, o+=sessionSigLen);
|
||||
const ephemKeySigLen = ba2int(signingServerRetval.slice(o, o+=1));
|
||||
const ephemKeySig = signingServerRetval.slice(o, o+=ephemKeySigLen);
|
||||
const ephemPubKey = signingServerRetval.slice(o, o+=65);
|
||||
const ephemValidFrom = signingServerRetval.slice(o, o+=4);
|
||||
const ephemValidUntil = signingServerRetval.slice(o, o+=4);
|
||||
|
||||
// check signature
|
||||
const to_be_signed = await sha256(comm_pk);
|
||||
assert(await this.verifyNotarySig(sessionSig, ephemPubKey, to_be_signed, 'raw') == true);
|
||||
// the ephemeral key with its validity time range is signed by master key
|
||||
const ephemTBS = await sha256(concatTA(ephemPubKey, ephemValidFrom, ephemValidUntil));
|
||||
assert(await this.verifyNotarySig(ephemKeySig, this.notary.pubkeyPEM, ephemTBS) == true);
|
||||
this.checkEphemKeyExpiration(ephemValidFrom, ephemValidUntil);
|
||||
return cpk;
|
||||
}
|
||||
|
||||
parseServerHello(s){
|
||||
let p = 0;
|
||||
assert(eq(s.slice(p, p+=1), [0x02])); // Server Hello
|
||||
@@ -392,6 +358,7 @@ export class TLS {
|
||||
return this.rsaSig;
|
||||
}
|
||||
|
||||
// TODO this description is incorrect
|
||||
// sendClientFinished accepts encrypted Client Finished (CF), auth tag for CF, verify_data for CF.
|
||||
// It then sends Client Key Exchange, Change Cipher Spec and encrypted Client Finished
|
||||
async sendClientFinished(encCF, tagCF){
|
||||
|
||||
@@ -6,31 +6,25 @@ import {concatTA, sha256, assert, xor, str2ba} from './utils.js';
|
||||
// class TLSNotarySession impements one notarization session
|
||||
// (one client request followed by one server response) using the TLSNotary protocol.
|
||||
export class TLSNotarySession{
|
||||
constructor(server, port, request, notary, sessionOptions, circuits, progressMonitor){
|
||||
// twopc is an instance of class TWOPC. Used to speak to the notary.
|
||||
twopc;
|
||||
this.twopc = new TWOPC(notary, request.length, circuits, progressMonitor);
|
||||
// tls is an instance of class TLS. Used to speak to the webserver.
|
||||
tls;
|
||||
this.tls = new TLS(server, port, request, sessionOptions);
|
||||
// probeTLS is used to probe the webserver to see if it supports TLSNotary,
|
||||
// before we start any time-intensive 2PC
|
||||
probeTLS;
|
||||
options;
|
||||
request;
|
||||
notary;
|
||||
pm;
|
||||
constructor(server, port, request, notary, sessionOptions, circuits, progressMonitor){
|
||||
this.twopc = new TWOPC(notary, request.length, circuits, progressMonitor);
|
||||
this.tls = new TLS(server, port, request, sessionOptions);
|
||||
this.probeTLS = new TLS(server, port, request, sessionOptions);
|
||||
this.request = str2ba(request);
|
||||
this.notary = notary;
|
||||
this.pm = progressMonitor;
|
||||
this.options = null;
|
||||
}
|
||||
|
||||
async start(){
|
||||
await this.probeTLS.buildAndSendClientHello();
|
||||
await this.probeTLS.receiveAndParseServerHello();
|
||||
await this.probeTLS.sckt.close();
|
||||
await this.twopc.preCompute();
|
||||
await this.twopc.init();
|
||||
await this.tls.buildAndSendClientHello();
|
||||
const serverEcPubkey = await this.tls.receiveAndParseServerHello();
|
||||
const serverX = serverEcPubkey.slice(1, 33);
|
||||
|
||||
@@ -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: '3.236.244.77',
|
||||
defaultNotaryIP: '127.0.0.1',
|
||||
//defaultNotaryIP: '3.236.244.77',
|
||||
defaultNotaryPort: 10011,
|
||||
// backupUrl is the URL to query to get the IP address of another notary
|
||||
// server in case if defaultNotaryIP is unreachable
|
||||
@@ -26,5 +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: false
|
||||
useNotaryNoSandbox: true
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
let db;
|
||||
let db_blobs;
|
||||
|
||||
|
||||
18
core/internal.js
Normal file
18
core/internal.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// allows to access PageSigner's internal classes by exposing them to the
|
||||
// extension's window global
|
||||
|
||||
import * as utils from './utils.js';
|
||||
import * as indexeddb from './indexeddb.js';
|
||||
import * as globals from './globals.js';
|
||||
import * as Main from './Main.js';
|
||||
import {OTSender} from './twopc/OTSender.js';
|
||||
import {OTReceiver} from './twopc/OTReceiver.js';
|
||||
|
||||
|
||||
window.PageSigner = {
|
||||
Main:Main,
|
||||
globals:globals,
|
||||
utils:utils,
|
||||
indexeddb:indexeddb,
|
||||
OTSender:OTSender,
|
||||
OTReceiver:OTReceiver};
|
||||
@@ -339,11 +339,3 @@ export async function verifyNotary(URLFetcherDoc) {
|
||||
console.log('oracle verification successfully finished');
|
||||
return pubkeyPEM;
|
||||
}
|
||||
|
||||
|
||||
if (typeof module !== 'undefined'){ // we are in node.js environment
|
||||
module.exports={
|
||||
check_oracle: verify_oracle,
|
||||
oracle,
|
||||
};
|
||||
}
|
||||
2391
core/third-party/nacl-fast.js
vendored
2391
core/third-party/nacl-fast.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -4,18 +4,6 @@ export class Evaluator{
|
||||
this.s = parent;
|
||||
}
|
||||
|
||||
getNonFixedLabels(encLabels, otKeys, nonFixedBits){
|
||||
const nonFixedLabels = [];
|
||||
for (let i = 0; i < nonFixedBits.length; i += 1) {
|
||||
const bit = nonFixedBits[i];
|
||||
const ct = encLabels.slice(i * 32, (i+1) * 32);
|
||||
const inputLabel = this.s.ot.decryptWithKey(ct, bit, otKeys[i]);
|
||||
nonFixedLabels.push(inputLabel);
|
||||
}
|
||||
return nonFixedLabels;
|
||||
}
|
||||
|
||||
|
||||
async evaluateBatch(batch, cNo){
|
||||
const outputs = await this.s.workers[cNo].evaluateBatch(batch);
|
||||
const parsed = [];
|
||||
@@ -24,5 +12,4 @@ export class Evaluator{
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
/* global chrome */
|
||||
|
||||
import WorkerPool from './WorkerPool.js';
|
||||
|
||||
export class GCWorker extends WorkerPool{
|
||||
|
||||
446
core/twopc/GHASH.js
Normal file
446
core/twopc/GHASH.js
Normal file
@@ -0,0 +1,446 @@
|
||||
import {sortKeys, assert, bytesToBits, concatTA, int2ba, xor, ba2int,
|
||||
splitIntoChunks} from '../utils.js';
|
||||
|
||||
|
||||
// class GHASH implements a method of computing the AES-GCM's GHASH function
|
||||
// in a secure two-party computation (2PC) setting using 1-of-2 Oblivious
|
||||
// Transfer (OT). The parties start with their secret shares of H (GHASH key) end at
|
||||
// the end each gets their share of the GHASH output.
|
||||
// The method is decribed here:
|
||||
// (https://tlsnotary.org/how_it_works#section4).
|
||||
|
||||
|
||||
export class GHASH {
|
||||
constructor(noOfAESBlocks){
|
||||
// otR is an instance of OTReceiver
|
||||
this.otR = null;
|
||||
// shares is a sparse array of shares of the powers of H. H is also
|
||||
// known as GHASH key - it is an all-zero bytestring AES-ECB-encrypted
|
||||
// with TLS session's client_write_key. Array's key is the power n,
|
||||
// Array's value is the value of the client's xor-share of H^n.
|
||||
this.shares = [];
|
||||
// maxPowerNeeded is the maximum power of H that we'll need in order to
|
||||
// compute the GHASH function. It is the number of AES blocks +
|
||||
// aad (1 block) + suffix (1 block)
|
||||
this.maxPowerNeeded = noOfAESBlocks + 2;
|
||||
// lastChoiceBits is the choice bits in client's most recent OT request.
|
||||
this.lastChoiceBits = null;
|
||||
// lastStrategy contains one of the two strategies which was used when
|
||||
// preparing the most recent OT request. The same strategy will be used
|
||||
// when processing an OT response.
|
||||
this.lastStrategy = null;
|
||||
// res contains an intermediate result during Block Aggregation. We
|
||||
// save it here before making an OT request and pick it up after
|
||||
// receiving a response.
|
||||
this.res = new Uint8Array(16).fill(0);
|
||||
|
||||
// strategy1&2 shows what existing shares we will be multiplying (values
|
||||
// 0 and 1) to obtain other odd shares (key).
|
||||
// Max sequential odd share that we can obtain on first round of
|
||||
// communication is 19 (we already have 1) shares of H^1, H^2, H^3 from
|
||||
// the Client Finished message and 2) squares of those 3 shares).
|
||||
// Note that "sequential" is a keyword here. We can't obtain 21 but we
|
||||
// indeed can obtain 25==24+1, 33==32+1 etc. However with 21 missing,
|
||||
// even if we have 25,33,etc, there will be a gap and we will not be able
|
||||
// to obtain all the needed shares by Block Aggregation.
|
||||
|
||||
// We request OT for each share in each pair of the strategy, i.e. for
|
||||
// shares: 4,1,4,3,8,1, etc. Even though it would be possible to introduce
|
||||
// optimizations in order to avoid requesting OT for the same share more
|
||||
// than once, that would only save us ~2000 OT instances at the cost of
|
||||
// complicating the code.
|
||||
|
||||
this.strategy1 = {
|
||||
5: [4, 1],
|
||||
7: [4, 3],
|
||||
9: [8, 1],
|
||||
11: [8, 3],
|
||||
13: [12, 1],
|
||||
15: [12, 3],
|
||||
17: [16, 1],
|
||||
19: [16, 3]};
|
||||
this.strategy2 = {
|
||||
21: [17, 4],
|
||||
23: [17, 6],
|
||||
25: [17, 8],
|
||||
27: [19, 8],
|
||||
29: [17, 12],
|
||||
31: [19, 12],
|
||||
33: [17, 16],
|
||||
35: [19, 16]};
|
||||
|
||||
// maxOddPowerNeeded is the maximum odd share that we need (it is a key
|
||||
// from one of the strategies)
|
||||
this.maxOddPowerNeeded = this.findMaxOddPower(this.maxPowerNeeded);
|
||||
// otCount is how many instances of OT the client (who is the OT receiver)
|
||||
// will have to execute. The OT count for Client_Finished is 256 and for
|
||||
// Server_Finished it is also 256.
|
||||
this.otCount = 256 + 256 + this.calculateOTCount();
|
||||
}
|
||||
|
||||
// sets the OTReceiver instance
|
||||
setOTReceiver(otR){
|
||||
this.otR = otR;
|
||||
}
|
||||
|
||||
// return true if we need another communication roundtrip with the notary
|
||||
isStep2Needed(){
|
||||
return this.maxOddPowerNeeded > 19;
|
||||
}
|
||||
|
||||
// findMaxOddPower finds the max odd share that we
|
||||
findMaxOddPower(maxPowerNeeded){
|
||||
assert(maxPowerNeeded <= 1026);
|
||||
|
||||
// maxHTable's <value> shows how many GHASH blocks can be processed
|
||||
// with Block Aggregation if we have all the sequential shares
|
||||
// starting with 1 up to and including <key>.
|
||||
// e.g. {5:29} means that if we have shares of H^1,H^2,H^3,H^4,H^5,
|
||||
// then we can process 29 GHASH blocks.
|
||||
// max TLS record size of 16KB requires 1026 GHASH blocks
|
||||
const maxHTable = {0: 0, 3: 19, 5: 29, 7: 71, 9: 89, 11: 107, 13: 125, 15: 271, 17: 305, 19: 339, 21: 373,
|
||||
23: 407, 25: 441, 27: 475, 29: 509, 31: 1023, 33: 1025, 35: 1027};
|
||||
|
||||
let maxOddPowerNeeded = null;
|
||||
for (const key of sortKeys(maxHTable)){
|
||||
const maxH = maxHTable[key];
|
||||
if (maxH >= maxPowerNeeded){
|
||||
maxOddPowerNeeded = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return maxOddPowerNeeded;
|
||||
}
|
||||
|
||||
// calculateOTCount calculates the amount of OTs the OT receiver
|
||||
// will have to execute when sending the request (Client/Server Finished
|
||||
// are not included)
|
||||
calculateOTCount(){
|
||||
// z is a dummy value which we set to indicate that we have the share
|
||||
const z = new Uint8Array(16).fill(0);
|
||||
// all shares of powers which the client has
|
||||
// add powers 1,2,3 which the client will already have
|
||||
const allShares = [undefined, z, z, z];
|
||||
// powerCount contains powers from strategies. Each power requires
|
||||
// 128 OTs.
|
||||
let powerCount = 0;
|
||||
const strategy = {...this.strategy1, ...this.strategy2};
|
||||
for (const k of sortKeys(strategy)){
|
||||
if (k > this.maxOddPowerNeeded){
|
||||
break;
|
||||
}
|
||||
allShares[k] = z;
|
||||
powerCount += 2;
|
||||
}
|
||||
this.freeSquare(allShares, this.maxPowerNeeded);
|
||||
|
||||
// how many auxillary shares needed for Block Aggregation. Each aux
|
||||
// share requires 128 OTs for the share itself and 128 OTs for
|
||||
// its aggregated value.
|
||||
const auxShares = [];
|
||||
for (let i=1; i <= this.maxPowerNeeded; i++){
|
||||
if (allShares[i] != undefined){
|
||||
continue; // the client already has this share
|
||||
}
|
||||
// a is the smaller power
|
||||
const [a, b] = this.findSum(allShares, i);
|
||||
if (! auxShares.includes(a)){
|
||||
auxShares.push(a);
|
||||
}
|
||||
}
|
||||
return powerCount * 128 + auxShares.length * 256;
|
||||
}
|
||||
|
||||
// prepareOTRequest prepares a request for Notary's masked XTables
|
||||
// corresponding to our shares in the strategies.
|
||||
async prepareOTRequest(strategyNo){
|
||||
assert(this.lastChoiceBits == null);
|
||||
this.freeSquare(this.shares, this.maxPowerNeeded);
|
||||
const strategy = strategyNo == 1 ? this.strategy1 : this.strategy2;
|
||||
const inputBits1Arr = [];
|
||||
const keys = Object.keys(strategy);
|
||||
for (const k of keys){
|
||||
if (k > this.maxOddPowerNeeded){
|
||||
break;
|
||||
}
|
||||
const v = strategy[k];
|
||||
inputBits1Arr.push(bytesToBits(this.shares[v[0]]).reverse());
|
||||
inputBits1Arr.push(bytesToBits(this.shares[v[1]]).reverse());
|
||||
}
|
||||
this.lastChoiceBits = [].concat(...inputBits1Arr);
|
||||
this.lastStrategy = strategy;
|
||||
return this.otR.createRequest(this.lastChoiceBits);
|
||||
}
|
||||
|
||||
processOTResponse(otResp){
|
||||
assert(this.lastChoiceBits != null);
|
||||
const hisXTables = this.otR.parseResponse(this.lastChoiceBits, otResp);
|
||||
this.getPowerShares(hisXTables, this.shares);
|
||||
this.freeSquare(this.shares, this.maxPowerNeeded);
|
||||
this.lastChoiceBits = null;
|
||||
this.lastStrategy = null;
|
||||
}
|
||||
|
||||
// buildStep1 starts the process of computing our share of the tag for the
|
||||
// client request. We already have shares of H^1, H^2, H^3 from the tag for
|
||||
// the Client_Finished earlier
|
||||
async buildStep1(){
|
||||
console.log('maxPowerNeeded is', this.maxPowerNeeded);
|
||||
assert(this.maxPowerNeeded <= 1026);
|
||||
if (this.maxOddPowerNeeded === 3){
|
||||
this.freeSquare(this.shares, this.maxPowerNeeded);
|
||||
return int2ba(this.maxPowerNeeded, 2);
|
||||
}
|
||||
return concatTA(int2ba(this.maxPowerNeeded, 2), await this.prepareOTRequest(1));
|
||||
}
|
||||
|
||||
// Step2 is needed when the amount of odd powers needed is > 19
|
||||
async buildStep2(){
|
||||
return await this.prepareOTRequest(2);
|
||||
}
|
||||
|
||||
// Step3 performs Block Aggregation for GHASH inputs
|
||||
async buildStep3(ghashInputs){
|
||||
this.res = this.multiplyShares(ghashInputs);
|
||||
return await this.blockAggregationBuildRequest(ghashInputs);
|
||||
}
|
||||
|
||||
// Add notary's masked XTables to our tag share
|
||||
processStep3Response(resp){
|
||||
assert(this.lastChoiceBits != null);
|
||||
let o = 0;
|
||||
const otResp = resp.slice(o, o += this.lastChoiceBits.length * 32);
|
||||
const hisTagShare = resp.slice(o, o += 16);
|
||||
assert(resp.length === o);
|
||||
const res = this.blockAggregationProcessResponse(otResp);
|
||||
return xor(res, hisTagShare);
|
||||
}
|
||||
|
||||
// buildFinRequest builds an OT request to compute the tag of either
|
||||
// Client Finished (CF) or Server Finished (SF) message
|
||||
// Note that you must use separate instances of the GHASH class: one for
|
||||
// CF and one for SF
|
||||
buildFinRequest(H1){
|
||||
assert(this.lastChoiceBits == null);
|
||||
const H2 = blockMult(H1, H1);
|
||||
const h1Bits = bytesToBits(H1).reverse();
|
||||
const h2Bits = bytesToBits(H2).reverse();
|
||||
this.shares[1] = H1;
|
||||
this.shares[2] = H2;
|
||||
this.lastChoiceBits = [].concat(h1Bits, h2Bits);
|
||||
return this.otR.createRequest(this.lastChoiceBits);
|
||||
}
|
||||
|
||||
// processFinResponse processes notary's OT response and multiplies
|
||||
// each share of H with the corresponding GHASH block
|
||||
processFinResponse(otResp, ghashInputs){
|
||||
assert(this.lastChoiceBits != null);
|
||||
const twoXTables = this.otR.parseResponse(this.lastChoiceBits, otResp);
|
||||
let H3 = new Uint8Array(16).fill(0);
|
||||
// we multiply our H1 share with his H2's masked XTable and
|
||||
// we multiply our H2 share with his H1's masked XTable
|
||||
const items = splitIntoChunks(twoXTables, 16);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
H3 = xor(H3, items[i]);
|
||||
}
|
||||
H3 = xor(H3, blockMult(this.shares[1], this.shares[2]));
|
||||
this.shares[3] = H3;
|
||||
const s1 = blockMult(ghashInputs[0], this.shares[3]);
|
||||
const s2 = blockMult(ghashInputs[1], this.shares[2]);
|
||||
const s3 = blockMult(ghashInputs[2], this.shares[1]);
|
||||
this.lastChoiceBits = null;
|
||||
return xor(xor(s1, s2), s3);
|
||||
}
|
||||
|
||||
// findSum finds summands from "powers" which add up to "sumNeeded"
|
||||
// those powers which we don't have are undefined
|
||||
findSum(powers, sumNeeded){
|
||||
for (let i=1; i < powers.length; i++){
|
||||
if (powers[i] == undefined){
|
||||
continue;
|
||||
}
|
||||
for (let j=1; j < powers.length; j++){
|
||||
if (powers[j] == undefined){
|
||||
continue;
|
||||
}
|
||||
if (i+j === sumNeeded){
|
||||
return [i, j];
|
||||
}
|
||||
}
|
||||
}
|
||||
// this should never happen because we always call
|
||||
// findSum() knowing that the sum can be found
|
||||
throw('sum not found');
|
||||
}
|
||||
|
||||
// Perform squaring of each odd share up to maxPower. It is "free" because
|
||||
// it is done locally without OT.
|
||||
// "powers" is a sparse array where idx is the power (or undefined if not set) and
|
||||
// item at that idx is client's share of H^power. Modifies "powers" in-place,
|
||||
// e.g if "powers" contains 1,2,3 and maxPower==19, then upon return "powers"
|
||||
// will contain 1,2,3,4,6,8,12,16
|
||||
freeSquare(powers, maxPower){
|
||||
for (let i=0; i < powers.length; i++){
|
||||
if (powers[i] == undefined || i % 2 == 0){
|
||||
continue;
|
||||
}
|
||||
if (i > maxPower){
|
||||
return;
|
||||
}
|
||||
let power = i;
|
||||
while (power <= maxPower){
|
||||
power = power * 2;
|
||||
if (powers.includes(power)){
|
||||
continue;
|
||||
}
|
||||
powers[power] = blockMult(powers[power/2], powers[power/2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// multiply H shares with corresponding ciphertext blocks
|
||||
// for all H shares which we do not have we wll use the Block Aggregation method
|
||||
multiplyShares(ghashInputs){
|
||||
let res = new Uint8Array(16).fill(0);
|
||||
// this.powersOfH is a sparse array, its .length is the max index
|
||||
for (let i=1; i < this.shares.length; i++){
|
||||
if (i > ghashInputs.length){
|
||||
// we will have more powers than the input blocks
|
||||
break;
|
||||
}
|
||||
if (this.shares[i] == undefined){
|
||||
continue;
|
||||
}
|
||||
const x = ghashInputs[ghashInputs.length-i];
|
||||
res = xor(res, blockMult(this.shares[i], x));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// for those shares of powers which are not in powersOfH, we do not compute the shares of
|
||||
// powers, but instead we compute the share of (X_n*H), where X_n is a ciphertext block
|
||||
|
||||
// e.g. if we need H^21*X_1 and we have shares of H^19 and H^2, we compute it as follows:
|
||||
// (H19_a + H19_b)*(H2_a + H2_b)*X_1 == H19_a*H2_a*X_1 + H19_a*X_1*H2_b + H19_b*X_1*H2_a +
|
||||
// H19_b*H2_b*X_1
|
||||
// only the 2 middle cross-terms need to be computed using OT
|
||||
// A will receive OT for H19_a*X_1 from B
|
||||
// B will receive OT for H19_b*X_1 from A
|
||||
// All other powers where one of the factors is H^2 can be "collapsed" into (i.e xored with)
|
||||
// the above two cross-terms, eg if parties have shares of H^23 and need to compute
|
||||
// H^25 == H^23*H^2, then H23_a*X_2 can be collapsed into H19_a*X_1 and
|
||||
// H23_b*X_2 can be collapsed into H19_b*X_1
|
||||
// Thus we would not need any extra OT to compute shares of H^25
|
||||
|
||||
async blockAggregationBuildRequest(ghashInputs){
|
||||
assert(this.lastChoiceBits == null);
|
||||
// aggregated object's keys are shares and values are the aggregated
|
||||
// value of shares * GHASH block
|
||||
let aggregated = {};
|
||||
// this.powersOfH is a sparse array. It contains: the powers 1,2,3 + odd powers that we
|
||||
// computed earlier + squares of all those powers
|
||||
for (let i=4; i < this.shares.length; i++){
|
||||
if (i > ghashInputs.length){
|
||||
// we stop iterating shares of powers of H
|
||||
break;
|
||||
}
|
||||
if (this.shares[i] != undefined){
|
||||
// we already multiplied the block with this share in multiplyShares()
|
||||
continue;
|
||||
}
|
||||
// found a share which does not exist in our sparse array,
|
||||
// we need X*H for this missing power
|
||||
// a is the smaller power, b is the bigger power
|
||||
const [a, b] = this.findSum(this.shares, i);
|
||||
const x = ghashInputs[ghashInputs.length-i];
|
||||
this.res = xor(this.res, blockMult(blockMult(this.shares[a], this.shares[b]), x));
|
||||
if (aggregated[a] == undefined){
|
||||
aggregated[a] = new Uint8Array(16).fill(0);
|
||||
}
|
||||
aggregated[a] = xor(aggregated[a], blockMult(this.shares[b], x));
|
||||
}
|
||||
|
||||
// request Notary's masked X-table for each Notary's small power a.
|
||||
// We send bits of our aggregated values
|
||||
// and also
|
||||
// request Notary's X-table for each Notary's aggregated value. We send bits
|
||||
// of our small power
|
||||
const sortedKeys = sortKeys(aggregated);
|
||||
let allBits = [];
|
||||
for (const key of sortedKeys){
|
||||
// OT starts with the highest bit because ours is the y value of block multiplication
|
||||
allBits = [].concat(allBits, bytesToBits(aggregated[key]).reverse());
|
||||
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);
|
||||
}
|
||||
|
||||
// add NOtary's XTable to our share of the tag
|
||||
blockAggregationProcessResponse(otResp){
|
||||
assert(this.lastChoiceBits != null);
|
||||
const XTableEntries = this.otR.parseResponse(this.lastChoiceBits, otResp);
|
||||
const items = splitIntoChunks(XTableEntries, 16);
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
this.res = xor(this.res, items[i]);
|
||||
}
|
||||
this.lastChoiceBits = null;
|
||||
return this.res;
|
||||
}
|
||||
|
||||
// Compute shares of powers listed in strategies
|
||||
// modifies PowersOfH in-place
|
||||
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);
|
||||
for (let j=0; j < stratKeys.length; j++){
|
||||
const oddPower = stratKeys[j];
|
||||
if (oddPower > this.maxOddPowerNeeded){
|
||||
assert(chunks.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);
|
||||
for (let i=0; i < 256; i++){
|
||||
xorSum = xor(xorSum, subChunks[i]);
|
||||
}
|
||||
const Cx = powersOfH[this.lastStrategy[oddPower][0]];
|
||||
const Cy = powersOfH[this.lastStrategy[oddPower][1]];
|
||||
const CxCy = blockMult(Cx, Cy);
|
||||
powersOfH[oddPower] = xor(xorSum, CxCy);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// perform GCM Galois Field block multiplication
|
||||
// x_,y_ are Uint8Arrays
|
||||
export function blockMult(x_, y_){
|
||||
// casting to BigInt just in case if ba2int returns a Number
|
||||
let x = BigInt(ba2int(x_));
|
||||
const y = BigInt(ba2int(y_));
|
||||
let res = 0n;
|
||||
for (let i=127n; i >= 0n; i--){
|
||||
res ^= x * ((y >> i) & 1n);
|
||||
x = (x >> 1n) ^ ((x & 1n) * BigInt(0xE1000000000000000000000000000000));
|
||||
}
|
||||
return int2ba(res, 16);
|
||||
}
|
||||
|
||||
// return a table of 128 x values needed for block multiplication
|
||||
// x is Uint8Array
|
||||
// Not in use because the Client is the OT receiver, he supplies
|
||||
// the bits of y. The Notary supplies the XTable.
|
||||
export function getXTable(x_){
|
||||
let x = ba2int(x_);
|
||||
const table = [];
|
||||
for (let i=0; i < 128; i++){
|
||||
table[i] = int2ba(x, 16);
|
||||
x = (x >> 1n) ^ ((x & 1n) * BigInt(0xE1000000000000000000000000000000));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
import {concatTA, assert, ba2int, expandRange} from './../utils.js';
|
||||
import {concatTA, assert} from './../utils.js';
|
||||
|
||||
export class Garbler{
|
||||
// class Garbler implements the role of the client as the garbler
|
||||
s;
|
||||
garbledC;
|
||||
|
||||
export class Garbler{
|
||||
constructor(parent){
|
||||
// this.s is the TWOPC class
|
||||
this.s = parent;
|
||||
@@ -13,64 +10,9 @@ export class Garbler{
|
||||
this.garbledC = [];
|
||||
}
|
||||
|
||||
async getAllB(allB){
|
||||
let fixedCount = 0;
|
||||
let nonFixedCount = 0;
|
||||
|
||||
for (let i = 1; i < this.s.cs.length; i++) {
|
||||
if (i !== 6){
|
||||
fixedCount += this.s.cs[i].notaryFixedInputSize;
|
||||
}
|
||||
else {
|
||||
fixedCount += 160 + this.s.C6Count * 128;
|
||||
}
|
||||
console.log('fixed count for ', i, ' is ', fixedCount);
|
||||
nonFixedCount += this.s.cs[i].notaryNonFixedInputSize;
|
||||
}
|
||||
|
||||
console.log('in getAllB fixedCount, nonFixedCount', fixedCount, nonFixedCount);
|
||||
const OT0PoolSize = Math.ceil(nonFixedCount/2 * 1.2);
|
||||
const OT1PoolSize = Math.ceil(nonFixedCount/2 * 1.2);
|
||||
|
||||
// 4000 OT for OT-ghash
|
||||
const expectedLen = (OT0PoolSize + OT1PoolSize + fixedCount + 6000)*32;
|
||||
assert(allB.length === expectedLen);
|
||||
|
||||
const fixedBlob = allB.slice(0, fixedCount*32);
|
||||
const nonFixedPoolBlob = allB.slice(fixedCount*32);
|
||||
|
||||
const encLabels = [];
|
||||
let fixedBlobIdx = 0;
|
||||
for (let j = 1; j < this.s.cs.length; j++) {
|
||||
const c = this.s.cs[j];
|
||||
let inputCount = c.notaryFixedInputSize;
|
||||
if (j === 6){
|
||||
inputCount = 160 + this.s.C6Count * 128;
|
||||
}
|
||||
assert(inputCount*32 === this.garbledC[j].il.notaryFixed.length);
|
||||
for (let i = 0; i < inputCount; i++) {
|
||||
const m0 = this.garbledC[j].il.notaryFixed.slice(i*32, i*32+16);
|
||||
const m1 = this.garbledC[j].il.notaryFixed.slice(i*32+16, i*32+32);
|
||||
const B = fixedBlob.slice(fixedBlobIdx*32, fixedBlobIdx*32+32);
|
||||
encLabels.push(this.s.ot.encrypt(m0, m1, B));
|
||||
fixedBlobIdx++;
|
||||
}
|
||||
}
|
||||
assert(fixedBlobIdx*32 === fixedBlob.length);
|
||||
|
||||
const arrOfB = [];
|
||||
for ( let i = 0; i < nonFixedPoolBlob.length/32; i++) {
|
||||
arrOfB[i] = nonFixedPoolBlob.slice(i*32, i*32+32);
|
||||
}
|
||||
await this.s.ot.prepareEncryptionKeys(arrOfB);
|
||||
return concatTA(...encLabels);
|
||||
}
|
||||
|
||||
async garbleAll(){
|
||||
// first garble circuit 5 once, so that future invocations can reuse labels
|
||||
const rv5 = await this.s.workers[5].garble();
|
||||
|
||||
// garble the rest of the circuits asyncronously
|
||||
// we don't parallelize garbling of non-c5 circuits, we garble them
|
||||
// one by one
|
||||
const allPromises = [];
|
||||
for (let cNo = 1; cNo < this.s.cs.length; cNo++){
|
||||
if (cNo === 5){
|
||||
@@ -81,102 +23,75 @@ export class Garbler{
|
||||
allPromises.push(worker.garble());
|
||||
}
|
||||
|
||||
// reuse labels of c5 and garble in batches
|
||||
const allTt = [new Uint8Array(rv5.tt)];
|
||||
const allOl = [new Uint8Array(rv5.ol)];
|
||||
const allIl = this.separateLabels(new Uint8Array(rv5.il), 5);
|
||||
|
||||
const outputs = await this.s.workers[5].garbleBatch(this.s.C5Count-1, {
|
||||
reuseLabels: concatTA(allIl.notaryFixed, allIl.clientNonFixed),
|
||||
reuseIndexes: expandRange(0, 320),
|
||||
reuseR: new Uint8Array(rv5.R)
|
||||
});
|
||||
|
||||
for (let i=0; i < this.s.C5Count-1; i++){
|
||||
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 out = outputs[i];
|
||||
allTt.push(new Uint8Array(out.tt));
|
||||
allOl.push(new Uint8Array(out.ol));
|
||||
const labels = this.separateLabels(new Uint8Array(out.il), 5);
|
||||
allIl.clientFixed = concatTA(allIl.clientFixed, labels.clientFixed);
|
||||
allIl.push(new Uint8Array(out.il));
|
||||
}
|
||||
this.garbledC[5] = {
|
||||
tt: concatTA(...allTt),
|
||||
ol: concatTA(...allOl),
|
||||
il: allIl};
|
||||
il: concatTA(...allIl)};
|
||||
|
||||
// all the other circuits have been garbled by now
|
||||
// all non-c5 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){
|
||||
// c5 already dealt with
|
||||
continue;
|
||||
continue; // c5 already dealt with
|
||||
}
|
||||
this.garbledC[cNo] = {
|
||||
tt: new Uint8Array(rv.tt),
|
||||
ol: new Uint8Array(rv.ol),
|
||||
il: this.separateLabels(new Uint8Array(rv.il), cNo)};
|
||||
il: new Uint8Array(rv.il)};
|
||||
}
|
||||
}
|
||||
|
||||
getNonFixedEncLabels(idxBlob, cNo){
|
||||
const nfis = this.s.cs[cNo].notaryNonFixedInputSize;
|
||||
assert(nfis*2 === idxBlob.length);
|
||||
|
||||
const encLabels = [];
|
||||
console.time('encryptWithKeyAtIndex');
|
||||
for (let i=0; i < nfis; i++){
|
||||
const idx = ba2int(idxBlob.subarray(i*2, i*2+2));
|
||||
const m0 = this.garbledC[cNo].il.notaryNonFixed.subarray(i*32, i*32+16);
|
||||
const m1 = this.garbledC[cNo].il.notaryNonFixed.subarray(i*32+16, i*32+32);
|
||||
const encr = this.s.ot.encryptWithKeyAtIndex(m0, m1, idx);
|
||||
encLabels.push(encr);
|
||||
// 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 il = this.garbledC[cNo].il;
|
||||
const c = this.s.cs[cNo];
|
||||
const ilArray = [];
|
||||
// chunkSize is the bytesize of input labels for one circuit
|
||||
const chunkSize = (c.notaryInputSize+c.clientInputSize)*32;
|
||||
assert(chunkSize*exeCount == il.length);
|
||||
for (let i=0; i < exeCount; i++){
|
||||
ilArray.push(il.slice(i*chunkSize, i*chunkSize+c.notaryInputSize*32));
|
||||
}
|
||||
console.timeEnd('encryptWithKeyAtIndex');
|
||||
console.log('encryptWithKeyAtIndex for count:', nfis);
|
||||
|
||||
return concatTA(...encLabels);
|
||||
return concatTA(...ilArray);
|
||||
}
|
||||
|
||||
getClientLabels(nonFixedBits, cNo){
|
||||
const fixedBits = this.s.cs[cNo].fixedInputs;
|
||||
const clientInputs = [].concat(nonFixedBits, fixedBits);
|
||||
const inputLabels = [];
|
||||
const clientLabelBlob = concatTA(
|
||||
this.garbledC[cNo].il.clientNonFixed,
|
||||
this.garbledC[cNo].il.clientFixed);
|
||||
// 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 il = this.garbledC[cNo].il;
|
||||
const c = this.s.cs[cNo];
|
||||
const ilArray = [];
|
||||
// chunkSize is the bytesize of input labels for one circuit
|
||||
const chunkSize = (c.notaryInputSize+c.clientInputSize)*32;
|
||||
assert(chunkSize*repeatCount == il.length);
|
||||
for (let i=0; i < repeatCount; i++){
|
||||
ilArray.push(il.slice(i*chunkSize+c.notaryInputSize*32, (i+1)*chunkSize));
|
||||
}
|
||||
const clientLabelBlob = concatTA(...ilArray);
|
||||
assert(clientInputs.length*32 == clientLabelBlob.length);
|
||||
|
||||
const out = [];
|
||||
for (let i=0; i < clientInputs.length; i++){
|
||||
const bit = clientInputs[i];
|
||||
const label = clientLabelBlob.subarray(i*32+bit*16, i*32+bit*16+16);
|
||||
inputLabels.push(label);
|
||||
out.push(label);
|
||||
}
|
||||
return concatTA(...inputLabels);
|
||||
}
|
||||
|
||||
// separate one continuous blob of input labels into 4 blobs as in Labels struct
|
||||
separateLabels(blob, cNo) {
|
||||
const c = this.s.cs[cNo];
|
||||
if (blob.length != (c.notaryInputSize+c.clientInputSize)*32) {
|
||||
throw('in separateLabels');
|
||||
}
|
||||
const labels = {};
|
||||
let offset = 0;
|
||||
labels['notaryNonFixed'] = blob.slice(offset, offset+c.notaryNonFixedInputSize*32);
|
||||
offset += c.notaryNonFixedInputSize * 32;
|
||||
|
||||
labels['notaryFixed'] = blob.slice(offset, offset+c.notaryFixedInputSize*32);
|
||||
offset += c.notaryFixedInputSize * 32;
|
||||
|
||||
labels['clientNonFixed'] = blob.slice(offset, offset+c.clientNonFixedInputSize*32);
|
||||
offset += c.clientNonFixedInputSize * 32;
|
||||
|
||||
labels['clientFixed'] = blob.slice(offset, offset+c.clientFixedInputSize*32);
|
||||
offset += c.clientFixedInputSize * 32;
|
||||
assert(offset === blob.length);
|
||||
return labels;
|
||||
return concatTA(...out);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
201
core/twopc/OT.js
201
core/twopc/OT.js
@@ -1,201 +0,0 @@
|
||||
import {OTWorker} from './OTWorker.js';
|
||||
import {concatTA, int2ba, assert, encrypt_generic, decrypt_generic} from './../utils.js';
|
||||
|
||||
export class OT{
|
||||
// class OT implements oblivious transfer protocol based on
|
||||
// Chou-Orlandi "Simplest OT"
|
||||
// as much pre-computation as possiblle is done in the offline phase
|
||||
|
||||
constructor(){
|
||||
this.decryptionKeys = [];
|
||||
this.notaryA = null; // A value of notary the sender
|
||||
this.worker = new OTWorker(4);
|
||||
|
||||
// pools are used to pre-compute in the offline phase the decryption key
|
||||
// for a receiver's bit in the oblivious transfer
|
||||
this.poolOf0 = []; // item format {k_R:<value>, B:<value>, idx:<>}
|
||||
this.poolOf1 = []; //
|
||||
|
||||
// OT for the sender
|
||||
this.a = sodium.crypto_core_ristretto255_scalar_random();
|
||||
this.A = sodium.crypto_scalarmult_ristretto255_base(this.a);
|
||||
this.encryptionKeys = [];
|
||||
}
|
||||
|
||||
setA(A){
|
||||
this.notaryA = A;
|
||||
}
|
||||
|
||||
getSenderA(){
|
||||
return this.A;
|
||||
}
|
||||
|
||||
// for each bit in bits pre-compute B and k_R (per [1])
|
||||
saveDecryptionKeysOld(bits){
|
||||
const receiverBs = [];
|
||||
// we will reuse the same B to save time
|
||||
// during release REUSE IS NOT ALLOWED - IT BREAKS SECURITY
|
||||
const b0 = sodium.crypto_core_ristretto255_scalar_random();
|
||||
const B0 = sodium.crypto_scalarmult_ristretto255_base(b0);
|
||||
const k0 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(b0, this.notaryA));
|
||||
|
||||
const b1 = sodium.crypto_core_ristretto255_scalar_random();
|
||||
const gb1 = sodium.crypto_scalarmult_ristretto255_base(b1);
|
||||
const B1 = sodium.crypto_core_ristretto255_add(this.notaryA, gb1);
|
||||
const k1 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(b1, this.notaryA));
|
||||
|
||||
for (let i=0; i < bits.length; i++){
|
||||
const bit = bits[i];
|
||||
this.decryptionKeys.push(bit === 0 ? k0 : k1);
|
||||
receiverBs.push(bit === 0 ? B0 : B1);
|
||||
}
|
||||
console.log('saveDecryptionKeys for count:', bits.length);
|
||||
return receiverBs;
|
||||
}
|
||||
|
||||
async saveDecryptionKeys(bits){
|
||||
const receiverBs = [];
|
||||
const entries = await this.worker.saveDecryptionKeys(bits, this.notaryA);
|
||||
for (const e of entries){
|
||||
this.decryptionKeys.push(e[0]);
|
||||
receiverBs.push(e[1]);
|
||||
}
|
||||
return receiverBs;
|
||||
}
|
||||
|
||||
|
||||
// decrypts 1-of-2 ciphertexts for a bit "bit" with a key "key" at index "idx"
|
||||
// returns the plaintext
|
||||
decryptWithKeyFromIndex(ciphertext, bit, idx){
|
||||
assert(ciphertext.length == 32);
|
||||
assert(bit === 0 || bit === 1);
|
||||
assert(this.decryptionKeys[idx] != undefined);
|
||||
const encMessage = ciphertext.slice(16*bit, 16*bit+16);
|
||||
return decrypt_generic(encMessage, this.decryptionKeys[idx], 0);
|
||||
}
|
||||
|
||||
// decrypts 1-of-2 ciphertexts for a bit "bit" with a key from obj.k_R
|
||||
decryptWithKey(ciphertext, bit, key){
|
||||
assert(ciphertext.length === 32 && key.length === 16);
|
||||
assert(bit === 0 || bit === 1);
|
||||
const encMessage = ciphertext.slice(16*bit, 16*bit+16);
|
||||
return decrypt_generic(encMessage, key, 0);
|
||||
}
|
||||
|
||||
// we prepare B and k_R for 120% of the bits. The extra 20% is needed because we don't
|
||||
// know in advance exactly how many 1s and 0s we'll need during the online phase
|
||||
precomputePoolOld(numBits){
|
||||
// we will reuse the same B to save time
|
||||
// during release REUSE IS NOT ALLOWED - IT BREAKS SECURITY
|
||||
const b0 = sodium.crypto_core_ristretto255_scalar_random();
|
||||
const B0 = sodium.crypto_scalarmult_ristretto255_base(b0);
|
||||
const k0 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(b0, this.notaryA));
|
||||
|
||||
const b1 = sodium.crypto_core_ristretto255_scalar_random();
|
||||
const gb1 = sodium.crypto_scalarmult_ristretto255_base(b1);
|
||||
const B1 = sodium.crypto_core_ristretto255_add(this.notaryA, gb1);
|
||||
const k1 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(b1, this.notaryA));
|
||||
|
||||
console.time('precomputePool');
|
||||
for (let i = 0; i < Math.ceil(numBits/2 * 1.2) ; i++){
|
||||
this.poolOf0.push({k_R:k0, B:B0});
|
||||
}
|
||||
for (let i = 0; i < Math.ceil(numBits/2 * 1.2); i++){
|
||||
this.poolOf1.push({k_R:k1, B:B1});
|
||||
}
|
||||
console.timeEnd('precomputePool');
|
||||
console.log('total bits', numBits);
|
||||
}
|
||||
|
||||
// we prepare B and k_R for 120% of the bits. The extra 20% is needed because we don't
|
||||
// know in advance exactly how many 1s and 0s we'll need during the online phase
|
||||
async precomputePool(numBits){
|
||||
const count = Math.ceil(numBits/2 * 1.2); // we need "count" zeroes and "count" ones
|
||||
const entries = await this.worker.precomputePool(count, this.notaryA);
|
||||
assert(entries.length === count*2);
|
||||
for (const e of entries.slice(0, count)){
|
||||
this.poolOf0.push({k_R:e[0], B:e[1]});
|
||||
}
|
||||
for (const e of entries.slice(count, count*2)){
|
||||
this.poolOf1.push({k_R:e[0], B:e[1]});
|
||||
}
|
||||
}
|
||||
|
||||
// given an array of bits, return the index for each bit in the pool
|
||||
// and decryption keys for OT
|
||||
getIndexesFromPool(bits){
|
||||
const idxArray = [];
|
||||
const otKeys = [];
|
||||
for (let i=0; i < bits.length; i++){
|
||||
const otbit = this.getFromPool(bits[i]);
|
||||
idxArray.push(int2ba(otbit.idx, 2));
|
||||
otKeys.push(otbit.k_R);
|
||||
}
|
||||
return [concatTA(...idxArray), otKeys];
|
||||
}
|
||||
|
||||
|
||||
// return an array of B values from poolOf0 and poolOf1 in random sequence
|
||||
// and remember each B's index in that sequence.
|
||||
getRandomizedPool(){
|
||||
function getRandomInt(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
const randomizedB = [];
|
||||
const fullPool = [].concat(this.poolOf0, this.poolOf1);
|
||||
const origLen = fullPool.length;
|
||||
for (let i=0; i < origLen; i++){
|
||||
const randIdx = getRandomInt(0, fullPool.length-1);
|
||||
const ot = fullPool.splice(randIdx, 1)[0];
|
||||
// modifying ot will be reflected in this.poolOf0/this.poolOf1 because of pass-by-reference
|
||||
ot.idx = i;
|
||||
randomizedB.push(ot.B);
|
||||
}
|
||||
return randomizedB;
|
||||
}
|
||||
|
||||
|
||||
// gets either 0 or 1 from pool
|
||||
getFromPool(bit){
|
||||
assert(bit === 0 || bit === 1);
|
||||
const pool = (bit === 0) ? this.poolOf0 : this.poolOf1;
|
||||
const item = pool.pop();
|
||||
assert(this.poolOf0.length > 0 && this.poolOf0.length > 0);
|
||||
return item;
|
||||
}
|
||||
|
||||
// given the receiver's B, encrypt m0 and m1
|
||||
// we don't parallelize this function because the amount of encryptions is small
|
||||
encrypt(m0, m1, B){
|
||||
const k0 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(this.a, B));
|
||||
const sub = sodium.crypto_core_ristretto255_sub(B, this.A);
|
||||
const k1 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(this.a, sub));
|
||||
const e0 = encrypt_generic(m0, k0, 0);
|
||||
const e1 = encrypt_generic(m1, k1, 0);
|
||||
return concatTA(e0, e1);
|
||||
}
|
||||
|
||||
encryptWithKeyAtIndex(m0, m1, idx){
|
||||
const k0 = this.encryptionKeys[idx][0];
|
||||
const k1 = this.encryptionKeys[idx][1];
|
||||
const e0 = encrypt_generic(m0, k0, 0);
|
||||
const e1 = encrypt_generic(m1, k1, 0);
|
||||
return concatTA(e0, e1);
|
||||
}
|
||||
|
||||
// (client as the sender) for each B in arrOfB save an encryption keypair [k0,k1]
|
||||
async prepareEncryptionKeys(arrOfB){
|
||||
console.time('prepareEncryptionKeys');
|
||||
const entries = await this.worker.prepareEncryptionKeys(arrOfB, this.a, this.A);
|
||||
assert(entries.length === arrOfB.length);
|
||||
for (const e of entries){
|
||||
this.encryptionKeys.push([e[0], e[1]]);
|
||||
}
|
||||
console.timeEnd('prepareEncryptionKeys');
|
||||
console.log('prepareEncryptionKeys for count:', arrOfB.length);
|
||||
}
|
||||
}
|
||||
|
||||
169
core/twopc/OTCommon.js
Normal file
169
core/twopc/OTCommon.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import {assert, getRandom, bytesToBits, concatTA, int2ba, xor, ba2int,
|
||||
bitsToBytes, splitIntoChunks, AESCTRencrypt, Salsa20} from '../utils.js';
|
||||
|
||||
// methods used by both OTSender and OTReceiver classes
|
||||
export default class OTCommon{
|
||||
// extend r (Uint8Array) into a matrix of 128 columns where depending on r's bit, each row
|
||||
// is either all 0s or all 1s
|
||||
extend_r(r){
|
||||
// 128 bits all set to 1
|
||||
const all_1 = new Uint8Array(16).fill(255);
|
||||
// 128 bits all set to 0
|
||||
const all_0 = new Uint8Array(16).fill(0);
|
||||
const matrix = [];
|
||||
const bits = bytesToBits(r).reverse();
|
||||
for (const bit of bits){
|
||||
matrix.push(bit == 0 ? all_0 : all_1);
|
||||
}
|
||||
return matrix;
|
||||
}
|
||||
|
||||
|
||||
// given a matrix, output 2 xor shares of it
|
||||
secretShareMatrix(matrix){
|
||||
const T0 = [];
|
||||
const T1 = [];
|
||||
for (let i=0; i < matrix.length; i++){
|
||||
const rand = getRandom(16);
|
||||
T0.push(rand);
|
||||
T1.push(xor(matrix[i], rand));
|
||||
}
|
||||
return [T0, T1];
|
||||
}
|
||||
|
||||
|
||||
// transpose a matrix of bits. matrix is an array of rows (each row is a Uin8Array)
|
||||
transposeMatrix(matrix){
|
||||
const colCount = matrix[0].length*8;
|
||||
const rowCount = matrix.length;
|
||||
assert(colCount == 128 || rowCount == 128);
|
||||
const newRows = [];
|
||||
for (let j=0; j < colCount; j++){
|
||||
// in which byte of the column is j located
|
||||
const byteNo = j >> 3; //Math.floor(j / 8);
|
||||
// what is the index of j inside the byte
|
||||
const bitIdx = j % 8;
|
||||
const newRowBits = [];
|
||||
for (let i=0; i < rowCount; i++){
|
||||
newRowBits.push((matrix[i][byteNo] >> (7-bitIdx)) & 1);
|
||||
}
|
||||
newRows.push(bitsToBytes(newRowBits.reverse()));
|
||||
}
|
||||
return newRows;
|
||||
}
|
||||
|
||||
|
||||
// pseudorandomly expands a 16-byte seed into a bytestring of bytesize "count"*16
|
||||
// to benefit from AES-NI, we use browser WebCrypto's AES-CTR: with seed as the key
|
||||
// we encrypt an all-zero bytestring.
|
||||
async expandSeed(seed, count){
|
||||
assert(seed.length == 16);
|
||||
return await AESCTRencrypt(seed, new Uint8Array(count*16).fill(0));
|
||||
}
|
||||
|
||||
|
||||
// encrypt each 16-byte chunk of msg with a fixed-key Salsa20
|
||||
async fixedKeyCipher(msg){
|
||||
assert(msg.length % 16 == 0);
|
||||
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, 29, 30, 31, 32]);
|
||||
const encryptedArr = [];
|
||||
const chunks = splitIntoChunks(msg, 16);
|
||||
for (const chunk of chunks){
|
||||
encryptedArr.push(Salsa20(fixedKey, chunk));
|
||||
}
|
||||
return concatTA(...encryptedArr);
|
||||
}
|
||||
|
||||
|
||||
// to break the correlation, KOS15 needs a hash function which has tweakable correlation
|
||||
// robustness (tcr). GKWY20 shows (Section 7.4) how to achieve tcr using a fixed-key cipher C
|
||||
// instead of a hash, i.e. instead of Hash(x, i) we can do C(C(x) xor i) xor C(x)
|
||||
async breakCorrelation(rows){
|
||||
assert(rows[0].length == 16);
|
||||
const AESx = await this.fixedKeyCipher(concatTA(...rows));
|
||||
const indexesArray = [];
|
||||
for (let i=0; i < rows.length; i++){
|
||||
indexesArray.push(int2ba(i, 16));
|
||||
}
|
||||
const indexes = concatTA(...indexesArray);
|
||||
return xor(await this.fixedKeyCipher(xor(AESx, indexes)), AESx);
|
||||
}
|
||||
|
||||
|
||||
// carry-less multiplication (i.e. multiplication in galois field) without reduction.
|
||||
// Let a's right-most bit have index 0. Then for every bit set in a, b is left-shifted
|
||||
// by the set bit's index value. All the left-shifted values are then XORed.
|
||||
// a and b are both UintArray's of 16 bytes. Returns UintArray's of 32 bytes
|
||||
|
||||
// this version is 25% faster than the naive implementation clmul128_unoptimized
|
||||
clmul128(a, b){
|
||||
const aBits = bytesToBits(a); // faster if turned to bits rather than shift each time
|
||||
const b_bi = ba2int(b);
|
||||
const shiftedB = [];
|
||||
// shift only 7 times and then use these 7 shifts to construct all other shifts
|
||||
for (let i=0n; i < 8n; i++){
|
||||
const tmp = new Uint8Array(32).fill(0);
|
||||
tmp.set(int2ba(b_bi << i, 17), 0);
|
||||
shiftedB.push(tmp);
|
||||
}
|
||||
const res = new Uint8Array(32).fill(0);
|
||||
for (let i = 0; i < 128; i++){
|
||||
if (aBits[i]){ // a's bit is set
|
||||
const byteNo = i >> 3; // this is faster than Math.floor(i / 8);
|
||||
const bitIdx = i % 8;
|
||||
const bLen = 17+byteNo;
|
||||
for (let i=0; i < bLen; i++){
|
||||
res[31-i] = res[31-i] ^ shiftedB[bitIdx][bLen-1-i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
// not in use, just for reference. This is the unoptimized version of clmul128
|
||||
clmul128_unoptimized(a, b){
|
||||
let res = 0n;
|
||||
const aBits = bytesToBits(a); // faster if turned to bits rather than shift each time
|
||||
const b_bi = ba2int(b);
|
||||
for (let i = 0n; i < 128n; i++){
|
||||
if (aBits[i]){ // a's bit is set
|
||||
res ^= (b_bi << i);
|
||||
}
|
||||
}
|
||||
return int2ba(res, 32);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// To test the protocol end-to-end, copy-paste this function into the extension's console
|
||||
async function testFullProtocol(){
|
||||
let otR = new PageSigner.OTReceiver(8);
|
||||
let otS = new PageSigner.OTSender(8);
|
||||
const [A, seedCommit] = await otR.setupStep1();
|
||||
const [allBs, senderSeedShare] = otS.setupStep1(A, seedCommit);
|
||||
const [encryptedColumns, receiverSeedShare, x, t] = await otR.setupStep2(allBs, senderSeedShare);
|
||||
await otS.setupStep2(encryptedColumns, receiverSeedShare, x, t);
|
||||
const requestBits = [0, 1, 1, 0];
|
||||
const otReq1 = otR.createRequest(requestBits);
|
||||
const senderMsg = new Uint8Array([].concat(
|
||||
Array(16).fill(0),
|
||||
Array(16).fill(1),
|
||||
Array(16).fill(2),
|
||||
Array(16).fill(3),
|
||||
Array(16).fill(4),
|
||||
Array(16).fill(5),
|
||||
Array(16).fill(6),
|
||||
Array(16).fill(7)));
|
||||
const otResp = otS.processRequest(otReq1, senderMsg);
|
||||
const decoded = otR.parseResponse(requestBits, otResp);
|
||||
console.log('the result is: ', decoded);
|
||||
const expected = new Uint8Array([].concat(
|
||||
Array(16).fill(0),
|
||||
Array(16).fill(3),
|
||||
Array(16).fill(5),
|
||||
Array(16).fill(6),
|
||||
));
|
||||
console.log('expected result is: ', expected);
|
||||
}
|
||||
153
core/twopc/OTReceiver.js
Normal file
153
core/twopc/OTReceiver.js
Normal file
@@ -0,0 +1,153 @@
|
||||
/* global sodium */
|
||||
|
||||
import {assert, getRandom, bytesToBits, concatTA, sha256, xor, int2ba,
|
||||
bitsToBytes, AESCTRencrypt} from '../utils.js';
|
||||
import OTCommon from './OTCommon.js';
|
||||
|
||||
// class OTReceiver implements the receiver of the 1-of-2 Oblivious Transfer.
|
||||
// run the full KOS15 protocol
|
||||
export class OTReceiver extends OTCommon{
|
||||
constructor(otCount){
|
||||
super(); //noop but JS requires it to be called
|
||||
this.otCount = otCount;
|
||||
this.extraOT = 256; // extended OT which will be sacrificed as part of KOS15 protocol
|
||||
this.totalOT = Math.ceil(otCount/8)*8 + this.extraOT;
|
||||
// seedShare is my xor share of a PRG seed
|
||||
this.seedShare = null;
|
||||
this.rbits = [];
|
||||
this.T0 = [];
|
||||
this.T1 = [];
|
||||
this.a = null;
|
||||
this.A = null;
|
||||
this.RT0 = [];
|
||||
// receivedSoFar counts how many bits of OT have been used up by the receiver
|
||||
this.receivedSoFar = 0;
|
||||
// expectingResponseSize is how many OTs the receiver expects to receive from the sender
|
||||
// at this point
|
||||
this.expectingResponseSize = 0;
|
||||
}
|
||||
|
||||
// run KOS15 to prepare Random OT. Step 1
|
||||
async setupStep1(){
|
||||
this.seedShare = getRandom(16);
|
||||
const seedCommit = await sha256(this.seedShare);
|
||||
const r = getRandom(this.totalOT/8);
|
||||
const R = this.extend_r(r);
|
||||
this.rbits = bytesToBits(r).reverse();
|
||||
[this.T0, this.T1] = this.secretShareMatrix(R);
|
||||
// for baseOT Bob is the sender, he chooses a and sends A
|
||||
this.a = sodium.crypto_core_ristretto255_scalar_random();
|
||||
this.A = sodium.crypto_scalarmult_ristretto255_base(this.a);
|
||||
return [this.A, seedCommit];
|
||||
}
|
||||
|
||||
// run KOS15 to prepare Random OT. Step 2
|
||||
async setupStep2(allBsBlob, senderSeedShare){
|
||||
// compute key_0 and key_1 for each B of the base OT
|
||||
assert(allBsBlob.length == 128*32);
|
||||
assert(senderSeedShare.length == 16);
|
||||
const encrKeys = [];
|
||||
for (let i=0; i < 128; i++){
|
||||
const B = allBsBlob.slice(i*32, (i+1)*32);
|
||||
const k0 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(this.a, B));
|
||||
const sub = sodium.crypto_core_ristretto255_sub(B, this.A);
|
||||
const k1 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(this.a, sub));
|
||||
encrKeys.push([k0, k1]);
|
||||
}
|
||||
// Use the i-th k0 to encrypt the i-th column in T0, likewise
|
||||
// use the i-th k1 to encrypt the i-th column in T1
|
||||
const T0columns = this.transposeMatrix(this.T0);
|
||||
const T1columns = this.transposeMatrix(this.T1);
|
||||
const encryptedColumns = [];
|
||||
for (let i=0; i < 128; i++){
|
||||
encryptedColumns.push(await AESCTRencrypt(encrKeys[i][0], T0columns[i]));
|
||||
encryptedColumns.push(await AESCTRencrypt(encrKeys[i][1], T1columns[i]));
|
||||
}
|
||||
|
||||
// KOS15 kicks in at this point to check if Receiver sent the correct columnsArray
|
||||
// combine seed shares and expand the seed
|
||||
const seed = xor(this.seedShare, senderSeedShare);
|
||||
const expandedSeed = await this.expandSeed(seed, this.totalOT);
|
||||
// Bob multiplies every 128-bit row of matrix T1 with the corresponding random
|
||||
// value in expandedSeed and XORs the products.
|
||||
// Bob multiplies every bit of r with the corresponding random
|
||||
// value in expandedSeed and XORs the products.
|
||||
// Bob sends seed,x,t to Alice
|
||||
let x = new Uint8Array(16).fill(0);
|
||||
let t = new Uint8Array(32).fill(0);
|
||||
for (let i=0; i < this.T0.length; i++){
|
||||
const rand = expandedSeed.subarray(i*16, (i+1)*16);
|
||||
if (this.rbits[i] == 1){
|
||||
x = xor(x, rand);
|
||||
}
|
||||
t = xor(t, this.clmul128(this.T0[i], rand));
|
||||
}
|
||||
|
||||
// we need to break correlations between Q0 and Q1
|
||||
// The last extraOTs were sacrificed as part of the KOS15 protocol
|
||||
// and so we don't need them anymore
|
||||
console.log('start breakCorrelation');
|
||||
this.RT0 = await this.breakCorrelation(this.T0.slice(0, -this.extraOT));
|
||||
console.log('end breakCorrelation');
|
||||
// also drop the unneeded bytes and bits of r
|
||||
this.rbits = this.rbits.slice(0, -this.extraOT);
|
||||
|
||||
|
||||
// now we have instances of Random OT where depending on r's bit,
|
||||
// each row in RT0 equals to a row either in RQ0 or RQ1
|
||||
console.log('done 2');
|
||||
// use Beaver Derandomization [Beaver91] to convert randomOT into standardOT
|
||||
return [concatTA(...encryptedColumns), this.seedShare, x, t];
|
||||
}
|
||||
|
||||
// Steps 5, 6 and 7 are repeated for every batch of OT
|
||||
|
||||
|
||||
// createRequest takes OT receiver's choice bits and instructs the
|
||||
// OT sender which random masks need to be flipped (Beaver Derandomization)
|
||||
createRequest(choiceBits){
|
||||
assert(this.receivedSoFar + choiceBits.length <= this.otCount, 'No more OTs left.');
|
||||
assert(this.expectingResponseSize == 0, 'The previous request must be processed before requesting more OTs.');
|
||||
// for Beaver Derandomization, tell the Sender which masks to flip: 0 means
|
||||
// no flip needed, 1 means a flip is needed
|
||||
const bitsToFlip = [];
|
||||
for (let i=0; i < choiceBits.length; i++){
|
||||
bitsToFlip.push(choiceBits[i] ^ this.rbits[this.receivedSoFar+i]);
|
||||
}
|
||||
// pad the bitcount to a multiple of 8
|
||||
let padCount = 0;
|
||||
if (choiceBits.length % 8 > 0){
|
||||
padCount = 8 - choiceBits.length % 8;
|
||||
}
|
||||
for (let i=0; i < padCount; i++){
|
||||
bitsToFlip.push(0);
|
||||
}
|
||||
|
||||
this.expectingResponseSize = choiceBits.length;
|
||||
// prefix with the amount of bits that Sender needs to drop
|
||||
// in cases when bitsArr.length is not a multiple of 8
|
||||
return concatTA(int2ba(padCount, 1), bitsToBytes(bitsToFlip));
|
||||
}
|
||||
|
||||
// parseResponse unmasks the OT sender's masked values based on the choice
|
||||
// bit and the random mask of the OT receiver
|
||||
parseResponse(choiceBits, maskedOT){
|
||||
assert(this.expectingResponseSize == choiceBits.length);
|
||||
assert(this.expectingResponseSize*32 == maskedOT.length);
|
||||
const decodedArr = [];
|
||||
for (let i=0; i < choiceBits.length; i++){
|
||||
const mask = this.RT0.slice((this.receivedSoFar+i)*16, (this.receivedSoFar+i)*16+16);
|
||||
const m0 = maskedOT.slice(i*32, i*32+16);
|
||||
const m1 = maskedOT.slice(i*32+16, i*32+32);
|
||||
if (choiceBits[i] == 0){
|
||||
decodedArr.push(xor(m0, mask));
|
||||
} else {
|
||||
decodedArr.push(xor(m1, mask));
|
||||
}
|
||||
}
|
||||
this.receivedSoFar += choiceBits.length;
|
||||
this.expectingResponseSize = 0;
|
||||
console.log('this.receivedSoFar', this.receivedSoFar);
|
||||
return concatTA(...decodedArr);
|
||||
}
|
||||
}
|
||||
136
core/twopc/OTSender.js
Normal file
136
core/twopc/OTSender.js
Normal file
@@ -0,0 +1,136 @@
|
||||
/* global sodium */
|
||||
|
||||
import {assert, getRandom, bytesToBits, concatTA, xor, eq, AESCTRdecrypt,
|
||||
sha256, ba2int} from '../utils.js';
|
||||
import OTCommon from './OTCommon.js';
|
||||
|
||||
|
||||
// class OTSender implements the sender of the Oblivious Transfer acc.to.
|
||||
// the KOS15 protocol
|
||||
export class OTSender extends OTCommon{
|
||||
constructor(otCount){
|
||||
super(); //noop but JS requires it to be called
|
||||
this.otCount = otCount;
|
||||
this.extraOT = 256; // extended OT which will be sacrificed as part of KOS15 protocol
|
||||
this.totalOT = Math.ceil(otCount/8)*8 + this.extraOT;
|
||||
this.S = null;
|
||||
this.sBits = [];
|
||||
this.decrKeys = [];
|
||||
this.RQ0 = [];
|
||||
this.RQ1 = [];
|
||||
// sentSoFar is how many OTs the sender has already sent
|
||||
this.sentSoFar = 0;
|
||||
// hisCommit is Receiver's commit to the PRG seed
|
||||
this.hisCommit = null;
|
||||
// seedShare is my xor share of a PRG seed
|
||||
this.seedShare = null;
|
||||
}
|
||||
|
||||
// part of KOS15
|
||||
setupStep1(A, hisCommit){
|
||||
this.hisCommit = hisCommit;
|
||||
this.seedShare = getRandom(16);
|
||||
// Alice computes her Bs and decryption keys based on each bit in S
|
||||
this.S = getRandom(16);
|
||||
this.sBits = bytesToBits(this.S).reverse();
|
||||
const allBs = [];
|
||||
this.decrKeys = [];
|
||||
for (const bit of this.sBits){
|
||||
const b = sodium.crypto_core_ristretto255_scalar_random();
|
||||
let B = sodium.crypto_scalarmult_ristretto255_base(b);
|
||||
if (bit == 1){
|
||||
B = sodium.crypto_core_ristretto255_add(A, B);
|
||||
}
|
||||
const k = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(b, A));
|
||||
this.decrKeys.push(k);
|
||||
allBs.push(B);
|
||||
}
|
||||
return [concatTA(...allBs), this.seedShare];
|
||||
}
|
||||
|
||||
|
||||
// part of KOS15
|
||||
async setupStep2(encryptedColumnsBlob, receiverSeedShare, x, t){
|
||||
assert(receiverSeedShare.length == 16);
|
||||
assert(encryptedColumnsBlob.length % 256 == 0);
|
||||
const encryptedColumns = [];
|
||||
const columnSize = encryptedColumnsBlob.length/256;
|
||||
for (let i=0; i < 256; i++){
|
||||
encryptedColumns.push(encryptedColumnsBlob.slice(i*columnSize, (i+1)*columnSize));
|
||||
}
|
||||
// Decrypt only those columns which correspond to S's bit
|
||||
const columns = [];
|
||||
for (let i=0; i < 128; i++){
|
||||
const col0 = encryptedColumns[i*2];
|
||||
const col1 = encryptedColumns[i*2+1];
|
||||
if (this.sBits[i] == 0){
|
||||
columns.push(await AESCTRdecrypt(this.decrKeys[i], col0));
|
||||
} else {
|
||||
columns.push(await AESCTRdecrypt(this.decrKeys[i], col1));
|
||||
}
|
||||
}
|
||||
const Q0 = this.transposeMatrix(columns);
|
||||
|
||||
// KOS15: Alice multiplies every 128-bit row of matrix Q1 with the corresponding random
|
||||
// value in expandedSeed and XORs the products
|
||||
assert(eq(await sha256(receiverSeedShare), this.hisCommit), 'Bad seed commit');
|
||||
const seed = xor(receiverSeedShare, this.seedShare);
|
||||
const expandedSeed = await this.expandSeed(seed, this.totalOT);
|
||||
let q = new Uint8Array(32).fill(0);
|
||||
for (let i=0; i < Q0.length; i++){
|
||||
const rand = expandedSeed.subarray(i*16, (i+1)*16);
|
||||
q = xor(q, this.clmul128(Q0[i], rand));
|
||||
}
|
||||
// Alice checks that t = q xor x * S
|
||||
assert(eq(t, xor(q, this.clmul128(x, this.S))));
|
||||
|
||||
// Alice xors each row of Q0 with S to get Q1
|
||||
const Q1 = [];
|
||||
for (let i=0; i < Q0.length; i++){
|
||||
Q1.push(xor(Q0[i], this.S));
|
||||
}
|
||||
|
||||
// we need to break correlations between Q0 and Q1
|
||||
// The last extraOTs were sacrificed as part of the KOS15 protocol
|
||||
// and so we don't need them anymore
|
||||
console.log('start breakCorrelation');
|
||||
this.RQ0 = await this.breakCorrelation(Q0.slice(0, -this.extraOT));
|
||||
this.RQ1 = await this.breakCorrelation(Q1.slice(0, -this.extraOT));
|
||||
console.log('end breakCorrelation');
|
||||
// now we have instances of Random OT where depending on r's bit,
|
||||
// each row in RT0 equals to a row either in RQ0 or RQ1
|
||||
console.log('done 2');
|
||||
// in Steps 5,6,7 we will use Beaver Derandomization to convert
|
||||
// randomOT into standardOT
|
||||
}
|
||||
|
||||
|
||||
// processRequest performs Beaver Derandomization:
|
||||
// for every bit in bitsToFlip, the Sender has two 16-byte messages for 1-of-2 OT and
|
||||
// two random masks (from the KOS15 protocol) r0 and r1
|
||||
// if the bit is 0, the Sender sends (m0 xor r0) and (m1 xor r1),
|
||||
// if the bit is 1, the Sender sends (m0 xor r1) and (m1 xor r0)
|
||||
processRequest(bitsBlob, messages){
|
||||
const dropCount = ba2int(bitsBlob.slice(0, 1));
|
||||
const bitsToFlipWithRem = bytesToBits(bitsBlob.slice(1));
|
||||
const bitsToFlip = bitsToFlipWithRem.slice(0, bitsToFlipWithRem.length-dropCount);
|
||||
assert(this.sentSoFar + bitsToFlip.length <= this.otCount);
|
||||
assert(bitsToFlip.length*32 == messages.length);
|
||||
const encodedToSend = [];
|
||||
for (let i=0; i < bitsToFlip.length; i++){
|
||||
const m0 = messages.slice(i*32, i*32+16);
|
||||
const m1 = messages.slice(i*32+16, i*32+32);
|
||||
const r0 = this.RQ0.slice((this.sentSoFar+i)*16, (this.sentSoFar+i)*16+16);
|
||||
const r1 = this.RQ1.slice((this.sentSoFar+i)*16, (this.sentSoFar+i)*16+16);
|
||||
if (bitsToFlip[i] == 0){
|
||||
encodedToSend.push(xor(m0, r0));
|
||||
encodedToSend.push(xor(m1, r1));
|
||||
} else {
|
||||
encodedToSend.push(xor(m0, r1));
|
||||
encodedToSend.push(xor(m1, r0));
|
||||
}
|
||||
}
|
||||
this.sentSoFar += bitsToFlip.length;
|
||||
return concatTA(...encodedToSend);
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
// note that unlike with GCWorker class, the caller of OTWorker's methods doesn't have
|
||||
// to manage the worker pool. We manage them internally inside the class.
|
||||
import {bitsToBytes, concatTA, assert} from './../utils.js';
|
||||
|
||||
import WorkerPool from './WorkerPool.js';
|
||||
|
||||
export class OTWorker extends WorkerPool{
|
||||
constructor(numWorkers){
|
||||
super(numWorkers, chrome.extension.getURL('core/twopc/webWorkers/otworker.js'));
|
||||
}
|
||||
|
||||
async saveDecryptionKeys(bits, A){
|
||||
let padCount = 0;
|
||||
if (bits.length % 8 > 0){
|
||||
// make bits length a multiple of 8
|
||||
padCount = 8 - bits.length % 8;
|
||||
bits.push(...Array(padCount).fill(0));
|
||||
}
|
||||
const bytes = bitsToBytes(bits);
|
||||
// put chunks of 128 bytes into a batch
|
||||
const batch = [];
|
||||
const chunkSize = 128;
|
||||
for (let i=0; i < Math.ceil(bytes.length/chunkSize); i++){
|
||||
batch.push([bytes.slice(i*chunkSize, (i+1)*chunkSize), A]);
|
||||
}
|
||||
const outputs = await this.workerPool(batch, this.saveDecryptionKeysDoWork);
|
||||
const arrOutput = [];
|
||||
for (let i=0; i < batch.length; i++){
|
||||
arrOutput[i] = new Uint8Array(outputs[batch.length-1-i]);
|
||||
}
|
||||
const dataBlob = concatTA(...arrOutput);
|
||||
assert(dataBlob.length === bits.length*48);
|
||||
// deserialize data
|
||||
const entries = [];
|
||||
for (let i=0; i < bits.length; i++){
|
||||
const k = dataBlob.slice(i*48, i*48+16);
|
||||
const B = dataBlob.slice(i*48+16, i*48+48);
|
||||
entries.push([k,B]);
|
||||
}
|
||||
return entries.slice(0, entries.length-padCount);
|
||||
}
|
||||
|
||||
async precomputePool(count, A){
|
||||
// chunk up into 512 items and put chunks into a batch
|
||||
const batch = [];
|
||||
const chunkSize = 512;
|
||||
const chunkCount = Math.ceil(count/chunkSize);
|
||||
const lastChunkSize = count - (chunkSize * (chunkCount-1));
|
||||
for (let i=0; i < chunkCount; i++){
|
||||
if (i === chunkCount-1){
|
||||
batch.push([lastChunkSize, A]);
|
||||
}
|
||||
else{
|
||||
batch.push([chunkSize, A]);
|
||||
}
|
||||
}
|
||||
const outputs = await this.workerPool(batch, this.precomputePoolDoWork);
|
||||
const arrOutput = [];
|
||||
for (let i=0; i < batch.length; i++){
|
||||
arrOutput[i] = new Uint8Array(outputs[i]);
|
||||
}
|
||||
// deserialize data
|
||||
const entries0 = [];
|
||||
const entries1 = [];
|
||||
for (let i=0; i < arrOutput.length; i++){
|
||||
const size = (i < (chunkCount-1)) ? chunkSize : lastChunkSize;
|
||||
const batch = arrOutput[i];
|
||||
for (let j=0; j < size*2; j++){
|
||||
const k = batch.slice(j*48, j*48+16);
|
||||
const B = batch.slice(j*48+16, j*48+48);
|
||||
if (j < size){
|
||||
entries0.push([k,B]);
|
||||
}
|
||||
else {
|
||||
entries1.push([k,B]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const allEntries = [].concat(entries0, entries1);
|
||||
assert(allEntries.length === count*2);
|
||||
return allEntries;
|
||||
}
|
||||
|
||||
async prepareEncryptionKeys(arrOfB, a, A){
|
||||
const batch = [];
|
||||
const chunkSize = 1024;
|
||||
const chunkCount = Math.ceil(arrOfB.length/chunkSize);
|
||||
for (let i=0; i < chunkCount; i++){
|
||||
batch.push([concatTA(...arrOfB.slice(i*chunkSize, (i+1)*chunkSize)), a, A]);
|
||||
}
|
||||
const outputs = await this.workerPool(batch, this.prepareEncryptionKeysDoWork);
|
||||
const arrOutput = [];
|
||||
for (let i=0; i < batch.length; i++){
|
||||
arrOutput[i] = new Uint8Array(outputs[i]);
|
||||
}
|
||||
const dataBlob = concatTA(...arrOutput);
|
||||
assert(dataBlob.length === arrOfB.length*32);
|
||||
// deserialize data
|
||||
const entries = [];
|
||||
for (let i=0; i < arrOfB.length; i++){
|
||||
const k0 = dataBlob.slice(i*32, i*32+16);
|
||||
const k1 = dataBlob.slice(i*32+16, i*32+32);
|
||||
entries.push([k0,k1]);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
prepareEncryptionKeysDoWork(batchItem, worker){
|
||||
const bytes = batchItem[0];
|
||||
const a = batchItem[1];
|
||||
const A = batchItem[2];
|
||||
return new Promise(function(resolve) {
|
||||
worker.onmessage = function(event) {
|
||||
worker['isResolved'] = true;
|
||||
resolve(event.data.blob);
|
||||
};
|
||||
const obj = {msg:'prepareEncryptionKeys', data:{bytes:bytes.buffer, a:a.buffer, A:A.buffer}};
|
||||
worker.postMessage(obj);
|
||||
});
|
||||
}
|
||||
|
||||
precomputePoolDoWork(batchItem, worker){
|
||||
const count = batchItem[0];
|
||||
const A = batchItem[1];
|
||||
return new Promise(function(resolve) {
|
||||
worker.onmessage = function(event) {
|
||||
worker['isResolved'] = true;
|
||||
resolve(event.data.blob);
|
||||
};
|
||||
const obj = {msg:'precomputePool', data:{count:count, A:A.buffer}};
|
||||
worker.postMessage(obj);
|
||||
});
|
||||
}
|
||||
|
||||
saveDecryptionKeysDoWork(batchItem, worker){
|
||||
const bytes = batchItem[0];
|
||||
const A = batchItem[1];
|
||||
return new Promise(function(resolve) {
|
||||
worker.onmessage = function(event) {
|
||||
worker['isResolved'] = true;
|
||||
resolve(event.data.blob);
|
||||
};
|
||||
const obj = {msg:'saveDecryptionKeys', data:{bytes:bytes.buffer, A:A.buffer}};
|
||||
worker.postMessage(obj);
|
||||
});
|
||||
}
|
||||
}
|
||||
1073
core/twopc/TWOPC.js
1073
core/twopc/TWOPC.js
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,26 @@
|
||||
// gcworker.js is a WebWorker which performs garbling and evaluation
|
||||
// of garbled circuits
|
||||
// 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
|
||||
var parentPort_;
|
||||
let circuit = null;
|
||||
let truthTable = null;
|
||||
let timeEvaluating = 0;
|
||||
|
||||
// sha0 is used by randomOracle
|
||||
const sha0 = new Uint8Array( hex2ba('da5698be17b9b46962335799779fbeca8ce5d491c0d26243bafef9ea1837a9d8'));
|
||||
// byteArray is used by randomOracle
|
||||
const byteArray = new Uint8Array(24);
|
||||
// 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,0,0,0,0]);
|
||||
// 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]);
|
||||
// randomPool will be filled with data from getRandom
|
||||
let randomPool;
|
||||
// randomPoolOffset will be moved after data was read from randomPool
|
||||
let randomPoolOffset = 0;
|
||||
let garbledAssigment;
|
||||
var crypto_;
|
||||
var nacl;
|
||||
|
||||
if (typeof(importScripts) !== 'undefined') {
|
||||
importScripts('./../../third-party/nacl-fast.js');
|
||||
crypto_ = self.crypto;
|
||||
self.onmessage = function(event) {
|
||||
processMessage(event.data);
|
||||
@@ -34,7 +34,6 @@ if (typeof(importScripts) !== 'undefined') {
|
||||
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)
|
||||
nacl = require('tweetnacl')
|
||||
const { parentPort } = require('worker_threads');
|
||||
parentPort_ = parentPort
|
||||
const { Crypto } = require("@peculiar/webcrypto");
|
||||
@@ -56,7 +55,7 @@ function processMessage(obj){
|
||||
// no need to respond to this message
|
||||
}
|
||||
else if (msg === 'setTruthTable'){
|
||||
assert(data.byteLength == circuit.andGateCount*64);
|
||||
assert(data.byteLength == circuit.andGateCount*48);
|
||||
truthTable = new Uint8Array(data);
|
||||
}
|
||||
else if (msg === 'garble'){
|
||||
@@ -70,7 +69,7 @@ function processMessage(obj){
|
||||
const reuseR = (data == undefined) ? undefined : data.reuseR;
|
||||
|
||||
const [truthTable, inputLabels, outputLabels, R] = garble(circuit, garbledAssigment, reuseLabels, reuseIndexes, reuseR);
|
||||
assert (truthTable.length === circuit.andGateCount*64);
|
||||
assert (truthTable.length === circuit.andGateCount*48);
|
||||
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};
|
||||
@@ -106,6 +105,8 @@ function postMsg(value, transferList){
|
||||
|
||||
function newR(){
|
||||
const R = getRandom(16);
|
||||
// set the last bit of R to 1 for point-and-permute
|
||||
// this guarantees that 2 labels of the same wire will have the opposite last bits
|
||||
R[15] = R[15] | 0x01;
|
||||
return R;
|
||||
}
|
||||
@@ -145,7 +146,8 @@ function garble(circuit, ga, reuseLabels = new Uint8Array(0) , reuseIndexes = []
|
||||
}
|
||||
}
|
||||
|
||||
const truthTable = new Uint8Array(circuit.andGateCount*64);
|
||||
const truthTable = new Uint8Array(circuit.andGateCount*48);
|
||||
|
||||
let andGateIdx = 0;
|
||||
// garble gates
|
||||
for (let i = 0; i < circuit.gatesCount; i++) {
|
||||
@@ -167,41 +169,64 @@ function garble(circuit, ga, reuseLabels = new Uint8Array(0) , reuseIndexes = []
|
||||
|
||||
}
|
||||
|
||||
|
||||
const garbleAnd = function (gateBlob, R, ga, tt, andGateIdx, id) {
|
||||
// get wire numbers
|
||||
const in1 = threeBytesToInt(gateBlob.subarray(1,4));
|
||||
const in2 = threeBytesToInt(gateBlob.subarray(4,7));
|
||||
const out = threeBytesToInt(gateBlob.subarray(7,10));
|
||||
|
||||
const randomLabel = getRandom(16);
|
||||
|
||||
gaSetIndexG(ga, out, 0, randomLabel);
|
||||
gaSetIndexG(ga, out, 1, xor(randomLabel, R, true));
|
||||
|
||||
// get labels of each wire
|
||||
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);
|
||||
const out_0 = gaGetIndexG(ga, out, 0);
|
||||
const out_1 = gaGetIndexG(ga, out, 1);
|
||||
|
||||
const values = [
|
||||
encrypt(in1_0, in2_0, id, out_0),
|
||||
encrypt(in1_0, in2_1, id, out_0),
|
||||
encrypt(in1_1, in2_0, id, out_0),
|
||||
encrypt(in1_1, in2_1, id, out_1)
|
||||
];
|
||||
// rows is a truthtable if wire labels in a canonical order, the third
|
||||
// item shows an index of output label
|
||||
const rows = [
|
||||
[in1_0, in2_0, 0],
|
||||
[in1_0, in2_1, 0],
|
||||
[in1_1, in2_0, 0],
|
||||
[in1_1, in2_1, 1]
|
||||
]
|
||||
|
||||
const points = [
|
||||
2 * getPoint(in1_0) + getPoint(in2_0),
|
||||
2 * getPoint(in1_0) + getPoint(in2_1),
|
||||
2 * getPoint(in1_1) + getPoint(in2_0),
|
||||
2 * getPoint(in1_1) + getPoint(in2_1)
|
||||
];
|
||||
// GRR3: garbled row reduction
|
||||
// We want to reduce a row where both labels' points are set to 1.
|
||||
// We first need to encrypt those labels with a dummy all-zero output label. The
|
||||
// result X will be the actual value of the output label that we need to set.
|
||||
// After we set the output label to X and encrypt again, the result will be 0 (but
|
||||
// we don't actually need to encrypt it again, we just know that the result will be 0)
|
||||
|
||||
tt.set(values[0], andGateIdx*64+16*points[0]);
|
||||
tt.set(values[1], andGateIdx*64+16*points[1]);
|
||||
tt.set(values[2], andGateIdx*64+16*points[2]);
|
||||
tt.set(values[3], andGateIdx*64+16*points[3]);
|
||||
let outLabels
|
||||
// idxToReduce is the index of the row that will be reduced
|
||||
let idxToReduce = -1;
|
||||
for (let i=0; i < rows.length; i++){
|
||||
if (getPoint(rows[i][0]) == 1 && getPoint(rows[i][1]) == 1){
|
||||
const outWire = encrypt(rows[i][0], rows[i][1], id, new Uint8Array(16).fill(0));
|
||||
if (i==3){
|
||||
outLabels = [xor(outWire, R), outWire]
|
||||
} else {
|
||||
outLabels = [outWire, xor(outWire, R)]
|
||||
}
|
||||
idxToReduce = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
gaSetIndexG(ga, out, 0, outLabels[0]);
|
||||
gaSetIndexG(ga, out, 1, outLabels[1]);
|
||||
assert(idxToReduce != -1)
|
||||
|
||||
for (let i=0; i < rows.length; i++){
|
||||
if (i == idxToReduce){
|
||||
// not encrypting this row because we already know that its encryption is 0
|
||||
// and the sum of its points is 3
|
||||
continue;
|
||||
}
|
||||
const value = encrypt(rows[i][0], rows[i][1], id, outLabels[rows[i][2]]);
|
||||
const point = 2 * getPoint(rows[i][0]) + getPoint(rows[i][1])
|
||||
tt.set(value, andGateIdx*48+16*point);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -255,7 +280,6 @@ function evaluate (circuit, ga, tt, inputLabels) {
|
||||
}
|
||||
console.timeEnd('worker_evaluate');
|
||||
const t1 = performance.now();
|
||||
timeEvaluating += (t1 - t0);
|
||||
|
||||
return ga.slice((circuit.wiresCount-circuit.outputSize)*16, circuit.wiresCount*16);
|
||||
}
|
||||
@@ -268,13 +292,20 @@ const evaluateAnd = function (ga, tt, andGateIdx, gateBlob, id) {
|
||||
const label1 = gaGetIndexE(ga, in1); // ga[in1];
|
||||
const label2 = gaGetIndexE(ga, in2); // ga[in2];
|
||||
|
||||
let cipher
|
||||
const point = 2 * getPoint(label1) + getPoint(label2);
|
||||
const offset = andGateIdx*64+16*point;
|
||||
const cipher = tt.subarray(offset, offset+16);
|
||||
|
||||
if (point == 3){
|
||||
// GRR3: all rows with point sum of 3 have been reduced
|
||||
// their encryption is an all-zero bytestring
|
||||
cipher = new Uint8Array(16).fill(0);
|
||||
} else {
|
||||
const offset = andGateIdx*48+16*point;
|
||||
cipher = tt.subarray(offset, offset+16);
|
||||
}
|
||||
gaSetIndexE(ga, out, decrypt(label1, label2, id, cipher));
|
||||
};
|
||||
|
||||
|
||||
const evaluateXor = function (ga, gateBlob) {
|
||||
const in1 = threeBytesToInt(gateBlob.subarray(1,4));
|
||||
const in2 = threeBytesToInt(gateBlob.subarray(4,7));
|
||||
@@ -315,10 +346,7 @@ function gaSetIndexG(ga, idx, pos, value){
|
||||
|
||||
|
||||
function xor(a, b, reuse) {
|
||||
if (a.length !== b.length){
|
||||
console.log('a.length !== b.length');
|
||||
throw('a.length !== b.length');
|
||||
}
|
||||
assert(a.length == b.length, 'a.length !== b.length')
|
||||
let bytes;
|
||||
if (reuse === true){
|
||||
// in some cases the calling function will have no more use of "a"
|
||||
@@ -341,16 +369,18 @@ function getPoint(arr) {
|
||||
|
||||
const decrypt = encrypt;
|
||||
|
||||
let a2;
|
||||
let b4;
|
||||
// Based on the the A4 method from Fig.1 and the D4 method in Fig6 of the BHKR13 paper
|
||||
// (https://eprint.iacr.org/2013/426.pdf)
|
||||
// Note that the paper doesn't prescribe a specific method to break the symmetry between A and B,
|
||||
// so we choose a circular byte shift instead of a circular bitshift as in Fig6.
|
||||
function encrypt(a, b, t, m) {
|
||||
// double a
|
||||
a2 = a.slice();
|
||||
const a2 = a.slice();
|
||||
const leastbyte = a2[0];
|
||||
a2.copyWithin(0,1,15); // Logical left shift by 1 byte
|
||||
a2[14] = leastbyte; // Restore old least byte as new greatest (non-pointer) byte
|
||||
// quadruple b
|
||||
b4 = b.slice();
|
||||
const b4 = b.slice();
|
||||
const leastbytes = [b4[0], b4[1]];
|
||||
b4.copyWithin(0,2,15); // Logical left shift by 2 byte
|
||||
[b4[13], b4[14]] = leastbytes; // Restore old least two bytes as new greatest bytes
|
||||
@@ -361,41 +391,23 @@ function encrypt(a, b, t, m) {
|
||||
return xor(ro, mXorK, true);
|
||||
}
|
||||
|
||||
|
||||
function randomOracle(m, t) {
|
||||
return nacl.secretbox(
|
||||
m,
|
||||
longToByteArray(t),
|
||||
sha0,
|
||||
).subarray(0,16);
|
||||
// convert the integer t to a 4-byte big-endian array and append
|
||||
// it to fixedKey in-place
|
||||
for (let index = 0; index < 4; index++) {
|
||||
const byte = t & 0xff;
|
||||
fixedKey[31-index] = byte;
|
||||
t = (t - byte) / 256;
|
||||
}
|
||||
return Salsa20(fixedKey, m);
|
||||
}
|
||||
|
||||
function longToByteArray(long) {
|
||||
// we want to represent the input as a 24-bytes array
|
||||
for (let index = 0; index < byteArray.length; index++) {
|
||||
const byte = long & 0xff;
|
||||
byteArray[index] = byte;
|
||||
long = (long - byte) / 256;
|
||||
}
|
||||
return byteArray;
|
||||
}
|
||||
|
||||
function threeBytesToInt(b){
|
||||
return b[2] + b[1]*256 + b[0]*65536;
|
||||
}
|
||||
|
||||
// convert a hex string into byte array
|
||||
function hex2ba(str) {
|
||||
var ba = [];
|
||||
// pad with a leading 0 if necessary
|
||||
if (str.length % 2) {
|
||||
str = '0' + str;
|
||||
}
|
||||
for (var i = 0; i < str.length; i += 2) {
|
||||
ba.push(parseInt('0x' + str.substr(i, 2)));
|
||||
}
|
||||
return ba;
|
||||
}
|
||||
|
||||
|
||||
function getRandom(count) {
|
||||
const rand = randomPool.subarray(randomPoolOffset, randomPoolOffset+count);
|
||||
@@ -439,3 +451,147 @@ function concatTA (...arr){
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
|
||||
// use Salsa20 as a random permutator. Instead of the nonce, we feed the data that needs
|
||||
// to be permuted.
|
||||
function Salsa20(key, data){
|
||||
const out = new Uint8Array(16);
|
||||
core_salsa20(out, data, key, sigma)
|
||||
return out;
|
||||
}
|
||||
|
||||
// copied from https://github.com/dchest/tweetnacl-js/blob/master/nacl-fast.js
|
||||
// and modified to output only 16 bytes
|
||||
function core_salsa20(o, p, k, 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,
|
||||
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;
|
||||
|
||||
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;
|
||||
// we only need 16 bytes of the output
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
// otworker.js is a WebWorker where most of the heavy computations for
|
||||
// Oblivious Transfer happen. Based on Chou-Orlandi "Simplest OT"
|
||||
|
||||
var sodium
|
||||
var parentPort_;
|
||||
|
||||
|
||||
if (typeof(importScripts) === 'undefined'){
|
||||
// we are in nodejs
|
||||
import('module').then((module) => {
|
||||
// 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
|
||||
sodium = require('libsodium-wrappers-sumo');
|
||||
parentPort.on('message', msg => {
|
||||
processMessage(msg);
|
||||
})
|
||||
})
|
||||
} else {
|
||||
importScripts('./../../third-party/sodium.js');
|
||||
self.onmessage = function(event) {
|
||||
processMessage(event.data);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function processMessage(obj){
|
||||
const msg = obj.msg;
|
||||
const data = obj.data;
|
||||
if (msg === 'saveDecryptionKeys'){
|
||||
const bytes = new Uint8Array(data.bytes);
|
||||
const A = new Uint8Array(data.A);
|
||||
const rv = saveDecryptionKeys(bytes, A);
|
||||
postMsg({'blob': rv.buffer});
|
||||
|
||||
}
|
||||
else if (msg === 'precomputePool'){
|
||||
const count = data.count;
|
||||
const A = new Uint8Array(data.A);
|
||||
const rv = precomputePool(count, A);
|
||||
postMsg({'blob': rv.buffer});
|
||||
}
|
||||
else if (msg === 'prepareEncryptionKeys'){
|
||||
const bytes = new Uint8Array(data.bytes);
|
||||
const a = new Uint8Array(data.a, data.A);
|
||||
const A = new Uint8Array(data.A);
|
||||
const rv = prepareEncryptionKeys(bytes, a, A);
|
||||
postMsg({'blob': rv.buffer});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function postMsg(value, transferList){
|
||||
if (typeof importScripts !== 'function'){
|
||||
parentPort_.postMessage({data:value}, transferList)
|
||||
} else {
|
||||
postMessage(value, transferList);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function prepareEncryptionKeys(bytes, a, A){
|
||||
const blob = [];
|
||||
const Bcount = bytes.length/32;
|
||||
for (let i=0; i < Bcount; i++){
|
||||
const B = bytes.slice(i*32, (i+1)*32);
|
||||
const k0 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(a, B));
|
||||
const sub = sodium.crypto_core_ristretto255_sub(B, A);
|
||||
const k1 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(a, sub));
|
||||
blob.push(k0);
|
||||
blob.push(k1);
|
||||
}
|
||||
return concatTA(...blob);
|
||||
}
|
||||
|
||||
function saveDecryptionKeys(bytes, A){
|
||||
const bits = bytesToBits(bytes);
|
||||
const blob = [];
|
||||
for (const bit of bits){
|
||||
if (bit === 0){
|
||||
const b0 = sodium.crypto_core_ristretto255_scalar_random();
|
||||
const B0 = sodium.crypto_scalarmult_ristretto255_base(b0);
|
||||
const k0 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(b0, A));
|
||||
blob.push(k0);
|
||||
blob.push(B0);
|
||||
}
|
||||
else {
|
||||
const b1 = sodium.crypto_core_ristretto255_scalar_random();
|
||||
const gb1 = sodium.crypto_scalarmult_ristretto255_base(b1);
|
||||
const B1 = sodium.crypto_core_ristretto255_add(A, gb1);
|
||||
const k1 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(b1, A));
|
||||
blob.push(k1);
|
||||
blob.push(B1);
|
||||
}
|
||||
}
|
||||
return concatTA(...blob);
|
||||
}
|
||||
|
||||
function precomputePool(count, A){
|
||||
const blob = [];
|
||||
for (let i = 0; i < count ; i++){
|
||||
const b0 = sodium.crypto_core_ristretto255_scalar_random();
|
||||
const B0 = sodium.crypto_scalarmult_ristretto255_base(b0);
|
||||
const k0 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(b0, A));
|
||||
blob.push(k0);
|
||||
blob.push(B0);
|
||||
}
|
||||
for (let i = 0; i < count; i++){
|
||||
const b1 = sodium.crypto_core_ristretto255_scalar_random();
|
||||
const gb1 = sodium.crypto_scalarmult_ristretto255_base(b1);
|
||||
const B1 = sodium.crypto_core_ristretto255_add(A, gb1);
|
||||
const k1 = sodium.crypto_generichash(16, sodium.crypto_scalarmult_ristretto255(b1, A));
|
||||
blob.push(k1);
|
||||
blob.push(B1);
|
||||
}
|
||||
return concatTA(...blob);
|
||||
}
|
||||
|
||||
// convert Uint8Array into an array of 0/1 where least bit has index 0
|
||||
function bytesToBits (ba){
|
||||
assert(ba instanceof Uint8Array);
|
||||
const bitArr = Array(ba.length*8);
|
||||
let idx = 0;
|
||||
for (let i=ba.length-1; i >= 0; i--){
|
||||
for (let j=0; j < 8; j++){
|
||||
bitArr[idx] = (ba[i] >> j) & 0x01;
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
return bitArr;
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
console.trace();
|
||||
throw message || 'Assertion failed';
|
||||
}
|
||||
}
|
||||
|
||||
// concatenate an array of typed arrays (specifically Uint8Array)
|
||||
function concatTA (...arr){
|
||||
let newLen = 0;
|
||||
for (const item of arr){
|
||||
assert(item instanceof Uint8Array);
|
||||
newLen += item.length;
|
||||
}
|
||||
const newArray = new Uint8Array(newLen);
|
||||
let offset = 0;
|
||||
for (const item of arr){
|
||||
newArray.set(item, offset);
|
||||
offset += item.length;
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
@@ -8,13 +8,13 @@ if (typeof(importScripts) === 'undefined'){
|
||||
// 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 require = module.createRequire(filePath);
|
||||
const { parentPort } = require('worker_threads');
|
||||
parentPort.on('message', msg => {
|
||||
const text = msg.text;
|
||||
const [obj, blob] = serializeCircuit(text);
|
||||
parentPort.postMessage({data: {'obj': obj, blob: blob.buffer}});
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
|
||||
335
core/utils.js
335
core/utils.js
@@ -1,3 +1,7 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable no-undef */
|
||||
/* global chrome, CBOR, COSE, Buffer, fastsha256 */
|
||||
|
||||
import {verifyChain} from './verifychain.js';
|
||||
import * as asn1js from './third-party/pkijs/asn1.js';
|
||||
import Certificate from './third-party/pkijs/Certificate.js';
|
||||
@@ -8,7 +12,7 @@ export function sortKeys(obj){
|
||||
return numArray.sort(function(a, b){return a-b;});
|
||||
}
|
||||
|
||||
// convert a byte array into a hex string
|
||||
// convert a Uint8Array into a hex string
|
||||
export function ba2hex(ba) {
|
||||
assert(ba instanceof Uint8Array);
|
||||
let hexstring = '';
|
||||
@@ -22,7 +26,7 @@ export function ba2hex(ba) {
|
||||
return hexstring;
|
||||
}
|
||||
|
||||
// convert a hex string into byte array
|
||||
// convert a hex string into a Uint8Array
|
||||
export function hex2ba(str) {
|
||||
const ba = [];
|
||||
// pad with a leading 0 if necessary
|
||||
@@ -118,7 +122,7 @@ export function ba2str(ba) {
|
||||
// xor 2 byte arrays of equal length
|
||||
export function xor (a, b){
|
||||
assert(a instanceof Uint8Array && b instanceof Uint8Array);
|
||||
assert(a.length === b.length);
|
||||
assert(a.length == b.length);
|
||||
var c = new Uint8Array(a.length);
|
||||
for (var i=0; i< a.length; i++){
|
||||
c[i] = a[i]^b[i];
|
||||
@@ -389,15 +393,15 @@ export function gunzip_http(dechunkedRecords) {
|
||||
return dechunkedRecords; // #nothing to gunzip
|
||||
}
|
||||
throw ('gzip enabled');
|
||||
var http_body = http_data.slice(http_header.length);
|
||||
var ungzipped = http_header;
|
||||
if (!http_body) {
|
||||
// HTTP 304 Not Modified has no body
|
||||
return [ungzipped];
|
||||
}
|
||||
var inflated = pako.inflate(http_body);
|
||||
ungzipped += ba2str(inflated);
|
||||
return [ungzipped];
|
||||
// var http_body = http_data.slice(http_header.length);
|
||||
// var ungzipped = http_header;
|
||||
// if (!http_body) {
|
||||
// // HTTP 304 Not Modified has no body
|
||||
// return [ungzipped];
|
||||
// }
|
||||
// var inflated = pako.inflate(http_body);
|
||||
// ungzipped += ba2str(inflated);
|
||||
// return [ungzipped];
|
||||
}
|
||||
|
||||
export function getTime() {
|
||||
@@ -428,7 +432,7 @@ export function pem2ba(pem) {
|
||||
}
|
||||
|
||||
|
||||
// compare bytes in 2 arrays. a or b can be either Array or Uint8Array
|
||||
// compares two Uint8Arrays or Arrays
|
||||
export function eq(a, b) {
|
||||
assert(Array.isArray(a) || a instanceof Uint8Array);
|
||||
assert(Array.isArray(b) || b instanceof Uint8Array);
|
||||
@@ -459,31 +463,6 @@ export function splitIntoChunks(ba, chunkSize) {
|
||||
return newArray;
|
||||
}
|
||||
|
||||
// perform GCM Galois Field block multiplication
|
||||
// x,y are byte arrays
|
||||
export function blockMult(x_,y_){
|
||||
// casting to BigInt just in case if ba2int returns a Number
|
||||
let x = BigInt(ba2int(x_));
|
||||
const y = BigInt(ba2int(y_));
|
||||
let res = 0n;
|
||||
for (let i=127n; i >= 0n; i--){
|
||||
res ^= x * ((y >> i) & 1n);
|
||||
x = (x >> 1n) ^ ((x & 1n) * BigInt(0xE1000000000000000000000000000000));
|
||||
}
|
||||
return int2ba(res, 16);
|
||||
}
|
||||
|
||||
// x is Uint8Array
|
||||
export function getXTable(x_){
|
||||
let x = ba2int(x_);
|
||||
const table = [];
|
||||
for (let i=0; i < 128; i++){
|
||||
table[i] = int2ba(x, 16);
|
||||
x = (x >> 1n) ^ ((x & 1n) * BigInt(0xE1000000000000000000000000000000));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
|
||||
// convert Uint8Array into an array of 0/1 where least bit has index 0
|
||||
export function bytesToBits (ba){
|
||||
@@ -610,29 +589,21 @@ export function encrypt_generic(plaintext, key, nonce) {
|
||||
return xor(tmp, ro);
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(24);
|
||||
const sha0 = hex2ba('da5698be17b9b46962335799779fbeca8ce5d491c0d26243bafef9ea1837a9d8');
|
||||
|
||||
// class PRF is initialized once and then it is a read-only
|
||||
function randomOracle(m, t) {
|
||||
// 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, 0, 0, 0, 0]);
|
||||
|
||||
export function longToByteArray(long) {
|
||||
// we want to represent the input as a 24-bytes array
|
||||
for (let index = 0; index < byteArray.length; index++) {
|
||||
const byte = long & 0xff;
|
||||
byteArray[index] = byte;
|
||||
long = (long - byte) / 256;
|
||||
// convert the integer t to a 4-byte big-endian array and append
|
||||
// it to fixedKey in-place
|
||||
for (let index = 0; index < 4; index++) {
|
||||
const byte = t & 0xff;
|
||||
fixedKey[31-index] = byte;
|
||||
t = (t - byte) / 256;
|
||||
}
|
||||
return byteArray;
|
||||
}
|
||||
|
||||
|
||||
export function randomOracle(m, t) {
|
||||
const nonce = longToByteArray(t);
|
||||
return nacl.secretbox(
|
||||
m,
|
||||
nonce, // Nonce 24 bytes because this sodium uses 192 bit blocks.
|
||||
sha0,
|
||||
).slice(0,16);
|
||||
return Salsa20(fixedKey, m);
|
||||
}
|
||||
|
||||
export const decrypt_generic = encrypt_generic;
|
||||
@@ -724,25 +695,212 @@ async function gcmEncrypt(key, plaintext, IV, aad){
|
||||
}
|
||||
|
||||
// WebCrypto doesn't provide AES-ECB encryption. We achieve it by using
|
||||
// the CTR mode and setting CTR's counter to what we want to AES-encrypt and setting CTR's
|
||||
// data to encrypt to zero, because in CTR ciphertext = AES(counter) XOR plaintext
|
||||
//
|
||||
export async function AESECBencrypt(key, data){
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
key.buffer,
|
||||
'AES-CTR',
|
||||
true,
|
||||
['encrypt', 'decrypt']);
|
||||
// the CBC mode with a zero IV. This workaraound only works for encrypting
|
||||
// 16 bytes at a time.
|
||||
|
||||
const zeroes = int2ba(0, 16);
|
||||
export async function AESECBencrypt(key, data){
|
||||
assert(data.length == 16, 'can only AES-ECB encrypt 16 bytes at a time');
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw', key.buffer, 'AES-CBC', false, ['encrypt']);
|
||||
|
||||
// Even if data is a multiple of 16, WebCrypto adds 16 bytes of
|
||||
// padding. We drop it.
|
||||
return new Uint8Array (await crypto.subtle.encrypt({
|
||||
name: 'AES-CTR',
|
||||
counter: data.buffer, length: 16},
|
||||
name: 'AES-CBC',
|
||||
iv: new Uint8Array(16).fill(0).buffer},
|
||||
cryptoKey,
|
||||
zeroes.buffer));
|
||||
data.buffer)).slice(0, 16);
|
||||
}
|
||||
|
||||
|
||||
// AEC-CTR encrypt data, setting initial counter to 0
|
||||
export async function AESCTRencrypt(key, data){
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw', key.buffer, 'AES-CTR', false, ['encrypt']);
|
||||
|
||||
return new Uint8Array (await crypto.subtle.encrypt({
|
||||
name: 'AES-CTR',
|
||||
counter: new Uint8Array(16).fill(0).buffer,
|
||||
length:64},
|
||||
cryptoKey,
|
||||
data.buffer));
|
||||
}
|
||||
|
||||
|
||||
// AEC-CTR decrypt ciphertext, setting initial counter to 0
|
||||
export async function AESCTRdecrypt(key, ciphertext){
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw', key.buffer, 'AES-CTR', false, ['decrypt']);
|
||||
|
||||
return new Uint8Array (await crypto.subtle.decrypt({
|
||||
name: 'AES-CTR',
|
||||
counter: new Uint8Array(16).fill(0).buffer,
|
||||
length:64},
|
||||
cryptoKey,
|
||||
ciphertext.buffer));
|
||||
}
|
||||
|
||||
|
||||
// use Salsa20 as a random permutator. Instead of the nonce, we feed the data that needs
|
||||
// to be permuted.
|
||||
export function Salsa20(key, data){
|
||||
// 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]);
|
||||
const out = new Uint8Array(16);
|
||||
core_salsa20(out, data, key, sigma);
|
||||
return out;
|
||||
}
|
||||
|
||||
// copied from https://github.com/dchest/tweetnacl-js/blob/master/nacl-fast.js
|
||||
// and modified to output only 16 bytes
|
||||
function core_salsa20(o, p, k, 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,
|
||||
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;
|
||||
|
||||
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;
|
||||
// we only need 16 bytes of the output
|
||||
}
|
||||
|
||||
|
||||
// ephemeral key usage time must be within the time of ephemeral key validity
|
||||
export function checkExpiration(validFrom, validUntil, time){
|
||||
time = time || Math.floor(new Date().getTime() / 1000);
|
||||
if (ba2int(validFrom) > time || time > ba2int(validUntil)){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// -----------OBSOLETE functions below this line
|
||||
function bestPathNew(num){
|
||||
const mainPowers = [];
|
||||
const auxPowers = []; // aux powers
|
||||
@@ -1242,14 +1400,6 @@ function allPrimeMultiples(num){
|
||||
return arr;
|
||||
}
|
||||
|
||||
// ephemeral key usage time must be within the time of ephemeral key validity
|
||||
export function checkExpiration(validFrom, validUntil, time){
|
||||
time = time || Math.floor(new Date().getTime() / 1000);
|
||||
if (ba2int(validFrom) > time || time > ba2int(validUntil)){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// computes AES GCM authentication tag
|
||||
// all 4 inputs arrays of bytes
|
||||
@@ -1310,28 +1460,3 @@ function getAuthTag(aad, ct, encZero, encIV, precompute){
|
||||
}
|
||||
|
||||
|
||||
if (typeof module !== 'undefined'){ // we are in node.js environment
|
||||
module.exports={
|
||||
assert,
|
||||
ba2ab,
|
||||
ba2bigint,
|
||||
ba2str,
|
||||
bi2ba,
|
||||
ab2ba,
|
||||
ba2int,
|
||||
buildChunkMetadata,
|
||||
b64encode,
|
||||
b64decode,
|
||||
b64urlencode,
|
||||
dechunk_http,
|
||||
gunzip_http,
|
||||
eq,
|
||||
getTime,
|
||||
pem2ba,
|
||||
pubkeyPEM2raw,
|
||||
sha256,
|
||||
sigDER2p1363,
|
||||
str2ba,
|
||||
xor
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import {pem2ba, eq, import_resource} from './utils.js';
|
||||
import {pem2ba, eq} from './utils.js';
|
||||
import * as asn1js from './third-party/pkijs/asn1.js';
|
||||
import Certificate from './third-party/pkijs/Certificate.js';
|
||||
import CertificateChainValidationEngine from './third-party/pkijs/CertificateChainValidationEngine.js';
|
||||
|
||||
import CertificateChainValidationEngine from
|
||||
'./third-party/pkijs/CertificateChainValidationEngine.js';
|
||||
|
||||
var trustedCertificates = [];
|
||||
|
||||
|
||||
// extract PEMs from Mozilla's CA store and convert into asn1js's Certificate object
|
||||
export async function parse_certs(text){
|
||||
// wait for pkijs module to load
|
||||
@@ -29,11 +28,6 @@ export async function parse_certs(text){
|
||||
}
|
||||
|
||||
|
||||
function getPubkey(c){
|
||||
return new Uint8Array(c.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHex);
|
||||
}
|
||||
|
||||
|
||||
// Verify that the common name in the Certificate is the name of the server we are sending to
|
||||
// Otherwise an attacker can send us his CA-issued certificate for evildomain.com
|
||||
export function checkCertSubjName(cert, serverName){
|
||||
@@ -164,14 +158,3 @@ export async function verifyChain(chain_der, date, trustedCerts) {
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
|
||||
if (typeof module !== 'undefined'){ // we are in node.js environment
|
||||
module.exports={
|
||||
checkCertSubjName,
|
||||
getCommonName,
|
||||
getModulus,
|
||||
parse_certs,
|
||||
verifyChain
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// FileChooser create a "choose file" button and sends the chosen file
|
||||
// to the extension
|
||||
/* global chrome*/
|
||||
|
||||
// class FileChooser create a "choose file" button and sends the chosen file
|
||||
// to the extension
|
||||
export class FileChooser{
|
||||
// show is called by extension's Main.openFileChooser()
|
||||
show(){
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* global chrome, swal*/
|
||||
|
||||
import {str2ba} from './utils.js';
|
||||
|
||||
document.addEventListener('load', onload);
|
||||
@@ -218,7 +220,7 @@ class Manager{
|
||||
};
|
||||
input3.value = 'Edit';
|
||||
// Edit button will be used in fututre versions
|
||||
input3.style.visibility = "hidden";
|
||||
input3.style.visibility = 'hidden';
|
||||
if (args.isImported || args.isEdited || (args.version < 5)){
|
||||
input3.style.opacity = '0.3';
|
||||
input3.onclick = null;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* global chrome*/
|
||||
|
||||
export class NotificationBar{
|
||||
constructor(){}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// import {ProgressBar} from './progressbar.min.js';
|
||||
/* global chrome, browser*/
|
||||
|
||||
class Popup{
|
||||
constructor(){
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* global chrome*/
|
||||
|
||||
import {decode_str} from './utils.js';
|
||||
|
||||
class RawViewer{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* global chrome*/
|
||||
|
||||
import {NotificationBar} from './NotificationBar.js';
|
||||
import {FileChooser} from './FileChooser.js';
|
||||
import {decode_str, str2ba } from './utils.js';
|
||||
|
||||
Reference in New Issue
Block a user