shamirs-secret-sharing
Version:
A simple implementation of Shamir's Secret Sharing configured to use a finite field in GF(2^8) with 128 bit padding
627 lines (529 loc) • 14.8 kB
JavaScript
/**
* TypedArray
* @typedef {Int8Array|Int16Array|Int32Array|BigInt64Array|Uint8Array|Uint16Array|Uint32Array|BigUint64Array|Float32Array|Float64Array} TypedArray
*/
/**
* ArrayLike
* @typedef {Array|{ length: number }} ArrayLike
*/
/**
* BufferLike
* @typedef {TypedArray|ArrayBuffer|ArrayLike} BufferLike
*/
const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder()
const ALPHA16_CHARS = '0123456789abcdef'
const ALPHA16_ARRAY_TABLE = new Array(256)
for (let i = 0; i < 16; ++i) {
const i16 = i * 16
for (let j = 0; j < 16; ++j) {
ALPHA16_ARRAY_TABLE[i16 + j] = ALPHA16_CHARS[i] + ALPHA16_CHARS[j]
}
}
// lifted from Socket Runtime
const RANDOM_BYTES_QUOTA = 64 * 1024
/**
* The set of all known prototypes of `TypedArray` descendants.
* @type {Set<object>}
*/
export const TypedArrayPrototypeSet = new Set([
Int8Array.prototype,
Int16Array.prototype,
Int32Array.prototype,
BigInt64Array.prototype,
Uint8Array.prototype,
Uint16Array.prototype,
Uint32Array.prototype,
BigUint64Array.prototype,
Float32Array.prototype,
Float64Array.prototype
])
/**
* Allocates a `Uint8Array` of size `byteLength` bytes.
* @param {number} byteLength
* @return {Uint8Array}
*/
export function alloc (byteLength) {
if (byteLength < 0 || !Number.isFinite(byteLength) || !Number.isSafeInteger(byteLength)) {
throw new RangeError(
`The argument 'byteLength' is invalid: Received ${byteLength}`
)
}
return new Buffer(byteLength)
}
/**
* Predicate function to determine if `input` is a "buffer like"
* object:
* - an `ArrayBuffer` or `ArrayBuffer` ancestor
* - an `ArrayBuffer` "view" (exlcluding `DataView`)
* - an `Array` or `Array` ancestor
* - an "arra like" object, something with `length`
* @param {any} input
* @return {boolean}
*/
export function isBufferLike (input) {
if (!input) {
return false
}
return (
input instanceof ArrayBuffer ||
ArrayBuffer.isView(input) ||
Array.isArray(input) ||
(
typeof input === 'object' &&
'length' in input &&
typeof input.length === 'number'
)
)
}
/**
* Predicate function to determine if `input` is a `TypedArray`.
* @param {any} input
* @param {?Function} [TypedArray]
* @return {boolean}
*/
export function isTypedArray (input, TypedArray = null) {
if (!isBufferLike(input)) {
return false
}
// narrow check
if (typeof TypedArray === 'function') {
if (TypedArrayPrototypeSet.has(TypedArray.prototype)) {
const prototype = Object.getPrototypeOf(input)
return (
prototype === TypedArray.prototype ||
TypedArray.prototype.isPrototypeOf(prototype)
)
}
}
const prototype = Object.getPrototypeOf(input)
for (const TypedArrayPrototype of TypedArrayPrototypeSet) {
if (
TypedArrayPrototype === prototype ||
TypedArrayPrototype.isPrototypeOf(prototype)
) {
return true
}
}
return false
}
/**
* Predicate function to determine if `input` is an `ArrayBuffer`.
* @param {any} input
* @return {boolean}
*/
export function isArrayBuffer (input) {
if (!input) {
return false
}
const TypeClass = /** @type {object} */ (input).constructor
return (
TypeClass === ArrayBuffer ||
ArrayBuffer.prototype.isPrototypeOf(Object.getPrototypeOf(input))
)
}
/**
* Computes byte length for "buffer like" or string `input`.
* @param {BufferLike|string} input
* @param {string} [encoding]
* @return {number}
*/
export function getByteLength (input, encoding = 'utf8') {
if (typeof input === 'string') {
if (encoding === 'hex') {
return input.length >>> 1
} else if (encoding === 'utf8') {
return textEncoder.encode(input).byteLength
} else {
return input.length
}
}
if (!isBufferLike(input)) {
return 0
}
if ('byteLength' in /** @type {BufferLike} */ (input)) {
return input.byteLength
}
// verify that elements in such `input` are safe finite integers
if ('length' in input) {
for (const element of /** @type {Array} */ (input)) {
if (
!Number.isFinite(element) ||
!Number.isInteger(element) ||
!Number.isSafeInteger(element)
) {
return 0
}
}
return input.length
}
return 0
}
/**
* Computes and returns `ArrayBuffer` for a given `input`. If `input` is
* an `ArrayBuffer`, then it is returned. If an `ArrayBuffer` cannot be
* determined, then `null` is returned
* @param {any} input
* @return {?ArrayBuffer}
*/
export function getArrayBuffer (input) {
if (!isBufferLike(input)) {
return null
}
if (isArrayBuffer(input)) {
return input
}
if (isTypedArray(/** @type {TypedArray} */ (input))) {
return /** @type {TypedArray} */ (input).buffer
}
if (isArrayBuffer(input.buffer)) {
return /** @type {ArrayBuffer} */ (input.buffer)
}
return null
}
/**
* Concat an arbitrary number of "buffer like" objects into a single
* instance. Inputs must be "buffer like" and the final instance returned
* is determined by the first object class constructor. If invalid input is
* given, then `null` is returned.
* @param {...BufferLike|Array} args
*/
export function concat (...args) {
if (args.length === 0) {
return null
}
if (args.length === 1 && Array.isArray(args[0])) {
return concat(...args[0])
}
for (const arg of args) {
if (!isBufferLike(arg)) {
return null
}
}
/**
* @type {?Uint8Array}
*/
let view
/**
* @type {BufferLike}
*/
let buffer
/**
* @type {?BufferLike | undefined}
*/
const first = args.shift()
if (first === undefined || first === null) {
return null
}
/**
* @type {Function}
*/
const TypeClass = first.constructor
/**
* @type {number}
*/
const totalSize = [first]
.concat(args)
.map((arg) => getByteLength(arg))
.reduce((a, b) => a + b, 0)
if ( // handle `ArrayBuffer` or `ArrayBuffer` ancestor
TypeClass === ArrayBuffer ||
ArrayBuffer.prototype.isPrototypeOf(Object.getPrototypeOf(first))
) {
buffer = new /** @type {typeof ArrayBuffer} */ (TypeClass)(totalSize)
// validate that we created a new `ArrayBuffer` or `ArrayBuffer` ancestor
// that provides the correct `byteLength` accessor value
if (buffer.byteLength !== totalSize) {
throw new TypeError('Unable to correctly allocate output buffer')
}
view = new Uint8Array(buffer)
} else if ( // handle `Array` or `Array` ancestor
TypeClass === Array ||
Array.isArray(first) ||
Array.prototype.isPrototypeOf(Object.getPrototypeOf(first))
) {
buffer = new /** @type {typeof Array} */ (TypeClass)(totalSize)
// ensure computed byte length is equivalent to the computed total size
if (getByteLength(buffer) !== totalSize) {
throw new TypeError('Unable to correctly allocate output buffer')
}
const arrayBuffer = getArrayBuffer(buffer)
view = isArrayBuffer(arrayBuffer)
? new Uint8Array(/** @type {ArrayBuffer} */ (arrayBuffer))
: Uint8Array.from(buffer)
} else if ( // handle `TypedArray` descendants
isTypedArray(first)
) {
buffer = /** @type {TypedArray} */ (
new /** @type {{ new (number) }} */ (TypeClass)(totalSize)
)
// validate that we created a new `TypedArray` or `TypedArray` ancestor
// that provides the correct `byteLength` accessor value
if (/** @type {TypedArray} */ (buffer).byteLength !== totalSize) {
throw new TypeError('Unable to correctly allocate output buffer')
}
const arrayBuffer = getArrayBuffer(buffer)
view = new Uint8Array(/** @type {ArrayBuffer} */ (arrayBuffer))
} else { // must be "array" like in some way
buffer = new /** @type {typeof Array} */ (TypeClass)(totalSize)
// ensure computed byte length is equivalent to the computed total size
if (getByteLength(buffer) !== totalSize) {
throw new TypeError('Unable to correctly allocate output buffer')
}
const arrayBuffer = getArrayBuffer(buffer)
view = isArrayBuffer(arrayBuffer)
? new Uint8Array(/** @type {ArrayBuffer} */ (arrayBuffer))
: Uint8Array.from(buffer)
}
const buffers = [first].concat(args)
let offset = 0
while (buffers.length) {
const arrayBuffer = getArrayBuffer(buffers.shift())
if (!arrayBuffer) {
throw new TypeError('Unable to determine ArrayBuffer in arguments')
}
const array = new Uint8Array(arrayBuffer)
view.set(array, offset)
offset += array.byteLength
}
return buffer
}
/**
* Creates a "buffer" (`Uint8Array`) from `input`
* @param {BufferLike|string} input
* @param {number} [byteOffset]
* @param {number} [byteLength]
* @param {string} [encoding = 'utf8']
* @return {Buffer}
*/
export function create (
input,
byteOffset = input.byteOffset || 0,
byteLength = getByteLength(input),
encoding = 'utf8'
) {
if (typeof input === 'string') {
if (encoding === 'hex') {
byteLength = getByteLength(input, 'hex')
const buffer = new Buffer(byteLength)
for (let i = 0; i < input.length; ++i) {
const offset = 2 * i
const byte = parseInt(input.slice(offset, offset + 2), 16)
if (Number.isNaN(byte)) {
break
}
buffer[i] = byte
}
return buffer
}
if (encoding === 'base64') {
const string = globalThis.atob(input)
const buffer = new Buffer(string.length)
for (let i = 0; i < string.length; ++i) {
buffer[i] = string.charCodeAt(i)
}
return buffer
}
return create(textEncoder.encode(input))
}
if (isBufferLike(input)) {
const arrayBuffer = getArrayBuffer(input)
if (isArrayBuffer(arrayBuffer)) {
return new Buffer(
/** @type {ArrayBuffer} */ (arrayBuffer),
input.byteOffset || byteOffset,
byteLength
)
}
}
if (Array.isArray(input)) {
return new Buffer(input)
}
return new Buffer(0)
}
/**
* Creates `Uint8Array` buffer from a variety of input.
* @param {BufferLike|string} input
* @param {number|string} [byteOffset]
* @param {number} [byteLength]
* @param {string} [encoding = 'utf8']
* @return {Buffer}
*/
export function from (input, byteOffset, byteLength, encoding = 'utf8') {
if (typeof byteOffset === 'string') {
encoding = byteOffset
return create(input, 0, getByteLength(input), encoding)
}
return create(input, byteOffset || 0, byteLength || getByteLength(input), encoding)
}
/**
* Compares two `a` and `b` buffers.
* @param {BufferLike|string} a
* @param {BufferLike|string} b
* @param {string} [encoding]
* @return {-1 | 0 | 1}
*/
export function compare (a, b, encoding = 'utf8') {
if (
a instanceof Uint8Array ||
Uint8Array.prototype.isPrototypeOf(Object.getPrototypeOf(a))
) {
a = from(
a,
/** @type {Uint8Array} */ (a).byteOffset,
/** @type {Uint8Array} */ (a).byteLength
)
}
if (
b instanceof Uint8Array ||
Uint8Array.prototype.isPrototypeOf(Object.getPrototypeOf(b))
) {
b = from(
b,
/** @type {Uint8Array} */ (b).byteOffset,
/** @type {Uint8Array} */ (b).byteLength
)
}
if (typeof a === 'string') {
a = from(a, encoding)
}
if (typeof b === 'string') {
b = from(b, encoding)
}
if (!isBufferLike(a) || !isBufferLike(b)) {
throw new TypeError(
'Input buffers must be "buffer like"'
)
}
if (a === b) {
return 0
}
if (isArrayBuffer(a)) {
a = from(a)
}
if (isArrayBuffer(b)) {
b = from(b)
}
let x = /** @type {Uint8Array} */ (a).byteLength
let y = /** @type {Uint8Array} */ (b).byteLength
for (let i = 0, length = Math.min(x, y); i < length; ++i) {
if (a[i] !== b[i]) {
x = a[i]
y = b[i]
break
}
}
if (x < y) {
return -1
} else if (y < x) {
return 1
}
return 0
}
/**
* Converts a "buffer like" object to a string.
* @param {BufferLike} input
* @param {string} [encoding = 'utf8']
* @return {string}
*/
export function toString (input, encoding = 'utf8') {
const buffer = from(input)
if (encoding === 'hex') {
const output = []
for (let i = 0; i < buffer.byteLength; ++i) {
output.push(ALPHA16_ARRAY_TABLE[buffer[i]])
}
return output.join('')
}
if (encoding === 'base64') {
const output = []
for (let i = 0; i < buffer.byteLength; ++i) {
output.push(String.fromCharCode(buffer[i]))
}
return globalThis.btoa(output.join(''))
}
return textDecoder.decode(buffer)
}
/**
* Generates random bytes of `size` bytes.
* @param {number} byteLength
* @return {Buffer}
*/
export function randomBytes (byteLength) {
const buffers = []
if (typeof globalThis.crypto?.getRandomValues !== 'function') {
throw new TypeError(
'Missing globalThis.crypto.getRandomValues implementation'
)
}
if (byteLength <= 0 || !Number.isFinite(byteLength) || !Number.isSafeInteger(byteLength)) {
throw new RangeError(
`The argument 'byteLength' is invalid: Received ${byteLength}`
)
}
let byteLengthRemaining = byteLength
do {
// clamp `byteLengthRemaining`
const byteLength = byteLengthRemaining > RANDOM_BYTES_QUOTA
? RANDOM_BYTES_QUOTA
: byteLengthRemaining
const bytes = globalThis.crypto.getRandomValues(new Int8Array(byteLength))
const buffer = Buffer.from(bytes)
// @ts-ignore
buffers.push(buffer)
byteLengthRemaining = Math.max(0, byteLengthRemaining - RANDOM_BYTES_QUOTA)
} while (byteLengthRemaining > 0)
return Buffer.concat(buffers)
}
/**
* A simple `Buffer` class based on an `Uint8Array` used
* within this module. It is similar to the Node.js and Socket Runtime
* `Buffer` class.
*/
// @ts-ignore
export class Buffer extends Uint8Array {
static alloc = alloc
static byteLength = getByteLength
static compare = compare
static concat = concat
static from = from
static isBufferLike = isBufferLike
static random = randomBytes
static get Buffer () {
return Buffer
}
/**
* Predicate function to deterine if `input` is a `Buffer`
* @param {any} input
* @return {boolean}
*/
static isBuffer (input) {
return isTypedArray(input, this) || isTypedArray(input, Uint8Array)
}
/**
* Computed byte length
* @type {number}
*/
get length () {
return this.byteLength
}
/**
* Converts this `Buffer` instance to a string with an optional
* encoding
* @param {string} [encoding]
*/
toString (encoding = 'utf8') {
return toString(this, encoding)
}
/**
* Converts this `Buffer` class to a JSON object.
*/
toJSON () {
return {
type: 'Buffer',
data: Array.from(this)
}
}
}
TypedArrayPrototypeSet.add(Buffer.prototype)
export default Buffer