packages/oo7-substrate/src/secretStore.js
const { Bond } = require('oo7')
const nacl = require('tweetnacl');
const { generateMnemonic, mnemonicToSeed } = require('bip39')
const { ss58Encode } = require('./ss58')
const { AccountId } = require('./types')
const { bytesToHex, hexToBytes } = require('./utils')
let cache = {}
function seedFromPhrase(phrase) {
if (!cache[phrase]) {
cache[phrase] = phrase.match(/^0x[0-9a-fA-F]{64}$/)
? hexToBytes(phrase)
: new Uint8Array(mnemonicToSeed(phrase).slice(0, 32))
}
return cache[phrase]
}
class SecretStore extends Bond {
constructor (storage) {
super()
this._storage = storage || typeof localStorage === 'undefined' ? {} : localStorage
this._keys = []
this._load()
}
submit (phrase, name) {
this._keys.push({phrase, name})
this._sync()
return this.accountFromPhrase(phrase)
}
accountFromPhrase (phrase) {
return new AccountId(nacl.sign.keyPair.fromSeed(seedFromPhrase(phrase)).publicKey)
}
accounts () {
return this._keys.map(k => k.account)
}
find (identifier) {
if (this._keys.indexOf(identifier) !== -1) {
return identifier
}
if (identifier instanceof Uint8Array && identifier.length == 32 || identifier instanceof AccountId) {
identifier = ss58Encode(identifier)
}
return this._byAddress[identifier] ? this._byAddress[identifier] : this._byName[identifier]
}
sign (from, data) {
let item = this.find(from)
if (item) {
console.info(`Signing data from ${item.name}`, bytesToHex(data))
let sig = nacl.sign.detached(data, item.key.secretKey)
console.info(`Signature is ${bytesToHex(sig)}`)
if (!nacl.sign.detached.verify(data, sig, item.key.publicKey)) {
console.warn(`Signature is INVALID!`)
return null
}
return sig
}
return null
}
forget (identifier) {
let item = this.find(identifier)
if (item) {
console.info(`Forgetting key ${item.name} (${item.address}, ${item.phrase})`)
this._keys = this._keys.filter(i => i !== item)
this._sync()
}
}
_load () {
if (this._storage.secretStore) {
this._keys = JSON.parse(this._storage.secretStore).map(({seed, phrase, name}) => ({ phrase, name, seed: hexToBytes(seed) }))
} else if (this._storage.secretStore2) {
this._keys = JSON.parse(this._storage.secretStore2).map(({seed, name}) => ({ phrase: seed, name }))
} else {
this._keys = [{
name: 'Default',
phrase: generateMnemonic()
}]
}
this._sync()
}
_sync () {
let byAddress = {}
let byName = {}
this._keys = this._keys.map(({seed, phrase, name, key}) => {
seed = seed || seedFromPhrase(phrase)
key = key || nacl.sign.keyPair.fromSeed(seed)
let account = new AccountId(key.publicKey)
let address = ss58Encode(account)
let item = {seed, phrase, name, key, account, address}
byAddress[address] = item
byName[name] = item
return item
})
this._byAddress = byAddress
this._byName = byName
this._storage.secretStore = JSON.stringify(this._keys.map(k => ({seed: bytesToHex(k.seed), phrase: k.phrase, name: k.name})))
this.trigger({keys: this._keys, byAddress: this._byAddress, byName: this._byName})
}
}
let s_secretStore = null;
function secretStore(storage) {
if (s_secretStore === null) {
s_secretStore = new SecretStore(storage);
}
return s_secretStore;
}
module.exports = { secretStore, SecretStore };