ssv-keys
Version:
Tool for splitting a validator key into a predefined threshold of shares via Shamir-Secret-Sharing (SSS), and encrypt them with a set of operator keys.
205 lines (189 loc) • 5.83 kB
text/typescript
import crypto from 'crypto';
import { syncScrypt } from 'scrypt-js';
import Wallet from 'ethereumjs-wallet';
import { keccak256, sha256 } from 'ethereumjs-util';
import { EthereumWalletError, KeyStoreDataFormatError, KeyStoreInvalidError, KeyStorePasswordError } from '../exceptions/keystore';
interface V4Keystore {
crypto: {
kdf: {
function: string,
params: {
dklen: number,
n: number,
r: number,
p: number,
salt: string
},
message: string
},
checksum: {
function: string,
params: any,
message: string
},
cipher: {
function: string,
params: {
iv: string
},
message: string
}
},
description: string,
pubkey: string,
path: string,
uuid: string
version: number
}
/**
* Decrypt private key from key store data
* Supports key store versions: v1, v3, v4
*
* Example of usage (Node env):
*
* const keyStoreFilePath = path.join(process.cwd(), 'validator_keys', 'keystore.json');
* const keyStoreString: string = fs.readFileSync(keyStoreFilePath).toString();
* const keyStoreData = JSON.parse(keyStoreString);
* const keyStore = new EthereumKeyStore(keyStoreData);
* const password = 'testtest';
* console.log('Private Key:', await keyStore.getPrivateKey(password));
*/
class EthereumKeyStore {
private readonly keyStoreData: any;
private privateKey = '';
private wallet: Wallet | undefined;
/**
* Receive key store data from string or parsed JSON
* @param keyStoreData
*/
constructor(keyStoreData: any) {
if (!keyStoreData) {
throw new KeyStoreDataFormatError(keyStoreData, 'Key store data should be JSON or string');
}
if (typeof keyStoreData === 'string') {
this.keyStoreData = JSON.parse(keyStoreData);
} else {
this.keyStoreData = keyStoreData;
}
if (!this.keyStoreData.version) {
throw new KeyStoreInvalidError(this.keyStoreData, 'Invalid keystore file');
}
}
getPublicKey(): string {
if (this.keyStoreData) {
switch (this.keyStoreData.version ?? this.keyStoreData.Version) {
case 1:
return this.keyStoreData.Address;
case 3:
return this.keyStoreData.id;
case 4:
return this.keyStoreData.pubkey;
}
}
return '';
}
/**
* Decrypt private key using user password
* @param password
*/
async getPrivateKey(password = ''): Promise<string> {
// In case private key exist we return it
if (this.privateKey) return this.privateKey;
switch (this.keyStoreData.version) {
case 1:
this.wallet = await Wallet.fromV1(this.keyStoreData, password);
break;
case 3:
this.wallet = await Wallet.fromV3(this.keyStoreData, password, true);
break;
case 4:
this.wallet = await this.fromV4(this.keyStoreData, password);
break;
}
if (this.wallet) {
this.privateKey = this.wallet.getPrivateKey().toString('hex');
if (!this.privateKey) {
throw new KeyStorePasswordError('Invalid password');
}
}
return this.privateKey;
}
/**
* Import a wallet (Version 4 of the Ethereum wallet format).
*
* @param input A JSON serialized string, or an object representing V3 Keystore.
* @param password The keystore password.
*/
public async fromV4(
input: string | V4Keystore,
password: string,
): Promise<Wallet> {
const json: V4Keystore = typeof input === 'object' ? input : JSON.parse(input);
if (json.version !== 4) {
throw new EthereumWalletError('Not a V4 wallet');
}
let derivedKey: Uint8Array;
let kdfParams: any;
if (json.crypto.kdf.function === 'scrypt') {
kdfParams = json.crypto.kdf.params;
derivedKey = syncScrypt(
Buffer.from(password),
Buffer.from(kdfParams.salt, 'hex'),
kdfParams.n,
kdfParams.r,
kdfParams.p,
kdfParams.dklen,
);
} else if (json.crypto.kdf.function === 'pbkdf2') {
kdfParams = json.crypto.kdf.params;
if (kdfParams.prf !== 'hmac-sha256') {
throw new EthereumWalletError('Unsupported parameters to PBKDF2');
}
derivedKey = crypto.pbkdf2Sync(
Buffer.from(password),
Buffer.from(kdfParams.salt, 'hex'),
kdfParams.c,
kdfParams.dklen,
'sha256',
);
} else {
throw new EthereumWalletError('Unsupported key derivation scheme');
}
const ciphertext = Buffer.from(json.crypto.cipher.message, 'hex');
const checksumBuffer = Buffer.concat([Buffer.from(derivedKey.slice(16, 32)), ciphertext]);
const hashFunctions: Record<string, any> = {
keccak256,
sha256,
};
const hashFunction: any = hashFunctions[json.crypto.checksum.function];
const mac: Buffer = hashFunction(checksumBuffer);
if (mac.toString('hex') !== json.crypto.checksum.message) {
throw new EthereumWalletError('Invalid password');
}
const decipher = crypto.createDecipheriv(
json.crypto.cipher.function,
derivedKey.slice(0, 16),
Buffer.from(json.crypto.cipher.params.iv, 'hex'),
);
const seed: Buffer = this.runCipherBuffer(decipher, ciphertext);
return new Wallet(seed);
}
/**
* @param cipher
* @param data
*/
protected runCipherBuffer(cipher: crypto.Cipher | crypto.Decipher, data: Buffer): Buffer {
return Buffer.concat([cipher.update(data), cipher.final()]);
}
/**
* Convert byte array to string
* @param byteArray
*/
static toHexString(byteArray: Uint8Array): string {
return Array.from(byteArray, (byte: number) => {
// eslint-disable-next-line no-bitwise
return (`0${(byte & 0xFF).toString(16)}`).slice(-2);
}).join('');
}
}
export default EthereumKeyStore;