Browse Source

* 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.

philemon 11 years ago
parent
commit
4378ac0db9
19 changed files with 1255 additions and 1205 deletions
  1. 10 7
      mmgen/Opts.py
  2. 26 22
      mmgen/addr.py
  3. 35 50
      mmgen/bitcoin.py
  4. 5 4
      mmgen/config.py
  5. 129 67
      mmgen/crypto.py
  6. 4 0
      mmgen/main.py
  7. 44 34
      mmgen/main_addrgen.py
  8. 34 28
      mmgen/main_addrimport.py
  9. 2 2
      mmgen/main_pywallet.py
  10. 284 29
      mmgen/main_txcreate.py
  11. 2 3
      mmgen/main_txsend.py
  12. 236 65
      mmgen/main_txsign.py
  13. 53 4
      mmgen/main_walletchk.py
  14. 33 5
      mmgen/main_walletgen.py
  15. 49 38
      mmgen/mnemonic.py
  16. 5 129
      mmgen/tests/bitcoin.py
  17. 46 23
      mmgen/tool.py
  18. 219 581
      mmgen/tx.py
  19. 39 114
      mmgen/util.py

+ 10 - 7
mmgen/Opts.py

@@ -57,7 +57,7 @@ def parse_opts(argv,help_data):
 	('outdir', 'export_incog_hidden'),
 	('outdir', 'export_incog_hidden'),
 	('from_incog_hidden','from_incog','from_seed','from_mnemonic','from_brain'),
 	('from_incog_hidden','from_incog','from_seed','from_mnemonic','from_brain'),
 	('export_incog','export_incog_hex','export_incog_hidden','export_mnemonic',
 	('export_incog','export_incog_hex','export_incog_hidden','export_mnemonic',
-	 'export_seed'),
+	'export_seed'),
 	('quiet','verbose')
 	('quiet','verbose')
 	): warn_incompatible_opts(opts,l)
 	): 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 not check_opts(opts,long_opts): sys.exit(1)
 
 
 	# If unset, set these to default values in mmgen.config:
 	# 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)
 		if v in opts: typeconvert_override_var(opts,v)
 		else: opts[v] = eval("g."+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
 	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("_","-")
 		what = "parameter for '--%s' option" % opt.replace("_","-")
 
 
 		# Check for file existence and readability
 		# 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
 			check_infile(val)  # exits on error
 			continue
 			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
 			if not opt_is_in_list(val,g.hash_presets.keys(),what): return False
 		elif opt == 'usr_randchars':
 		elif opt == 'usr_randchars':
 			if not opt_is_int(val,what): return False
 			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.min_urandchars,what): return False
 			if not opt_compares(val,"<=",g.max_urandchars,what): return False
 			if not opt_compares(val,"<=",g.max_urandchars,what): return False
 		else:
 		else:
@@ -187,6 +189,7 @@ def typeconvert_override_var(opts,opt):
 	if   vtype == int:   f,t = int,"an integer"
 	if   vtype == int:   f,t = int,"an integer"
 	elif vtype == str:   f,t = str,"a string"
 	elif vtype == str:   f,t = str,"a string"
 	elif vtype == float: f,t = float,"a float"
 	elif vtype == float: f,t = float,"a float"
+	elif vtype == bool:  f,t = bool,"a boolean value"
 
 
 	try:
 	try:
 		opts[opt] = f(opts[opt])
 		opts[opt] = f(opts[opt])

+ 26 - 22
mmgen/addr.py

@@ -58,10 +58,10 @@ def test_for_keyconv():
 	return True
 	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")
 			msg("Using (slow) internal ECDSA library for address generation")
 			from mmgen.bitcoin import privnum2addr
 			from mmgen.bitcoin import privnum2addr
 			keyconv = False
 			keyconv = False
@@ -69,18 +69,21 @@ def generate_addrs(seed, addrnums, opts):
 			from subprocess import Popen, PIPE
 			from subprocess import Popen, PIPE
 			keyconv = "keyconv"
 			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
 	from collections import namedtuple
 	addrinfo = namedtuple("addrinfo",fmt)
 	addrinfo = namedtuple("addrinfo",fmt)
 	addrinfo_args = "%s" % ",".join(fmt.split())
 	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,[]
 	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:
 	while pos != t_addrs:
 		seed = sha512(seed).digest()
 		seed = sha512(seed).digest()
@@ -91,50 +94,51 @@ def generate_addrs(seed, addrnums, opts):
 
 
 		pos += 1
 		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/
 		# Secret key is double sha256 of seed hash round /num/
 		sec = sha256(sha256(seed).digest()).hexdigest()
 		sec = sha256(sha256(seed).digest()).hexdigest()
 		wif = numtowif(int(sec,16))
 		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] \
 			Popen([keyconv, wif], stdout=PIPE).stdout.readline().split()[1] \
 			if keyconv else privnum2addr(int(sec,16))
 			if keyconv else privnum2addr(int(sec,16))
 
 
 		out.append(eval("addrinfo("+addrinfo_args+")"))
 		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
 	return out
 
 
 
 
 def format_addr_data(addr_data, addr_data_chksum, seed_id, addr_idxs, opts):
 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))
 	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 = [] 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")
 		out.append("# Record this value to a secure location\n")
+	else: out = []
 
 
 	out.append("%s {" % seed_id.upper())
 	out.append("%s {" % seed_id.upper())
 
 
 	for d in addr_data:
 	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))
 			out.append(fs.format(d.num, d.addr))
 		else:
 		else:
 			out.append(fs.format(d.num, "wif: "+d.wif))
 			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:
 			if 'b16' in opts:
 				out.append(fs.format("", "hex: "+d.sec))
 				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(fs.format("", "wif: "+d.wif))
 
 
 	out.append("}")
 	out.append("}")

+ 35 - 50
mmgen/bitcoin.py

@@ -52,65 +52,56 @@ b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
 # The "zero address":
 # The "zero address":
 # 1111111111111111111114oLvT2 (use step2 = ("0" * 40) to generate)
 # 1111111111111111111114oLvT2 (use step2 = ("0" * 40) to generate)
 #
 #
+
 def pubhex2hexaddr(pubhex):
 def pubhex2hexaddr(pubhex):
 	step1 = sha256(unhexlify(pubhex)).digest()
 	step1 = sha256(unhexlify(pubhex)).digest()
 	return hashlib_new('ripemd160',step1).hexdigest()
 	return hashlib_new('ripemd160',step1).hexdigest()
 
 
-def hexaddr2addr(hexaddr):
+def hexaddr2addr(hexaddr, vers_num='00'):
 	# See above:
 	# 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()
 	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):
 def verify_addr(addr,verbose=False,return_hex=False):
 
 
-	if addr[0] != "1":
-		if verbose: print "%s: Invalid address" % addr
-		return False
-
-	num = _b58tonum(addr[1:])
-	if num == False: return False
-	addr_hex = hex(num)[2:].rstrip("L").zfill(48)
-
-	step1 = sha256(unhexlify('00'+addr_hex[:40])).digest()
-	step2 = sha256(step1).hexdigest()
+	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
 
 
-	if step2[:8] != addr_hex[40:]:
-		if verbose: print "Invalid checksum in address %s" % ("1" + addr)
-		return False
+	if verbose: print "Invalid address '%s'" % addr
+	return False
 
 
-	return addr_hex[:40] if return_hex else True
 
 
 # Reworked code from here:
 # Reworked code from here:
 
 
 def _numtob58(num):
 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):
 def _b58tonum(b58num):
 	for i in 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):
 def numtowif(numpriv):
-	step1 = '80'+hex(numpriv)[2:].rstrip('L').zfill(64)
+	step1 = '80' + "{:064x}".format(numpriv)
 	step2 = sha256(unhexlify(step1)).digest()
 	step2 = sha256(unhexlify(step1)).digest()
 	step3 = sha256(step2).hexdigest()
 	step3 = sha256(step2).hexdigest()
 	key = step1 + step3[:8]
 	key = step1 + step3[:8]
@@ -131,8 +122,8 @@ def b58decode(b58num):
 	# Zap all spaces:
 	# Zap all spaces:
 	num = _b58tonum(b58num.translate(None,' \t\n\r'))
 	num = _b58tonum(b58num.translate(None,' \t\n\r'))
 	if num == False: return False
 	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:
 # These yield bytewise equivalence in our special cases:
 
 
@@ -165,7 +156,7 @@ def wiftohex(wifpriv,compressed=False):
 	idx = 68 if compressed else 66
 	idx = 68 if compressed else 66
 	num = _b58tonum(wifpriv)
 	num = _b58tonum(wifpriv)
 	if num == False: return False
 	if num == False: return False
-	key = hex(num)[2:].rstrip('L')
+	key = "{:x}".format(num)
 	if compressed and key[66:68] != '01': return False
 	if compressed and key[66:68] != '01': return False
 	round1 = sha256(unhexlify(key[:idx])).digest()
 	round1 = sha256(unhexlify(key[:idx])).digest()
 	round2 = sha256(round1).hexdigest()
 	round2 = sha256(round1).hexdigest()
@@ -182,16 +173,10 @@ def privnum2pubhex(numpriv,compressed=False):
 	pko = ecdsa.SigningKey.from_secret_exponent(numpriv,secp256k1)
 	pko = ecdsa.SigningKey.from_secret_exponent(numpriv,secp256k1)
 	pubkey = hexlify(pko.get_verifying_key().to_string())
 	pubkey = hexlify(pko.get_verifying_key().to_string())
 	if compressed:
 	if compressed:
-		p = '03' if pubkey[-1] in "13579bdf" else '02'
+		p = '02' if pubkey[-1] in "02468ace" else '03'
 		return p+pubkey[:64]
 		return p+pubkey[:64]
 	else:
 	else:
 		return '04'+pubkey
 		return '04'+pubkey
 
 
 def privnum2addr(numpriv,compressed=False):
 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)))

+ 5 - 4
mmgen/config.py

@@ -28,7 +28,8 @@ email = "<mmgen-py@yandex.com>"
 Cdates = '2013-2014'
 Cdates = '2013-2014'
 version = '0.7.7'
 version = '0.7.7'
 
 
-quiet,verbose = False,False
+quiet,verbose,no_keyconv = False,False,False
+
 min_screen_width = 80
 min_screen_width = 80
 max_tx_comment_len = 72
 max_tx_comment_len = 72
 
 
@@ -52,18 +53,18 @@ sigtx_ext    = "sig"
 addrfile_ext = "addrs"
 addrfile_ext = "addrs"
 addrfile_chksum_ext = "chk"
 addrfile_chksum_ext = "chk"
 keyfile_ext  = "keys"
 keyfile_ext  = "keys"
-keylist_ext  = "keylist"
+keyaddrfile_ext  = "akeys"
 mmenc_ext    = "mmenc"
 mmenc_ext    = "mmenc"
 
 
 default_wl    = "electrum"
 default_wl    = "electrum"
 #default_wl    = "tirosh"
 #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_lens = 128,192,256
 seed_len  = 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
 http_timeout = 30
 
 

+ 129 - 67
mmgen/crypto.py

@@ -28,13 +28,41 @@ import mmgen.config as g
 from mmgen.util import *
 from mmgen.util import *
 from mmgen.term import get_char
 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):
 def encrypt_seed(seed, key):
 	return encrypt_data(seed, key, iv=1, what="seed")
 	return encrypt_data(seed, key, iv=1, what="seed")
 
 
 
 
 def decrypt_seed(enc_seed, key, seed_id, key_id):
 def decrypt_seed(enc_seed, key, seed_id, key_id):
 
 
-	vmsg("Checking key...")
+	vmsg_r("Checking key...")
 	chk1 = make_chksum_8(key)
 	chk1 = make_chksum_8(key)
 	if key_id:
 	if key_id:
 		if not compare_checksums(chk1, "of key", key_id, "in header"):
 		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:
 				else:
 					msg("Incorrect passphrase")
 					msg("Incorrect passphrase")
 
 
+			vmsg("")
 			return False
 			return False
 #	else:
 #	else:
 #		qmsg("Generated IDs (Seed/Key): %s/%s" % (chk2,chk1))
 #		qmsg("Generated IDs (Seed/Key): %s/%s" % (chk2,chk1))
 
 
 	if g.debug: print "Decrypted seed: %s" % hexlify(dec_seed)
 	if g.debug: print "Decrypted seed: %s" % hexlify(dec_seed)
 
 
+	vmsg("OK")
 	return dec_seed
 	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"):
 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.Cipher import AES
 	from Crypto.Util import Counter
 	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)
 	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:
 	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)
 	key = scrypt_hash_passphrase(passwd, salt, hash_preset)
 	if g.verbose or verbose:
 	if g.verbose or verbose:
 		msg("done")
 		msg("done")
@@ -133,7 +163,7 @@ def make_key(passwd, salt, hash_preset, what="key", verbose=False):
 def get_random_data_from_user(uchars):
 def get_random_data_from_user(uchars):
 
 
 	if g.quiet: msg("Enter %s random symbols" % 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: "
 	prompt = "You may begin typing.  %s symbols left: "
 	msg_r(prompt % uchars)
 	msg_r(prompt % uchars)
@@ -187,26 +217,60 @@ def get_random(length,opts):
 def get_seed_from_wallet(
 def get_seed_from_wallet(
 		infile,
 		infile,
 		opts,
 		opts,
-		prompt="{} wallet".format(g.proj_name),
+		prompt_info="{} wallet".format(g.proj_name),
 		silent=False
 		silent=False
 		):
 		):
 
 
 	wdata = get_data_from_wallet(infile,silent=silent)
 	wdata = get_data_from_wallet(infile,silent=silent)
 	label,metadata,hash_preset,salt,enc_seed = wdata
 	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)
 	key = make_key(passwd, salt, hash_preset)
 
 
 	return decrypt_seed(enc_seed, key, metadata[0], metadata[1])
 	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(
 def get_seed_from_incog_wallet(
 		infile,
 		infile,
 		opts,
 		opts,
-		prompt_what="{} incognito wallet".format(g.proj_name),
+		prompt_info="{} incognito wallet".format(g.proj_name),
 		silent=False,
 		silent=False,
 		hex_input=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)
 				msg("Data in file '%s' is not in hexadecimal format" % infile)
 				sys.exit(2)
 				sys.exit(2)
 		# File could be of invalid length, so check:
 		# 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:
 		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
 			return False
 
 
 	iv, enc_incog_data = d[0:g.aesctr_iv_len], d[g.aesctr_iv_len:]
 	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.")
 	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'])
 			else 'incog_iv_id'])
 
 
-	passwd = get_mmgen_passphrase(prompt_what,opts)
-
-	qmsg("Configured hash presets: %s" % " ".join(sorted(g.hash_presets)))
 	while True:
 	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))
 
 
-	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)
+		seed = decrypt_seed(enc_seed, key, "", "")
+		old_fmt_sid = make_chksum_8(seed)
 
 
-	iv = get_random(g.aesctr_iv_len,opts)
-	iv_id = make_iv_chksum(iv)
-	msg("Incog ID: %s" % iv_id)
+		def confirm_correct_seed_id(sid):
+			m = "Seed ID: %s.  Is the Seed ID correct?" % sid
+			return keypress_confirm(m, True)
 
 
-	# 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)
+		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 iv+wrap_enc,seed_id,key_id,iv_id,preset
+	return seed
 
 
 
 
-def get_seed(infile,opts,silent=False):
+def _get_seed(infile,opts,silent=False,seed_id=""):
 
 
 	ext = get_extension(infile)
 	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")
 		else: msg("No seed source type specified and no file supplied")
 		sys.exit(2)
 		sys.exit(2)
 
 
+	seed_id_str = " for seed ID "+seed_id if seed_id else ""
 	if source == "mnemonic":
 	if source == "mnemonic":
-		prompt = "Enter mnemonic: "
+		prompt = "Enter mnemonic%s: " % seed_id_str
 		words = get_words(infile,"mnemonic data",prompt,opts)
 		words = get_words(infile,"mnemonic data",prompt,opts)
 		wl = get_default_wordlist()
 		wl = get_default_wordlist()
 		from mmgen.mnemonic import get_seed_from_mnemonic
 		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:
 		if 'from_brain' not in opts:
 			msg("'--from-brain' parameters must be specified for brainwallet file")
 			msg("'--from-brain' parameters must be specified for brainwallet file")
 			sys.exit(2)
 			sys.exit(2)
-		prompt = "Enter brainwallet passphrase: "
+		prompt = "Enter brainwallet passphrase%s: " % seed_id_str
 		words = get_words(infile,"brainwallet data",prompt,opts)
 		words = get_words(infile,"brainwallet data",prompt,opts)
 		seed = _get_seed_from_brain_passphrase(words,opts)
 		seed = _get_seed_from_brain_passphrase(words,opts)
 	elif source == "seed":
 	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)
 		words = get_words(infile,"seed data",prompt,opts)
 		seed = get_seed_from_seed_data(words)
 		seed = get_seed_from_seed_data(words)
 	elif source == "wallet":
 	elif source == "wallet":
 		seed = get_seed_from_wallet(infile, opts, silent=silent)
 		seed = get_seed_from_wallet(infile, opts, silent=silent)
 	elif source == "incognito wallet":
 	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)
 		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
 # Repeat if entered data is invalid
-def get_seed_retry(infile,opts):
+def get_seed_retry(infile,opts,seed_id=""):
 	silent = False
 	silent = False
 	while True:
 	while True:
-		seed = get_seed(infile,opts,silent=silent)
+		seed = _get_seed(infile,opts,silent=silent,seed_id=seed_id)
 		silent = True
 		silent = True
 		if seed: return seed
 		if seed: return seed
 
 
@@ -385,7 +441,7 @@ def mmgen_encrypt(data,what="data",hash_preset='',opts={}):
 	return salt+iv+enc_d
 	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
 	dstart = salt_len + g.aesctr_iv_len
 	salt,iv,enc_d = data[:salt_len],data[salt_len:dstart],data[dstart:]
 	salt,iv,enc_d = data[:salt_len],data[salt_len:dstart],data[dstart:]
 	vmsg("Preparing to decrypt %s" % what)
 	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)
 	key = make_key(passwd, salt, hp)
 	dec_d = decrypt_data(enc_d, key, int(hexlify(iv),16), what)
 	dec_d = decrypt_data(enc_d, key, int(hexlify(iv),16), what)
 	if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
 	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:]
 		return dec_d[sha256_len+nonce_len:]
 	else:
 	else:
 		msg("Incorrect passphrase or hash preset")
 		msg("Incorrect passphrase or hash preset")
 		return False
 		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...")

+ 4 - 0
mmgen/main.py

@@ -41,3 +41,7 @@ def main(progname):
 		sys.stderr.write("\nUser interrupt\n")
 		sys.stderr.write("\nUser interrupt\n")
 		termios.tcsetattr(fd, termios.TCSADRAIN, old)
 		termios.tcsetattr(fd, termios.TCSADRAIN, old)
 		sys.exit(1)
 		sys.exit(1)
+	except EOFError:
+		sys.stderr.write("\nEnd of file\n")
+		termios.tcsetattr(fd, termios.TCSADRAIN, old)
+		sys.exit(1)

+ 44 - 34
mmgen/main_addrgen.py

@@ -42,10 +42,10 @@ help_data = {
 -h, --help              Print this help message{}
 -h, --help              Print this help message{}
 -d, --outdir=       d   Specify an alternate directory 'd' for output
 -d, --outdir=       d   Specify an alternate directory 'd' for output
 -c, --save-checksum     Save address list checksum to file
 -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
 -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}
 -l, --seed-len=     N   Length of seed.  Options: {seed_lens}
                         (default: {g.seed_len})
                         (default: {g.seed_len})
 -p, --hash-preset=  p   Use scrypt.hash() parameters from preset 'p' when
 -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
 -X, --from-incog-hex    Generate {what} from incognito hexadecimal wallet
 -G, --from-incog-hidden=f,o,l Generate {what} from incognito data in file
 -G, --from-incog-hidden=f,o,l Generate {what} from incognito data in file
                         'f' at offset 'o', with seed length of 'l'
                         '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
 -m, --from-mnemonic     Generate {what} from an electrum-like mnemonic
 -s, --from-seed         Generate {what} from a seed in .{g.seed_ext} format
 -s, --from-seed         Generate {what} from a seed in .{g.seed_ext} format
 """.format(
 """.format(
 		*(
 		*(
-		   (
+			(
 "\n-A, --no-addresses      Print only secret keys, no addresses",
 "\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"
 "\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]),
 		seed_lens=", ".join([str(i) for i in g.seed_lens]),
 		what=what, g=g
 		what=what, g=g
 ),
 ),
@@ -109,6 +108,13 @@ invocations with that passphrase
 				if what == "keys" else "")
 				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)
 opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
 if 'show_hash_presets' in opts: show_hash_presets()
 if 'show_hash_presets' in opts: show_hash_presets()
@@ -129,7 +135,7 @@ elif len(cmd_args) == 2:
 	check_infile(infile)
 	check_infile(infile)
 else: usage(help_data)
 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)
 if not addr_idxs: sys.exit(2)
 
 
@@ -137,38 +143,44 @@ do_license_msg()
 
 
 # Interact with user:
 # Interact with user:
 if what == "keys" and not g.quiet:
 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:
 # Generate data:
 
 
 seed    = get_seed_retry(infile,opts)
 seed    = get_seed_retry(infile,opts)
 seed_id = make_chksum_8(seed)
 seed_id = make_chksum_8(seed)
 
 
-for l in (
-	('flat_list', 'no_addresses'),
-	('flat_list', 'b16'),
-): warn_incompatible_opts(opts,l)
-
-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(
+opts['gen_what'] = "a" if what == "addresses" else (
+	"k" if 'no_addresses' in opts else "ka")
+
+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)
 		addr_data, addr_data_chksum, seed_id, addr_idxs, opts)
 
 
 outfile_base = "{}[{}]".format(seed_id, fmt_addr_idxs(addr_idxs))
 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:
 	if 'save_checksum' in opts:
 		write_to_file(outfile_base+"."+g.addrfile_chksum_ext,
 		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:
 	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.")
 		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)
 	addr_data_str = mmgen_encrypt(addr_data_str,"key list","",opts)
 	enc_ext = "." + g.mmenc_ext
 	enc_ext = "." + g.mmenc_ext
 else: enc_ext = ""
 else: enc_ext = ""
@@ -178,13 +190,11 @@ if 'stdout' in opts or not sys.stdout.isatty():
 	if enc_ext and sys.stdout.isatty():
 	if enc_ext and sys.stdout.isatty():
 		msg("Cannot write encrypted data to screen.  Exiting")
 		msg("Cannot write encrypted data to screen.  Exiting")
 		sys.exit(2)
 		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:
 else:
-	confirm_overwrite = False if g.quiet else True
 	outfile = "%s.%s%s" % (outfile_base, (
 	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)

+ 34 - 28
mmgen/main_addrimport.py

@@ -24,7 +24,7 @@ import sys
 from mmgen.Opts   import *
 from mmgen.Opts   import *
 from mmgen.license import *
 from mmgen.license import *
 from mmgen.util 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 = {
 help_data = {
 	'prog_name': g.prog_name,
 	'prog_name': g.prog_name,
@@ -32,38 +32,44 @@ help_data = {
                      watching wallet""".format(pnm=g.proj_name),
                      watching wallet""".format(pnm=g.proj_name),
 	'usage':"[opts] [mmgen address file]",
 	'usage':"[opts] [mmgen address file]",
 	'options': """
 	'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)
 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:
 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
 from mmgen.bitcoin import verify_addr
 qmsg_r("Validating addresses...")
 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):
 	if not verify_addr(i[1],verbose=True):
 		msg("%s: invalid address" % i)
 		msg("%s: invalid address" % i)
 		sys.exit(2)
 		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
 import mmgen.config as g
 g.http_timeout = 3600
 g.http_timeout = 3600
@@ -94,8 +100,9 @@ def import_address(addr,label,rescan):
 		err_flag = True
 		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:
 if "rescan" in opts:
 	import threading
 	import threading
@@ -105,10 +112,9 @@ else:
 	msg_fmt = "\r%-" + str(w1) + "s %-34s %-" + str(w2) + "s"
 	msg_fmt = "\r%-" + str(w1) + "s %-34s %-" + str(w2) + "s"
 
 
 msg("Importing addresses")
 msg("Importing addresses")
-for n,i in enumerate(addr_data):
+for n,i in enumerate(addr_list):
 	if i[0]:
 	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"
 	else: label = "non-mmgen"
 
 
 	if "rescan" in opts:
 	if "rescan" in opts:
@@ -123,7 +129,7 @@ for n,i in enumerate(addr_data):
 				elapsed = int(time.time() - start)
 				elapsed = int(time.time() - start)
 				msg_r(msg_fmt % (
 				msg_r(msg_fmt % (
 						secs_to_hms(elapsed),
 						secs_to_hms(elapsed),
-						("%s/%s:" % (n+1,len(addr_data))),
+						("%s/%s:" % (n+1,len(addr_list))),
 						i[1], "(" + label + ")"
 						i[1], "(" + label + ")"
 					)
 					)
 				)
 				)
@@ -134,7 +140,7 @@ for n,i in enumerate(addr_data):
 				break
 				break
 	else:
 	else:
 		import_address(i[1],label,rescan=False)
 		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 + ")"))
 							i[1], "(" + label + ")"))
 		if err_flag: msg("\nImport failed"); sys.exit(2)
 		if err_flag: msg("\nImport failed"); sys.exit(2)
 		msg(" - OK")
 		msg(" - OK")

+ 2 - 2
mmgen/main_pywallet.py

@@ -1671,7 +1671,7 @@ data = "\n".join(data) + "\n"
 
 
 # Output data
 # Output data
 if 'stdout' in opts or not sys.stdout.isatty():
 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:
 else:
 	write_walletdat_dump_to_file(wallet_id, data, len_arg, ext, what, opts)
 	write_walletdat_dump_to_file(wallet_id, data, len_arg, ext, what, opts)

+ 284 - 29
mmgen/main_txcreate.py

@@ -28,7 +28,6 @@ import mmgen.config as g
 from mmgen.Opts import *
 from mmgen.Opts import *
 from mmgen.license import *
 from mmgen.license import *
 from mmgen.tx import *
 from mmgen.tx import *
-from mmgen.util import msg, msg_r, keypress_confirm
 
 
 help_data = {
 help_data = {
 	'prog_name': g.prog_name,
 	'prog_name': g.prog_name,
@@ -43,6 +42,7 @@ help_data = {
 -i, --info            Display unspent outputs and exit
 -i, --info            Display unspent outputs and exit
 -q, --quiet           Suppress warnings; overwrite files without
 -q, --quiet           Suppress warnings; overwrite files without
                       prompting
                       prompting
+-v, --verbose         Produce more verbose output
 """.format(g=g),
 """.format(g=g),
 	'notes': """
 	'notes': """
 
 
@@ -60,40 +60,300 @@ with no amount on the command line.
 """.format(g=g,pnm=g.proj_name)
 """.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)
 opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
 if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 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()
 c = connect_to_bitcoind()
 
 
 if not 'info' in opts:
 if not 'info' in opts:
 	do_license_msg(immed=True)
 	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]
 	addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext]
 	cmd_args = set(cmd_args) - set(addrfiles)
 	cmd_args = set(cmd_args) - set(addrfiles)
 
 
 	for a in addrfiles:
 	for a in addrfiles:
 		check_infile(a)
 		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:
 	for a in cmd_args:
 		if "," in a:
 		if "," in a:
-			a1,a2 = a.split(",")
+			a1,a2 = split2(a,",")
 			if is_btc_addr(a1):
 			if is_btc_addr(a1):
 				btcaddr = a1
 				btcaddr = a1
 			elif is_mmgen_addr(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)
 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))
 	us = eval(get_data_from_file(g.bogus_wallet_data))
 else:
 else:
 	us = c.listunspent()
 	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)
 unspent = sort_and_view(us,opts)
 
 
@@ -165,7 +422,7 @@ while True:
 	mmaddrs.discard("")
 	mmaddrs.discard("")
 
 
 	if mmaddrs and len(mmaddrs) < len(sel_unspent):
 	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?"):
 		if not keypress_confirm("Accept?"):
 			continue
 			continue
 
 
@@ -177,10 +434,10 @@ while True:
 		if keypress_confirm(prompt,default_yes=True):
 		if keypress_confirm(prompt,default_yes=True):
 			break
 			break
 	else:
 	else:
-		msg(txmsg['not_enough_btc'] % change)
+		msg(wmsg['not_enough_btc'] % change)
 
 
 if change > 0 and not change_addr:
 if change > 0 and not change_addr:
-	msg(txmsg['throwaway_change'] % change)
+	msg(wmsg['throwaway_change'] % change)
 	sys.exit(2)
 	sys.exit(2)
 
 
 if change_addr in tx_out and not change:
 if change_addr in tx_out and not change:
@@ -198,8 +455,6 @@ if g.debug:
 	print "tx_out:", repr(tx_out)
 	print "tx_out:", repr(tx_out)
 
 
 if 'comment_file' in opts:
 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):
 	if keypress_confirm("Edit comment?",False):
 		comment = get_tx_comment_from_user(comment)
 		comment = get_tx_comment_from_user(comment)
 else:
 else:
@@ -219,7 +474,7 @@ metadata = tx_id, amt, make_timestamp()
 
 
 if reply and reply in "YyVv":
 if reply and reply in "YyVv":
 	view_tx_data(c,[i.__dict__ for i in sel_unspent],tx_hex,b2m_map,
 	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?"
 prompt = "Save transaction?"
 if keypress_confirm(prompt,default_yes=True):
 if keypress_confirm(prompt,default_yes=True):

+ 2 - 3
mmgen/main_txsend.py

@@ -51,7 +51,7 @@ do_license_msg()
 
 
 tx_data = get_lines_from_file(infile,"signed transaction data")
 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)
 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"
 prompt = "View transaction data? (y)es, (N)o, (v)iew in pager"
 reply = prompt_and_get_char(prompt,"YyNnVv",enter_ok=True)
 reply = prompt_and_get_char(prompt,"YyNnVv",enter_ok=True)
 if reply and reply in "YyVv":
 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?"):
 if keypress_confirm("Edit transaction comment?"):
 	comment = get_tx_comment_from_user(comment)
 	comment = get_tx_comment_from_user(comment)

+ 236 - 65
mmgen/main_txsign.py

@@ -26,7 +26,6 @@ import mmgen.config as g
 from mmgen.Opts import *
 from mmgen.Opts import *
 from mmgen.license import *
 from mmgen.license import *
 from mmgen.tx import *
 from mmgen.tx import *
-from mmgen.util import msg,qmsg
 
 
 help_data = {
 help_data = {
 	'prog_name': g.prog_name,
 	'prog_name': g.prog_name,
@@ -39,16 +38,18 @@ help_data = {
 -i, --info               Display information about the transaction and exit
 -i, --info               Display information about the transaction and exit
 -I, --tx-id              Display transaction ID and exit
 -I, --tx-id              Display transaction ID and exit
 -k, --keys-from-file= f  Provide additional keys for non-{pnm} addresses
 -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'
 -P, --passwd-file=    f  Get MMGen wallet or bitcoind passphrase from file 'f'
 -q, --quiet              Suppress warnings; overwrite files without
 -q, --quiet              Suppress warnings; overwrite files without
                          prompting
                          prompting
 -v, --verbose            Produce more verbose output
 -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,
 -b, --from-brain=    l,p Generate keys from a user-created password,
                          i.e. a "brainwallet", using seed length 'l' and
                          i.e. a "brainwallet", using seed length 'l' and
                          hash preset 'p'
                          hash preset 'p'
@@ -57,9 +58,10 @@ help_data = {
 -X, --from-incog-hex     Generate keys from an incognito hexadecimal wallet
 -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
 -G, --from-incog-hidden= f,o,l  Generate keys from incognito data in file
                          'f' at offset 'o', with seed length of 'l'
                          '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
 -m, --from-mnemonic      Generate keys from an electrum-like mnemonic
 -s, --from-seed          Generate keys from a seed in .{g.seed_ext} format
 -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': """
 	'notes': """
 
 
 Transactions with either {pnm} or non-{pnm} input addresses may be signed.
 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())
 """.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)
 opts,infiles = parse_opts(sys.argv,help_data)
 
 
 for l in (
 for l in (
-('tx_id', 'info'),
-('keys_from_file','all_keys_from_file')
+('tx_id', 'info')
 ): warn_incompatible_opts(opts,l)
 ): warn_incompatible_opts(opts,l)
 
 
 if 'from_incog_hex' in opts or 'from_incog_hidden' in opts:
 if 'from_incog_hex' in opts or 'from_incog_hidden' in opts:
 	opts['from_incog'] = True
 	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)
 if not infiles: usage(help_data)
 for i in infiles: check_infile(i)
 for i in infiles: check_infile(i)
@@ -108,24 +297,15 @@ c = connect_to_bitcoind()
 
 
 saved_seeds = {}
 saved_seeds = {}
 tx_files  = [i for i in set(infiles) if get_extension(i) == g.rawtx_ext]
 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)
 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:
 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 = ""
 tx_num_str = ""
 for tx_num,tx_file in enumerate(tx_files,1):
 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"
 	m = "" if 'tx_id' in opts else "transaction data"
 	tx_data = get_lines_from_file(tx_file,m)
 	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)
 	qmsg("Successfully opened transaction file '%s'" % tx_file)
 
 
 	if 'tx_id' in opts:
 	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)
 		view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata)
 		sys.exit(0)
 		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]]
+	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,reply in "Vv")
 
 
-	if 'all_keys_from_file' in opts: other_inputs = inputs_data
+	# Start
+	other_addrs = list(set([i['address'] for i in inputs_data
+			if not parse_mmgen_label(i['account'])[0]]))
 
 
-	keys = keys_from_file
+	keys = get_keys_from_keylist(from_file['kldata'],other_addrs)
 
 
-	if other_inputs and not keys and not 'use_wallet_dat' in opts:
-		missing_keys_errormsg(other_inputs)
+	if other_addrs and not 'use_wallet_dat' in opts:
+		missing_keys_errormsg(other_addrs)
 		sys.exit(2)
 		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
+	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()])
 
 
-	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)
+	keys += check_maps_from_kafile(imap,"input",from_file['mmdata'],True)
+	check_maps_from_kafile(omap,"output",from_file['mmdata'])
 
 
-	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)
+	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 = [
 	sig_data = [
 		{"txid":i['txid'],"vout":i['vout'],"scriptPubKey":i['scriptPubKey']}
 		{"txid":i['txid'],"vout":i['vout'],"scriptPubKey":i['scriptPubKey']}
 			for i in inputs_data]
 			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']:
 	if sig_tx['complete']:
 		msg("OK")
 		msg("OK")

+ 53 - 4
mmgen/main_walletchk.py

@@ -25,7 +25,7 @@ import sys
 import mmgen.config as g
 import mmgen.config as g
 from mmgen.Opts import *
 from mmgen.Opts import *
 from mmgen.util import *
 from mmgen.util import *
-from mmgen.crypto import get_seed_retry,wallet_to_incog_data
+from mmgen.crypto import *
 
 
 help_data = {
 help_data = {
 	'prog_name': g.prog_name,
 	'prog_name': g.prog_name,
@@ -47,6 +47,7 @@ help_data = {
 -X, --export-incog-hex Export wallet to incognito hexadecimal format
 -X, --export-incog-hex Export wallet to incognito hexadecimal format
 -G, --export-incog-hidden=f,o  Hide incognito data in existing file 'f'
 -G, --export-incog-hidden=f,o  Hide incognito data in existing file 'f'
                        at offset 'o' (comma-separated)
                        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
 -m, --export-mnemonic  Export the wallet's mnemonic to file
 -s, --export-seed      Export the wallet's seed to file
 -s, --export-seed      Export the wallet's seed to file
 """.format(g=g),
 """.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)
 opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
 if 'export_incog_hidden' in opts or 'export_incog_hex' in opts:
 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:
 	if "export_incog_hidden" in opts:
 		export_to_hidden_incog(incog_enc,opts)
 		export_to_hidden_incog(incog_enc,opts)
 	else:
 	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" % (
 		fn = "%s-%s-%s[%s,%s].%s" % (
 			seed_id, key_id, iv_id, seed_len, preset,
 			seed_id, key_id, iv_id, seed_len, preset,
 			g.incog_hex_ext if "export_incog_hex" in opts else g.incog_ext
 			g.incog_hex_ext if "export_incog_hex" in opts else g.incog_ext
@@ -102,8 +152,7 @@ else:
 if 'export_mnemonic' in opts:
 if 'export_mnemonic' in opts:
 	wl = get_default_wordlist()
 	wl = get_default_wordlist()
 	from mmgen.mnemonic import get_mnemonic_from_seed
 	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)
 	fn = "%s.%s" % (make_chksum_8(seed).upper(), g.mn_ext)
 	write_to_file_or_stdout(fn, " ".join(mn)+"\n", opts, "mnemonic data")
 	write_to_file_or_stdout(fn, " ".join(mn)+"\n", opts, "mnemonic data")
 
 

+ 33 - 5
mmgen/main_walletgen.py

@@ -55,6 +55,9 @@ help_data = {
                            i.e. a "brainwallet", using seed length 'l' and
                            i.e. a "brainwallet", using seed length 'l' and
                            hash preset 'p' (comma-separated)
                            hash preset 'p' (comma-separated)
 -g, --from-incog           Generate wallet from an incognito-format wallet
 -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
 -m, --from-mnemonic        Generate wallet from an Electrum-like mnemonic
 -s, --from-seed            Generate wallet from a seed in .{g.seed_ext} format
 -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),
 """.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)
 """.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)
 opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
 if 'show_hash_presets' in opts: show_hash_presets()
 if 'show_hash_presets' in opts: show_hash_presets()
@@ -121,16 +148,17 @@ else: usage(help_data)
 do_license_msg()
 do_license_msg()
 
 
 if 'from_brain' in opts and not g.quiet:
 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)),
 			g.proj_name, *get_from_brain_opt_params(opts)),
 		"continue")
 		"continue")
 
 
 for i in 'from_mnemonic','from_brain','from_seed','from_incog':
 for i in 'from_mnemonic','from_brain','from_seed','from_incog':
 	if infile or (i in opts):
 	if infile or (i in opts):
 		seed = get_seed_retry(infile,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
 		break
 else:
 else:
 	# Truncate random data for smaller seed lengths
 	# Truncate random data for smaller seed lengths
@@ -138,7 +166,7 @@ else:
 
 
 salt = sha256(get_random(128,opts)).digest()[:g.salt_len]
 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)
 passwd = get_new_passphrase("new {} wallet".format(g.proj_name), opts)
 
 

+ 49 - 38
mmgen/mnemonic.py

@@ -17,9 +17,13 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 """
 """
-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 = {
 wl_checksums = {
 	"electrum": '5ca31424',
 	"electrum": '5ca31424',
 	"tirosh":   '1a5faeff'
 	"tirosh":   '1a5faeff'
@@ -27,70 +31,77 @@ wl_checksums = {
 
 
 # These are the only base-1626 specific configs:
 # These are the only base-1626 specific configs:
 mn_base = 1626
 mn_base = 1626
-def mn_fill(mn):    return len(mn) * 8 / 3
-def mn_len(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 mn2hex_pad(mn):     return len(mn) * 8 / 3
+def hex2mn_pad(hexnum): return len(hexnum) * 3 / 8
 
 
-def baseNtohex(base,words,wordlist,fill=0):
+# Universal base-conversion routines:
+def baseNtohex(base,words,wordlist,pad=0):
 	deconv = \
 	deconv = \
 		[wordlist.index(words[::-1][i])*(base**i) for i in range(len(words))]
 		[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):
 	for n,w in enumerate(mn,1):
 		if w not in wl:
 		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
 	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))
+
+	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)
 
 
-	return seed
+	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):
 
 
-	from binascii import hexlify
+def get_mnemonic_from_seed(seed, wl, label="", verbose=False):
 
 
-	if print_info:
-		msg("Wordlist:    %s" % label.capitalize())
-		msg("Seed length: %s bits" % (len(seed) * 8))
-		msg("Seed:        %s" % hexlify(seed))
+	if len(seed)*8 not in g.seed_lens:
+		msg("%s: invalid seed length" % len(seed))
+		sys.exit(3)
+
+	from binascii import hexlify
 
 
 	hseed = 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)))
 		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("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)
 		sys.exit(3)
 
 
 	return mn
 	return mn
 
 
 
 
-def check_wordlist(wl_str,label):
-
-	wl = wl_str.strip().split("\n")
+def check_wordlist(wl,label):
 
 
 	print "Wordlist: %s" % label.capitalize()
 	print "Wordlist: %s" % label.capitalize()
 
 

+ 5 - 129
mmgen/tests/bitcoin.py

@@ -22,26 +22,14 @@ bitcoin.py:  Test suite for mmgen.bitcoin module
 import mmgen.bitcoin as b
 import mmgen.bitcoin as b
 from   mmgen.util import msg
 from   mmgen.util import msg
 from   mmgen.tests.test import *
 from   mmgen.tests.test import *
-from   binascii import hexlify, unhexlify
-
+from   binascii import hexlify
 import sys
 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):
 def keyconv_compare_randloop(loops, quiet=False):
 	for i in range(1,int(loops)+1):
 	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)
 		if not quiet: sys.stderr.write("-- %s --\n" % i)
 		ret = keyconv_compare(wif,quiet)
 		ret = keyconv_compare(wif,quiet)
@@ -55,7 +43,6 @@ def keyconv_compare_randloop(loops, quiet=False):
 	else:
 	else:
 		print "%s iterations completed" % i
 		print "%s iterations completed" % i
 
 
-
 def keyconv_compare(wif,quiet=False):
 def keyconv_compare(wif,quiet=False):
 	do_msg = nomsg if quiet else msg
 	do_msg = nomsg if quiet else msg
 	do_msg("WIF:               %s" % wif)
 	do_msg("WIF:               %s" % wif)
@@ -81,135 +68,24 @@ def keyconv_compare(wif,quiet=False):
 def _do_hextowif(hex_in,quiet=False):
 def _do_hextowif(hex_in,quiet=False):
 	do_msg = nomsg if quiet else msg
 	do_msg = nomsg if quiet else msg
 	do_msg("Input:        %s" % hex_in)
 	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)
 	do_msg("WIF encoded:  %s" % wif)
-	wif_dec = wiftohex(wif)
+	wif_dec = b.wiftohex(wif)
 	do_msg("WIF decoded:  %s" % wif_dec)
 	do_msg("WIF decoded:  %s" % wif_dec)
 	if hex_in != wif_dec:
 	if hex_in != wif_dec:
 		print "ERROR!  Decoded data doesn't match original data"
 		print "ERROR!  Decoded data doesn't match original data"
 		sys.exit(9)
 		sys.exit(9)
 	return wif
 	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))
 	r_hex = hexlify(get_random(32))
 
 
 	return _do_hextowif(r_hex,quiet)
 	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 = {
 tests = {
 	"keyconv_compare":          ['wif [str]','quiet [bool=False]'],
 	"keyconv_compare":          ['wif [str]','quiet [bool=False]'],
 	"keyconv_compare_randloop": ['iterations [int]','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)
 args = process_test_args(sys.argv, tests)

+ 46 - 23
mmgen/tool.py

@@ -39,9 +39,9 @@ def Vmsg_r(s):
 opts = {}
 opts = {}
 commands = {
 commands = {
 	"strtob58":     ['<string> [str]'],
 	"strtob58":     ['<string> [str]'],
+	"b58tostr":     ['<b58 number> [str]'],
 	"hextob58":     ['<hex number> [str]'],
 	"hextob58":     ['<hex number> [str]'],
 	"b58tohex":     ['<b58 number> [str]'],
 	"b58tohex":     ['<b58 number> [str]'],
-	"b58tostr":     ['<b58 number> [str]'],
 	"b58randenc":   [],
 	"b58randenc":   [],
 	"randhex":      ['nbytes [int=32]'],
 	"randhex":      ['nbytes [int=32]'],
 	"randwif":      ['compressed [bool=False]'],
 	"randwif":      ['compressed [bool=False]'],
@@ -51,6 +51,8 @@ commands = {
 	"hex2wif":      ['<private key in hex format> [str]', 'compressed [bool=False]'],
 	"hex2wif":      ['<private key in hex format> [str]', 'compressed [bool=False]'],
 	"hexdump":      ['<infile> [str]', 'cols [int=8]', 'line_nums [bool=True]'],
 	"hexdump":      ['<infile> [str]', 'cols [int=8]', 'line_nums [bool=True]'],
 	"unhexdump":    ['<infile> [str]'],
 	"unhexdump":    ['<infile> [str]'],
+	"hex2mn":       ['<hexadecimal string> [str]','wordlist [str="electrum"]'],
+	"mn2hex":       ['<mnemonic> [str]', 'wordlist [str="electrum"]'],
 	"mn_rand128":   ['wordlist [str="electrum"]'],
 	"mn_rand128":   ['wordlist [str="electrum"]'],
 	"mn_rand192":   ['wordlist [str="electrum"]'],
 	"mn_rand192":   ['wordlist [str="electrum"]'],
 	"mn_rand256":   ['wordlist [str="electrum"]'],
 	"mn_rand256":   ['wordlist [str="electrum"]'],
@@ -59,10 +61,11 @@ commands = {
 	"id8":          ['<infile> [str]'],
 	"id8":          ['<infile> [str]'],
 	"id6":          ['<infile> [str]'],
 	"id6":          ['<infile> [str]'],
 	"str2id6":      ['<string (spaces are ignored)> [str]'],
 	"str2id6":      ['<string (spaces are ignored)> [str]'],
-	"listaddresses":['minconf [int=1]', 'showempty [bool=False]'],
+	"listaddresses":['minconf [int=1]','showempty [bool=False]','pager [bool=False]'],
 	"getbalance":   ['minconf [int=1]'],
 	"getbalance":   ['minconf [int=1]'],
 	"viewtx":       ['<MMGen tx file> [str]','pager [bool=False]'],
 	"viewtx":       ['<MMGen tx file> [str]','pager [bool=False]'],
-	"check_addrfile": ['<MMGen addr file> [str]'],
+	"addrfile_chksum": ['<MMGen addr file> [str]'],
+	"keyaddrfile_chksum": ['<MMGen addr file> [str]'],
 	"find_incog_data": ['<file or device name> [str]','<Incog ID> [str]','keep_searching [bool=False]'],
 	"find_incog_data": ['<file or device name> [str]','<Incog ID> [str]','keep_searching [bool=False]'],
 	"hexreverse":   ['<hexadecimal string> [str]'],
 	"hexreverse":   ['<hexadecimal string> [str]'],
 	"sha256x2":     ['<str, hexstr or filename> [str]',
 	"sha256x2":     ['<str, hexstr or filename> [str]',
@@ -122,8 +125,9 @@ command_help = """
       * The encrypted file is indistinguishable from random data
       * The encrypted file is indistinguishable from random data
 
 
   {pnm}-specific operations:
   {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)
   id6          - generate 6-character {pnm} ID for a file (or stdin)
   id8          - generate 8-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
   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_rand256   - generate random 256-bit mnemonic
   mn_stats     - show stats for mnemonic wordlist
   mn_stats     - show stats for mnemonic wordlist
   mn_printlist - print 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
   IMPORTANT NOTE: Though {pnm} mnemonics use the Electrum wordlist, they're
   computed using a different algorithm and are NOT Electrum-compatible!
   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' %
 		Msg('"%s": invalid wordlist.  Valid choices: %s' %
 			(wordlist,'"'+'" "'.join(wordlists)+'"'))
 			(wordlist,'"'+'" "'.join(wordlists)+'"'))
 		sys.exit(1)
 		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):
 def do_random_mn(nbytes,wordlist):
 	r = get_random(nbytes,opts)
 	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)
 		print " ".join(mn)
 
 
 def mn_rand128(wordlist="electrum"): do_random_mn(16,wordlist)
 def mn_rand128(wordlist="electrum"): do_random_mn(16,wordlist)
 def mn_rand192(wordlist="electrum"): do_random_mn(24,wordlist)
 def mn_rand192(wordlist="electrum"): do_random_mn(24,wordlist)
 def mn_rand256(wordlist="electrum"): do_random_mn(32,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"):
 def mn_stats(wordlist="electrum"):
 	l = get_wordlist(wordlist)
 	l = get_wordlist(wordlist)
 	check_wordlist(l,wordlist)
 	check_wordlist(l,wordlist)
 
 
 def mn_printlist(wordlist="electrum"):
 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 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 id6(infile): print make_chksum_6(get_data_from_file(infile,dash=True))
 def str2id6(s):  print make_chksum_6("".join(s.split()))
 def str2id6(s):  print make_chksum_6("".join(s.split()))
 
 
 # List MMGen addresses and their balances:
 # 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
 	from mmgen.tx import connect_to_bitcoind,trim_exponent,is_mmgen_addr
 	c = connect_to_bitcoind()
 	c = connect_to_bitcoind()
 
 
@@ -349,7 +363,7 @@ def listaddresses(minconf=1,showempty=False):
 		max([len(k) for k in addrs.keys()]),
 		max([len(k) for k in addrs.keys()]),
 		max([len(str(addrs[k][1])) 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):
 	def s_mmgen(ma):
 		return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *ma.split("_"))
 		return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *ma.split("_"))
@@ -357,9 +371,13 @@ def listaddresses(minconf=1,showempty=False):
 	old_sid = ""
 	old_sid = ""
 	for k in sorted(addrs.keys(),key=s_mmgen):
 	for k in sorted(addrs.keys(),key=s_mmgen):
 		sid,num = k.split("_")
 		sid,num = k.split("_")
-		if old_sid and old_sid != sid: print
+		if old_sid and old_sid != sid: out.append("")
 		old_sid = sid
 		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):
 def getbalance(minconf=1):
@@ -390,10 +408,11 @@ def viewtx(infile,pager=False):
 	c = connect_to_bitcoind()
 	c = connect_to_bitcoind()
 	tx_data = get_lines_from_file(infile,"transaction data")
 	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)
 	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):
 def hexreverse(hex_str):
 	print ba.hexlify(decode_pretty_hexdump(hex_str)[::-1])
 	print ba.hexlify(decode_pretty_hexdump(hex_str)[::-1])
@@ -418,7 +437,7 @@ def pubkey2hexaddr(pubkeyhex):
 	print bitcoin.pubhex2hexaddr(pubkeyhex)
 	print bitcoin.pubhex2hexaddr(pubkeyhex)
 
 
 def pubkey2addr(pubkeyhex):
 def pubkey2addr(pubkeyhex):
-	print bitcoin.pubhex2addr(pubkeyhex)
+	print bitcoin.hexaddr2addr(bitcoin.pubhex2hexaddr(pubkeyhex))
 
 
 def privhex2addr(privkeyhex,compressed=False):
 def privhex2addr(privkeyhex,compressed=False):
 	print bitcoin.privnum2addr(int(privkeyhex,16),compressed)
 	print bitcoin.privnum2addr(int(privkeyhex,16),compressed)
@@ -444,7 +463,7 @@ def encrypt(infile,outfile="",hash_preset=''):
 def decrypt(infile,outfile="",hash_preset=''):
 def decrypt(infile,outfile="",hash_preset=''):
 	enc_d = get_data_from_file(infile,"encrypted data")
 	enc_d = get_data_from_file(infile,"encrypted data")
 	while True:
 	while True:
-		dec_d = mmgen_decrypt(enc_d,"user data","",opts)
+		dec_d = mmgen_decrypt(enc_d,"user data")
 		if dec_d: break
 		if dec_d: break
 		msg("Trying again...")
 		msg("Trying again...")
 	if outfile == '-':
 	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
 	ivsize,bsize,mod = g.aesctr_iv_len,4096,4096*8
 	n,carry = 0," "*ivsize
 	n,carry = 0," "*ivsize
 	f = os.open(filename,os.O_RDONLY)
 	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:
 	while True:
 		d = os.read(f,bsize)
 		d = os.read(f,bsize)
 		if not d: break
 		if not d: break

+ 219 - 581
mmgen/tx.py

@@ -26,82 +26,7 @@ from decimal import Decimal
 
 
 import mmgen.config as g
 import mmgen.config as g
 from mmgen.util import *
 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):
 def trim_exponent(n):
 	'''Remove exponent and trailing zeros.
 	'''Remove exponent and trailing zeros.
@@ -109,8 +34,6 @@ def trim_exponent(n):
 	d = Decimal(n)
 	d = Decimal(n)
 	return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
 	return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
 
 
-
-
 def is_btc_amt(amt):
 def is_btc_amt(amt):
 	# amt must be a string!
 	# amt must be a string!
 
 
@@ -134,196 +57,80 @@ def is_btc_amt(amt):
 
 
 	return trim_exponent(ret)
 	return trim_exponent(ret)
 
 
-
 def normalize_btc_amt(amt):
 def normalize_btc_amt(amt):
 	# amt must be a string!
 	# amt must be a string!
 	ret = is_btc_amt(amt)
 	ret = is_btc_amt(amt)
 	if ret: return ret
 	if ret: return ret
 	else:   sys.exit(3)
 	else:   sys.exit(3)
 
 
+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 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
+def is_mmgen_seed_id(s):
+	import re
+	return re.match(r"^[0123456789ABCDEF]{8}$",s) is not None
 
 
-		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")
+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
 
 
-		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)]
+def is_mmgen_addr(s):
+	seed_id,idx = split2(s,":")
+	return is_mmgen_seed_id(seed_id) and is_mmgen_idx(idx)
 
 
-		msg("\n".join(out) +"\n\n" + write_to_file_msg + options_msg)
-		write_to_file_msg = ""
+def is_btc_addr(s):
+	from mmgen.bitcoin import verify_addr
+	return verify_addr(s)
 
 
-		skip_prompt = False
+def is_b58_str(s):
+	from mmgen.bitcoin import b58a
+	for ch in s:
+		if ch not in b58a: return False
+	return True
 
 
-		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
+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
 
 
-			break
+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)
 
 
-		msg("\n")
-		if reply == 'q': break
+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
 
 
-	return tuple(unspent)
+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)
 
 
-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)
+	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):
 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']
 				total_in += j['amount']
 				addr = j['address']
 				addr = j['address']
 				mmid,label = parse_mmgen_label(j['account']) \
 				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 ""
 				mmid_str = ((34-len(addr))*" " + " (%s)" % mmid) if mmid else ""
 
 
 				for d in (
 				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")
 	o = out.encode("utf8")
 	if pager: do_pager(o)
 	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"
 	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
 		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 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)
+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
 
 
-	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
 	nchars = 24
-	return make_chksum_N(
-		" ".join(["{} {}".format(*d[:2]) for d in addr_data]), nchars, sep=True
-	)
-
-def check_addr_data_hash(seed_id,addr_data):
-	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")
+	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 parse_addrs_file(f):
 
 
-	lines = get_lines_from_file(f,"address data",trim_comments=True)
+def get_addr_data_hash(e,keys=False):
+	def s_addrdata(a): return int(a[0])
+	adata = [(k,e[k]) for k in e.keys()]
+	return make_addr_data_chksum(sorted(adata,key=s_addrdata),keys)
 
 
-	try:
-		seed_id,obrace = lines[0].split()
-	except:
-		msg("Invalid first line: '%s'" % lines[0])
-		sys.exit(3)
 
 
-	cbrace = lines[-1]
+def _parse_addrfile_body(lines,keys=False,check=False):
 
 
-	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]):
 			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)
 				sys.exit(3)
 
 
 			if not is_btc_addr(d[1]):
 			if not is_btc_addr(d[1]):
@@ -589,210 +269,112 @@ def parse_addrs_file(f):
 				sys.exit(3)
 				sys.exit(3)
 
 
 			if len(d) == 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)
+				comment = d[2]
+				check_addr_label(comment)
+			else:
+				comment = ""
 
 
+			ret.append((d[0],d[1],comment))
 
 
-def sign_transaction(c,tx_hex,tx_num_str,sig_data,keys=None):
+		return ret
 
 
-	if keys:
-		qmsg("%s keys total" % len(keys))
-		if g.debug: print "Keys:\n  %s" % "\n  ".join(keys)
+	def parse_key_lines(lines):
+		ret = []
+		for l in lines:
+			d = l.split(None,2)
 
 
-	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)
+			if d[0] != "wif:":
+				msg("Invalid key line in file: '%s'" % l)
+				sys.exit(3)
 
 
-	return sig_tx
+			if not is_btc_key(d[1]):
+				msg("'%s': invalid Bitcoin key" % d[1])
+				sys.exit(3)
 
 
-def get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts):
+			ret.append(d[1])
 
 
-	if seed_id in saved_seeds.keys():
-		return saved_seeds[seed_id]
+		return ret
 
 
-	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)
+	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)
 			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)
 
 
-		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 parse_addrfile(f,addr_data,keys=False):
+	return parse_addrfile_lines(
+				get_lines_from_file(f,"address data",trim_comments=True),
+					addr_data,keys)
 
 
-def sign_tx_with_bitcoind_wallet(c,tx_hex,tx_num_str,sig_data,keys,opts):
+def parse_addrfile_lines(lines,addr_data,keys=False,exit_on_error=True):
 
 
 	try:
 	try:
-		sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys)
+		seed_id,obrace = lines[0].split()
 	except:
 	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 preverify_keys(addrs_in, keys_in, mm_inputs):
-
-	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)')
-
-	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")
+		errmsg = "Invalid first line: '%s'" % lines[0]
 	else:
 	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)
+		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:
 		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))
+			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)
 		sys.exit(3)
+	else:
+		return False
 
 
-	if missing:
-		confirm_or_exit(txmsg['missing_mappings'] %
-				" ".join(missing),"continue")
-	else: qmsg("Address mappings OK")
 
 
+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 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 get_tx_comment_from_file(infile):
 def get_tx_comment_from_file(infile):
 	s = get_data_from_file(infile,"transaction comment")
 	s = get_data_from_file(infile,"transaction comment")
 	if is_valid_tx_comment(s, verbose=True):
 	if is_valid_tx_comment(s, verbose=True):
 		return s.decode("utf8").strip()
 		return s.decode("utf8").strip()
-	else: return False
-
+	else:
+		sys.exit(2)
 
 
 def get_tx_comment_from_user(comment=""):
 def get_tx_comment_from_user(comment=""):
-
 	try:
 	try:
 		while True:
 		while True:
 			s = my_raw_input("Comment: ",insert_txt=comment.encode("utf8"))
 			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):
 			if is_valid_tx_comment(s, verbose=True):
 				return s.decode("utf8")
 				return s.decode("utf8")
 	except KeyboardInterrupt:
 	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):
 def make_tx_data(metadata_fmt, tx_hex, inputs_data, b2m_map, comment):
 	from mmgen.bitcoin import b58encode
 	from mmgen.bitcoin import b58encode
 	s = (b58encode(comment.encode("utf8")),) if comment else ()
 	s = (b58encode(comment.encode("utf8")),) if comment else ()
 	lines = (metadata_fmt, tx_hex, repr(inputs_data), repr(b2m_map)) + s
 	lines = (metadata_fmt, tx_hex, repr(inputs_data), repr(b2m_map)) + s
 	return "\n".join(lines)+"\n"
 	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)

+ 39 - 114
mmgen/util.py

@@ -28,74 +28,33 @@ import mmgen.config as g
 
 
 def msg(s):    sys.stderr.write(s + "\n")
 def msg(s):    sys.stderr.write(s + "\n")
 def msg_r(s):  sys.stderr.write(s)
 def msg_r(s):  sys.stderr.write(s)
-def qmsg(s,alt=""):
+def qmsg(s,alt=False):
 	if g.quiet:
 	if g.quiet:
-		if alt: sys.stderr.write(alt + "\n")
+		if alt != False: sys.stderr.write(alt + "\n")
 	else: sys.stderr.write(s + "\n")
 	else: sys.stderr.write(s + "\n")
-def qmsg_r(s,alt=""):
+def qmsg_r(s,alt=False):
 	if g.quiet:
 	if g.quiet:
-		if alt: sys.stderr.write(alt)
+		if alt != False: sys.stderr.write(alt)
 	else: sys.stderr.write(s)
 	else: sys.stderr.write(s)
 def vmsg(s):
 def vmsg(s):
 	if g.verbose: sys.stderr.write(s + "\n")
 	if g.verbose: sys.stderr.write(s + "\n")
 def vmsg_r(s):
 def vmsg_r(s):
 	if g.verbose: sys.stderr.write(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! ##############################
-
-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()
-}
+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 ""
+
+	if what in "a":
+		return "" if n == 1 else "es"
+	if what in "k":
+		return "" if n == 1 else "s"
 
 
 def get_extension(f):
 def get_extension(f):
 	import os
 	import os
@@ -138,6 +97,11 @@ def _is_hex(s):
 	except: return False
 	except: return False
 	else: return True
 	else: return True
 
 
+def is_utf8(s):
+	try: s.decode("utf8")
+	except: return False
+	else: return True
+
 def match_ext(addr,ext):
 def match_ext(addr,ext):
 	return addr.split(".")[-1] == 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
 	r = 1 if len(data) % gw else 0
 	return "".join(
 	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]) +
 			hexlify(data[i*gw:i*gw+gw]) +
 			(" " if (i+1) % cols else "\n")
 			(" " if (i+1) % cols else "\n")
 				for i in range(len(data)/gw + r)
 				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):
 def decode_pretty_hexdump(data):
 	import re
 	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())))
 	return unhexlify("".join(("".join(lines).split())))
 
 
 def get_hash_params(hash_preset):
 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_outfile(f): return check_file_type_and_access(f,"output file")
 def check_outdir(f):  return check_file_type_and_access(f,"directory")
 def check_outdir(f):  return check_file_type_and_access(f,"directory")
 
 
-
 def _validate_addr_num(n):
 def _validate_addr_num(n):
 
 
 	try: n = int(n)
 	try: n = int(n)
@@ -258,7 +222,7 @@ def make_full_path(outdir,outfile):
 #	os.path.join() doesn't work?
 #	os.path.join() doesn't work?
 
 
 
 
-def parse_address_list(arg,sep=","):
+def parse_addr_idxs(arg,sep=","):
 
 
 	ret = []
 	ret = []
 
 
@@ -278,7 +242,7 @@ def parse_address_list(arg,sep=","):
 			if end < beg:
 			if end < beg:
 				msg("'%s-%s': end of range less than beginning" % (beg,end))
 				msg("'%s-%s': end of range less than beginning" % (beg,end))
 				return False
 				return False
-			for k in range(beg,end+1): ret.append(k)
+			ret.extend(range(beg,end+1))
 		else:
 		else:
 			msg("'%s': invalid argument for address range" % i)
 			msg("'%s': invalid argument for address range" % i)
 			return False
 			return False
@@ -327,11 +291,8 @@ def confirm_or_false(message, question, expect="YES"):
 	p = question+"  "+conf_msg if question[0].isupper() else \
 	p = question+"  "+conf_msg if question[0].isupper() else \
 		"Are you sure you want to %s?\n%s" % (question,conf_msg)
 		"Are you sure you want to %s?\n%s" % (question,conf_msg)
 
 
-	ret = True if my_raw_input(p).strip() == expect else False
-
 	vmsg("")
 	vmsg("")
-	return ret
-
+	return my_raw_input(p).strip() == expect
 
 
 
 
 def write_to_stdout(data, what, confirm=True):
 def write_to_stdout(data, what, confirm=True):
@@ -341,7 +302,9 @@ def write_to_stdout(data, what, confirm=True):
 		try:
 		try:
 			import os
 			import os
 			of = os.readlink("/proc/%d/fd/1" % os.getpid())
 			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:
 		except:
 			msg("Redirecting output to file")
 			msg("Redirecting output to file")
 	sys.stdout.write(data)
 	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():
 	if 'stdout' in opts or not sys.stdout.isatty():
 		write_to_stdout(data, what, confirm=True)
 		write_to_stdout(data, what, confirm=True)
 	else:
 	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
 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)
 		seed_id,key_id,seed_len,hash_preset,g.wallet_ext)
 
 
 	d = "\n".join((chk,)+lines)+"\n"
 	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)
 		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):
 def _get_words_from_user(prompt, opts):
 	# split() also strips
 	# 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)
 	if g.debug: print "Sanitized input: [%s]" % " ".join(words)
 	return words
 	return words
 
 
@@ -610,7 +570,7 @@ def get_seed_from_seed_data(words):
 			msg("Invalid b58 number: %s" % val)
 			msg("Invalid b58 number: %s" % val)
 			return False
 			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
 		return seed
 	else:
 	else:
 		msg("Invalid checksum for {} seed".format(g.proj_name))
 		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'],
 		return get_data_from_file(opts['passwd_file'],
 				"passphrase").strip("\r\n")
 				"passphrase").strip("\r\n")
 	else:
 	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):
 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)
 		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
 from mmgen.term import kb_hold_protect,get_char
 
 
 def get_hash_preset_from_user(hp='3',what="data"):
 def get_hash_preset_from_user(hp='3',what="data"):
 	p = "Enter hash preset for %s, or ENTER to accept the default ('%s'): " \
 	p = "Enter hash preset for %s, or ENTER to accept the default ('%s'): " \
-		 % (what,hp)
+			% (what,hp)
 	while True:
 	while True:
 		ret = my_raw_input(p)
 		ret = my_raw_input(p)
 		if ret:
 		if ret:
@@ -761,5 +688,3 @@ def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False):
 
 
 		if verbose: msg("\nInvalid reply")
 		if verbose: msg("\nInvalid reply")
 		else: msg_r("\r")
 		else: msg_r("\r")
-
-