UNPKG

@keyper/specs

Version:

keyper specifications

310 lines (276 loc) 7.28 kB
import * as crypto from "crypto"; const blake2b = require("blake2b"); const randomBytes = require( "randombytes"); const scrypt = require( "scrypt.js"); const uuidv4 = require( "uuid").v4; interface V3Params { kdf: string cipher: string salt: string | Buffer iv: string | Buffer uuid: string | Buffer dklen: number c: number n: number r: number p: number } interface V3ParamsStrict { kdf: string cipher: string salt: Buffer iv: Buffer uuid: Buffer dklen: number c: number n: number r: number p: number } function validateHexString(paramName: string, str: string, length?: number) { if (str.toLowerCase().startsWith('0x')) { str = str.slice(2) } if (!str && !length) { return str } if ((length as number) % 2) { throw new Error(`Invalid length argument, must be an even number`) } if (typeof length === 'number' && str.length !== length) { throw new Error(`Invalid ${paramName}, string must be ${length} hex characters`) } if (!/^([0-9a-f]{2})+$/i.test(str)) { const howMany = typeof length === 'number' ? length : 'empty or a non-zero even number of' throw new Error(`Invalid ${paramName}, string must be ${howMany} hex characters`) } return str } function validateBuffer(paramName: string, buff: Buffer, length?: number) { if (!Buffer.isBuffer(buff)) { const howManyHex = typeof length === 'number' ? `${length * 2}` : 'empty or a non-zero even number of' const howManyBytes = typeof length === 'number' ? ` (${length} bytes)` : '' throw new Error( `Invalid ${paramName}, must be a string (${howManyHex} hex characters) or buffer${howManyBytes}`, ) } if (typeof length === 'number' && buff.length !== length) { throw new Error(`Invalid ${paramName}, buffer must be ${length} bytes`) } return buff } function mergeToV3ParamsWithDefaults(params?: Partial<V3Params>): V3ParamsStrict { const v3Defaults: V3ParamsStrict = { cipher: 'aes-128-ctr', kdf: 'scrypt', salt: randomBytes(32), iv: randomBytes(16), uuid: randomBytes(16), dklen: 32, c: 262144, n: 262144, r: 8, p: 1, } if (!params) { return v3Defaults } if (typeof params.salt === 'string') { params.salt = Buffer.from(validateHexString('salt', params.salt), 'hex') } if (typeof params.iv === 'string') { params.iv = Buffer.from(validateHexString('iv', params.iv, 32), 'hex') } if (typeof params.uuid === 'string') { params.uuid = Buffer.from(validateHexString('uuid', params.uuid, 32), 'hex') } if (params.salt) { validateBuffer('salt', params.salt) } if (params.iv) { validateBuffer('iv', params.iv, 16) } if (params.uuid) { validateBuffer('uuid', params.uuid, 16) } return { ...v3Defaults, ...(params as V3ParamsStrict), } } // KDF const enum KDFFunctions { PBKDF = 'pbkdf2', Scrypt = 'scrypt', } interface ScryptKDFParams { dklen: number n: number p: number r: number salt: Buffer } interface ScryptKDFParamsOut { dklen: number n: number p: number r: number salt: string } interface PBKDFParams { c: number dklen: number prf: string salt: Buffer } interface PBKDFParamsOut { c: number dklen: number prf: string salt: string } type KDFParams = ScryptKDFParams | PBKDFParams type KDFParamsOut = ScryptKDFParamsOut | PBKDFParamsOut function kdfParamsForPBKDF(opts: V3ParamsStrict): PBKDFParams { return { dklen: opts.dklen, salt: opts.salt, c: opts.c, prf: 'hmac-sha256', } } function kdfParamsForScrypt(opts: V3ParamsStrict): ScryptKDFParams { return { dklen: opts.dklen, salt: opts.salt, n: opts.n, r: opts.r, p: opts.p, } } interface V3Keystore { crypto: { cipher: string cipherparams: { iv: string } ciphertext: string kdf: string kdfparams: KDFParamsOut mac: string } id: string version: number } function runCipherBuffer(cipher: crypto.Cipher | crypto.Decipher, data: Buffer): Buffer { return Buffer.concat([cipher.update(data), cipher.final()]) } export function encrypt(privKey: Buffer, password: string, opts?: Partial<V3Params>): V3Keystore { const v3Params: V3ParamsStrict = mergeToV3ParamsWithDefaults(opts) let kdfParams: KDFParams let derivedKey: Buffer switch (v3Params.kdf) { case KDFFunctions.PBKDF: kdfParams = kdfParamsForPBKDF(v3Params) derivedKey = crypto.pbkdf2Sync( Buffer.from(password), kdfParams.salt, kdfParams.c, kdfParams.dklen, 'sha256', ) break case KDFFunctions.Scrypt: kdfParams = kdfParamsForScrypt(v3Params) derivedKey = scrypt( Buffer.from(password), kdfParams.salt, kdfParams.n, kdfParams.r, kdfParams.p, kdfParams.dklen, ) break default: throw new Error('Unsupported kdf') } const cipher: crypto.Cipher = crypto.createCipheriv( v3Params.cipher, derivedKey.slice(0, 16), v3Params.iv, ) if (!cipher) { throw new Error('Unsupported cipher') } const ciphertext = runCipherBuffer(cipher, privKey) const mac = blake2b(32).update( Buffer.concat([derivedKey.slice(16, 32), Buffer.from(ciphertext)]) ).digest('hex'); return { version: 3, id: uuidv4({ random: v3Params.uuid }), crypto: { ciphertext: ciphertext.toString('hex'), cipherparams: { iv: v3Params.iv.toString('hex') }, cipher: v3Params.cipher, kdf: v3Params.kdf, kdfparams: { ...kdfParams, salt: kdfParams.salt.toString('hex'), }, mac: mac.toString('hex'), }, } } export function decrypt( input: string | V3Keystore, password: string, nonStrict: boolean = false, ): string { const json: V3Keystore = typeof input === 'object' ? input : JSON.parse(nonStrict ? input.toLowerCase() : input) if (json.version !== 3) { throw new Error('Not a V3 wallet') } let derivedKey: Buffer, kdfparams: any if (json.crypto.kdf === 'scrypt') { kdfparams = json.crypto.kdfparams derivedKey = scrypt( Buffer.from(password), Buffer.from(kdfparams.salt, 'hex'), kdfparams.n, kdfparams.r, kdfparams.p, kdfparams.dklen, ) } else if (json.crypto.kdf === 'pbkdf2') { kdfparams = json.crypto.kdfparams if (kdfparams.prf !== 'hmac-sha256') { throw new Error('Unsupported parameters to PBKDF2') } derivedKey = crypto.pbkdf2Sync( Buffer.from(password), Buffer.from(kdfparams.salt, 'hex'), kdfparams.c, kdfparams.dklen, 'sha256', ) } else { throw new Error('Unsupported key derivation scheme') } const ciphertext = Buffer.from(json.crypto.ciphertext, 'hex') const mac = blake2b(32).update( Buffer.concat([derivedKey.slice(16, 32), Buffer.from(ciphertext)]) ).digest('hex'); if (mac.toString('hex') !== json.crypto.mac) { throw new Error('Key derivation failed - possibly wrong passphrase') } const decipher = crypto.createDecipheriv( json.crypto.cipher, derivedKey.slice(0, 16), Buffer.from(json.crypto.cipherparams.iv, 'hex'), ) const seed = runCipherBuffer(decipher, ciphertext) return seed.toString("hex"); }