diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2e5db9c0b26786e1c94c716eb2169bad4ccf4e8b..0449525ceec96e1249e88663a94bc114194d70e6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,9 @@ Changelog ------- * New hive node has been added (https://hived.emre.sh) * Add option to use a derived WIF from coldcard hardware wallet to derive a new account password -* beempy keygen and beempy importaccount have now a new option import-coldcard +* beempy passwordgen and beempy importaccount have now a new option import-coldcard +* beempy passwordgen handles now password generation and import, whereas beempy keygen handles BIP39 related key generation. +* refactoring and better unit testing 0.24.19 ------- diff --git a/beem/cli.py b/beem/cli.py index c838fda781bf63fc82a39fbb304c8a5dde28df00..b2481e3dbb53ce978fb8332aa1fbf0a889c86ab0 100644 --- a/beem/cli.py +++ b/beem/cli.py @@ -7,8 +7,6 @@ from prettytable import PrettyTable from datetime import datetime, timedelta import calendar import pytz -import secrets -import string import time import hashlib import math @@ -35,7 +33,7 @@ from beem.memo import Memo from beem.asset import Asset from beem.witness import Witness, WitnessesRankedByVote, WitnessesVotedByAccount from beem.blockchain import Blockchain -from beem.utils import formatTimeString, construct_authorperm, derive_beneficiaries, derive_tags, seperate_yaml_dict_from_body, derive_permlink, make_patch +from beem.utils import formatTimeString, construct_authorperm, derive_beneficiaries, derive_tags, seperate_yaml_dict_from_body, derive_permlink, make_patch, create_new_password, import_coldcard_wif, generate_password, import_pubkeys, import_custom_json from beem.vote import AccountVotes, ActiveVotes, Vote from beem import exceptions from beem.version import version as __version__ @@ -807,51 +805,19 @@ def delkey(confirm, pub): @click.option('--passphrase', '-p', help='Sets a BIP39 passphrase', is_flag=True, default=False) @click.option('--path', '-m', help='Sets a path for BIP39 key creations. When path is set, network, role, account_keys, account and sequence is not used') @click.option('--network', '-n', help='Network index, when using BIP39, 0 for steem and 13 for hive, (default is 13)', default=13) -@click.option('--role', '-r', help='Defines the key role for BIP39 when a single key is generated (default = owner).', default="owner") +@click.option('--role', '-r', help='Defines which key role should be created (default = owner).', default="owner") @click.option('--account-keys', '-k', help='Derives four BIP39 keys for each role', is_flag=True, default=False) @click.option('--sequence', '-s', help='Sequence key number, when using BIP39 (default is 0)', default=0) -@click.option('--account', '-a', help='account name for password based key generation or sequence number for BIP39 key, default = 0') -@click.option('--import-password', '-i', help='Imports a password and derives all four account keys', is_flag=True, default=False) -@click.option('--import-coldcard', '-o', help='Text file with a BIP85 WIF generated by a coldcard. The imported WIF is used to derives all four account keys') -@click.option('--create-password', '-c', help='Creates a new password and derives four account keys from it', is_flag=True, default=False) -@click.option('--wif', '-w', help='Defines how many times the password is replaced by its WIF representation for password based keys (default = 0).', default=0) +@click.option('--account', '-a', help='sequence number for BIP39 key, default = 0') +@click.option('--wif', '-w', help='Defines how many times the password is replaced by its WIF representation for password based keys (default = 0).') @click.option('--export-pub', '-u', help='Exports the public account keys to a json file for account creation or keychange') @click.option('--export', '-e', help='The results are stored in a text file and will not be shown') -def keygen(import_word_list, strength, passphrase, path, network, role, account_keys, sequence, account, import_password, import_coldcard, create_password, wif, export_pub, export): - """ Creates a new random BIP39 key or password based key and prints its derived private key and public key. +def keygen(import_word_list, strength, passphrase, path, network, role, account_keys, sequence, account, wif, export_pub, export): + """ Creates a new random BIP39 key and prints its derived private key and public key. The generated key is not stored. Can also be used to create new keys for an account. - Can also be used to derive account keys from a password or BIP39 wordlist + Can also be used to derive account keys from a password or BIP39 wordlist. """ stm = shared_blockchain_instance() - if not account and (import_password or create_password or import_coldcard): - account = stm.config["default_account"] - if import_password: - import_password = click.prompt("Enter password", confirmation_prompt=False, hide_input=True) - elif import_coldcard is not None: - next_var = "" - import_password = "" - path = "" - with open(import_coldcard) as fp: - for line in fp: - if line.strip() == "": - continue - if line.strip() == "WIF (privkey):": - next_var = "wif" - continue - elif "Path Used" in line.strip(): - next_var = "path" - continue - if next_var == "wif": - import_password = line.strip() - elif next_var == "path": - path = line - next_var = "" - elif create_password: - alphabet = string.ascii_letters + string.digits - while True: - import_password = ''.join(secrets.choice(alphabet) for i in range(32)) - if (any(c.islower() for c in import_password) and any(c.isupper() for c in import_password) and any(c.isdigit() for c in import_password)): - break pub_json = {"owner": "", "active": "", "posting": "", "memo": ""} if not account_keys and len(role.split(",")) > 1: @@ -859,34 +825,12 @@ def keygen(import_word_list, strength, passphrase, path, network, role, account_ account_keys = True else: roles = ['owner', 'active', 'posting', 'memo'] + if wif is not None: + wif = int(wif) + else: + wif = 0 - if import_password or create_password or import_coldcard: - if wif > 0: - password = import_password - for _ in range(wif): - pk = PasswordKey("", password, role="") - password = str(pk.get_private()) - password = 'P' + password - else: - password = import_password - t = PrettyTable(["Key", "Value"]) - t_pub = PrettyTable(["Key", "Value"]) - t.add_row(["Username", account]) - t_pub.add_row(["Username", account]) - if import_coldcard: - t_pub.add_row(["cold card path", path]) - t.align = "l" - t_pub.align = "l" - for r in roles: - pk = PasswordKey(account, password, role=r) - t.add_row(["%s Private Key" % r, str(pk.get_private())]) - t_pub.add_row(["%s Public Key" % r, format(pk.get_public(), "STM")]) - pub_json[r] = format(pk.get_public(), "STM") - t.add_row(["Backup (Master) Password", password]) - if wif > 0: - t.add_row(["WIF itersions", wif]) - t.add_row(["Entered/created Password", import_password]) - elif stm.use_ledger: + if stm.use_ledger: if stm.rpc is not None: stm.rpc.rpcconnect() ledgertx = stm.new_tx() @@ -950,7 +894,7 @@ def keygen(import_word_list, strength, passphrase, path, network, role, account_ print(" ".join(word_array)) word_list = " ".join(word_array) if passphrase: - passphrase = import_password = click.prompt("Enter passphrase", confirmation_prompt=True, hide_input=True) + passphrase = click.prompt("Enter passphrase", confirmation_prompt=True, hide_input=True) else: passphrase = "" mk = MnemonicKey(word_list=word_list, passphrase=passphrase, account_sequence=account, key_sequence=sequence) @@ -965,7 +909,7 @@ def keygen(import_word_list, strength, passphrase, path, network, role, account_ else: mk.set_path_BIP48(network_index=network, role=role, account_sequence=account, key_sequence=sequence) if passphrase: - passphrase = import_password = click.prompt("Enter passphrase", confirmation_prompt=True, hide_input=True) + passphrase = click.prompt("Enter passphrase", confirmation_prompt=True, hide_input=True) else: passphrase = "" word_list = mk.generate_mnemonic(passphrase=passphrase, strength=strength) @@ -1010,6 +954,79 @@ def keygen(import_word_list, strength, passphrase, path, network, role, account_ print(t) +@cli.command() +@click.option('--role', '-r', help='Defines which key role should be created. When owner is not set as role and an cold card wif is imported, the Master Password is not shown. (default = owner,active,posting,memo when creating account keys).', default="owner,active,posting,memo") +@click.option('--account', '-a', help='account name for password based key generation') +@click.option('--import-password', '-i', help='Imports a password and derives all four account keys', is_flag=True, default=False) +@click.option('--import-coldcard', '-o', help='Text file with a BIP85 WIF generated by a coldcard. The imported WIF is used to derives all four account keys') +@click.option('--wif', '-w', help='Defines how many times the password is replaced by its WIF representation for password based keys (default = 0 or 1 when importing a cold card wif).') +@click.option('--export-pub', '-u', help='Exports the public account keys to a json file for account creation or keychange') +@click.option('--export', '-e', help='The results are stored in a text file and will not be shown') +def passwordgen(role, account, import_password, import_coldcard, wif, export_pub, export): + """ Creates a new password based key and prints its derived private key and public key. + The generated key is not stored. The password is used to create new keys for an account. + """ + stm = shared_blockchain_instance() + if not account: + account = stm.config["default_account"] + if import_password: + import_password = click.prompt("Enter password", confirmation_prompt=False, hide_input=True) + elif import_coldcard is not None: + import_password, path = import_coldcard_wif(import_coldcard) + else: + import_password = create_new_password(length=32) + pub_json = {"owner": "", "active": "", "posting": "", "memo": ""} + + if len(role.split(",")) > 1: + roles = role.split(",") + elif role in ['owner', 'active', 'posting', 'memo']: + roles = [role] + else: + roles = ['owner', 'active', 'posting', 'memo'] + if wif is not None: + wif = int(wif) + elif import_coldcard: + wif = 1 + else: + wif = 0 + + password = generate_password(import_password, wif) + t = PrettyTable(["Key", "Value"]) + t_pub = PrettyTable(["Key", "Value"]) + t.add_row(["Username", account]) + t_pub.add_row(["Username", account]) + if import_coldcard: + t_pub.add_row(["cold card path", path]) + t.align = "l" + t_pub.align = "l" + for r in roles: + pk = PasswordKey(account, password, role=r) + t.add_row(["%s Private Key" % r, str(pk.get_private())]) + t_pub.add_row(["%s Public Key" % r, format(pk.get_public(), "STM")]) + pub_json[r] = format(pk.get_public(), "STM") + if "owner" in roles or import_coldcard is None: + t.add_row(["Backup (Master) Password", password]) + if wif > 0: + t.add_row(["WIF itersions", wif]) + if "owner" in roles or import_coldcard is None: + t.add_row(["Entered/created Password", import_password]) + + if export_pub and export_pub != "": + pub_json = json.dumps(pub_json, indent=4) + with open(export_pub, 'w') as fp: + fp.write(pub_json) + print("%s was sucessfully saved." % export_pub) + if export and export != "": + with open(export, 'w') as fp: + fp.write(str(t)) + fp.write("\n") + fp.write(str(t_pub)) + print("%s was sucessfully saved." % export) + else: + print(t_pub) + print(t) + + @cli.command() @click.argument('name') @click.option('--unsafe-import-token', @@ -1973,18 +1990,7 @@ def changekeys(account, owner, active, posting, memo, import_pub, export): account = Account(account, blockchain_instance=stm) if import_pub and import_pub != "": - if not os.path.isfile(import_pub): - raise Exception("File %s does not exist!" % import_pub) - with open(import_pub) as fp: - pubkeys = fp.read() - if pubkeys.find('\0') > 0: - with open(import_pub, encoding='utf-16') as fp: - pubkeys = fp.read() - pubkeys = ast.literal_eval(pubkeys) - owner = pubkeys["owner"] - active = pubkeys["active"] - posting = pubkeys["posting"] - memo = pubkeys["memo"] + owner, active, posting, memo = import_pubkeys(import_pub) if owner is None and active is None and memo is None and posting is None: raise ValueError("All pubkeys are None or empty!") @@ -2047,18 +2053,7 @@ def newaccount(accountname, account, owner, active, memo, posting, wif, create_c return acc = Account(account, blockchain_instance=stm) if import_pub and import_pub != "": - if not os.path.isfile(import_pub): - raise Exception("File %s does not exist!" % import_pub) - with open(import_pub) as fp: - pubkeys = fp.read() - if pubkeys.find('\0') > 0: - with open(import_pub, encoding='utf-16') as fp: - pubkeys = fp.read() - pubkeys = ast.literal_eval(pubkeys) - owner = pubkeys["owner"] - active = pubkeys["active"] - posting = pubkeys["posting"] - memo = pubkeys["memo"] + owner, active, posting, memo = import_pubkeys(import_pub) if create_claimed_account: tx = stm.create_claimed_account(accountname, creator=acc, owner_key=owner, active_key=active, memo_key=memo, posting_key=posting) else: @@ -2068,14 +2063,7 @@ def newaccount(accountname, account, owner, active, memo, posting, wif, create_c if not import_password: print("You cannot chose an empty password") return - if wif > 0: - password = import_password - for _ in range(wif): - pk = PasswordKey("", password, role="") - password = str(pk.get_private()) - password = 'P' + password - else: - password = import_password + password = generate_password(import_password, wif) if create_claimed_account: tx = stm.create_claimed_account(accountname, creator=acc, password=password) else: @@ -2170,7 +2158,7 @@ def delprofile(variable, account, export): @click.argument('account', nargs=1, required=True) @click.option('--roles', '-r', help='Import specified keys (owner, active, posting, memo).', default=["active", "posting", "memo"]) @click.option('--import-coldcard', '-i', help='Text file with a BIP85 WIF generated by a coldcard. The imported WIF is used as passphrase') -@click.option('--wif', '-w', help='Defines how many times the password is replaced by its WIF representation for password based keys (default = 0).', default=0) +@click.option('--wif', '-w', help='Defines how many times the password is replaced by its WIF representation for password based keys (default = 0 or 1 when importing a cold card wif).') def importaccount(account, roles, import_coldcard, wif): """Import an account using a passphrase""" from beemgraphenebase.account import PasswordKey @@ -2187,27 +2175,15 @@ def importaccount(account, roles, import_coldcard, wif): print("You cannot chose an empty Passphrase") return else: - next_var = "" - password = "" - with open(import_coldcard) as fp: - for line in fp: - if line.strip() == "": - continue - if line.strip() == "WIF (privkey):": - next_var = "wif" - continue - elif "Path Used" in line.strip(): - next_var = "path" - continue - if next_var == "wif": - password = line.strip() - next_var = "" + password, path = import_coldcard_wif(import_coldcard) + if wif is not None: + wif = int(wif) + elif import_coldcard is not None: + wif = 1 + else: + wif = 0 - if wif > 0: - for _ in range(wif): - pk = PasswordKey("", password, role="") - password = str(pk.get_private()) - password = 'P' + password + password = generate_password(password, wif) if "owner" in roles: owner_key = PasswordKey(account["name"], password, role="owner") @@ -4680,35 +4656,9 @@ def customjson(jsonid, json_data, account, active, export): print("First argument must be the custom_json id") if json_data is None: print("Second argument must be the json_data, can be a string or a file name.") - if isinstance(json_data, tuple) and len(json_data) > 1: - data = {} - key = None - for j in json_data: - if key is None: - key = j - else: - data[key] = j - key = None - if key is not None: - print("Value is missing for key: %s" % key) - return - else: - try: - with open(json_data[0], 'r') as f: - data = json.load(f) - except: - print("%s is not a valid file or json field" % json_data) - return - for d in data: - if isinstance(data[d], str) and data[d][0] == "{" and data[d][-1] == "}": - field = {} - for keyvalue in data[d][1:-1].split(","): - key = keyvalue.split(":")[0].strip() - value = keyvalue.split(":")[1].strip() - if jsonid == "ssc-mainnet1" and key == "quantity": - value = float(value) - field[key] = value - data[d] = field + data = import_custom_json(jsonid, json_data) + if data is None: + return stm = shared_blockchain_instance() if stm.rpc is not None: stm.rpc.rpcconnect() diff --git a/beem/utils.py b/beem/utils.py index af863475188842706265a914b63a329000a80473..c71762708ea9629a260992594d7dcc9bddb88dbf 100644 --- a/beem/utils.py +++ b/beem/utils.py @@ -8,6 +8,11 @@ import pytz import difflib from ruamel.yaml import YAML import difflib +import secrets +import string +from beemgraphenebase.account import PasswordKey +import ast +import os timeFormat = "%Y-%m-%dT%H:%M:%S" # https://github.com/matiasb/python-unidiff/blob/master/unidiff/constants.py#L37 @@ -383,3 +388,97 @@ def load_dirty_json(dirty_json): dirty_json = re.sub(r, s, dirty_json) clean_json = json.loads(dirty_json) return clean_json + + +def create_new_password(length=32): + """Creates a random password containing alphanumeric chars with at least 1 number and 1 upper and lower char""" + alphabet = string.ascii_letters + string.digits + while True: + import_password = ''.join(secrets.choice(alphabet) for i in range(length)) + if (any(c.islower() for c in import_password) and any(c.isupper() for c in import_password) and any(c.isdigit() for c in import_password)): + break + return import_password + + +def import_coldcard_wif(filename): + """Reads a exported coldcard Wif text file and returns the WIF and used path""" + next_var = "" + import_password = "" + path = "" + with open(filename) as fp: + for line in fp: + if line.strip() == "": + continue + if line.strip() == "WIF (privkey):": + next_var = "wif" + continue + elif "Path Used" in line.strip(): + next_var = "path" + continue + if next_var == "wif": + import_password = line.strip() + elif next_var == "path": + path = line + next_var = "" + return import_password, path.lstrip().replace("\n", "") + + +def generate_password(import_password, wif=1): + if wif > 0: + password = import_password + for _ in range(wif): + pk = PasswordKey("", password, role="") + password = str(pk.get_private()) + password = 'P' + password + else: + password = import_password + return password + + +def import_pubkeys(import_pub): + if not os.path.isfile(import_pub): + raise Exception("File %s does not exist!" % import_pub) + with open(import_pub) as fp: + pubkeys = fp.read() + if pubkeys.find('\0') > 0: + with open(import_pub, encoding='utf-16') as fp: + pubkeys = fp.read() + pubkeys = ast.literal_eval(pubkeys) + owner = pubkeys["owner"] + active = pubkeys["active"] + posting = pubkeys["posting"] + memo = pubkeys["memo"] + return owner, active, posting, memo + + +def import_custom_json(jsonid, json_data): + data = {} + if isinstance(json_data, tuple) and len(json_data) > 1: + key = None + for j in json_data: + if key is None: + key = j + else: + data[key] = j + key = None + if key is not None: + print("Value is missing for key: %s" % key) + return None + else: + try: + with open(json_data[0], 'r') as f: + data = json.load(f) + except: + print("%s is not a valid file or json field" % json_data) + return None + for d in data: + if isinstance(data[d], str) and data[d][0] == "{" and data[d][-1] == "}": + field = {} + for keyvalue in data[d][1:-1].split(","): + key = keyvalue.split(":")[0].strip() + value = keyvalue.split(":")[1].strip() + if jsonid == "ssc-mainnet1" and key == "quantity": + value = float(value) + field[key] = value + data[d] = field + return data diff --git a/tests/beem/data/drv-wif-idx100.txt b/tests/beem/data/drv-wif-idx100.txt new file mode 100644 index 0000000000000000000000000000000000000000..577c944136615e2c98a887982ea6015057fbf908 --- /dev/null +++ b/tests/beem/data/drv-wif-idx100.txt @@ -0,0 +1,8 @@ +WIF (privkey): +L5K7x3Zs6jgY5jMovRzdgucWHmvuidyPj1f8ioCAzGjHMhjmL5EL + +Path Used (index=100): + m/83696968'/2'/100' + +Raw Entropy: +f17b6e42ce2b40d385467a18f08a0159391cf113855903158a06f95a6d1e7697 diff --git a/tests/beem/data/pubkey.json b/tests/beem/data/pubkey.json new file mode 100644 index 0000000000000000000000000000000000000000..a2e23807300aade39cce489567c01b6704d44f70 --- /dev/null +++ b/tests/beem/data/pubkey.json @@ -0,0 +1,6 @@ +{ + "owner": "STM51mq6zWEz3NGRYL8uMpJAe9c1qzf4ufh2ha4QqWzizqVrPL9Nq", + "active": "STM6oVMzJJJgSu3hV1DZBcLdMUJYj3Cs6kGXf6WVLP3HhgLgNkA5J", + "posting": "STM8XJdv7T36XhKRmPaodt8tqoeMbNgLrsiyweNESvnKqZWQQekCQ", + "memo": "STM87KR1HKDoLiC3dv3goE99KDqEocBi3br8vcop6DgrCTwJcWexH" +} \ No newline at end of file diff --git a/tests/beem/data/tmp.json b/tests/beem/data/tmp.json new file mode 100644 index 0000000000000000000000000000000000000000..908d21f1df38bfd516e54b4053b2680828de312a --- /dev/null +++ b/tests/beem/data/tmp.json @@ -0,0 +1,6 @@ +{ + "owner": "STM7d8DzUzjs5jbSkBVNctRaZFGe991MhzzTrqMoTVvZJ5oyZN7Cj", + "active": "STM7oADsCds97GqyEDY4cQC66brVrg7XHuRa2MLvYbuGrdKnNoQa6", + "posting": "STM5fpGcVwvUFF55EzWQ35oJeERcWvt4M9dXwehdpYmKaFCCqihL7", + "memo": "STM6A7DywWvMZRokxAK5CpTo8XAPKbrMennAs4ntwRFq5nj2jR7nG" +} \ No newline at end of file diff --git a/tests/beem/test_cli.py b/tests/beem/test_cli.py index 2842b4e2047ed44a748f2f46f145fbea852f661b..6cc73db9e2bb789a485131321e3f0846f5facdde 100644 --- a/tests/beem/test_cli.py +++ b/tests/beem/test_cli.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import unittest import click +import os from click.testing import CliRunner from pprint import pprint from beem import Steem, exceptions @@ -10,6 +11,7 @@ from beemgraphenebase.account import PrivateKey from beem.cli import cli, balance from beem.instance import set_shared_steem_instance, shared_steem_instance from beembase.operationids import getOperationNameForId +from beem.utils import import_pubkeys from .nodes import get_hive_nodes, get_steem_nodes wif = "5Jt2wTfhUt5GkZHV1HYVfkEaJ6XnY8D2iA4qjtK9nnGXAhThM3w" @@ -147,6 +149,22 @@ class Testcases(unittest.TestCase): result = runner.invoke(cli, ['keygen']) self.assertEqual(result.exit_code, 0) + def test_passwordgen(self): + runner = CliRunner() + result = runner.invoke(cli, ['passwordgen']) + self.assertEqual(result.exit_code, 0) + data_dir = os.path.join(os.path.dirname(__file__), 'data') + file = os.path.join(data_dir, "drv-wif-idx100.txt") + file2 = os.path.join(data_dir, "wif_pub_temp.json") + result = runner.invoke(cli, ['passwordgen', '-a', 'test', '-o', file, '-u', file2, '-w', 1]) + self.assertEqual(result.exit_code, 0) + owner, active, posting, memo = import_pubkeys(file2) + self.assertEqual(owner, "STM7d8DzUzjs5jbSkBVNctRaZFGe991MhzzTrqMoTVvZJ5oyZN7Cj") + self.assertEqual(active, "STM7oADsCds97GqyEDY4cQC66brVrg7XHuRa2MLvYbuGrdKnNoQa6") + self.assertEqual(posting, "STM5fpGcVwvUFF55EzWQ35oJeERcWvt4M9dXwehdpYmKaFCCqihL7") + self.assertEqual(memo, "STM6A7DywWvMZRokxAK5CpTo8XAPKbrMennAs4ntwRFq5nj2jR7nG") + os.remove(file2) + def test_set(self): runner = CliRunner() result = runner.invoke(cli, ['-o', 'set', 'set_default_vote_weight', '100']) diff --git a/tests/beem/test_utils.py b/tests/beem/test_utils.py index 2563f8a5ce96cefd2255738c847bfed6a61669d9..267bf00ed8d82ccf40e0798a18e5d8343367e697 100644 --- a/tests/beem/test_utils.py +++ b/tests/beem/test_utils.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import unittest from datetime import datetime, date, timedelta +import os from beem.utils import ( formatTimedelta, assets_from_string, @@ -18,7 +19,11 @@ from beem.utils import ( derive_beneficiaries, derive_tags, seperate_yaml_dict_from_body, - make_patch + make_patch, + create_new_password, + generate_password, + import_coldcard_wif, + import_pubkeys ) @@ -176,3 +181,36 @@ class Testcases(unittest.TestCase): self.assertEqual(par, {"par1": "data1", "par2": "data2", "par3": 3}) self.assertEqual(body, " test ---") + def test_create_new_password(self): + new_password = create_new_password() + self.assertEqual(len(new_password), 32) + self.assertTrue(any(c.islower() for c in new_password)) + self.assertTrue(any(c.isupper() for c in new_password)) + self.assertTrue(any(c.isdigit() for c in new_password)) + + new_password2 = create_new_password() + self.assertFalse(new_password == new_password2) + new_password = create_new_password(length=16) + self.assertEqual(len(new_password), 16) + + def test_generate_password(self): + new_password = generate_password("test", wif=0) + self.assertEqual(new_password, "test") + new_password = generate_password("test", wif=1) + self.assertAlmostEqual(new_password, "P5K2YUVmWfxbmvsNxCsfvArXdGXm7d5DC9pn4yD75k2UaSYgkXTh") + + def test_import_coldcard_wif(self): + data_dir = os.path.join(os.path.dirname(__file__), 'data') + file = os.path.join(data_dir, "drv-wif-idx100.txt") + wif, path = import_coldcard_wif(file) + self.assertEqual(wif, "L5K7x3Zs6jgY5jMovRzdgucWHmvuidyPj1f8ioCAzGjHMhjmL5EL") + self.assertEqual(path, "m/83696968'/2'/100'") + + def test_import_pubkeys(self): + data_dir = os.path.join(os.path.dirname(__file__), 'data') + file = os.path.join(data_dir, "pubkey.json") + owner, active, posting, memo = import_pubkeys(file) + self.assertEqual(owner, "STM7d8DzUzjs5jbSkBVNctRaZFGe991MhzzTrqMoTVvZJ5oyZN7Cj") + self.assertEqual(active, "STM7oADsCds97GqyEDY4cQC66brVrg7XHuRa2MLvYbuGrdKnNoQa6") + self.assertEqual(posting, "STM5fpGcVwvUFF55EzWQ35oJeERcWvt4M9dXwehdpYmKaFCCqihL7") + self.assertEqual(memo, "STM6A7DywWvMZRokxAK5CpTo8XAPKbrMennAs4ntwRFq5nj2jR7nG")