UNPKG

@aeternity/aepp-sdk

Version:
231 lines (203 loc) 8.37 kB
import nacl from 'tweetnacl' import { v4 as uuid } from 'uuid' import { encodeBase58Check } from './crypto' import { isBase64, isHex } from './string' const _sodium = require('libsodium-wrappers-sumo') /** * KeyStore module * @module @aeternity/aepp-sdk/es/utils/keystore * @example import { Keystore } from '@aeternity/aepp-sdk' * @example const { Keystore } = require('@aeternity/aepp-sdk') */ const DEFAULTS = { crypto: { secret_type: 'ed25519', symmetric_alg: 'xsalsa20-poly1305', kdf: 'argon2id', kdf_params: { memlimit_kib: 65536, opslimit: 3, parallelism: 1 } } } // DERIVED KEY PART const DERIVED_KEY_FUNCTIONS = { argon2id: deriveKeyUsingArgon2id } export async function deriveKeyUsingArgon2id (password, salt, options) { const { memlimit_kib: memoryCost, opslimit: timeCost } = options.kdf_params // const isBrowser = !(typeof module !== 'undefined' && module.exports) return _sodium.ready.then(async () => { // tslint:disable-next-line:typedef const sodium = _sodium const result = sodium.crypto_pwhash( 32, password, salt, timeCost, memoryCost * 1024, sodium.crypto_pwhash_ALG_ARGON2ID13 ) return Buffer.from(result) }) } // CRYPTO PART const CRYPTO_FUNCTIONS = { 'xsalsa20-poly1305': { encrypt: encryptXsalsa20Poly1305, decrypt: decryptXsalsa20Poly1305 } } function encryptXsalsa20Poly1305 ({ plaintext, key, nonce }) { return nacl.secretbox(plaintext, nonce, key) } function decryptXsalsa20Poly1305 ({ ciphertext, key, nonce }) { const res = nacl.secretbox.open(ciphertext, nonce, key) if (!res) throw new Error('Invalid password or nonce') return res } /** * Convert a string to a Buffer. If encoding is not specified, hex-encoding * will be used if the input is valid hex. If the input is valid base64 but * not valid hex, base64 will be used. Otherwise, utf8 will be used. * @param {string} str String to be converted. * @param {string=} enc Encoding of the input string (optional). * @return {buffer} Buffer (bytearray) containing the input data. */ function str2buf (str, enc) { if (!str || str.constructor !== String) return str if (!enc && isHex(str)) enc = 'hex' if (!enc && isBase64(str)) enc = 'base64' return Buffer.from(str, enc) } /** * Symmetric private key encryption using secret (derived) key. * @param {buffer|string} plaintext Data to be encrypted. * @param {buffer|string} key Secret key. * @param {buffer|string} nonce Randomly generated nonce. * @param {string=} algo Encryption algorithm (default: DEFAULTS.crypto.symmetric_alg). * @return {buffer} Encrypted data. */ function encrypt (plaintext, key, nonce, algo = DEFAULTS.crypto.symmetric_alg) { if (!CRYPTO_FUNCTIONS[algo]) throw new Error(algo + ' is not available') return CRYPTO_FUNCTIONS[algo].encrypt({ plaintext, nonce, key }) } /** * Symmetric private key decryption using secret (derived) key. * @param {buffer|Uint8Array} ciphertext Data to be decrypted. * @param {buffer|Uint8Array} key Secret key. * @param {buffer|Uint8Array} nonce Nonce from key-object. * @param {string=} algo Encryption algorithm. * @return {buffer} Decrypted data. */ function decrypt (ciphertext, key, nonce, algo) { if (!CRYPTO_FUNCTIONS[algo]) throw new Error(algo + ' is not available') return CRYPTO_FUNCTIONS[algo].decrypt({ ciphertext, nonce, key }) } /** * Derive secret key from password with key derivation function. * @param {string} password User-supplied password. * @param {buffer|Uint8Array} nonce Randomly generated nonce. * @param {Object=} options Encryption parameters. * @param {string=} options.kdf Key derivation function (default: DEFAULTS.crypto.kdf). * @param {Object=} options.kdf_params KDF parameters (default: DEFAULTS.crypto.kdf_params). * @return {buffer} Secret key derived from password. */ async function deriveKey (password, nonce, options = { kdf_params: DEFAULTS.crypto.kdf_params, kdf: DEFAULTS.crypto.kdf }) { if (typeof password === 'undefined' || password === null || !nonce) { throw new Error('Must provide password and nonce to derive a key') } if (!Object.prototype.hasOwnProperty.call(DERIVED_KEY_FUNCTIONS, options.kdf)) throw new Error('Unsupported kdf type') return DERIVED_KEY_FUNCTIONS[options.kdf](password, nonce, options) } /** * Assemble key data object in secret-storage format. * @param {buffer} name Key name. * @param {buffer} derivedKey Password-derived secret key. * @param {buffer} privateKey Private key. * @param {buffer} nonce Randomly generated 24byte nonce. * @param {buffer} salt Randomly generated 16byte salt. * @param {Object=} options Encryption parameters. * @param {string=} options.kdf Key derivation function (default: argon2id). * @param {string=} options.cipher Symmetric cipher (default: constants.cipher). * @param {Object=} options.kdf_params KDF parameters (default: constants.<kdf>). * @return {Object} */ function marshal (name, derivedKey, privateKey, nonce, salt, options = {}) { const opt = Object.assign({}, DEFAULTS.crypto, options) return Object.assign( { name, version: 1, public_key: getAddressFromPriv(privateKey), id: uuid() }, { crypto: Object.assign( { secret_type: opt.secret_type, symmetric_alg: opt.symmetric_alg, ciphertext: Buffer.from(encrypt(Buffer.from(privateKey), derivedKey, nonce, opt.symmetric_alg)).toString('hex'), cipher_params: { nonce: Buffer.from(nonce).toString('hex') } }, { kdf: opt.kdf, kdf_params: { ...opt.kdf_params, salt: Buffer.from(salt).toString('hex') } } ) } ) } export function getAddressFromPriv (secret) { const keys = nacl.sign.keyPair.fromSecretKey(str2buf(secret)) const publicBuffer = Buffer.from(keys.publicKey) return `ak_${encodeBase58Check(publicBuffer)}` } /** * Recover plaintext private key from secret-storage key object. * @param {String} password Keystore object password. * @param {Object} keyObject Keystore object. * @return {Buffer} Plaintext private key. */ export async function recover (password, keyObject) { validateKeyObj(keyObject) const nonce = Uint8Array.from(str2buf(keyObject.crypto.cipher_params.nonce)) const salt = Uint8Array.from(str2buf(keyObject.crypto.kdf_params.salt)) const kdfParams = keyObject.crypto.kdf_params const kdf = keyObject.crypto.kdf const key = await decrypt( Uint8Array.from(str2buf(keyObject.crypto.ciphertext)), await deriveKey(password, salt, { kdf, kdf_params: kdfParams }), nonce, keyObject.crypto.symmetric_alg ) if (!key) throw new Error('Invalid password') if (Buffer.from(key).length === 64) return Buffer.from(key).toString('hex') return Buffer.from(key).toString('utf-8') } /** * Export private key to keystore secret-storage format. * @param {String} name Key name. * @param {String} password User-supplied password. * @param {String} privateKey Private key. * @param {Buffer} nonce Randomly generated 24byte nonce. * @param {Buffer} salt Randomly generated 16byte salt. * @param {Object=} options Encryption parameters. * @param {String=} options.kdf Key derivation function (default: pbkdf2). * @param {String=} options.cipher Symmetric cipher (default: constants.cipher). * @param {Object=} options.kdfparams KDF parameters (default: constants.<kdf>). * @return {Object} */ export async function dump (name, password, privateKey, nonce = nacl.randomBytes(24), salt = nacl.randomBytes(16), options = {}) { const opt = Object.assign({}, DEFAULTS.crypto, options) return marshal( name, await deriveKey(password, salt, opt), privateKey, nonce, salt, opt ) } export function validateKeyObj (obj) { const root = ['crypto', 'id', 'version', 'public_key'] const cryptoKeys = ['cipher_params', 'ciphertext', 'symmetric_alg', 'kdf', 'kdf_params'] const missingRootKeys = root.filter(key => !Object.prototype.hasOwnProperty.call(obj, key)) if (missingRootKeys.length) throw new Error(`Invalid key file format. Require properties: ${missingRootKeys}`) const missingCryptoKeys = cryptoKeys.filter(key => !Object.prototype.hasOwnProperty.call(obj.crypto, key)) if (missingCryptoKeys.length) throw new Error(`Invalid key file format. Require properties: ${missingCryptoKeys}`) return true }