ethereumjs-wallet
Version:
Utilities for handling Ethereum keys
280 lines (236 loc) • 8.44 kB
text/typescript
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