UNPKG

ethereumjs-wallet

Version:
280 lines (236 loc) 8.44 kB
import * as crypto from 'crypto' import { keccak256, sha256, toBuffer } from 'ethereumjs-util' import { scrypt } from 'scrypt-js' import Wallet from './index' const utf8 = require('utf8') const aesjs = require('aes-js') function runCipherBuffer(cipher: crypto.Cipher | crypto.Decipher, data: Buffer): Buffer { return Buffer.concat([cipher.update(data), cipher.final()]) } // evp_kdf export interface EvpKdfOpts { count: number keysize: number ivsize: number digest: string } const evpKdfDefaults: EvpKdfOpts = { count: 1, keysize: 16, ivsize: 16, digest: 'md5', } function mergeEvpKdfOptsWithDefaults(opts?: Partial<EvpKdfOpts>): EvpKdfOpts { if (!opts) { return evpKdfDefaults } return { count: opts.count || evpKdfDefaults.count, keysize: opts.keysize || evpKdfDefaults.keysize, ivsize: opts.ivsize || evpKdfDefaults.ivsize, digest: opts.digest || evpKdfDefaults.digest, } } /* * opts: * - digest - digest algorithm, defaults to md5 * - count - hash iterations * - keysize - desired key size * - ivsize - desired IV size * * Algorithm form https://www.openssl.org/docs/manmaster/crypto/EVP_BytesToKey.html * * FIXME: not optimised at all */ function evp_kdf(data: Buffer, salt: Buffer, opts?: Partial<EvpKdfOpts>) { const params = mergeEvpKdfOptsWithDefaults(opts) // A single EVP iteration, returns `D_i`, where block equlas to `D_(i-1)` function iter(block: Buffer) { let hash = crypto.createHash(params.digest) hash.update(block) hash.update(data) hash.update(salt) block = hash.digest() for (let i = 1, len = params.count; i < len; i++) { hash = crypto.createHash(params.digest) hash.update(block) block = hash.digest() } return block } const ret: Buffer[] = [] let i = 0 while (Buffer.concat(ret).length < params.keysize + params.ivsize) { ret[i] = iter(i === 0 ? Buffer.alloc(0) : ret[i - 1]) i++ } const tmp = Buffer.concat(ret) return { key: tmp.slice(0, params.keysize), iv: tmp.slice(params.keysize, params.keysize + params.ivsize), } } // http://stackoverflow.com/questions/25288311/cryptojs-aes-pattern-always-ends-with function decodeCryptojsSalt(input: string): { ciphertext: Buffer; salt?: Buffer } { const ciphertext = Buffer.from(input, 'base64') if (ciphertext.slice(0, 8).toString() === 'Salted__') { return { salt: ciphertext.slice(8, 16), ciphertext: ciphertext.slice(16), } } return { ciphertext } } // { // "address": "0x169aab499b549eac087035e640d3f7d882ef5e2d", // "encrypted": true, // "locked": true, // "hash": "342f636d174cc1caa49ce16e5b257877191b663e0af0271d2ea03ac7e139317d", // "private": "U2FsdGVkX19ZrornRBIfl1IDdcj6S9YywY8EgOeOtLj2DHybM/CHL4Jl0jcwjT+36kDnjj+qEfUBu6J1mGQF/fNcD/TsAUgGUTEUEOsP1CKDvNHfLmWLIfxqnYHhHsG5", // "public": "U2FsdGVkX19EaDNK52q7LEz3hL/VR3dYW5VcoP04tcVKNS0Q3JINpM4XzttRJCBtq4g22hNDrOR8RWyHuh3nPo0pRSe9r5AUfEiCLaMBAhI16kf2KqCA8ah4brkya9ZLECdIl0HDTMYfDASBnyNXd87qodt46U0vdRT3PppK+9hsyqP8yqm9kFcWqMHktqubBE937LIU0W22Rfw6cJRwIw==" // } export interface EtherWalletOptions { address: string encrypted: boolean locked: boolean hash: string private: string public: string } /* * Third Party API: Import a wallet generated by EtherWallet * This wallet format is created by https://github.com/SilentCicero/ethereumjs-accounts * and used on https://www.myetherwallet.com/ */ export function fromEtherWallet(input: string | EtherWalletOptions, password: string): Wallet { const json: EtherWalletOptions = typeof input === 'object' ? input : JSON.parse(input) let privateKey: Buffer if (!json.locked) { if (json.private.length !== 64) { throw new Error('Invalid private key length') } privateKey = Buffer.from(json.private, 'hex') } else { if (typeof password !== 'string') { throw new Error('Password required') } if (password.length < 7) { throw new Error('Password must be at least 7 characters') } // the "encrypted" version has the low 4 bytes // of the hash of the address appended const hash = json.encrypted ? json.private.slice(0, 128) : json.private // decode openssl ciphertext + salt encoding const cipher = decodeCryptojsSalt(hash) if (!cipher.salt) { throw new Error('Unsupported EtherWallet key format') } // derive key/iv using OpenSSL EVP as implemented in CryptoJS const evp = evp_kdf(Buffer.from(password), cipher.salt, { keysize: 32, ivsize: 16 }) const decipher = crypto.createDecipheriv('aes-256-cbc', evp.key, evp.iv) privateKey = runCipherBuffer(decipher, Buffer.from(cipher.ciphertext)) // NOTE: yes, they've run it through UTF8 privateKey = Buffer.from(utf8.decode(privateKey.toString()), 'hex') } const wallet = new Wallet(privateKey) if (wallet.getAddressString() !== json.address) { throw new Error('Invalid private key or address') } return wallet } /** * Third Party API: Import a brain wallet used by Ether.Camp */ export function fromEtherCamp(passphrase: string): Wallet { return new Wallet(keccak256(Buffer.from(passphrase))) } /** * Third Party API: Import a wallet from a KryptoKit seed */ export async function fromKryptoKit(entropy: string, password: string): Promise<Wallet> { function kryptoKitBrokenScryptSeed(buf: Buffer) { // js-scrypt calls `Buffer.from(String(salt), 'utf8')` on the seed even though it is a buffer // // The `buffer`` implementation used does the below transformation (doesn't matches the current version): // https://github.com/feross/buffer/blob/67c61181b938b17d10dbfc0a545f713b8bd59de8/index.js function decodeUtf8Char(str: string) { try { return decodeURIComponent(str) } catch (err) { return String.fromCharCode(0xfffd) // UTF 8 invalid char } } let res = '', tmp = '' for (let i = 0; i < buf.length; i++) { if (buf[i] <= 0x7f) { res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i]) tmp = '' } else { tmp += '%' + buf[i].toString(16) } } return Buffer.from(res + decodeUtf8Char(tmp)) } if (entropy[0] === '#') { entropy = entropy.slice(1) } const type = entropy[0] entropy = entropy.slice(1) let privateKey: Buffer if (type === 'd') { privateKey = sha256(toBuffer(entropy)) } else if (type === 'q') { if (typeof password !== 'string') { throw new Error('Password required') } const encryptedSeed = sha256(Buffer.from(entropy.slice(0, 30))) const checksum = entropy.slice(30, 46) const salt = kryptoKitBrokenScryptSeed(encryptedSeed) const aesKey = await scrypt(Buffer.from(password, 'utf8'), salt, 16384, 8, 1, 32) /* FIXME: try to use `crypto` instead of `aesjs` // NOTE: ECB doesn't use the IV, so it can be anything var decipher = crypto.createDecipheriv("aes-256-ecb", aesKey, Buffer.from(0)) // FIXME: this is a clear abuse, but seems to match how ECB in aesjs works privKey = Buffer.concat([ decipher.update(encryptedSeed).slice(0, 16), decipher.update(encryptedSeed).slice(0, 16), ]) */ const decipher = new aesjs.ModeOfOperation.ecb(aesKey) /* decrypt returns an Uint8Array, perhaps there is a better way to concatenate */ privateKey = Buffer.concat([ Buffer.from(decipher.decrypt(encryptedSeed.slice(0, 16))), Buffer.from(decipher.decrypt(encryptedSeed.slice(16, 32))), ]) if (checksum.length > 0) { if (checksum !== sha256(sha256(privateKey)).slice(0, 8).toString('hex')) { throw new Error('Failed to decrypt input - possibly invalid passphrase') } } } else { throw new Error('Unsupported or invalid entropy type') } return new Wallet(privateKey) } /** * Third Party API: Import a brain wallet used by Quorum Wallet */ export function fromQuorumWallet(passphrase: string, userid: string): Wallet { if (passphrase.length < 10) { throw new Error('Passphrase must be at least 10 characters') } if (userid.length < 10) { throw new Error('User id must be at least 10 characters') } const merged = passphrase + userid const seed = crypto.pbkdf2Sync(merged, merged, 2000, 32, 'sha256') return new Wallet(seed) } const Thirdparty = { fromEtherWallet, fromEtherCamp, fromKryptoKit, fromQuorumWallet, } export default Thirdparty