diff --git a/duniterpy/helpers.py b/duniterpy/helpers.py index 5e7f68b01605c2d4808ec56c8cf696fb5b7bb8b8..251a53f095f9838880836784e23d3066d93467ad 100644 --- a/duniterpy/helpers.py +++ b/duniterpy/helpers.py @@ -25,3 +25,17 @@ def ensure_str(data: Union[str, bytes]) -> str: return str(data, 'utf-8') return data + + +def xor_bytes(b1: bytes, b2: bytes) -> bytearray: + """ + Apply XOR operation on two bytes arguments + + :param b1: First bytes argument + :param b2: Second bytes argument + :rtype bytearray: + """ + result = bytearray() + for i1, i2 in zip(b1, b2): + result.append(i1 ^ i2) + return result diff --git a/duniterpy/key/signing_key.py b/duniterpy/key/signing_key.py index e2cfbe8a940a5b8da3884cf5a08b59f1dec925c6..cff0452bc209f6412042603b853d5016b1428494 100644 --- a/duniterpy/key/signing_key.py +++ b/duniterpy/key/signing_key.py @@ -7,10 +7,11 @@ from re import compile, MULTILINE, search from typing import Optional, Union, TypeVar, Type import libnacl.sign +import pyaes from pylibscrypt import scrypt from .base58 import Base58Encoder -from ..helpers import ensure_bytes +from ..helpers import ensure_bytes, xor_bytes SEED_LENGTH = 32 # Length of the key crypto_sign_BYTES = 64 @@ -81,7 +82,7 @@ class SigningKey(libnacl.sign.Signer): @classmethod def from_wif_file(cls: Type[SigningKeyType], path: str) -> SigningKeyType: """ - Return SigningKey instance from Duniter WIF v1 file + Return SigningKey instance from Duniter WIF file :param path: Path to WIF file """ @@ -113,17 +114,17 @@ class SigningKey(libnacl.sign.Signer): return cls(seed) - def save_wif(self, path: str) -> None: + def save_wif_file(self, path: str) -> None: """ - Save a Wallet Import Format file (v1) + Save a Wallet Import Format file (WIF) v1 :param path: Path to file """ # Cesium v1 version = 1 - # add version to seed - seed_fi = version.to_bytes(version, 'little') + self.seed + # add format to seed (1=WIF,2=EWIF) + seed_fi = b"\x01" + self.seed # calculate checksum sha256_v1 = libnacl.crypto_hash_sha256(seed_fi) @@ -136,5 +137,116 @@ class SigningKey(libnacl.sign.Signer): fh.write( """Type: WIF Version: {version} -Data: {data}""".format(version=1, data=wif_key) +Data: {data}""".format(version=version, data=wif_key) + ) + + @classmethod + def from_ewif_file(cls: Type[SigningKeyType], path: str, password: str) -> SigningKeyType: + """ + Return SigningKey instance from Duniter EWIF file + + :param path: Path to WIF file + :param password: Password of the encrypted seed + """ + with open(path, 'r') as fh: + wif_content = fh.read() + + regex = compile('Data: ([1-9A-HJ-NP-Za-km-z]+)', MULTILINE) + match = search(regex, wif_content) + if not match: + raise Exception('Error: Bad format EWIF v1 file') + + ewif_hex = match.groups()[0] + ewif_bytes = Base58Encoder.decode(ewif_hex) + if len(ewif_bytes) != 39: + raise Exception("Error: the size of EWIF is invalid") + + fi = ewif_bytes[0:1] + checksum_from_ewif = ewif_bytes[-2:] + ewif_no_checksum = ewif_bytes[0:-2] + salt = ewif_bytes[1:5] + encryptedhalf1 = ewif_bytes[5:21] + encryptedhalf2 = ewif_bytes[21:37] + + if fi != b"\x02": + raise Exception("Error: bad EWIF version") + + # checksum control + checksum = libnacl.crypto_hash_sha256(libnacl.crypto_hash_sha256(ewif_no_checksum))[0:2] + if checksum_from_ewif != checksum: + raise Exception("Error: bad checksum of the EWIF") + + # SCRYPT + password_bytes = password.encode("utf-8") + scrypt_seed = scrypt(password_bytes, salt, 16384, 8, 8, 64) + derivedhalf1 = scrypt_seed[0:32] + derivedhalf2 = scrypt_seed[32:64] + + # AES + aes = pyaes.AESModeOfOperationECB(derivedhalf2) + decryptedhalf1 = aes.decrypt(encryptedhalf1) + decryptedhalf2 = aes.decrypt(encryptedhalf2) + + # XOR + seed1 = xor_bytes(decryptedhalf1, derivedhalf1[0:16]) + seed2 = xor_bytes(decryptedhalf2, derivedhalf1[16:32]) + seed = bytes(seed1 + seed2) + + # Password Control + signer = SigningKey(seed) + salt_from_seed = libnacl.crypto_hash_sha256( + libnacl.crypto_hash_sha256( + Base58Encoder.decode(signer.pubkey)))[0:4] + if salt_from_seed != salt: + raise Exception("Error: bad Password of EWIF address") + + return cls(seed) + + def save_ewif_file(self, path: str, password: str) -> None: + """ + Save an Encrypted Wallet Import Format file (WIF v2) + + :param path: Path to file + :param password: + """ + # WIF Format version + version = 1 + + # add version to seed + salt = libnacl.crypto_hash_sha256( + libnacl.crypto_hash_sha256( + Base58Encoder.decode(self.pubkey)))[0:4] + + # SCRYPT + password_bytes = password.encode("utf-8") + scrypt_seed = scrypt(password_bytes, salt, 16384, 8, 8, 64) + derivedhalf1 = scrypt_seed[0:32] + derivedhalf2 = scrypt_seed[32:64] + + # XOR + seed1_xor_derivedhalf1_1 = bytes(xor_bytes(self.seed[0:16], derivedhalf1[0:16])) + seed2_xor_derivedhalf1_2 = bytes(xor_bytes(self.seed[16:32], derivedhalf1[16:32])) + + # AES + aes = pyaes.AESModeOfOperationECB(derivedhalf2) + encryptedhalf1 = aes.encrypt(seed1_xor_derivedhalf1_1) + encryptedhalf2 = aes.encrypt(seed2_xor_derivedhalf1_2) + + # add format to final seed (1=WIF,2=EWIF) + seed_bytes = b'\x02' + salt + encryptedhalf1 + encryptedhalf2 + + # calculate checksum + sha256_v1 = libnacl.crypto_hash_sha256(seed_bytes) + sha256_v2 = libnacl.crypto_hash_sha256(sha256_v1) + checksum = sha256_v2[0:2] + + # B58 encode final key string + ewif_key = Base58Encoder.encode(seed_bytes + checksum) + + # save file + with open(path, 'w') as fh: + fh.write( + """Type: EWIF +Version: {version} +Data: {data}""".format(version=version, data=ewif_key) ) diff --git a/examples/save_and_load_private_key_file_ewif.py b/examples/save_and_load_private_key_file_ewif.py new file mode 100644 index 0000000000000000000000000000000000000000..6fe0a68208dd6b19eac2a71b79e44c0db92031c4 --- /dev/null +++ b/examples/save_and_load_private_key_file_ewif.py @@ -0,0 +1,61 @@ +from duniterpy.key import SigningKey +import getpass +import os + +if "XDG_CONFIG_HOME" in os.environ: + home_path = os.environ["XDG_CONFIG_HOME"] +elif "HOME" in os.environ: + home_path = os.environ["HOME"] +elif "APPDATA" in os.environ: + home_path = os.environ["APPDATA"] +else: + home_path = os.path.dirname(__file__) + +# CONFIG ####################################### + +# WARNING : Hide this file in a safe and secure place +# If one day you forget your credentials, +# you'll have to use one of your private keys instead +PRIVATE_KEY_FILE_PATH = os.path.join(home_path, ".duniter_account_ewif_v1.duniterkey") + +################################################ + +# prompt hidden user entry +salt = getpass.getpass("Enter your passphrase (salt): ") + +# prompt hidden user entry +password = getpass.getpass("Enter your password: ") + +# prompt public key +pubkey = input("Enter your public key: ") + +# init signer instance +signer = SigningKey.from_credentials(salt, password) + +# check public key +if signer.pubkey != pubkey: + print("Bad credentials!") + exit(1) + +# prompt hidden user entry +ewif_password = getpass.getpass("Enter an encryption password: ") + +# save private key in a file (EWIF v1 format) +signer.save_ewif_file(PRIVATE_KEY_FILE_PATH, ewif_password) + +# document saved +print("Private key for public key %s saved in %s" % (signer.pubkey, PRIVATE_KEY_FILE_PATH)) + +try: + # load private keys from file + loaded_signer = SigningKey.from_ewif_file(PRIVATE_KEY_FILE_PATH, ewif_password) + + # check public key from file + print("Public key %s loaded from file %s" % (loaded_signer.pubkey, PRIVATE_KEY_FILE_PATH)) + +except Exception as e: + print(e) + exit(1) + + +exit(0) diff --git a/examples/save_and_load_private_key_file_wif.py b/examples/save_and_load_private_key_file_wif.py index 944269adaabe2881acaeba318c87caded6d40275..568b9a4157b2bf3b5199b8b9534d8dc32dd6120a 100644 --- a/examples/save_and_load_private_key_file_wif.py +++ b/examples/save_and_load_private_key_file_wif.py @@ -1,5 +1,4 @@ from duniterpy.key import SigningKey -from libnacl.utils import load_key import getpass import os @@ -39,7 +38,7 @@ if signer.pubkey != pubkey: exit(1) # save private key in a file (WIF v1 format) -signer.save_wif(PRIVATE_KEY_FILE_PATH) +signer.save_wif_file(PRIVATE_KEY_FILE_PATH) # document saved print("Private key for public key %s saved in %s" % (signer.pubkey, PRIVATE_KEY_FILE_PATH)) diff --git a/requirements.txt b/requirements.txt index 38c71ad378007086211cfe5d8f4fde06629f5fb5..91829112429a6ad57982cc59bb4553216d7ff504 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ base58 >= 1.0.0 jsonschema >= 2.6.0 pypeg2 >= 2.15.2 attr >= 0.3.1 +pyaes >= 1.6.1 \ No newline at end of file