Home Reference Source

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 };