} an map of {@code n} parts that are arrays of bytes of the
+ * secret length
+ */
+function split(randomBytes, n, k, secret) {
+ if (k <= 1) throw new Error('K must be > 1');
+ if (n < k) throw new Error('N must be >= K');
+ if (n > 255) throw new Error('N must be <= 255');
+
+ const values = new Array(n)
+ .fill(0)
+ .map(() => new Uint8Array(secret.length).fill(0));
+ // eslint-disable-next-line no-plusplus
+ for (let i = 0; i < secret.length; i++) {
+ const p = GF256.generate(randomBytes, k - 1, secret[i]);
+ // eslint-disable-next-line no-plusplus
+ for (let x = 1; x <= n; x++) {
+ values[x - 1][i] = GF256.eval(p, x);
+ }
+ }
+
+ const parts = {};
+
+ // eslint-disable-next-line no-plusplus
+ for (let i = 0; i < values.length; i++) {
+ const part = `${i + 1}`;
+ parts[part] = values[i];
+ }
+
+ return parts;
+}
+
+exports.split = split;
+
+/**
+ * Joins the given parts to recover the original secret.
+ *
+ * N.B.: There is no way to determine whether or not the returned value is actually the
+ * original secret. If the parts are incorrect, or are under the threshold value used to split the
+ * secret, a random value will be returned.
+ *
+ * @param {Object.} parts an map of {@code n} parts that are arrays of bytes
+ * of the secret length
+ * @return {Uint8Array} the original secret
+ *
+ */
+function join(parts) {
+ if (Object.keys(parts).length === 0) throw new Error('No parts provided');
+ const lengths = Object.values(parts).map(x => x.length);
+ const max = Math.max.apply(null, lengths);
+ const min = Math.min.apply(null, lengths);
+ if (max !== min) {
+ throw new Error(`Varying lengths of part values. Min ${min}, Max ${max}`);
+ }
+ const secret = new Uint8Array(max);
+ // eslint-disable-next-line no-plusplus
+ for (let i = 0; i < secret.length; i++) {
+ const keys = Object.keys(parts);
+ const points = new Array(keys.length)
+ .fill(0)
+ .map(() => new Uint8Array(2).fill(0));
+ // eslint-disable-next-line no-plusplus
+ for (let j = 0; j < keys.length; j++) {
+ const key = keys[j];
+ const k = Number(key);
+ points[j][0] = k;
+ points[j][1] = parts[key][i];
+ }
+ secret[i] = GF256.interpolate(points);
+ }
+
+ return secret;
+}
+
+exports.join = join;
diff --git a/src/test/java/com/codahale/shamir/polygot/JavaScriptUtils.java b/src/test/java/com/codahale/shamir/polygot/JavaScriptUtils.java
new file mode 100644
index 0000000..dbff7a1
--- /dev/null
+++ b/src/test/java/com/codahale/shamir/polygot/JavaScriptUtils.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright © 2017 Coda Hale (coda.hale@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.codahale.shamir.polygot;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+
+/**
+ * The GraalJS internals cannot be fooled into casting between unsigned JavaScript bytes and signed
+ * Java bytes. We only ever want to do this within unit tests to compare arrays in-memory. This
+ * class uses an inefficent workaround of building bidirectional maps. The keys are Integer to fit a
+ * signed byte.
+ */
+public class JavaScriptUtils {
+ // https://stackoverflow.com/a/12310078/329496
+ public static String byteToBinaryString(final byte b) {
+ return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0');
+ }
+
+ static Map signedToUnsignedByteMap() {
+ final Map result = new HashMap<>();
+ for (byte b = Byte.MIN_VALUE; ; b++) {
+ final String bits = byteToBinaryString(b);
+ final Integer i = Integer.parseInt(bits, 2) & 0xff;
+ result.put((int) b, i);
+ // here we avoid overflow on b++ causing an infinit loop
+ if (b == Byte.MAX_VALUE) break;
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ public static Map signedToUnsignedByteMap = signedToUnsignedByteMap();
+
+ public static Map unsignedToSignedByteMap =
+ signedToUnsignedByteMap.entrySet().stream()
+ .collect(Collectors.toMap(Entry::getValue, Entry::getKey));;
+}
diff --git a/src/test/java/com/codahale/shamir/polygot/NotRandomSource.java b/src/test/java/com/codahale/shamir/polygot/NotRandomSource.java
new file mode 100644
index 0000000..51798aa
--- /dev/null
+++ b/src/test/java/com/codahale/shamir/polygot/NotRandomSource.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright © 2017 Coda Hale (coda.hale@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.codahale.shamir.polygot;
+
+import java.security.SecureRandom;
+
+/** This class ensures that unit tests can repeatedly initalise with the same polynomial. */
+public class NotRandomSource extends SecureRandom {
+
+ @Override
+ public void nextBytes(byte[] bytes) {
+ for (int b = 0; b < bytes.length; b++) {
+ bytes[b] = (byte) (b + 1);
+ }
+ }
+
+ public byte[] notRandomBytes(int len) {
+ byte[] bytes = new byte[len];
+ this.nextBytes(bytes);
+ return bytes;
+ }
+}
diff --git a/src/test/java/com/codahale/shamir/polygot/package-info.java b/src/test/java/com/codahale/shamir/polygot/package-info.java
new file mode 100644
index 0000000..064fdde
--- /dev/null
+++ b/src/test/java/com/codahale/shamir/polygot/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright © 2017 Coda Hale (coda.hale@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*
+ * This package provides classes to aid testing JavaScript against Java within
+ * the same JVM using GraalJS.
+ */
+package com.codahale.shamir.polygot;
diff --git a/src/test/js/Benchmark.js b/src/test/js/Benchmark.js
new file mode 100644
index 0000000..f90493b
--- /dev/null
+++ b/src/test/js/Benchmark.js
@@ -0,0 +1,53 @@
+const { split, join } = require('../../main/js/Scheme.js');
+
+const { randomBytes } = require('crypto');
+
+const secret = new Uint8Array(16);
+
+for (let i = 0; i < secret.length; i++) {
+ secret[i] = i % 255;
+}
+
+const mainrun = 200;
+
+const parts = 4;
+const quorum = 3;
+
+var splits = split(randomBytes, parts, quorum, secret);
+
+function benchmarkSplit() {
+ for( let i = 0; i < mainrun; i++ ) {
+ splits = split(randomBytes, parts, quorum, secret);
+ }
+}
+
+// https://nodejs.org/docs/latest-v10.x/api/perf_hooks.html#perf_hooks_performance_now
+const {
+ performance,
+ PerformanceObserver
+} = require('perf_hooks');
+const wrappedSplit = performance.timerify(benchmarkSplit);
+const obsSplit = new PerformanceObserver((list) => {
+ const timed = list.getEntries()[0].duration / mainrun;
+ console.log(`split ${timed} ms`);
+ obsSplit.disconnect();
+});
+obsSplit.observe({ entryTypes: ['function'] });
+wrappedSplit();
+
+var recovered = join(splits);
+
+function benchmarkJoin() {
+ for( let i = 0; i < mainrun; i++ ) {
+ recovered = join(splits);
+ }
+}
+
+const wrappedJoin = performance.timerify(benchmarkJoin);
+const obsJoin = new PerformanceObserver((list) => {
+ const timed = list.getEntries()[0].duration / mainrun
+ console.log(`join ${timed} ms`);
+ obsJoin.disconnect();
+});
+obsJoin.observe({ entryTypes: ['function'] });
+wrappedJoin();
diff --git a/src/test/js/GF256Tests.js b/src/test/js/GF256Tests.js
new file mode 100644
index 0000000..45784f5
--- /dev/null
+++ b/src/test/js/GF256Tests.js
@@ -0,0 +1,159 @@
+/*
+ * Copyright © 2019 Simon Massey (massey1905@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const test = require('tape');
+const { randomBytes } = require('crypto');
+
+const GF256 = require('../../main/js/GF256.js');
+
+/* eslint-disable prefer-arrow-callback */
+/* eslint-disable func-names */
+/* eslint-disable no-plusplus */
+test('GF256Tests add', function (t) {
+ t.plan(1);
+ t.equal(GF256.add(100, 30), 122);
+});
+
+test('GF256Tests sub', function (t) {
+ t.plan(1);
+ t.equal(GF256.sub(100, 30), 122);
+});
+
+test('GF256Tests mul', function (t) {
+ t.plan(4);
+ t.equal(GF256.mul(90, 21), 254);
+ t.equal(GF256.mul(133, 5), 167);
+ t.equal(GF256.mul(0, 21), 0);
+ t.equal(GF256.mul(0xb6, 0x53), 0x36);
+});
+
+test('GF256Tests div', function (t) {
+ t.plan(4);
+ t.equal(GF256.div(90, 21), 189);
+ t.equal(GF256.div(6, 55), 151);
+ t.equal(GF256.div(22, 192), 138);
+ t.equal(GF256.div(0, 192), 0);
+});
+
+test('GF256Tests degree', function (t) {
+ t.plan(4);
+ t.equal(GF256.degree([1, 2]), 1);
+ t.equal(GF256.degree([1, 2, 0]), 1);
+ t.equal(GF256.degree([1, 2, 3]), 2);
+ t.equal(GF256.degree([0, 0, 0]), 0);
+});
+
+
+const bytes = new Uint8Array(function () {
+ const returned = [];
+ for (let i = 1; i <= 255; i++) {
+ returned.push(i);
+ }
+ return returned;
+}());
+
+function makePairs(arr) {
+ const res = [];
+ const l = arr.length;
+ for (let i = 0; i < l; ++i) {
+ for (let j = i + 1; j < l; ++j) res.push([arr[i], arr[j]]);
+ }
+ return res;
+}
+
+const pairs = makePairs(bytes);
+
+test('GF256Tests mul is commutative', function (t) {
+ pairs.forEach(function (pair) {
+ if (GF256.mul(pair[0], pair[1]) !== GF256.mul(pair[1], pair[0])) {
+ throw new Error(`mul not commutative for pair ${pair}`);
+ }
+ });
+ t.end();
+});
+
+test('GF256Tests add is commutative', function (t) {
+ pairs.forEach(function (pair) {
+ if (GF256.add(pair[0], pair[1]) !== GF256.add(pair[1], pair[0])) {
+ throw new Error(`add not commutitive for pair ${pair}`);
+ }
+ });
+ t.end();
+});
+
+test('GF256Tests sub is the inverse of add', function (t) {
+ pairs.forEach(function (pair) {
+ if (GF256.sub(GF256.add(pair[0], pair[1]), pair[1]) !== pair[0]) {
+ throw new Error(`sub is not the inverse of add for pair ${pair}`);
+ }
+ });
+ t.end();
+});
+
+test('GF256Tests div is the inverse of mul', function (t) {
+ pairs.forEach(function (pair) {
+ if (GF256.div(GF256.mul(pair[0], pair[1]), pair[1]) !== pair[0]) {
+ throw new Error(`div is not the inverse of mul for pair ${pair}`);
+ }
+ });
+ t.end();
+});
+
+test('GF256Tests mul is the inverse of div', function (t) {
+ pairs.forEach(function (pair) {
+ if (GF256.mul(GF256.div(pair[0], pair[1]), pair[1]) !== pair[0]) {
+ throw new Error(`mul is not the inverse of div for pair ${pair}`);
+ }
+ });
+ t.end();
+});
+
+test('GF256Tests eval', function (t) {
+ t.equal(GF256.eval([1, 0, 2, 3], 2), 17);
+ t.end();
+});
+
+test('GF256Tests interpolate', function (t) {
+ t.equal(GF256.interpolate([[1, 1], [2, 2], [3, 3]]), 0);
+ t.equal(GF256.interpolate([[1, 80], [2, 90], [3, 20]]), 30);
+ t.equal(GF256.interpolate([[1, 43], [2, 22], [3, 86]]), 107);
+ t.end();
+});
+
+let countDownFrom2 = 2;
+
+const zeroLastByteFirstTWoAttemptsRandomBytes = function (length) {
+ const p = randomBytes(length);
+ if (countDownFrom2 >= 0) {
+ p[p.length - 1] = 0;
+ countDownFrom2--;
+ }
+ return p;
+};
+
+test('GF256Tests generate', function (t) {
+ const p = GF256.generate(zeroLastByteFirstTWoAttemptsRandomBytes, 5, 20);
+ t.equal(p[0], 20);
+ t.equal(p.length, 6);
+ t.notOk(p[p.length - 1] === 0);
+ t.end();
+});
+
+test('GF256Tests eval', function (t) {
+ t.plan(1);
+ const v = GF256.eval([1, 0, 2, 3], 2);
+ t.equal(v, 17);
+});
diff --git a/src/test/js/PolygotTests.js b/src/test/js/PolygotTests.js
new file mode 100644
index 0000000..1e1a8d4
--- /dev/null
+++ b/src/test/js/PolygotTests.js
@@ -0,0 +1,226 @@
+/*
+ * Copyright © 2019 Simon Massey (massey1905@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const test = require('tape-catch');
+const { randomBytes } = require('crypto');
+
+/* eslint-disable prefer-arrow-callback */
+/* eslint-disable func-names */
+/* eslint-disable no-plusplus */
+/* eslint-disable no-bitwise */
+
+// https://codereview.stackexchange.com/a/3589/75693
+function bytesToSring(bytes) {
+ const chars = [];
+ for (let i = 0, n = bytes.length; i < n;) {
+ chars.push(((bytes[i++] & 0xff) << 8) | (bytes[i++] & 0xff));
+ }
+ return String.fromCharCode.apply(null, chars);
+}
+
+// https://codereview.stackexchange.com/a/3589/75693
+function stringToBytes(str) {
+ const bytes = [];
+ for (let i = 0, n = str.length; i < n; i++) {
+ const char = str.charCodeAt(i);
+ bytes.push(char >>> 8, char & 0xff);
+ }
+ return bytes;
+}
+
+// eslint-disable-next-line no-undef
+const ByteArray = Java.type('byte[]');
+// eslint-disable-next-line no-undef
+const JavaScriptUtils = Java.type('com.codahale.shamir.polygot.JavaScriptUtils');
+
+function jsByteArrayToJavaByteArray(jsarray) {
+ const jbarray = new ByteArray(jsarray.length);
+ for (let i = 0; i < jsarray.length; i++) {
+ jbarray[i] = JavaScriptUtils.unsignedToSignedByteMap.get(jsarray[i]);
+ }
+ return jbarray;
+}
+
+function jBytesArrayToJavaScriptByteArray(barray) {
+ const jsarray = [];
+ for (let i = 0; i < barray.length; i++) {
+ jsarray.push(JavaScriptUtils.signedToUnsignedByteMap.get(barray[i]));
+ }
+ return jsarray;
+}
+
+test('PolygotTests Java and JavaScript byte array roundtrip', function (t) {
+ t.plan(1);
+ const secretUtf8 = 'ᚠᛇᚻ';
+ const jsBytes = stringToBytes(secretUtf8);
+ const jbytes = jsByteArrayToJavaByteArray(jsBytes);
+ const jsRoundTripBytes = jBytesArrayToJavaScriptByteArray(jbytes);
+ t.equal(secretUtf8, bytesToSring(jsRoundTripBytes));
+});
+
+// eslint-disable-next-line no-undef
+const Scheme = Java.type('com.codahale.shamir.Scheme');
+// eslint-disable-next-line no-undef
+const SecureRandom = Java.type('java.security.SecureRandom');
+const secureRandom = new SecureRandom();
+
+test('PolygotTests JavaScript strings with all Java logic', function (t) {
+ t.plan(1);
+ const scheme = new Scheme(secureRandom, 5, 3);
+ const secretUtf8 = 'ᚠᛇᚻ';
+
+ const parts = scheme.split(jsByteArrayToJavaByteArray(stringToBytes(secretUtf8)));
+ const joined = scheme.join(parts);
+
+ t.equal(secretUtf8, bytesToSring(jBytesArrayToJavaScriptByteArray(joined)));
+});
+
+const { split } = require('../../main/js/Scheme.js');
+
+// eslint-disable-next-line no-undef
+const HashMap = Java.type('java.util.HashMap');
+
+function javaScriptToJavaParts(parts) {
+ const map = new HashMap();
+ // eslint-disable-next-line no-restricted-syntax, guard-for-in
+ for (const key in parts) {
+ const bytes = parts[key];
+ const jbarr = jsByteArrayToJavaByteArray(bytes);
+ map.put(Number(key), jbarr);
+ }
+ return map;
+}
+
+function javaToJavaScriptParts(javaMap) {
+ const result = {};
+ const entrySetIterator = javaMap.entrySet().iterator();
+ while (entrySetIterator.hasNext()) {
+ const pair = entrySetIterator.next();
+ const key = pair.getKey();
+ const value = pair.getValue();
+ result[key] = jBytesArrayToJavaScriptByteArray(value);
+ }
+ return result;
+}
+
+// eslint-disable-next-line no-undef
+const Collectors = Java.type('java.util.stream.Collectors');
+
+function equalParts(jParts, jsParts) {
+ // js keys are strings
+ const jsKeysNumbers = Object.keys(jsParts);
+ // j keys are java integers that we map to strings
+ const jKeysSet = jParts
+ .keySet()
+ .stream()
+ .map(n => n.toString())
+ .collect(Collectors.toList());
+
+ // check that all js keys are in the j keys
+ // eslint-disable-next-line no-restricted-syntax
+ for (const jsk of jsKeysNumbers) {
+ if (!jKeysSet.contains(jsk)) {
+ throw new Error(`jKeysSet ${jKeysSet} does not contain jsk ${jsk}`);
+ }
+ }
+
+ // check that all j keys are in the js keys
+ // eslint-disable-next-line no-restricted-syntax
+ for (const jk of jKeysSet) {
+ if (!jsKeysNumbers.includes(jk)) {
+ throw new Error(`jsKeysNumbers ${jsKeysNumbers} does not contain jk ${jk}`);
+ }
+ }
+
+ // eslint-disable-next-line no-restricted-syntax
+ for (const k of Object.keys(jsParts)) {
+ const jArray = jBytesArrayToJavaScriptByteArray(jParts.get(Number(k)));
+ const jsArray = jsParts[k];
+ if (jArray.length !== jsArray.length) {
+ throw new Error(`unequal lengths ${jArray.length} != ${jsArray.length}`);
+ }
+ for (let l = 0; l < jArray.length; l++) {
+ if (jArray[l] !== jsArray[l]) {
+ throw new Error(`at index ${l}: ${jArray[l]} != ${jsArray[l]}`);
+ }
+ }
+ }
+
+ return true;
+}
+
+test('PolygotTests roundrip parts between JavaScript and Java', function (t) {
+ t.plan(1);
+ const secretUtf8 = 'ᚠᛇᚻ';
+ const secret = stringToBytes(secretUtf8);
+ const parts = split(randomBytes, 3, 2, secret);
+ const jParts = javaScriptToJavaParts(parts);
+ const jsParts = javaToJavaScriptParts(jParts);
+ t.ok(equalParts(jParts, jsParts), 'roundtrip parts');
+});
+
+// eslint-disable-next-line no-undef
+const NotRandomSource = Java.type('com.codahale.shamir.polygot.NotRandomSource');
+const notRandomSource = new NotRandomSource();
+function notRandomSourceJavaScript(len) {
+ const bytes = [];
+ for (let i = 0; i < len; i++) {
+ bytes[i] = i + 1;
+ }
+ return bytes;
+}
+
+test('PolygotTests compare Java and JavaScript split', function (t) {
+ t.plan(1);
+ const secretUtf8 = 'ᚠᛇᚻ';
+ const secret = stringToBytes(secretUtf8);
+
+ const jsParts = split(notRandomSourceJavaScript, 3, 2, secret);
+
+ const jscheme = new Scheme(notRandomSource, 3, 2);
+ const jParts = jscheme.split(jsByteArrayToJavaByteArray(secret));
+ t.ok(equalParts(jParts, jsParts), 'splits match');
+});
+
+test('PolygotTests JavaScript split with Java join', function (t) {
+ t.plan(1);
+ const secretUtf8 = 'ᚠᛇᚻ';
+
+ const secret = stringToBytes(secretUtf8);
+ const jsParts = split(randomBytes, 3, 2, secret);
+ const jscheme = new Scheme(secureRandom, 3, 2);
+ const joined = jscheme.join(javaScriptToJavaParts(jsParts));
+
+ t.equal(
+ bytesToSring(jBytesArrayToJavaScriptByteArray(joined)),
+ secretUtf8,
+ 'java joined js parts',
+ );
+});
+
+const { join } = require('../../main/js/Scheme.js');
+
+test('PolygotTests Java split with JavaScript join', function (t) {
+ t.plan(1);
+ const secretUtf8 = 'ᚠᛇᚻ';
+
+ const secret = stringToBytes(secretUtf8);
+ const jscheme = new Scheme(secureRandom, 3, 2);
+ const jParts = jscheme.split(jsByteArrayToJavaByteArray(secret));
+ const joined = join(javaToJavaScriptParts(jParts));
+
+ t.equal(bytesToSring(joined), secretUtf8, 'java joined js parts');
+});
diff --git a/src/test/js/SchemeTests.js b/src/test/js/SchemeTests.js
new file mode 100644
index 0000000..373c4ae
--- /dev/null
+++ b/src/test/js/SchemeTests.js
@@ -0,0 +1,140 @@
+/*
+ * Copyright © 2019 Simon Massey (massey1905@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const test = require('tape');
+
+const { split, join } = require('../../main/js/Scheme.js');
+
+const { randomBytes } = require('crypto');
+
+/* eslint-disable prefer-arrow-callback */
+/* eslint-disable func-names */
+/* eslint-disable no-plusplus */
+/* eslint-disable no-bitwise */
+
+// https://codereview.stackexchange.com/a/3589/75693
+function bytesToSring(bytes) {
+ const chars = [];
+ for (let i = 0, n = bytes.length; i < n;) {
+ chars.push(((bytes[i++] & 0xff) << 8) | (bytes[i++] & 0xff));
+ }
+ return String.fromCharCode.apply(null, chars);
+}
+
+
+// https://codereview.stackexchange.com/a/3589/75693
+function stringToBytes(str) {
+ const bytes = [];
+ for (let i = 0, n = str.length; i < n; i++) {
+ const char = str.charCodeAt(i);
+ bytes.push(char >>> 8, char & 0xff);
+ }
+ return bytes;
+}
+
+test('SchemeTests roundtrip', function (t) {
+ const parts = 5;
+ const quorum = 3;
+
+ // http://kermitproject.org/utf8.html
+ // From the Anglo-Saxon Rune Poem (Rune version)
+ const secretUtf8 = `ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ
+ᛋᚳᛖᚪᛚ᛫ᚦᛖᚪᚻ᛫ᛗᚪᚾᚾᚪ᛫ᚷᛖᚻᚹᛦᛚᚳ᛫ᛗᛁᚳᛚᚢᚾ᛫ᚻᛦᛏ᛫ᛞᚫᛚᚪᚾ
+ᚷᛁᚠ᛫ᚻᛖ᛫ᚹᛁᛚᛖ᛫ᚠᚩᚱ᛫ᛞᚱᛁᚻᛏᚾᛖ᛫ᛞᚩᛗᛖᛋ᛫ᚻᛚᛇᛏᚪᚾ᛬`;
+
+ // string length is only 117 characters but the byte length is 234.
+ const secret = stringToBytes(secretUtf8);
+
+ const splits = split(randomBytes, parts, quorum, secret);
+ // only quorum parts are necessary
+
+ delete splits['2'];
+ delete splits['3'];
+
+ const joined = join(splits);
+ t.equal(joined[200], secret[200]);
+ t.equal(joined[201], secret[201]);
+ const joinedUtf8 = bytesToSring(joined);
+ t.equal(secretUtf8, joinedUtf8);
+ t.end();
+});
+
+test('SchemeTests roundtrip two parts', function (t) {
+ const parts = 3;
+ const quorum = 2;
+
+ const secretUtf8 = 'ᚠᛇᚻ';
+ const secret = stringToBytes(secretUtf8);
+
+ for (let i = 1; i <= 3; i++) {
+ const splits = split(randomBytes, parts, quorum, secret);
+ delete splits[`${i}`];
+ const joinedUtf8 = bytesToSring(join(splits));
+ t.equal(secretUtf8, joinedUtf8);
+ }
+
+ t.end();
+});
+
+test('SchemeTests split input validation', function (t) {
+ const secretUtf8 = 'ᚠᛇᚻ';
+ const secret = stringToBytes(secretUtf8);
+
+ t.plan(3);
+
+ try {
+ split(randomBytes, 256, 2, secret);
+ t.notOk(true);
+ } catch (e) {
+ t.ok(e.toString().includes('N must be <= 255'), e);
+ }
+
+ try {
+ split(randomBytes, 3, 1, secret);
+ t.notOk(true);
+ } catch (e) {
+ t.ok(e.toString().includes('K must be > 1'), e);
+ }
+
+ try {
+ split(randomBytes, 2, 3, secret);
+ t.notOk(true);
+ } catch (e) {
+ t.ok(e.toString().includes('N must be >= K'), e);
+ }
+
+ t.end();
+});
+
+test('SchemeTests join input validation', function (t) {
+ try {
+ join({});
+ t.notOk(true);
+ } catch (e) {
+ t.ok(e.toString().includes('No parts provided'), e);
+ }
+
+ try {
+ const splits = split(randomBytes, 3, 2, stringToBytes('ᚠᛇᚻ'));
+ splits['2'] = Uint8Array.of(216, 30, 190, 102);
+ join(splits);
+ t.notOk(true);
+ } catch (e) {
+ t.ok(e.toString().includes('Varying lengths of part values'), e);
+ }
+
+ t.end();
+});
diff --git a/src/test/js/TieredSharing.js b/src/test/js/TieredSharing.js
new file mode 100644
index 0000000..a59665d
--- /dev/null
+++ b/src/test/js/TieredSharing.js
@@ -0,0 +1,81 @@
+/*
+ * Copyright © 2019 Simon Massey (massey1905@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const test = require('tape');
+
+const { split, join } = require('../../main/js/Scheme.js');
+
+const { randomBytes } = require('crypto');
+
+/* eslint-disable prefer-arrow-callback */
+/* eslint-disable func-names */
+/* eslint-disable no-plusplus */
+/* eslint-disable no-bitwise */
+
+// https://codereview.stackexchange.com/a/3589/75693
+function bytesToSring(bytes) {
+ const chars = [];
+ for (let i = 0, n = bytes.length; i < n;) {
+ chars.push(((bytes[i++] & 0xff) << 8) | (bytes[i++] & 0xff));
+ }
+ return String.fromCharCode.apply(null, chars);
+}
+
+
+// https://codereview.stackexchange.com/a/3589/75693
+function stringToBytes(str) {
+ const bytes = [];
+ for (let i = 0, n = str.length; i < n; i++) {
+ const char = str.charCodeAt(i);
+ bytes.push(char >>> 8, char & 0xff);
+ }
+ return bytes;
+}
+
+test('TieredSharing roundtrip', function (t) {
+
+ const secretUtf8 = 'ᚠᛇᚻ';
+ const secret = stringToBytes(secretUtf8);
+
+ const adminParts = 3;
+ const adminQuorum = 2;
+ const adminSplits = split(randomBytes, adminParts, adminQuorum, secret);
+
+ const userParts = 4;
+ const userQuorum = 3;
+ const usersSplits = split(randomBytes, userParts, userQuorum, adminSplits['3'] );
+
+ // throw away third share that is split into 4 user parts
+ delete adminSplits['3'];
+
+ // reconstruct the secret with two admin shares
+ const joinedAdminShares = join(adminSplits);
+ t.equal(secretUtf8, bytesToSring(joinedAdminShares));
+
+ // throw away second admin share we only have one remaining
+ delete adminSplits['2'];
+ // throw away one user share as we only need three
+ delete usersSplits['1'];
+
+ // reconstruct the third admin share from the three user shares
+ const joinedUserShares = join(usersSplits);
+
+ // use the first admin share and the recovered third share
+ const mixedShares = { '1': adminSplits['1'], '3': joinedUserShares };
+ const joinedMixedShares = join(mixedShares);
+ t.equal(secretUtf8, bytesToSring(joinedMixedShares));
+ t.end();
+});
diff --git a/test_js_against_java.sh b/test_js_against_java.sh
new file mode 100755
index 0000000..08e25bf
--- /dev/null
+++ b/test_js_against_java.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+#
+# Copyright © 2017 Coda Hale (coda.hale@gmail.com)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# This tests the
+docker build -f Dockerfile.graaljs .
\ No newline at end of file