UNPKG

@orbitdb/core

Version:

Distributed p2p database on IPFS

302 lines (261 loc) 8.55 kB
/** * @module KeyStore * @description * Provides a local key manager for OrbitDB. * @example <caption>Create a keystore with defaults.</caption> * const keystore = await KeyStore() * @example <caption>Create a keystore with custom storage.</caption> * const storage = await MemoryStorage() * const keystore = await KeyStore({ storage }) */ import { privateKeyFromRaw, publicKeyFromRaw, generateKeyPair } from '@libp2p/crypto/keys' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { compare as uint8ArrayCompare } from 'uint8arrays/compare' import ComposedStorage from './storage/composed.js' import LevelStorage from './storage/level.js' import LRUStorage from './storage/lru.js' const verifySignature = async (signature, publicKey, data) => { if (!signature) { throw new Error('No signature given') } if (!publicKey) { throw new Error('Given publicKey was undefined') } if (!data) { throw new Error('Given input data was undefined') } if (!(data instanceof Uint8Array)) { data = typeof data === 'string' ? uint8ArrayFromString(data) : new Uint8Array(data) } const isValid = (key, msg, sig) => key.verify(msg, sig) let res = false try { const pubKey = publicKeyFromRaw(uint8ArrayFromString(publicKey, 'base16')) res = await isValid(pubKey, data, uint8ArrayFromString(signature, 'base16')) } catch (e) { // Catch error: sig length wrong } return Promise.resolve(res) } /** * Signs data using a key pair. * @param {Secp256k1PrivateKey} key The key to use for signing data. * @param {string|Uint8Array} data The data to sign. * @return {string} A signature. * @throws No signing key given if no key is provided. * @throws Given input data was undefined if no data is provided. * @static * @private */ const signMessage = async (key, data) => { if (!key) { throw new Error('No signing key given') } if (!data) { throw new Error('Given input data was undefined') } if (!(data instanceof Uint8Array)) { data = typeof data === 'string' ? uint8ArrayFromString(data) : new Uint8Array(data) } return uint8ArrayToString(await key.sign(data), 'base16') } const verifiedCachePromise = LRUStorage({ size: 1000 }) /** * Verifies input data against a cached version of the signed message. * @param {string} signature The generated signature. * @param {string} publicKey The derived public key of the key pair. * @param {string} data The data to be verified. * @return {boolean} True if the the data and cache match, false otherwise. * @static * @private */ const verifyMessage = async (signature, publicKey, data) => { const verifiedCache = await verifiedCachePromise const cached = await verifiedCache.get(signature) let res = false if (!cached) { const verified = await verifySignature(signature, publicKey, data) res = verified if (verified) { await verifiedCache.put(signature, { publicKey, data }) } } else { const compare = (cached, data) => { const match = data instanceof Uint8Array ? uint8ArrayCompare(cached, data) === 0 : cached.toString() === data.toString() return match } res = cached.publicKey === publicKey && compare(cached.data, data) } return res } const defaultPath = './keystore' /** * Creates an instance of KeyStore. * @param {Object} params One or more parameters for configuring KeyStore. * @param {Object} [params.storage] An instance of a storage class. Can be one * of ComposedStorage, IPFSBlockStorage, LevelStorage, etc. Defaults to * ComposedStorage. * @param {string} [params.path=./keystore] The path to a valid storage. * @return {module:KeyStore~KeyStore} An instance of KeyStore. * @instance */ const KeyStore = async ({ storage, path } = {}) => { /** * @namespace module:KeyStore~KeyStore * @description The instance returned by {@link module:KeyStore}. */ // Persistent storage for keys storage = storage || await ComposedStorage(await LRUStorage({ size: 1000 }), await LevelStorage({ path: path || defaultPath })) // Cache for deserialized/unmarshaled keys const keyCache = await LRUStorage({ size: 1000 }) /** * Closes the KeyStore's underlying storage. * @memberof module:KeyStore~KeyStore * @async * @instance */ const close = async () => { await storage.close() await keyCache.close() } /** * Clears the KeyStore's underlying storage. * @memberof module:KeyStore~KeyStore * @async * @instance */ const clear = async () => { await storage.clear() await keyCache.clear() } /** * Checks if a key exists in the key store . * @param {string} id The id of an [Identity]{@link module:Identities~Identity} to check the key for. * @return {boolean} True if the key exists, false otherwise. * @throws id needed to check a key if no id is specified. * @memberof module:KeyStore~KeyStore * @async * @instance */ const hasKey = async (id) => { if (!id) { throw new Error('id needed to check a key') } let hasKey = false let key = await keyCache.get(id) if (key) { hasKey = true } else { try { key = await storage.get('private_' + id) hasKey = key !== undefined && key !== null } catch (e) { // Catches 'Error: ENOENT: no such file or directory, open <path>' console.error('Error: ENOENT: no such file or directory') } } return hasKey } /** * Adds a private key to the keystore. * @param {string} id An id of the [Identity]{@link module:Identities~Identity} to whom the key belongs to. * @param {Uint8Array} key The private key to store. * @memberof module:KeyStore~KeyStore * @async * @instance */ const addKey = async (id, key) => { const { privateKey } = key await storage.put('private_' + id, privateKey) // Unmarshal the key and add it to the cache const unmarshaledPrivateKey = privateKeyFromRaw(privateKey) await keyCache.put(id, unmarshaledPrivateKey) } /** * Creates a key pair and stores it to the keystore. * @param {string} id An id of the [Identity]{@link module:Identities~Identity} to generate the key pair for. * @throws id needed to create a key if no id is specified. * @memberof module:KeyStore~KeyStore * @async * @instance */ const createKey = async (id) => { if (!id) { throw new Error('id needed to create a key') } // Generate a private key const keyPair = await generateKeyPair('secp256k1') const key = { publicKey: keyPair.publicKey.raw, privateKey: keyPair.raw } await addKey(id, key) return keyPair } /** * Gets a key from keystore. * @param {string} id An id of the [Identity]{@link module:Identities~Identity} whose key to retrieve. * @return {Uint8Array} The key specified by id. * @throws id needed to get a key if no id is specified. * @memberof module:KeyStore~KeyStore * @async * @instance */ const getKey = async (id) => { if (!id) { throw new Error('id needed to get a key') } let key = await keyCache.get(id) if (!key) { let storedKey try { storedKey = await storage.get('private_' + id) } catch (e) { // ignore ENOENT error } if (!storedKey) { return } key = privateKeyFromRaw(storedKey) await keyCache.put(id, key) } return key } /** * Gets the serialized public key from a key pair. * @param {*} keys A key pair. * @param {Object} options One or more options. * @param {Object} [options.format=hex] The format the public key should be * returned in. * @return {Uint8Array|String} The public key. * @throws Supported formats are `hex` and `buffer` if an invalid format is * passed in options. * @memberof module:KeyStore~KeyStore * @async * @instance */ const getPublic = (keys, options = {}) => { const formats = ['hex', 'buffer'] const format = options.format || 'hex' if (formats.indexOf(format) === -1) { throw new Error('Supported formats are `hex` and `buffer`') } const pubKey = keys.publicKey.raw return format === 'buffer' ? pubKey : uint8ArrayToString(pubKey, 'base16') } return { clear, close, hasKey, addKey, createKey, getKey, getPublic } } export { KeyStore as default, verifyMessage, signMessage }