diff --git a/mmgen/autosign.py b/mmgen/autosign.py index 2305dcfd..056729bb 100755 --- a/mmgen/autosign.py +++ b/mmgen/autosign.py @@ -275,6 +275,7 @@ class Signable: self.cfg, tx1, seedfiles = self.parent.wallet_files[:], + keylist = self.parent.keylist, passwdfile = str(self.parent.keyfile), autosign = True).keys) if tx2: @@ -424,6 +425,7 @@ class Autosign: macOS_ramdisk_name = 'AutosignRamDisk' wallet_subdir = 'autosign' linux_blkid_cmd = 'sudo blkid -s LABEL -o value' + keylist_fn = 'keylist.mmenc' cmds = ('setup', 'xmr_setup', 'sign', 'wait') @@ -672,6 +674,7 @@ class Autosign: self.led.set('busy') self.do_mount() key_ok = self.decrypt_wallets() + self.init_non_mmgen_keys() if key_ok: if self.cfg.stealth_led: self.led.set('busy') @@ -780,6 +783,9 @@ class Autosign: ss_out = Wallet(self.cfg, ss=ss_in, passwd_file=str(self.keyfile)) ss_out.write_to_file(desc='autosign wallet', outdir=self.wallet_dir) + if self.cfg.keys_from_file: + self.setup_non_mmgen_keys() + @property def xmrwallet_cfg(self): if not hasattr(self, '_xmrwallet_cfg'): @@ -908,3 +914,34 @@ class Autosign: enabled = self.cfg.led, simulate = self.cfg.test_suite_autosign_led_simulate) self.led.set('off') + + def setup_non_mmgen_keys(self): + from .fileutil import get_lines_from_file, write_data_to_file + from .crypto import Crypto + lines = get_lines_from_file(self.cfg, self.cfg.keys_from_file, desc='keylist data') + write_data_to_file( + self.cfg, + str(self.wallet_dir / self.keylist_fn), + Crypto(self.cfg).mmgen_encrypt( + data = '\n'.join(lines).encode(), + passwd = self.keyfile.read_text()), + desc = 'encrypted keylist data', + binary = True) + if keypress_confirm(self.cfg, 'Securely delete original keylist file?'): + shred_file(self.cfg, self.cfg.keys_from_file) + + def init_non_mmgen_keys(self): + if not hasattr(self, 'keylist'): + path = self.wallet_dir / self.keylist_fn + if path.exists(): + from .crypto import Crypto + from .fileutil import get_data_from_file + self.keylist = Crypto(self.cfg).mmgen_decrypt( + get_data_from_file( + self.cfg, + path, + desc = 'encrypted keylist data', + binary = True), + passwd = self.keyfile.read_text()).decode().split() + else: + self.keylist = None diff --git a/mmgen/data/version b/mmgen/data/version index 26b375ca..9eaa8074 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -15.1.dev48 +15.1.dev49 diff --git a/mmgen/main_autosign.py b/mmgen/main_autosign.py index 7310ed3c..0013947d 100755 --- a/mmgen/main_autosign.py +++ b/mmgen/main_autosign.py @@ -36,6 +36,12 @@ opts_data = { --, --longhelp Print help message for long (global) options -c, --coins=c Coins to sign for (comma-separated list) -I, --no-insert-check Don’t check for device insertion +-k, --keys-from-file=F Use wif keys listed in file ‘F’ for signing non-MMGen + inputs. The file may be MMGen encrypted if desired. The + ‘setup’ operation creates a temporary encrypted copy of + the file in volatile memory for use during the signing + session, thus permitting the deletion of the original + file for increased security. -l, --seed-len=N Specify wallet seed length of ‘N’ bits (for setup only) -L, --led Use status LED to signal standby, busy and error -m, --mountpoint=M Specify an alternate mountpoint 'M' diff --git a/mmgen/tx/base.py b/mmgen/tx/base.py index 98c97619..43288adc 100755 --- a/mmgen/tx/base.py +++ b/mmgen/tx/base.py @@ -95,8 +95,7 @@ class Base(MMGenObject): This transaction includes inputs with non-{gc.proj_name} addresses. When signing the transaction, private keys for the addresses listed below must be supplied using the --keys-from-file option. The key file must contain - one key per line. Please note that this transaction cannot be autosigned, - as autosigning does not support the use of key files. + one key per line. Non-{gc.proj_name} addresses found in inputs: {{}} @@ -213,6 +212,7 @@ class Base(MMGenObject): quiet = True) def check_non_mmgen_inputs(self, *, caller, non_mmaddrs=None): + assert caller in ('txcreate', 'txdo', 'txsign', 'autosign') non_mmaddrs = non_mmaddrs or self.get_non_mmaddrs('inputs') if non_mmaddrs: indent = ' ' @@ -223,7 +223,7 @@ class Base(MMGenObject): die('UserOptError', f'\n{indent}ERROR: {m}\n') else: msg(f'\n{indent}WARNING: {m}\n') - if not self.cfg.yes: + if not (caller == 'autosign' or self.cfg.yes): from ..ui import keypress_confirm keypress_confirm(self.cfg, 'Continue?', default_yes=True, do_exit=True) diff --git a/mmgen/tx/keys.py b/mmgen/tx/keys.py index fc262640..75b6c880 100755 --- a/mmgen/tx/keys.py +++ b/mmgen/tx/keys.py @@ -89,6 +89,7 @@ class TxKeys: self.keylist = keylist if autosign else keylist or get_keylist(cfg) self.keyaddrlist = keyaddrlist if autosign else keyaddrlist or get_keyaddrlist(cfg, tx.proto) self.passwdfile = passwdfile + self.autosign = autosign self.saved_seeds = {} def get_keys_for_non_mmgen_inputs(self): @@ -96,7 +97,7 @@ class TxKeys: sep = '\n ' if addrs := self.tx.get_non_mmaddrs('inputs'): self.tx.check_non_mmgen_inputs( - caller = 'txsign', + caller = 'autosign' if self.autosign and self.keylist else 'txsign', non_mmaddrs = addrs) kal = KeyAddrList( cfg = self.cfg, diff --git a/test/cmdtest_d/automount.py b/test/cmdtest_d/automount.py index 0b442afd..3bb4fbdd 100755 --- a/test/cmdtest_d/automount.py +++ b/test/cmdtest_d/automount.py @@ -15,7 +15,7 @@ import time from .autosign import CmdTestAutosignThreaded from .regtest import CmdTestRegtest, rt_pw -from ..include.common import gr_uc +from ..include.common import gr_uc, create_addrpairs class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest): 'automounted transacting operations via regtest mode' @@ -23,21 +23,29 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest): networks = ('btc', 'bch', 'ltc') tmpdir_nums = [49] bdb_wallet = True + keylist_passwd = 'abc' rt_data = { 'rtFundAmt': {'btc':'500', 'bch':'500', 'ltc':'5500'}, } + bal1_chk = { + 'btc': '502.46', + 'bch': '502.46', + 'ltc': '5502.46'} bal2_chk = { - 'btc': '491.11002204', - 'bch': '498.7653392', - 'ltc': '5491.11002204'} + 'btc': '493.56992828', + 'bch': '501.22524576', + 'ltc': '5493.56992828'} cmd_group = ( ('setup', 'regtest mode setup'), ('walletgen_alice', 'wallet generation (Alice)'), ('addrgen_alice', 'address generation (Alice)'), ('addrimport_alice', 'importing Alice’s addresses'), + ('addrimport_alice_non_mmgen', 'importing Alice’s non-MMGen addresses'), ('fund_alice', 'funding Alice’s wallet'), + ('fund_alice_non_mmgen1', 'funding Alice’s wallet (non-MMGen addr #1)'), + ('fund_alice_non_mmgen2', 'funding Alice’s wallet (non-MMGen addr #2)'), ('generate', 'mining a block'), ('alice_bal1', 'checking Alice’s balance'), ('alice_txcreate1', 'creating a transaction'), @@ -92,9 +100,30 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest): self.opts.append('--alice') + self.non_mmgen_addrs = create_addrpairs(self.proto, 'C', 2) + + def addrimport_alice_non_mmgen(self): + self.write_to_tmpfile( + 'non_mmgen_addrs', + '\n'.join(e.addr for e in self.non_mmgen_addrs)) + return self.spawn( + 'mmgen-addrimport', + ['--alice', '--quiet', '--addrlist', f'{self.tmpdir}/non_mmgen_addrs']) + + def fund_alice_non_mmgen1(self): + return self.fund_wallet('alice', '1.23', addr=self.non_mmgen_addrs[0].addr) + + def fund_alice_non_mmgen2(self): + return self.fund_wallet('alice', '1.23', addr=self.non_mmgen_addrs[1].addr) + + def alice_bal1(self): + return self._user_bal_cli('alice', chk=self.bal1_chk[self.coin]) + def alice_txcreate1(self): return self._user_txcreate( 'alice', + inputs = '1-3', + tweaks = ['confirm_non_mmgen'], chg_addr = 'C:5', data_arg = 'data:'+gr_uc[:24]) @@ -147,7 +176,18 @@ class CmdTestAutosignAutomount(CmdTestAutosignThreaded, CmdTestRegtest): return self._user_txcreate('alice', chg_addr='C:5', exit_val=2, expect_str='unsent transaction') def alice_run_autosign_setup(self): - return self.run_setup(mn_type='default', use_dfl_wallet=True, passwd=rt_pw) + from mmgen.crypto import Crypto + from mmgen.cfg import Config + new_cfg = Config({'_clone': self.cfg, 'usr_randchars': 0, 'hash_preset': '1'}) + enc_data = Crypto(new_cfg).mmgen_encrypt( + '\n'.join(e.wif for e in self.non_mmgen_addrs).encode(), passwd=self.keylist_passwd) + self.write_to_tmpfile('non_mmgen_keys.mmenc', enc_data, binary=True) + return self.run_setup( + mn_type = 'default', + use_dfl_wallet = True, + wallet_passwd = rt_pw, + add_opts = [f'--keys-from-file={self.tmpdir}/non_mmgen_keys.mmenc'], + keylist_passwd = self.keylist_passwd) def alice_txsend1(self): return self._user_txsend('alice', comment='This one’s worth a comment', no_wait=True) diff --git a/test/cmdtest_d/autosign.py b/test/cmdtest_d/autosign.py index f7e15d48..89b7be9e 100755 --- a/test/cmdtest_d/autosign.py +++ b/test/cmdtest_d/autosign.py @@ -197,6 +197,7 @@ class CmdTestAutosignBase(CmdTestBase): seed_len = None, usr_entry_modes = False, wallet_passwd = None, + keylist_passwd = None, add_opts = [], expect_args = []): @@ -244,6 +245,10 @@ class CmdTestAutosignBase(CmdTestBase): if expect_args: t.expect(*expect_args) + if keylist_passwd: + t.passphrase('keylist data', keylist_passwd) + t.expect('(y/N): ', 'y') + t.read() self.remove_device() diff --git a/test/include/common.py b/test/include/common.py index a47d39a6..99d794a4 100755 --- a/test/include/common.py +++ b/test/include/common.py @@ -23,6 +23,7 @@ test.include.common: Shared routines and data for the MMGen test suites import sys, os, re, atexit from subprocess import run, PIPE, DEVNULL from pathlib import Path +from collections import namedtuple from mmgen.cfg import gv from mmgen.color import yellow, green, orange @@ -362,6 +363,17 @@ def make_burn_addr(proto, mmtype='compressed', hexdata=None): proto = proto, mmtype = mmtype).pubhash2addr(hexdata or '00'*20) +def create_addrpairs(proto, mmtype, num): + ap = namedtuple('addrpair', ['wif', 'addr']) + from mmgen.tool.coin import tool_cmd + n = 123456789123456789 + return [ap(*tool_cmd( + cfg = cfg, + cmdname = 'privhex2pair', + proto = proto, + mmtype = mmtype).privhex2pair(f'{n+m:064x}')) + for m in range(num)] + def VirtBlockDevice(img_path, size): if sys.platform == 'linux': return VirtBlockDeviceLinux(img_path, size)