ara-crypto
Version:
Cryptographic functions used in Ara modules
378 lines (316 loc) • 8.99 kB
JavaScript
/* eslint-disable no-inline-comments */
const isBuffer = require('is-buffer')
const { randomBytes } = require('../random-bytes')
const { SharePoint } = require('./share')
const { blake2b } = require('../blake2b')
const kZeroes = new Array(1024).join('0')
/**
* A class for encoding various values used in the secret
* sharing context.
* @public
* @class Codec
*/
class Codec {
/**
* Codec class constructor.
* @public
* @constructor
* @param {Context} ctx
*/
constructor(ctx) {
this.ctx = ctx
}
/**
* Generate a fixed size random number as a BLAKE2b hash
* based on context entropy and a fixed size PRNG.
* @public
* @param {Number} size
* @return {Buffer}
* @throws TypeError
* @throws RangeError
*/
random(size) {
const seed = Buffer.concat([
this.ctx.entropy,
randomBytes(Math.max(size, 8))
])
const value = Buffer.alloc(size)
value.fill(blake2b(seed))
return value
}
/**
* Pad left side of a string with '0' until length is
* a given multiple of bits defaulting to context bits.
* @public
* @param {String} string
* @param {?(Number)} [multiple this.ctx.bits]
* @returns String
*/
pad(string, multiple) {
const { bits } = this.ctx
let missing = 0
let result = string
if (!multiple) {
// eslint-disable-next-line no-param-reassign
multiple = bits
}
if (string) {
missing = string.length % multiple
}
if (missing) {
const { length } = string
result = (kZeroes + string).slice(-((multiple - missing) + length))
}
return result
}
/**
* Split a string into parts by the bits set for
* the context with an optional padding.
* @public
* @param {String} string
* @param {Number} padding
* @param {Number} radix
* @return {Array<Number>}
*/
split(string, padding, radix) {
const parts = []
const { bits } = this.ctx
let i = 0
if (padding) {
// eslint-disable-next-line no-param-reassign
string = this.pad(string, padding)
}
for (i = string.length; i > bits; i -= bits) {
parts.push(parseInt(string.slice(i - bits, i), radix))
}
parts.push(parseInt(string.slice(0, i), radix))
return parts
}
/**
* Evaluation of `x' using Horner's method
* @see {@link https://en.wikipedia.org/wiki/Horner%27s_method}
* @public
* @param {Number} x
* @param {Array<Number>} coef
* @return {Number}
*/
horner(x, coef) {
const { logs, exps } = this.ctx.table
const { maxShares } = this.ctx
const n = maxShares
let b = 0
for (let i = coef.length - 1; i >= 0; --i) {
if (0 === b) {
b = coef[i]
} else {
// eslint-disable-next-line no-bitwise
b = exps[(logs[x] + logs[b]) % n] ^ coef[i]
}
}
return b
}
/**
* Computes the Lagrange interpolation polynomial f(x)
* using a set of x and x points.
* @public
* @param {Number} x
* @param {Object} points
* @param {Number} points.x
* @param {Number} points.y
* @return {Number}
*/
lagrange(x, points) {
const { logs, exps } = this.ctx.table
const { maxShares } = this.ctx
let sum = 0
for (let i = 0; i < points.x.length; ++i) {
if (points.y[i]) {
let product = logs[points.y[i]]
for (let j = 0; j < points.x.length; ++j) {
if (i !== j) {
if (x === points.x[j]) {
product = -1
break
}
// eslint-disable-next-line no-bitwise
const z = (logs[x ^ points.x[j]] - logs[points.x[i] ^ points.x[j]])
product = (product + z + maxShares) % maxShares
}
}
// eslint-disable-next-line no-bitwise
sum = -1 === product ? sum : sum ^ exps[product]
}
}
return sum
}
/**
* @public
* @param {Number} secret0
* @param {Object} opts
* @param {Number} opts.threshold
* @param {Number} opts.shares
* @return {Array<Object>}
* @throws TypeError
*/
partition(secret0, opts) {
const { bits } = this.ctx
// eslint-disable-next-line no-shadow
const shares = []
if (!opts || 'object' !== typeof opts) {
throw new TypeError('Codec: Expecting object.')
}
if (!opts.threshold || 'number' !== typeof opts.threshold) {
throw new TypeError('Codec: Expecting threshold to be a number.')
}
if (!opts.shares || 'number' !== typeof opts.shares) {
throw new TypeError('Codec: Expecting shares to be a number.')
}
if ('number' !== typeof secret0) {
throw new TypeError('Codec: Expecting secret0 to be a number.')
}
const coef = [ secret0 ] // f(0) = secret
for (let i = 1; i < opts.threshold; ++i) {
// convert a random value buffer of size in bits (b/8) from a hex string
// to a parsed integer of base 16
coef[i] = parseInt(this.random(bits / 8).toString('hex'), 16)
}
for (let i = 1; i < 1 + opts.shares; ++i) {
const x = i
const y = this.horner(x, coef)
shares[i - 1] = new SharePoint(this.ctx, { x, y })
}
return shares
}
/**
* @public
* @param {String|Number} id
* @param {String|Buffer} data
* @return {String}
* @throws TypeError
*/
encode(id, data) {
const { bits, radix, size } = this.ctx
// eslint-disable-next-line no-param-reassign
id = parseInt(id, radix)
const maxLength = (size - 1).toString(radix).length
// bits(36) + id(16)
const header = Buffer.concat([
Buffer.from(bits.toString(36).toUpperCase()),
Buffer.from(this.pad(id.toString(radix), maxLength))
])
if (false === isBuffer(data)) {
// eslint-disable-next-line no-param-reassign
data = Buffer.from(data)
}
return header.toString() + data.toString()
}
/**
* @public
*/
decode(buffer, encoding) {
const { bytesPerChar } = this.ctx
const padding = 2 * bytesPerChar
const offset = padding
const result = []
if (isBuffer(buffer)) {
// eslint-disable-next-line no-param-reassign
buffer = buffer.toString(encoding)
}
// eslint-disable-next-line no-param-reassign
buffer = this.pad(buffer, padding)
for (let i = 0; i < buffer.length; i += offset) {
result.unshift(decode(buffer.slice(i, i + offset), 'hex'))
}
return result.join('')
function decode(chunk) {
return String.fromCharCode(parseInt(chunk, 16))
}
}
/**
* Converts a string or buffer to a hex string padded based
* on the context `bytesPerChar'.
* @public
* @param {String|Buffer} buffer
* @param {?(String)} [encoding = utf8]
* @return {String}
*/
hex(buffer, encoding) {
const { bytesPerChar } = this.ctx
const padding = 2 * bytesPerChar
const codec = this
// eslint-disable-next-line no-param-reassign
if (!encoding) { encoding = 'utf8' }
if ('string' === typeof buffer) {
return fromString()
}
if (isBuffer(buffer)) {
return fromBuffer()
}
throw new TypeError('Codec: Unsupported input type.')
function fromBuffer() {
const chunks = []
for (let i = 0; i < buffer.length; ++i) {
chunks.unshift(codec.pad(buffer[i].toString(16), padding))
}
return chunks.join('')
}
function fromString() {
if ('utf8' === encoding) {
return fromUTF8String()
} if ('binary' === encoding) {
return fromBinaryString()
}
throw new TypeError('Codec: Unsupported encoding type.')
}
function fromUTF8String() {
const chunks = []
for (let i = 0; i < buffer.length; ++i) {
chunks.unshift(codec.pad(char(buffer[i]).toString(16), padding))
}
return chunks.join('')
}
function fromBinaryString() {
const chunks = []
// eslint-disable-next-line no-param-reassign
buffer = codec.pad(buffer, 4)
for (let i = buffer.length; i >= 4; i -= 4) {
const chunk = parseInt(buffer.slice(i - 4, i), 2)
chunks.unshift(chunk.toString(16))
}
return chunks.join('')
}
function char(c) {
return c.charCodeAt(0)
}
}
/**
* @public
* @param {Buffer} buffer
* @return {String}
*/
binary(buffer, radix) {
const chunks = []
// eslint-disable-next-line no-param-reassign
if (!radix) { radix = 16 }
for (let i = buffer.length - 1; i >= 0; --i) {
let chunk = null
if (isBuffer(buffer)) {
chunk = buffer[i]
} else if ('string' === typeof buffer) {
chunk = parseInt(buffer[i], radix)
} else if (Array.isArray(buffer)) {
chunk = buffer[i]
if ('string' === typeof chunk) {
chunk = parseInt(chunk, radix)
}
} else {
throw new TypeError('')
}
chunks.unshift(this.pad(chunk.toString(2), 4))
}
return chunks.join('')
}
}
module.exports = {
Codec
}