Browse Source

Hand merged changes from OO repository
new file: obj.py
new file: filename.py
new file: seed.py

philemon 10 years ago
parent
commit
4d618ac6f2

+ 0 - 53
MANIFEST

@@ -1,53 +0,0 @@
-# file GENERATED by distutils, do NOT edit
-mmgen-addrgen
-mmgen-addrimport
-mmgen-keygen
-mmgen-passchg
-mmgen-pywallet
-mmgen-tool
-mmgen-txcreate
-mmgen-txsend
-mmgen-txsign
-mmgen-walletchk
-mmgen-walletgen
-setup.py
-mmgen/__init__.py
-mmgen/addr.py
-mmgen/bitcoin.py
-mmgen/config.py
-mmgen/crypto.py
-mmgen/license.py
-mmgen/main.py
-mmgen/main_addrgen.py
-mmgen/main_addrimport.py
-mmgen/main_passchg.py
-mmgen/main_pywallet.py
-mmgen/main_tool.py
-mmgen/main_txcreate.py
-mmgen/main_txsend.py
-mmgen/main_txsign.py
-mmgen/main_walletchk.py
-mmgen/main_walletgen.py
-mmgen/mn_electrum.py
-mmgen/mn_tirosh.py
-mmgen/mnemonic.py
-mmgen/opt.py
-mmgen/opts.py
-mmgen/term.py
-mmgen/test.py
-mmgen/tool.py
-mmgen/tx.py
-mmgen/util.py
-mmgen/rpc/__init__.py
-mmgen/rpc/config.py
-mmgen/rpc/connection.py
-mmgen/rpc/data.py
-mmgen/rpc/exceptions.py
-mmgen/rpc/proxy.py
-mmgen/rpc/util.py
-mmgen/share/Opts.py
-mmgen/share/__init__.py
-test/__init__.py
-test/gentest.py
-test/test.py
-test/tooltest.py

+ 11 - 5
mmgen/config.py

@@ -51,8 +51,9 @@ email     = "<mmgen-py@yandex.com>"
 Cdates    = '2013-2015'
 version   = '0.8.0'
 
-required_opts = [ # list must contain "usr_randchars"
-	"quiet","verbose","debug","outdir","echo_passphrase","passwd_file","usr_randchars"
+required_opts = [
+	"quiet","verbose","debug","outdir","echo_passphrase","passwd_file",
+	"usr_randchars","stdout","show_hash_presets"
 ]
 min_screen_width = 80
 max_tx_comment_len = 72
@@ -64,7 +65,9 @@ brain_ext     = "mmbrain"
 incog_ext     = "mmincog"
 incog_hex_ext = "mmincox"
 
-seedfile_exts = wallet_ext, seed_ext, mn_ext, brain_ext, incog_ext
+seedfile_exts = (
+	wallet_ext, seed_ext, mn_ext, brain_ext, incog_ext, incog_hex_ext
+)
 
 rawtx_ext           = "raw"
 sigtx_ext           = "sig"
@@ -74,8 +77,8 @@ keyfile_ext         = "keys"
 keyaddrfile_ext     = "akeys"
 mmenc_ext           = "mmenc"
 
-default_wl    = "electrum"
-#default_wl    = "tirosh"
+default_wordlist    = "electrum"
+#default_wordlist    = "tirosh"
 
 # Global value sets user opt
 dfl_vars = "seed_len","hash_preset","usr_randchars","debug"
@@ -93,6 +96,7 @@ max_urandchars,min_urandchars = 80,10
 
 salt_len      = 16
 aesctr_iv_len = 16
+hincog_chk_len = 8
 
 hash_presets = {
 #   Scrypt params:
@@ -117,6 +121,8 @@ max_addr_label_len = 32
 wallet_label_symbols = addr_label_symbols
 max_wallet_label_len = 48
 
+printable_nospc = [chr(i+33) for i in range(94)]
+printable       = printable_nospc + [' ','\n','\t']
 #addr_label_punc = ".","_",",","-"," ","(",")"
 #addr_label_symbols = tuple(ascii_letters + digits) + addr_label_punc
 #wallet_label_punc = addr_label_punc

+ 3 - 3
mmgen/crypto.py

@@ -66,7 +66,7 @@ def decrypt_seed(enc_seed, key, seed_id, key_id):
 	vmsg_r("Checking key...")
 	chk1 = make_chksum_8(key)
 	if key_id:
-		if not compare_checksums(chk1, "of key", key_id, "in header"):
+		if not compare_chksums(key_id,"key id",chk1,"computed",die=False):
 			msg("Incorrect passphrase")
 			return False
 
@@ -75,12 +75,12 @@ def decrypt_seed(enc_seed, key, seed_id, key_id):
 	chk2 = make_chksum_8(dec_seed)
 
 	if seed_id:
-		if compare_checksums(chk2,"of decrypted seed",seed_id,"in header"):
+		if compare_chksums(seed_id,"seed id",chk2,"decrypted seed",die=False):
 			qmsg("Passphrase is OK")
 		else:
 			if not opt.debug:
 				msg_r("Checking key ID...")
-				if compare_checksums(chk1, "of key", key_id, "in header"):
+				if compare_chksums(key_id,"key id",chk1,"computed",die=False):
 					msg("Key ID is correct but decryption of seed failed")
 				else:
 					msg("Incorrect passphrase")

+ 90 - 0
mmgen/filename.py

@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2015 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/>.
+
+"""
+filename.py:  Filename class and methods for the MMGen suite
+"""
+import sys,os
+from mmgen.obj import MMGenObject
+import mmgen.config as g
+from mmgen.util import msg
+
+class Filename(MMGenObject):
+
+	exts = {
+		'seed': {
+			"mmdat":   "Wallet",
+			"mmseed":  "SeedFile",
+			"mmwords": "Mnemonic",
+			"mmbrain": "Brainwallet",
+			"mmincog": "IncogWallet",
+			"mmincox": "IncogWalletHex",
+		},
+		'tx': {
+			"raw":         "RawTX",
+			"sig":         "SigTX",
+		},
+		'addr': {
+			"addrs":       "AddrInfo",
+			"keys":        "KeyInfo",
+			"akeys":       "KeyAddrInfo",
+			"akeys.mmenc": "KeyAddrInfoEnc",
+		},
+		'other': {
+			"chk":         "AddrInfoChecksum",
+			"mmenc":       "MMEncInfo",
+		},
+	}
+
+	ftypes = {
+		'seed': {
+			"hincog":   "IncogWalletHidden",
+		},
+	}
+
+	def __init__(self,fn,ftype=""):
+		import os
+		self.name     = fn
+		self.dirname  = os.path.dirname(fn)
+		self.basename = os.path.basename(fn)
+		self.ext      = None
+
+		def mf1(k): return k == ftype
+		def mf2(k): return '.'+k == fn[-len('.'+k):]
+		# find file info for ftype or extension
+		e,attr,have_match = (self.ftypes,"ftype",mf1) if ftype else \
+							(self.exts,"ext",mf2)
+
+		for k in e:
+			for j in e[k]:
+				if have_match(j):
+					setattr(self,attr,j)
+					self.fclass = k
+					self.linked_obj = e[k][j]
+
+		if not hasattr(self,attr):
+			die(2,"Unrecognized %s for file '%s'" % (attr,fn))
+
+		# TODO: Check for Windows
+		import stat
+		if stat.S_ISBLK(os.stat(fn).st_mode):
+			fd = os.open(fn, os.O_RDONLY)
+			self.size = os.lseek(fd, 0, os.SEEK_END)
+			os.close(fd)
+		else:
+			self.size = os.stat(fn).st_size

+ 1 - 25
mmgen/license.py

@@ -17,14 +17,9 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-license.py:  Show the license
+license.py:  Text of GPLv3
 """
-
-import sys
-from mmgen.util import msg, msg_r
-from mmgen.term import get_char
 import mmgen.config as g
-import mmgen.opt as opt
 
 gpl = {
 	'warning': """
@@ -587,22 +582,3 @@ Program, unless a warranty or assumption of liability accompanies a
 copy of the Program in return for a fee.
 """
 }
-
-def do_license_msg(immed=False):
-
-	if opt.quiet or g.no_license: return
-
-	msg(gpl['warning'])
-	prompt = "%s " % gpl['prompt'].strip()
-
-	while True:
-		from mmgen.util import my_raw_input
-		reply = get_char(prompt, immed_chars="wc" if immed else "")
-		if reply == 'w':
-			from mmgen.term import do_pager
-			do_pager(gpl['conditions'])
-		elif reply == 'c':
-			msg(""); break
-		else:
-			msg_r("\r")
-	msg("")

+ 0 - 2
mmgen/main_addrgen.py

@@ -25,7 +25,6 @@ import sys
 
 import mmgen.config as g
 import mmgen.opt as opt
-from mmgen.license import *
 from mmgen.util import *
 from mmgen.crypto import *
 from mmgen.addr import *
@@ -115,7 +114,6 @@ UNENCRYPTED form.  Generate only the key(s) you need and guard them carefully.
 
 cmd_args = opt.opts.init(opts_data,add_opts=["b16"])
 
-if opt.show_hash_presets: show_hash_presets()
 if opt.from_incog_hex or opt.from_incog_hidden: opt.from_incog = True
 
 if len(cmd_args) == 1 and any([

+ 0 - 1
mmgen/main_addrimport.py

@@ -23,7 +23,6 @@ mmgen-addrimport: Import addresses into a MMGen bitcoind tracking wallet
 import sys, time
 import mmgen.config as g
 import mmgen.opt as opt
-from mmgen.license import *
 from mmgen.util import *
 from mmgen.tx import connect_to_bitcoind
 from mmgen.addr import AddrInfo,AddrInfoEntry

+ 0 - 2
mmgen/main_passchg.py

@@ -56,8 +56,6 @@ NOTE: The key ID will change if either the passphrase or hash preset are
 
 cmd_args = opt.opts.init(opts_data)
 
-if opt.show_hash_presets: show_hash_presets()
-
 if len(cmd_args) != 1:
 	msg("One input file must be specified")
 	sys.exit(2)

+ 0 - 1
mmgen/main_txcreate.py

@@ -26,7 +26,6 @@ from decimal import Decimal
 
 import mmgen.config as g
 import mmgen.opt as opt
-from mmgen.license import *
 from mmgen.tx import *
 
 opts_data = {

+ 1 - 2
mmgen/main_txsend.py

@@ -24,9 +24,8 @@ import sys
 
 import mmgen.config as g
 import mmgen.opt as opt
-from mmgen.license import *
 from mmgen.tx import *
-from mmgen.util import msg,check_infile,get_lines_from_file,confirm_or_exit
+from mmgen.util import *
 
 opts_data = {
 	'desc':    "Send a Bitcoin transaction signed by {}-txsign".format(g.proj_name.lower()),

+ 1 - 1
mmgen/main_txsign.py

@@ -24,8 +24,8 @@ import sys
 
 import mmgen.config as g
 import mmgen.opt as opt
-from mmgen.license import *
 from mmgen.tx import *
+from mmgen.util import do_license_msg
 
 opts_data = {
 	'desc':    "Sign Bitcoin transactions generated by {}-txcreate".format(g.proj_name.lower()),

+ 2 - 2
mmgen/main_walletchk.py

@@ -156,13 +156,13 @@ else:
 if opt.export_mnemonic:
 	wl = get_default_wordlist()
 	from mmgen.mnemonic import get_mnemonic_from_seed
-	mn = get_mnemonic_from_seed(seed, wl, g.default_wl, opt.debug)
+	mn = get_mnemonic_from_seed(seed, wl, g.default_wordlist, opt.debug)
 	fn = "%s.%s" % (make_chksum_8(seed).upper(), g.mn_ext)
 	write_to_file_or_stdout(fn, " ".join(mn)+"\n", "mnemonic data")
 
 elif opt.export_seed:
 	from mmgen.bitcoin import b58encode_pad
-	data = col4(b58encode_pad(seed))
+	data = split_into_columns(4,b58encode_pad(seed))
 	chk = make_chksum_6(b58encode_pad(seed))
 	fn = "%s.%s" % (make_chksum_8(seed).upper(), g.seed_ext)
 	write_to_file_or_stdout(fn, "%s %s\n" % (chk,data), "seed data")

+ 0 - 3
mmgen/main_walletgen.py

@@ -25,7 +25,6 @@ from hashlib import sha256
 
 import mmgen.config as g
 import mmgen.opt as opt
-from mmgen.license import *
 from mmgen.util import *
 from mmgen.crypto import *
 
@@ -123,8 +122,6 @@ future, you must continue using these same parameters
 import mmgen.opt as opt
 cmd_args = opt.opts.init(opts_data)
 
-if opt.show_hash_presets: show_hash_presets()
-
 if len(cmd_args) == 1:
 	infile = cmd_args[0]
 	check_infile(infile)

+ 1 - 1
mmgen/mn_electrum.py

@@ -26,7 +26,7 @@
 # list of words from:
 # http://en.wiktionary.org/wiki/Wiktionary:Frequency_lists/Contemporary_poetry
 
-electrum_words = """
+words = """
 able
 about
 above

+ 1 - 1
mmgen/mn_tirosh.py

@@ -48,7 +48,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 # THE SOFTWARE.
 
-tirosh_words = """
+words = """
 abraham
 absent
 absorb

+ 2 - 2
mmgen/mnemonic.py

@@ -127,8 +127,8 @@ def check_wordlist(wl,label):
 		Msg("ERROR: List is not sorted!")
 		sys.exit(3)
 
-from mmgen.mn_electrum  import electrum_words as el
-from mmgen.mn_tirosh    import tirosh_words   as tl
+from mmgen.mn_electrum  import words as el
+from mmgen.mn_tirosh    import words as tl
 wordlists = sorted(wl_checksums)
 
 def get_wordlist(wordlist):

+ 92 - 0
mmgen/obj.py

@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2015 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/>.
+
+"""
+obj.py:  The MMGenObject class and methods
+"""
+import mmgen.config as g
+from mmgen.util import msgrepr_exit,msgrepr
+
+lvl = 0
+
+class MMGenObject(object):
+
+	# Pretty-print any object of type MMGenObject, recursing into sub-objects
+	def __str__(self):
+		global lvl
+		indent = lvl * "    "
+
+		def fix_linebreaks(v,fixed_indent=None):
+			if "\n" in v:
+				i = indent+"    " if fixed_indent == None else fixed_indent*" "
+				return "\n"+i + v.replace("\n","\n"+i)
+			else: return repr(v)
+
+		def conv(v,col_w):
+			vret = ""
+			if type(v) == str:
+				if not (set(list(v)) <= set(list(g.printable))):
+					vret = repr(v)
+				else:
+					vret = fix_linebreaks(v,fixed_indent=0)
+			elif type(v) == int or type(v) == long:
+				vret = str(v)
+			elif type(v) == dict:
+				sep = "\n{}{}".format(indent," "*4)
+				cw = max(len(k) for k in v) + 2
+				t = sep.join(["{:<{w}}: {}".format(
+					repr(k),
+	(fix_linebreaks(v[k],fixed_indent=0) if type(v[k]) == str else v[k]),
+					w=cw)
+				for k in sorted(v)])
+				vret = "{" + sep + t + "\n" + indent + "}"
+			elif type(v) in (list,tuple):
+				sep = "\n{}{}".format(indent," "*4)
+				t = " ".join([repr(e) for e in sorted(v)])
+				o,c = ("[","]") if type(v) == list else ("(",")")
+				vret = o + sep + t + "\n" + indent + c
+			elif repr(v)[:14] == '<bound method ':
+				vret = " ".join(repr(v).split()[0:3]) + ">"
+#				vret = repr(v)
+
+			return vret or type(v)
+
+		out = []
+		def f(k): return k[:2] != "__"
+		keys = filter(f, dir(self))
+		col_w = max(len(k) for k in keys)
+		fs = "{}%-{}s: %s".format(indent,col_w)
+
+  		methods = [k for k in keys if repr(getattr(self,k))[:14] == '<bound method ']
+
+  		def f(k): return repr(getattr(self,k))[:14] == '<bound method '
+  		methods = filter(f,keys)
+  		def f(k): return repr(getattr(self,k))[:7] == '<mmgen.'
+  		objects = filter(f,keys)
+		other = list(set(keys) - set(methods) - set(objects))
+
+		for k in sorted(methods) + sorted(other) + sorted(objects):
+			val = getattr(self,k)
+			if str(type(val))[:13] == "<class 'mmgen": # recurse into sub-objects
+				out.append("\n%s%s (%s):" % (indent,k,repr(type(val))))
+				lvl += 1
+				out.append(str(getattr(self,k))+"\n")
+				lvl -= 1
+			else:
+				out.append(fs % (k, conv(val,col_w)))
+		return "\n".join(out)

+ 10 - 0
mmgen/opts.py

@@ -66,6 +66,15 @@ def typeconvert_from_dfl(key):
 		msg(fs % (opt.__dict__[key],opt.replace("_","-"),m))
 		sys.exit(1)
 
+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 init(opts_data,add_opts=[]):
 
 	if len(sys.argv) == 2 and sys.argv[1] == '--version':
@@ -113,6 +122,7 @@ def init(opts_data,add_opts=[]):
 			typeconvert_from_dfl(k)
 		else: opt.__dict__[k] = g.__dict__[k]
 
+	if opt.show_hash_presets: _show_hash_presets()
 	if opt.debug: opt.verbose = True
 
 	if g.debug:

+ 630 - 0
mmgen/seed.py

@@ -0,0 +1,630 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2015 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/>.
+
+"""
+seed.py:  Seed-related classes and methods for the MMGen suite
+"""
+import sys
+from binascii import hexlify,unhexlify
+
+import mmgen.config as g
+from mmgen.obj import *
+from mmgen.filename import *
+from mmgen.util import *
+from mmgen.bitcoin import b58encode_pad,b58decode_pad
+from mmgen.crypto import *
+
+class Seed(MMGenObject):
+	def __init__(self,seed_bin=None):
+		if not seed_bin:
+			from mmgen.crypto import get_random
+			# Truncate random data for smaller seed lengths
+			seed_bin = sha256(get_random(1033)).digest()[:opt.seed_len/8]
+		elif len(seed_bin)*8 not in g.seed_lens:
+			die(3,"%s: invalid seed length" % len(seed_bin))
+
+		self.data      = seed_bin
+		self.hexdata   = hexlify(seed_bin)
+		self.sid       = make_chksum_8(seed_bin)
+		self.len_bytes = len(seed_bin)
+		self.len_bits  = len(seed_bin) * 8
+
+class SeedSource(MMGenObject):
+
+	class SeedSourceData(MMGenObject): pass
+
+	desc = "seed source"
+	seed_opts = {
+		"mnemonic":     "Mnemonic",
+		"brain":        "Brainwallet",
+		"seed":         "SeedFile",
+		"incog":        "IncogWallet",
+		"incog_hex":    "IncogWalletHex",
+		"incog_hidden": "IncogWalletHidden",
+	}
+
+	def __init__(self,fn=None,seed=None,passwd=None):
+
+		self.ssdata = self.SeedSourceData()
+
+		if seed:
+			self.desc = "new " + self.desc
+			self.seed = seed
+			self.ssdata.passwd = passwd
+			self._pre_encode()
+			self._encode()
+		else:
+			self._get_formatted_data(fn)
+			self._deformat()
+			self._decode()
+
+	def _get_formatted_data(self,fn):
+		if fn:
+			self.infile = fn
+			self.fmt_data = get_data_from_file(fn.name,self.desc)
+		else:
+			self.infile = None
+			self.fmt_data = get_data_from_user(self.desc)
+
+	def _pre_encode(self): pass
+
+	def init(cls,fn=None,seed=None,passwd=None):
+
+		sstype = None
+		sopts=["%s_%s" % (l,k) for k in cls.seed_opts for l in "from","export"]
+		for o in sopts:
+			if o in opt.__dict__ and opt.__dict__[o]:
+				sstype = cls.seed_opts[o.split("_",1)[1]]
+				break
+
+		if seed:
+			return globals()[sstype or "Wallet"](seed=seed)
+		else:
+			if fn:
+				if opt.from_incog_hidden:
+					fn = Filename(fn,ftype="hincog")
+				else:
+					fn = Filename(fn)
+				sstype = fn.linked_obj
+				return globals()[sstype](fn=fn)
+			else:
+				return globals()[sstype or "Wallet"]()
+
+	init = classmethod(init)
+
+	def write_to_file(self):
+		self._format()
+		write_to_file_or_stdout(self._filename(),self.fmt_data, self.desc)
+
+class SeedSourceUnenc(SeedSource): pass
+
+class SeedSourceEnc(SeedSource):
+
+	_ss_enc_msg = {
+		'choose_passphrase': """
+You must choose a passphrase to encrypt your new %s 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 _pre_encode(self):
+		if not self.ssdata.passwd:
+			self._get_hash_preset()
+			self._get_first_passwd()
+		self._encrypt_seed()
+
+	def _get_first_passwd(self):
+		qmsg(self._ss_enc_msg['choose_passphrase'] % (self.desc,opt.hash_preset))
+		self.ssdata.passwd = get_new_passphrase(what=self.desc)
+
+	def _get_hash_preset(self):
+		self.ssdata.hash_preset = \
+			opt.hash_preset or get_hash_preset_from_user(what=self.desc)
+
+	def _encrypt_seed(self):
+		d = self.ssdata
+		d.salt     = sha256(get_random(128)).digest()[:g.salt_len]
+		key        = make_key(d.passwd, d.salt, d.hash_preset)
+		d.key_id   = make_chksum_8(key)
+		d.enc_seed = encrypt_seed(self.seed.data,key)
+
+class Mnemonic (SeedSourceUnenc):
+
+	desc = "mnemonic data"
+	wl_checksums = {
+		"electrum": '5ca31424',
+		"tirosh":   '1a5faeff'
+	}
+	mn_base = 1626
+	wordlists = sorted(wl_checksums)
+
+	def _mn2hex_pad(self,mn): return len(mn) * 8 / 3
+	def _hex2mn_pad(self,hexnum): return len(hexnum) * 3 / 8
+
+	def _baseNtohex(self,base,words,wl,pad=0):
+		deconv =  [wl.index(words[::-1][i])*(base**i)
+					for i in range(len(words))]
+		ret = ("{:0%sx}" % pad).format(sum(deconv))
+		return "%s%s" % (('0' if len(ret) % 2 else ''), ret)
+
+	def _hextobaseN(self,base,hexnum,wl,pad=0):
+		num,ret = int(hexnum,16),[]
+		while num:
+			ret.append(num % base)
+			num /= base
+		return [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]]
+
+	def _get_wordlist(self,wordlist=g.default_wordlist):
+		wordlist = wordlist.lower()
+		if wordlist not in self.wordlists:
+			die(1,'"%s": invalid wordlist.  Valid choices: %s' %
+				(wordlist,'"'+'" "'.join(self.wordlists)+'"'))
+
+		if wordlist == "electrum":
+			from mmgen.mn_electrum  import words
+		elif wordlist == "tirosh":
+			from mmgen.mn_tirosh    import words
+		else:
+			die(3,"Internal error: unknown wordlist")
+
+		return words.strip().split("\n")
+
+	def _encode(self):
+
+		wl = self._get_wordlist()
+		seed_hex = hexlify(self.seed.data)
+		mn = self._hextobaseN(self.mn_base,seed_hex,wl,self._hex2mn_pad(seed_hex))
+
+		rev = self._baseNtohex(self.mn_base,mn,wl,self._mn2hex_pad(mn))
+		if rev != seed_hex:
+			msg("ERROR: seed recomputed from wordlist doesn't match original seed!")
+			msg("Original seed:   %s" % seed_hex)
+			msg("Recomputed seed: %s" % rev)
+			sys.exit(3)
+
+		self.ssdata.mnemonic = mn
+
+	def _format(self):
+		self.fmt_data = " ".join(self.ssdata.mnemonic) + "\n"
+
+	def _deformat(self):
+
+		mn = self.fmt_data.split()
+		wl = self._get_wordlist()
+
+		if len(mn) not in g.mn_lens:
+			die(3,"Invalid mnemonic (%i words).  Allowed numbers of words: %s" %
+					(len(mn),", ".join([str(i) for i in g.mn_lens])))
+
+		for n,w in enumerate(mn,1):
+			if w not in wl:
+				die(3,"Invalid mnemonic: word #%s is not in the wordlist" % n)
+
+		self.ssdata.mnemonic = mn
+
+	def _decode(self):
+
+		mn = self.ssdata.mnemonic
+		wl = self._get_wordlist()
+
+		seed_hex = self._baseNtohex(self.mn_base,mn,wl,self._mn2hex_pad(mn))
+
+		rev = self._hextobaseN(self.mn_base,seed_hex,wl,self._hex2mn_pad(seed_hex))
+		if rev != mn:
+			msg("ERROR: mnemonic recomputed from seed not the same as original")
+			die(3,"Recomputed mnemonic:\n%s" % " ".join(rev))
+
+		qmsg("Valid mnemonic for seed ID %s" % make_chksum_8(unhexlify(seed_hex)))
+
+		self.seed = Seed(unhexlify(seed_hex))
+
+	def _filename(self):
+		return "%s.%s" % (self.seed.sid, g.mn_ext)
+
+class SeedFile (SeedSourceUnenc):
+
+	desc = "seed data"
+
+	def _encode(self):
+		b58seed = b58encode_pad(self.seed.data)
+		self.ssdata.chksum = make_chksum_6(b58seed)
+		self.ssdata.b58seed = b58seed
+
+	def _decode(self):
+
+		seed = b58decode_pad(self.ssdata.b58seed)
+		if seed == False:
+			msg("Invalid base 58 string: %s" % val)
+			return False
+
+		msg("Valid seed data for seed ID %s" % make_chksum_8(seed))
+		self.seed = Seed(seed)
+
+	def _format(self):
+		self.fmt_data = "%s %s\n" % (
+				self.ssdata.chksum,
+				split_into_columns(4,self.ssdata.b58seed)
+			)
+
+	def _deformat(self):
+		what = self.desc
+		ld = self.fmt_data.split()
+
+		if not (7 <= len(ld) <= 12): # 6 <= padded b58 data (ld[1:]) <= 11
+			msg("Invalid data length (%s) in %s" % (len(ld),what))
+			return False
+
+		a,b = ld[0],"".join(ld[1:])
+
+		if not is_chksum_6(a):
+			msg("'%s': invalid checksum format, in %s" % (a, what))
+			return False
+
+		if not is_b58string(b):
+			msg("'%s': not a base 58 string, in %s" % (b, what))
+			return False
+
+		vmsg_r("Validating %s checksum..." % what)
+
+		compare_chksums(a,"checksum",make_chksum_6(b),"base 58 data")
+
+		self.ssdata.chksum = a
+		self.ssdata.b58seed = b
+
+	def _filename(self):
+		return "%s.%s" % (self.seed.sid, g.seed_ext)
+
+class Wallet (SeedSourceEnc):
+
+	desc = "%s wallet" % g.proj_name
+
+	def _encode(self):
+		d = self.ssdata
+		d.label = opt.label or "No Label"
+		d.pw_status = "NE" if len(d.passwd) else "E"
+		d.timestamp = make_timestamp()
+
+	def _format(self):
+		d = self.ssdata
+		s = self.seed
+		s_fmt  = b58encode_pad(d.salt)
+		es_fmt = b58encode_pad(d.enc_seed)
+		lines = (
+			d.label,
+			"{} {} {} {} {}".format(s.sid.lower(), d.key_id.lower(),
+										s.len_bits, d.pw_status, d.timestamp),
+			"{}: {} {} {}".format(d.hash_preset,*get_hash_params(d.hash_preset)),
+			"{} {}".format(make_chksum_6(s_fmt),  split_into_columns(4,s_fmt)),
+			"{} {}".format(make_chksum_6(es_fmt), split_into_columns(4,es_fmt))
+		)
+		chksum = make_chksum_6(" ".join(lines))
+		self.fmt_data = "%s\n" % "\n".join((chksum,)+lines)
+
+	def _decode(self):
+		d = self.ssdata
+		# Needed for multiple transactions with {}-txsign
+		prompt_add = " "+self.infile.name if opt.quiet else ""
+		passwd = get_mmgen_passphrase(self.desc+prompt_add)
+		key = make_key(passwd, d.salt, d.hash_preset)
+		self.seed = Seed(decrypt_seed(d.enc_seed, key, d.seed_id, d.key_id))
+
+	def _check_master_chksum(self,lines):
+
+		if len(lines) != 6:
+			vmsg("Invalid number of lines (%s) in %s data" % (len(lines),self.desc))
+		elif not is_chksum_6(lines[0]):
+			vmsg("Incorrect Master checksum (%s) in %s data" % (lines[0],self.desc))
+		else:
+			chk = make_chksum_6(" ".join(lines[1:]))
+			if compare_chksums(lines[0],"master wallet",chk,"computed"):
+				return True
+
+		msg("Invalid %s data" % self.desc)
+		sys.exit(2)
+
+	def _deformat(self):
+
+		qmsg("Getting {} wallet data from file '{}'".format(
+			g.proj_name,self.infile.name))
+
+		lines = self.fmt_data.rstrip().split("\n")
+
+		self._check_master_chksum(lines)
+
+		d = self.ssdata
+		d.label = lines[1]
+
+		d1,d2,d3,d4,d5 = lines[2].split()
+		d.seed_id = d1.upper()
+		d.key_id  = d2.upper()
+		d.seed_len = int(d3)
+		d.pw_status,d.timestamp = d4,d5
+
+		hpdata = lines[3].split()
+		d.hash_preset = hpdata[0][:-1]  # a string!
+		hash_params = [int(i) for i in hpdata[1:]]
+
+		if hash_params != get_hash_params(d.hash_preset):
+			msg("Hash parameters '%s' don't match hash preset '%s'" %
+					(" ".join(hash_params), d.hash_preset))
+			sys.exit(3)
+
+		for i,key in (4,"salt"),(5,"enc_seed"):
+			l = lines[i].split(" ",1)
+			if len(l) != 2:
+				msg("Invalid format for %s in %s: %s" % (key,self.desc,val))
+				sys.exit(3)
+			chk,val = l[0],l[1].replace(" ","")
+			compare_chksums(chk,"wallet "+key,
+								make_chksum_6(val),"computed checksum")
+			val_bin = b58decode_pad(val)
+			if val_bin == False:
+				msg("Invalid base 58 number: %s" % val)
+				sys.exit(3)
+			setattr(d,key,val_bin)
+
+	def _filename(self):
+		return "{}-{}[{},{}].{}".format(
+				self.seed.sid,
+				self.ssdata.key_id,
+				self.seed.len_bits,
+				self.ssdata.hash_preset,
+				g.wallet_ext
+			)
+
+# 	def __str__(self):
+##	label,metadata,hash_preset,salt,enc_seed):
+# 		d = self.ssdata
+# 		s = self.seed
+# 		out = ["WALLET DATA"]
+# 		fs = "  {:18} {}"
+# 		pw_empty = "Yes" if d.metadata[3] == "E" else "No"
+# 		for i in (
+# 			("Label:",         d.label),
+# 			("Seed ID:",       s.sid),
+# 			("Key  ID:",       d.key_id),
+# 			("Seed length:",   "%s bits (%s bytes)" % (s.len_bits,s.len_bytes)),
+# 			("Scrypt params:", "Preset '%s' (%s)" % (opt.hash_preset,
+# 					" ".join([str(i) for i in get_hash_params(opt.hash_preset)])
+# 					)
+# 			),
+# 			("Passphrase empty?", pw_empty),
+# 			("Timestamp:",     "%s UTC" % d.metadata[4]),
+# 		): out.append(fs.format(*i))
+#
+# 		fs = "  {:6} {}"
+# 		for i in (
+# 			("Salt:",   ""),
+# 			("  b58:",  b58encode_pad(d.salt)),
+# 			("  hex:",  hexlify(d.salt)),
+# 			("Encrypted seed:", ""),
+# 			("  b58:",  b58encode_pad(d.enc_seed)),
+# 			("  hex:",  hexlify(d.enc_seed))
+# 		): out.append(fs.format(*i))
+#
+# 		return "\n".join(out)
+
+class Brainwallet (SeedSourceEnc):
+
+	desc = "brainwallet"
+
+	def _deformat(self):
+		self.brainpasswd = " ".join(self.fmt_data.split())
+
+	def _decode(self):
+		self._get_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(self.brainpasswd, "",
+					self.ssdata.hash_preset, buflen=opt.seed_len/8)
+		vmsg("Done")
+		self.seed = Seed(seed)
+
+
+class IncogWallet (SeedSourceEnc):
+
+	desc = "incognito wallet"
+
+	_icg_msg = {
+		'incog_iv_id': """
+Check that the generated Incog ID above is correct.  If it's not, then your
+incognito data is incorrect or corrupted.
+	""".strip(),
+		'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.
+	""".strip(),
+	'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 _make_iv_chksum(self,s): return sha256(s).hexdigest()[:8].upper()
+
+	def _get_incog_data_len(self,seed_len):
+		return g.aesctr_iv_len + g.salt_len + g.hincog_chk_len + seed_len/8
+
+	def _encode (self):
+		d = self.ssdata
+		# IV is used BOTH to initialize counter and to salt password!
+		d.iv = get_random(g.aesctr_iv_len)
+		d.iv_id = self._make_iv_chksum(d.iv)
+		msg("Incog ID: %s" % d.iv_id)
+
+		d.salt = get_random(g.salt_len)
+		key = make_key(d.passwd, d.salt, d.hash_preset, "incog wallet key")
+		chk = sha256(self.seed.data).digest()[:8]
+		d.enc_seed = encrypt_data(chk + self.seed.data, key, 1, "seed")
+
+		d.wrapper_key = make_key(d.passwd, d.iv, d.hash_preset, "incog wrapper key")
+		d.key_id = make_chksum_8(d.wrapper_key)
+		d.data_len = self._get_incog_data_len(opt.seed_len)
+
+	def _format(self):
+		d = self.ssdata
+		self.fmt_data = d.iv + encrypt_data(
+							d.salt + d.enc_seed,
+							d.wrapper_key,
+							int(hexlify(d.iv),16),
+							"incog data"
+						)
+
+	def _filename(self):
+		return "{}-{}-{}[{},{}].{}".format(
+				self.seed.sid,
+				self.ssdata.key_id,
+				self.ssdata.iv_id,
+				self.seed.len_bits,
+				self.ssdata.hash_preset,
+				g.incog_ext
+			)
+
+	def _deformat(self):
+
+		# Data could be of invalid length, so check:
+		valid_dlens = map(self._get_incog_data_len, g.seed_lens)
+		# => [56, 64, 72]
+		raw_d = self.fmt_data
+		if len(raw_d) not in valid_dlens:
+			die(1,
+		"Invalid incognito file size: %s.  Valid sizes (in bytes): %s" %
+				(len(raw_d), " ".join(map(str, valid_dlens))))
+
+		d = self.ssdata
+		d.iv             = raw_d[0:g.aesctr_iv_len]
+		d.incog_id       = self._make_iv_chksum(d.iv)
+		d.enc_incog_data = raw_d[g.aesctr_iv_len:]
+		msg("Incog ID: %s" % d.incog_id)
+		qmsg("Check the applicable value against your records.")
+		k = 'incog_iv_id_hidden' if opt.from_incog_hidden else 'incog_iv_id'
+		vmsg("\n%s\n" % self._icg_msg[k])
+
+	def _decode(self):
+		d = self.ssdata
+		prompt_info="{} incognito wallet".format(g.proj_name)
+
+		while True:
+			passwd = get_mmgen_passphrase(prompt_info+" "+d.incog_id)
+
+			qmsg("Configured hash presets: %s" %
+						" ".join(sorted(g.hash_presets)))
+			d.hash_preset = get_hash_preset_from_user(what="incog wallet")
+
+			# IV is used BOTH to initialize counter and to salt password!
+			key = make_key(passwd, d.iv, d.hash_preset, "wrapper key")
+			dd = decrypt_data(d.enc_incog_data, key,
+					int(hexlify(d.iv),16), "incog data")
+
+			d.salt     = dd[0:g.salt_len]
+			d.enc_seed = dd[g.salt_len:]
+
+			key = make_key(passwd, d.salt, d.hash_preset, "main key")
+			vmsg("Key ID: %s" % make_chksum_8(key))
+
+			ret = decrypt_seed(d.enc_seed, key, "", "")
+
+			chk,seed_maybe = ret[:8],ret[8:]
+			if sha256(seed_maybe).digest()[:8] == chk:
+				msg("Passphrase and hash preset are correct")
+				seed = seed_maybe
+				break
+			else:
+				msg("Incorrect passphrase or hash preset")
+
+		self.seed = Seed(seed)
+
+
+class IncogWalletHex (IncogWallet):
+
+	def _deformat(self):
+		self.fmt_data = decode_pretty_hexdump(self.fmt_data)
+		IncogWallet._deformat(self)
+
+
+class IncogWalletHidden (IncogWallet):
+
+	def _parse_hincog_opt(self):
+		class HincogParams(MMGenObject): pass
+		o = opt.from_incog_hidden or opt.export_incog_hidden
+		p = HincogParams()
+		a,b = o.split(",")
+		p.filename = a
+		p.offset   = int(b)
+		return p
+
+	def _check_valid_offset(self,fn,action):
+		d = self.ssdata
+		if fn.size < d.hincog_offset + d.data_len:
+			die(1,
+"Destination file has length %s, too short to %s %s bytes of data at offset %s"
+				% (f.size,action,d.data_len,d.hincog_offset))
+
+
+	# overrides method in SeedSource
+	def _get_formatted_data(self,fn):
+		if fn: die(1,
+"Specify the filename as a parameter of the '--from-hidden-incog' option")
+		d = self.ssdata
+		p = self._parse_hincog_opt()
+		d.hincog_offset = p.offset
+		self.infile = Filename(p.filename,ftype="hincog")
+
+		qmsg("Getting hidden incog data from file '%s'" % self.infile.name)
+
+		# Already sanity-checked:
+		d.data_len = self._get_incog_data_len(opt.seed_len)
+		self._check_valid_offset(self.infile,"read")
+
+		import os
+		fh = os.open(self.infile.name,os.O_RDONLY)
+		os.lseek(fh,int(p.offset),os.SEEK_SET)
+		self.fmt_data = os.read(fh,d.data_len)
+		os.close(fh)
+		qmsg("Data read from file '%s' at offset %s" %
+				(self.infile.name,p.offset), "Data read from file")
+
+
+	# overrides method in SeedSource
+	def write_to_file(self):
+		d = self.ssdata
+		self._format()
+		compare_or_die(d.data_len, "target data length",
+				len(self.fmt_data),"length of formatted " + self.desc)
+		p = self._parse_hincog_opt()
+		d.hincog_offset = p.offset
+		self.outfile = f = Filename(p.filename,ftype="hincog")
+
+		if opt.debug:
+			Msg("Incog data len %s, offset %s" % (d.data_len,p.offset))
+		self._check_valid_offset(f,"write")
+
+		if not opt.quiet: confirm_or_exit("","alter file '%s'" % f.name)
+		import os
+		fh = os.open(f.name,os.O_RDWR)
+		os.lseek(fh, int(p.offset), os.SEEK_SET)
+		os.write(fh, self.fmt_data)
+		os.close(fh)
+		msg("Data written to file '%s' at offset %s" % (f.name,p.offset))

+ 89 - 65
mmgen/util.py

@@ -20,9 +20,10 @@
 util.py:  Low-level routines imported by other modules for the MMGen suite
 """
 
-import sys
+import sys,os,time,stat,re
 from hashlib import sha256
 from binascii import hexlify,unhexlify
+from string import hexdigits
 
 import mmgen.config as g
 
@@ -34,10 +35,25 @@ def green(s):  return _grn+s+_reset
 def yellow(s): return _yel+s+_reset
 def cyan(s):   return _cya+s+_reset
 
-def msgred(s):    sys.stderr.write(red(s+"\n"))
-
 def msg(s):    sys.stderr.write(s+"\n")
 def msg_r(s):  sys.stderr.write(s)
+def Msg(s):    sys.stdout.write(s + "\n")
+def Msg_r(s):  sys.stdout.write(s)
+def msgred(s): sys.stderr.write(red(s+"\n"))
+def msgrepr(*args):
+	for d in args:
+		sys.stdout.write(repr(d)+"\n")
+def msgrepr_exit(*args):
+	for d in args:
+		sys.stdout.write(repr(d)+"\n")
+	sys.exit()
+
+def die(ev,s):
+	sys.stderr.write(s+"\n"); sys.exit(ev)
+def Die(ev,s):
+	sys.stdout.write(s+"\n"); sys.exit(ev)
+
+import opt
 def qmsg(s,alt=False):
 	if opt.quiet:
 		if alt != False: sys.stderr.write(alt + "\n")
@@ -51,26 +67,11 @@ def vmsg(s):
 def vmsg_r(s):
 	if opt.verbose: sys.stderr.write(s)
 
-def Msg(s):    sys.stdout.write(s + "\n")
-def Msg_r(s):  sys.stdout.write(s)
 def Vmsg(s):
 	if opt.verbose: sys.stdout.write(s + "\n")
 def Vmsg_r(s):
 	if opt.verbose: sys.stdout.write(s)
 
-def die(ev,s):
-	sys.stderr.write(s+"\n"); sys.exit(ev)
-def Die(ev,s):
-	sys.stdout.write(s+"\n"); sys.exit(ev)
-
-def msgrepr(*args):
-	for d in args:
-		sys.stdout.write(repr(d)+"\n")
-def msgrepr_exit(*args):
-	for d in args:
-		sys.stdout.write(repr(d)+"\n")
-	sys.exit()
-
 def suf(arg,what):
 	t = type(arg)
 	if t == int:
@@ -87,7 +88,6 @@ def suf(arg,what):
 		return "" if n == 1 else "s"
 
 def get_extension(f):
-	import os
 	return os.path.splitext(f)[1][1:]
 
 def make_chksum_N(s,n,sep=False):
@@ -100,6 +100,8 @@ 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 is_chksum_6(s): return len(s) == 6 and is_hexstring_lc(s)
+
 def make_iv_chksum(s): return sha256(s).hexdigest()[:8].upper()
 
 def splitN(s,n,sep=None):                      # always return an n-element list
@@ -108,25 +110,31 @@ def splitN(s,n,sep=None):                      # always return an n-element list
 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 split_into_columns(col_wid,s):
+	return " ".join([s[col_wid*i:col_wid*(i+1)]
+					for i in range(len(s)/col_wid+1)]).rstrip()
 
 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 _is_hex(s):
-	try: int(s,16)
-	except: return False
-	else: return True
+def _is_whatstring(s,chars):
+	return set(list(s)) <= set(chars)
+
+def is_hexstring(s):
+	return _is_whatstring(s.lower(),hexdigits.lower())
+def is_hexstring_lc(s):
+	return _is_whatstring(s,hexdigits.lower())
+def is_hexstring_uc(s):
+	return _is_whatstring(s,hexdigits.upper())
+def is_b58string(s):
+	from mmgen.bitcoin import b58a
+	return _is_whatstring(s,b58a)
 
 def is_utf8(s):
 	try: s.decode("utf8")
@@ -154,7 +162,6 @@ def pretty_hexdump(data,gw=2,cols=8,line_nums=False):
 	).rstrip()
 
 def decode_pretty_hexdump(data):
-	import re
 	from string import hexdigits
 	lines = [re.sub('^['+hexdigits+']+:\s+','',l) for l in data.split("\n")]
 	return unhexlify("".join(("".join(lines).split())))
@@ -166,32 +173,29 @@ def get_hash_params(hash_preset):
 		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)
+def compare_chksums(chk1, desc1, chk2, desc2, die=True):
 
-def compare_checksums(chksum1, desc1, chksum2, desc2):
+	if not chk1 == chk2:
+		if die:
+			die(3,"Checksum error: %s checksum (%s) doesn't match %s checksum (%s)"
+				% (desc2,chk2,desc1,chk1))
+		else: return False
 
-	if chksum1.lower() == chksum2.lower():
-		vmsg("OK (%s)" % chksum1.upper())
-		return True
-	else:
-		if opt.debug:
-			Msg(
-	"ERROR: Computed checksum %s (%s) doesn't match checksum %s (%s)"
-			% (desc1,chksum1,desc2,chksum2))
-		return False
+	vmsg("%s checksum OK (%s)" % (desc1.capitalize(),chk1))
+	return True
+
+def compare_or_die(val1, desc1, val2, desc2):
+	if cmp(val1,val2):
+		die(3,"Error: %s (%s) doesn't match %s (%s)"
+				% (desc2,val2,desc1,val1))
+	vmsg("%s OK (%s)" % (desc2.capitalize(),val2))
+	return True
 
 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
+	wl_id = g.default_wordlist
+	if wl_id == "electrum": from mmgen.mn_electrum import words as wl
+	elif wl_id == "tirosh": from mmgen.mn_tirosh   import words as wl
 	return wl.strip().split("\n")
 
 def open_file_or_exit(filename,mode):
@@ -250,7 +254,6 @@ def _validate_addr_num(n):
 
 
 def make_full_path(outdir,outfile):
-	import os
 	return os.path.normpath(os.path.join(outdir, os.path.basename(outfile)))
 
 
@@ -282,7 +285,7 @@ def parse_addr_idxs(arg,sep=","):
 	return sorted(set(ret))
 
 
-def get_new_passphrase(what,  passchg=False):
+def get_new_passphrase(what,passchg=False):
 
 	w = "{}passphrase for {}".format("new " if passchg else "", what)
 	if opt.passwd_file:
@@ -330,7 +333,6 @@ def write_to_stdout(data, what, confirm=True):
 		confirm_or_exit("",'output {} to screen'.format(what))
 	elif not sys.stdout.isatty():
 		try:
-			import os
 			of = os.readlink("/proc/%d/fd/1" % os.getpid())
 			of_maybe = os.path.relpath(of)
 			of = of if of_maybe.find(os.path.pardir) == 0 else of_maybe
@@ -344,8 +346,7 @@ def write_to_file(outfile,data,what="data",confirm_overwrite=False,verbose=False
 
 	if opt.outdir: outfile = make_full_path(opt.outdir,outfile)
 
-	from os import stat
-	try:    stat(outfile)
+	try:    os.stat(outfile)
 	except: pass
 	else:
 		if confirm_overwrite:
@@ -424,8 +425,8 @@ def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed):
 		label,
 		"{} {} {} {} {}".format(*metadata),
 		"{}: {} {} {}".format(hash_preset,*get_hash_params(hash_preset)),
-		"{} {}".format(make_chksum_6(sf),  col4(sf)),
-		"{} {}".format(make_chksum_6(esf), col4(esf))
+		"{} {}".format(make_chksum_6(sf),  split_into_columns(4,sf)),
+		"{} {}".format(make_chksum_6(esf), split_into_columns(4,esf))
 	)
 
 	chk = make_chksum_6(" ".join(lines))
@@ -450,7 +451,7 @@ def _check_mmseed_format(words):
 
 	if len(words) < 3 or len(words) > 12:
 		msg("Invalid data length (%s) in %s" % (len(words),what))
-	elif not _is_hex(words[0]):
+	elif not is_hexstring(words[0]):
 		msg("Invalid format of checksum '%s' in %s"%(words[0], what))
 	elif chklen != 6:
 		msg("Incorrect length of checksum (%s) in %s" % (chklen,what))
@@ -468,7 +469,7 @@ def _check_wallet_format(infile, lines):
 		vmsg("Invalid number of lines (%s) in %s" % (len(lines),what))
 	elif chklen != 6:
 		vmsg("Incorrect length of Master checksum (%s) in %s" % (chklen,what))
-	elif not _is_hex(lines[0]):
+	elif not is_hexstring(lines[0]):
 		vmsg("Invalid format of Master checksum '%s' in %s"%(lines[0], what))
 	else: valid = True
 
@@ -555,7 +556,6 @@ def get_words(infile,what,prompt):
 		return _get_words_from_user(prompt)
 
 def remove_comments(lines):
-	import re
 	# re.sub(pattern, repl, string, count=0, flags=0)
 	ret = []
 	for i in lines:
@@ -573,6 +573,11 @@ def get_lines_from_file(infile,what="",trim_comments=False):
 	return remove_comments(lines) if trim_comments else lines
 
 
+def get_data_from_user(what="data",silent=False):
+	data = my_raw_input("Enter %s: " % what, echo=opt.echo_passphrase)
+	if opt.debug: Msg("User input: [%s]" % data)
+	return data
+
 def get_data_from_file(infile,what="data",dash=False,silent=False):
 	if dash and infile == "-": return sys.stdin.read()
 	if not silent:
@@ -595,7 +600,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_chksums(chk, "seed", stored_chk, "input",die=False):
 		seed = b58decode_pad(seed_b58)
 		if seed == False:
 			msg("Invalid b58 number: %s" % val)
@@ -639,7 +644,6 @@ def get_bitcoind_passphrase(prompt):
 
 def check_data_fits_file_at_offset(fname,offset,dlen,action):
 	# TODO: Check for Windows
-	import os, stat
 	if stat.S_ISBLK(os.stat(fname).st_mode):
 		fd = os.open(fname, os.O_RDONLY)
 		fsize = os.lseek(fd, 0, os.SEEK_END)
@@ -656,9 +660,9 @@ def check_data_fits_file_at_offset(fname,offset,dlen,action):
 
 from mmgen.term import kb_hold_protect,get_char
 
-def get_hash_preset_from_user(hp='3',what="data"):
-	p = "Enter hash preset for %s, or ENTER to accept the default ('%s'): " \
-			% (what,hp)
+def get_hash_preset_from_user(hp=g.hash_preset,what="data"):
+	p = """Enter hash preset for %s,
+or hit ENTER to accept the default value ('%s'): """ % (what,hp)
 	while True:
 		ret = my_raw_input(p)
 		if ret:
@@ -721,3 +725,23 @@ def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False):
 
 		if verbose: msg("\nInvalid reply")
 		else: msg_r("\r")
+
+
+def do_license_msg(immed=False):
+
+	from mmgen.license import gpl
+	if opt.quiet or g.no_license: return
+
+	msg(gpl['warning'])
+	prompt = "%s " % gpl['prompt'].strip()
+
+	while True:
+		reply = get_char(prompt, immed_chars="wc" if immed else "")
+		if reply == 'w':
+			from mmgen.term import do_pager
+			do_pager(gpl['conditions'])
+		elif reply == 'c':
+			msg(""); break
+		else:
+			msg_r("\r")
+	msg("")

+ 4 - 4
test/test.py

@@ -407,8 +407,8 @@ class MMGenExpect(object):
 				passphrase+"\n",regex=True)
 
 	def hash_preset(self,what,preset=''):
-		my_expect(self.p,("Enter hash preset for %s, or ENTER .*?:" % what),
-				str(preset)+"\n",regex=True)
+		my_expect(self.p,("Enter hash preset for %s," % what))
+		my_expect(self.p,("or hit ENTER .*?:"), str(preset)+"\n",regex=True)
 
 	def written_to_file(self,what,overwrite_unlikely=False,query="Overwrite?  "):
 		s1 = "%s written to file " % what
@@ -505,8 +505,8 @@ def add_comments_to_addr_file(addrfile,tfile):
 
 def make_brainwallet_file(fn):
 	# Print random words with random whitespace in between
-	from mmgen.mn_tirosh import tirosh_words
-	wl = tirosh_words.split("\n")
+	from mmgen.mn_tirosh import words
+	wl = words.split("\n")
 	nwords,ws_list,max_spaces = 10,"    \n",5
 	def rand_ws_seq():
 		nchars = getrandnum(1) % max_spaces + 1