235 lines
6.2 KiB
TypeScript
235 lines
6.2 KiB
TypeScript
import {
|
|
createCipheriv,
|
|
createDecipheriv,
|
|
pbkdf2Sync,
|
|
randomBytes
|
|
} from 'crypto';
|
|
import { privateToAddress, sha3 } from 'ethereumjs-util';
|
|
import scrypt from 'scryptsy';
|
|
import uuid from 'uuid';
|
|
import { decipherBuffer, decodeCryptojsSalt, evp_kdf } from './decrypt';
|
|
|
|
export interface UtcKeystore {
|
|
version: number;
|
|
id: string;
|
|
address: string;
|
|
Crypto: object;
|
|
}
|
|
|
|
// adapted from https://github.com/kvhnuke/etherwallet/blob/de536ffebb4f2d1af892a32697e89d1a0d906b01/app/scripts/myetherwallet.js#L342
|
|
export function determineKeystoreType(file: string): string {
|
|
const parsed = JSON.parse(file);
|
|
|
|
if (parsed.encseed) {
|
|
return 'presale';
|
|
} else if (parsed.Crypto || parsed.crypto) {
|
|
return 'v2-v3-utc';
|
|
} else if (parsed.hash && parsed.locked === true) {
|
|
return 'v1-encrypted';
|
|
} else if (parsed.hash && parsed.locked === false) {
|
|
return 'v1-unencrypted';
|
|
} else if (parsed.publisher === 'MyEtherWallet') {
|
|
return 'v2-unencrypted';
|
|
} else {
|
|
throw new Error('Invalid keystore');
|
|
}
|
|
}
|
|
|
|
export function isKeystorePassRequired(file: string): boolean {
|
|
switch (determineKeystoreType(file)) {
|
|
case 'presale':
|
|
return true;
|
|
case 'v1-unencrypted':
|
|
return false;
|
|
case 'v1-encrypted':
|
|
return true;
|
|
case 'v2-unencrypted':
|
|
return false;
|
|
case 'v2-v3-utc':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// adapted from https://github.com/kvhnuke/etherwallet/blob/de536ffebb4f2d1af892a32697e89d1a0d906b01/app/scripts/myetherwallet.js#L218
|
|
export function decryptPresaleToPrivKey(
|
|
file: string,
|
|
password: string
|
|
): Buffer {
|
|
const json = JSON.parse(file);
|
|
const encseed = new Buffer(json.encseed, 'hex');
|
|
const derivedKey = pbkdf2Sync(
|
|
new Buffer(password),
|
|
new Buffer(password),
|
|
2000,
|
|
32,
|
|
'sha256'
|
|
).slice(0, 16);
|
|
const decipher = createDecipheriv(
|
|
'aes-128-cbc',
|
|
derivedKey,
|
|
encseed.slice(0, 16)
|
|
);
|
|
const seed = decipherBuffer(decipher, encseed.slice(16));
|
|
const privkey = sha3(seed);
|
|
const address = privateToAddress(privkey);
|
|
|
|
if (address.toString('hex') !== json.ethaddr) {
|
|
throw new Error('Decoded key mismatch - possibly wrong passphrase');
|
|
}
|
|
return privkey;
|
|
}
|
|
|
|
// adapted from https://github.com/kvhnuke/etherwallet/blob/de536ffebb4f2d1af892a32697e89d1a0d906b01/app/scripts/myetherwallet.js#L179
|
|
export function decryptMewV1ToPrivKey(file: string, password: string): Buffer {
|
|
const json = JSON.parse(file);
|
|
let privkey;
|
|
let address;
|
|
|
|
if (typeof password !== 'string') {
|
|
throw new Error('Password required');
|
|
}
|
|
if (password.length < 7) {
|
|
throw new Error('Password must be at least 7 characters');
|
|
}
|
|
let cipher = json.encrypted ? json.private.slice(0, 128) : json.private;
|
|
cipher = decodeCryptojsSalt(cipher);
|
|
const evp = evp_kdf(new Buffer(password), cipher.salt, {
|
|
keysize: 32,
|
|
ivsize: 16
|
|
});
|
|
const decipher = createDecipheriv('aes-256-cbc', evp.key, evp.iv);
|
|
privkey = decipherBuffer(decipher, new Buffer(cipher.ciphertext));
|
|
privkey = new Buffer(privkey.toString(), 'hex');
|
|
address = '0x' + privateToAddress(privkey).toString('hex');
|
|
|
|
if (address !== json.address) {
|
|
throw new Error('Invalid private key or address');
|
|
}
|
|
return privkey;
|
|
}
|
|
|
|
export const scryptSettings = {
|
|
n: 1024
|
|
};
|
|
|
|
export const kdf = 'scrypt';
|
|
|
|
export function pkeyToKeystore(
|
|
pkey: Buffer,
|
|
address: string,
|
|
password: string
|
|
): UtcKeystore {
|
|
const salt = randomBytes(32);
|
|
const iv = randomBytes(16);
|
|
let derivedKey;
|
|
const kdfparams: any = {
|
|
dklen: 32,
|
|
salt: salt.toString('hex')
|
|
};
|
|
if (kdf === 'scrypt') {
|
|
// FIXME: support progress reporting callback
|
|
kdfparams.n = 1024;
|
|
kdfparams.r = 8;
|
|
kdfparams.p = 1;
|
|
derivedKey = scrypt(
|
|
new Buffer(password),
|
|
salt,
|
|
kdfparams.n,
|
|
kdfparams.r,
|
|
kdfparams.p,
|
|
kdfparams.dklen
|
|
);
|
|
} else {
|
|
throw new Error('Unsupported kdf');
|
|
}
|
|
const cipher = createCipheriv('aes-128-ctr', derivedKey.slice(0, 16), iv);
|
|
if (!cipher) {
|
|
throw new Error('Unsupported cipher');
|
|
}
|
|
const ciphertext = Buffer.concat([cipher.update(pkey), cipher.final()]);
|
|
const mac = sha3(
|
|
Buffer.concat([
|
|
derivedKey.slice(16, 32),
|
|
new Buffer(ciphertext as any, 'hex')
|
|
])
|
|
);
|
|
return {
|
|
version: 3,
|
|
id: uuid.v4({
|
|
random: randomBytes(16) as any
|
|
}),
|
|
address,
|
|
Crypto: {
|
|
ciphertext: ciphertext.toString('hex'),
|
|
cipherparams: {
|
|
iv: iv.toString('hex')
|
|
},
|
|
cipher: 'aes-128-ctr',
|
|
kdf,
|
|
kdfparams,
|
|
mac: mac.toString('hex')
|
|
}
|
|
};
|
|
}
|
|
|
|
export function getV3Filename(address: string) {
|
|
const ts = new Date();
|
|
return ['UTC--', ts.toJSON().replace(/:/g, '-'), '--', address].join('');
|
|
}
|
|
|
|
export function decryptUtcKeystoreToPkey(
|
|
input: string,
|
|
password: string
|
|
): Buffer {
|
|
const kstore = JSON.parse(input.toLowerCase());
|
|
if (kstore.version !== 3) {
|
|
throw new Error('Not a V3 wallet');
|
|
}
|
|
let derivedKey;
|
|
let kdfparams;
|
|
|
|
if (kstore.crypto.kdf === 'scrypt') {
|
|
kdfparams = kstore.crypto.kdfparams;
|
|
derivedKey = scrypt(
|
|
new Buffer(password),
|
|
new Buffer(kdfparams.salt, 'hex'),
|
|
kdfparams.n,
|
|
kdfparams.r,
|
|
kdfparams.p,
|
|
kdfparams.dklen
|
|
);
|
|
} else if (kstore.crypto.kdf === 'pbkdf2') {
|
|
kdfparams = kstore.crypto.kdfparams;
|
|
if (kdfparams.prf !== 'hmac-sha256') {
|
|
throw new Error('Unsupported parameters to PBKDF2');
|
|
}
|
|
derivedKey = pbkdf2Sync(
|
|
new Buffer(password),
|
|
new Buffer(kdfparams.salt, 'hex'),
|
|
kdfparams.c,
|
|
kdfparams.dklen,
|
|
'sha256'
|
|
);
|
|
} else {
|
|
throw new Error('Unsupported key derivation scheme');
|
|
}
|
|
const ciphertext = new Buffer(kstore.crypto.ciphertext, 'hex');
|
|
const mac = sha3(Buffer.concat([derivedKey.slice(16, 32), ciphertext]));
|
|
if (mac.toString('hex') !== kstore.crypto.mac) {
|
|
throw new Error('Key derivation failed - possibly wrong passphrase');
|
|
}
|
|
const decipher = createDecipheriv(
|
|
kstore.crypto.cipher,
|
|
derivedKey.slice(0, 16),
|
|
new Buffer(kstore.crypto.cipherparams.iv, 'hex')
|
|
);
|
|
let seed = decipherBuffer(decipher, ciphertext);
|
|
while (seed.length < 32) {
|
|
const nullBuff = new Buffer([0x00]);
|
|
seed = Buffer.concat([nullBuff, seed]);
|
|
}
|
|
return seed;
|
|
}
|