Files
PageSigner/core/twopc/OTCommon.js
themighty1 79173c511e - 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
2022-01-17 10:18:04 +03:00

169 lines
5.8 KiB
JavaScript

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);
}