mirror of https://github.com/status-im/nim-eth.git
add scrypt kdf to keyfile and implement test for it
This commit is contained in:
parent
765883c454
commit
7afa22cf41
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
import nimcrypto/[bcmode, hmac, rijndael, pbkdf2, sha2, sysrand, utils, keccak],
|
import nimcrypto/[bcmode, hmac, rijndael, pbkdf2, sha2, sysrand, utils, keccak, scrypt],
|
||||||
eth/keys, json, uuid, strutils, stew/results
|
eth/keys, json, uuid, strutils, stew/results
|
||||||
|
|
||||||
export results
|
export results
|
||||||
|
@ -43,6 +43,8 @@ type
|
||||||
CipherNotSupported = "kf: `cipher` parameter is not supported"
|
CipherNotSupported = "kf: `cipher` parameter is not supported"
|
||||||
IncorrectMac = "kf: `mac` verification failed"
|
IncorrectMac = "kf: `mac` verification failed"
|
||||||
IncorrectPrivateKey = "kf: incorrect private key"
|
IncorrectPrivateKey = "kf: incorrect private key"
|
||||||
|
IncorrectN = "kf: incorrect N value for scrypt"
|
||||||
|
ScryptBadParam = "kf: bad scrypt's parameters"
|
||||||
OsError = "kf: OS specific error"
|
OsError = "kf: OS specific error"
|
||||||
JsonError = "kf: JSON encoder/decoder error"
|
JsonError = "kf: JSON encoder/decoder error"
|
||||||
|
|
||||||
|
@ -59,6 +61,32 @@ type
|
||||||
CipherNoSupport, ## Cipher not supported
|
CipherNoSupport, ## Cipher not supported
|
||||||
AES128CTR ## AES-128-CTR
|
AES128CTR ## AES-128-CTR
|
||||||
|
|
||||||
|
CipherParams = object
|
||||||
|
iv: seq[byte]
|
||||||
|
|
||||||
|
Cipher = object
|
||||||
|
kind: CryptKind
|
||||||
|
params: CipherParams
|
||||||
|
text: seq[byte]
|
||||||
|
|
||||||
|
Crypto = object
|
||||||
|
kind: KdfKind
|
||||||
|
cipher: Cipher
|
||||||
|
kdfParams: JsonNode
|
||||||
|
mac: seq[byte]
|
||||||
|
|
||||||
|
ScryptParams* = object
|
||||||
|
dklen: int
|
||||||
|
n, p, r: int
|
||||||
|
salt: string
|
||||||
|
|
||||||
|
Pbkdf2Params* = object
|
||||||
|
dklen: int
|
||||||
|
c: int
|
||||||
|
prf: HashKind
|
||||||
|
salt: string
|
||||||
|
|
||||||
|
DKey = array[DKLen, byte]
|
||||||
KfResult*[T] = Result[T, KeyFileError]
|
KfResult*[T] = Result[T, KeyFileError]
|
||||||
|
|
||||||
proc mapErrTo[T, E](r: Result[T, E], v: static KeyFileError): KfResult[T] =
|
proc mapErrTo[T, E](r: Result[T, E], v: static KeyFileError): KfResult[T] =
|
||||||
|
@ -113,9 +141,9 @@ proc deriveKey(password: string,
|
||||||
salt: string,
|
salt: string,
|
||||||
kdfkind: KdfKind,
|
kdfkind: KdfKind,
|
||||||
hashkind: HashKind,
|
hashkind: HashKind,
|
||||||
workfactor: int): KfResult[array[DKLen, byte]] =
|
workfactor: int): KfResult[DKey] =
|
||||||
if kdfkind == PBKDF2:
|
if kdfkind == PBKDF2:
|
||||||
var output: array[DKLen, byte]
|
var output: DKey
|
||||||
var c = if workfactor == 0: Pbkdf2WorkFactor else: workfactor
|
var c = if workfactor == 0: Pbkdf2WorkFactor else: workfactor
|
||||||
case hashkind
|
case hashkind
|
||||||
of HashSHA2_224:
|
of HashSHA2_224:
|
||||||
|
@ -171,6 +199,16 @@ proc deriveKey(password: string,
|
||||||
else:
|
else:
|
||||||
err(NotImplemented)
|
err(NotImplemented)
|
||||||
|
|
||||||
|
proc deriveKey(password: string, salt: string,
|
||||||
|
workFactor, r, p: int): KfResult[DKey] =
|
||||||
|
|
||||||
|
let wf = if workFactor == 0: ScryptWorkFactor else: workFactor
|
||||||
|
var output: DKey
|
||||||
|
if scrypt(password, salt, wf, r, p, output) == 0:
|
||||||
|
return err(ScryptBadParam)
|
||||||
|
|
||||||
|
result = ok(output)
|
||||||
|
|
||||||
proc encryptKey(seckey: PrivateKey,
|
proc encryptKey(seckey: PrivateKey,
|
||||||
cryptkind: CryptKind,
|
cryptkind: CryptKind,
|
||||||
key: openarray[byte],
|
key: openarray[byte],
|
||||||
|
@ -279,11 +317,11 @@ proc createKeyFileJson*(seckey: PrivateKey,
|
||||||
|
|
||||||
let u = ? uuidGenerate().mapErrTo(UuidError)
|
let u = ? uuidGenerate().mapErrTo(UuidError)
|
||||||
|
|
||||||
if kdfkind != PBKDF2:
|
|
||||||
return err(NotImplemented)
|
|
||||||
|
|
||||||
let
|
let
|
||||||
dkey = ? deriveKey(password, saltstr, kdfkind, HashSHA2_256, workfactor)
|
dkey = case kdfkind
|
||||||
|
of PBKDF2: ? deriveKey(password, saltstr, kdfkind, HashSHA2_256, workfactor)
|
||||||
|
of SCRYPT: ? deriveKey(password, saltstr, workfactor, ScryptR, ScryptP)
|
||||||
|
|
||||||
ciphertext = ? encryptKey(seckey, cryptkind, dkey, iv)
|
ciphertext = ? encryptKey(seckey, cryptkind, dkey, iv)
|
||||||
|
|
||||||
var ctx: keccak256
|
var ctx: keccak256
|
||||||
|
@ -313,11 +351,8 @@ proc createKeyFileJson*(seckey: PrivateKey,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
proc decodeKeyFileJson*(j: JsonNode,
|
proc decodeCrypto(n: JsonNode): KfResult[Crypto] =
|
||||||
password: string): KfResult[PrivateKey] =
|
var crypto = n.getOrDefault("crypto")
|
||||||
## Decode private key into ``seckey`` from keyfile json object ``j`` using
|
|
||||||
## password string ``password``.
|
|
||||||
var crypto = j.getOrDefault("crypto")
|
|
||||||
if isNil(crypto):
|
if isNil(crypto):
|
||||||
return err(MalformedError)
|
return err(MalformedError)
|
||||||
|
|
||||||
|
@ -325,57 +360,122 @@ proc decodeKeyFileJson*(j: JsonNode,
|
||||||
if isNil(kdf):
|
if isNil(kdf):
|
||||||
return err(MalformedError)
|
return err(MalformedError)
|
||||||
|
|
||||||
|
var c: Crypto
|
||||||
|
case kdf.getStr()
|
||||||
|
of "pbkdf2": c.kind = PBKDF2
|
||||||
|
of "scrypt": c.kind = SCRYPT
|
||||||
|
else: return err(KdfNotSupported)
|
||||||
|
|
||||||
var cipherparams = crypto.getOrDefault("cipherparams")
|
var cipherparams = crypto.getOrDefault("cipherparams")
|
||||||
if isNil(cipherparams):
|
if isNil(cipherparams):
|
||||||
return err(MalformedError)
|
return err(MalformedError)
|
||||||
|
|
||||||
if kdf.getStr() == "pbkdf2":
|
c.cipher.kind = getCipher(crypto.getOrDefault("cipher").getStr())
|
||||||
var params = crypto.getOrDefault("kdfparams")
|
c.cipher.params.iv = decodeHex(cipherparams.getOrDefault("iv").getStr())
|
||||||
|
c.cipher.text = decodeHex(crypto.getOrDefault("ciphertext").getStr())
|
||||||
|
c.mac = decodeHex(crypto.getOrDefault("mac").getStr())
|
||||||
|
c.kdfParams = crypto.getOrDefault("kdfparams")
|
||||||
|
|
||||||
if isNil(params):
|
if c.cipher.kind == CipherNoSupport:
|
||||||
|
return err(CipherNotSupported)
|
||||||
|
if len(c.cipher.text) == 0:
|
||||||
|
return err(EmptyCiphertext)
|
||||||
|
if len(c.cipher.text) != KeyLength:
|
||||||
|
return err(IncorrectPrivateKey)
|
||||||
|
if len(c.mac) == 0:
|
||||||
|
return err(EmptyMac)
|
||||||
|
if isNil(c.kdfParams):
|
||||||
return err(MalformedError)
|
return err(MalformedError)
|
||||||
|
|
||||||
var salt = decodeSalt(params.getOrDefault("salt").getStr())
|
result = ok(c)
|
||||||
var ciphertext = decodeHex(crypto.getOrDefault("ciphertext").getStr())
|
|
||||||
var mactext = decodeHex(crypto.getOrDefault("mac").getStr())
|
|
||||||
var cryptkind = getCipher(crypto.getOrDefault("cipher").getStr())
|
|
||||||
var iv = decodeHex(cipherparams.getOrDefault("iv").getStr())
|
|
||||||
|
|
||||||
if len(salt) == 0:
|
proc decodePbkdf2Params(params: JsonNode): KfResult[Pbkdf2Params] =
|
||||||
|
var p: Pbkdf2Params
|
||||||
|
p.salt = decodeSalt(params.getOrDefault("salt").getStr())
|
||||||
|
if len(p.salt) == 0:
|
||||||
return err(EmptySalt)
|
return err(EmptySalt)
|
||||||
if len(ciphertext) == 0:
|
|
||||||
return err(EmptyCiphertext)
|
|
||||||
if len(mactext) == 0:
|
|
||||||
return err(EmptyMac)
|
|
||||||
if cryptkind == CipherNoSupport:
|
|
||||||
return err(CipherNotSupported)
|
|
||||||
|
|
||||||
var dklen = params.getOrDefault("dklen").getInt()
|
p.dklen = params.getOrDefault("dklen").getInt()
|
||||||
var c = params.getOrDefault("c").getInt()
|
p.c = params.getOrDefault("c").getInt()
|
||||||
var hash = getPrfHash(params.getOrDefault("prf").getStr())
|
p.prf = getPrfHash(params.getOrDefault("prf").getStr())
|
||||||
|
|
||||||
if hash == HashNoSupport:
|
if p.prf == HashNoSupport:
|
||||||
return err(PrfNotSupported)
|
return err(PrfNotSupported)
|
||||||
if dklen == 0 or dklen > MaxDKLen:
|
if p.dklen == 0 or p.dklen > MaxDKLen:
|
||||||
return err(IncorrectDKLen)
|
return err(IncorrectDKLen)
|
||||||
if len(ciphertext) != KeyLength:
|
result = ok(p)
|
||||||
return err(IncorrectPrivateKey)
|
|
||||||
|
|
||||||
let dkey = ? deriveKey(password, salt, PBKDF2, hash, c)
|
proc decodeScryptParams(params: JsonNode): KfResult[ScryptParams] =
|
||||||
|
var p: ScryptParams
|
||||||
|
p.salt = decodeSalt(params.getOrDefault("salt").getStr())
|
||||||
|
if len(p.salt) == 0:
|
||||||
|
return err(EmptySalt)
|
||||||
|
|
||||||
|
p.dklen = params.getOrDefault("dklen").getInt()
|
||||||
|
p.n = params.getOrDefault("n").getInt()
|
||||||
|
p.p = params.getOrDefault("p").getInt()
|
||||||
|
p.r = params.getOrDefault("r").getInt()
|
||||||
|
|
||||||
|
if p.n <= 1 or (p.n and (p.n-1)) != 0:
|
||||||
|
return err(IncorrectN)
|
||||||
|
if p.dklen == 0 or p.dklen > MaxDKLen:
|
||||||
|
return err(IncorrectDKLen)
|
||||||
|
|
||||||
|
const
|
||||||
|
maxInt = high(int64)
|
||||||
|
maxIntd128 = maxInt div 128
|
||||||
|
maxIntd256 = maxInt div 256
|
||||||
|
|
||||||
|
let
|
||||||
|
badParam1 = uint64(p.r)*uint64(p.p) >= 1 shl 30
|
||||||
|
badParam2 = p.r > maxIntd128 div p.p
|
||||||
|
badParam3 = p.r > maxIntd256
|
||||||
|
badParam4 = p.n > maxIntd128 div p.r
|
||||||
|
|
||||||
|
if badParam1 or badParam2 or badParam3 or badParam4:
|
||||||
|
return err(ScryptBadParam)
|
||||||
|
|
||||||
|
result = ok(p)
|
||||||
|
|
||||||
|
func decryptPrivateKey(crypto: Crypto, dkey: DKey): KfResult[PrivateKey] =
|
||||||
var ctx: keccak256
|
var ctx: keccak256
|
||||||
ctx.init()
|
ctx.init()
|
||||||
ctx.update(toOpenArray(dkey, 16, 31))
|
ctx.update(toOpenArray(dkey, 16, 31))
|
||||||
ctx.update(ciphertext)
|
ctx.update(crypto.cipher.text)
|
||||||
var mac = ctx.finish()
|
var mac = ctx.finish()
|
||||||
if not compareMac(mac.data, mactext):
|
if not compareMac(mac.data, crypto.mac):
|
||||||
return err(IncorrectMac)
|
return err(IncorrectMac)
|
||||||
|
|
||||||
let plaintext = ? decryptKey(ciphertext, cryptkind, dkey, iv)
|
let plaintext = ? decryptKey(crypto.cipher.text, crypto.cipher.kind, dkey, crypto.cipher.params.iv)
|
||||||
|
|
||||||
PrivateKey.fromRaw(plaintext).mapErrTo(IncorrectPrivateKey)
|
PrivateKey.fromRaw(plaintext).mapErrTo(IncorrectPrivateKey)
|
||||||
else:
|
|
||||||
err(KdfNotSupported)
|
proc decodeKeyFileJson*(j: JsonNode,
|
||||||
|
password: string): KfResult[PrivateKey] =
|
||||||
|
## Decode private key into ``seckey`` from keyfile json object ``j`` using
|
||||||
|
## password string ``password``.
|
||||||
|
let res = decodeCrypto(j)
|
||||||
|
if res.isErr:
|
||||||
|
return err(res.error)
|
||||||
|
let crypto = res.get()
|
||||||
|
|
||||||
|
case crypto.kind
|
||||||
|
of PBKDF2:
|
||||||
|
let res = decodePbkdf2Params(crypto.kdfParams)
|
||||||
|
if res.isErr:
|
||||||
|
return err(res.error)
|
||||||
|
|
||||||
|
let params = res.get()
|
||||||
|
let dkey = ? deriveKey(password, params.salt, PBKDF2, params.prf, params.c)
|
||||||
|
result = decryptPrivateKey(crypto, dkey)
|
||||||
|
|
||||||
|
of SCRYPT:
|
||||||
|
let res = decodeScryptParams(crypto.kdfParams)
|
||||||
|
if res.isErr:
|
||||||
|
return err(res.error)
|
||||||
|
|
||||||
|
let params = res.get()
|
||||||
|
let dkey = ? deriveKey(password, params.salt, params.n, params.r, params.p)
|
||||||
|
result = decryptPrivateKey(crypto, dkey)
|
||||||
|
|
||||||
proc loadKeyFile*(pathname: string,
|
proc loadKeyFile*(pathname: string,
|
||||||
password: string): KfResult[PrivateKey] =
|
password: string): KfResult[PrivateKey] =
|
||||||
|
|
|
@ -84,6 +84,83 @@ var TestVectors = [
|
||||||
"name": "evilnonce",
|
"name": "evilnonce",
|
||||||
"password": "bar",
|
"password": "bar",
|
||||||
"priv": "0202020202020202020202020202020202020202020202020202020202020202"
|
"priv": "0202020202020202020202020202020202020202020202020202020202020202"
|
||||||
|
},
|
||||||
|
%*{
|
||||||
|
"keyfile": {
|
||||||
|
"version" : 3,
|
||||||
|
"crypto" : {
|
||||||
|
"cipher" : "aes-128-ctr",
|
||||||
|
"cipherparams" : {
|
||||||
|
"iv" : "83dbcc02d8ccb40e466191a123791e0e"
|
||||||
|
},
|
||||||
|
"ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c",
|
||||||
|
"kdf" : "scrypt",
|
||||||
|
"kdfparams" : {
|
||||||
|
"dklen" : 32,
|
||||||
|
"n" : 262144,
|
||||||
|
"r" : 1,
|
||||||
|
"p" : 8,
|
||||||
|
"salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19"
|
||||||
|
},
|
||||||
|
"mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097"
|
||||||
|
},
|
||||||
|
"id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6"
|
||||||
|
},
|
||||||
|
"name" : "test2",
|
||||||
|
"password": "testpassword",
|
||||||
|
"priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d"
|
||||||
|
},
|
||||||
|
%*{
|
||||||
|
"keyfile": {
|
||||||
|
"version": 3,
|
||||||
|
"address": "460121576cc7df020759730751f92bd62fd78dd6",
|
||||||
|
"crypto": {
|
||||||
|
"ciphertext": "54ae683c6287fa3d58321f09d56e26d94e58a00d4f90bdd95782ae0e4aab618b",
|
||||||
|
"cipherparams": {
|
||||||
|
"iv": "681679cdb125bba9495d068b002816a4"
|
||||||
|
},
|
||||||
|
"cipher": "aes-128-ctr",
|
||||||
|
"kdf": "scrypt",
|
||||||
|
"kdfparams": {
|
||||||
|
"dklen": 32,
|
||||||
|
"salt": "c3407f363fce02a66e3c4bf4a8f6b7da1c1f54266cef66381f0625c251c32785",
|
||||||
|
"n": 8192,
|
||||||
|
"r": 8,
|
||||||
|
"p": 1
|
||||||
|
},
|
||||||
|
"mac": "dea6bdf22a2f522166ed82808c22a6311e84c355f4bbe100d4260483ff675a46"
|
||||||
|
},
|
||||||
|
"id": "0eb785e0-340a-4290-9c42-90a11973ee47"
|
||||||
|
},
|
||||||
|
"name": "mycrypto",
|
||||||
|
"password": "foobartest121",
|
||||||
|
"priv": "05a4d3eb46c742cb8850440145ce70cbc80b59f891cf5f50fd3e9c280b50c4e4"
|
||||||
|
},
|
||||||
|
%*{
|
||||||
|
"keyfile": {
|
||||||
|
"crypto": {
|
||||||
|
"cipher": "aes-128-ctr",
|
||||||
|
"cipherparams": {
|
||||||
|
"iv": "7e7b02d2b4ef45d6c98cb885e75f48d5",
|
||||||
|
},
|
||||||
|
"ciphertext": "a7a5743a6c7eb3fa52396bd3fd94043b79075aac3ccbae8e62d3af94db00397c",
|
||||||
|
"kdf": "scrypt",
|
||||||
|
"kdfparams": {
|
||||||
|
"dklen": 32,
|
||||||
|
"n": 8192,
|
||||||
|
"p": 1,
|
||||||
|
"r": 8,
|
||||||
|
"salt": "247797c7a357b707a3bdbfaa55f4c553756bca09fec20ddc938e7636d21e4a20",
|
||||||
|
},
|
||||||
|
"mac": "5a3ba5bebfda2c384586eda5fcda9c8397d37c9b0cc347fea86525cf2ea3a468",
|
||||||
|
},
|
||||||
|
"address": "0b6f2de3dee015a95d3330dcb7baf8e08aa0112d",
|
||||||
|
"id": "3c8efdd6-d538-47ec-b241-36783d3418b9",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"password": "moomoocow",
|
||||||
|
"priv": "21eac69b9a52f466bfe9047f0f21c9caf3a5cdaadf84e2750a9b3265d450d481",
|
||||||
|
"name": "eth-keyfile-conftest"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -115,6 +192,39 @@ suite "KeyFile test suite":
|
||||||
"wrongpassword")
|
"wrongpassword")
|
||||||
check:
|
check:
|
||||||
seckey.error == KeyFileError.IncorrectMac
|
seckey.error == KeyFileError.IncorrectMac
|
||||||
|
|
||||||
|
|
||||||
|
test "KeyStoreTests/basic_tests.json test2":
|
||||||
|
var expectkey = PrivateKey.fromHex(TestVectors[3].getOrDefault("priv").getStr())[]
|
||||||
|
let seckey =
|
||||||
|
decodeKeyFileJson(TestVectors[3].getOrDefault("keyfile"),
|
||||||
|
TestVectors[3].getOrDefault("password").getStr())[]
|
||||||
|
check:
|
||||||
|
seckey.toRaw == expectkey.toRaw
|
||||||
|
|
||||||
|
test "KeyStoreTests/basic_tests.json mycrypto":
|
||||||
|
var expectkey = PrivateKey.fromHex(TestVectors[4].getOrDefault("priv").getStr())[]
|
||||||
|
let seckey =
|
||||||
|
decodeKeyFileJson(TestVectors[4].getOrDefault("keyfile"),
|
||||||
|
TestVectors[4].getOrDefault("password").getStr())[]
|
||||||
|
check:
|
||||||
|
seckey.toRaw == expectkey.toRaw
|
||||||
|
|
||||||
|
test "eth-key/conftest.py":
|
||||||
|
var expectkey = PrivateKey.fromHex(TestVectors[5].getOrDefault("priv").getStr())[]
|
||||||
|
let seckey =
|
||||||
|
decodeKeyFileJson(TestVectors[5].getOrDefault("keyfile"),
|
||||||
|
TestVectors[5].getOrDefault("password").getStr())[]
|
||||||
|
check:
|
||||||
|
seckey.toRaw == expectkey.toRaw
|
||||||
|
|
||||||
|
test "eth-key/conftest.py with wrong password":
|
||||||
|
let seckey =
|
||||||
|
decodeKeyFileJson(TestVectors[5].getOrDefault("keyfile"),
|
||||||
|
"wrongpassword")
|
||||||
|
check:
|
||||||
|
seckey.error == KeyFileError.IncorrectMac
|
||||||
|
|
||||||
test "Create/Save/Load test":
|
test "Create/Save/Load test":
|
||||||
var seckey0 = PrivateKey.random(rng[])
|
var seckey0 = PrivateKey.random(rng[])
|
||||||
let jobject = createKeyFileJson(seckey0, "randompassword")[]
|
let jobject = createKeyFileJson(seckey0, "randompassword")[]
|
||||||
|
@ -125,6 +235,15 @@ suite "KeyFile test suite":
|
||||||
check:
|
check:
|
||||||
seckey0.toRaw == seckey1.toRaw
|
seckey0.toRaw == seckey1.toRaw
|
||||||
removeFile("test.keyfile")
|
removeFile("test.keyfile")
|
||||||
|
|
||||||
|
test "Scrypt roundtrip":
|
||||||
|
let
|
||||||
|
seckey1 = PrivateKey.random(rng[])
|
||||||
|
jobject = createKeyFileJson(seckey1, "miawmiawcat", 3, AES128CTR, SCRYPT)[]
|
||||||
|
privKey = decodeKeyFileJson(jobject, "miawmiawcat")[]
|
||||||
|
|
||||||
|
check privKey.toRaw == secKey1.toRaw
|
||||||
|
|
||||||
test "Load non-existent pathname test":
|
test "Load non-existent pathname test":
|
||||||
check:
|
check:
|
||||||
loadKeyFile("nonexistant.keyfile", "password").error ==
|
loadKeyFile("nonexistant.keyfile", "password").error ==
|
||||||
|
|
Loading…
Reference in New Issue