Commit 2a5cac04 authored by Vincent Texier's avatar Vincent Texier

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

Add pyaes in requirements.txt
parent ced4a798
Pipeline #4362 passed with stages
in 3 minutes and 35 seconds
......@@ -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
......@@ -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)
)
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 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))
......
......@@ -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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment