Skip to content
Merged
87 changes: 37 additions & 50 deletions packages/neuron-wallet/src/models/keys/keystore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import crypto from 'crypto'
import SHA3 from 'sha3'
import { Keccak } from 'sha3'
import { v4 as uuid } from 'uuid'

import { UnsupportedCipher, IncorrectPassword, InvalidKeystore } from 'exceptions'
Expand Down Expand Up @@ -48,24 +48,21 @@ export default class Keystore {
}
}

static create = (extendedPrivateKey: ExtendedPrivateKey, password: string) => {
const salt = crypto.randomBytes(32)
const iv = crypto.randomBytes(16)
const params = {
n: 8192,
r: 8,
p: 1,
}
static create = (
extendedPrivateKey: ExtendedPrivateKey,
password: string,
options: { salt?: Buffer; iv?: Buffer } = {}
) => {
const salt = options.salt || crypto.randomBytes(32)
const iv = options.iv || crypto.randomBytes(16)
const kdfparams: KdfParams = {
dklen: 32,
salt: salt.toString('hex'),
...params,
n: 2 ** 18,
r: 8,
p: 1,
}
const derivedKey: Buffer = crypto.scryptSync(Buffer.from(password), salt, kdfparams.dklen, {
N: kdfparams.n,
r: kdfparams.r,
p: kdfparams.p,
})
const derivedKey = crypto.scryptSync(password, salt, kdfparams.dklen, Keystore.scryptOptions(kdfparams))

const cipher = crypto.createCipheriv(CIPHER, derivedKey.slice(0, 16), iv)
if (!cipher) {
Expand All @@ -75,11 +72,6 @@ export default class Keystore {
cipher.update(Buffer.from(extendedPrivateKey.serialize(), 'hex')),
cipher.final(),
])
const hash = new SHA3(256)
const mac = hash
.update(Buffer.concat([derivedKey.slice(16, 32), ciphertext]))
.digest()
.toString('hex')

return new Keystore(
{
Expand All @@ -90,31 +82,17 @@ export default class Keystore {
cipher: CIPHER,
kdf: 'scrypt',
kdfparams,
mac,
mac: Keystore.mac(derivedKey, ciphertext),
},
uuid()
)
}

// Decrypt and return serialized extended private key.
decrypt(password: string): string {
const { kdfparams } = this.crypto
const derivedKey: Buffer = crypto.scryptSync(
Buffer.from(password),
Buffer.from(kdfparams.salt, 'hex'),
kdfparams.dklen,
{
N: kdfparams.n,
r: kdfparams.r,
p: kdfparams.p,
}
)
const derivedKey = this.derivedKey(password)
const ciphertext = Buffer.from(this.crypto.ciphertext, 'hex')
const mac = new SHA3(256)
.update(Buffer.concat([derivedKey.slice(16, 32), ciphertext]))
.digest()
.toString('hex')
if (mac !== this.crypto.mac) {
if (Keystore.mac(derivedKey, ciphertext) !== this.crypto.mac) {
throw new IncorrectPassword()
}
const decipher = crypto.createDecipheriv(
Expand All @@ -130,22 +108,31 @@ export default class Keystore {
}

checkPassword = (password: string) => {
const derivedKey = this.derivedKey(password)
const ciphertext = Buffer.from(this.crypto.ciphertext, 'hex')
return Keystore.mac(derivedKey, ciphertext) === this.crypto.mac
}

derivedKey = (password: string) => {
const { kdfparams } = this.crypto
const derivedKey: Buffer = crypto.scryptSync(
Buffer.from(password),
return crypto.scryptSync(
password,
Buffer.from(kdfparams.salt, 'hex'),
kdfparams.dklen,
{
N: kdfparams.n,
r: kdfparams.r,
p: kdfparams.p,
}
Keystore.scryptOptions(kdfparams)
)
const ciphertext = Buffer.from(this.crypto.ciphertext, 'hex')
const mac = new SHA3(256)
.update(Buffer.concat([derivedKey.slice(16, 32), ciphertext]))
.digest()
.toString('hex')
return mac === this.crypto.mac
}

static mac = (derivedKey: Buffer, ciphertext: Buffer) => {
return new Keccak(256).update(Buffer.concat([derivedKey.slice(16, 32), ciphertext])).digest('hex')
}

static scryptOptions = (kdfparams: KdfParams) => {
return {
N: kdfparams.n,
r: kdfparams.r,
p: kdfparams.p,
maxmem: 128 * (kdfparams.n + kdfparams.p + 2) * kdfparams.r,
}
}
}
30 changes: 29 additions & 1 deletion packages/neuron-wallet/tests/models/keys/keystore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('load and check password', () => {
expect(keystore.checkPassword(password)).toBe(true)
})

it('descrypt', () => {
it('decrypts', () => {
expect(keystore.decrypt(password)).toEqual(
new ExtendedPrivateKey(fixture.privateKey, fixture.chainCode).serialize()
)
Expand All @@ -31,3 +31,31 @@ describe('load and check password', () => {
expect(extendedPrivateKey.chainCode).toEqual(fixture.chainCode)
})
})

describe('load ckb cli light keystore', () => {
const password = '123'
const keystoreString =
'{"crypto":{"cipher": "aes-128-ctr", "ciphertext": "253397209cae86474e368720f9baa30f448767047d2cc5a7672ef121861974ed", "cipherparams": {"iv": "8bd8523e0048db3a4ae2534aec6d303a"}, "kdf": "scrypt", "kdfparams": {"dklen": 32, "n": 4096, "p": 6, "r": 8, "salt": "be3d86c99f4895f99d1a0048afb61a34153fa83d5edd033fc914de2c502f57e7"}, "mac": "4453cf5d4f6ec43d0664c3895c4ab9b1c9bcd2d02c7abb190c84375a42739099" },"id": "id", "version": 3}'
const keystore = Keystore.fromJson(keystoreString)

it('checks correct password', () => {
expect(keystore.checkPassword(password)).toBe(true)
})
})

describe('load ckb cli standard keystore', () => {
const password = '123'
const keystoreString =
'{"address":"ea22142fa5be326e834681144ca30326f99a6d5a","crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"29304e5bcbb1885ef5cdcb40b5312b58"},"ciphertext":"93054530a8fbe5b11995acda856585d7362ac7d2b1e4f268c633d997be2d6532c4962501d0835bf52a4693ae7a091ac9bac9297793f4116ef7c123edb00dbc85","kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"724327e67ca321ccf15035bb78a0a05c816bebbe218a0840abdc26da8453c1f4"},"mac":"1d0e5660ffbfc1f9ff4da97aefcfc2153c0ec1b411e35ffee26ee92815cc06f9"},"id":"43c1116e-efd5-4c9e-a86a-3ec0ab163122","version":3}'
const keystore = Keystore.fromJson(keystoreString)

it('checks correct password', () => {
expect(keystore.checkPassword(password)).toBe(true)
})

it('loads private key', () => {
const extendedPrivateKey = keystore.extendedPrivateKey(password)
expect(extendedPrivateKey.privateKey).toEqual('8af124598932440269a81771ad662642e83a38b323b2f70223b8ae0b6c5e0779')
expect(extendedPrivateKey.chainCode).toEqual('615302e2c93151a55c29121dd02ad554e47908a6df6d7374f357092cec11675b')
})
})