Skip to content
Snippets Groups Projects
Commit 2a5cac04 authored by Vincent Texier's avatar Vincent Texier
Browse files

issue #50 Add export/import EWIF V1 file and its example (lib pyaes required)

Add pyaes in requirements.txt
parent ced4a798
No related branches found
No related tags found
No related merge requests found
Pipeline #4362 passed
...@@ -25,3 +25,17 @@ def ensure_str(data: Union[str, bytes]) -> str: ...@@ -25,3 +25,17 @@ def ensure_str(data: Union[str, bytes]) -> str:
return str(data, 'utf-8') return str(data, 'utf-8')
return data 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
...@@ -7,10 +7,11 @@ from re import compile, MULTILINE, search ...@@ -7,10 +7,11 @@ from re import compile, MULTILINE, search
from typing import Optional, Union, TypeVar, Type from typing import Optional, Union, TypeVar, Type
import libnacl.sign import libnacl.sign
import pyaes
from pylibscrypt import scrypt from pylibscrypt import scrypt
from .base58 import Base58Encoder from .base58 import Base58Encoder
from ..helpers import ensure_bytes from ..helpers import ensure_bytes, xor_bytes
SEED_LENGTH = 32 # Length of the key SEED_LENGTH = 32 # Length of the key
crypto_sign_BYTES = 64 crypto_sign_BYTES = 64
...@@ -81,7 +82,7 @@ class SigningKey(libnacl.sign.Signer): ...@@ -81,7 +82,7 @@ class SigningKey(libnacl.sign.Signer):
@classmethod @classmethod
def from_wif_file(cls: Type[SigningKeyType], path: str) -> SigningKeyType: 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 :param path: Path to WIF file
""" """
...@@ -113,17 +114,17 @@ class SigningKey(libnacl.sign.Signer): ...@@ -113,17 +114,17 @@ class SigningKey(libnacl.sign.Signer):
return cls(seed) 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 :param path: Path to file
""" """
# Cesium v1 # Cesium v1
version = 1 version = 1
# add version to seed # add format to seed (1=WIF,2=EWIF)
seed_fi = version.to_bytes(version, 'little') + self.seed seed_fi = b"\x01" + self.seed
# calculate checksum # calculate checksum
sha256_v1 = libnacl.crypto_hash_sha256(seed_fi) sha256_v1 = libnacl.crypto_hash_sha256(seed_fi)
...@@ -136,5 +137,116 @@ class SigningKey(libnacl.sign.Signer): ...@@ -136,5 +137,116 @@ class SigningKey(libnacl.sign.Signer):
fh.write( fh.write(
"""Type: WIF """Type: WIF
Version: {version} 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)
) )
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)
from duniterpy.key import SigningKey from duniterpy.key import SigningKey
from libnacl.utils import load_key
import getpass import getpass
import os import os
...@@ -39,7 +38,7 @@ if signer.pubkey != pubkey: ...@@ -39,7 +38,7 @@ if signer.pubkey != pubkey:
exit(1) exit(1)
# save private key in a file (WIF v1 format) # 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 # document saved
print("Private key for public key %s saved in %s" % (signer.pubkey, PRIVATE_KEY_FILE_PATH)) print("Private key for public key %s saved in %s" % (signer.pubkey, PRIVATE_KEY_FILE_PATH))
......
...@@ -5,3 +5,4 @@ base58 >= 1.0.0 ...@@ -5,3 +5,4 @@ base58 >= 1.0.0
jsonschema >= 2.6.0 jsonschema >= 2.6.0
pypeg2 >= 2.15.2 pypeg2 >= 2.15.2
attr >= 0.3.1 attr >= 0.3.1
pyaes >= 1.6.1
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment