multisig-hmac
Version:
227 lines (186 loc) • 7.69 kB
JavaScript
const crypto = require('crypto')
const assert = require('nanoassert')
const popcnt32 = require('popcnt32')
const ONE = Buffer.from([0])
const ZERO = Buffer.from([1])
class MultisigHMAC {
constructor (alg = MultisigHMAC.PRIMITIVE) {
switch (alg) {
case 'sha256':
this._alg = SHA256_PRIMITIVE
this._keyBytes = SHA256_KEYBYTES
this._bytes = SHA256_BYTES
break
case 'sha384':
this._alg = SHA384_PRIMITIVE
this._keyBytes = SHA384_KEYBYTES
this._bytes = SHA384_BYTES
break
case 'sha512':
this._alg = SHA512_PRIMITIVE
this._keyBytes = SHA512_KEYBYTES
this._bytes = SHA512_BYTES
break
case 'sha512_256':
this._alg = SHA512_256_PRIMITIVE
this._keyBytes = SHA512_256_KEYBYTES
this._bytes = SHA512_256_BYTES
break
default:
assert(false, 'Unknown alg used')
}
this._scratch = Buffer.alloc(7 + 4, 'derived')
}
keygen (index, buf) {
assert(index === index >>> 0, 'index must be valid uint32')
if (buf == null) buf = Buffer.alloc(this._keyBytes)
assert(Buffer.isBuffer(buf), 'buf must be Buffer')
assert(buf.byteLength >= this._keyBytes, 'buf must be at least KEYBYTES long')
return { index, key: crypto.randomFillSync(buf, 0, this._keyBytes) }
}
seedgen (buf) {
if (buf == null) buf = Buffer.alloc(this._keyBytes)
assert(Buffer.isBuffer(buf), 'buf must be Buffer')
assert(buf.byteLength >= this._keyBytes, 'buf must be at least KEYBYTES long')
return crypto.randomFillSync(buf, 0, this._keyBytes)
}
deriveKey (masterSeed, index, buf) {
assert(Buffer.isBuffer(masterSeed), 'masterSeed must be Buffer')
assert(masterSeed.byteLength === this._keyBytes, 'masterSeed must KEYBYTES long')
assert(index === index >>> 0, 'index must be valid uint32')
if (buf == null) buf = Buffer.alloc(this._keyBytes)
assert(Buffer.isBuffer(buf), 'buf must be Buffer')
assert(buf.byteLength >= this._keyBytes, 'buf must be at least KEYBYTES long')
// KDF
this._scratch.writeUInt32LE(index, 7)
buf.set(crypto.createHmac(this._alg, masterSeed)
.update(this._scratch)
.update(ZERO)
.digest())
buf.set(crypto.createHmac(this._alg, masterSeed)
.update(buf.subarray(0, this._bytes))
.update(ONE)
.digest(), this._bytes)
return { index, key: buf }
}
sign (keyObj, data, buf) {
assert(keyObj.index === keyObj.index >>> 0, 'keyObj.index must be valid uint32')
assert(Buffer.isBuffer(keyObj.key), 'keyObj.key must be Buffer')
assert(keyObj.key.byteLength === this._keyBytes, 'keyObj.key must be KEYBYTES long')
assert(typeof data === 'string' || Buffer.isBuffer(data), 'data must be String or Buffer')
if (buf == null) buf = Buffer.alloc(this._bytes)
assert(Buffer.isBuffer(buf), 'buf must be Buffer')
assert(buf.byteLength >= this._bytes, 'buf must be at least BYTES long')
return {
bitfield: 1 << keyObj.index,
signature: crypto.createHmac(this._alg, keyObj.key).update(data).digest(buf)
}
}
combine (signatures, buf) {
assert(signatures.every(s => s.bitfield === s.bitfield >>> 0), 'one or more signatures had signature.bitfield not be uint32')
assert(signatures.every(s => s.signature.byteLength === this._bytes), 'one or more signatures had signature.signature byteLength not be BYTES long')
const bitfields = signatures.map(s => s.bitfield)
const res = {
bitfield: xorInts(bitfields),
signature: xorBufs(signatures.map(s => s.signature), this._bytes, buf)
}
assert(MultisigHMAC.keysCount(res.bitfield) === MultisigHMAC.keysCount(orInts(bitfields)), 'one or more signatures cancelled out')
assert(res.signature.reduce((s, b) => s + b, 0) > 0, 'one or more signatures cancelled out')
return res
}
verify (keys, signature, data, threshold, sigScratchBuf) {
assert(threshold > 0, 'threshold must be at least 1')
assert(threshold === threshold >>> 0, 'threshold must be valid uint32')
var bitfield = signature.bitfield
const nKeys = MultisigHMAC.keysCount(bitfield)
const highestKey = 32 - Math.clz32(bitfield)
assert(keys.length >= nKeys && keys.length >= highestKey, 'Not enough keys given based on signature.bitfield')
assert(typeof data === 'string' || Buffer.isBuffer(data), 'data must be String or Buffer')
if (nKeys < threshold) return false
const usedKeys = MultisigHMAC.keyIndexes(bitfield)
var sig = Buffer.from(signature.signature)
for (var i = 0; i < usedKeys.length; i++) {
const key = keys[usedKeys[i]]
const keySig = this.sign(key, data, sigScratchBuf)
sig = xorBufs([sig, keySig.signature], this._bytes)
bitfield ^= keySig.bitfield
}
return bitfield === 0 && sig.every(b => b === 0)
}
verifyDerived (masterSeed, signature, data, threshold, keyScratchBuf, sigScratchBuf) {
assert(Buffer.isBuffer(masterSeed), 'masterSeed must be Buffer')
assert(masterSeed.byteLength === this._keyBytes, 'masterSeed must KEYBYTES long')
assert(threshold > 0, 'threshold must be at least 1')
assert(threshold === threshold >>> 0, 'threshold must be valid uint32')
var bitfield = signature.bitfield
assert(typeof data === 'string' || Buffer.isBuffer(data), 'data must be String or Buffer')
const usedKeys = MultisigHMAC.keyIndexes(bitfield)
var sig = Buffer.from(signature.signature)
for (var i = 0; i < usedKeys.length; i++) {
const key = this.deriveKey(masterSeed, usedKeys[i], keyScratchBuf)
const keySig = this.sign(key, data)
xorBufs([sig, keySig.signature], this._bytes)
bitfield ^= keySig.bitfield
}
return bitfield === 0 && sig.every(b => b === 0)
}
static keysCount (bitfield) {
assert(bitfield === bitfield >>> 0, 'bitfield must be uint32')
return popcnt32(bitfield)
}
static keyIndexes (bitfield) {
assert(bitfield === bitfield >>> 0, 'bitfield must be uint32')
return indexes(bitfield)
}
}
const SHA256_KEYBYTES = MultisigHMAC.SHA256_KEYBYTES = 512 / 8
const SHA256_BYTES = MultisigHMAC.SHA256_BYTES = 256 / 8
const SHA256_PRIMITIVE = MultisigHMAC.SHA256_PRIMITIVE = 'sha256'
const SHA384_KEYBYTES = MultisigHMAC.SHA384_KEYBYTES = 1024 / 8
const SHA384_BYTES = MultisigHMAC.SHA384_BYTES = 384 / 8
const SHA384_PRIMITIVE = MultisigHMAC.SHA384_PRIMITIVE = 'sha384'
const SHA512_KEYBYTES = MultisigHMAC.SHA512_KEYBYTES = 1024 / 8
const SHA512_BYTES = MultisigHMAC.SHA512_BYTES = 512 / 8
const SHA512_PRIMITIVE = MultisigHMAC.SHA512_PRIMITIVE = 'sha512'
const SHA512_256_KEYBYTES = MultisigHMAC.SHA512_256_KEYBYTES = 1024 / 8
const SHA512_256_BYTES = MultisigHMAC.SHA512_256_BYTES = 256 / 8
const SHA512_256_PRIMITIVE = MultisigHMAC.SHA512_256_PRIMITIVE = 'sha512_256'
MultisigHMAC.KEYBYTES = SHA256_KEYBYTES
MultisigHMAC.BYTES = SHA256_BYTES
MultisigHMAC.PRIMITIVE = SHA256_PRIMITIVE
function xorInts (ints) {
return ints.reduce((sum, int) => {
sum ^= int
return sum
})
}
function orInts (ints) {
return ints.reduce((sum, int) => {
sum |= int
return sum
})
}
function xorBufs (bufs, bytes, buf) {
if (buf == null) buf = Buffer.alloc(bytes)
assert(Buffer.isBuffer(buf), 'buf must be Buffer')
assert(buf.byteLength >= bytes, 'buf must be at least BYTES long')
return bufs.reduce((r, b) => {
for (var i = 0; i < r.byteLength; i++) {
r[i] ^= b[i]
}
return r
}, buf)
}
function indexes (int) {
var xs = []
var i = 0
while (int > 0) {
if (int & 0x1) {
xs.push(i)
}
int >>= 1
i++
}
return xs
}
module.exports = MultisigHMAC