Browse Source

New 'crypto.py' module for higher-level functions from 'util.py'.
'mmgen-keygen', 'mmgen-txsign': encrypted keylist support

philemon 10 years ago
parent
commit
26aa2eb64b
14 changed files with 682 additions and 626 deletions
  1. 14 2
      mmgen-addrgen
  2. 1 1
      mmgen-addrimport
  3. 1 0
      mmgen-passchg
  4. 3 3
      mmgen-pywallet
  5. 13 3
      mmgen-txsign
  6. 1 0
      mmgen-walletchk
  7. 1 0
      mmgen-walletgen
  8. 2 2
      mmgen/Opts.py
  9. 394 0
      mmgen/crypto.py
  10. 3 2
      mmgen/license.py
  11. 45 6
      mmgen/term.py
  12. 24 37
      mmgen/tool.py
  13. 3 1
      mmgen/tx.py
  14. 177 569
      mmgen/util.py

+ 14 - 2
mmgen-addrgen

@@ -19,7 +19,7 @@
 """
 mmgen-addrgen: Generate a list or range of addresses from a mmgen
                deterministic wallet.
-               Call as 'btc-keygen' to allow key generation as well.
+               Call as 'btc-keygen' to allow key generation.
 """
 
 import sys
@@ -28,6 +28,7 @@ import mmgen.config as g
 from mmgen.Opts import *
 from mmgen.license import *
 from mmgen.util import *
+from mmgen.crypto import *
 from mmgen.addr import *
 from mmgen.tx import make_addr_data_chksum
 
@@ -149,7 +150,7 @@ seed_id = make_chksum_8(seed)
 for l in (
 	('flat_list', 'no_addresses'),
 	('flat_list', 'b16'),
-): check_incompatible_opts(opts,l)
+): warn_incompatible_opts(opts,l)
 
 opts['gen_what'] = \
 	("addrs") if what == "addresses" else (
@@ -163,6 +164,17 @@ addr_data_str    = format_addr_data(
 
 outfile_base = "{}[{}]".format(seed_id, fmt_addr_idxs(addr_idxs))
 
+if 'flat_list' in opts:
+	confirm = False if g.quiet else True
+	outfile = "%s.%s" % (outfile_base,g.keylist_ext)
+	if (user_confirm("Encrypt key list?")):
+		enc_data = mmgen_encrypt(addr_data_str,"",opts)
+		outfile += "."+g.mmenc_ext
+		write_to_file(outfile,enc_data,opts,"encrypted key list",confirm,True)
+	else:
+		write_to_file(outfile,addr_data_str,opts,"key list",confirm,True)
+	sys.exit()
+
 # Output data:
 if 'stdout' in opts:
 	confirm = True if (what == "keys" and not g.quiet) else False

+ 1 - 1
mmgen-addrimport

@@ -56,7 +56,7 @@ else:
 
 if 'addrlist' in opts:
 	lines = get_lines_from_file(opts['addrlist'],"non-mmgen addresses",
-			remove_comments=True)
+			trim_comments=True)
 	addr_data += [(None,l) for l in lines]
 
 from mmgen.bitcoin import verify_addr

+ 1 - 0
mmgen-passchg

@@ -23,6 +23,7 @@ mmgen-passchg: Change a mmgen deterministic wallet's passphrase, label or
 import sys
 from mmgen.Opts import *
 from mmgen.util import *
+from mmgen.crypto import *
 import mmgen.config as g
 
 help_data = {

+ 3 - 3
mmgen-pywallet

@@ -86,8 +86,8 @@ help_data = {
 }
 
 opts,cmd_args = parse_opts(sys.argv,help_data)
-from mmgen.Opts import check_incompatible_opts
-check_incompatible_opts(opts,('json','keys','addrs','keysforaddrs'))
+from mmgen.Opts import warn_incompatible_opts
+warn_incompatible_opts(opts,('json','keys','addrs','keysforaddrs'))
 
 if len(cmd_args) == 1:
 	from mmgen.util import check_infile
@@ -1653,7 +1653,7 @@ elif 'addrs' in opts:
 
 elif 'keysforaddrs' in opts:
 	from mmgen.util import get_lines_from_file
-	usr_addrs = set(get_lines_from_file(opts['keysforaddrs'],"addresses",remove_comments=True))
+	usr_addrs = set(get_lines_from_file(opts['keysforaddrs'],"addresses",trim_comments=True))
 	data = [i['sec'] for i in json_db['keys'] if i['addr'] in usr_addrs]
 	ext,what = "keys","private keys"
 	if len(data) < len(usr_addrs):

+ 13 - 3
mmgen-txsign

@@ -92,7 +92,7 @@ opts,infiles = parse_opts(sys.argv,help_data)
 for l in (
 ('tx_id', 'info'),
 ('keys_from_file','all_keys_from_file')
-): check_incompatible_opts(opts,l)
+): warn_incompatible_opts(opts,l)
 
 if "quiet" in opts: g.quiet = True
 if 'from_incog_hex' in opts or 'from_incog_hidden' in opts:
@@ -114,8 +114,18 @@ infiles  = list(set(infiles) - set(tx_files) - set(addrfiles))
 if not "info" in opts: do_license_msg(immed=True)
 
 if 'keys_from_file' in opts:
-	keys_from_file = get_lines_from_file(opts['keys_from_file'],"key data",
-						remove_comments=True)
+	from mmgen.crypto import mmgen_decrypt
+	fn = opts['keys_from_file']
+	if get_extension(fn) == g.mmenc_ext:
+		enc_d = get_data_from_file(fn,"encrypted keylist")
+		dec_d = mmgen_decrypt(enc_d,"",opts)
+		if dec_d:
+			keys_from_file = remove_comments(dec_d.split("\n"))
+		else:
+			msg("Decryption of encrypted keylist failed")
+			sys.exit(2)
+	else:
+		keys_from_file = get_lines_from_file(fn,"key data",trim_comments=True)
 else: keys_from_file = []
 
 for tx_file in tx_files:

+ 1 - 0
mmgen-walletchk

@@ -24,6 +24,7 @@ import sys
 import mmgen.config as g
 from mmgen.Opts import *
 from mmgen.util import *
+from mmgen.crypto import get_seed_from_wallet,wallet_to_incog_data
 
 help_data = {
 	'prog_name': g.prog_name,

+ 1 - 0
mmgen-walletgen

@@ -26,6 +26,7 @@ import mmgen.config as g
 from mmgen.Opts import *
 from mmgen.license import *
 from mmgen.util import *
+from mmgen.crypto import *
 
 help_data = {
 	'prog_name': g.prog_name,

+ 2 - 2
mmgen/Opts.py

@@ -29,7 +29,7 @@ def print_version_info():
 Copyright (C) {g.Cdates} by {g.author} {g.email}.
 """.format(g=g).strip()
 
-def check_incompatible_opts(opts,incompat_list):
+def warn_incompatible_opts(opts,incompat_list):
 	bad = [k for k in opts.keys() if k in incompat_list]
 	if len(bad) > 1:
 		msg("Mutually exclusive options: %s" % " ".join(
@@ -55,7 +55,7 @@ def parse_opts(argv,help_data):
 	('export_incog','export_incog_hex','export_incog_hidden','export_mnemonic',
 	 'export_seed'),
 	('quiet','verbose')
-	): check_incompatible_opts(opts,l)
+	): warn_incompatible_opts(opts,l)
 
 	# check_opts() doesn't touch opts[]
 	if not check_opts(opts,long_opts): sys.exit(1)

+ 394 - 0
mmgen/crypto.py

@@ -0,0 +1,394 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C) 2013-2014 by philemon <mmgen-py@yandex.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+crypto.py:  Cryptographic and related routines for the mmgen-tool utility
+"""
+
+import sys
+from binascii import hexlify
+from hashlib import sha256
+
+import mmgen.config as g
+from mmgen.util import *
+from mmgen.term import get_char
+
+def encrypt_seed(seed, key):
+	return encrypt_data(seed, key, iv=1, what="seed")
+
+
+def decrypt_seed(enc_seed, key, seed_id, key_id):
+
+	vmsg("Checking key...")
+	chk1 = make_chksum_8(key)
+	if key_id:
+		if not compare_checksums(chk1, "of key", key_id, "in header"):
+			msg("Incorrect passphrase")
+			return False
+
+	dec_seed = decrypt_data(enc_seed, key, iv=1, what="seed")
+
+	chk2 = make_chksum_8(dec_seed)
+
+	if seed_id:
+		if compare_checksums(chk2,"of decrypted seed",seed_id,"in header"):
+			qmsg("Passphrase is OK")
+		else:
+			if not g.debug:
+				msg_r("Checking key ID...")
+				if compare_checksums(chk1, "of key", key_id, "in header"):
+					msg("Key ID is correct but decryption of seed failed")
+				else:
+					msg("Incorrect passphrase")
+
+			return False
+#	else:
+#		qmsg("Generated IDs (Seed/Key): %s/%s" % (chk2,chk1))
+
+	if g.debug: print "Decrypted seed: %s" % hexlify(dec_seed)
+
+	return dec_seed
+
+
+def encrypt_data(data, key, iv=1, what="data", verify=True):
+	"""
+	Encrypt arbitrary data using AES256 in counter mode
+	"""
+
+	# 192-bit seed is 24 bytes -> not multiple of 16.  Must use MODE_CTR
+	from Crypto.Cipher import AES
+	from Crypto.Util import Counter
+
+	vmsg("Encrypting %s" % what)
+
+	c = AES.new(key, AES.MODE_CTR,
+			counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
+	enc_data = c.encrypt(data)
+
+	if verify:
+		vmsg_r("Performing a test decryption of the %s..." % what)
+
+		c = AES.new(key, AES.MODE_CTR,
+				counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
+		dec_data = c.decrypt(enc_data)
+
+		if dec_data == data: vmsg("done\n")
+		else:
+			msg("ERROR.\nDecrypted %s doesn't match original %s" % (what,what))
+			sys.exit(2)
+
+	return enc_data
+
+
+def decrypt_data(enc_data, key, iv=1, what="data"):
+
+	vmsg("Decrypting %s with key..." % what)
+
+	from Crypto.Cipher import AES
+	from Crypto.Util import Counter
+
+	c = AES.new(key, AES.MODE_CTR,
+			counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
+
+	return c.decrypt(enc_data)
+
+
+def scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32):
+
+	# Buflen arg is for brainwallets only, which use this function to generate
+	# the seed directly.
+
+	N,r,p = get_hash_params(hash_preset)
+
+	import scrypt
+	return scrypt.hash(passwd, salt, 2**N, r, p, buflen=buflen)
+
+
+def make_key(passwd, salt, hash_preset, what="key"):
+
+	vmsg_r("Generating %s.  Please wait..." % what)
+	key = scrypt_hash_passphrase(passwd, salt, hash_preset)
+	vmsg("done")
+	if g.debug: print "Key: %s" % hexlify(key)
+	return key
+
+
+def get_random_data_from_user(uchars):
+
+	if g.quiet: msg("Enter %s random symbols" % uchars)
+	else:       msg(cmessages['usr_rand_notice'] % uchars)
+
+	prompt = "You may begin typing.  %s symbols left: "
+	msg_r(prompt % uchars)
+
+	import time
+	# time.clock() always returns zero, so we'll use time.time()
+	saved_time = time.time()
+
+	key_data,time_data = "",[]
+
+	for i in range(uchars):
+		key_data += get_char(immed_chars="ALL")
+		msg_r("\r" + prompt % (uchars - i - 1))
+		now = time.time()
+		time_data.append(now - saved_time)
+		saved_time = now
+
+	if g.quiet: msg_r("\r")
+	else: msg_r("\rThank you.  That's enough.%s\n\n" % (" "*18))
+
+	fmt_time_data = ["{:.22f}".format(i) for i in time_data]
+
+	if g.debug:
+		msg("\nUser input:\n%s\nKeystroke time intervals:\n%s\n" %
+				(key_data,"\n".join(fmt_time_data)))
+
+	prompt = "User random data successfully acquired.  Press ENTER to continue"
+	prompt_and_get_char(prompt,"",enter_ok=True)
+
+	return key_data+"".join(fmt_time_data)
+
+
+def get_random(length,opts):
+	from Crypto import Random
+	os_rand = Random.new().read(length)
+	if 'usr_randchars' in opts and opts['usr_randchars'] not in (0,-1):
+		kwhat = "a key from random data with "
+		if not g.user_entropy:
+			g.user_entropy = sha256(
+				get_random_data_from_user(opts['usr_randchars'])).digest()
+			kwhat += "user entropy"
+		else:
+			kwhat += "saved user entropy"
+		key = make_key(g.user_entropy, "", '2', what=kwhat)
+		return encrypt_data(os_rand,key,what="random data",verify=False)
+	else:
+		return os_rand
+
+
+def get_seed_from_wallet(
+		infile,
+		opts,
+		prompt="Enter {} wallet passphrase: ".format(g.proj_name),
+		silent=False
+		):
+
+	wdata = get_data_from_wallet(infile,silent=silent)
+	label,metadata,hash_preset,salt,enc_seed = wdata
+
+	if g.verbose: display_control_data(*wdata)
+
+	passwd = get_mmgen_passphrase(prompt,opts)
+
+	key = make_key(passwd, salt, hash_preset)
+
+	return decrypt_seed(enc_seed, key, metadata[0], metadata[1])
+
+
+def get_seed_from_incog_wallet(
+		infile,
+		opts,
+		prompt="Enter {} wallet passphrase: ".format(g.proj_name),
+		silent=False,
+		hex_input=False
+	):
+
+	what = "incognito wallet data"
+
+	if "from_incog_hidden" in opts:
+		d = get_hidden_incog_data(opts)
+	else:
+		d = get_data_from_file(infile,what)
+		if hex_input:
+			try:
+				d = unhexlify("".join(d.split()).strip())
+			except:
+				msg("Data in file '%s' is not in hexadecimal format" % infile)
+				sys.exit(2)
+		# File could be of invalid length, so check:
+		valid_dlens = [i/8 + g.aesctr_iv_len + g.salt_len for i in g.seed_lens]
+		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]))
+				)
+			return False
+
+	iv, enc_incog_data = d[0:g.aesctr_iv_len], d[g.aesctr_iv_len:]
+
+	msg("Incog ID: %s (IV ID: %s)" % (make_iv_chksum(iv),make_chksum_8(iv)))
+	qmsg("Check the applicable value against your records.")
+	vmsg(cmessages['incog_iv_id_hidden' if "from_incog_hidden" in opts
+			else 'incog_iv_id'])
+
+	passwd = get_mmgen_passphrase(prompt,opts)
+
+	qmsg("Configured hash presets: %s" % " ".join(sorted(g.hash_presets)))
+	while True:
+		p = "Enter hash preset for %s wallet (default='%s'): "
+		hp = my_raw_input(p % (g.proj_name, g.hash_preset))
+		if not hp:
+			hp = g.hash_preset; break
+		elif hp in g.hash_presets:
+			break
+		msg("%s: Invalid hash preset" % hp)
+
+	# 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)
+
+	salt,enc_seed = d[0:g.salt_len], d[g.salt_len:]
+
+	key = make_key(passwd, salt, hp, "main key")
+	vmsg("Key ID: %s" % make_chksum_8(key))
+
+	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'])
+
+	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("Enter mmgen passphrase: ",opts)
+	key = make_key(passwd, salt, preset, "main key")
+	# We don't need the seed; just do this to verify password.
+	if decrypt_seed(enc_seed, key, seed_id, key_id) == False:
+		sys.exit(2)
+
+	iv = get_random(g.aesctr_iv_len,opts)
+	iv_id = make_iv_chksum(iv)
+	msg("Incog ID: %s" % iv_id)
+
+	# IV is used BOTH to initialize counter and to salt password!
+	key = make_key(passwd, iv, preset, "wrapper key")
+	m = "incog data"
+	wrap_enc = encrypt_data(salt + enc_seed, key, int(hexlify(iv),16), m)
+
+	return iv+wrap_enc,seed_id,key_id,iv_id,preset
+
+
+def get_seed(infile,opts,silent=False):
+
+	ext = get_extension(infile)
+
+	if   ext == g.mn_ext:           source = "mnemonic"
+	elif ext == g.brain_ext:        source = "brainwallet"
+	elif ext == g.seed_ext:         source = "seed"
+	elif ext == g.wallet_ext:       source = "wallet"
+	elif ext == g.incog_ext:        source = "incognito wallet"
+	elif ext == g.incog_hex_ext:    source = "incognito wallet"
+	elif 'from_mnemonic'  in opts: source = "mnemonic"
+	elif 'from_brain'     in opts: source = "brainwallet"
+	elif 'from_seed'      in opts: source = "seed"
+	elif 'from_incog'     in opts: source = "incognito wallet"
+	else:
+		if infile: msg(
+			"Invalid file extension for file: %s\nValid extensions: '.%s'" %
+			(infile, "', '.".join(g.seedfile_exts)))
+		else: msg("No seed source type specified and no file supplied")
+		sys.exit(2)
+
+	if source == "mnemonic":
+		prompt = "Enter mnemonic: "
+		words = get_words(infile,"mnemonic data",prompt,opts)
+		wl = get_default_wordlist()
+		from mmgen.mnemonic import get_seed_from_mnemonic
+		seed = get_seed_from_mnemonic(words,wl)
+	elif source == "brainwallet":
+		if 'from_brain' not in opts:
+			msg("'--from-brain' parameters must be specified for brainwallet file")
+			sys.exit(2)
+		prompt = "Enter brainwallet passphrase: "
+		words = get_words(infile,"brainwallet data",prompt,opts)
+		seed = _get_seed_from_brain_passphrase(words,opts)
+	elif source == "seed":
+		prompt = "Enter seed in %s format: " % g.seed_ext
+		words = get_words(infile,"seed data",prompt,opts)
+		seed = get_seed_from_seed_data(words)
+	elif source == "wallet":
+		seed = get_seed_from_wallet(infile, opts, silent=silent)
+	elif source == "incognito wallet":
+		h = True if ext == g.incog_hex_ext or 'from_incog_hex' in opts else False
+		seed = get_seed_from_incog_wallet(infile, opts, silent=silent, hex_input=h)
+
+
+	if infile and not seed and (
+		source == "seed" or source == "mnemonic" or source == "incognito wallet"):
+		msg("Invalid %s file '%s'" % (source,infile))
+		sys.exit(2)
+
+	if g.debug: print "Seed: %s" % hexlify(seed)
+
+	return seed
+
+
+# Repeat if entered data is invalid
+def get_seed_retry(infile,opts):
+	silent = False
+	while True:
+		seed = get_seed(infile,opts,silent=silent)
+		silent = True
+		if seed: return seed
+
+
+def _get_seed_from_brain_passphrase(words,opts):
+	bp = " ".join(words)
+	if g.debug: print "Sanitized brain passphrase: %s" % bp
+	seed_len,hash_preset = get_from_brain_opt_params(opts)
+	if g.debug: print "Brainwallet l = %s, p = %s" % (seed_len,hash_preset)
+	vmsg_r("Hashing brainwallet data.  Please wait...")
+	# Use buflen arg of scrypt.hash() to get seed of desired length
+	seed = scrypt_hash_passphrase(bp, "", hash_preset, buflen=seed_len/8)
+	vmsg("Done")
+	return seed
+
+
+# Vars for mmgen_*crypt functions only
+salt_len,sha256_len,nonce_len = 32,32,32
+
+def mmgen_encrypt(data,hash_preset,opts):
+	salt,iv,nonce = get_random(salt_len,opts),\
+		get_random(g.aesctr_iv_len,opts), get_random(nonce_len,opts)
+	hp,m = (hash_preset,"user-requested") if hash_preset else ('3',"default")
+	qmsg("Using %s hash preset of '%s'" % (m,hp))
+	passwd = get_new_passphrase("passphrase",{})
+	key = make_key(passwd, salt, hp)
+	enc_d = encrypt_data(sha256(nonce+data).digest() + nonce + data, key,
+				int(hexlify(iv),16))
+	return salt+iv+enc_d
+
+
+def mmgen_decrypt(data,hash_preset,opts):
+	dstart = salt_len + g.aesctr_iv_len
+	salt,iv,enc_d = data[:salt_len],data[salt_len:dstart],data[dstart:]
+	hp,m = (hash_preset,"user-requested") if hash_preset else ('3',"default")
+	qmsg("Using %s hash preset of '%s'" % (m,hp))
+	passwd = get_mmgen_passphrase("Enter passphrase: ",{})
+	key = make_key(passwd, salt, hp)
+	dec_d = decrypt_data(enc_d, key, int(hexlify(iv),16))
+	if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
+		return dec_d[sha256_len+nonce_len:]
+	else:
+		msg("Incorrect passphrase or hash preset")
+		return False

+ 3 - 2
mmgen/license.py

@@ -20,7 +20,8 @@ license.py:  Show the license
 """
 
 import sys
-from mmgen.util import msg, msg_r, get_char
+from mmgen.util import msg, msg_r
+from mmgen.term import get_char
 import mmgen.config as g
 
 gpl = {
@@ -595,7 +596,7 @@ def do_license_msg(immed=False):
 	while True:
 		reply = get_char(prompt, immed_chars="wc" if immed else "")
 		if reply == 'w':
-			from mmgen.util import do_pager
+			from mmgen.term import do_pager
 			do_pager(gpl['conditions'])
 		elif reply == 'c':
 			msg(""); break

+ 45 - 6
mmgen/term.py

@@ -20,9 +20,7 @@ term.py:  Terminal-handling routines for the mmgen suite
 """
 
 import sys, os, struct
-
-def msg(s):   sys.stderr.write(s + "\n")
-def msg_r(s): sys.stderr.write(s)
+from mmgen.util import msg, msg_r
 
 def _kb_hold_protect_unix():
 
@@ -122,7 +120,6 @@ def _get_terminal_size_linux():
 	def ioctl_GWINSZ(fd):
 		try:
 			import fcntl
-			import termios
 			cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
 			return cr
 		except:
@@ -165,18 +162,23 @@ def _get_terminal_size_mswin():
 	except:
 		return 80,25
 
+def mswin_dummy_flush(fd,termconst): pass
+
 try:
 	import tty, termios
 	from select import select
 	get_char = _get_keypress_unix
 	kb_hold_protect = _kb_hold_protect_unix
 	get_terminal_size = _get_terminal_size_linux
+	myflush = termios.tcflush
+# call: myflush(sys.stdin, termios.TCIOFLUSH)
 except:
 	try:
 		import msvcrt, time
 		get_char = _get_keypress_mswin
 		kb_hold_protect = _kb_hold_protect_mswin
 		get_terminal_size = _get_terminal_size_mswin
+		myflush = mswin_dummy_flush
 	except:
 		if not sys.platform.startswith("linux") \
 				and not sys.platform.startswith("win"):
@@ -186,5 +188,42 @@ except:
 			msg("Unable to set terminal mode")
 		sys.exit(2)
 
-if __name__ == "__main__":
-	print "columns: {}, rows: {}".format(*get_terminal_size())
+
+def do_pager(text):
+
+	pagers = ["less","more"]
+	shell = False
+
+	from os import environ
+
+# Hack for MS Windows command line (i.e. non CygWin) environment
+# When 'shell' is true, Windows aborts the calling program if executable
+# not found.
+# When 'shell' is false, an exception is raised, invoking the fallback
+# 'print' instead of the pager.
+# We risk assuming that "more" will always be available on a stock
+# Windows installation.
+	if sys.platform.startswith("win") and 'HOME' not in environ:
+		shell = True
+		pagers = ["more"]
+
+	if 'PAGER' in environ and environ['PAGER'] != pagers[0]:
+		pagers = [environ['PAGER']] + pagers
+
+	for pager in pagers:
+		end = "" if pager == "less" else "\n(end of text)\n"
+		try:
+			from subprocess import Popen, PIPE, STDOUT
+			p = Popen([pager], stdin=PIPE, shell=shell)
+		except: pass
+		else:
+			try:
+				p.communicate(text+end+"\n")
+			except KeyboardInterrupt:
+				# Has no effect.  Why?
+				if pager != "less":
+					msg("\n(User interrupt)\n")
+			finally:
+				msg_r("\r")
+				break
+	else: print text+end

+ 24 - 37
mmgen/tool.py

@@ -24,6 +24,7 @@ import mmgen.bitcoin as bitcoin
 import binascii as ba
 
 import mmgen.config as g
+from mmgen.crypto import *
 from mmgen.util import *
 from mmgen.tx import *
 
@@ -55,7 +56,8 @@ commands = {
 	"mn_printlist": ['wordlist [str="electrum"]'],
 	"id8":          ['<infile> [str]'],
 	"id6":          ['<infile> [str]'],
-	"listaddresses": ['minconf [int=1]', 'showempty [bool=False]'],
+	"str2id6":      ['<string (spaces are ignored)> [str]'],
+	"listaddresses":['minconf [int=1]', 'showempty [bool=False]'],
 	"getbalance":   ['minconf [int=1]'],
 	"viewtx":       ['<MMGen tx file> [str]'],
 	"check_addrfile": ['<MMGen addr file> [str]'],
@@ -119,8 +121,9 @@ command_help = """
   {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
-  id6          - generate 6-character {pnm} ID checksum for file (or stdin)
-  id8          - generate 8-character {pnm} ID checksum for 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)
+  str2id6      - generate 6-character {pnm} ID for a string, ignoring spaces
 
   Mnemonic operations (choose "electrum" (default), "tirosh" or "all"
   wordlists):
@@ -301,6 +304,7 @@ def mn_printlist(wordlist="electrum"):
 
 def id8(infile): print make_chksum_8(get_data_from_file(infile,dash=True))
 def id6(infile): print make_chksum_6(get_data_from_file(infile,dash=True))
+def str2id6(s):  print make_chksum_6("".join(s.split()))
 
 # List MMGen addresses and their balances:
 def listaddresses(minconf=1,showempty=False):
@@ -410,48 +414,31 @@ def wif2hex(wif,compressed=False):
 def hex2wif(hexpriv,compressed=False):
 	print bitcoin.hextowif(hexpriv,compressed)
 
-salt_len,sha256_len,nonce_len = 32,32,32
 
 def encrypt(infile,outfile="",hash_preset=''):
-	d = get_data_from_file(infile,"data for encryption")
-	salt,iv,nonce = get_random(salt_len,opts),\
-		get_random(g.aesctr_iv_len,opts), get_random(nonce_len,opts)
-	hp,m = (hash_preset,"user-requested") if hash_preset else ('3',"default")
-	qmsg("Using %s hash preset of '%s'" % (m,hp))
-	passwd = get_new_passphrase("passphrase",{})
-	key = make_key(passwd, salt, hp)
-	from hashlib import sha256
-	enc_d = encrypt_data(sha256(nonce+d).digest() + nonce + d, key,
-				int(ba.hexlify(iv),16))
-	if outfile == '-':  sys.stdout.write(salt+iv+enc_d)
+	data = get_data_from_file(infile,"data for encryption")
+	enc_d = mmgen_encrypt(data,hash_preset,opts)
+	if outfile == '-':
+		write_to_stdout(enc_d,"encrypted data",confirm=True)
 	else:
 		if not outfile:
 			outfile = os.path.basename(infile) + "." + g.mmenc_ext
-		write_to_file(outfile, salt+iv+enc_d, opts,"encrypted data",True,True)
+		write_to_file(outfile, enc_d, opts,"encrypted data",True,True)
+
 
 def decrypt(infile,outfile="",hash_preset=''):
-	d = get_data_from_file(infile,"encrypted data")
-	dstart = salt_len + g.aesctr_iv_len
-	salt,iv,enc_d = d[:salt_len],d[salt_len:dstart],d[dstart:]
-	hp,m = (hash_preset,"user-requested") if hash_preset else ('3',"default")
-	qmsg("Using %s hash preset of '%s'" % (m,hp))
-	passwd = get_mmgen_passphrase("Enter passphrase: ",{})
-	key = make_key(passwd, salt, hp)
-	dec_d = decrypt_data(enc_d, key, int(ba.hexlify(iv),16))
-	from hashlib import sha256
-	if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
-		out = dec_d[sha256_len+nonce_len:]
-		if outfile == '-':  sys.stdout.write(out)
-		else:
-			if not outfile:
-				outfile = os.path.basename(infile)
-				if outfile[-len(g.mmenc_ext)-1:] == "."+g.mmenc_ext:
-					outfile = outfile[:-len(g.mmenc_ext)-1]
-				else:
-					outfile = outfile + ".dec"
-			write_to_file(outfile, out, opts,"decrypted data",True,True)
+	enc_d = get_data_from_file(infile,"encrypted data")
+	dec_d = mmgen_decrypt(enc_d,hash_preset,opts)
+	if outfile == '-':
+		write_to_stdout(dec_d,"decrypted data",confirm=True)
 	else:
-		msg("Incorrect passphrase or hash preset")
+		if not outfile:
+			outfile = os.path.basename(infile)
+			if outfile[-len(g.mmenc_ext)-1:] == "."+g.mmenc_ext:
+				outfile = outfile[:-len(g.mmenc_ext)-1]
+			else:
+				outfile = outfile + ".dec"
+		write_to_file(outfile, dec_d, opts,"decrypted data",True,True)
 
 
 def find_incog_data(filename,iv_id,keep_searching=False):

+ 3 - 1
mmgen/tx.py

@@ -25,6 +25,8 @@ from decimal import Decimal
 
 import mmgen.config as g
 from mmgen.util import *
+from mmgen.crypto import get_seed_retry
+from mmgen.term import do_pager,get_char
 
 txmsg = {
 'not_enough_btc': "Not enough BTC in the inputs for this transaction (%s BTC)",
@@ -538,7 +540,7 @@ def check_addr_data_hash(seed_id,addr_data):
 
 def parse_addrs_file(f):
 
-	lines = get_lines_from_file(f,"address data",remove_comments=True)
+	lines = get_lines_from_file(f,"address data",trim_comments=True)
 
 	try:
 		seed_id,obrace = lines[0].split()

+ 177 - 569
mmgen/util.py

@@ -16,7 +16,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 """
-util.py:  Shared routines for the mmgen suite
+util.py:  Low-level routines imported by other modules for the MMGen suite
 """
 
 import sys
@@ -24,8 +24,6 @@ from hashlib import sha256
 from binascii import hexlify,unhexlify
 
 import mmgen.config as g
-from mmgen.bitcoin import b58decode_pad,b58encode_pad
-from mmgen.term import *
 
 def msg(s):    sys.stderr.write(s + "\n")
 def msg_r(s):  sys.stderr.write(s)
@@ -42,97 +40,6 @@ def vmsg(s):
 def vmsg_r(s):
 	if g.verbose: sys.stderr.write(s)
 
-def bail(): sys.exit(9)
-
-def get_extension(f):
-	import os
-	return os.path.splitext(f)[1][1:]
-
-def get_random_data_from_user(uchars):
-
-	if g.quiet: msg("Enter %s random symbols" % uchars)
-	else:       msg(cmessages['usr_rand_notice'] % uchars)
-
-	prompt = "You may begin typing.  %s symbols left: "
-	msg_r(prompt % uchars)
-
-	import time
-	# time.clock() always returns zero, so we'll use time.time()
-	saved_time = time.time()
-
-	key_data,time_data = "",[]
-
-	for i in range(uchars):
-		key_data += get_char(immed_chars="ALL")
-		msg_r("\r" + prompt % (uchars - i - 1))
-		now = time.time()
-		time_data.append(now - saved_time)
-		saved_time = now
-
-	if g.quiet: msg_r("\r")
-	else: msg_r("\rThank you.  That's enough.%s\n\n" % (" "*18))
-
-	fmt_time_data = ["{:.22f}".format(i) for i in time_data]
-
-	if g.debug:
-		msg("\nUser input:\n%s\nKeystroke time intervals:\n%s\n" %
-				(key_data,"\n".join(fmt_time_data)))
-
-	prompt = "User random data successfully acquired.  Press ENTER to continue"
-	prompt_and_get_char(prompt,"",enter_ok=True)
-
-	return key_data+"".join(fmt_time_data)
-
-
-def get_random(length,opts):
-	from Crypto import Random
-	os_rand = Random.new().read(length)
-	if 'usr_randchars' in opts and opts['usr_randchars'] not in (0,-1):
-		kwhat = "a key from random data with "
-		if not g.user_entropy:
-			g.user_entropy = sha256(
-				get_random_data_from_user(opts['usr_randchars'])).digest()
-			kwhat += "user entropy"
-		else:
-			kwhat += "saved user entropy"
-		key = make_key(g.user_entropy, "", '2', what=kwhat)
-		return encrypt_data(os_rand,key,what="random data",verify=False)
-	else:
-		return os_rand
-
-def my_raw_input(prompt,echo=True):
-	try:
-		if echo:
-			reply = raw_input(prompt)
-		else:
-			from getpass import getpass
-			reply = getpass(prompt)
-	except KeyboardInterrupt:
-		msg("\nUser interrupt")
-		sys.exit(1)
-
-	kb_hold_protect()
-	return reply
-
-
-def _get_hash_params(hash_preset):
-	if hash_preset in g.hash_presets:
-		return g.hash_presets[hash_preset] # N,p,r,buflen
-	else: # Shouldn't be here
-		msg("%s: invalid 'hash_preset' value" % hash_preset)
-		sys.exit(3)
-
-
-def show_hash_presets():
-	fs = "  {:<7} {:<6} {:<3}  {}"
-	msg("Available parameters for scrypt.hash():")
-	msg(fs.format("Preset","N","r","p"))
-	for i in sorted(g.hash_presets.keys()):
-		msg(fs.format("'%s'" % i, *g.hash_presets[i]))
-	msg("N = memory usage (power of two), p = iterations (rounds)")
-	sys.exit(0)
-
-
 cmessages = {
 	'null': "",
 	'incog_iv_id': """
@@ -189,71 +96,113 @@ just hit ENTER twice.
 """.strip()
 }
 
+def get_extension(f):
+	import os
+	return os.path.splitext(f)[1][1:]
 
-def confirm_or_exit(message, question, expect="YES"):
-
-	vmsg("")
-
-	m = message.strip()
-	if m: msg(m)
-
-	conf_msg = "Type uppercase '%s' to confirm: " % expect
-
-	p = question+"  "+conf_msg if question[0].isupper() else \
-		"Are you sure you want to %s?\n%s" % (question,conf_msg)
-
-	if my_raw_input(p).strip() != expect:
-		msg("Exiting at user request")
-		sys.exit(2)
-
-	vmsg("")
+def make_chksum_N(s,n,sep=False):
+	if n%4 or not (4 <= n <= 64): return False
+	s = sha256(sha256(s).digest()).hexdigest().upper()
+	sep = " " if sep else ""
+	return sep.join([s[i*4:i*4+4] for i in range(n/4)])
+def make_chksum_8(s,sep=False):
+	s = sha256(sha256(s).digest()).hexdigest()[:8].upper()
+	return "{} {}".format(s[:4],s[4:]) if sep else s
+def make_chksum_6(s): return sha256(s).hexdigest()[:6]
+def make_iv_chksum(s): return sha256(s).hexdigest()[:8].upper()
 
+def splitN(s,n,sep=None):                      # always return an n-element list
+	ret = s.split(sep,n-1)
+	return ret + ["" for i in range(n-len(ret))]
+def split2(s,sep=None): return splitN(s,2,sep) # always return a 2-element list
+def split3(s,sep=None): return splitN(s,3,sep) # always return a 3-element list
 
-def user_confirm(prompt,default_yes=False,verbose=False):
+def col4(s):
+	nondiv = 1 if len(s) % 4 else 0
+	return " ".join([s[4*i:4*i+4] for i in range(len(s)/4 + nondiv)])
 
-	q = "(Y/n)" if default_yes else "(y/N)"
+def make_timestamp():
+	import time
+	tv = time.gmtime(time.time())[:6]
+	return "{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}".format(*tv)
+def make_timestr():
+	import time
+	tv = time.gmtime(time.time())[:6]
+	return "{:04d}/{:02d}/{:02d} {:02d}:{:02d}:{:02d}".format(*tv)
+def secs_to_hms(secs):
+	return "{:02d}:{:02d}:{:02d}".format(secs/3600, (secs/60) % 60, secs % 60)
 
-	while True:
-		reply = get_char("%s %s: " % (prompt, q)).strip("\n\r")
+def _is_hex(s):
+	try: int(s,16)
+	except: return False
+	else: return True
 
-		if not reply:
-			if default_yes: msg(""); return True
-			else:           msg(""); return False
-		elif reply in 'yY': msg(""); return True
-		elif reply in 'nN': msg(""); return False
-		else:
-			if verbose: msg("\nInvalid reply")
-			else: msg_r("\r")
+def match_ext(addr,ext):
+	return addr.split(".")[-1] == ext
 
+def get_from_brain_opt_params(opts):
+	l,p = opts['from_brain'].split(",")
+	return(int(l),p)
 
-def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False):
+def pretty_hexdump(data,gw=2,cols=8,line_nums=False):
+	r = 1 if len(data) % gw else 0
+	return "".join(
+		[
+			("" if (line_nums == False or i % cols) else "%03i: " % (i/cols)) +
+			hexlify(data[i*gw:i*gw+gw]) +
+			(" " if (i+1) % cols else "\n")
+				for i in range(len(data)/gw + r)
+		]
+	).rstrip()
 
-	while True:
-		reply = get_char("%s: " % prompt).strip("\n\r")
+def decode_pretty_hexdump(data):
+	import re
+	lines = [re.sub('^\d+:\s+','',l) for l in data.split("\n")]
+	return unhexlify("".join(("".join(lines).split())))
 
-		if reply in chars or (enter_ok and not reply):
-			msg("")
-			return reply
+def get_hash_params(hash_preset):
+	if hash_preset in g.hash_presets:
+		return g.hash_presets[hash_preset] # N,p,r,buflen
+	else: # Shouldn't be here
+		msg("%s: invalid 'hash_preset' value" % hash_preset)
+		sys.exit(3)
 
-		if verbose: msg("\nInvalid reply")
-		else: msg_r("\r")
+def show_hash_presets():
+	fs = "  {:<7} {:<6} {:<3}  {}"
+	msg("Available parameters for scrypt.hash():")
+	msg(fs.format("Preset","N","r","p"))
+	for i in sorted(g.hash_presets.keys()):
+		msg(fs.format("'%s'" % i, *g.hash_presets[i]))
+	msg("N = memory usage (power of two), p = iterations (rounds)")
+	sys.exit(0)
 
+def compare_checksums(chksum1, desc1, chksum2, desc2):
 
-def make_chksum_N(s,n,sep=False):
-	if n%4 or not (4 <= n <= 64): return False
-	s = sha256(sha256(s).digest()).hexdigest().upper()
-	sep = " " if sep else ""
-	return sep.join([s[i*4:i*4+4] for i in range(n/4)])
+	if chksum1.lower() == chksum2.lower():
+		vmsg("OK (%s)" % chksum1.upper())
+		return True
+	else:
+		if g.debug:
+			print \
+	"ERROR!\nComputed checksum %s (%s) doesn't match checksum %s (%s)" \
+			% (desc1,chksum1,desc2,chksum2)
+		return False
 
-def make_chksum_8(s,sep=False):
-	s = sha256(sha256(s).digest()).hexdigest()[:8].upper()
-	return "{} {}".format(s[:4],s[4:]) if sep else s
+def get_default_wordlist():
 
-def make_chksum_6(s):
-	return sha256(s).hexdigest()[:6]
+	wl_id = g.default_wl
+	if wl_id == "electrum": from mmgen.mn_electrum import electrum_words as wl
+	elif wl_id == "tirosh": from mmgen.mn_tirosh   import tirosh_words as wl
+	return wl.strip().split("\n")
 
-def make_iv_chksum(s):
-	return sha256(s).hexdigest()[:8].upper()
+def open_file_or_exit(filename,mode):
+	try:
+		f = open(filename, mode)
+	except:
+		what = "reading" if mode == 'r' else "writing"
+		msg("Unable to open file '%s' for %s" % (filename,what))
+		sys.exit(2)
+	return f
 
 
 def check_file_type_and_access(fname,ftype):
@@ -287,6 +236,7 @@ def check_infile(f):  return check_file_type_and_access(f,"input file")
 def check_outfile(f): return check_file_type_and_access(f,"output file")
 def check_outdir(f):  return check_file_type_and_access(f,"directory")
 
+
 def _validate_addr_num(n):
 
 	try: n = int(n)
@@ -301,6 +251,12 @@ def _validate_addr_num(n):
 	return n
 
 
+def make_full_path(outdir,outfile):
+	import os
+	return os.path.normpath(os.sep.join([outdir, os.path.basename(outfile)]))
+#	os.path.join() doesn't work?
+
+
 def parse_address_list(arg,sep=","):
 
 	ret = []
@@ -354,65 +310,23 @@ def get_new_passphrase(what, opts):
 	return pw
 
 
-def _scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32):
-
-	# Buflen arg is for brainwallets only, which use this function to generate
-	# the seed directly.
-
-	N,r,p = _get_hash_params(hash_preset)
-
-	import scrypt
-	return scrypt.hash(passwd, salt, 2**N, r, p, buflen=buflen)
-
-
-def get_from_brain_opt_params(opts):
-	l,p = opts['from_brain'].split(",")
-	return(int(l),p)
-
-
-def _get_seed_from_brain_passphrase(words,opts):
-	bp = " ".join(words)
-	if g.debug: print "Sanitized brain passphrase: %s" % bp
-	seed_len,hash_preset = get_from_brain_opt_params(opts)
-	if g.debug: print "Brainwallet l = %s, p = %s" % (seed_len,hash_preset)
-	vmsg_r("Hashing brainwallet data.  Please wait...")
-	# Use buflen arg of scrypt.hash() to get seed of desired length
-	seed = _scrypt_hash_passphrase(bp, "", hash_preset, buflen=seed_len/8)
-	vmsg("Done")
-	return seed
-
-
-def encrypt_seed(seed, key):
-	return encrypt_data(seed, key, iv=1, what="seed")
-
-def encrypt_data(data, key, iv=1, what="data", verify=True):
-	"""
-	Encrypt arbitrary data using AES256 in counter mode
-	"""
-
-	# 192-bit seed is 24 bytes -> not multiple of 16.  Must use MODE_CTR
-	from Crypto.Cipher import AES
-	from Crypto.Util import Counter
+def confirm_or_exit(message, question, expect="YES"):
 
-	vmsg("Encrypting %s" % what)
+	vmsg("")
 
-	c = AES.new(key, AES.MODE_CTR,
-			counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
-	enc_data = c.encrypt(data)
+	m = message.strip()
+	if m: msg(m)
 
-	if verify:
-		vmsg_r("Performing a test decryption of the %s..." % what)
+	conf_msg = "Type uppercase '%s' to confirm: " % expect
 
-		c = AES.new(key, AES.MODE_CTR,
-				counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
-		dec_data = c.decrypt(enc_data)
+	p = question+"  "+conf_msg if question[0].isupper() else \
+		"Are you sure you want to %s?\n%s" % (question,conf_msg)
 
-		if dec_data == data: vmsg("done\n")
-		else:
-			msg("ERROR.\nDecrypted %s doesn't match original %s" % (what,what))
-			sys.exit(2)
+	if my_raw_input(p).strip() != expect:
+		msg("Exiting at user request")
+		sys.exit(2)
 
-	return enc_data
+	vmsg("")
 
 
 def write_to_stdout(data, what, confirm=True):
@@ -422,36 +336,12 @@ def write_to_stdout(data, what, confirm=True):
 		try:
 			import os
 			of = os.readlink("/proc/%d/fd/1" % os.getpid())
-			msg("Redirecting output to file '%s'" % of)
+			msg("Redirecting output to file '%s'" % os.path.relpath(of))
 		except:
 			msg("Redirecting output to file")
 	sys.stdout.write(data)
 
 
-def get_default_wordlist():
-
-	wl_id = g.default_wl
-	if wl_id == "electrum": from mmgen.mn_electrum import electrum_words as wl
-	elif wl_id == "tirosh": from mmgen.mn_tirosh   import tirosh_words as wl
-	return wl.strip().split("\n")
-
-
-def open_file_or_exit(filename,mode):
-	try:
-		f = open(filename, mode)
-	except:
-		what = "reading" if mode == 'r' else "writing"
-		msg("Unable to open file '%s' for %s" % (filename,what))
-		sys.exit(2)
-	return f
-
-
-def make_full_path(outdir,outfile):
-	import os
-	return os.path.normpath(os.sep.join([outdir, os.path.basename(outfile)]))
-#	os.path.join() doesn't work?
-
-
 def write_to_file(outfile,data,opts,what="data",confirm=False,verbose=False):
 
 	if 'outdir' in opts: outfile = make_full_path(opts['outdir'],outfile)
@@ -487,11 +377,12 @@ def export_to_file(outfile, data, opts, what="data"):
 		write_to_file(outfile,data,opts,what,c,True)
 
 
-def _display_control_data(label,metadata,hash_preset,salt,enc_seed):
+from mmgen.bitcoin import b58decode_pad,b58encode_pad
+
+def display_control_data(label,metadata,hash_preset,salt,enc_seed):
 	msg("WALLET DATA")
 	fs = "  {:18} {}"
 	pw_empty = "yes" if metadata[3] == "E" else "no"
-	from mmgen.bitcoin import b58encode_pad
 	for i in (
 		("Label:",               label),
 		("Seed ID:",             metadata[0].upper()),
@@ -499,7 +390,7 @@ def _display_control_data(label,metadata,hash_preset,salt,enc_seed):
 		("Seed length:",         "%s bits (%s bytes)" %
 				(metadata[2],int(metadata[2])/8)),
 		("Scrypt params:",  "Preset '%s' (%s)" % (hash_preset,
-				" ".join([str(i) for i in _get_hash_params(hash_preset)]))),
+				" ".join([str(i) for i in get_hash_params(hash_preset)]))),
 		("Passphrase empty?", pw_empty.capitalize()),
 		("Timestamp:",           "%s UTC" % metadata[4]),
 	): msg(fs.format(*i))
@@ -515,31 +406,6 @@ def _display_control_data(label,metadata,hash_preset,salt,enc_seed):
 	): msg(fs.format(*i))
 
 
-def splitN(s,n,sep=None):                      # always return an n-element list
-	ret = s.split(sep,n-1)
-	return ret + ["" for i in range(n-len(ret))]
-
-def split2(s,sep=None): return splitN(s,2,sep) # always return a 2-element list
-def split3(s,sep=None): return splitN(s,3,sep) # always return a 3-element list
-
-def col4(s):
-	nondiv = 1 if len(s) % 4 else 0
-	return " ".join([s[4*i:4*i+4] for i in range(len(s)/4 + nondiv)])
-
-def make_timestamp():
-	import time
-	tv = time.gmtime(time.time())[:6]
-	return "{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}".format(*tv)
-
-def make_timestr():
-	import time
-	tv = time.gmtime(time.time())[:6]
-	return "{:04d}/{:02d}/{:02d} {:02d}:{:02d}:{:02d}".format(*tv)
-
-def secs_to_hms(secs):
-	return "{:02d}:{:02d}:{:02d}".format(secs/3600, (secs/60) % 60, secs % 60)
-
-
 def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts):
 
 	seed_id = make_chksum_8(seed)
@@ -555,7 +421,7 @@ def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts):
 	lines = (
 		label,
 		"{} {} {} {} {}".format(*metadata),
-		"{}: {} {} {}".format(hash_preset,*_get_hash_params(hash_preset)),
+		"{}: {} {} {}".format(hash_preset,*get_hash_params(hash_preset)),
 		"{} {}".format(make_chksum_6(sf),  col4(sf)),
 		"{} {}".format(make_chksum_6(esf), col4(esf))
 	)
@@ -569,28 +435,8 @@ def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts):
 	write_to_file(outfile,d,opts,"wallet",c,True)
 
 	if g.verbose:
-		_display_control_data(label,metadata,hash_preset,salt,enc_seed)
-
+		display_control_data(label,metadata,hash_preset,salt,enc_seed)
 
-def _compare_checksums(chksum1, desc1, chksum2, desc2):
-
-	if chksum1.lower() == chksum2.lower():
-		vmsg("OK (%s)" % chksum1.upper())
-		return True
-	else:
-		if g.debug:
-			print \
-	"ERROR!\nComputed checksum %s (%s) doesn't match checksum %s (%s)" \
-			% (desc1,chksum1,desc2,chksum2)
-		return False
-
-def _is_hex(s):
-	try: int(s,16)
-	except: return False
-	else: return True
-
-def match_ext(addr,ext):
-	return addr.split(".")[-1] == ext
 
 def _check_mmseed_format(words):
 
@@ -664,7 +510,7 @@ def get_data_from_wallet(infile,silent=False):
 	hash_preset = hd[0][:-1]
 	hash_params = [int(i) for i in hd[1:]]
 
-	if hash_params != _get_hash_params(hash_preset):
+	if hash_params != get_hash_params(hash_preset):
 		msg("Hash parameters '%s' don't match hash preset '%s'" %
 				(" ".join(hash_params), hash_preset))
 		sys.exit(9)
@@ -702,22 +548,29 @@ def _get_words_from_file(infile,what):
 	return words
 
 
-def get_lines_from_file(infile,what="",remove_comments=False):
+def get_words(infile,what,prompt,opts):
+	if infile:
+		return _get_words_from_file(infile,what)
+	else:
+		return _get_words_from_user(prompt,opts)
+
+def remove_comments(lines):
+	import re
+	# re.sub(pattern, repl, string, count=0, flags=0)
+	ret = []
+	for i in lines:
+		i = re.sub('#.*','',i,1)
+		i = re.sub('\s+$','',i)
+		if i: ret.append(i)
+	return ret
+
+def get_lines_from_file(infile,what="",trim_comments=False):
 	if what != "":
 		qmsg("Getting %s from file '%s'" % (what,infile))
 	f = open_file_or_exit(infile,'r')
-	lines = f.read().splitlines(); f.close()
-	if remove_comments:
-		import re
-		# re.sub(pattern, repl, string, count=0, flags=0)
-		ret = []
-		for i in lines:
-			i = re.sub('#.*','',i,1)
-			i = re.sub('\s+$','',i)
-			if i: ret.append(i)
-		return ret
-	else:
-		return lines
+	lines = f.read().splitlines()
+	f.close()
+	return remove_comments(lines) if trim_comments else lines
 
 
 def get_data_from_file(infile,what="data",dash=False):
@@ -729,7 +582,7 @@ def get_data_from_file(infile,what="data",dash=False):
 	return data
 
 
-def _get_seed_from_seed_data(words):
+def get_seed_from_seed_data(words):
 
 	if not _check_mmseed_format(words):
 		msg("Invalid %s data" % g.seed_ext)
@@ -741,7 +594,7 @@ def _get_seed_from_seed_data(words):
 	chk = make_chksum_6(seed_b58)
 	vmsg_r("Validating %s checksum..." % g.seed_ext)
 
-	if _compare_checksums(chk, "from seed", stored_chk, "from input"):
+	if compare_checksums(chk, "from seed", stored_chk, "from input"):
 		seed = b58decode_pad(seed_b58)
 		if seed == False:
 			msg("Invalid b58 number: %s" % val)
@@ -782,25 +635,6 @@ def get_bitcoind_passphrase(prompt,opts):
 					echo=True if 'echo_passphrase' in opts else False)
 
 
-def get_seed_from_wallet(
-		infile,
-		opts,
-		prompt="Enter {} wallet passphrase: ".format(g.proj_name),
-		silent=False
-		):
-
-	wdata = get_data_from_wallet(infile,silent=silent)
-	label,metadata,hash_preset,salt,enc_seed = wdata
-
-	if g.verbose: _display_control_data(*wdata)
-
-	passwd = get_mmgen_passphrase(prompt,opts)
-
-	key = make_key(passwd, salt, hash_preset)
-
-	return decrypt_seed(enc_seed, key, metadata[0], metadata[1])
-
-
 def check_data_fits_file_at_offset(fname,offset,dlen,action):
 	# TODO: Check for Windows
 	import os, stat
@@ -835,236 +669,6 @@ def get_hidden_incog_data(opts):
 				"Data read from file")
 		return data
 
-def get_seed_from_incog_wallet(
-		infile,
-		opts,
-		prompt="Enter {} wallet passphrase: ".format(g.proj_name),
-		silent=False,
-		hex_input=False
-	):
-
-	what = "incognito wallet data"
-
-	if "from_incog_hidden" in opts:
-		d = get_hidden_incog_data(opts)
-	else:
-		d = get_data_from_file(infile,what)
-		if hex_input:
-			try:
-				d = unhexlify("".join(d.split()).strip())
-			except:
-				msg("Data in file '%s' is not in hexadecimal format" % infile)
-				sys.exit(2)
-		# File could be of invalid length, so check:
-		valid_dlens = [i/8 + g.aesctr_iv_len + g.salt_len for i in g.seed_lens]
-		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]))
-				)
-			return False
-
-	iv, enc_incog_data = d[0:g.aesctr_iv_len], d[g.aesctr_iv_len:]
-
-	msg("Incog ID: %s (IV ID: %s)" % (make_iv_chksum(iv),make_chksum_8(iv)))
-	qmsg("Check the applicable value against your records.")
-	vmsg(cmessages['incog_iv_id_hidden' if "from_incog_hidden" in opts
-			else 'incog_iv_id'])
-
-	passwd = get_mmgen_passphrase(prompt,opts)
-
-	qmsg("Configured hash presets: %s" % " ".join(sorted(g.hash_presets)))
-	while True:
-		p = "Enter hash preset for %s wallet (default='%s'): "
-		hp = my_raw_input(p % (g.proj_name, g.hash_preset))
-		if not hp:
-			hp = g.hash_preset; break
-		elif hp in g.hash_presets:
-			break
-		msg("%s: Invalid hash preset" % hp)
-
-	# 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)
-
-	salt,enc_seed = d[0:g.salt_len], d[g.salt_len:]
-
-	key = make_key(passwd, salt, hp, "main key")
-	vmsg("Key ID: %s" % make_chksum_8(key))
-
-	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'])
-
-	return seed
-
-
-def make_key(passwd, salt, hash_preset, what="key"):
-
-	vmsg_r("Generating %s.  Please wait..." % what)
-	key = _scrypt_hash_passphrase(passwd, salt, hash_preset)
-	vmsg("done")
-	if g.debug: print "Key: %s" % hexlify(key)
-	return key
-
-
-def decrypt_seed(enc_seed, key, seed_id, key_id):
-
-	vmsg("Checking key...")
-	chk1 = make_chksum_8(key)
-	if key_id:
-		if not _compare_checksums(chk1, "of key", key_id, "in header"):
-			msg("Incorrect passphrase")
-			return False
-
-	dec_seed = decrypt_data(enc_seed, key, iv=1, what="seed")
-
-	chk2 = make_chksum_8(dec_seed)
-
-	if seed_id:
-		if _compare_checksums(chk2,"of decrypted seed",seed_id,"in header"):
-			qmsg("Passphrase is OK")
-		else:
-			if not g.debug:
-				msg_r("Checking key ID...")
-				if _compare_checksums(chk1, "of key", key_id, "in header"):
-					msg("Key ID is correct but decryption of seed failed")
-				else:
-					msg("Incorrect passphrase")
-
-			return False
-#	else:
-#		qmsg("Generated IDs (Seed/Key): %s/%s" % (chk2,chk1))
-
-	if g.debug: print "Decrypted seed: %s" % hexlify(dec_seed)
-
-	return dec_seed
-
-
-def decrypt_data(enc_data, key, iv=1, what="data"):
-
-	vmsg("Decrypting %s with key..." % what)
-
-	from Crypto.Cipher import AES
-	from Crypto.Util import Counter
-
-	c = AES.new(key, AES.MODE_CTR,
-			counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
-
-	return c.decrypt(enc_data)
-
-
-
-def _get_words(infile,what,prompt,opts):
-	if infile:
-		return _get_words_from_file(infile,what)
-	else:
-		return _get_words_from_user(prompt,opts)
-
-
-def get_seed(infile,opts,silent=False):
-
-	ext = get_extension(infile)
-
-	if   ext == g.mn_ext:           source = "mnemonic"
-	elif ext == g.brain_ext:        source = "brainwallet"
-	elif ext == g.seed_ext:         source = "seed"
-	elif ext == g.wallet_ext:       source = "wallet"
-	elif ext == g.incog_ext:        source = "incognito wallet"
-	elif ext == g.incog_hex_ext:    source = "incognito wallet"
-	elif 'from_mnemonic'  in opts: source = "mnemonic"
-	elif 'from_brain'     in opts: source = "brainwallet"
-	elif 'from_seed'      in opts: source = "seed"
-	elif 'from_incog'     in opts: source = "incognito wallet"
-	else:
-		if infile: msg(
-			"Invalid file extension for file: %s\nValid extensions: '.%s'" %
-			(infile, "', '.".join(g.seedfile_exts)))
-		else: msg("No seed source type specified and no file supplied")
-		sys.exit(2)
-
-	if source == "mnemonic":
-		prompt = "Enter mnemonic: "
-		words = _get_words(infile,"mnemonic data",prompt,opts)
-		wl = get_default_wordlist()
-		from mmgen.mnemonic import get_seed_from_mnemonic
-		seed = get_seed_from_mnemonic(words,wl)
-	elif source == "brainwallet":
-		if 'from_brain' not in opts:
-			msg("'--from-brain' parameters must be specified for brainwallet file")
-			sys.exit(2)
-		prompt = "Enter brainwallet passphrase: "
-		words = _get_words(infile,"brainwallet data",prompt,opts)
-		seed = _get_seed_from_brain_passphrase(words,opts)
-	elif source == "seed":
-		prompt = "Enter seed in %s format: " % g.seed_ext
-		words = _get_words(infile,"seed data",prompt,opts)
-		seed = _get_seed_from_seed_data(words)
-	elif source == "wallet":
-		seed = get_seed_from_wallet(infile, opts, silent=silent)
-	elif source == "incognito wallet":
-		h = True if ext == g.incog_hex_ext or 'from_incog_hex' in opts else False
-		seed = get_seed_from_incog_wallet(infile, opts, silent=silent, hex_input=h)
-
-
-	if infile and not seed and (
-		source == "seed" or source == "mnemonic" or source == "incognito wallet"):
-		msg("Invalid %s file '%s'" % (source,infile))
-		sys.exit(2)
-
-	if g.debug: print "Seed: %s" % hexlify(seed)
-
-	return seed
-
-# Repeat if entered data is invalid
-def get_seed_retry(infile,opts):
-	silent = False
-	while True:
-		seed = get_seed(infile,opts,silent=silent)
-		silent = True
-		if seed: return seed
-
-
-def do_pager(text):
-
-	pagers = ["less","more"]
-	shell = False
-
-	from os import environ
-
-# Hack for MS Windows command line (i.e. non CygWin) environment
-# When 'shell' is true, Windows aborts the calling program if executable
-# not found.
-# When 'shell' is false, an exception is raised, invoking the fallback
-# 'print' instead of the pager.
-# We risk assuming that "more" will always be available on a stock
-# Windows installation.
-	if sys.platform.startswith("win") and 'HOME' not in environ:
-		shell = True
-		pagers = ["more"]
-
-	if 'PAGER' in environ and environ['PAGER'] != pagers[0]:
-		pagers = [environ['PAGER']] + pagers
-
-	for pager in pagers:
-		end = "" if pager == "less" else "\n(end of text)\n"
-		try:
-			from subprocess import Popen, PIPE, STDOUT
-			p = Popen([pager], stdin=PIPE, shell=shell)
-		except: pass
-		else:
-			try:
-				p.communicate(text+end+"\n")
-			except KeyboardInterrupt:
-				# Has no effect.  Why?
-				if pager != "less":
-					msg("\n(User interrupt)\n")
-			finally:
-				msg_r("\r")
-				break
-	else: print text+end
-
 
 def export_to_hidden_incog(incog_enc,opts):
 	outfile,offset = opts['export_incog_hidden'].split(",") #Already sanity-checked
@@ -1081,46 +685,50 @@ def export_to_hidden_incog(incog_enc,opts):
 			(os.path.relpath(outfile),offset))
 
 
-def pretty_hexdump(data,gw=2,cols=8,line_nums=False):
-	r = 1 if len(data) % gw else 0
-	return "".join(
-		[
-			("" if (line_nums == False or i % cols) else "%03i: " % (i/cols)) +
-			hexlify(data[i*gw:i*gw+gw]) +
-			(" " if (i+1) % cols else "\n")
-				for i in range(len(data)/gw + r)
-		]
-	).rstrip()
+from mmgen.term import kb_hold_protect,get_char
 
-def decode_pretty_hexdump(data):
-	import re
-	lines = [re.sub('^\d+:\s+','',l) for l in data.split("\n")]
-	return unhexlify("".join(("".join(lines).split())))
+def my_raw_input(prompt,echo=True):
+	try:
+		if echo:
+			reply = raw_input(prompt)
+		else:
+			from getpass import getpass
+			reply = getpass(prompt)
+	except KeyboardInterrupt:
+		msg("\nUser interrupt")
+		sys.exit(1)
 
+	kb_hold_protect()
+	return reply
 
-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]
+def user_confirm(prompt,default_yes=False,verbose=False):
 
-	passwd = get_mmgen_passphrase("Enter mmgen passphrase: ",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)
+	q = "(Y/n)" if default_yes else "(y/N)"
+
+	while True:
+		reply = get_char("%s %s: " % (prompt, q)).strip("\n\r")
+
+		if not reply:
+			if default_yes: msg(""); return True
+			else:           msg(""); return False
+		elif reply in 'yY': msg(""); return True
+		elif reply in 'nN': msg(""); return False
+		else:
+			if verbose: msg("\nInvalid reply")
+			else: msg_r("\r")
 
-	iv = get_random(g.aesctr_iv_len,opts)
-	iv_id = make_iv_chksum(iv)
-	msg("Incog ID: %s" % iv_id)
 
-	# IV is used BOTH to initialize counter and to salt password!
-	key = make_key(passwd, iv, preset, "wrapper key")
-	m = "incog data"
-	wrap_enc = encrypt_data(salt + enc_seed, key, int(hexlify(iv),16), m)
+def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False):
+
+	while True:
+		reply = get_char("%s: " % prompt).strip("\n\r")
 
-	return iv+wrap_enc,seed_id,key_id,iv_id,preset
+		if reply in chars or (enter_ok and not reply):
+			msg("")
+			return reply
+
+		if verbose: msg("\nInvalid reply")
+		else: msg_r("\r")
 
 
-if __name__ == "__main__":
-	print "util.py"