const sodium = require('sodium-universal')
const c = require('compact-encoding')
const assert = require('nanoassert')
const b4a = require('b4a')
const { HMAC } = require('sodium-hmac')
const bip39 = require('bip39-mnemonic')
const { sign } = require('./crypto')
const MASTER_CHAIN_CODE = b4a.from('ed25519 seed')
const MASTER_SYMMETRIC_CODE = b4a.from('Symmetric key seed')
const HARDENED_OFFSET = 0x80000000
class KeyChain {
constructor (seed) {
this.seed = seed
this._buffer = b4a.alloc(64)
this.chainKey = this._buffer.subarray(0, 32)
this.chainCode = this._buffer.subarray(32)
this.publicKey = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
this.secretKey = b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)
this._initialise()
}
static generateMnemonic () {
return bip39.generateMnemonic()
}
static deriveSeed (mnemonic) {
return bip39.mnemonicToSeed(mnemonic)
}
static async from ({ mnemonic, seed }, path) {
assert(mnemonic || seed, 'No mnemonic or seed was passed.')
if (!seed) seed = await KeyChain.deriveSeed(mnemonic)
const key = new KeyChain(seed)
if (!path) return key
return key.get(path)
}
getSymmetricKey (path) {
const buffer = b4a.alloc(64)
const chainKey = buffer.subarray(0, 32)
const key = buffer.subarray(32)
HMAC.sha512(this.seed, MASTER_SYMMETRIC_CODE, buffer)
for (const step of path) {
const label = b4a.alloc(step.length + 1)
b4a.write(label, step, 1, 'utf8')
HMAC.sha512(label, chainKey, buffer)
}
return key
}
get isKeychain () {
return true
}
_initialise () {
HMAC.sha512(this.seed, MASTER_CHAIN_CODE, this._buffer)
sodium.crypto_sign_seed_keypair(this.publicKey, this.secretKey, this.chainKey)
}
clear () {
this.secretKey.fill(0)
this.chainKey.fill(0)
this.chainCode.fill(0)
}
get (path) {
return this.derive(path)
}
sign (signable) {
return sign(signable, this)
}
derive (path) {
const buffer = b4a.from(this._buffer)
const chainKey = buffer.subarray(0, 32)
const chainCode = buffer.subarray(32)
for (const step of path) {
const index = ensureHardened(step) // hardened indices are >= 2^31
HMAC.sha512(encodeDerivationData(chainKey, index), chainCode, buffer)
}
return createKeyPair(chainKey)
}
}
module.exports = KeyChain
function createKeyPair (seed) {
if (!seed) throw new Error('No seed provided')
const publicKey = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
const secretKey = b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)
sodium.crypto_sign_seed_keypair(publicKey, secretKey, seed)
return {
publicKey,
secretKey
}
}
function encodeDerivationData (key, step) {
const state = { buffer: b4a.alloc(37), start: 0, end: 37 }
c.uint8.encode(state, 0)
c.fixed32.encode(state, key)
state.buffer.writeUInt32BE(step, state.start)
state.buffer[33] |= 0x80
return state.buffer
}
function ensureHardened (n) {
if (n >= HARDENED_OFFSET) return n
return n + HARDENED_OFFSET
}