@ethereumjs/wallet
Version:
Utilities for handling Ethereum keys
192 lines (166 loc) • 5.79 kB
text/typescript
import { bytesToUtf8, concatBytes, unprefixedHexToBytes, utf8ToBytes } from '@ethereumjs/util'
import { base64 } from '@scure/base'
import { decrypt } from 'ethereum-cryptography/aes.js'
import { keccak256 } from 'ethereum-cryptography/keccak.js'
import { pbkdf2Sync } from 'ethereum-cryptography/pbkdf2.js'
import { md5 } from 'js-md5'
import { Wallet } from './wallet.js'
// 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: Uint8Array, salt: Uint8Array, opts?: Partial<EvpKdfOpts>) {
const params = mergeEvpKdfOptsWithDefaults(opts)
// A single EVP iteration, returns `D_i`, where block equlas to `D_(i-1)`
function iter(block: Uint8Array) {
if (params.digest !== 'md5') throw new Error('Only md5 is supported in evp_kdf')
let hash = md5.create()
hash.update(block)
hash.update(data)
hash.update(salt)
block = Uint8Array.from(hash.array())
for (let i = 1, len = params.count; i < len; i++) {
hash = md5.create()
hash.update(block)
block = new Uint8Array(hash.arrayBuffer())
}
return block
}
const ret: Uint8Array[] = []
let i = 0
while (concatBytes(...ret).length < params.keysize + params.ivsize) {
ret[i] = iter(i === 0 ? new Uint8Array() : ret[i - 1])
i++
}
const tmp = concatBytes(...ret)
return {
key: tmp.subarray(0, params.keysize),
iv: tmp.subarray(params.keysize, params.keysize + params.ivsize),
}
}
// http://stackoverflow.com/questions/25288311/cryptojs-aes-pattern-always-ends-with
function decodeCryptojsSalt(input: string): { ciphertext: Uint8Array; salt?: Uint8Array } {
const ciphertext = base64.decode(input)
if (bytesToUtf8(ciphertext.subarray(0, 8)) === 'Salted__') {
return {
salt: ciphertext.subarray(8, 16),
ciphertext: ciphertext.subarray(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 async function fromEtherWallet(
input: string | EtherWalletOptions,
password: string
): Promise<Wallet> {
const json: EtherWalletOptions = typeof input === 'object' ? input : JSON.parse(input)
let privateKey: Uint8Array
if (!json.locked) {
if (json.private.length !== 64) {
throw new Error('Invalid private key length')
}
privateKey = unprefixedHexToBytes(json.private)
} 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(utf8ToBytes(password), cipher.salt, { keysize: 32, ivsize: 16 })
const pr = await decrypt(cipher.ciphertext, evp.key, evp.iv, 'aes-256-cbc')
// NOTE: yes, they've run it through UTF8
privateKey = unprefixedHexToBytes(bytesToUtf8(pr))
}
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(utf8ToBytes(passphrase)))
}
/**
* 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 = utf8ToBytes(passphrase + userid)
const seed = pbkdf2Sync(merged, merged, 2000, 32, 'sha256')
return new Wallet(seed)
}
export const Thirdparty = {
fromEtherWallet,
fromEtherCamp,
fromQuorumWallet,
}