diff --git a/eth2deposit/key_handling/key_derivation/mnemonic.py b/eth2deposit/key_handling/key_derivation/mnemonic.py index ef8e93c..4c8dd62 100644 --- a/eth2deposit/key_handling/key_derivation/mnemonic.py +++ b/eth2deposit/key_handling/key_derivation/mnemonic.py @@ -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]: diff --git a/eth2deposit/key_handling/keystore.py b/eth2deposit/key_handling/keystore.py index 7d70fae..5a1ac63 100644 --- a/eth2deposit/key_handling/keystore.py +++ b/eth2deposit/key_handling/keystore.py @@ -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) diff --git a/eth2deposit/utils/constants.py b/eth2deposit/utils/constants.py index 52a25e6..69b1236 100644 --- a/eth2deposit/utils/constants.py +++ b/eth2deposit/utils/constants.py @@ -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)) diff --git a/eth2deposit/utils/crypto.py b/eth2deposit/utils/crypto.py index b66befb..5a850bb 100644 --- a/eth2deposit/utils/crypto.py +++ b/eth2deposit/utils/crypto.py @@ -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] diff --git a/tests/test_key_handling/keystore_test_vectors/test0.json b/tests/test_key_handling/keystore_test_vectors/test0.json index 4ef33f7..9e996b2 100644 --- a/tests/test_key_handling/keystore_test_vectors/test0.json +++ b/tests/test_key_handling/keystore_test_vectors/test0.json @@ -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 } \ No newline at end of file diff --git a/tests/test_key_handling/keystore_test_vectors/test1.json b/tests/test_key_handling/keystore_test_vectors/test1.json index 2f87b4b..7f7a25b 100644 --- a/tests/test_key_handling/keystore_test_vectors/test1.json +++ b/tests/test_key_handling/keystore_test_vectors/test1.json @@ -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 } \ No newline at end of file diff --git a/tests/test_key_handling/test_keystore.py b/tests/test_key_handling/test_keystore.py index 5f321c1..d5994ec 100644 --- a/tests/test_key_handling/test_keystore.py +++ b/tests/test_key_handling/test_keystore.py @@ -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