From 4378ac0db91ec11da4395692059911c827787dc4 Mon Sep 17 00:00:00 2001 From: philemon Date: Wed, 20 Aug 2014 20:34:29 +0400 Subject: [PATCH] * New incognito format with checksum for password verification. Old format continues to be supported with '--old-incog-fmt' option * mmgen-txsign: '--mmgen-keys-from-file' option (supersedes '--all-keys-from-file' option) allows offline signing of transactions with both MMGen and non-MMGen inputs. Instead of a flat keylist, a key-address file (the output of 'mmgen-keygen'), optionally encrypted, is used both as a key source and to verify MMGen-to-BTC mappings for both inputs and outputs, eliminating the need for an additional address file. * mmgen-addrimport: '--keyaddr-file' option allows using key-address file (possibly encrypted) as an address source. --- mmgen/Opts.py | 17 +- mmgen/addr.py | 48 +-- mmgen/bitcoin.py | 85 ++-- mmgen/config.py | 9 +- mmgen/crypto.py | 200 ++++++---- mmgen/main.py | 4 + mmgen/main_addrgen.py | 76 ++-- mmgen/main_addrimport.py | 62 +-- mmgen/main_pywallet.py | 4 +- mmgen/main_txcreate.py | 313 +++++++++++++-- mmgen/main_txsend.py | 5 +- mmgen/main_txsign.py | 309 +++++++++++---- mmgen/main_walletchk.py | 57 ++- mmgen/main_walletgen.py | 38 +- mmgen/mnemonic.py | 87 ++-- mmgen/tests/bitcoin.py | 134 +------ mmgen/tool.py | 69 ++-- mmgen/tx.py | 832 +++++++++++---------------------------- mmgen/util.py | 151 ++----- 19 files changed, 1275 insertions(+), 1225 deletions(-) diff --git a/mmgen/Opts.py b/mmgen/Opts.py index 178f825e..7cece467 100755 --- a/mmgen/Opts.py +++ b/mmgen/Opts.py @@ -57,7 +57,7 @@ def parse_opts(argv,help_data): ('outdir', 'export_incog_hidden'), ('from_incog_hidden','from_incog','from_seed','from_mnemonic','from_brain'), ('export_incog','export_incog_hex','export_incog_hidden','export_mnemonic', - 'export_seed'), + 'export_seed'), ('quiet','verbose') ): warn_incompatible_opts(opts,l) @@ -65,12 +65,14 @@ def parse_opts(argv,help_data): if not check_opts(opts,long_opts): sys.exit(1) # If unset, set these to default values in mmgen.config: - for v in g.cl_override_vars: + for v in g.dfl_vars: if v in opts: typeconvert_override_var(opts,v) else: opts[v] = eval("g."+v) - if "verbose" in opts: g.verbose = True - if "quiet" in opts: g.quiet = True + # Opposite of above: if set, override the default values in mmgen.config: + if 'no_keyconv' in opts: g.no_keyconv = opts['no_keyconv'] + if 'verbose' in opts: g.verbose = opts['verbose'] + if 'quiet' in opts: g.quiet = opts['quiet'] if g.debug: print "opts after typeconvert: %s" % opts @@ -123,8 +125,8 @@ def check_opts(opts,long_opts): what = "parameter for '--%s' option" % opt.replace("_","-") # Check for file existence and readability - if opt in ('keys_from_file','all_keys_from_file','addrlist', - 'passwd_file','keysforaddrs'): + if opt in ('keys_from_file','mmgen_keys_from_file', + 'passwd_file','keysforaddrs','comment_file'): check_infile(val) # exits on error continue @@ -170,7 +172,7 @@ def check_opts(opts,long_opts): if not opt_is_in_list(val,g.hash_presets.keys(),what): return False elif opt == 'usr_randchars': if not opt_is_int(val,what): return False - if val == '0': return True + if val == '0': continue if not opt_compares(val,">=",g.min_urandchars,what): return False if not opt_compares(val,"<=",g.max_urandchars,what): return False else: @@ -187,6 +189,7 @@ def typeconvert_override_var(opts,opt): if vtype == int: f,t = int,"an integer" elif vtype == str: f,t = str,"a string" elif vtype == float: f,t = float,"a float" + elif vtype == bool: f,t = bool,"a boolean value" try: opts[opt] = f(opts[opt]) diff --git a/mmgen/addr.py b/mmgen/addr.py index 0ec42cfb..8c97ec53 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -58,10 +58,10 @@ def test_for_keyconv(): return True -def generate_addrs(seed, addrnums, opts): +def generate_addrs(seed, addrnums, opts, seed_id=""): - if 'addrs' in opts['gen_what']: - if 'no_keyconv' in opts or test_for_keyconv() == False: + if 'a' in opts['gen_what']: + if g.no_keyconv or test_for_keyconv() == False: msg("Using (slow) internal ECDSA library for address generation") from mmgen.bitcoin import privnum2addr keyconv = False @@ -69,18 +69,21 @@ def generate_addrs(seed, addrnums, opts): from subprocess import Popen, PIPE keyconv = "keyconv" - fmt = "num addr" if opts['gen_what'] == ["addrs"] else ( - "num sec wif" if opts['gen_what'] == ["keys"] else "num sec wif addr") + fmt = "num sec wif addr" if 'ka' in opts['gen_what'] else ( + "num sec wif" if 'k' in opts['gen_what'] else "num addr") from collections import namedtuple addrinfo = namedtuple("addrinfo",fmt) addrinfo_args = "%s" % ",".join(fmt.split()) + addrnums = sorted(set(addrnums)) # don't trust the calling function t_addrs,num,pos,out = len(addrnums),0,0,[] - addrnums.sort() # needed only if caller didn't sort - ws,wp = ('key','keys') if 'keys' in opts['gen_what'] \ - else ('address','addresses') + w = { + 'ka': ('key/address pair','s'), + 'k': ('key','s'), + 'a': ('address','es') + }[opts['gen_what']] while pos != t_addrs: seed = sha512(seed).digest() @@ -91,50 +94,51 @@ def generate_addrs(seed, addrnums, opts): pos += 1 - qmsg_r("\rGenerating %s #%s (%s of %s)" % (ws,num,pos,t_addrs)) + qmsg_r("\rGenerating %s #%s (%s of %s)" % (w[0],num,pos,t_addrs)) # Secret key is double sha256 of seed hash round /num/ sec = sha256(sha256(seed).digest()).hexdigest() wif = numtowif(int(sec,16)) - if 'addrs' in opts['gen_what']: addr = \ + if 'a' in opts['gen_what']: addr = \ Popen([keyconv, wif], stdout=PIPE).stdout.readline().split()[1] \ if keyconv else privnum2addr(int(sec,16)) out.append(eval("addrinfo("+addrinfo_args+")")) - qmsg("\rGenerated %s %s%s"%(t_addrs, (ws if t_addrs == 1 else wp), " "*15)) + m = w[0] if t_addrs == 1 else w[0]+w[1] + if seed_id: + qmsg("\r%s: %s %s generated%s" % (seed_id,t_addrs,m," "*15)) + else: + qmsg("\rGenerated %s %s%s" % (t_addrs,m," "*15)) return out def format_addr_data(addr_data, addr_data_chksum, seed_id, addr_idxs, opts): - if 'flat_list' in opts: - return "\n\n".join(["# {}:{d.num} {d.addr}\n{d.wif}".format(seed_id,d=d) - for d in addr_data])+"\n\n" - fs = " {:<%s} {}" % len(str(addr_data[-1].num)) - if 'addrs' not in opts['gen_what']: out = [] - else: + if 'a' in opts['gen_what']: out = [] if 'stdout' in opts else [addrmsgs['addrfile_header']+"\n"] - out.append("# Address data checksum for {}[{}]: {}".format( - seed_id, fmt_addr_idxs(addr_idxs), addr_data_chksum)) + w = "Key-address" if 'k' in opts['gen_what'] else "Address" + out.append("# {} data checksum for {}[{}]: {}".format( + w, seed_id, fmt_addr_idxs(addr_idxs), addr_data_chksum)) out.append("# Record this value to a secure location\n") + else: out = [] out.append("%s {" % seed_id.upper()) for d in addr_data: - if 'addrs' in opts['gen_what']: # First line with number + if 'a' in opts['gen_what']: # First line with number out.append(fs.format(d.num, d.addr)) else: out.append(fs.format(d.num, "wif: "+d.wif)) - if 'keys' in opts['gen_what']: # Subsequent lines + if 'k' in opts['gen_what']: # Subsequent lines if 'b16' in opts: out.append(fs.format("", "hex: "+d.sec)) - if 'addrs' in opts['gen_what']: + if 'a' in opts['gen_what']: out.append(fs.format("", "wif: "+d.wif)) out.append("}") diff --git a/mmgen/bitcoin.py b/mmgen/bitcoin.py index 38d084d7..30094767 100755 --- a/mmgen/bitcoin.py +++ b/mmgen/bitcoin.py @@ -52,65 +52,56 @@ b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' # The "zero address": # 1111111111111111111114oLvT2 (use step2 = ("0" * 40) to generate) # + def pubhex2hexaddr(pubhex): step1 = sha256(unhexlify(pubhex)).digest() return hashlib_new('ripemd160',step1).hexdigest() -def hexaddr2addr(hexaddr): +def hexaddr2addr(hexaddr, vers_num='00'): # See above: - extra_ones = (len(hexaddr) - len(hexaddr.lstrip("0"))) / 2 - step1 = sha256(unhexlify('00'+hexaddr)).digest() + hexaddr2 = vers_num + hexaddr + step1 = sha256(unhexlify(hexaddr2)).digest() step2 = sha256(step1).hexdigest() - pubkey = int(hexaddr + step2[:8], 16) - return "1" + ("1" * extra_ones) + _numtob58(pubkey) - -def pubhex2addr(pubhex): - return hexaddr2addr(pubhex2hexaddr(pubhex)) + pubkey = hexaddr2 + step2[:8] + lzeroes = (len(hexaddr2) - len(hexaddr2.lstrip("0"))) / 2 + return ("1" * lzeroes) + _numtob58(int(pubkey,16)) def verify_addr(addr,verbose=False,return_hex=False): - if addr[0] != "1": - if verbose: print "%s: Invalid address" % addr - return False + for vers_num,ldigit in ('00','1'),('05','3'): + if addr[0] != ldigit: continue + num = _b58tonum(addr) + if num == False: break + addr_hex = "{:050x}".format(num) + if addr_hex[:2] != vers_num: continue + step1 = sha256(unhexlify(addr_hex[:42])).digest() + step2 = sha256(step1).hexdigest() + if step2[:8] == addr_hex[42:]: + return addr_hex[2:42] if return_hex else True + else: + if verbose: print "Invalid checksum in address '%s'" % addr + break - num = _b58tonum(addr[1:]) - if num == False: return False - addr_hex = hex(num)[2:].rstrip("L").zfill(48) + if verbose: print "Invalid address '%s'" % addr + return False - step1 = sha256(unhexlify('00'+addr_hex[:40])).digest() - step2 = sha256(step1).hexdigest() - - if step2[:8] != addr_hex[40:]: - if verbose: print "Invalid checksum in address %s" % ("1" + addr) - return False - - return addr_hex[:40] if return_hex else True # Reworked code from here: def _numtob58(num): - b58conv,i = [],0 - while True: - n = num / (58**i); i += 1 - if not n: break - b58conv.append(b58a[n % 58]) - return ''.join(b58conv)[::-1] + ret = [] + while num: + ret.append(b58a[num % 58]) + num /= 58 + return ''.join(ret)[::-1] def _b58tonum(b58num): for i in b58num: - if not i in b58a: - print "Invalid symbol in b58 number: '%s'" % i - return False - - b58deconv = [] - b58num_r = b58num[::-1] - for i in range(len(b58num)): - idx = b58a.index(b58num_r[i]) - b58deconv.append(idx * (58**i)) - return sum(b58deconv) + if not i in b58a: return False + return sum([b58a.index(n) * (58**i) for i,n in enumerate(list(b58num[::-1]))]) def numtowif(numpriv): - step1 = '80'+hex(numpriv)[2:].rstrip('L').zfill(64) + step1 = '80' + "{:064x}".format(numpriv) step2 = sha256(unhexlify(step1)).digest() step3 = sha256(step2).hexdigest() key = step1 + step3[:8] @@ -131,8 +122,8 @@ def b58decode(b58num): # Zap all spaces: num = _b58tonum(b58num.translate(None,' \t\n\r')) if num == False: return False - out = hex(num)[2:].rstrip('L') - return unhexlify("0" + out if len(out) % 2 else out) + out = "{:x}".format(num) + return unhexlify("0"*(len(out)%2) + out) # These yield bytewise equivalence in our special cases: @@ -165,7 +156,7 @@ def wiftohex(wifpriv,compressed=False): idx = 68 if compressed else 66 num = _b58tonum(wifpriv) if num == False: return False - key = hex(num)[2:].rstrip('L') + key = "{:x}".format(num) if compressed and key[66:68] != '01': return False round1 = sha256(unhexlify(key[:idx])).digest() round2 = sha256(round1).hexdigest() @@ -182,16 +173,10 @@ def privnum2pubhex(numpriv,compressed=False): pko = ecdsa.SigningKey.from_secret_exponent(numpriv,secp256k1) pubkey = hexlify(pko.get_verifying_key().to_string()) if compressed: - p = '03' if pubkey[-1] in "13579bdf" else '02' + p = '02' if pubkey[-1] in "02468ace" else '03' return p+pubkey[:64] else: return '04'+pubkey def privnum2addr(numpriv,compressed=False): - return pubhex2addr(privnum2pubhex(numpriv,compressed)) - -# Used only in test suite. To check validity, recode with numtowif() -def wiftonum(wifpriv): - num = _b58tonum(wifpriv) - if num == False: return False - return (num % (1<<288)) >> 32 + return hexaddr2addr(pubhex2hexaddr(privnum2pubhex(numpriv,compressed))) diff --git a/mmgen/config.py b/mmgen/config.py index 39b7bb4e..42b22232 100755 --- a/mmgen/config.py +++ b/mmgen/config.py @@ -28,7 +28,8 @@ email = "" Cdates = '2013-2014' version = '0.7.7' -quiet,verbose = False,False +quiet,verbose,no_keyconv = False,False,False + min_screen_width = 80 max_tx_comment_len = 72 @@ -52,18 +53,18 @@ sigtx_ext = "sig" addrfile_ext = "addrs" addrfile_chksum_ext = "chk" keyfile_ext = "keys" -keylist_ext = "keylist" +keyaddrfile_ext = "akeys" mmenc_ext = "mmenc" default_wl = "electrum" #default_wl = "tirosh" -cl_override_vars = 'seed_len','hash_preset','usr_randchars' +dfl_vars = "seed_len","hash_preset","usr_randchars" seed_lens = 128,192,256 seed_len = 256 -mnemonic_lens = [i / 32 * 3 for i in seed_lens] +mn_lens = [i / 32 * 3 for i in seed_lens] http_timeout = 30 diff --git a/mmgen/crypto.py b/mmgen/crypto.py index d4622090..8b9d950e 100755 --- a/mmgen/crypto.py +++ b/mmgen/crypto.py @@ -28,13 +28,41 @@ import mmgen.config as g from mmgen.util import * from mmgen.term import get_char +crmsg = { + 'incog_iv_id': """ + Check that the generated Incog ID above is correct. + If it's not, then your incognito data is incorrect or corrupted. +""", + 'incog_iv_id_hidden': """ + Check that the generated Incog ID above is correct. + If it's not, then your incognito data is incorrect or corrupted, + or you've supplied an incorrect offset. +""", + 'usr_rand_notice': """ +You've chosen to not fully trust your OS's random number generator and provide +some additional entropy of your own. Please type %s symbols on your keyboard. +Type slowly and choose your symbols carefully for maximum randomness. Try to +use both upper and lowercase as well as punctuation and numerals. What you +type will not be displayed on the screen. Note that the timings between your +keystrokes will also be used as a source of randomness. +""", + 'incorrect_incog_passphrase_try_again': """ +Incorrect passphrase, hash preset, or maybe old-format incog wallet. +Try again? (Y)es, (n)o, (m)ore information: +""".strip(), + 'confirm_seed_id': """ +If the seed ID above is correct but you're seeing this message, then you need +to exit and re-run the program with the '--old-incog-fmt' option. +""".strip(), +} + def encrypt_seed(seed, key): return encrypt_data(seed, key, iv=1, what="seed") def decrypt_seed(enc_seed, key, seed_id, key_id): - vmsg("Checking key...") + vmsg_r("Checking key...") chk1 = make_chksum_8(key) if key_id: if not compare_checksums(chk1, "of key", key_id, "in header"): @@ -56,12 +84,14 @@ def decrypt_seed(enc_seed, key, seed_id, key_id): else: msg("Incorrect passphrase") + vmsg("") return False # else: # qmsg("Generated IDs (Seed/Key): %s/%s" % (chk2,chk1)) if g.debug: print "Decrypted seed: %s" % hexlify(dec_seed) + vmsg("OK") return dec_seed @@ -97,7 +127,7 @@ def encrypt_data(data, key, iv=1, what="data", verify=True): def decrypt_data(enc_data, key, iv=1, what="data"): - vmsg("Decrypting %s with key..." % what) + vmsg_r("Decrypting %s with key..." % what) from Crypto.Cipher import AES from Crypto.Util import Counter @@ -119,10 +149,10 @@ def scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32): return scrypt.hash(passwd, salt, 2**N, r, p, buflen=buflen) -def make_key(passwd, salt, hash_preset, what="key", verbose=False): +def make_key(passwd, salt, hash_preset, what="encryption key", verbose=False): if g.verbose or verbose: - msg_r("Generating %s. Please wait..." % what) + msg_r("Generating %s from passphrase.\nPlease wait..." % what) key = scrypt_hash_passphrase(passwd, salt, hash_preset) if g.verbose or verbose: msg("done") @@ -133,7 +163,7 @@ def make_key(passwd, salt, hash_preset, what="key", verbose=False): def get_random_data_from_user(uchars): if g.quiet: msg("Enter %s random symbols" % uchars) - else: msg(cmessages['usr_rand_notice'] % uchars) + else: msg(crmsg['usr_rand_notice'] % uchars) prompt = "You may begin typing. %s symbols left: " msg_r(prompt % uchars) @@ -187,26 +217,60 @@ def get_random(length,opts): def get_seed_from_wallet( infile, opts, - prompt="{} wallet".format(g.proj_name), + prompt_info="{} wallet".format(g.proj_name), silent=False ): wdata = get_data_from_wallet(infile,silent=silent) label,metadata,hash_preset,salt,enc_seed = wdata - if g.verbose: display_control_data(*wdata) + if g.debug: display_control_data(*wdata) - passwd = get_mmgen_passphrase(prompt,opts) + padd = " "+infile if g.quiet else "" + passwd = get_mmgen_passphrase(prompt_info+padd,opts) key = make_key(passwd, salt, hash_preset) return decrypt_seed(enc_seed, key, metadata[0], metadata[1]) +def get_hidden_incog_data(opts): + # Already sanity-checked: + fname,offset,seed_len = opts['from_incog_hidden'].split(",") + qmsg("Getting hidden incog data from file '%s'" % fname) + + z = 0 if 'old_incog_fmt' in opts else 8 + dlen = g.aesctr_iv_len + g.salt_len + (int(seed_len)/8) + z + + fsize = check_data_fits_file_at_offset(fname,int(offset),dlen,"read") + + import os + f = os.open(fname,os.O_RDONLY) + os.lseek(f, int(offset), os.SEEK_SET) + data = os.read(f, dlen) + os.close(f) + qmsg("Data read from file '%s' at offset %s" % (fname,offset), + "Data read from file") + return data + +def confirm_old_format(): + + while True: + reply = get_char( + crmsg['incorrect_incog_passphrase_try_again']+" ").strip("\n\r") + if not reply: msg(""); return False + elif reply in 'yY': msg(""); return False + elif reply in 'nN': msg("\nExiting at user request"); sys.exit(1) + elif reply in 'mM': msg(""); return True + else: + if g.verbose: msg("\nInvalid reply") + else: msg_r("\r") + + def get_seed_from_incog_wallet( infile, opts, - prompt_what="{} incognito wallet".format(g.proj_name), + prompt_info="{} incognito wallet".format(g.proj_name), silent=False, hex_input=False ): @@ -224,75 +288,66 @@ def get_seed_from_incog_wallet( msg("Data in file '%s' is not in hexadecimal format" % infile) sys.exit(2) # File could be of invalid length, so check: - valid_dlens = [i/8 + g.aesctr_iv_len + g.salt_len for i in g.seed_lens] + z = 0 if 'old_incog_fmt' in opts else 8 + valid_dlens = [i/8 + g.aesctr_iv_len + g.salt_len + z for i in g.seed_lens] + # New fmt: [56, 64, 72]. Old fmt: [48, 56, 64]. if len(d) not in valid_dlens: - qmsg("Invalid incognito file size: %s. Valid sizes (in bytes): %s" % - (len(d), " ".join([str(i) for i in valid_dlens])) - ) + vn = [i/8 + g.aesctr_iv_len + g.salt_len + 8 for i in g.seed_lens] + if len(d) in vn: + msg("Re-run the program without the '--old-incog-fmt' option") + sys.exit() + else: qmsg( + "Invalid incognito file size: %s. Valid sizes (in bytes): %s" % + (len(d), " ".join([str(i) for i in valid_dlens]))) return False iv, enc_incog_data = d[0:g.aesctr_iv_len], d[g.aesctr_iv_len:] - msg("Incog ID: %s (IV ID: %s)" % (make_iv_chksum(iv),make_chksum_8(iv))) + incog_id = make_iv_chksum(iv) + msg("Incog ID: %s (IV ID: %s)" % (incog_id,make_chksum_8(iv))) qmsg("Check the applicable value against your records.") - vmsg(cmessages['incog_iv_id_hidden' if "from_incog_hidden" in opts + vmsg(crmsg['incog_iv_id_hidden' if "from_incog_hidden" in opts else 'incog_iv_id']) - passwd = get_mmgen_passphrase(prompt_what,opts) - - qmsg("Configured hash presets: %s" % " ".join(sorted(g.hash_presets))) while True: - p = "Enter hash preset for %s wallet (default='%s'): " - hp = my_raw_input(p % (g.proj_name, g.hash_preset)) - if not hp: - hp = g.hash_preset; break - elif hp in g.hash_presets: - break - msg("%s: Invalid hash preset" % hp) + passwd = get_mmgen_passphrase(prompt_info+" "+incog_id,opts) - # IV is used BOTH to initialize counter and to salt password! - key = make_key(passwd, iv, hp, "wrapper key") - d = decrypt_data(enc_incog_data, key, int(hexlify(iv),16), "incog data") - if d == False: sys.exit(2) + qmsg("Configured hash presets: %s" % " ".join(sorted(g.hash_presets))) + hp = get_hash_preset_from_user(what="incog wallet") - salt,enc_seed = d[0:g.salt_len], d[g.salt_len:] + # IV is used BOTH to initialize counter and to salt password! + key = make_key(passwd, iv, hp, "wrapper key") + d = decrypt_data(enc_incog_data, key, int(hexlify(iv),16), "incog data") - key = make_key(passwd, salt, hp, "main key") - vmsg("Key ID: %s" % make_chksum_8(key)) + salt,enc_seed = d[0:g.salt_len], d[g.salt_len:] - seed = decrypt_seed(enc_seed, key, "", "") - qmsg("Seed ID: %s. Check that this value is correct." % make_chksum_8(seed)) - vmsg(cmessages['incog_key_id_hidden' if "from_incog_hidden" in opts - else 'incog_key_id']) + key = make_key(passwd, salt, hp, "main key") + vmsg("Key ID: %s" % make_chksum_8(key)) + + seed = decrypt_seed(enc_seed, key, "", "") + old_fmt_sid = make_chksum_8(seed) + + def confirm_correct_seed_id(sid): + m = "Seed ID: %s. Is the Seed ID correct?" % sid + return keypress_confirm(m, True) + + if 'old_incog_fmt' in opts: + if confirm_correct_seed_id(old_fmt_sid): + break + else: + chk,seed_maybe = seed[:8],seed[8:] + if sha256(seed_maybe).digest()[:8] == chk: + msg("Passphrase and hash preset are correct") + seed = seed_maybe + break + elif confirm_old_format(): + if confirm_correct_seed_id(old_fmt_sid): + break return seed -def wallet_to_incog_data(infile,opts): - - d = get_data_from_wallet(infile,silent=True) - seed_id,key_id,preset,salt,enc_seed = \ - d[1][0], d[1][1], d[2].split(":")[0], d[3], d[4] - - passwd = get_mmgen_passphrase("{} wallet".format(g.proj_name),opts) - key = make_key(passwd, salt, preset, "main key") - # We don't need the seed; just do this to verify password. - if decrypt_seed(enc_seed, key, seed_id, key_id) == False: - sys.exit(2) - - iv = get_random(g.aesctr_iv_len,opts) - iv_id = make_iv_chksum(iv) - msg("Incog ID: %s" % iv_id) - - # IV is used BOTH to initialize counter and to salt password! - key = make_key(passwd, iv, preset, "wrapper key") - m = "incog data" - wrap_enc = encrypt_data(salt + enc_seed, key, int(hexlify(iv),16), m) - - return iv+wrap_enc,seed_id,key_id,iv_id,preset - - -def get_seed(infile,opts,silent=False): +def _get_seed(infile,opts,silent=False,seed_id=""): ext = get_extension(infile) @@ -313,8 +368,9 @@ def get_seed(infile,opts,silent=False): else: msg("No seed source type specified and no file supplied") sys.exit(2) + seed_id_str = " for seed ID "+seed_id if seed_id else "" if source == "mnemonic": - prompt = "Enter mnemonic: " + prompt = "Enter mnemonic%s: " % seed_id_str words = get_words(infile,"mnemonic data",prompt,opts) wl = get_default_wordlist() from mmgen.mnemonic import get_seed_from_mnemonic @@ -323,17 +379,17 @@ def get_seed(infile,opts,silent=False): if 'from_brain' not in opts: msg("'--from-brain' parameters must be specified for brainwallet file") sys.exit(2) - prompt = "Enter brainwallet passphrase: " + prompt = "Enter brainwallet passphrase%s: " % seed_id_str words = get_words(infile,"brainwallet data",prompt,opts) seed = _get_seed_from_brain_passphrase(words,opts) elif source == "seed": - prompt = "Enter seed in %s format: " % g.seed_ext + prompt = "Enter seed%s in %s format: " % (seed_id_str,g.seed_ext) words = get_words(infile,"seed data",prompt,opts) seed = get_seed_from_seed_data(words) elif source == "wallet": seed = get_seed_from_wallet(infile, opts, silent=silent) elif source == "incognito wallet": - h = True if ext == g.incog_hex_ext or 'from_incog_hex' in opts else False + h = ext == g.incog_hex_ext or 'from_incog_hex' in opts seed = get_seed_from_incog_wallet(infile, opts, silent=silent, hex_input=h) @@ -348,10 +404,10 @@ def get_seed(infile,opts,silent=False): # Repeat if entered data is invalid -def get_seed_retry(infile,opts): +def get_seed_retry(infile,opts,seed_id=""): silent = False while True: - seed = get_seed(infile,opts,silent=silent) + seed = _get_seed(infile,opts,silent=silent,seed_id=seed_id) silent = True if seed: return seed @@ -385,7 +441,7 @@ def mmgen_encrypt(data,what="data",hash_preset='',opts={}): return salt+iv+enc_d -def mmgen_decrypt(data,what="data",hash_preset='',opts={}): +def mmgen_decrypt(data,what="data",hash_preset=""): dstart = salt_len + g.aesctr_iv_len salt,iv,enc_d = data[:salt_len],data[salt_len:dstart],data[dstart:] vmsg("Preparing to decrypt %s" % what) @@ -396,8 +452,14 @@ def mmgen_decrypt(data,what="data",hash_preset='',opts={}): key = make_key(passwd, salt, hp) dec_d = decrypt_data(enc_d, key, int(hexlify(iv),16), what) if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest(): - vmsg("Success. Passphrase and hash preset are correct") + vmsg("OK") return dec_d[sha256_len+nonce_len:] else: msg("Incorrect passphrase or hash preset") return False + +def mmgen_decrypt_retry(d,what="data"): + while True: + d_dec = mmgen_decrypt(d,what) + if d_dec: return d_dec + msg("Trying again...") diff --git a/mmgen/main.py b/mmgen/main.py index 88a19b8b..27e67399 100755 --- a/mmgen/main.py +++ b/mmgen/main.py @@ -41,3 +41,7 @@ def main(progname): sys.stderr.write("\nUser interrupt\n") termios.tcsetattr(fd, termios.TCSADRAIN, old) sys.exit(1) + except EOFError: + sys.stderr.write("\nEnd of file\n") + termios.tcsetattr(fd, termios.TCSADRAIN, old) + sys.exit(1) diff --git a/mmgen/main_addrgen.py b/mmgen/main_addrgen.py index 1c38ee5a..92bf2b62 100755 --- a/mmgen/main_addrgen.py +++ b/mmgen/main_addrgen.py @@ -42,10 +42,10 @@ help_data = { -h, --help Print this help message{} -d, --outdir= d Specify an alternate directory 'd' for output -c, --save-checksum Save address list checksum to file --e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry{} +-e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry -H, --show-hash-presets Show information on available hash presets --K, --no-keyconv Use internal libraries for address generation - instead of 'keyconv' +-K, --no-keyconv Force use of internal libraries for address gener- + ation, even if 'keyconv' is available -l, --seed-len= N Length of seed. Options: {seed_lens} (default: {g.seed_len}) -p, --hash-preset= p Use scrypt.hash() parameters from preset 'p' when @@ -63,17 +63,16 @@ help_data = { -X, --from-incog-hex Generate {what} from incognito hexadecimal wallet -G, --from-incog-hidden=f,o,l Generate {what} from incognito data in file 'f' at offset 'o', with seed length of 'l' +-o, --old-incog-fmt Use old (pre-0.7.8) incog format -m, --from-mnemonic Generate {what} from an electrum-like mnemonic -s, --from-seed Generate {what} from a seed in .{g.seed_ext} format """.format( *( - ( + ( "\n-A, --no-addresses Print only secret keys, no addresses", -"\n-f, --flat-list Produce a flat list of keys suitable for use with" + -"\n '{}-txsign'".format(g.proj_name.lower()), "\n-x, --b16 Print secret keys in hexadecimal too" ) - if what == "keys" else ("","","")), + if what == "keys" else ("","")), seed_lens=", ".join([str(i) for i in g.seed_lens]), what=what, g=g ), @@ -109,6 +108,13 @@ invocations with that passphrase if what == "keys" else "") } +wmsg = { + 'unencrypted_secret_keys': """ +This program generates secret keys from your {} seed, outputting them in +UNENCRYPTED form. Generate only the key(s) you need and guard them carefully. +""".format(g.proj_name), +} + opts,cmd_args = parse_opts(sys.argv,help_data) if 'show_hash_presets' in opts: show_hash_presets() @@ -129,7 +135,7 @@ elif len(cmd_args) == 2: check_infile(infile) else: usage(help_data) -addr_idxs = parse_address_list(addr_idx_arg) +addr_idxs = parse_addr_idxs(addr_idx_arg) if not addr_idxs: sys.exit(2) @@ -137,38 +143,44 @@ do_license_msg() # Interact with user: if what == "keys" and not g.quiet: - confirm_or_exit(cmessages['unencrypted_secret_keys'], 'continue') + confirm_or_exit(wmsg['unencrypted_secret_keys'], 'continue') # Generate data: seed = get_seed_retry(infile,opts) seed_id = make_chksum_8(seed) -for l in ( - ('flat_list', 'no_addresses'), - ('flat_list', 'b16'), -): warn_incompatible_opts(opts,l) +opts['gen_what'] = "a" if what == "addresses" else ( + "k" if 'no_addresses' in opts else "ka") -opts['gen_what'] = \ - ["addrs"] if what == "addresses" else ( - ["keys"] if 'no_addresses' in opts else ["addrs","keys"]) -addr_data = generate_addrs(seed, addr_idxs, opts) -addr_data_chksum = make_addr_data_chksum([(a.num,a.addr) - for a in addr_data]) if 'addrs' in opts['gen_what'] else "" -addr_data_str = format_addr_data( +addr_data = generate_addrs(seed, addr_idxs, opts) + +if 'a' in opts['gen_what']: + if 'k' in opts['gen_what']: + def l(a): return ( a.num, (a.addr,"",a.wif) ) + keys = True + else: + def l(a): return ( a.num, (a.addr,) ) + keys = False + addr_data_chksum = make_addr_data_chksum([l(a) for a in addr_data],keys) +else: + addr_data_chksum = "" + +addr_data_str = format_addr_data( addr_data, addr_data_chksum, seed_id, addr_idxs, opts) outfile_base = "{}[{}]".format(seed_id, fmt_addr_idxs(addr_idxs)) -if 'addrs' in opts['gen_what']: - qmsg("Checksum for address data %s: %s" % (outfile_base,addr_data_chksum)) +if 'a' in opts['gen_what']: + w = "key-address" if 'k' in opts['gen_what'] else "address" + qmsg("Checksum for %s data %s: %s" % (w,outfile_base,addr_data_chksum)) if 'save_checksum' in opts: write_to_file(outfile_base+"."+g.addrfile_chksum_ext, - addr_data_chksum+"\n",opts,"address data checksum",True,True,False) + addr_data_chksum+"\n",opts,"%s data checksum" % w,True,True,False) else: - qmsg("This checksum will be used to verify the address file in the future.") + qmsg("This checksum will be used to verify the %s file in the future."%w) qmsg("Record it to a safe location.") -if 'flat_list' in opts and keypress_confirm("Encrypt key list?"): +if 'k' in opts['gen_what'] and keypress_confirm("Encrypt key list?"): addr_data_str = mmgen_encrypt(addr_data_str,"key list","",opts) enc_ext = "." + g.mmenc_ext else: enc_ext = "" @@ -178,13 +190,11 @@ if 'stdout' in opts or not sys.stdout.isatty(): if enc_ext and sys.stdout.isatty(): msg("Cannot write encrypted data to screen. Exiting") sys.exit(2) - c = True if (what == "keys" and not g.quiet and sys.stdout.isatty()) else False - write_to_stdout(addr_data_str,what,c) + write_to_stdout(addr_data_str,what, + (what=="keys"and not g.quiet and sys.stdout.isatty())) else: - confirm_overwrite = False if g.quiet else True outfile = "%s.%s%s" % (outfile_base, ( - g.keylist_ext if 'flat_list' in opts else ( - g.keyfile_ext if opts['gen_what'] == ["keys"] else ( - g.addrfile_ext if opts['gen_what'] == ["addrs"] else "akeys"))), enc_ext) - write_to_file(outfile,addr_data_str,opts,what,confirm_overwrite,True) - + g.keyaddrfile_ext if "ka" in opts['gen_what'] else ( + g.keyfile_ext if "k" in opts['gen_what'] else + g.addrfile_ext)), enc_ext) + write_to_file(outfile,addr_data_str,opts,what,not g.quiet,True) diff --git a/mmgen/main_addrimport.py b/mmgen/main_addrimport.py index f39e50d8..2fe91bfd 100755 --- a/mmgen/main_addrimport.py +++ b/mmgen/main_addrimport.py @@ -24,7 +24,7 @@ import sys from mmgen.Opts import * from mmgen.license import * from mmgen.util import * -from mmgen.tx import connect_to_bitcoind,parse_addrs_file +from mmgen.tx import connect_to_bitcoind,parse_addrfile,parse_keyaddr_file help_data = { 'prog_name': g.prog_name, @@ -32,38 +32,44 @@ help_data = { watching wallet""".format(pnm=g.proj_name), 'usage':"[opts] [mmgen address file]", 'options': """ --h, --help Print this help message --l, --addrlist= f Import the non-mmgen Bitcoin addresses listed in file 'f' --q, --quiet Suppress warnings --r, --rescan Rescan the blockchain. Required if address to import is - on the blockchain and has a balance. Rescanning is slow. +-h, --help Print this help message +-l, --addrlist Address source is a flat list of addresses +-k, --keyaddr-file Address source is a key-address file +-q, --quiet Suppress warnings +-r, --rescan Rescan the blockchain. Required if address to import is + on the blockchain and has a balance. Rescanning is slow. """ } opts,cmd_args = parse_opts(sys.argv,help_data) -if len(cmd_args) != 1 and not 'addrlist' in opts: - msg("You must specify an mmgen address list (and/or non-mmgen addresses with the '--addrlist' option)") - sys.exit(1) - -if cmd_args: - check_infile(cmd_args[0]) - seed_id,addr_data = parse_addrs_file(cmd_args[0]) +if len(cmd_args) == 1: + infile = cmd_args[0] + check_infile(infile) + if 'addrlist' in opts: + lines = get_lines_from_file(infile,"non-{} addresses".format(g.proj_name), + trim_comments=True) + addr_list = [(None,l) for l in lines] + seed_id = "" + else: + addr_data = {} + pf = parse_keyaddr_file if 'keyaddr_file' in opts else parse_addrfile + pf(infile,addr_data) + seed_id = addr_data.keys()[0] + e = addr_data[seed_id] + addr_list = [(k,e[k][0],e[k][1]) for k in e.keys()] else: - seed_id,addr_data = "",[] - -if 'addrlist' in opts: - lines = get_lines_from_file(opts['addrlist'],"non-mmgen addresses", - trim_comments=True) - addr_data += [(None,l) for l in lines] + msg_r("You must specify an mmgen address list (or a list of ") + msg("non-%s addresses with\nthe '--addrlist' option)" % g.proj_name) + sys.exit(1) from mmgen.bitcoin import verify_addr qmsg_r("Validating addresses...") -for i in addr_data: +for n,i in enumerate(addr_list,1): if not verify_addr(i[1],verbose=True): msg("%s: invalid address" % i) sys.exit(2) -qmsg("OK") +qmsg("OK. %s addresses%s" % (n," from seed ID "+seed_id if seed_id else "")) import mmgen.config as g g.http_timeout = 3600 @@ -94,8 +100,9 @@ def import_address(addr,label,rescan): err_flag = True -w1 = len(str(len(addr_data))) * 2 + 2 -w2 = len(str(max([i[0] for i in addr_data if i[0]]))) + 12 +w1 = len(str(len(addr_list))) * 2 + 2 +w2 = "" if 'addrlist' in opts else \ + len(str(max([i[0] for i in addr_list if i[0]]))) + 12 \ if "rescan" in opts: import threading @@ -105,10 +112,9 @@ else: msg_fmt = "\r%-" + str(w1) + "s %-34s %-" + str(w2) + "s" msg("Importing addresses") -for n,i in enumerate(addr_data): +for n,i in enumerate(addr_list): if i[0]: - comment = " " + i[2] if len(i) == 3 else "" - label = "%s:%s%s" % (seed_id,i[0],comment) + label = "%s:%s%s" % (seed_id,i[0], (" "+i[2] if i[2] else "")) else: label = "non-mmgen" if "rescan" in opts: @@ -123,7 +129,7 @@ for n,i in enumerate(addr_data): elapsed = int(time.time() - start) msg_r(msg_fmt % ( secs_to_hms(elapsed), - ("%s/%s:" % (n+1,len(addr_data))), + ("%s/%s:" % (n+1,len(addr_list))), i[1], "(" + label + ")" ) ) @@ -134,7 +140,7 @@ for n,i in enumerate(addr_data): break else: import_address(i[1],label,rescan=False) - msg_r(msg_fmt % (("%s/%s:" % (n+1,len(addr_data))), + msg_r(msg_fmt % (("%s/%s:" % (n+1,len(addr_list))), i[1], "(" + label + ")")) if err_flag: msg("\nImport failed"); sys.exit(2) msg(" - OK") diff --git a/mmgen/main_pywallet.py b/mmgen/main_pywallet.py index 086ccebe..21a806d5 100755 --- a/mmgen/main_pywallet.py +++ b/mmgen/main_pywallet.py @@ -1671,7 +1671,7 @@ data = "\n".join(data) + "\n" # Output data if 'stdout' in opts or not sys.stdout.isatty(): - c = False if ('addrs' in opts or not sys.stdout.isatty()) else True - write_to_stdout(data,"secret keys",c) + conf = not ('addrs' in opts or not sys.stdout.isatty()) + write_to_stdout(data,"secret keys",conf) else: write_walletdat_dump_to_file(wallet_id, data, len_arg, ext, what, opts) diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index 09028790..fa12e0b1 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -28,7 +28,6 @@ import mmgen.config as g from mmgen.Opts import * from mmgen.license import * from mmgen.tx import * -from mmgen.util import msg, msg_r, keypress_confirm help_data = { 'prog_name': g.prog_name, @@ -43,6 +42,7 @@ help_data = { -i, --info Display unspent outputs and exit -q, --quiet Suppress warnings; overwrite files without prompting +-v, --verbose Produce more verbose output """.format(g=g), 'notes': """ @@ -60,40 +60,300 @@ with no amount on the command line. """.format(g=g,pnm=g.proj_name) } +wmsg = { + 'too_many_acct_addresses': """ +ERROR: More than one address found for account: "%s". +Your "wallet.dat" file appears to have been altered by a non-{pnm} program. +Please restore your tracking wallet from a backup or create a new one and +re-import your addresses. +""".strip().format(pnm=g.proj_name), + 'addr_in_addrfile_only': """ +Warning: output address {mmgenaddr} is not in the tracking wallet, which means +its balance will not be tracked. You're strongly advised to import the address +into your tracking wallet before broadcasting this transaction. +""".strip(), + 'addr_not_found': """ +No data for MMgen address {mmgenaddr} could be found in either the tracking +wallet or the supplied address file. Please import this address into your +tracking wallet, or supply an address file for it on the command line. +""".strip(), + 'addr_not_found_no_addrfile': """ +No data for MMgen address {mmgenaddr} could be found in the tracking wallet. +Please import this address into your tracking wallet or supply an address file +for it on the command line. +""".strip(), + 'no_spendable_outputs': """ +No spendable outputs found! Import addresses with balances into your +watch-only wallet using '{pnm}-addrimport' and then re-run this program. +""".strip().format(pnm=g.proj_name.lower()), + 'mixed_inputs': """ +NOTE: This transaction uses a mixture of both mmgen and non-mmgen inputs, which +makes the signing process more complicated. When signing the transaction, keys +for the non-{pnm} inputs must be supplied to '{pnl}-txsign' in a file with the +'--keys-from-file' option. + +Selected mmgen inputs: %s +""".strip().format(pnm=g.proj_name,pnl=g.proj_name.lower()), + 'not_enough_btc': """ +Not enough BTC in the inputs for this transaction (%s BTC) +""".strip(), + 'throwaway_change': """ +ERROR: This transaction produces change (%s BTC); however, no change address +was specified. +""".strip(), +} + +def format_unspent_outputs_for_printing(out,sort_info,total): + + pfs = " %-4s %-67s %-34s %-12s %-13s %-8s %-10s %s" + pout = [pfs % ("Num","TX id,Vout","Address","MMgen ID", + "Amount (BTC)","Conf.","Age (days)", "Comment")] + + for n,i in enumerate(out): + addr = "=" if i.skip == "addr" and "grouped" in sort_info else i.address + tx = " " * 63 + "=" \ + if i.skip == "txid" and "grouped" in sort_info else str(i.txid) + + s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr, + i.mmid,i.amt,i.confirmations,i.days,i.label) + pout.append(s.rstrip()) + + return \ +"Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n".format( + make_timestr(), " ".join(sort_info), "\n".join(pout), total + ) + + +def sort_and_view(unspent,opts): + + def s_amt(i): return i.amount + def s_txid(i): return "%s %03s" % (i.txid,i.vout) + def s_addr(i): return i.address + def s_age(i): return i.confirmations + def s_mmgen(i): + m = parse_mmgen_label(i.account)[0] + if m: return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *m.split(":")) + else: return "G" + i.account + + sort,group,show_days,show_mmaddr,reverse = "age",False,False,True,True + unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default + + total = trim_exponent(sum([i.amount for i in unspent])) + max_acct_len = max([len(i.account) for i in unspent]) + + hdr_fmt = "UNSPENT OUTPUTS (sort order: %s) Total BTC: %s" + options_msg = """ +Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr +Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen +""".strip() + prompt = \ +"('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): " + + from copy import deepcopy + from mmgen.term import get_terminal_size + + write_to_file_msg = "" + msg("") + + while True: + cols = get_terminal_size()[0] + if cols < g.min_screen_width: + msg("%s-txcreate requires a screen at least %s characters wide" % + (g.proj_name.lower(),g.min_screen_width)) + sys.exit(2) + + addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46) + acct_w = min(max_acct_len, max(24,int(addr_w-10))) + btaddr_w = addr_w - acct_w - 1 + tx_w = max(11,min(64, cols-addr_w-32)) + txdots = "..." if tx_w < 64 else "" + fs = " %-4s %-" + str(tx_w) + "s %-2s %-" + str(addr_w) + "s %-13s %-s" + table_hdr = fs % ("Num","TX id Vout","","Address","Amount (BTC)", + "Age(d)" if show_days else "Conf.") + + unsp = deepcopy(unspent) + for i in unsp: i.skip = "" + if group and (sort == "address" or sort == "txid"): + for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]: + if sort == "address" and a.address == b.address: b.skip = "addr" + elif sort == "txid" and a.txid == b.txid: b.skip = "txid" + + for i in unsp: + amt = str(trim_exponent(i.amount)) + lfill = 3 - len(amt.split(".")[0]) if "." in amt else 3 - len(amt) + i.amt = " "*lfill + amt + i.days = int(i.confirmations * g.mins_per_block / (60*24)) + i.age = i.days if show_days else i.confirmations + i.mmid,i.label = parse_mmgen_label(i.account) + + if i.skip == "addr": + i.addr = "|" + "." * 33 + else: + if show_mmaddr: + dots = ".." if btaddr_w < len(i.address) else "" + i.addr = "%s%s %s" % ( + i.address[:btaddr_w-len(dots)], + dots, + i.account[:acct_w]) + else: + i.addr = i.address + + i.tx = " " * (tx_w-4) + "|..." if i.skip == "txid" \ + else i.txid[:tx_w-len(txdots)]+txdots + + sort_info = ["reverse"] if reverse else [] + sort_info.append(sort if sort else "unsorted") + if group and (sort == "address" or sort == "txid"): + sort_info.append("grouped") + + out = [hdr_fmt % (" ".join(sort_info), total), table_hdr] + out += [fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,i.age) + for n,i in enumerate(unsp)] + + msg("\n".join(out) +"\n\n" + write_to_file_msg + options_msg) + write_to_file_msg = "" + + skip_prompt = False + + while True: + reply = get_char(prompt, immed_chars="atDdAMrgmeqpvw") + + if reply == 'a': unspent.sort(key=s_amt); sort = "amount" + elif reply == 't': unspent.sort(key=s_txid); sort = "txid" + elif reply == 'D': show_days = not show_days + elif reply == 'd': unspent.sort(key=s_addr); sort = "address" + elif reply == 'A': unspent.sort(key=s_age); sort = "age" + elif reply == 'M': + unspent.sort(key=s_mmgen); sort = "mmgen" + show_mmaddr = True + elif reply == 'r': + unspent.reverse() + reverse = not reverse + elif reply == 'g': group = not group + elif reply == 'm': show_mmaddr = not show_mmaddr + elif reply == 'e': pass + elif reply == 'q': pass + elif reply == 'p': + d = format_unspent_outputs_for_printing(unsp,sort_info,total) + of = "listunspent[%s].out" % ",".join(sort_info) + write_to_file(of, d, opts,"",False,False) + write_to_file_msg = "Data written to '%s'\n\n" % of + elif reply == 'v': + do_pager("\n".join(out)) + continue + elif reply == 'w': + data = format_unspent_outputs_for_printing(unsp,sort_info,total) + do_pager(data) + continue + else: + msg("\nInvalid input") + continue + + break + + msg("\n") + if reply == 'q': break + + return tuple(unspent) + + +def select_outputs(unspent,prompt): + + while True: + reply = my_raw_input(prompt).strip() + + if not reply: continue + + selected = parse_addr_idxs(reply,sep=None) + + if not selected: continue + + if selected[-1] > len(unspent): + msg("Inputs must be less than %s" % len(unspent)) + continue + + return selected + + +def get_acct_data_from_wallet(c,acct_data): + # acct_data is global object initialized by caller + vmsg_r("Getting account data from wallet...") + accts,i = c.listaccounts(minconf=0,includeWatchonly=True),0 + for acct in accts: + ma,comment = parse_mmgen_label(acct) + if ma: + i += 1 + addrlist = c.getaddressesbyaccount(acct) + if len(addrlist) != 1: + msg(wmsg['too_many_acct_addresses'] % acct) + sys.exit(2) + seed_id,idx = ma.split(":") + if seed_id not in acct_data: + acct_data[seed_id] = {} + acct_data[seed_id][idx] = (addrlist[0],comment) + vmsg("%s %s addresses found, %s accounts total" % (i,g.proj_name,len(accts))) + +def mmaddr2btcaddr_unspent(unspent,mmaddr): + vmsg_r("Searching for {g.proj_name} address {m} in wallet...".format(g=g,m=mmaddr)) + m = [u for u in unspent if u.account.split()[0] == mmaddr] + if len(m) == 0: + vmsg("not found") + return "","" + elif len(m) > 1: + msg(wmsg['too_many_acct_addresses'] % acct); sys.exit(2) + else: + vmsg("success (%s)" % m[0].address) + return m[0].address, split2(m[0].account)[1] + sys.exit() + + +def mmaddr2btcaddr(c,mmaddr,acct_data,addr_data,b2m_map): + # assume mmaddr has already been checked + if not acct_data: get_acct_data_from_wallet(c,acct_data) + btcaddr,comment = mmaddr2btcaddr_addrdata(mmaddr,acct_data,"wallet") +# btcaddr,comment = mmaddr2btcaddr_unspent(us,mmaddr) + if not btcaddr: + if addr_data: + btcaddr,comment = mmaddr2btcaddr_addrdata(mmaddr,addr_data,"addr file") + if btcaddr: + msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr)) + if not keypress_confirm("Continue anyway?"): + sys.exit(1) + else: + msg(wmsg['addr_not_found'].format(mmgenaddr=mmaddr)) + sys.exit(2) + else: + msg(wmsg['addr_not_found_no_addrfile'].format(mmgenaddr=mmaddr)) + sys.exit(2) + + b2m_map[btcaddr] = mmaddr,comment + return btcaddr + + opts,cmd_args = parse_opts(sys.argv,help_data) if g.debug: show_opts_and_cmd_args(opts,cmd_args) +if 'comment_file' in opts: + comment = get_tx_comment_from_file(opts['comment_file']) + c = connect_to_bitcoind() if not 'info' in opts: do_license_msg(immed=True) - tx_out,addr_data,b2m_map,acct_data,change_addr = {},[],{},[],"" + tx_out,addr_data,b2m_map,acct_data,change_addr = {},{},{},{},"" addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext] cmd_args = set(cmd_args) - set(addrfiles) for a in addrfiles: check_infile(a) - addr_data.append(parse_addrs_file(a)) - - def mmaddr2btcaddr(c,mmaddr,acct_data,addr_data,b2m_map): - # assume mmaddr has already been checked - btcaddr,label = mmaddr2btcaddr_bitcoind(c,mmaddr,acct_data) - if not btcaddr: - if addr_data: - btcaddr,label = mmaddr2btcaddr_addrfile(mmaddr,addr_data) - else: - msg(txmsg['addrfile_no_data_msg'] % mmaddr) - sys.exit(2) - - b2m_map[btcaddr] = mmaddr,label - return btcaddr + parse_addrfile(a,addr_data) for a in cmd_args: if "," in a: - a1,a2 = a.split(",") + a1,a2 = split2(a,",") if is_btc_addr(a1): btcaddr = a1 elif is_mmgen_addr(a1): @@ -131,16 +391,13 @@ if not 'info' in opts: if g.debug: show_opts_and_cmd_args(opts,cmd_args) -#write_to_file("bogus_unspent.json", repr(us), opts); sys.exit() - -#if False: -if g.bogus_wallet_data: - import mmgen.rpc +if g.bogus_wallet_data: # for debugging purposes only us = eval(get_data_from_file(g.bogus_wallet_data)) else: us = c.listunspent() +# write_to_file("bogus_unspent.json", repr(us), opts); sys.exit() -if not us: msg(txmsg['no_spendable_outputs']); sys.exit(2) +if not us: msg(wmsg['no_spendable_outputs']); sys.exit(2) unspent = sort_and_view(us,opts) @@ -165,7 +422,7 @@ while True: mmaddrs.discard("") if mmaddrs and len(mmaddrs) < len(sel_unspent): - msg(txmsg['mixed_inputs'] % ", ".join(sorted(mmaddrs))) + msg(wmsg['mixed_inputs'] % ", ".join(sorted(mmaddrs))) if not keypress_confirm("Accept?"): continue @@ -177,10 +434,10 @@ while True: if keypress_confirm(prompt,default_yes=True): break else: - msg(txmsg['not_enough_btc'] % change) + msg(wmsg['not_enough_btc'] % change) if change > 0 and not change_addr: - msg(txmsg['throwaway_change'] % change) + msg(wmsg['throwaway_change'] % change) sys.exit(2) if change_addr in tx_out and not change: @@ -198,8 +455,6 @@ if g.debug: print "tx_out:", repr(tx_out) if 'comment_file' in opts: - comment = get_tx_comment_from_file(opts['comment_file']) - if comment == False: sys.exit(2) if keypress_confirm("Edit comment?",False): comment = get_tx_comment_from_user(comment) else: @@ -219,7 +474,7 @@ metadata = tx_id, amt, make_timestamp() if reply and reply in "YyVv": view_tx_data(c,[i.__dict__ for i in sel_unspent],tx_hex,b2m_map, - comment,metadata,True if reply in "Vv" else False) + comment,metadata,reply in "Vv") prompt = "Save transaction?" if keypress_confirm(prompt,default_yes=True): diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index 31c36c81..677dd625 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -51,7 +51,7 @@ do_license_msg() tx_data = get_lines_from_file(infile,"signed transaction data") -metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_data(tx_data,infile) +metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,infile) qmsg("Signed transaction file '%s' is valid" % infile) @@ -60,8 +60,7 @@ c = connect_to_bitcoind() prompt = "View transaction data? (y)es, (N)o, (v)iew in pager" reply = prompt_and_get_char(prompt,"YyNnVv",enter_ok=True) if reply and reply in "YyVv": - view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata, - pager=True if reply in "Vv" else False) + view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,reply in "Vv") if keypress_confirm("Edit transaction comment?"): comment = get_tx_comment_from_user(comment) diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py index 2e28cae6..48c277ec 100755 --- a/mmgen/main_txsign.py +++ b/mmgen/main_txsign.py @@ -26,7 +26,6 @@ import mmgen.config as g from mmgen.Opts import * from mmgen.license import * from mmgen.tx import * -from mmgen.util import msg,qmsg help_data = { 'prog_name': g.prog_name, @@ -39,16 +38,18 @@ help_data = { -i, --info Display information about the transaction and exit -I, --tx-id Display transaction ID and exit -k, --keys-from-file= f Provide additional keys for non-{pnm} addresses --K, --all-keys-from-file=f Like '-k', only use the keyfile as key source - for ALL inputs, including {pnm} ones. Can be used - for online signing without an {pnm} seed source. - {pnm}-to-BTC mappings can optionally be verified - using address file(s) listed on the command line +-K, --no-keyconv Force use of internal libraries for address gener- + ation, even if 'keyconv' is available +-M, --mmgen-keys-from-file=f Provide keys for {pnm} addresses in a key- + address file (output of '{pnl}-keygen'). Permits + online signing without an {pnm} seed source. + The key-address file is also used to verify + {pnm}-to-BTC mappings, so its checksum should + be recorded by the user. -P, --passwd-file= f Get MMGen wallet or bitcoind passphrase from file 'f' -q, --quiet Suppress warnings; overwrite files without prompting -v, --verbose Produce more verbose output --V, --skip-key-preverify Skip optional key pre-verification step -b, --from-brain= l,p Generate keys from a user-created password, i.e. a "brainwallet", using seed length 'l' and hash preset 'p' @@ -57,9 +58,10 @@ help_data = { -X, --from-incog-hex Generate keys from an incognito hexadecimal wallet -G, --from-incog-hidden= f,o,l Generate keys from incognito data in file 'f' at offset 'o', with seed length of 'l' +-o, --old-incog-fmt Use old (pre-0.7.8) incog format -m, --from-mnemonic Generate keys from an electrum-like mnemonic -s, --from-seed Generate keys from a seed in .{g.seed_ext} format -""".format(g=g,pnm=g.proj_name), +""".format(g=g,pnm=g.proj_name,pnl=g.proj_name.lower()), 'notes': """ Transactions with either {pnm} or non-{pnm} input addresses may be signed. @@ -89,17 +91,204 @@ Seed data supplied in files must have the following extensions: """.format(g=g,pnm=g.proj_name,pnl=g.proj_name.lower()) } +wmsg = { + 'mm2btc_mapping_error': """ +MMGen -> BTC address mappings differ! +From %-18s %s -> %s +From %-18s %s -> %s +""".strip(), + 'removed_dups': """ +Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file +""".strip().format(pnm=g.proj_name), +} + +def get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts): + + if seed_id in saved_seeds.keys(): + return saved_seeds[seed_id] + + from mmgen.crypto import get_seed_retry + + while True: + if infiles: + seed = get_seed_retry(infiles.pop(0),opts) + elif "from_brain" in opts or "from_mnemonic" in opts \ + or "from_seed" in opts or "from_incog" in opts: + qmsg("Need seed data for seed ID %s" % seed_id) + seed = get_seed_retry("",opts,seed_id) + msg("User input produced seed ID %s" % make_chksum_8(seed)) + else: + msg("ERROR: No seed source found for seed ID: %s" % seed_id) + sys.exit(2) + + sid = make_chksum_8(seed) + saved_seeds[sid] = seed + + if sid == seed_id: return seed + + +def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds,opts): + + seed_ids = set([i[:8] for i in mmgen_addrs]) + vmsg("Need seed%s: %s" % (suf(seed_ids,"k")," ".join(seed_ids))) + d = [] + + from mmgen.addr import generate_addrs + for seed_id in seed_ids: + # Returns only if seed is found + seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts) + addr_nums = [int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id] +# num sec wif addr + d += [("{}:{}".format(seed_id,r.num),r.addr,r.wif) + for r in generate_addrs(seed,addr_nums,{'gen_what':"ka"},seed_id)] + return d + + +def sign_transaction(c,tx_hex,tx_num_str,sig_data,keys=None): + + if keys: + qmsg("Passing %s key%s to bitcoind" % (len(keys),suf(keys,"k"))) + if g.debug: print "Keys:\n %s" % "\n ".join(keys) + + msg_r("Signing transaction{}...".format(tx_num_str)) + from mmgen.rpc import exceptions + try: + sig_tx = c.signrawtransaction(tx_hex,sig_data,keys) + except exceptions.InvalidAddressOrKey: + msg("failed\nInvalid address or key") + sys.exit(3) + + return sig_tx + + +def sign_tx_with_bitcoind_wallet(c,tx_hex,tx_num_str,sig_data,keys,opts): + + try: + sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys) + except: + from mmgen.rpc import exceptions + msg("Using keys in wallet.dat as per user request") + prompt = "Enter passphrase for bitcoind wallet: " + while True: + passwd = get_bitcoind_passphrase(prompt,opts) + + try: + c.walletpassphrase(passwd, 9999) + except exceptions.WalletPassphraseIncorrect: + msg("Passphrase incorrect") + else: + msg("Passphrase OK"); break + + sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys) + + msg("Locking wallet") + try: + c.walletlock() + except: + msg("Failed to lock wallet") + + return sig_tx + + +def check_maps_from_seeds(maplist,label,infiles,saved_seeds,opts,return_keys=False): + + if not maplist: return [] + qmsg("Checking MMGen -> BTC address mappings for %ss (from seeds)" % label) + d = get_keys_for_mmgen_addrs(maplist.keys(),infiles,saved_seeds,opts) +# 0=mmaddr 1=addr 2=wif + m = dict([(e[0],e[1]) for e in d]) + for a,b in zip(sorted(m),sorted(maplist)): + if a != b: + al,bl = "generated seed:","tx file:" + msg(wmsg['mm2btc_mapping_error'] % (al,a,m[a],bl,b,maplist[b])) + sys.exit(3) + if return_keys: + ret = [e[2] for e in d] + vmsg("Added %s wif key%s from seeds" % (len(ret),suf(ret,"k"))) + return ret + +def missing_keys_errormsg(addrs): + print """ +A key file must be supplied (or use the '--use-wallet-dat' option) +for the following non-{} address{}:\n {}""".format( + g.proj_name,suf(addrs,"a"),"\n ".join(addrs)).strip() + + +def parse_mmgen_keyaddr_file(opts): + adata = {} + parse_keyaddr_file(opts['mmgen_keys_from_file'],adata) + for sid in adata.keys(): # one seed id, one loop + idxs = adata[sid] + count = len(idxs.keys()) + vmsg("Found %s wif key%s for seed ID %s" % (count,suf(count,"k"),sid)) + # idx: (0=addr, 1=comment 2=wif) -> mmaddr: (0=addr, 1=wif) + return dict([("{}:{}".format(sid,k),(idxs[k][0],idxs[k][2])) + for k in idxs.keys()]) + + +def parse_keylist(opts,from_file): + fn = opts['keys_from_file'] + d = get_data_from_file(fn,"non-%s keylist" % g.proj_name) + enc_ext = get_extension(fn) == g.mmenc_ext + if enc_ext or not is_utf8(d): + if not enc_ext: qmsg("Keylist file appears to be encrypted") + from crypto import mmgen_decrypt_retry + d = mmgen_decrypt_retry(d,"encrypted keylist") + # Check for duplication with key-address file + keys_all = set(remove_comments(d.split("\n"))) + d = from_file['mmdata'] + kawifs = [d[k][1] for k in d.keys()] + keys = [k for k in keys_all if k not in kawifs] + removed = len(keys_all) - len(keys) + if removed: vmsg(wmsg['removed_dups'] % (removed,suf(removed,"k"))) + addrs = [] + wif2addr_f = get_wif2addr_f() + for n,k in enumerate(keys,1): + qmsg_r("\rGenerating addresses from keylist: %s/%s" % (n,len(keys))) + addrs.append(wif2addr_f(k)) + qmsg("\rGenerated addresses from keylist: %s/%s " % (n,len(keys))) + + return dict(zip(addrs,keys)) + + +# Check inputs and outputs maps against key-address file, deleting entries: +def check_maps_from_kafile(imap,what,kadata,return_keys=False): + qmsg("Checking MMGen -> BTC address mappings for %ss (from key-address file)" % what) + ret = [] + for k in imap.keys(): + if k in kadata.keys(): + if kadata[k][0] == imap[k]: + del imap[k] + ret += [kadata[k][1]] + else: + kl,il = "key-address file:","tx file:" + msg(wmsg['mm2btc_mapping_error']%(kl,k,kadata[k][0],il,k,imap[k])) + sys.exit(2) + if ret: vmsg("Removed %s address%s from %ss map" % (len(ret),suf(ret,"a"),what)) + if return_keys: + vmsg("Added %s wif key%s from %ss map" % (len(ret),suf(ret,"k"),what)) + return ret + + +def get_keys_from_keylist(kldata,other_addrs): + ret = [] + for addr in other_addrs[:]: + if addr in kldata.keys(): + ret += [kldata[addr]] + other_addrs.remove(addr) + vmsg("Added %s wif key%s from user-supplied keylist" % + (len(ret),suf(ret,"k"))) + return ret + + opts,infiles = parse_opts(sys.argv,help_data) for l in ( -('tx_id', 'info'), -('keys_from_file','all_keys_from_file') +('tx_id', 'info') ): warn_incompatible_opts(opts,l) if 'from_incog_hex' in opts or 'from_incog_hidden' in opts: opts['from_incog'] = True -if 'all_keys_from_file' in opts: - opts['keys_from_file'] = opts['all_keys_from_file'] if not infiles: usage(help_data) for i in infiles: check_infile(i) @@ -108,24 +297,15 @@ c = connect_to_bitcoind() saved_seeds = {} tx_files = [i for i in set(infiles) if get_extension(i) == g.rawtx_ext] -addrfiles = [a for a in set(infiles) if get_extension(a) == g.addrfile_ext] -seed_files = list(set(infiles) - set(tx_files) - set(addrfiles)) +seed_files = list(set(infiles) - set(tx_files)) if not "info" in opts: do_license_msg(immed=True) +from_file = { 'mmdata':{}, 'kldata':{} } +if 'mmgen_keys_from_file' in opts: + from_file['mmdata'] = parse_mmgen_keyaddr_file(opts) or {} if 'keys_from_file' in opts: - fn = opts['keys_from_file'] - d = get_data_from_file(fn,"keylist") - if get_extension(fn) == g.mmenc_ext or not \ - is_btc_key(remove_comments(d.split("\n"))[0][:55]): - qmsg("Keylist appears to be encrypted") - from mmgen.crypto import mmgen_decrypt - while True: - d_dec = mmgen_decrypt(d,"encrypted keylist","",opts) - if d_dec: d = d_dec; break - msg("Trying again...") - keys_from_file = remove_comments(d.split("\n")) -else: keys_from_file = [] + from_file['kldata'] = parse_keylist(opts,from_file) or {} tx_num_str = "" for tx_num,tx_file in enumerate(tx_files,1): @@ -136,7 +316,7 @@ for tx_num,tx_file in enumerate(tx_files,1): m = "" if 'tx_id' in opts else "transaction data" tx_data = get_lines_from_file(tx_file,m) - metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_data(tx_data,tx_file) + metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,tx_file) qmsg("Successfully opened transaction file '%s'" % tx_file) if 'tx_id' in opts: @@ -147,56 +327,47 @@ for tx_num,tx_file in enumerate(tx_files,1): view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata) sys.exit(0) -# Are inputs mmgen addresses? - mmgen_inputs = [i for i in inputs_data if parse_mmgen_label(i['account'])[0]] - other_inputs = [i for i in inputs_data if not parse_mmgen_label(i['account'])[0]] - - if 'all_keys_from_file' in opts: other_inputs = inputs_data - - keys = keys_from_file - - if other_inputs and not keys and not 'use_wallet_dat' in opts: - missing_keys_errormsg(other_inputs) - sys.exit(2) - - if other_inputs and keys and not 'skip_key_preverify' in opts: - addrs = [i['address'] for i in other_inputs] - mm_inputs = mmgen_inputs if 'all_keys_from_file' in opts else [] - preverify_keys(addrs, keys, mm_inputs) - opts['skip_key_preverify'] = True - - if 'all_keys_from_file' in opts: - if addrfiles: - check_mmgen_to_btc_addr_mappings_addrfile(mmgen_inputs,b2m_map,addrfiles) - else: - confirm_or_exit(txmsg['skip_mapping_checks_warning'],"continue") - else: - check_mmgen_to_btc_addr_mappings( - mmgen_inputs,b2m_map,seed_files,saved_seeds,opts) - p = "View data for transaction{}? (y)es, (N)o, (v)iew in pager" reply = prompt_and_get_char(p.format(tx_num_str),"YyNnVv",enter_ok=True) if reply and reply in "YyVv": - view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata, - True if reply in "Vv" else False) + view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,reply in "Vv") + # Start + other_addrs = list(set([i['address'] for i in inputs_data + if not parse_mmgen_label(i['account'])[0]])) + + keys = get_keys_from_keylist(from_file['kldata'],other_addrs) + + if other_addrs and not 'use_wallet_dat' in opts: + missing_keys_errormsg(other_addrs) + sys.exit(2) + + imap = dict([(i['account'].split()[0],i['address']) for i in inputs_data + if parse_mmgen_label(i['account'])[0]]) + omap = dict([(j[0],i) for i,j in b2m_map.items()]) + sids = set([i[:8] for i in imap.keys()]) + + keys += check_maps_from_kafile(imap,"input",from_file['mmdata'],True) + check_maps_from_kafile(omap,"output",from_file['mmdata']) + + keys += check_maps_from_seeds(imap,"input",seed_files,saved_seeds,opts,True) + check_maps_from_seeds(omap,"output",seed_files,saved_seeds,opts) + + extra_sids = set(saved_seeds.keys()) - sids + if extra_sids: + msg("Unused seed ID%s: %s" % + (suf(extra_sids,"k")," ".join(extra_sids))) + + # Begin signing sig_data = [ {"txid":i['txid'],"vout":i['vout'],"scriptPubKey":i['scriptPubKey']} for i in inputs_data] - if mmgen_inputs and not 'all_keys_from_file' in opts: - ml = [i['account'].split()[0] for i in mmgen_inputs] - keys += get_keys_for_mmgen_addrs(ml,seed_files,saved_seeds,opts) - - if 'use_wallet_dat' in opts: - sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,tx_num_str,sig_data,keys,opts) - else: - sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys) - elif other_inputs: - if keys: - sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys) - else: - sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,tx_num_str,sig_data,keys,opts) + if 'use_wallet_dat' in opts: + sig_tx = sign_tx_with_bitcoind_wallet( + c,tx_hex,tx_num_str,sig_data,keys,opts) + else: + sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys) if sig_tx['complete']: msg("OK") diff --git a/mmgen/main_walletchk.py b/mmgen/main_walletchk.py index 7e5d2dfb..6118ea36 100755 --- a/mmgen/main_walletchk.py +++ b/mmgen/main_walletchk.py @@ -25,7 +25,7 @@ import sys import mmgen.config as g from mmgen.Opts import * from mmgen.util import * -from mmgen.crypto import get_seed_retry,wallet_to_incog_data +from mmgen.crypto import * help_data = { 'prog_name': g.prog_name, @@ -47,6 +47,7 @@ help_data = { -X, --export-incog-hex Export wallet to incognito hexadecimal format -G, --export-incog-hidden=f,o Hide incognito data in existing file 'f' at offset 'o' (comma-separated) +-o, --old-incog-fmt Use old (pre-0.7.8) incog format -m, --export-mnemonic Export the wallet's mnemonic to file -s, --export-seed Export the wallet's seed to file """.format(g=g), @@ -60,6 +61,54 @@ to disable this option, then specify '-r0' on the command line. """ } +def wallet_to_incog_data(infile,opts): + + d = get_data_from_wallet(infile,silent=True) + seed_id,key_id,preset,salt,enc_seed = \ + d[1][0], d[1][1], d[2].split(":")[0], d[3], d[4] + + while True: + passwd = get_mmgen_passphrase("{} wallet".format(g.proj_name),opts) + key = make_key(passwd, salt, preset, "main key") + seed = decrypt_seed(enc_seed, key, seed_id, key_id) + if seed: break + + iv = get_random(g.aesctr_iv_len,opts) + iv_id = make_iv_chksum(iv) + msg("Incog ID: %s" % iv_id) + + if not 'old_incog_fmt' in opts: + salt = get_random(g.salt_len,opts) + key = make_key(passwd, salt, preset, "incog wallet key") + key_id = make_chksum_8(key) + from hashlib import sha256 + chk = sha256(seed).digest()[:8] + enc_seed = encrypt_data(chk+seed, key, 1, "seed") + + # IV is used BOTH to initialize counter and to salt password! + key = make_key(passwd, iv, preset, "incog wrapper key") + m = "incog data" + wrap_enc = encrypt_data(salt + enc_seed, key, int(hexlify(iv),16), m) + + return iv+wrap_enc,seed_id,key_id,iv_id,preset + + +def export_to_hidden_incog(incog_enc,opts): + outfile,offset = opts['export_incog_hidden'].split(",") #Already sanity-checked + if 'outdir' in opts: outfile = make_full_path(opts['outdir'],outfile) + + check_data_fits_file_at_offset(outfile,int(offset),len(incog_enc),"write") + + if not g.quiet: confirm_or_exit("","alter file '%s'" % outfile) + import os + f = os.open(outfile,os.O_RDWR) + os.lseek(f, int(offset), os.SEEK_SET) + os.write(f, incog_enc) + os.close(f) + msg("Data written to file '%s' at offset %s" % + (os.path.relpath(outfile),offset)) + + opts,cmd_args = parse_opts(sys.argv,help_data) if 'export_incog_hidden' in opts or 'export_incog_hex' in opts: @@ -82,7 +131,8 @@ elif 'export_incog' in opts: if "export_incog_hidden" in opts: export_to_hidden_incog(incog_enc,opts) else: - seed_len = (len(incog_enc)-g.salt_len-g.aesctr_iv_len)*8 + z = 0 if 'old_incog_fmt' in opts else 8 + seed_len = (len(incog_enc)-g.salt_len-g.aesctr_iv_len-z)*8 fn = "%s-%s-%s[%s,%s].%s" % ( seed_id, key_id, iv_id, seed_len, preset, g.incog_hex_ext if "export_incog_hex" in opts else g.incog_ext @@ -102,8 +152,7 @@ else: if 'export_mnemonic' in opts: wl = get_default_wordlist() from mmgen.mnemonic import get_mnemonic_from_seed - p = True if g.debug else False - mn = get_mnemonic_from_seed(seed, wl, g.default_wl, print_info=p) + mn = get_mnemonic_from_seed(seed, wl, g.default_wl, g.debug) fn = "%s.%s" % (make_chksum_8(seed).upper(), g.mn_ext) write_to_file_or_stdout(fn, " ".join(mn)+"\n", opts, "mnemonic data") diff --git a/mmgen/main_walletgen.py b/mmgen/main_walletgen.py index f7dbb350..75dccbd0 100755 --- a/mmgen/main_walletgen.py +++ b/mmgen/main_walletgen.py @@ -55,6 +55,9 @@ help_data = { i.e. a "brainwallet", using seed length 'l' and hash preset 'p' (comma-separated) -g, --from-incog Generate wallet from an incognito-format wallet +-G, --from-incog-hidden= f,o,l Generate keys from incognito data in file + 'f' at offset 'o', with seed length of 'l' +-o, --old-incog-fmt Use old (pre-0.7.8) incog format -m, --from-mnemonic Generate wallet from an Electrum-like mnemonic -s, --from-seed Generate wallet from a seed in .{g.seed_ext} format """.format(seed_lens=",".join([str(i) for i in g.seed_lens]), g=g), @@ -94,6 +97,30 @@ in all future invocations with that passphrase. """.format(g=g) } +wmsg = { + 'choose_wallet_passphrase': """ +You must choose a passphrase to encrypt the wallet with. A key will be +generated from your passphrase using a hash preset of '%s'. Please note that +no strength checking of passphrases is performed. For an empty passphrase, +just hit ENTER twice. +""".strip(), + 'brain_warning': """ +############################## EXPERTS ONLY! ############################## + +A brainwallet will be secure only if you really know what you're doing and +have put much care into its creation. {} assumes no responsibility for +coins stolen as a result of a poorly crafted brainwallet passphrase. + +A key will be generated from your passphrase using the parameters requested +by you: seed length {}, hash preset '{}'. For brainwallets it's highly +recommended to use one of the higher-numbered presets + +Remember the seed length and hash preset parameters you've specified. To +generate the correct keys/addresses associated with this passphrase in the +future, you must continue using these same parameters +""", +} + opts,cmd_args = parse_opts(sys.argv,help_data) if 'show_hash_presets' in opts: show_hash_presets() @@ -121,16 +148,17 @@ else: usage(help_data) do_license_msg() if 'from_brain' in opts and not g.quiet: - confirm_or_exit(cmessages['brain_warning'].format( + confirm_or_exit(wmsg['brain_warning'].format( g.proj_name, *get_from_brain_opt_params(opts)), "continue") for i in 'from_mnemonic','from_brain','from_seed','from_incog': if infile or (i in opts): seed = get_seed_retry(infile,opts) - if "from_incog" in opts or get_extension(infile) == g.incog_ext: - qmsg(cmessages['incog'] % make_chksum_8(seed)) - else: qmsg("") +# if "from_incog" in opts or get_extension(infile) == g.incog_ext: +# qmsg(cmessages['incog'] % make_chksum_8(seed)) +# else: qmsg("") + qmsg("") break else: # Truncate random data for smaller seed lengths @@ -138,7 +166,7 @@ else: salt = sha256(get_random(128,opts)).digest()[:g.salt_len] -qmsg(cmessages['choose_wallet_passphrase'] % opts['hash_preset']) +qmsg(wmsg['choose_wallet_passphrase'] % opts['hash_preset']) passwd = get_new_passphrase("new {} wallet".format(g.proj_name), opts) diff --git a/mmgen/mnemonic.py b/mmgen/mnemonic.py index 7fad9a1f..54a97624 100755 --- a/mmgen/mnemonic.py +++ b/mmgen/mnemonic.py @@ -17,9 +17,13 @@ # along with this program. If not, see . """ -mnemonic.py: Mnemomic routines for the MMGen suite +mnemonic.py: Mnemonic routines for the MMGen suite """ +import sys +from mmgen.util import msg,msg_r,make_chksum_8 +import mmgen.config as g + wl_checksums = { "electrum": '5ca31424', "tirosh": '1a5faeff' @@ -27,70 +31,77 @@ wl_checksums = { # These are the only base-1626 specific configs: mn_base = 1626 -def mn_fill(mn): return len(mn) * 8 / 3 -def mn_len(hexnum): return len(hexnum) * 3 / 8 +def mn2hex_pad(mn): return len(mn) * 8 / 3 +def hex2mn_pad(hexnum): return len(hexnum) * 3 / 8 -import sys - -from mmgen.util import msg,make_chksum_8 -import mmgen.config as g - -# These universal base-conversion routines work for any base - -def baseNtohex(base,words,wordlist,fill=0): +# Universal base-conversion routines: +def baseNtohex(base,words,wordlist,pad=0): deconv = \ [wordlist.index(words[::-1][i])*(base**i) for i in range(len(words))] - return hex(sum(deconv))[2:].rstrip('L').zfill(fill) + return ("{:0%sx}"%pad).format(sum(deconv)) -def hextobaseN(base,hexnum,wordlist,mn_len): - num = int(hexnum,16) - return [wordlist[num / (base**i) % base] for i in range(mn_len)][::-1] +def hextobaseN(base,hexnum,wordlist,pad=0): + num,ret = int(hexnum,16),[] + while num: + ret.append(num % base) + num /= base + return [wordlist[n] for n in [0] * (pad-len(ret)) + ret[::-1]] -def get_seed_from_mnemonic(mn,wl): +def get_seed_from_mnemonic(mn,wl,silent=False,label=""): - if len(mn) not in g.mnemonic_lens: - msg("Bad mnemonic (%i words). Allowed numbers of words: %s" % - (len(mn),", ".join([str(i) for i in g.mnemonic_lens]))) - return False + if len(mn) not in g.mn_lens: + msg("Invalid mnemonic (%i words). Allowed numbers of words: %s" % + (len(mn),", ".join([str(i) for i in g.mn_lens]))) + sys.exit(3) for n,w in enumerate(mn,1): if w not in wl: - msg("Bad mnemonic: word number %s is not in the wordlist" % n) - return False + msg("Invalid mnemonic: word #%s is not in the wordlist" % n) + sys.exit(3) from binascii import unhexlify - seed = unhexlify(baseNtohex(mn_base,mn,wl,mn_fill(mn))) - msg("Valid mnemomic for seed ID %s" % make_chksum_8(seed)) + hseed = baseNtohex(mn_base,mn,wl,mn2hex_pad(mn)) - return seed + rev = hextobaseN(mn_base,hseed,wl,hex2mn_pad(hseed)) + if rev != mn: + msg("ERROR: mnemonic recomputed from seed not the same as original") + msg("Recomputed mnemonic:\n%s" % " ".join(rev)) + sys.exit(3) + + if not silent: + msg("Valid mnemonic for seed ID %s" % make_chksum_8(unhexlify(hseed))) + + return unhexlify(hseed) -def get_mnemonic_from_seed(seed, wl, label, print_info=False): +def get_mnemonic_from_seed(seed, wl, label="", verbose=False): + + if len(seed)*8 not in g.seed_lens: + msg("%s: invalid seed length" % len(seed)) + sys.exit(3) from binascii import hexlify - if print_info: - msg("Wordlist: %s" % label.capitalize()) - msg("Seed length: %s bits" % (len(seed) * 8)) - msg("Seed: %s" % hexlify(seed)) - hseed = hexlify(seed) - mn = hextobaseN(mn_base,hseed,wl,mn_len(hseed)) + mn = hextobaseN(mn_base,hseed,wl,hex2mn_pad(hseed)) - if print_info: + if verbose: + msg("Wordlist: %s" % label.capitalize()) + msg("Seed length: %s bits" % (len(seed) * 8)) + msg("Seed: %s" % hseed) msg("mnemonic (%s words):\n%s" % (len(mn), " ".join(mn))) - if int(baseNtohex(mn_base,mn,wl,mn_fill(mn)),16) != int(hexlify(seed),16): + rev = baseNtohex(mn_base,mn,wl,mn2hex_pad(mn)) + if rev != hseed: msg("ERROR: seed recomputed from wordlist not the same as original seed!") - msg("Recomputed seed %s" % baseNtohex(mn_base,mn,wl,mn_fill(mn))) + msg("Original seed: %s" % hseed) + msg("Recomputed seed: %s" % rev) sys.exit(3) return mn -def check_wordlist(wl_str,label): - - wl = wl_str.strip().split("\n") +def check_wordlist(wl,label): print "Wordlist: %s" % label.capitalize() diff --git a/mmgen/tests/bitcoin.py b/mmgen/tests/bitcoin.py index 55036a9d..f91e304c 100755 --- a/mmgen/tests/bitcoin.py +++ b/mmgen/tests/bitcoin.py @@ -22,26 +22,14 @@ bitcoin.py: Test suite for mmgen.bitcoin module import mmgen.bitcoin as b from mmgen.util import msg from mmgen.tests.test import * -from binascii import hexlify, unhexlify - +from binascii import hexlify import sys -def b58_randenc(): - r = get_random(24) - r_enc = b.b58encode(r) - print "Data (hex): %s" % hexlify(r) - print "Base 58: %s" % r_enc - r_dec = b.b58decode(r_enc) - print "Decoded data: %s" % hexlify(r_dec) - if r_dec != r: - print "ERROR! Decoded data doesn't match original" - sys.exit(9) - def keyconv_compare_randloop(loops, quiet=False): for i in range(1,int(loops)+1): - wif = numtowif_rand(quiet=True) + wif = _numtowif_rand(quiet=True) if not quiet: sys.stderr.write("-- %s --\n" % i) ret = keyconv_compare(wif,quiet) @@ -55,7 +43,6 @@ def keyconv_compare_randloop(loops, quiet=False): else: print "%s iterations completed" % i - def keyconv_compare(wif,quiet=False): do_msg = nomsg if quiet else msg do_msg("WIF: %s" % wif) @@ -81,135 +68,24 @@ def keyconv_compare(wif,quiet=False): def _do_hextowif(hex_in,quiet=False): do_msg = nomsg if quiet else msg do_msg("Input: %s" % hex_in) - wif = numtowif(int(hex_in,16)) + wif = b.numtowif(int(hex_in,16)) do_msg("WIF encoded: %s" % wif) - wif_dec = wiftohex(wif) + wif_dec = b.wiftohex(wif) do_msg("WIF decoded: %s" % wif_dec) if hex_in != wif_dec: print "ERROR! Decoded data doesn't match original data" sys.exit(9) return wif - -def hextowiftopubkey(hex_in,quiet=False): - if len(hex_in) != 64: - print "Input must be a hex number 64 bits in length (%s input)" \ - % len(hex_in) - sys.exit(2) - - wif = _do_hextowif(hex_in,quiet=quiet) - - keyconv_compare(wif) - - -def numtowif_rand(quiet=False): +def _numtowif_rand(quiet=False): r_hex = hexlify(get_random(32)) return _do_hextowif(r_hex,quiet) -def strtob58(s,quiet=False): - print "Input: %s" % s - s_enc = b.b58encode(s) - print "Encoded data: %s" % s_enc - s_dec = b.b58decode(s_enc) - print "Decoded data: %s" % s_dec - test_equality(s,s_dec,[""],quiet) - -def hextob58(s_in,f_enc=b.b58encode, f_dec=b.b58decode, quiet=False): - do_msg = nomsg if quiet else msg - do_msg("Input: %s" % s_in) - s_bin = unhexlify(s_in) - s_enc = f_enc(s_bin) - do_msg("Encoded data: %s" % s_enc) - s_dec = hexlify(f_dec(s_enc)) - do_msg("Recoded data: %s" % s_dec) - test_equality(s_in,s_dec,["0"],quiet) - -def b58tohex(s_in,f_dec=b.b58decode, f_enc=b.b58encode,quiet=False): - print "Input: %s" % s_in - s_dec = f_dec(s_in) - print "Decoded data: %s" % hexlify(s_dec) - s_enc = f_enc(s_dec) - print "Recoded data: %s" % s_enc - test_equality(s_in,s_enc,["1"],quiet) - -def hextob58_pad(s_in, quiet=False): - hextob58(s_in,f_enc=b.b58encode_pad, f_dec=b.b58decode_pad, quiet=quiet) - -def b58tohex_pad(s_in, quiet=False): - b58tohex(s_in,f_dec=b.b58decode_pad, f_enc=b.b58encode_pad, quiet=quiet) - -def hextob58_pad_randloop(loops, quiet=False): - for i in range(1,int(loops)+1): - r = hexlify(get_random(32)) - hextob58(r,f_enc=b.b58encode_pad, f_dec=b.b58decode_pad, quiet=quiet) - if not quiet: print - if not i % 100 and quiet: - sys.stderr.write("\riteration: %i " % i) - - sys.stderr.write("\r%s iterations completed\n" % i) - -def test_wiftohex(s_in,f_dec=b.wiftohex,f_enc=b.numtowif): - print "Input: %s" % s_in - s_dec = f_dec(s_in) - print "Decoded data: %s" % s_dec - s_enc = f_enc(int(s_dec,16)) - print "Recoded data: %s" % s_enc - -def hextosha256(s_in): - print "Entered data: %s" % s_in - s_enc = sha256(unhexlify(s_in)).hexdigest() - print "Encoded data: %s" % s_enc - -def pubhextoaddr(s_in): - print "Entered data: %s" % s_in - s_enc = b.pubhex2addr(s_in) - print "Encoded data: %s" % s_enc - -def hextowif_comp(s_in): - print "Entered data: %s" % s_in - s_enc = b.hextowif(s_in,compressed=True) - print "Encoded data: %s" % s_enc - s_dec = b.wiftohex(s_enc,compressed=True) - print "Decoded data: %s" % s_dec - -def wiftohex_comp(s_in): - print "Entered data: %s" % s_in - s_enc = b.wiftohex(s_in,compressed=True) - print "Encoded data: %s" % s_enc - s_dec = b.hextowif(s_enc,compressed=True) - print "Decoded data: %s" % s_dec - -def privhextoaddr_comp(hexpriv): - print b.privnum2addr(int(hexpriv,16),compressed=True) - -def wiftoaddr_comp(s_in): - print "Entered data: %s" % s_in - s_enc = b.wiftohex(s_in,compressed=True) - print "Encoded data: %s" % s_enc - s_enc = b.privnum2addr(int(s_enc,16),compressed=True) - print "Encoded data: %s" % s_enc - tests = { "keyconv_compare": ['wif [str]','quiet [bool=False]'], "keyconv_compare_randloop": ['iterations [int]','quiet [bool=False]'], - "b58_randenc": ['quiet [bool=False]'], - "strtob58": ['string [str]','quiet [bool=False]'], - "hextob58": ['hexnum [str]','quiet [bool=False]'], - "b58tohex": ['b58num [str]','quiet [bool=False]'], - "hextob58_pad": ['hexnum [str]','quiet [bool=False]'], - "b58tohex_pad": ['b58num [str]','quiet [bool=False]'], - "hextob58_pad_randloop": ['iterations [int]','quiet [bool=False]'], - "test_wiftohex": ['wif [str]', 'quiet [bool=False]'], - "numtowif_rand": ['quiet [bool=False]'], - "hextosha256": ['hexnum [str]','quiet [bool=False]'], - "hextowiftopubkey": ['hexnum [str]','quiet [bool=False]'], - "pubhextoaddr": ['hexnum [str]','quiet [bool=False]'], - "hextowif_comp": ['hexnum [str]'], - "wiftohex_comp": ['wif [str]'], - "privhextoaddr_comp": ['hexnum [str]'], - "wiftoaddr_comp": ['wif [str]'], } args = process_test_args(sys.argv, tests) diff --git a/mmgen/tool.py b/mmgen/tool.py index 87dcf789..450ba7e7 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -39,9 +39,9 @@ def Vmsg_r(s): opts = {} commands = { "strtob58": [' [str]'], + "b58tostr": [' [str]'], "hextob58": [' [str]'], "b58tohex": [' [str]'], - "b58tostr": [' [str]'], "b58randenc": [], "randhex": ['nbytes [int=32]'], "randwif": ['compressed [bool=False]'], @@ -51,6 +51,8 @@ commands = { "hex2wif": [' [str]', 'compressed [bool=False]'], "hexdump": [' [str]', 'cols [int=8]', 'line_nums [bool=True]'], "unhexdump": [' [str]'], + "hex2mn": [' [str]','wordlist [str="electrum"]'], + "mn2hex": [' [str]', 'wordlist [str="electrum"]'], "mn_rand128": ['wordlist [str="electrum"]'], "mn_rand192": ['wordlist [str="electrum"]'], "mn_rand256": ['wordlist [str="electrum"]'], @@ -59,10 +61,11 @@ commands = { "id8": [' [str]'], "id6": [' [str]'], "str2id6": [' [str]'], - "listaddresses":['minconf [int=1]', 'showempty [bool=False]'], + "listaddresses":['minconf [int=1]','showempty [bool=False]','pager [bool=False]'], "getbalance": ['minconf [int=1]'], "viewtx": [' [str]','pager [bool=False]'], - "check_addrfile": [' [str]'], + "addrfile_chksum": [' [str]'], + "keyaddrfile_chksum": [' [str]'], "find_incog_data": [' [str]',' [str]','keep_searching [bool=False]'], "hexreverse": [' [str]'], "sha256x2": [' [str]', @@ -122,8 +125,9 @@ command_help = """ * The encrypted file is indistinguishable from random data {pnm}-specific operations: - check_addrfile - compute checksum and address list for {pnm} address file - find_incog_data - Use an Incog ID to find hidden incognito wallet data + addrfile_chksum - compute checksum for {pnm} address file + keyaddrfile_chksum - compute checksum for {pnm} key file + find_incog_data - Use an Incog ID to find hidden incognito wallet data id6 - generate 6-character {pnm} ID for a file (or stdin) id8 - generate 8-character {pnm} ID for a file (or stdin) str2id6 - generate 6-character {pnm} ID for a string, ignoring spaces @@ -135,6 +139,8 @@ command_help = """ mn_rand256 - generate random 256-bit mnemonic mn_stats - show stats for mnemonic wordlist mn_printlist - print mnemonic wordlist + hex2mn - convert a 16, 24 or 32-byte number in hex format to a mnemonic + mn2hex - convert a 12, 18 or 24-word mnemonic to a number in hex format IMPORTANT NOTE: Though {pnm} mnemonics use the Electrum wordlist, they're computed using a different algorithm and are NOT Electrum-compatible! @@ -286,37 +292,45 @@ def get_wordlist(wordlist): Msg('"%s": invalid wordlist. Valid choices: %s' % (wordlist,'"'+'" "'.join(wordlists)+'"')) sys.exit(1) - return el if wordlist == "electrum" else tl + return (el if wordlist == "electrum" else tl).strip().split("\n") def do_random_mn(nbytes,wordlist): r = get_random(nbytes,opts) - wlists = wordlists if wordlist == "all" else [wordlist] - for wl in wlists: - l = get_wordlist(wl) - if wl == wlists[0]: Vmsg("Seed: %s" % ba.hexlify(r)) - mn = get_mnemonic_from_seed(r,l.strip().split("\n"), - wordlist,print_info=False) - Vmsg("%s wordlist mnemonic:" % (wl.capitalize())) + Vmsg("Seed: %s" % ba.hexlify(r)) + for wlname in (wordlists if wordlist == "all" else [wordlist]): + wl = get_wordlist(wlname) + mn = get_mnemonic_from_seed(r,wl,wordlist) + Vmsg("%s wordlist mnemonic:" % (wlname.capitalize())) print " ".join(mn) def mn_rand128(wordlist="electrum"): do_random_mn(16,wordlist) def mn_rand192(wordlist="electrum"): do_random_mn(24,wordlist) def mn_rand256(wordlist="electrum"): do_random_mn(32,wordlist) +def hex2mn(s,wordlist="electrum"): + import mmgen.mnemonic + wl = get_wordlist(wordlist) + print " ".join(get_mnemonic_from_seed(ba.unhexlify(s), wl, wordlist)) + +def mn2hex(s,wordlist="electrum"): + import mmgen.mnemonic + wl = get_wordlist(wordlist) + print ba.hexlify(get_seed_from_mnemonic(s.split(),wl,True)) + def mn_stats(wordlist="electrum"): l = get_wordlist(wordlist) check_wordlist(l,wordlist) def mn_printlist(wordlist="electrum"): - l = get_wordlist(wordlist) - print "%s" % l.strip() + wl = get_wordlist(wordlist) + print "\n".join(wl) def id8(infile): print make_chksum_8(get_data_from_file(infile,dash=True)) def id6(infile): print make_chksum_6(get_data_from_file(infile,dash=True)) def str2id6(s): print make_chksum_6("".join(s.split())) # List MMGen addresses and their balances: -def listaddresses(minconf=1,showempty=False): +def listaddresses(minconf=1,showempty=False,pager=False): from mmgen.tx import connect_to_bitcoind,trim_exponent,is_mmgen_addr c = connect_to_bitcoind() @@ -349,7 +363,7 @@ def listaddresses(minconf=1,showempty=False): max([len(k) for k in addrs.keys()]), max([len(str(addrs[k][1])) for k in addrs.keys()]) ) - print fs % ("ADDRESS","COMMENT","BALANCE") + out = [ fs % ("ADDRESS","COMMENT","BALANCE") ] def s_mmgen(ma): return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *ma.split("_")) @@ -357,9 +371,13 @@ def listaddresses(minconf=1,showempty=False): old_sid = "" for k in sorted(addrs.keys(),key=s_mmgen): sid,num = k.split("_") - if old_sid and old_sid != sid: print + if old_sid and old_sid != sid: out.append("") old_sid = sid - print fs % (sid+":"+num, addrs[k][1], trim_exponent(addrs[k][0])) + out.append(fs % (sid+":"+num, addrs[k][1], trim_exponent(addrs[k][0]))) + + o = "\n".join(out) + if pager: do_pager(o) + else: print o def getbalance(minconf=1): @@ -390,10 +408,11 @@ def viewtx(infile,pager=False): c = connect_to_bitcoind() tx_data = get_lines_from_file(infile,"transaction data") - metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_data(tx_data,infile) + metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,infile) view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager) -def check_addrfile(infile): parse_addrs_file(infile) +def addrfile_chksum(infile): parse_addrfile(infile,{}) +def keyaddrfile_chksum(infile): parse_keyaddr_file(infile,{}) def hexreverse(hex_str): print ba.hexlify(decode_pretty_hexdump(hex_str)[::-1]) @@ -418,7 +437,7 @@ def pubkey2hexaddr(pubkeyhex): print bitcoin.pubhex2hexaddr(pubkeyhex) def pubkey2addr(pubkeyhex): - print bitcoin.pubhex2addr(pubkeyhex) + print bitcoin.hexaddr2addr(bitcoin.pubhex2hexaddr(pubkeyhex)) def privhex2addr(privkeyhex,compressed=False): print bitcoin.privnum2addr(int(privkeyhex,16),compressed) @@ -444,7 +463,7 @@ def encrypt(infile,outfile="",hash_preset=''): def decrypt(infile,outfile="",hash_preset=''): enc_d = get_data_from_file(infile,"encrypted data") while True: - dec_d = mmgen_decrypt(enc_d,"user data","",opts) + dec_d = mmgen_decrypt(enc_d,"user data") if dec_d: break msg("Trying again...") if outfile == '-': @@ -463,6 +482,10 @@ def find_incog_data(filename,iv_id,keep_searching=False): ivsize,bsize,mod = g.aesctr_iv_len,4096,4096*8 n,carry = 0," "*ivsize f = os.open(filename,os.O_RDONLY) + for ch in iv_id: + if ch not in "0123456789ABCDEF": + msg("'%s': invalid Incog ID" % iv_id) + sys.exit(2) while True: d = os.read(f,bsize) if not d: break diff --git a/mmgen/tx.py b/mmgen/tx.py index 9006015b..d98531b0 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -26,82 +26,7 @@ from decimal import Decimal import mmgen.config as g from mmgen.util import * -from mmgen.crypto import get_seed_retry -from mmgen.term import do_pager,get_char - -txmsg = { -'not_enough_btc': "Not enough BTC in the inputs for this transaction (%s BTC)", -'throwaway_change': """ -ERROR: This transaction produces change (%s BTC); however, no change address -was specified. -""".strip(), -'mixed_inputs': """ -NOTE: This transaction uses a mixture of both mmgen and non-mmgen inputs, which -makes the signing process more complicated. When signing the transaction, keys -for the non-mmgen inputs must be supplied in a separate file using either the -'-k' or '-K' option to '{}-txsign'. - -Selected mmgen inputs: %s""".format(g.proj_name.lower()), -'too_many_acct_addresses': """ -ERROR: More than one address found for account: "%s". -The tracking "wallet.dat" file appears to have been altered by a non-{g.proj_name} -program. Please restore "wallet.dat" from a backup or create a new wallet -and re-import your addresses.""".strip().format(g=g), - 'addrfile_no_data_msg': """ -No data found for MMgen address '%s'. Please import this address into -your tracking wallet, or supply an address file for it on the command line. -""".strip(), - 'addrfile_warn_msg': """ -Warning: output address '{mmaddr}' is not in the tracking wallet, which means -its balance will not be tracked. You're strongly advised to import the address -into your tracking wallet before broadcasting this transaction. -""".strip(), - 'addrfile_fail_msg': """ -No data for MMgen address '{mmaddr}' could be found in either the tracking -wallet or the supplied address file. Please import this address into your -tracking wallet, or supply an address file for it on the command line. -""".strip(), - 'no_spendable_outputs': """ -No spendable outputs found! Import addresses with balances into your -watch-only wallet using '{}-addrimport' and then re-run this program. -""".strip().format(g.proj_name.lower()), - 'mapping_error': """ -MMGen -> BTC address mappings differ! -In transaction: %s -Generated from seed: %s -""".strip(), - 'skip_mapping_checks_warning': """ -You've chosen the '--all-keys-from-file' option. Since all signing keys will -be taken from this file, no {pnm} seed source will be consulted and {pnm}-to- -BTC mapping checks cannot not be performed. Were an attacker to compromise -your tracking wallet or raw transaction file, he could thus cause you to spend -coin to an unintended address. For greater security, supply a trusted {pnm} -address file for your output addresses on the command line. -""".strip().format(pnm=g.proj_name), - 'missing_mappings': """ -No information was found in the supplied address files for the following {pnm} -addresses: %s -The {pnm}-to-BTC mappings for these addresses cannot be verified! -""".strip().format(pnm=g.proj_name), -} - - -def connect_to_bitcoind(): - - host,port,user,passwd = "localhost",8332,"rpcuser","rpcpassword" - cfg = get_bitcoind_cfg_options((user,passwd)) - - import mmgen.rpc.connection - f = mmgen.rpc.connection.BitcoinConnection - - try: - c = f(cfg[user],cfg[passwd],host,port) - except: - msg("Unable to establish RPC connection with bitcoind") - sys.exit(2) - - return c - +from mmgen.term import do_pager def trim_exponent(n): '''Remove exponent and trailing zeros. @@ -109,8 +34,6 @@ def trim_exponent(n): d = Decimal(n) return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize() - - def is_btc_amt(amt): # amt must be a string! @@ -134,197 +57,81 @@ def is_btc_amt(amt): return trim_exponent(ret) - def normalize_btc_amt(amt): # amt must be a string! ret = is_btc_amt(amt) if ret: return ret else: sys.exit(3) - -def get_bitcoind_cfg_options(cfg_keys): - - if "HOME" in os.environ: # Linux - homedir,datadir = os.environ["HOME"],".bitcoin" - elif "HOMEPATH" in os.environ: # Windows: - homedir,data_dir = os.environ["HOMEPATH"],r"Application Data\Bitcoin" - else: - msg("Neither $HOME nor %HOMEPATH% are set") - msg("Don't know where to look for 'bitcoin.conf'") - sys.exit(3) - - cfg_file = os.sep.join((homedir, datadir, "bitcoin.conf")) - - cfg = dict([(k,v) for k,v in [split2(line.translate(None,"\t "),"=") - for line in get_lines_from_file(cfg_file)] if k in cfg_keys]) - - for k in set(cfg_keys) - set(cfg.keys()): - msg("Configuration option '%s' must be set in %s" % (k,cfg_file)) - sys.exit(2) - - return cfg - - -def format_unspent_outputs_for_printing(out,sort_info,total): - - pfs = " %-4s %-67s %-34s %-12s %-13s %-8s %-10s %s" - pout = [pfs % ("Num","TX id,Vout","Address","MMgen ID", - "Amount (BTC)","Conf.","Age (days)", "Comment")] - - for n,i in enumerate(out): - addr = "=" if i.skip == "addr" and "grouped" in sort_info else i.address - tx = " " * 63 + "=" \ - if i.skip == "txid" and "grouped" in sort_info else str(i.txid) - - s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr, - i.mmid,i.amt,i.confirmations,i.days,i.label) - pout.append(s.rstrip()) - - return \ -"Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n".format( - make_timestr(), " ".join(sort_info), "\n".join(pout), total - ) - - -def sort_and_view(unspent,opts): - - def s_amt(i): return i.amount - def s_txid(i): return "%s %03s" % (i.txid,i.vout) - def s_addr(i): return i.address - def s_age(i): return i.confirmations - def s_mmgen(i): - m = parse_mmgen_label(i.account)[0] - if m: return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *m.split(":")) - else: return "G" + i.account - - sort,group,show_days,show_mmaddr,reverse = "age",False,False,True,True - unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default - - total = trim_exponent(sum([i.amount for i in unspent])) - max_acct_len = max([len(i.account) for i in unspent]) - - hdr_fmt = "UNSPENT OUTPUTS (sort order: %s) Total BTC: %s" - options_msg = """ -Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr -Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen -""".strip() - prompt = \ -"('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): " - - from copy import deepcopy - from mmgen.term import get_terminal_size - - write_to_file_msg = "" - msg("") - - while True: - cols = get_terminal_size()[0] - if cols < g.min_screen_width: - msg("%s-txcreate requires a screen at least %s characters wide" % - (g.proj_name.lower(),g.min_screen_width)) - sys.exit(2) - - addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46) - acct_w = min(max_acct_len, max(24,int(addr_w-10))) - btaddr_w = addr_w - acct_w - 1 - tx_w = max(11,min(64, cols-addr_w-32)) - txdots = "..." if tx_w < 64 else "" - fs = " %-4s %-" + str(tx_w) + "s %-2s %-" + str(addr_w) + "s %-13s %-s" - table_hdr = fs % ("Num","TX id Vout","","Address","Amount (BTC)", - "Age(d)" if show_days else "Conf.") - - unsp = deepcopy(unspent) - for i in unsp: i.skip = "" - if group and (sort == "address" or sort == "txid"): - for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]: - if sort == "address" and a.address == b.address: b.skip = "addr" - elif sort == "txid" and a.txid == b.txid: b.skip = "txid" - - for i in unsp: - amt = str(trim_exponent(i.amount)) - lfill = 3 - len(amt.split(".")[0]) if "." in amt else 3 - len(amt) - i.amt = " "*lfill + amt - i.days = int(i.confirmations * g.mins_per_block / (60*24)) - i.age = i.days if show_days else i.confirmations - i.mmid,i.label = parse_mmgen_label(i.account) - - if i.skip == "addr": - i.addr = "|" + "." * 33 - else: - if show_mmaddr: - dots = ".." if btaddr_w < len(i.address) else "" - i.addr = "%s%s %s" % ( - i.address[:btaddr_w-len(dots)], - dots, - i.account[:acct_w]) - else: - i.addr = i.address - - i.tx = " " * (tx_w-4) + "|..." if i.skip == "txid" \ - else i.txid[:tx_w-len(txdots)]+txdots - - sort_info = ["reverse"] if reverse else [] - sort_info.append(sort if sort else "unsorted") - if group and (sort == "address" or sort == "txid"): - sort_info.append("grouped") - - out = [hdr_fmt % (" ".join(sort_info), total), table_hdr] - out += [fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,i.age) - for n,i in enumerate(unsp)] - - msg("\n".join(out) +"\n\n" + write_to_file_msg + options_msg) - write_to_file_msg = "" - - skip_prompt = False - - while True: - reply = get_char(prompt, immed_chars="atDdAMrgmeqpvw") - - if reply == 'a': unspent.sort(key=s_amt); sort = "amount" - elif reply == 't': unspent.sort(key=s_txid); sort = "txid" - elif reply == 'D': show_days = False if show_days else True - elif reply == 'd': unspent.sort(key=s_addr); sort = "address" - elif reply == 'A': unspent.sort(key=s_age); sort = "age" - elif reply == 'M': - unspent.sort(key=s_mmgen); sort = "mmgen" - show_mmaddr = True - elif reply == 'r': - unspent.reverse() - reverse = False if reverse else True - elif reply == 'g': group = False if group else True - elif reply == 'm': show_mmaddr = False if show_mmaddr else True - elif reply == 'e': pass - elif reply == 'q': pass - elif reply == 'p': - d = format_unspent_outputs_for_printing(unsp,sort_info,total) - of = "listunspent[%s].out" % ",".join(sort_info) - write_to_file(of, d, opts,"",False,False) - write_to_file_msg = "Data written to '%s'\n\n" % of - elif reply == 'v': - do_pager("\n".join(out)) - continue - elif reply == 'w': - data = format_unspent_outputs_for_printing(unsp,sort_info,total) - do_pager(data) - continue - else: - msg("\nInvalid input") - continue - - break - - msg("\n") - if reply == 'q': break - - return tuple(unspent) - - def parse_mmgen_label(s,check_label_len=False): l = split2(s) if not is_mmgen_addr(l[0]): return "",s if check_label_len: check_addr_label(l[1]) return tuple(l) +def is_mmgen_seed_id(s): + import re + return re.match(r"^[0123456789ABCDEF]{8}$",s) is not None + +def is_mmgen_idx(s): + import re + m = g.mmgen_idx_max_digits + return re.match(r"^[0123456789]{1,"+str(m)+r"}$",s) is not None + +def is_mmgen_addr(s): + seed_id,idx = split2(s,":") + return is_mmgen_seed_id(seed_id) and is_mmgen_idx(idx) + +def is_btc_addr(s): + from mmgen.bitcoin import verify_addr + return verify_addr(s) + +def is_b58_str(s): + from mmgen.bitcoin import b58a + for ch in s: + if ch not in b58a: return False + return True + +def is_btc_key(s): + if s == "": return False + compressed = not s[0] == '5' + from mmgen.bitcoin import wiftohex + return wiftohex(s,compressed) is not False + +def wiftoaddr(s): + if s == "": return False + compressed = not s[0] == '5' + from mmgen.bitcoin import wiftohex,privnum2addr + hex_key = wiftohex(s,compressed) + if not hex_key: return False + return privnum2addr(int(hex_key,16),compressed) + +def is_valid_tx_comment(s, verbose=True): + if len(s) > g.max_tx_comment_len: + if verbose: msg("Invalid transaction comment (longer than %s characters)" % + g.max_tx_comment_len) + return False + try: s.decode("utf8") + except: + if verbose: msg("Invalid transaction comment (not UTF-8)") + return False + else: return True + +def check_addr_label(label): + + if len(label) > g.max_addr_label_len: + msg("'%s': overlong label (length must be <=%s)" % + (label,g.max_addr_label_len)) + sys.exit(3) + + for ch in label: + if ch not in g.addr_label_symbols: + msg(""" +"%s": illegal character in label "%s". +Only ASCII printable characters are permitted. +""".strip() % (ch,label)) + sys.exit(3) + def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False): @@ -343,7 +150,7 @@ def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False): total_in += j['amount'] addr = j['address'] mmid,label = parse_mmgen_label(j['account']) \ - if 'account' in j else ("","") + if 'account' in j else ("","") mmid_str = ((34-len(addr))*" " + " (%s)" % mmid) if mmid else "" for d in ( @@ -378,10 +185,13 @@ def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False): o = out.encode("utf8") if pager: do_pager(o) - else: print "\n"+o + else: + print "\n"+o + get_char("Press any key to continue: ") + msg("") -def parse_tx_data(tx_data,infile): +def parse_tx_file(tx_data,infile): err_str,err_fmt = "","Invalid %s in transaction file" @@ -424,164 +234,34 @@ def parse_tx_data(tx_data,infile): return metadata.split(),tx_hex,inputs_data,outputs_data,comment -def select_outputs(unspent,prompt): - - while True: - reply = my_raw_input(prompt).strip() - - if not reply: continue - - from mmgen.util import parse_address_list - selected = parse_address_list(reply,sep=None) - - if not selected: continue - - if selected[-1] > len(unspent): - msg("Inputs must be less than %s" % len(unspent)) - continue - - return selected - -def is_mmgen_seed_id(s): - import re - return True if re.match(r"^[0123456789ABCDEF]{8}$",s) else False - -def is_mmgen_idx(s): - import re - m = g.mmgen_idx_max_digits - return True if re.match(r"^[0123456789]{1,"+str(m)+r"}$",s) else False - -def is_mmgen_addr(s): - seed_id,idx = split2(s,":") - return is_mmgen_seed_id(seed_id) and is_mmgen_idx(idx) - -def is_btc_addr(s): - from mmgen.bitcoin import verify_addr - return verify_addr(s) - -def is_b58_str(s): - from mmgen.bitcoin import b58a - for ch in s: - if ch not in b58a: return False - return True - -def is_btc_key(s): - if s == "": return False - compressed = False if s[0] == '5' else True - from mmgen.bitcoin import wiftohex - return True if wiftohex(s,compressed) else False - -def mmaddr2btcaddr_bitcoind(c,mmaddr,acct_data): - - # Don't want to create a new object, so use append() - if not acct_data: - for i in c.listaccounts(minconf=0,includeWatchonly=True): - acct_data.append(i) - - for acct in acct_data: - m,comment = parse_mmgen_label(acct) - if m == mmaddr: - addrlist = c.getaddressesbyaccount(acct) - if len(addrlist) == 1: - return addrlist[0],comment - else: - msg(txmsg['too_many_acct_addresses'] % acct); sys.exit(2) - - return "","" +def get_wif2addr_f(): + if g.no_keyconv: return wiftoaddr + from mmgen.addr import test_for_keyconv + return wiftoaddr_keyconv if test_for_keyconv() else wiftoaddr -def mmaddr2btcaddr_addrfile(mmaddr,addr_data,silent=False): - - mmid,mmidx = mmaddr.split(":") - - for ad in addr_data: - if mmid == ad[0]: - for j in ad[1]: - if j[0] == mmidx: - if not silent: - msg(txmsg['addrfile_warn_msg'].format(mmaddr=mmaddr)) - if not keypress_confirm("Continue anyway?"): - sys.exit(1) - return j[1:] if len(j) == 3 else (j[1],"") - - if silent: return "","" - else: - msg(txmsg['addrfile_fail_msg'].format(mmaddr=mmaddr)) - sys.exit(2) - - -def check_mmgen_to_btc_addr_mappings(mmgen_inputs,b2m_map,infiles,saved_seeds,opts): - in_maplist = [(i['account'].split()[0],i['address']) for i in mmgen_inputs] - out_maplist = [(i[1][0],i[0]) for i in b2m_map.items()] - - for maplist,label in (in_maplist,"inputs"), (out_maplist,"outputs"): - if not maplist: continue - qmsg("Checking MMGen -> BTC address mappings for %s" % label) - pairs = get_keys_for_mmgen_addrs([i[0] for i in maplist], - infiles,saved_seeds,opts,gen_pairs=True) - for a,b in zip(sorted(pairs),sorted(maplist)): - if a != b: - msg(txmsg['mapping_error'] % (" ".join(a)," ".join(b))) - sys.exit(3) - - qmsg("Address mappings OK") - - -def check_addr_label(label): - - if len(label) > g.max_addr_label_len: - msg("'%s': overlong label (length must be <=%s)" % - (label,g.max_addr_label_len)) - sys.exit(3) - - for ch in label: - if ch not in g.addr_label_symbols: - msg(""" -"%s": illegal character in label "%s". -Only ASCII printable characters are permitted. -""".strip() % (ch,label)) - sys.exit(3) - -def make_addr_data_chksum(addr_data): +def make_addr_data_chksum(adata,keys=False): nchars = 24 - return make_chksum_N( - " ".join(["{} {}".format(*d[:2]) for d in addr_data]), nchars, sep=True - ) + return make_chksum_N(" ".join([" ".join( + [str(n),d[0],d[2]] if keys else [str(n),d[0]] + ) for n,d in adata]), nchars, sep=True) -def check_addr_data_hash(seed_id,addr_data): + +def get_addr_data_hash(e,keys=False): def s_addrdata(a): return int(a[0]) - addr_data_chksum = make_addr_data_chksum(sorted(addr_data,key=s_addrdata)) - from mmgen.addr import fmt_addr_idxs - fl = fmt_addr_idxs([int(a[0]) for a in addr_data]) - qmsg_r("Computed checksum for addr data {}[{}]: ".format(seed_id,fl)) - msg(addr_data_chksum) - qmsg("Check this value against your records") + adata = [(k,e[k]) for k in e.keys()] + return make_addr_data_chksum(sorted(adata,key=s_addrdata),keys) -def parse_addrs_file(f): - lines = get_lines_from_file(f,"address data",trim_comments=True) +def _parse_addrfile_body(lines,keys=False,check=False): - try: - seed_id,obrace = lines[0].split() - except: - msg("Invalid first line: '%s'" % lines[0]) - sys.exit(3) - - cbrace = lines[-1] - - if obrace != '{': - msg("'%s': invalid first line" % lines[0]) - elif cbrace != '}': - msg("'%s': invalid last line" % cbrace) - elif not is_mmgen_seed_id(seed_id): - msg("'%s': invalid Seed ID" % seed_id) - else: - addr_data = [] - for i in lines[1:-1]: - d = i.split(None,2) + def parse_addr_lines(lines): + ret = [] + for l in lines: + d = l.split(None,2) if not is_mmgen_idx(d[0]): - msg("'%s': invalid address num. in line: %s" % (d[0],d)) + msg("'%s': invalid address num. in line: '%s'" % (d[0],l)) sys.exit(3) if not is_btc_addr(d[1]): @@ -589,210 +269,112 @@ def parse_addrs_file(f): sys.exit(3) if len(d) == 3: - check_addr_label(d[2]) - - addr_data.append(tuple(d)) - - check_addr_data_hash(seed_id,addr_data) - - return seed_id,addr_data - - sys.exit(3) - - -def sign_transaction(c,tx_hex,tx_num_str,sig_data,keys=None): - - if keys: - qmsg("%s keys total" % len(keys)) - if g.debug: print "Keys:\n %s" % "\n ".join(keys) - - msg_r("Signing transaction{}...".format(tx_num_str)) - from mmgen.rpc import exceptions - try: - sig_tx = c.signrawtransaction(tx_hex,sig_data,keys) - except exceptions.InvalidAddressOrKey: - msg("failed\nInvalid address or key") - sys.exit(3) - - return sig_tx - -def get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts): - - if seed_id in saved_seeds.keys(): - return saved_seeds[seed_id] - - while True: - if infiles: - seed = get_seed_retry(infiles.pop(0),opts) - elif "from_brain" in opts or "from_mnemonic" in opts \ - or "from_seed" in opts or "from_incog" in opts: - msg("Need data for seed ID %s" % seed_id) - seed = get_seed_retry("",opts) - msg("User input produced seed ID %s" % make_chksum_8(seed)) - else: - msg("ERROR: No seed source found for seed ID: %s" % seed_id) - sys.exit(2) - - s_id = make_chksum_8(seed) - saved_seeds[s_id] = seed - - if s_id == seed_id: return seed - - -def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds,opts,gen_pairs=False): - - seed_ids = list(set([i[:8] for i in mmgen_addrs])) - ret = [] - - for seed_id in seed_ids: - # Returns only if seed is found - seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts) - - addr_ids = [int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id] - from mmgen.addr import generate_addrs - if gen_pairs: - ret += [("{}:{}".format(seed_id,i.num),i.addr) - for i in generate_addrs(seed,addr_ids, - {'gen_what':["addrs"]})] - else: - ret += [i.wif for i in generate_addrs(seed,addr_ids, - {'gen_what':["keys"]})] - - return ret - - -def sign_tx_with_bitcoind_wallet(c,tx_hex,tx_num_str,sig_data,keys,opts): - - try: - sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys) - except: - from mmgen.rpc import exceptions - msg("Using keys in wallet.dat as per user request") - prompt = "Enter passphrase for bitcoind wallet: " - while True: - passwd = get_bitcoind_passphrase(prompt,opts) - - try: - c.walletpassphrase(passwd, 9999) - except exceptions.WalletPassphraseIncorrect: - msg("Passphrase incorrect") + comment = d[2] + check_addr_label(comment) else: - msg("Passphrase OK"); break + comment = "" - sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys) + ret.append((d[0],d[1],comment)) - msg("Locking wallet") - try: - c.walletlock() - except: - msg("Failed to lock wallet") + return ret - return sig_tx + def parse_key_lines(lines): + ret = [] + for l in lines: + d = l.split(None,2) + + if d[0] != "wif:": + msg("Invalid key line in file: '%s'" % l) + sys.exit(3) + + if not is_btc_key(d[1]): + msg("'%s': invalid Bitcoin key" % d[1]) + sys.exit(3) + + ret.append(d[1]) + + return ret + + z = len(lines) / 2 + if keys: + adata = parse_addr_lines([lines[i*2] for i in range(z)]) + kdata = parse_key_lines([lines[i*2+1] for i in range(z)]) + if len(adata) != len(kdata): + msg("Odd number of lines in key file") + sys.exit(2) + if check or keypress_confirm("Check key-to-address validity?"): + wif2addr_f = get_wif2addr_f() + for i in range(z): + msg_r("\rVerifying keys %s/%s" % (i+1,z)) + if adata[i][1] != wif2addr_f(kdata[i]): + msg("Key doesn't match address!\n %s\n %s" % + kdata[i],adata[i][1]) + sys.exit(2) + msg(" - done") + return [adata[i] + (kdata[i],) for i in range(z)] + else: + return parse_addr_lines(lines) -def preverify_keys(addrs_in, keys_in, mm_inputs): +def parse_addrfile(f,addr_data,keys=False): + return parse_addrfile_lines( + get_lines_from_file(f,"address data",trim_comments=True), + addr_data,keys) - addrs,keys = set(addrs_in),set(keys_in) - - import mmgen.bitcoin as b - - qmsg_r('Checking that user-supplied key list contains valid keys...') - - invalid_keys = [k for k in keys if not is_btc_key(k)] - if invalid_keys: - s = "" if len(invalid_keys) == 1 else "s" - msg("\n%s/%s invalid key%s in keylist!\n" % (len(invalid_keys),len(keys),s)) - sys.exit(2) - - qmsg("OK") - - # Check that keys match addresses: - msg('Pre-verifying keys in user-supplied key list (Ctrl-C to skip)') +def parse_addrfile_lines(lines,addr_data,keys=False,exit_on_error=True): try: - for n,k in enumerate(keys,1): - msg_r("\rkey %s of %s" % (n,len(keys))) - c = False if k[0] == '5' else True - hexkey = b.wiftohex(k,compressed=c) - addr = b.privnum2addr(int(hexkey,16),compressed=c) - if addr in addrs: - addrs.remove(addr) - if not addrs: break - except KeyboardInterrupt: - msg("\nSkipping") - else: - msg("") - if addrs: - mms = dict([(i['address'],i['account'].split()[0]) - for i in mm_inputs if i['address'] in addrs]) - s = "" if len(addrs) == 1 else "es" - msg( -"Cannot sign transaction. No keys found for the following address%s:"%s) - for a in sorted(addrs): - print " %s%s" % (a, " ({})".format(mms[a]) if a in mms else "") - sys.exit(2) - else: - extra_keys = len(keys) - len(set(addrs_in)) - if extra_keys > 0: - s = "" if extra_keys == 1 else "s" - msg("%s extra key%s found" % (extra_keys,s)) - - - -def missing_keys_errormsg(other_addrs): - msg(""" -A key file must be supplied (or use the "-w" option) for the following -non-mmgen address%s: -""".strip() % ("" if len(other_addrs) == 1 else "es")) - print " %s" % "\n ".join([i['address'] for i in other_addrs]) - - -def check_mmgen_to_btc_addr_mappings_addrfile(mmgen_inputs,b2m_map,addrfiles): - addr_data = [parse_addrs_file(a) for a in addrfiles] - in_maplist = [(i['account'].split()[0],i['address']) for i in mmgen_inputs] - out_maplist = [(i[1][0],i[0]) for i in b2m_map.items()] - - missing,wrong = [],[] - for maplist,label in (in_maplist,"inputs"), (out_maplist,"outputs"): - qmsg("Checking MMGen -> BTC address mappings for %s" % label) - for i in maplist: - btaddr = mmaddr2btcaddr_addrfile(i[0],addr_data,silent=True)[0] - if not btaddr: missing.append(i[0]) - elif btaddr != i[1]: wrong.append((i[0],i[1],btaddr)) - - if wrong: - fs = " {:11} {:35} {}" - msg("ERROR: The following address mappings did not match!") - msg(fs.format("MMGen addr","In TX file:","In address file:")) - for w in wrong: msg(fs.format(*w)) - sys.exit(3) - - if missing: - confirm_or_exit(txmsg['missing_mappings'] % - " ".join(missing),"continue") - else: qmsg("Address mappings OK") - - -def is_valid_tx_comment(s, verbose=True): - if len(s) > g.max_tx_comment_len: - if verbose: msg("Invalid transaction comment (longer than %s characters)" % - g.max_tx_comment_len) - return False - try: s.decode("utf8") + seed_id,obrace = lines[0].split() except: - if verbose: msg("Invalid transaction comment (not UTF-8)") + errmsg = "Invalid first line: '%s'" % lines[0] + else: + cbrace = lines[-1] + if obrace != '{': + errmsg = "'%s': invalid first line" % lines[0] + elif cbrace != '}': + errmsg = "'%s': invalid last line" % cbrace + elif not is_mmgen_seed_id(seed_id): + errmsg = "'%s': invalid Seed ID" % seed_id + else: + ldata = _parse_addrfile_body(lines[1:-1],keys) + if seed_id not in addr_data: addr_data[seed_id] = {} + for l in ldata: + addr_data[seed_id][l[0]] = l[1:] + chk = get_addr_data_hash(addr_data[seed_id],keys) + from mmgen.addr import fmt_addr_idxs + fl = fmt_addr_idxs([int(i) for i in addr_data[seed_id].keys()]) + w = "key" if keys else "addr" + qmsg_r("Computed checksum for "+w+" data ",w.capitalize()+" checksum ") + msg("{}[{}]: {}".format(seed_id,fl,chk)) + qmsg("Check this value against your records") + return True + + if exit_on_error: + msg(errmsg) + sys.exit(3) + else: return False - else: return True + + +def parse_keyaddr_file(infile,addr_data): + d = get_data_from_file(infile,"%s key-address file data" % g.proj_name) + enc_ext = get_extension(infile) == g.mmenc_ext + if enc_ext or not is_utf8(d): + m = "Decrypting" if enc_ext else "Attempting to decrypt" + msg("%s key-address file %s" % (m,infile)) + from crypto import mmgen_decrypt_retry + d = mmgen_decrypt_retry(d,"key-address file") + parse_addrfile_lines(remove_comments(d.split("\n")),addr_data,True,False) + def get_tx_comment_from_file(infile): s = get_data_from_file(infile,"transaction comment") if is_valid_tx_comment(s, verbose=True): return s.decode("utf8").strip() - else: return False - + else: + sys.exit(2) def get_tx_comment_from_user(comment=""): - try: while True: s = my_raw_input("Comment: ",insert_txt=comment.encode("utf8")) @@ -800,12 +382,68 @@ def get_tx_comment_from_user(comment=""): if is_valid_tx_comment(s, verbose=True): return s.decode("utf8") except KeyboardInterrupt: - msg("User interrupt") - return False - + msg("User interrupt") + return False def make_tx_data(metadata_fmt, tx_hex, inputs_data, b2m_map, comment): from mmgen.bitcoin import b58encode s = (b58encode(comment.encode("utf8")),) if comment else () lines = (metadata_fmt, tx_hex, repr(inputs_data), repr(b2m_map)) + s return "\n".join(lines)+"\n" + +def mmaddr2btcaddr_addrdata(mmaddr,addr_data,source=""): + seed_id,idx = mmaddr.split(":") + if seed_id in addr_data: + if idx in addr_data[seed_id]: + vmsg("%s -> %s%s" % (mmaddr,addr_data[seed_id][idx][0], + " (from "+source+")" if source else "")) + return addr_data[seed_id][idx] + + return "","" + +def get_bitcoind_cfg_options(cfg_keys): + + if "HOME" in os.environ: # Linux + homedir,datadir = os.environ["HOME"],".bitcoin" + elif "HOMEPATH" in os.environ: # Windows: + homedir,data_dir = os.environ["HOMEPATH"],r"Application Data\Bitcoin" + else: + msg("Neither $HOME nor %HOMEPATH% are set") + msg("Don't know where to look for 'bitcoin.conf'") + sys.exit(3) + + cfg_file = os.sep.join((homedir, datadir, "bitcoin.conf")) + + cfg = dict([(k,v) for k,v in [split2(line.translate(None,"\t "),"=") + for line in get_lines_from_file(cfg_file)] if k in cfg_keys]) + + for k in set(cfg_keys) - set(cfg.keys()): + msg("Configuration option '%s' must be set in %s" % (k,cfg_file)) + sys.exit(2) + + return cfg + +def connect_to_bitcoind(): + + host,port,user,passwd = "localhost",8332,"rpcuser","rpcpassword" + cfg = get_bitcoind_cfg_options((user,passwd)) + + import mmgen.rpc.connection + f = mmgen.rpc.connection.BitcoinConnection + + try: + c = f(cfg[user],cfg[passwd],host,port) + except: + msg("Unable to establish RPC connection with bitcoind") + sys.exit(2) + + return c + + +def wiftoaddr_keyconv(wif): + from subprocess import Popen, PIPE + if wif[0] == '5': + return Popen(["keyconv", wif], + stdout=PIPE).stdout.readline().split()[1] + else: + return wiftoaddr(wif) diff --git a/mmgen/util.py b/mmgen/util.py index 86bca533..11be5e28 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -28,74 +28,33 @@ import mmgen.config as g def msg(s): sys.stderr.write(s + "\n") def msg_r(s): sys.stderr.write(s) -def qmsg(s,alt=""): +def qmsg(s,alt=False): if g.quiet: - if alt: sys.stderr.write(alt + "\n") + if alt != False: sys.stderr.write(alt + "\n") else: sys.stderr.write(s + "\n") -def qmsg_r(s,alt=""): +def qmsg_r(s,alt=False): if g.quiet: - if alt: sys.stderr.write(alt) + if alt != False: sys.stderr.write(alt) else: sys.stderr.write(s) def vmsg(s): if g.verbose: sys.stderr.write(s + "\n") def vmsg_r(s): if g.verbose: sys.stderr.write(s) -cmessages = { - 'null': "", - 'incog_iv_id': """ - If you know your Incog ID, check it against the value above. If it's - incorrect, then your incognito data is invalid. -""", - 'incog_iv_id_hidden': """ - If you know your Incog ID, check it against the value above. If it's - incorrect, then your incognito data is invalid or you've supplied - an incorrect offset. -""", - 'incog_key_id': """ - Check that the generated seed ID is correct. If it's not, then your - password or hash preset is incorrect or incognito data is corrupted. -""", - 'incog_key_id_hidden': """ - Check that the generated seed ID is correct. If it's not, then your - password or hash preset is incorrect or incognito data is corrupted. - If the key ID is correct but the seed ID is not, then you might have - chosen an incorrect seed length. -""", - 'unencrypted_secret_keys': """ -This program generates secret keys from your {} seed, outputting them in -UNENCRYPTED form. Generate only the key(s) you need and guard them carefully. -""".format(g.proj_name), - 'brain_warning': """ -############################## EXPERTS ONLY! ############################## +def suf(arg,what): + t = type(arg) + if t == int: + n = arg + elif t == list or t == tuple or t == set: + n = len(arg) + else: + msg("%s: invalid parameter" % arg) + return "" -A brainwallet will be secure only if you really know what you're doing and -have put much care into its creation. {} assumes no responsibility for -coins stolen as a result of a poorly crafted brainwallet passphrase. - -A key will be generated from your passphrase using the parameters requested -by you: seed length {}, hash preset '{}'. For brainwallets it's highly -recommended to use one of the higher-numbered presets - -Remember the seed length and hash preset parameters you've specified. To -generate the correct keys/addresses associated with this passphrase in the -future, you must continue using these same parameters -""", - 'usr_rand_notice': """ -You've chosen to not fully trust your OS's random number generator and provide -some additional entropy of your own. Please type %s symbols on your keyboard. -Type slowly and choose your symbols carefully for maximum randomness. Try to -use both upper and lowercase as well as punctuation and numerals. What you -type will not be displayed on the screen. Note that the timings between your -keystrokes will also be used as a source of randomness. -""", - 'choose_wallet_passphrase': """ -Now you must choose a passphrase to encrypt the wallet with. A key will be -generated from your passphrase using a hash preset of '%s'. Please note that -no strength checking of passphrases is performed. For an empty passphrase, -just hit ENTER twice. -""".strip() -} + if what in "a": + return "" if n == 1 else "es" + if what in "k": + return "" if n == 1 else "s" def get_extension(f): import os @@ -138,6 +97,11 @@ def _is_hex(s): except: return False else: return True +def is_utf8(s): + try: s.decode("utf8") + except: return False + else: return True + def match_ext(addr,ext): return addr.split(".")[-1] == ext @@ -149,7 +113,7 @@ def pretty_hexdump(data,gw=2,cols=8,line_nums=False): r = 1 if len(data) % gw else 0 return "".join( [ - ("" if (line_nums == False or i % cols) else "%03i: " % (i/cols)) + + ("" if (line_nums == False or i % cols) else "{:06x}: ".format(i*gw)) + hexlify(data[i*gw:i*gw+gw]) + (" " if (i+1) % cols else "\n") for i in range(len(data)/gw + r) @@ -158,7 +122,8 @@ def pretty_hexdump(data,gw=2,cols=8,line_nums=False): def decode_pretty_hexdump(data): import re - lines = [re.sub('^\d+:\s+','',l) for l in data.split("\n")] + from string import hexdigits + lines = [re.sub('^['+hexdigits+']+:\s+','',l) for l in data.split("\n")] return unhexlify("".join(("".join(lines).split()))) def get_hash_params(hash_preset): @@ -237,7 +202,6 @@ def check_infile(f): return check_file_type_and_access(f,"input file") def check_outfile(f): return check_file_type_and_access(f,"output file") def check_outdir(f): return check_file_type_and_access(f,"directory") - def _validate_addr_num(n): try: n = int(n) @@ -258,7 +222,7 @@ def make_full_path(outdir,outfile): # os.path.join() doesn't work? -def parse_address_list(arg,sep=","): +def parse_addr_idxs(arg,sep=","): ret = [] @@ -278,7 +242,7 @@ def parse_address_list(arg,sep=","): if end < beg: msg("'%s-%s': end of range less than beginning" % (beg,end)) return False - for k in range(beg,end+1): ret.append(k) + ret.extend(range(beg,end+1)) else: msg("'%s': invalid argument for address range" % i) return False @@ -327,11 +291,8 @@ def confirm_or_false(message, question, expect="YES"): p = question+" "+conf_msg if question[0].isupper() else \ "Are you sure you want to %s?\n%s" % (question,conf_msg) - ret = True if my_raw_input(p).strip() == expect else False - vmsg("") - return ret - + return my_raw_input(p).strip() == expect def write_to_stdout(data, what, confirm=True): @@ -341,7 +302,9 @@ def write_to_stdout(data, what, confirm=True): try: import os of = os.readlink("/proc/%d/fd/1" % os.getpid()) - msg("Redirecting output to file '%s'" % os.path.relpath(of)) + of_maybe = os.path.relpath(of) + of = of if of_maybe.find(os.path.pardir) == 0 else of_maybe + msg("Redirecting output to file '%s'" % of) except: msg("Redirecting output to file") sys.stdout.write(data) @@ -383,8 +346,7 @@ def write_to_file_or_stdout(outfile, data, opts, what="data"): if 'stdout' in opts or not sys.stdout.isatty(): write_to_stdout(data, what, confirm=True) else: - confirm_overwrite = False if g.quiet else True - write_to_file(outfile,data,opts,what,confirm_overwrite,True) + write_to_file(outfile,data,opts,what,not g.quiet,True) from mmgen.bitcoin import b58decode_pad,b58encode_pad @@ -441,10 +403,9 @@ def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts): seed_id,key_id,seed_len,hash_preset,g.wallet_ext) d = "\n".join((chk,)+lines)+"\n" - confirm_overwrite = False if g.quiet else True - write_to_file(outfile,d,opts,"wallet",confirm_overwrite,True) + write_to_file(outfile,d,opts,"wallet",not g.quiet,True) - if g.verbose: + if g.debug: display_control_data(label,metadata,hash_preset,salt,enc_seed) @@ -542,8 +503,7 @@ def get_data_from_wallet(infile,silent=False): def _get_words_from_user(prompt, opts): # split() also strips - words = my_raw_input(prompt, - echo=True if 'echo_passphrase' in opts else False).split() + words = my_raw_input(prompt, echo='echo_passphrase' in opts).split() if g.debug: print "Sanitized input: [%s]" % " ".join(words) return words @@ -610,7 +570,7 @@ def get_seed_from_seed_data(words): msg("Invalid b58 number: %s" % val) return False - vmsg("%s data produces seed ID: %s" % (g.seed_ext,make_chksum_8(seed))) + msg("Valid seed data for seed ID %s" % make_chksum_8(seed)) return seed else: msg("Invalid checksum for {} seed".format(g.proj_name)) @@ -643,8 +603,7 @@ def get_bitcoind_passphrase(prompt,opts): return get_data_from_file(opts['passwd_file'], "passphrase").strip("\r\n") else: - return my_raw_input(prompt, - echo=True if 'echo_passphrase' in opts else False) + return my_raw_input(prompt, echo='echo_passphrase' in opts) def check_data_fits_file_at_offset(fname,offset,dlen,action): @@ -664,43 +623,11 @@ def check_data_fits_file_at_offset(fname,offset,dlen,action): sys.exit(1) -def get_hidden_incog_data(opts): - # Already sanity-checked: - fname,offset,seed_len = opts['from_incog_hidden'].split(",") - qmsg("Getting hidden incog data from file '%s'" % fname) - - dlen = g.aesctr_iv_len + g.salt_len + (int(seed_len)/8) - - fsize = check_data_fits_file_at_offset(fname,int(offset),dlen,"read") - - f = os.open(fname,os.O_RDONLY) - os.lseek(f, int(offset), os.SEEK_SET) - data = os.read(f, dlen) - os.close(f) - qmsg("Data read from file '%s' at offset %s" % (fname,offset), - "Data read from file") - return data - - -def export_to_hidden_incog(incog_enc,opts): - outfile,offset = opts['export_incog_hidden'].split(",") #Already sanity-checked - if 'outdir' in opts: outfile = make_full_path(opts['outdir'],outfile) - - check_data_fits_file_at_offset(outfile,int(offset),len(incog_enc),"write") - - if not g.quiet: confirm_or_exit("","alter file '%s'" % outfile) - f = os.open(outfile,os.O_RDWR) - os.lseek(f, int(offset), os.SEEK_SET) - os.write(f, incog_enc) - os.close(f) - msg("Data written to file '%s' at offset %s" % - (os.path.relpath(outfile),offset)) - from mmgen.term import kb_hold_protect,get_char def get_hash_preset_from_user(hp='3',what="data"): p = "Enter hash preset for %s, or ENTER to accept the default ('%s'): " \ - % (what,hp) + % (what,hp) while True: ret = my_raw_input(p) if ret: @@ -761,5 +688,3 @@ def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False): if verbose: msg("\nInvalid reply") else: msg_r("\r") - -