UNPKG

fourq

Version:

365 lines (317 loc) 11.9 kB
/* * 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; }