fourq
Version:
365 lines (317 loc) • 11.9 kB
text/typescript
/*
* Copyright 2014-2021 Firestack, all rights reserved.
*/
declare var require: any;
let FourQImported = false
let FourQlib
try {
FourQlib = require('../build/Release/addon');
FourQImported = true
} catch (e) {
console.error(e)
FourQlib = new Proxy({}, {
get: (t, p, r) => {
if (t[p] !== undefined) {
return FourQlib
}
return t[p]
}
})
}
const crypto = require('crypto');
const isApple = (process.platform === 'darwin');
export enum CryptoScheme {
ED25519 = 'ED',
FourQ = '4Q',
}
export interface SchnorrSignature {
type: string;
data: Buffer;
}
export interface SchnorrKeyPair {
isSchnorr: boolean;
type: CryptoScheme;
publicKey: Buffer;
secretKey: Buffer;
}
export interface DiffieHellmanKeyPair {
isDH: boolean;
type: CryptoScheme;
publicKey: Buffer;
secretKey: Buffer;
}
export interface IdentityKeyPair {
isIdentity: boolean;
type: CryptoScheme;
publicKeySchnorr: Buffer;
publicKeyECDH: Buffer;
secretKey: Buffer;
}
export type BufferLike = Buffer | string;
export class FourQ {
public static imported = FourQImported
public static STRING_MODE_TEXT = 0;
public static STRING_MODE_BASE64 = 1;
public static STRING_MODE_HEX = 2;
public static unsafeBuffers = false;
public static stringMode = FourQ.STRING_MODE_BASE64;
public static features = {
/**
* serversideUnknowabilityLevel
*
* See FourQ.cc Verify function: non 32-byte pubkey will force it to use a random dummy key
* to block timing attack (verification uniformity), and enforce server-side unknowability.
* this means it will take uniform time for signature verification as if ECC verify has been
* called with invalid signature. This is useful when enforcing server-side unknowability to
* nodes running this package in a containerized environment, e.g. inside a blockchain node,
* you may want this feature to prevent adversarial nodes from simply returning true/false
* without actually cryptographically verifying the signature (in that case, honest nodes
* suffer from high CPU consumption of verification while adversarial nodes engage in no work).
*
* 0: disable serverside unknowability
* 1: only enforce serverside unknowability for invalid verification input
* (with uniform failure time as actual ECC verification failure time)
* TODO 2: enforce serverside unknowability for all verifications
* (ANY verification, whether verification input is valid or not will take uniform time,
* about 40k verifications per second per core; this is very CPU intensive.)
*/
serversideUnknowabilityLevel: 1,
};
/**
* Schnorr Variety (Generate, Sign, Verify)
*/
public static generateKeyPair(): SchnorrKeyPair {
return FourQ.generateFromSeed();
}
public static generateFromSeed(seed?: BufferLike): SchnorrKeyPair {
if (!seed) { seed = crypto.randomBytes(32); }
const a: Buffer = toBuffer(seed);
if (a.length !== 32) { throw new Error('Seed must be 32-byte'); }
const publicKey = FourQlib.generateFromSeed(a.buffer);
return {
isSchnorr: true,
type: CryptoScheme.FourQ,
publicKey: Buffer.from(publicKey),
secretKey: a,
};
}
public static sign(message: BufferLike, secretKey: BufferLike): SchnorrSignature {
const a: Buffer = toBuffer(message);
const b: Buffer = toBuffer(secretKey);
if (b.length !== 32) { throw new Error('Secret key must be 32-byte'); }
const sigData = FourQlib.sign(a.buffer, b.buffer);
return {
type: CryptoScheme.FourQ,
data: Buffer.from(sigData),
};
}
public static verify(signature: BufferLike, message: BufferLike, publicKey: BufferLike, serversideUnknowabilityOverride: number = -1) {
const a: Uint8Array = toBuffer(signature);
if (a.length !== 64) { throw new Error('Signature key must be 64-byte'); }
const b: Uint8Array = toBuffer(message);
let c: Uint8Array = toBuffer(publicKey);
const ssu = (serversideUnknowabilityOverride !== -1) ?
serversideUnknowabilityOverride
: FourQ.features.serversideUnknowabilityLevel;
if (ssu >= 1) {
if (!c || c.length !== 32) {
// Force servers to at least verify with a dummy key when input might not be valid
// Make sure verify operation take near uniform time whether verification failed or not
// This is to make sure there is not spamm & info leak from how fast the verification was done
c = Buffer.allocUnsafeSlow(1); c[0] = 0; // dummy zeroed out public key
}
}
return FourQlib.verify(a.buffer, b.buffer, c.buffer);
}
/**
* ECDH Variety (Generate, GetSharedSecret)
*/
public static ecdhGenerateKeyPair(): DiffieHellmanKeyPair {
return FourQ.ecdhGenerateFromSeed();
}
public static ecdhGenerateFromSeed(seed?: BufferLike): DiffieHellmanKeyPair {
if (!seed) { seed = crypto.randomBytes(32); }
const a: Buffer = toBuffer(seed);
if (a.length !== 32) { throw new Error('Seed must be 32-byte'); }
const publicKey = FourQlib.generateFromSeedECDH(a.buffer);
return {
isDH: true,
type: CryptoScheme.FourQ,
publicKey: Buffer.from(publicKey),
secretKey: a,
};
}
public static getSharedSecret(mySecret: BufferLike, theirPublicKey: BufferLike): Buffer {
const a: Buffer = toBuffer(mySecret);
if (a.length !== 32) { throw new Error('Your secret key must be 32-byte'); }
const b: Buffer = toBuffer(theirPublicKey);
if (b.length !== 32) { throw new Error('Their public key must be 32-byte'); }
const sharedSecret = FourQlib.getSharedSecretECDH(a.buffer, b.buffer);
return Buffer.from(sharedSecret);
}
/**
* Other Util Functions
*/
public static randomBytes(length: number = 32): Buffer {
return toBuffer(crypto.randomBytes(length));
}
public static generateIdentity(seed?: BufferLike): IdentityKeyPair {
if (!seed) { seed = crypto.randomBytes(32); }
const secretKeyBuffer: Buffer = toBuffer(seed);
const schnorrKeypair = FourQ.generateFromSeed(seed);
const ecdhKeypair = FourQ.ecdhGenerateFromSeed(seed);
return {
isIdentity: true,
type: CryptoScheme.FourQ,
publicKeySchnorr: schnorrKeypair.publicKey,
publicKeyECDH: ecdhKeypair.publicKey,
secretKey: secretKeyBuffer,
};
}
public static xorCryptSHA512(cypherBuffer64Byte: BufferLike, content: BufferLike) {
const a: Buffer = toBuffer(cypherBuffer64Byte);
if (a.length !== 64) { throw new Error('Your cypherBuffer must be 64-byte'); }
const b: Buffer = toBuffer(content);
const xorEncrypted = FourQlib.xorCryptSHA512(a.buffer, b.buffer);
return Buffer.from(xorEncrypted);
}
/**
* Basic Testing
*/
public static test(verbose: boolean = true) {
const iterationsK = 10;
const iterations = iterationsK * 1000;
const iterationName = `${iterationsK}k`;
const rand = Array.from({length: 32}, () => Math.floor(Math.random() * 256));
const message = Buffer.from(rand);
if (isApple) {
if (verbose) { console.log('FourQ (Generic, Slow)'); }
} else {
if (verbose) { console.log('FourQ (Optimized)'); }
}
const wrongKey = FourQ.generateFromSeed();
const wrongPubKey = crypto.randomBytes(32);
const keys = [];
const k1 = Date.now();
for (let i = 0; i < 25000; ++i) {
keys.push(FourQ.generateFromSeed());
}
const k2 = Date.now();
if (verbose) {
console.log(`${iterationName} Key generation, took ${k2 - k1} ms ` +
`(${(iterationsK/((k2 - k1)/1000)).toFixed(2)}k/s)`);
}
const sigs = [];
const s1 = Date.now();
for (let i = 0; i < iterations; ++i) {
sigs.push(FourQ.sign(message, keys[i].secretKey));
}
const s2 = Date.now();
if (verbose) {
console.log(`${iterationName} Signing, took ${s2 - s1} ms ` +
`(${(iterationsK/((s2 - s1)/1000)).toFixed(2)}k/s)`);
}
const v1 = Date.now();
for (let i = 0; i < iterations; ++i) {
const res = FourQ.verify(sigs[i].data, message, keys[i].publicKey);
if (!res) {
console.log('Verification Failed.', sigs[i].data, message, keys[i].publicKey);
break;
}
}
const v2 = Date.now();
if (verbose) {
console.log(`${iterationName} Valid verifications, took ${v2 - v1} ms ` +
`(${(iterationsK/((v2 - v1)/1000)).toFixed(2)}k/s)`);
}
const wrongSigningKeys: Buffer[] = [];
for (let i = 0; i < iterations; ++i) {
wrongSigningKeys.push(crypto.randomBytes(32));
}
const sb1 = Date.now();
for (let i = 0; i < iterations; ++i) {
FourQ.sign(message, wrongSigningKeys[i]);
}
const sb2 = Date.now();
if (verbose) {
console.log(`${iterationName} Signing with bogus secret key, took ${sb2 - sb1} ms ` +
`(${(iterationsK/((sb2 - sb1)/1000)).toFixed(2)}k/s)`);
}
const iv1 = Date.now();
for (let i = 0; i < iterations; ++i) {
FourQ.verify(sigs[i].data, message, wrongKey.publicKey);
}
const iv2 = Date.now();
if (verbose) {
console.log(`${iterationName} Valid sig verification with wrong public key, took ${iv2 - iv1} ms ` +
`(${(iterationsK/((iv2 - iv1)/1000)).toFixed(2)}k/s)`);
}
const wrongSigs = [];
for (let i = 0; i < iterations; ++i) {
wrongSigs.push({
type: CryptoScheme.FourQ,
data: crypto.randomBytes(64),
});
}
const isv1 = Date.now();
for (let i = 0; i < iterations; ++i) {
FourQ.verify(wrongSigs[i].data, message, keys[i].publicKey);
}
const isv2 = Date.now();
if (verbose) {
console.log(`${iterationName} Bogus sig verifications with wrong public key, took ${isv2 - isv1} ms ` +
`(${(iterationsK/((isv2 - isv1)/1000)).toFixed(2)}k/s)`);
}
const isvp1 = Date.now();
for (let i = 0; i < iterations; ++i) {
FourQ.verify(wrongSigs[i].data, message, wrongPubKey);
}
const isvp2 = Date.now();
if (verbose) {
console.log(`${iterationName} Bogus sig Verifications with bogus public key, took ${isvp2 - isvp1} ms ` +
`(${(iterationsK/((isvp2 - isvp1)/1000)).toFixed(2)}k/s)`);
}
}
public static memoryLeakTest(callback?) {
FourQ.test(false);
const baseUsage = process.memoryUsage();
for (let i = 0; i < 10000000; ++i) {
FourQ.test(false);
memoryCompare(baseUsage, callback);
}
}
}
function toBuffer(a: BufferLike): Buffer {
if (!a) { return null; }
if (Buffer.isBuffer(a)) {
if (FourQ.unsafeBuffers) { return a; }
const b = Buffer.allocUnsafeSlow(a.length);
a.copy(b);
return b;
}
if (typeof a === 'string') {
if (FourQ.stringMode === FourQ.STRING_MODE_BASE64) {
a = Buffer.from(a, 'base64');
} else if (FourQ.stringMode === FourQ.STRING_MODE_HEX) {
a = Buffer.from(a, 'hex');
} else {
a = Buffer.from(a);
}
if (FourQ.unsafeBuffers) { return a; }
const b = Buffer.allocUnsafeSlow(a.length);
a.copy(b);
return b;
}
throw new Error('Unknown type supplied.');
}
function memoryCompare(baseUsage, callback) {
const usageNow = process.memoryUsage();
const rss = Math.floor((usageNow.rss - baseUsage.rss) / 1024 / 1024);
const heapTotal = Math.floor((usageNow.heapTotal - baseUsage.heapTotal) / 1024 / 1024);
const heapUsed = Math.floor((usageNow.heapUsed - baseUsage.heapUsed) / 1024 / 1024);
const external = Math.floor((usageNow.external - baseUsage.external) / 1024 / 1024);
console.log(`dTotal: ${heapTotal}, dUsed: ${heapUsed}, external: ${external}`);
const data = { delta: { rss, heapTotal, heapUsed, external } };
if (callback) { callback(data); }
return data;
}