diff --git a/ExampleGrinder/App.js b/ExampleGrinder/App.js index 5fde8e7..5d41d73 100644 --- a/ExampleGrinder/App.js +++ b/ExampleGrinder/App.js @@ -6,25 +6,27 @@ */ /*eslint no-unused-vars: "warn"*/ import React, { Component } from 'react'; -import { Platform, StyleSheet, Text, View } from 'react-native'; -import { generateSecureRandom } from '../RNSecureRandom/index'; -const instructions = Platform.select({ - ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu', - android: - 'Double tap R on your keyboard to reload,\n' + - 'Shake or press menu button for dev menu' -}); +import { StyleSheet, Text, View } from 'react-native'; +import { randomBitGenerator } from '../lib/randomBitGenerator.js'; type Props = {}; + export default class App extends Component { + state = { randomBits: '' }; + async componentDidMount() { + let randomNumber = await randomBitGenerator(14); + this.setState({ randomBits: randomNumber }); + } render() { return ( - Welcome to React Native! + {this.state.randomBits} To get started, edit App.js - {instructions} + + {this.state.randomBits.length} + ); } diff --git a/e2e/firstTest.spec.js b/e2e/firstTest.spec.js index 8c50844..ae1b723 100644 --- a/e2e/firstTest.spec.js +++ b/e2e/firstTest.spec.js @@ -6,4 +6,8 @@ describe('Example', () => { it('should have welcome screen', async () => { await expect(element(by.id('welcome'))).toBeVisible(); }); + + it('should have the correct length', async () => { + await expect(element(by.id('randombits'))).toHaveText('14'); + }); }); diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..69e25e2 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,46 @@ +const config = { + bits: 8, // default number of bits + radix: 16, // work with hex by default + minbits: 3, + maxbits: 20, // this permits 1,048,575 shares, though going this high is not recommended in js! + bytesperchar: 2, + maxbytesperchar: 6, // math.pow(256,7) > math.pow(2,53) + + // primitive polynomials (in decimal form) for galois fields gf(2^n), for 2 <= n <= 30 + // the index of each term in the array corresponds to the n for that polynomial + // i.e. to get the polynomial for n=16, use primitivepolynomials[16] + primitivepolynomials: [ + null, + null, + 1, + 3, + 3, + 5, + 3, + 3, + 29, + 17, + 9, + 5, + 83, + 27, + 43, + 3, + 45, + 9, + 39, + 39, + 9, + 5, + 3, + 33, + 27, + 9, + 71, + 39, + 9, + 5, + 83 + ] +}; +export default config; diff --git a/lib/galoisField.js b/lib/galoisField.js new file mode 100644 index 0000000..2d6d958 --- /dev/null +++ b/lib/galoisField.js @@ -0,0 +1,87 @@ +import config from './config'; + +//class that represents polynomials as bit arrays +export class Polynomial { + constructor(value) { + if (value % 1 !== 0) { + throw new Error('value must be an integer, got ' + value); + } + this.value = value; + } + getValue() { + return this.value; + } + setValue(value) { + this.value = value; + } + plus(rightHandPolynomial) { + this.value = this.value ^ rightHandPolynomial; + return this; + } + timesMonomialOfDegree(n) { + this.value = this.value << n; + return this; + } + subtractTermsAboveDegree(n) { + this.value = this.value & (Math.pow(2, n + 1) - 1); + return this; + } + static toIntegerForm(bitArray) { + return parseInt(bitArray, 2); + } + static toPolynomialForm(integer) { + return new Number(integer).toString(2); + } +} + +export class CharacteristicTwoGaloisField { + constructor(numElements) { + //this.n represents n in GF(2^n) + this.n = Math.log2(numElements); + if ( + this.n && + (this.n % 1 !== 0 || this.n < config.minBits || this.n > config.maxBits) + ) { + throw new Error( + 'Number of n must be an integer between ' + + config.minBits + + ' and ' + + config.maxBits + + ', inclusive.' + ); + } + this.numberOfElementsInField = numElements; + this.computeLogAndExpTables(); + } + fieldContains(value) { + return value < this.numberOfElementsInField; + } + getExponentOfNextDegree(expOfCurrentDegree) { + let primitivePolynomial = config.primitivepolynomials[this.n]; + let polynomial = expOfCurrentDegree.timesMonomialOfDegree(1); + if (!this.fieldContains(polynomial.getValue())) { + polynomial = polynomial + .plus(primitivePolynomial) + .subtractTermsAboveDegree(this.n - 1); + } + return polynomial; + } + computeLogAndExpTables() { + this.exps = []; + this.logs = []; + let polynomial = new Polynomial(1); + for (let i = 0; i < this.numberOfElementsInField; i++) { + this.exps[i] = polynomial.getValue(); + this.logs[polynomial.getValue()] = i; + polynomial = this.getExponentOfNextDegree(polynomial); + } + } + multiply(polynomialOne, polynomialTwo) { + return new Polynomial( + this.exps[ + (this.logs[polynomialOne] + this.logs[polynomialTwo]) % + (this.numberOfElementsInField - 1) + ] + ); + } +} diff --git a/lib/galoisField.test.js b/lib/galoisField.test.js new file mode 100644 index 0000000..c22f7ec --- /dev/null +++ b/lib/galoisField.test.js @@ -0,0 +1,65 @@ +import { Polynomial, CharacteristicTwoGaloisField } from './galoisField'; +//Testing Polynomial Class +describe('adds two polynomials in galois field of characteristic two', () => { + it('should output the correct XOR value of two polynomials represened as bit arrays', () => { + var polynomialOne = Polynomial.toIntegerForm('1010'); //equivelent of x^3 + x (^ is exponent in this context) + var polynomialTwo = Polynomial.toIntegerForm('0110'); //equivelent of x^2 + x + var expectedResult = Polynomial.toIntegerForm('1100'); //equivelent of x^3 + x^2 + var actualResult = new Polynomial(polynomialOne).plus(polynomialTwo); + expect(expectedResult).toBe(actualResult.getValue()); + expect(new Polynomial(1).plus(5).getValue()).toBe(4); + }); +}); + +describe('multiplies a polynomial by a monomial of the given degree', () => { + it('should multiply the valueToMultiply by monomial', () => { + var valueToMultiply = Polynomial.toIntegerForm('11100'); //x^4 + x^3 + x^2 in polynomial form, and 28 in integer form + var monomial = '10'; //equivelently x^1 in polynomial form and 2 in integer form + var degreeOfMonomial = Math.log2(Polynomial.toIntegerForm(monomial)); //degree is 1 + var expectedResult = Polynomial.toIntegerForm('111000'); + var actualResult = new Polynomial(valueToMultiply).timesMonomialOfDegree( + degreeOfMonomial + ); + expect(expectedResult).toBe(actualResult.getValue()); + }); +}); + +describe('subtract terms above degree', () => { + it('bitwise AND with bit array containing all 1 values up to given degree', () => { + var polynomialWithBigDegree = Polynomial.toIntegerForm('11100'); + var expectedResult = Polynomial.toIntegerForm('100'); + var actualResult = new Polynomial( + polynomialWithBigDegree + ).subtractTermsAboveDegree(2); + expect(expectedResult).toBe(actualResult.getValue()); + }); +}); + +//Testing ChracteristicTwoGaloisField class +describe('determines if an element is contained within the field', () => { + it('should return false if the degree of the element is above n in 2^n', () => { + var polynomial = Polynomial.toIntegerForm('1111110'); + var gf = new CharacteristicTwoGaloisField(Math.pow(2, 5)); + var expectedResult = false; + var actualResult = gf.fieldContains(polynomial); + expect(expectedResult).toBe(actualResult); + }); +}); +describe('create exponent and log tables for the field', () => { + it('should output the correct exp and log tables for GF(2^3)', () => { + var gf = new CharacteristicTwoGaloisField(Math.pow(2, 3)); + expect(gf.exps[0]).toEqual(1); + expect(gf.exps[1]).toEqual(2); + expect(gf.exps[2]).toEqual(4); + expect(gf.exps[3]).toEqual(3); + expect(gf.exps[4]).toEqual(6); + expect(gf.exps[5]).toEqual(7); + expect(gf.exps[6]).toEqual(5); + expect(gf.exps[7]).toEqual(1); + for (var i = 1; i < Math.pow(2, 3); i++) { + expect(gf.exps[gf.logs[i]]).toEqual(i); + } + expect(gf.logs[1]).toBe(7); + expect(gf.logs[0]).toBeFalsy(); + }); +}); diff --git a/lib/randomBitGenerator.js b/lib/randomBitGenerator.js new file mode 100644 index 0000000..43f1387 --- /dev/null +++ b/lib/randomBitGenerator.js @@ -0,0 +1,15 @@ +import { generateSecureRandom } from '../RNSecureRandom/index'; +import { bytesToBits } from './utils'; + +export async function randomBitGenerator(numBits) { + var numBytes, + str = null; + + numBytes = Math.ceil(numBits / 8); + while (str === null) { + let uIntByteArr = await generateSecureRandom(numBytes); + str = bytesToBits(uIntByteArr); + } + str = str.substr(-numBits); + return str; +} diff --git a/lib/sssa.js b/lib/sssa.js new file mode 100644 index 0000000..9bbf40e --- /dev/null +++ b/lib/sssa.js @@ -0,0 +1,225 @@ +import { randomBitGenerator } from './randomBitGenerator'; +import { Polynomial, CharacteristicTwoGaloisField } from './galoisField'; +var isBase64 = require('is-base64'); +import { + splitBitsToIntArray, + base64ToBits, + padLeft, + bin2hex +} from './utils.js'; +export class SSSA { + constructor(coeffLength) { + this.fieldSize = Math.pow(2, coeffLength); + this.char2GF = new CharacteristicTwoGaloisField(this.fieldSize); + this.coeffLength = coeffLength; + } + // Polynomial evaluation at `x` using Horner's Method + // NOTE: fx=fx * x + coeff[i] -> exp(log(fx) + log(x)) + coeff[i], + // so if fx===0, just set fx to coeff[i] because + // using the exp/log form will result in incorrect value + horner(x, coeffs) { + var galoisField = this.char2GF; + var fx = 0; + + for (var i = coeffs.length - 1; i >= 0; i--) { + var coefficient = Polynomial.toIntegerForm(coeffs[i]); + if (fx !== 0) { + fx = galoisField + .multiply(x, fx) + .plus(coefficient) + .getValue(); + } else { + fx = coefficient; + } + } + + return fx; + } + + getPointsOnPolynomialFor(secretByte, numShares, threshold) { + var shares = [], + coeffs = [secretByte], + i, + len; + + for (i = 1; i < threshold; i++) { + coeffs[i] = randomBitGenerator(this.coeffLength, 2); + } + + for (i = 1, len = numShares + 1; i < len; i++) { + shares[i - 1] = { + x: i, + y: this.horner(i, coeffs) + }; + } + + return shares; + } + + generateShares(secret, numShares, threshold, padLength) { + var neededBits, + subShares, + x = new Array(numShares), + y = new Array(numShares), + maxShares = this.fieldSize - 1, + i, + j, + len; + + // Security: + // For additional security, pad in multiples of 128 bits by default. + // A small trade-off in larger share size to help prevent leakage of information + // about small-ish secrets and increase the difficulty of attacking them. + padLength = padLength || 128; + + if (typeof secret !== 'string') { + throw new Error('Secret must be a string.'); + } + + if (typeof numShares !== 'number' || numShares % 1 !== 0 || numShares < 2) { + throw new Error( + 'Number of shares must be an integer between 2 and (' + + this.fieldSize - + 1 + + '), inclusive.' + ); + } + + if (numShares > maxShares) { + neededBits = Math.ceil(Math.log(numShares + 1) / Math.LN2); + throw new Error( + 'Number of shares must be an integer between 2 and (' + + this.fieldSize - + 1 + + '), inclusive. To create ' + + numShares + + ' shares, use at least ' + + neededBits + + ' bits.' + ); + } + + if (typeof threshold !== 'number' || threshold % 1 !== 0 || threshold < 2) { + throw new Error( + 'Threshold number of shares must be an integer between 2 and 2^bits-1 (' + + maxShares + + '), inclusive.' + ); + } + + if (threshold > maxShares) { + neededBits = Math.ceil(Math.log(threshold + 1) / Math.LN2); + throw new Error( + 'Threshold number of shares must be an integer between 2 and 2^bits-1 (' + + maxShares + + '), inclusive. To use a threshold of ' + + threshold + + ', create an SSSA instance with a coefficient size of atleast' + + neededBits + + ' bits.' + ); + } + + if (threshold > numShares) { + throw new Error( + 'Threshold number of shares was ' + + threshold + + ' but must be less than or equal to the ' + + numShares + + ' shares specified as the total to generate.' + ); + } + + if ( + typeof padLength !== 'number' || + padLength % 1 !== 0 || + padLength < 0 || + padLength > 1024 + ) { + throw new Error( + 'Zero-pad length must be an integer between 0 and 1024 inclusive.' + ); + } + + if (!isBase64(secret)) { + throw new Error('secret but be base-64'); + } + + secret = '1' + base64ToBits(secret); // append a 1 as a marker so that we can preserve the correct number of leading zeros in our secret + secret = splitBitsToIntArray(secret, padLength, this.coeffLength); //uses global coefficient length + + for (i = 0, len = secret.length; i < len; i++) { + subShares = this.getPointsOnPolynomialFor( + secret[i], + numShares, + threshold + ); + for (j = 0; j < numShares; j++) { + x[j] = x[j] || subShares[j].x; + y[j] = padLeft(subShares[j].y.toString(2)) + (y[j] || ''); + } + } + + for (i = 0; i < numShares; i++) { + x[i] = this.constructPublicShareString(x[i], bin2hex(y[i])); //changed to hex so it can be used with RegEx + } + + return x; + } + constructPublicShareString(id, data) { + var idHex, idMax, idPaddingLen, newShareString; + + idMax = this.fieldSize - 1; + idPaddingLen = idMax.toString(16).length; + idHex = padLeft(id.toString(16), idPaddingLen); + + if (typeof id !== 'number' || id % 1 !== 0 || id < 1 || id > idMax) { + throw new Error( + 'Share id must be an integer between 1 and ' + idMax + ', inclusive.' + ); + } + + newShareString = idHex + data; + + return newShareString; + } + + deconstructPublicShareString(share) { + var id, + idLen, + max, + obj = {}, + regexStr, + shareComponents; + + max = this.fieldSize - 1; + + // Determine the ID length which is variable and based on the bit count. + idLen = max.toString(16).length; + + // Extract all the parts now that the segment sizes are known. + regexStr = '^([a-fA-F0-9]{' + idLen + '})([a-fA-F0-9]+)$'; + shareComponents = new RegExp(regexStr).exec(share); //first element of output array is entire share, second element is the first part, up to idLen chars, and the third element is the second component (ie the data) + + // The ID is a Hex number and needs to be converted to an Integer + if (shareComponents) { + id = parseInt(shareComponents[1]); + } + + if (typeof id !== 'number' || id % 1 !== 0 || id < 1 || id > max) { + throw new Error( + 'Invalid share : Share id must be an integer between 1 and ' + + max + + ', inclusive.' + ); + } + + if (shareComponents && shareComponents[2]) { + obj.id = id; + obj.data = shareComponents[2]; + return obj; + } + + throw new Error('The share data provided is invalid : ' + share); + } +} diff --git a/lib/sssa.test.js b/lib/sssa.test.js new file mode 100644 index 0000000..84f7d8e --- /dev/null +++ b/lib/sssa.test.js @@ -0,0 +1,32 @@ +import { SSSA } from './sssa.js'; + +describe('horner method of polynomial evaluation', () => { + it('evaluates 7x + 1 at x=2 in GF(8)', () => { + var sssa = new SSSA(3); + var coeffs = ['111', '1']; + var xCoordinate = 2; + var expectedValue = 5; + expect(sssa.horner(xCoordinate, coeffs)).toBe(expectedValue); + }); + it('evalutes 5x^2 + 4x + 3 at x=3 in GF(8)', () => { + var sssa = new SSSA(3); + var coeffs = ['101', '100', '11']; + var xCoordinate = 3; + var expectedValue = 6; + expect(sssa.horner(xCoordinate, coeffs)).toBe(expectedValue); + }); +}); +describe('public share construction and deconstruction', () => { + it('combines x-coordinate and corresponding data into one string', () => { + var sssa = new SSSA(3); + var xCoordinate = 1; //new Number(1).toString(16); + var data = 'afd'; + var publicShareString = sssa.constructPublicShareString(xCoordinate, data); + var deconstructedShareString = sssa.deconstructPublicShareString( + publicShareString + ); + expect(publicShareString.length).toBe(4); + expect(deconstructedShareString.data).toBe(data); + expect(deconstructedShareString.id).toBe(parseInt(xCoordinate, 16)); + }); +}); diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..c675868 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,76 @@ +import { toByteArray } from 'base64-js'; + +export function splitBitsToIntArray(str, padLength, intSize) { + var parts = [], + i; + + if (padLength) { + str = padLeft(str, padLength); + } + + for (i = str.length; i > intSize; i -= intSize) { + parts.push(parseInt(str.slice(i - intSize, i), 2)); + } + + parts.push(parseInt(str.slice(0, i), 2)); + + return parts; +} +export function padLeft(str, endLength) { + var missing; + var pregenpadding = new Array(1024).join('0'); // Pre-generate a string of 1024 0's for use by padLeft(). + + if (str) { + missing = endLength - (str.length % endLength); + } + + if (missing !== endLength) { + return (pregenpadding + str).slice(-(missing + str.length)); + } + + return str; +} +export function base64ToBits(base64) { + return bytesToBits(toByteArray(base64)); +} +export function bytesToBits(byteArray) { + var i = 0, + len, + str = ''; + + if (byteArray) { + len = byteArray.length; + } + + while (i < len) { + //converts it to a byte array and pads left with up to 8 0's + //make sure we are adding increments of 8 bits + str = str + padLeft(byteArray[i].toString(2), 8); + i++; + } + + // return null so this result can be re-processed if the result is all 0's. + const allZeroPattern = /^0+$/; + if (str.match(allZeroPattern)) { + return null; + } + return str; +} + +export function bin2hex(str) { + var hex = '', + num, + i; + + str = padLeft(str, 4); + + for (i = str.length; i >= 4; i -= 4) { + num = parseInt(str.slice(i - 4, i), 2); + if (isNaN(num)) { + throw new Error('Invalid binary character.'); + } + hex = num.toString(16) + hex; + } + + return hex; +} diff --git a/package.json b/package.json index dd5146a..8fdc8e9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ ], "homepage": "https://github.com/rh389/react-native-securerandom#readme", "dependencies": { - "base64-js": "*" + "base64-js": "^1.3.0", + "is-base64": "^0.1.0" }, "devDependencies": { "@babel/core": "^7.1.2", diff --git a/yarn.lock b/yarn.lock index bb1dddf..1655ffa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3547,6 +3547,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-base64@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-base64/-/is-base64-0.1.0.tgz#a6f20610c6ef4863a51cba32bc0222544b932622" + integrity sha512-WRRyllsGXJM7ZN7gPTCCQ/6wNPTRDwiWdPK66l5sJzcU/oOzcIcRRf0Rux8bkpox/1yjt0F6VJRsQOIG2qz5sg== + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"