""" duniter public and private keys @author: inso """ from re import compile, MULTILINE, search from typing import Optional, Union, TypeVar, Type import libnacl.sign import pyaes from libnacl.utils import load_key from pylibscrypt import scrypt from .base58 import Base58Encoder from ..helpers import ensure_bytes, xor_bytes SEED_LENGTH = 32 # Length of the key crypto_sign_BYTES = 64 class ScryptParams: def __init__(self, n: int, r: int, p: int) -> None: """ Init a ScryptParams instance with crypto parameters :param n: scrypt param N :param r: scrypt param r :param p: scrypt param p """ self.N = n self.r = r self.p = p # required to type hint cls in classmethod SigningKeyType = TypeVar('SigningKeyType', bound='SigningKey') class SigningKey(libnacl.sign.Signer): def __init__(self, seed: bytes) -> None: """ Init pubkey property :param str seed: Hexadecimal seed string """ super().__init__(seed) self.pubkey = Base58Encoder.encode(self.vk) @classmethod def from_credentials(cls: Type[SigningKeyType], salt: Union[str, bytes], password: Union[str, bytes], scrypt_params: Optional[ScryptParams] = None) -> SigningKeyType: """ Create a SigningKey object from credentials :param salt: Secret salt passphrase credential :param password: Secret password credential :param scrypt_params: ScryptParams instance """ if scrypt_params is None: scrypt_params = ScryptParams(4096, 16, 1) salt = ensure_bytes(salt) password = ensure_bytes(password) seed = scrypt(password, salt, scrypt_params.N, scrypt_params.r, scrypt_params.p, SEED_LENGTH) return cls(seed) def save_private_key(self, path: str) -> None: """ Save authentication file :param path: Authentication file path """ self.save(path) @staticmethod def from_private_key(path: str) -> SigningKeyType: """ Read authentication file Add public key attribute :param path: Authentication file path """ key = load_key(path) key.pubkey = Base58Encoder.encode(key.vk) return key def decrypt_seal(self, message: bytes) -> str: """ Decrypt message with a curve25519 version of the ed25519 key pair :param message: Encrypted message :return: """ curve25519_public_key = libnacl.crypto_sign_ed25519_pk_to_curve25519(self.vk) curve25519_secret_key = libnacl.crypto_sign_ed25519_sk_to_curve25519(self.sk) return libnacl.crypto_box_seal_open(message, curve25519_public_key, curve25519_secret_key).decode('utf-8') @classmethod def from_pubsec_file(cls: Type[SigningKeyType], path: str) -> SigningKeyType: """ Return SigningKey instance from Duniter WIF file :param path: Path to WIF file """ with open(path, 'r') as fh: pubsec_content = fh.read() # line patterns regex_pubkey = compile("pub: ([1-9A-HJ-NP-Za-km-z]+)", MULTILINE) regex_signkey = compile("sec: ([1-9A-HJ-NP-Za-km-z]+)", MULTILINE) # check public key field match = search(regex_pubkey, pubsec_content) if not match: raise Exception('Error: Bad format PubSec v1 file, missing public key') # check signkey field match = search(regex_signkey, pubsec_content) if not match: raise Exception('Error: Bad format PubSec v1 file, missing sec key') # capture signkey signkey_hex = match.groups()[0] # extract seed from signkey seed = bytes(Base58Encoder.decode(signkey_hex)[0:32]) return cls(seed) def save_pubsec_file(self, path: str) -> None: """ Save a Duniter PubSec file (PubSec) v1 :param path: Path to file """ # version version = 1 # base58 encode keys base58_signing_key = Base58Encoder.encode(self.sk) base58_public_key = self.pubkey # save file with open(path, 'w') as fh: fh.write( """Type: PubSec Version: {version} pub: {pubkey} sec: {signkey}""".format(version=version, pubkey=base58_public_key, signkey=base58_signing_key) ) @staticmethod def from_wif_or_ewif_file(path: str, password: Optional[str] = None) -> SigningKeyType: """ Return SigningKey instance from Duniter WIF or EWIF file :param path: Path to WIF of EWIF file :param password: Password needed for EWIF file """ with open(path, 'r') as fh: wif_content = fh.read() # check data field regex = compile('Data: ([1-9A-HJ-NP-Za-km-z]+)', MULTILINE) match = search(regex, wif_content) if not match: raise Exception('Error: Bad format WIF or EWIF v1 file') # capture hexa wif key wif_hex = match.groups()[0] return SigningKey.from_wif_or_ewif_hex(wif_hex, password) @staticmethod def from_wif_or_ewif_hex(wif_hex: str, password: Optional[str] = None) -> SigningKeyType: """ Return SigningKey instance from Duniter WIF or EWIF in hexadecimal format :param wif_hex: WIF or EWIF string in hexadecimal format :param password: Password of EWIF encrypted seed """ wif_bytes = Base58Encoder.decode(wif_hex) fi = wif_bytes[0:1] if fi == b"\x01": return SigningKey.from_wif_hex(wif_hex) elif fi == b"\x02" and password is not None: return SigningKey.from_ewif_hex(wif_hex, password) else: raise Exception("Error: Bad format: not WIF nor EWIF") @staticmethod def from_wif_file(path: str) -> SigningKeyType: """ Return SigningKey instance from Duniter WIF file :param path: Path to WIF file """ with open(path, 'r') as fh: wif_content = fh.read() # check data field regex = compile('Data: ([1-9A-HJ-NP-Za-km-z]+)', MULTILINE) match = search(regex, wif_content) if not match: raise Exception('Error: Bad format WIF v1 file') # capture hexa wif key wif_hex = match.groups()[0] return SigningKey.from_wif_hex(wif_hex) @classmethod def from_wif_hex(cls: Type[SigningKeyType], wif_hex: str) -> SigningKeyType: """ Return SigningKey instance from Duniter WIF in hexadecimal format :param wif_hex: WIF string in hexadecimal format """ wif_bytes = Base58Encoder.decode(wif_hex) if len(wif_bytes) != 35: raise Exception("Error: the size of WIF is invalid") # extract data checksum_from_wif = wif_bytes[-2:] fi = wif_bytes[0:1] seed = wif_bytes[1:-2] seed_fi = wif_bytes[0:-2] # check WIF format flag if fi != b"\x01": raise Exception("Error: bad format version, not WIF") # checksum control checksum = libnacl.crypto_hash_sha256(libnacl.crypto_hash_sha256(seed_fi))[0:2] if checksum_from_wif != checksum: raise Exception("Error: bad checksum of the WIF") return cls(seed) def save_wif_file(self, path: str) -> None: """ Save a Wallet Import Format file (WIF) v1 :param path: Path to file """ # version version = 1 # add format to seed (1=WIF,2=EWIF) seed_fi = b"\x01" + self.seed # calculate checksum sha256_v1 = libnacl.crypto_hash_sha256(seed_fi) sha256_v2 = libnacl.crypto_hash_sha256(sha256_v1) checksum = sha256_v2[0:2] # base58 encode key and checksum wif_key = Base58Encoder.encode(seed_fi + checksum) with open(path, 'w') as fh: fh.write( """Type: WIF Version: {version} Data: {data}""".format(version=version, data=wif_key) ) @staticmethod def from_ewif_file(path: str, password: str) -> SigningKeyType: """ Return SigningKey instance from Duniter EWIF file :param path: Path to EWIF file :param password: Password of the encrypted seed """ with open(path, 'r') as fh: wif_content = fh.read() # check data field 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') # capture ewif key ewif_hex = match.groups()[0] return SigningKey.from_ewif_hex(ewif_hex, password) @classmethod def from_ewif_hex(cls: Type[SigningKeyType], ewif_hex: str, password: str) -> SigningKeyType: """ Return SigningKey instance from Duniter EWIF in hexadecimal format :param ewif_hex: EWIF string in hexadecimal format :param password: Password of the encrypted seed """ ewif_bytes = Base58Encoder.decode(ewif_hex) if len(ewif_bytes) != 39: raise Exception("Error: the size of EWIF is invalid") # extract data 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] # check format flag if fi != b"\x02": raise Exception("Error: bad format version, not EWIF") # 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: """ # 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) )