Unicode support keystore passwords

This commit is contained in:
Carl Beekhuizen 2020-06-26 16:10:19 +02:00
parent 4f481012d4
commit 39703588d7
No known key found for this signature in database
GPG Key ID: 8F29E54F49E7AAB5
7 changed files with 35 additions and 15 deletions

View File

@ -26,9 +26,9 @@ def get_seed(*, mnemonic: str, password: str='') -> bytes:
"""
Derives the seed for the pre-image root of the tree.
"""
mnemonic = normalize('NFKD', mnemonic)
encoded_mnemonic = normalize('NFKD', mnemonic).encode('utf-8')
salt = normalize('NFKD', 'mnemonic' + password).encode('utf-8')
return PBKDF2(password=mnemonic, salt=salt, dklen=64, c=2048, prf='sha512')
return PBKDF2(password=encoded_mnemonic, salt=salt, dklen=64, c=2048, prf='sha512')
def get_languages(path: str) -> List[str]:

View File

@ -5,16 +5,21 @@ from dataclasses import (
field as dataclass_field
)
import json
from py_ecc.bls import G2ProofOfPossession as bls
from secrets import randbits
from typing import Any, Dict, Union
from unicodedata import normalize
from uuid import uuid4
from eth2deposit.utils.crypto import (
AES_128_CTR,
PBKDF2,
scrypt,
SHA256,
)
from py_ecc.bls import G2ProofOfPossession as bls
from eth2deposit.utils.constants import (
UNICODE_CONTROL_CHARS,
)
hexdigits = set('0123456789abcdef')
@ -91,13 +96,22 @@ class Keystore(BytesDataclass):
version = json_dict['version']
return cls(crypto=crypto, pubkey=pubkey, path=path, uuid=uuid, version=version)
@staticmethod
def _process_password(password: str) -> bytes:
password = normalize('NFKD', password)
password = ''.join(c for c in password if ord(c) not in UNICODE_CONTROL_CHARS)
return password.encode('UTF-8')
@classmethod
def encrypt(cls, *, secret: bytes, password: str, path: str='',
kdf_salt: bytes=randbits(256).to_bytes(32, 'big'),
aes_iv: bytes=randbits(128).to_bytes(16, 'big')) -> 'Keystore':
keystore = cls()
keystore.crypto.kdf.params['salt'] = kdf_salt
decryption_key = keystore.kdf(password=password, **keystore.crypto.kdf.params)
decryption_key = keystore.kdf(
password=cls._process_password(password),
**keystore.crypto.kdf.params
)
keystore.crypto.cipher.params['iv'] = aes_iv
cipher = AES_128_CTR(key=decryption_key[:16], **keystore.crypto.cipher.params)
keystore.crypto.cipher.message = cipher.encrypt(secret)
@ -107,7 +121,10 @@ class Keystore(BytesDataclass):
return keystore
def decrypt(self, password: str) -> bytes:
decryption_key = self.kdf(password=password, **self.crypto.kdf.params)
decryption_key = self.kdf(
password=self._process_password(password),
**self.crypto.kdf.params
)
assert SHA256(decryption_key[16:32] + self.crypto.cipher.message) == self.crypto.checksum.message
cipher = AES_128_CTR(key=decryption_key[:16], **self.crypto.cipher.params)
return cipher.decrypt(self.crypto.cipher.message)

View File

@ -13,3 +13,7 @@ MAX_DEPOSIT_AMOUNT = 2 ** 5 * 10 ** 9
# File/folder constants
WORD_LISTS_PATH = os.path.join('eth2deposit', 'key_handling', 'key_derivation', 'word_lists')
DEFAULT_VALIDATOR_KEYS_FOLDER_NAME = 'validator_keys'
# Sundry constants
UNICODE_CONTROL_CHARS = list(range(0x00, 0x20)) + list(range(0x7F, 0xA0))

View File

@ -24,11 +24,10 @@ def scrypt(*, password: str, salt: str, n: int, r: int, p: int, dklen: int) -> b
return res if isinstance(res, bytes) else res[0] # PyCryptodome can return Tuple[bytes]
def PBKDF2(*, password: str, salt: bytes, dklen: int, c: int, prf: str) -> bytes:
def PBKDF2(*, password: bytes, salt: bytes, dklen: int, c: int, prf: str) -> bytes:
assert('sha' in prf)
_hash = _sha256 if 'sha256' in prf else _sha512
password_bytes = password.encode("utf-8")
res = _PBKDF2(password=password_bytes, salt=salt, dkLen=dklen, count=c, hmac_hash_module=_hash) # type: ignore
res = _PBKDF2(password=password, salt=salt, dkLen=dklen, count=c, hmac_hash_module=_hash) # type: ignore
return res if isinstance(res, bytes) else res[0] # PyCryptodome can return Tuple[bytes]

View File

@ -13,18 +13,18 @@
"checksum": {
"function": "sha256",
"params": {},
"message": "18b148af8e52920318084560fd766f9d09587b4915258dec0676cba5b0da09d8"
"message": "8a9f5d9912ed7e75ea794bc5a89bca5f193721d30868ade6f73043c6ea6febf1"
},
"cipher": {
"function": "aes-128-ctr",
"params": {
"iv": "264daa3f303d7259501c93d997d84fe6"
},
"message": "a9249e0ca7315836356e4c7440361ff22b9fe71e2e2ed34fc1eb03976924ed48"
"message": "cee03fde2af33149775b7223e7845e4fb2c8ae1792e5f99fe9ecf474cc8c16ad"
}
},
"pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07",
"path": "m/12381/60/3141592653589793238/4626433832795028841",
"path": "m/12381/60/0/0",
"uuid": "64625def-3331-4eea-ab6f-782f3ed16a83",
"version": 4
}

View File

@ -14,18 +14,18 @@
"checksum": {
"function": "sha256",
"params": {},
"message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb"
"message": "d2217fe5f3e9a1e34581ef8a78f7c9928e436d36dacc5e846690a5581e8ea484"
},
"cipher": {
"function": "aes-128-ctr",
"params": {
"iv": "264daa3f303d7259501c93d997d84fe6"
},
"message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30"
"message": "06ae90d55fe0a6e9c5c3bc5b170827b2e5cce3929ed3f116c2811e6366dfe20f"
}
},
"pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07",
"path": "m/12381/60/0/0",
"path": "m/12381/60/3141592653/589793238",
"uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f",
"version": 4
}

View File

@ -7,7 +7,7 @@ from eth2deposit.key_handling.keystore import (
Pbkdf2Keystore,
)
test_vector_password = 'testpassword'
test_vector_password = '𝔱𝔢𝔰𝔱𝔭𝔞𝔰𝔰𝔴𝔬𝔯𝔡🔑'
test_vector_secret = bytes.fromhex('000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f')
test_vector_folder = os.path.join(os.getcwd(), 'tests', 'test_key_handling', 'keystore_test_vectors')
_, _, test_vector_files = next(os.walk(test_vector_folder)) # type: ignore