Browse Source

New feature: export wallet to incognito format

  Incognito wallet is 48, 56 or 64 bytes of apparently random data.

  Allows user to hide wallet data in a pre-existing file or on a disk
  partition (preferably filled in advance with random data).

  Can be used to hide wallet securely in unencrypted cloud storage or
  on paper, without revealing the nature of the data.

  Data may be written at a user-specified offset into the file or
  partition, in which case user must remember the offset.

  Each export operation uses a new random init vector to create different
  data each time.  This allows the user to hide wallets at different
  locations on the Net without detection.

  User must remember hash preset in addition to passphrase (though trial
  and error can be used if it's forgotten).

  Fully integrated with address generation and tx signing operations.
philemon 10 years ago
parent
commit
a3c4bd8731
11 changed files with 384 additions and 125 deletions
  1. 14 6
      mmgen-addrgen
  2. 5 3
      mmgen-txsign
  3. 65 11
      mmgen-walletchk
  4. 10 6
      mmgen-walletgen
  5. 60 7
      mmgen/Opts.py
  6. 7 1
      mmgen/addr.py
  7. 6 2
      mmgen/config.py
  8. 1 1
      mmgen/license.py
  9. 12 6
      mmgen/tx.py
  10. 200 78
      mmgen/util.py
  11. 4 4
      scripts/bitcoind-walletunlock.py

+ 14 - 6
mmgen-addrgen

@@ -66,6 +66,9 @@ help_data = {
 -b, --from-brain     l,p Generate {W} from a user-created password,
                          i.e. a "brainwallet", using seed length 'l' and
                          hash preset 'p' (comma-separated)
+-g, --from-incognito     Generate {W} from an incognito-format wallet
+-G, --hidden-incog-data  f,o,l  Generate {W} from incognito data in file
+                                'f' at offset 'o', with seed length of 'l'
 -m, --from-mnemonic      Generate {W} from an electrum-like mnemonic
 -s, --from-seed          Generate {W} from a seed in .{S} format
 
@@ -107,12 +110,14 @@ in all future invocations with that passphrase
 	)
 }
 
-short_opts = ["h","A","d:","e","H","K","l:","p:","P:","q","S",
-				"v","x","b:","m","s"]
+short_opts = ["h","A","d:","e",
+			"H","K","l:","p:",
+			"P:","q","S","v","x","b:",
+			"g","G:","m","s"]
 long_opts = ["help","no_addresses","outdir=","echo_passphrase",
 			"show_hash_presets","no_keyconv","seed_len=","hash_preset=",
 			"passwd_file=","quiet","stdout","verbose","b16","from_brain=",
-			"from_mnemonic","from_seed"]
+			"from_incognito","hidden_incog_data=","from_mnemonic","from_seed"]
 
 if invoked_as == "addrgen":
 	for i in "A","x":              short_opts.remove(i)
@@ -123,6 +128,7 @@ opts,cmd_args = process_opts(sys.argv,help_data,"".join(short_opts),long_opts)
 if 'show_hash_presets' in opts: show_hash_presets()
 if 'quiet' in opts: g.quiet = True
 if 'verbose' in opts: g.verbose = True
+if 'hidden_incog_data' in opts: opts['from_incognito'] = True
 
 opts['gen_what'] = gen_what
 
@@ -131,9 +137,10 @@ check_opts(opts,long_opts)
 if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 
 if len(cmd_args) == 1 and (
-			'from_mnemonic' in opts or
-			'from_brain' in opts or
-			'from_seed' in opts
+			'from_mnemonic' in opts
+			or 'from_brain' in opts
+			or 'from_seed' in opts
+			or 'from_incognito' in opts
 		):
 	infile,addr_list_arg = "",cmd_args[0]
 elif len(cmd_args) == 2:
@@ -160,6 +167,7 @@ else:
 
 seed          = get_seed_retry(infile,opts)
 seed_id       = make_chksum_8(seed)
+
 addr_data     = generate_addrs(seed, addr_list, opts)
 addr_data_str = format_addr_data(addr_data, seed_id, opts)
 

+ 5 - 3
mmgen-txsign

@@ -46,9 +46,10 @@ help_data = {
 -b, --from-brain     l,p Generate keys from a user-created password,
                          i.e. a "brainwallet", using seed length 'l' and
                          hash preset 'p'
+-w, --use-wallet-dat     Get keys from a running bitcoind
+-g, --from-incognito     Generate keys from an incognito-format wallet
 -m, --from-mnemonic      Generate keys from an electrum-like mnemonic
 -s, --from-seed          Generate keys from a seed in .{} format
--w, --use-wallet-dat     Get keys from a running bitcoind
 
 Transactions with either mmgen or non-mmgen input addresses may be signed.
 For non-mmgen inputs, the bitcoind wallet.dat is used as the key source.
@@ -77,10 +78,11 @@ Seed data supplied in files must have the following extensions:
 """.format(g.seed_ext,g.wallet_ext,g.seed_ext,g.mn_ext,g.brain_ext)
 }
 
-short_opts = "hd:eiIk:P:qVb:msw"
+short_opts = "hd:eiIk:P:qVb:wgms"
 long_opts  = "help","outdir=","echo_passphrase","info","tx_id",\
 			"keys_from_file=","passwd_file=","quiet","skip_key_preverify",\
-			"from_brain=","from_mnemonic","from_seed","use_wallet_dat"
+			"from_brain=","use_wallet_dat",\
+			"from_incognito","from_mnemonic","from_seed"
 
 opts,infiles = process_opts(sys.argv,help_data,short_opts,long_opts)
 if "quiet" in opts: g.quiet = True

+ 65 - 11
mmgen-walletchk

@@ -35,22 +35,27 @@ help_data = {
 -h, --help             Print this help message
 -d, --outdir        d  Specify an alternate directory 'd' for output
 -e, --echo-passphrase  Print passphrase to screen when typing it
--m, --export-mnemonic  Export the wallet's mnemonic to file
 -P, --passwd-file   f  Get passphrase from file 'f'
 -q, --quiet            Suppress warnings; overwrite files without prompting
--s, --export-seed      Export the wallet's seed to file
 -S, --stdout           Print seed or mnemonic data to standard output
 -v, --verbose          Produce more verbose output
+-g, --export-incognito Export wallet to incognito format
+-G, --hide-incog-data  f,o  Hide incognito data in existing file 'f'
+                            at offset 'o' (comma-separated)
+-m, --export-mnemonic  Export the wallet's mnemonic to file
+-s, --export-seed      Export the wallet's seed to file
 """
 }
 
-short_opts = "hd:emP:qsSv"
-long_opts  = "help","outdir=","echo_passphrase","export_mnemonic",\
-				"passwd_file=","quiet","export_seed","stdout","verbose"
+short_opts = "hd:eP:qSvgG:ms"
+long_opts  = "help","outdir=","echo_passphrase","passwd_file=","quiet",\
+			"stdout","verbose",\
+			"export_incognito","hide_incog_data=","export_mnemonic","export_seed"
 
 opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
 if 'quiet' in opts: g.quiet = True
 if 'verbose' in opts: g.verbose = True
+if 'hide_incog_data' in opts: opts['export_incognito'] = True
 
 # Argument sanity checks and processing:
 check_opts(opts,long_opts)
@@ -61,20 +66,69 @@ check_infile(cmd_args[0])
 
 if 'export_mnemonic' in opts:
 	qmsg("Exporting mnemonic data to file by user request")
-if 'export_seed' in opts:
+elif 'export_seed' in opts:
 	qmsg("Exporting seed data to file by user request")
+elif 'export_incognito' in opts:
+	qmsg("Exporting wallet to incognito format by user request")
+
+	d = get_data_from_wallet(cmd_args[0],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)
+
+	from Crypto import Random
+	iv = Random.new().read(g.aesctr_iv_len)
+	iv_id = make_chksum_8(iv)
+	qmsg("IV ID: %s" % iv_id)
+
+	from binascii import hexlify
+	from hashlib import sha256
+	# IV is used BOTH to initialize counter and to salt password!
+	key = make_key(passwd, iv, preset, "wrapper key")
+	incog_enc = encrypt_seed(salt + enc_seed, key, iv=int(hexlify(iv),16))
+
+	if "hide_incog_data" in opts:
+		fname,offset = opts['hide_incog_data'].split(",") # Already sanity-checked
+		offset = int(offset)
+
+		check_data_fits_file_at_offset(fname,offset,len(iv + incog_enc),"write")
+
+		if not g.quiet: confirm_or_exit("","alter file '%s'" % fname)
+		f = os.open(fname,os.O_RDWR)
+		os.lseek(f, offset, os.SEEK_SET)
+		os.write(f, iv + incog_enc)
+		os.close(f)
+		qmsg("Data written to file '%s' at offset %s" % (fname,offset),
+				"Data written to file")
+	else:
+		fn = "%s-%s-%s[%s,%s].%s" % (seed_id, key_id, iv_id,
+			len(enc_seed)*8, preset, g.incog_ext)
+		export_to_file(fn, iv + incog_enc, "incognito wallet data", opts)
+
+	sys.exit()
 
 seed = get_seed_from_wallet(cmd_args[0], opts)
 if seed: qmsg("Wallet is OK")
+else:
+	msg("Error opening wallet")
+	sys.exit(2)
 
 if 'export_mnemonic' in opts:
 	wl = get_default_wordlist()
-
 	from mmgen.mnemonic import get_mnemonic_from_seed
 	p = True if g.debug else False
 	mn = get_mnemonic_from_seed(seed, wl, g.default_wl, print_info=p)
+	fn = "%s.%s" % (make_chksum_8(seed).upper(), g.mn_ext)
+	export_to_file(fn, " ".join(mn)+"\n", "mnemonic data", opts)
 
-	write_mnemonic(mn, seed, opts)
-
-if 'export_seed' in opts:
-	write_seed(seed, opts)
+elif 'export_seed' in opts:
+	from mmgen.bitcoin import b58encode_pad
+	data = col4(b58encode_pad(seed))
+	chk = make_chksum_6(b58encode_pad(seed))
+	fn = "%s.%s" % (make_chksum_8(seed).upper(), g.seed_ext)
+	export_to_file(fn, "%s %s\n" % (chk,data), "seed data", opts)

+ 10 - 6
mmgen-walletgen

@@ -54,6 +54,7 @@ help_data = {
 -b, --from-brain       l,p Generate wallet from a user-created passphrase,
                            i.e. a "brainwallet", using seed length 'l' and
                            hash preset 'p' (comma-separated)
+-g, --from-incognito       Generate wallet from an incognito-format wallet
 -m, --from-mnemonic        Generate wallet from an Electrum-like mnemonic
 -s, --from-seed            Generate wallet from a seed in .{S} format
 
@@ -93,10 +94,10 @@ in all future invocations with that passphrase.
 	)
 }
 
-short_opts = "hd:eHl:L:p:P:qu:vb:ms"
+short_opts = "hd:eHl:L:p:P:qu:vb:gms"
 long_opts  = "help","outdir=","echo_passphrase","show_hash_presets","seed_len=",\
-			"label=","hash_preset=","passwd_file=","quiet","usr_randlen=",\
-			"verbose","from_brain=","from_mnemonic","from_seed"
+			"label=","hash_preset=","passwd_file=","quiet","usr_randlen=","verbose",\
+			"from_brain=","from_incognito","from_mnemonic","from_seed"
 
 opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
 if 'quiet' in opts: g.quiet = True
@@ -111,7 +112,7 @@ if len(cmd_args) == 1:
 	infile = cmd_args[0]
 	check_infile(infile)
 	ext = infile.split(".")[-1]
-	ok_exts = g.seed_ext, g.mn_ext, g.brain_ext
+	ok_exts = g.seedfile_exts
 	for e in ok_exts:
 		if e == ext: break
 	else:
@@ -151,10 +152,13 @@ if g.debug: display_user_random_data(usr_keys,key_timings)
 usr_rand_data = sha256(usr_keys).digest() + \
 				sha256("".join(key_timings)).digest()
 
-for i in 'from_mnemonic','from_brain','from_seed':
+for i in 'from_mnemonic','from_brain','from_seed','from_incognito':
 	if infile or (i in opts):
 		seed = get_seed_retry(infile,opts)
-		qmsg(""); break
+		if "from_incognito" in opts or get_extension(infile) == g.incog_ext:
+			qmsg(cmessages['incognito'] % make_chksum_8(seed))
+		else: qmsg("")
+		break
 else:
 	# Truncate random data for smaller seed lengths
 	seed = os_rand_data[0] + usr_rand_data

+ 60 - 7
mmgen/Opts.py

@@ -18,7 +18,6 @@
 
 import sys, getopt
 import mmgen.config as g
-from mmgen.util import msg,check_infile
 
 def usage(hd):
 	print "USAGE: %s %s" % (hd['prog_name'], hd['usage'])
@@ -70,6 +69,15 @@ def process_opts(argv,help_data,short_opts,long_opts):
 	return opts,args
 
 
+def show_opts_and_cmd_args(opts,cmd_args):
+	print "Processed options:     %s" % repr(opts)
+	print "Cmd args:              %s" % repr(cmd_args)
+
+
+# Everything below here is MMGen-specific:
+
+from mmgen.util import msg,check_infile
+
 def check_opts(opts,long_opts):
 
 	# These must be set to the default values in mmgen.config:
@@ -91,6 +99,7 @@ def check_opts(opts,long_opts):
 		if opt == 'outdir':
 			what = "output directory"
 			import re, os, stat
+			# TODO Non-portable:
 			d = re.sub(r'/*$','', val)
 			opts[opt] = d
 
@@ -121,12 +130,61 @@ def check_opts(opts,long_opts):
 "%s": illegal character in label.  Only ASCII characters are permitted.
 """.strip() % ch)
 					sys.exit(1)
+		elif opt == 'hide_incog_data' or opt == 'hidden_incog_data':
+			try:
+				if opt == 'hide_incog_data':
+					outfile,offset = val.split(",")
+				else:
+					outfile,offset,seed_len = val.split(",")
+			except:
+				msg("'%s': invalid %s" % (val,what))
+				sys.exit(1)
+
+			try:
+				o = int(offset)
+			except:
+				msg("'%s': invalid 'o' %s (not an integer)" % (offset,what))
+				sys.exit(1)
+
+			if o < 0:
+				msg("'%s': invalid 'o' %s (less than zero)" % (offset,what))
+				sys.exit(1)
+
+			if opt == 'hidden_incog_data':
+				try:
+					sl = int(seed_len)
+				except:
+					msg("'%s': invalid 'l' %s (not an integer)" % (sl,what))
+					sys.exit(1)
+
+				if sl not in g.seed_lens:
+					msg("'%s': invalid 'l' %s (valid choices: %s)" %
+						(sl,what," ".join(str(i) for i in g.seed_lens)))
+					sys.exit(1)
+
+			import os, stat
+			try: mode = os.stat(outfile).st_mode
+			except:
+				msg("Unable to stat requested %s '%s'" % (what,outfile))
+				sys.exit(1)
+
+			if not (stat.S_ISREG(mode) or stat.S_ISBLK(mode)):
+				msg("Requested %s '%s' is not a file or block device" %
+						(what,outfile))
+				sys.exit(1)
+
+			ac,m = (os.W_OK,"writ") \
+				if "hide_incog_data" in opts else (os.R_OK,"read")
+			if not os.access(outfile, ac):
+				msg("Requested %s '%s' is un%sable by you" % (what,outfile,m))
+				sys.exit(1)
+
 		elif opt == 'from_brain':
 			try:
 				l,p = val.split(",")
 			except:
 				msg("'%s': invalid %s" % (val,what))
-				sys.exit(1)
+				sys.exit(2)
 
 			try:
 				int(l)
@@ -162,11 +220,6 @@ def check_opts(opts,long_opts):
 			if g.debug: print "check_opts(): No test for opt '%s'" % opt
 
 
-def show_opts_and_cmd_args(opts,cmd_args):
-	print "Processed options:     %s" % repr(opts)
-	print "Cmd args:              %s" % repr(cmd_args)
-
-
 def set_if_unset_and_typeconvert(opts,opt):
 
 	if opt in g.cl_override_vars:

+ 7 - 1
mmgen/addr.py

@@ -63,6 +63,9 @@ def generate_addrs(seed, addrnums, opts):
 	while a:
 		seed = sha512(seed).digest()
 		i += 1 # round /i/
+
+		if g.debug: print "Seed round %s: %s" % (i, hexlify(seed))
+
 		if i < a[0]: continue
 
 		a.pop(0)
@@ -74,6 +77,9 @@ def generate_addrs(seed, addrnums, opts):
 		sec = sha256(sha256(seed).digest()).hexdigest()
 		wif = numtowif(int(sec,16))
 
+		if g.debug:
+			print "Privkey round %s:\n  hex: %s\n  wif: %s" % (i, sec, wif)
+
 		el = { 'num': i }
 
 		if not 'print_addresses_only' in opts:
@@ -140,7 +146,7 @@ def format_addr_data(addrlist, seed_chksum, opts):
 # Everything following a hash symbol '#' is a comment and ignored by {}.
 # A text label of {} characters or less may be added to the right of each
 # address, and it will be appended to the bitcoind wallet label upon import.
-# The label may contain printable ASCII symbols.
+# The label may contain any printable ASCII symbol.
 """.strip().format(g.proj_name_cap,g.max_addr_label_len)
 
 	data = []

+ 6 - 2
mmgen/config.py

@@ -32,8 +32,9 @@ wallet_ext    = "mmdat"
 seed_ext      = "mmseed"
 mn_ext        = "mmwords"
 brain_ext     = "mmbrain"
+incog_ext     = "mmincog"
 
-seedfile_exts = wallet_ext, seed_ext, mn_ext, brain_ext
+seedfile_exts = wallet_ext, seed_ext, mn_ext, brain_ext, incog_ext
 
 addrfile_ext = "addrs"
 keyfile_ext  = "keys"
@@ -53,13 +54,15 @@ http_timeout = 30
 keyconv_exec = "keyconv"
 
 from os import getenv
-debug = True if getenv("MMGEN_DEBUG") else False
+debug      = True if getenv("MMGEN_DEBUG") else False
+no_license = True if getenv("MMGEN_NOLICENSE") else False
 
 mins_per_block = 8.5
 passwd_max_tries = 5
 max_randlen,min_randlen = 80,5
 usr_randlen = 20
 salt_len    = 16
+aesctr_iv_len  = 16
 
 hash_preset = '3'
 hash_presets = {
@@ -72,6 +75,7 @@ hash_presets = {
 	'4': [15, 8, 12],
 	'5': [16, 8, 16],
 	'6': [17, 8, 20],
+	'7': [18, 8, 24],
 }
 
 mmgen_idx_max_digits = 7

+ 1 - 1
mmgen/license.py

@@ -587,7 +587,7 @@ copy of the Program in return for a fee.
 def do_license_msg(immed=False):
 
 	import mmgen.config as g
-	if g.quiet: return
+	if g.quiet or g.no_license: return
 
 	msg(gpl['warning'])
 	prompt = "%s " % gpl['prompt'].strip()

+ 12 - 6
mmgen/tx.py

@@ -584,7 +584,7 @@ Generated from seed: %s
 	""".strip() % (" ".join(a)," ".join(b)))
 				sys.exit(3)
 
-	qmsg("Address mappings OK\n")
+	qmsg("Address mappings OK")
 
 
 def check_addr_label(label):
@@ -671,14 +671,14 @@ def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,seeds,opts,gen_pairs=False):
 
 	while seed_ids:
 		if seeds_keys:
-			seed = seeds[seeds_keys.pop()]
+			seed = seeds[seeds_keys.pop(0)]
 		else:
 			infile = False
 			if infiles:
-				infile = infiles.pop()
+				infile = infiles.pop(0)
 				seed = get_seed_retry(infile,opts)
 			elif "from_brain" in opts or "from_mnemonic" in opts \
-						or "from_seed" in opts:
+				or "from_seed" in opts or "from_incognito" in opts:
 				msg("Need data for seed ID %s" % seed_ids[0])
 				seed = get_seed_retry("",opts)
 			else:
@@ -707,8 +707,14 @@ def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,seeds,opts,gen_pairs=False):
 				else:      msg(" for ID %s" % seed_id)
 			else:
 				msg("Seed source produced an invalid seed ID (%s)" % seed_id)
-				if infile:
-					msg("Invalid input file: %s" % infile)
+				if "from_incognito" in opts or infile.split(".")[-1] == g.incog_ext:
+					msg(
+"""Incorrect hash preset, password or incognito wallet data
+
+Trying again...""")
+					infiles.insert(0,infile) # ugly!
+				elif infile:
+					msg("Invalid input file '%s'" % infile)
 					sys.exit(2)
 
 	return ret

+ 200 - 78
mmgen/util.py

@@ -27,10 +27,14 @@ from mmgen.term import *
 
 def msg(s):    sys.stderr.write(s + "\n")
 def msg_r(s):  sys.stderr.write(s)
-def qmsg(s):
-	if not g.quiet: sys.stderr.write(s + "\n")
-def qmsg_r(s):
-	if not g.quiet: sys.stderr.write(s)
+def qmsg(s,alt=""):
+	if g.quiet:
+		if alt: sys.stderr.write(alt + "\n")
+	else: sys.stderr.write(s + "\n")
+def qmsg_r(s,alt=""):
+	if g.quiet:
+		if alt: sys.stderr.write(alt)
+	else: sys.stderr.write(s)
 def vmsg(s):
 	if g.verbose: sys.stderr.write(s + "\n")
 def vmsg_r(s):
@@ -38,6 +42,8 @@ def vmsg_r(s):
 
 def bail(): sys.exit(9)
 
+def get_extension(f): return f.split(".")[-1]
+
 def my_raw_input(prompt,echo=True,allowed_chars=""):
 	try:
 		if echo:
@@ -96,6 +102,25 @@ def show_hash_presets():
 
 cmessages = {
 	'null': "",
+	'incognito_iv_id': """
+   If you know your IV ID, check it against the value above.  If it's
+   incorrect, then your incognito data is invalid.
+""",
+	'incognito_iv_id_hidden': """
+   If you know your IV ID, check it against the value above.  If it's
+   incorrect, then your incognito data is invalid or you've supplied
+   an incorrect offset.
+""",
+	'incognito_key_id': """
+   Check that the generated seed ID is correct.  If it's not, then your
+   password or hash preset is incorrect or incognito data is corrupted.
+""",
+	'incognito_key_id_hidden': """
+   Check that the generated seed ID is correct.  If it's not, then your
+   password or hash preset is incorrect or incognito data is corrupted.
+   If the key ID is correct but the seed ID is not, then you might have
+   chosen an incorrect seed length.
+""",
 	'unencrypted_secret_keys': """
 This program generates secret keys from your {} seed, outputting them in
 UNENCRYPTED form.  Generate only the key(s) you need and guard them carefully.
@@ -136,7 +161,7 @@ def confirm_or_exit(message, question, expect="YES"):
 		msg("Exiting at user request")
 		sys.exit(2)
 
-	msg("")
+	vmsg("")
 
 
 def user_confirm(prompt,default_yes=False,verbose=False):
@@ -265,6 +290,9 @@ def get_new_passphrase(what, opts):
 
 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
@@ -288,7 +316,7 @@ def _get_seed_from_brain_passphrase(words,opts):
 	return seed
 
 
-def encrypt_seed(seed, key):
+def encrypt_seed(seed, key, iv=1):
 	"""
 	Encrypt a seed for an {} deterministic wallet
 	""".format(g.proj_name_cap)
@@ -297,12 +325,14 @@ def encrypt_seed(seed, key):
 	from Crypto.Cipher import AES
 	from Crypto.Util import Counter
 
-	c = AES.new(key, AES.MODE_CTR,counter=Counter.new(128))
+	c = AES.new(key, AES.MODE_CTR,
+			counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
 	enc_seed = c.encrypt(seed)
 
 	vmsg_r("Performing a test decryption of the seed...")
 
-	c = AES.new(key, AES.MODE_CTR,counter=Counter.new(128))
+	c = AES.new(key, AES.MODE_CTR,
+			counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
 	dec_seed = c.decrypt(enc_seed)
 
 	if dec_seed == seed: vmsg("done")
@@ -366,60 +396,44 @@ def write_to_file(outfile,data,confirm=False):
 	f.close
 
 
-def write_seed(seed, opts):
-
-	outfile = "%s.%s" % (make_chksum_8(seed).upper(),g.seed_ext)
-	if 'outdir' in opts:
-		outfile = "%s/%s" % (opts['outdir'], outfile)
-
-	from mmgen.bitcoin import b58encode_pad
-	data = col4(b58encode_pad(seed))
-	chk = make_chksum_6(b58encode_pad(seed))
-
-	o = "%s %s\n" % (chk,data)
+def export_to_file(outfile, data, label, opts):
 
 	if 'stdout' in opts:
-		write_to_stdout(o,"seed data",confirm=True)
+		write_to_stdout(data, label, confirm=True)
 	elif not sys.stdout.isatty():
-		write_to_stdout(o,"seed data",confirm=False)
+		write_to_stdout(data, label, confirm=False)
 	else:
-		write_to_file(outfile,o)
-		msg("%s data saved to file '%s'" % ("Seed",outfile))
-
-
-def write_mnemonic(mn, seed, opts):
-
-	outfile = "%s.%s" % (make_chksum_8(seed).upper(),g.mn_ext)
-	if 'outdir' in opts:
-		outfile = "%s/%s" % (opts['outdir'], outfile)
-
-	o = " ".join(mn) + "\n"
-
-	if 'stdout' in opts:
-		write_to_stdout(o,"mnemonic data",confirm=True)
-	elif not sys.stdout.isatty():
-		write_to_stdout(o,"mnemonic data",confirm=False)
-	else:
-		write_to_file(outfile,o)
-		msg("%s data saved to file '%s'" % ("Mnemonic",outfile))
+		if 'outdir' in opts:
+			outfile = "%s/%s" % (opts['outdir'], outfile)
+		write_to_file(outfile, data, confirm=False if g.quiet else True)
+		msg("%s saved to file '%s'" % (label.capitalize(), outfile))
 
 
 def _display_control_data(label,metadata,hash_preset,salt,enc_seed):
 	msg("WALLET DATA")
-	fs = "  {:25} {}"
+	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]),
 		("Key  ID:",             metadata[1]),
-		("Seed length:",         metadata[2]),
-		("Scrypt hash params:",  "Preset '%s' (%s)" % (hash_preset,
-			" ".join([str(i) for i in _get_hash_params(hash_preset)]))),
-		("Passphrase is empty:", pw_empty),
+		("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)]))),
+		("Passphrase empty?", pw_empty.capitalize()),
 		("Timestamp:",           "%s UTC" % metadata[4]),
-		("Salt:",                b58encode_pad(salt)),
-		("Encrypted seed:",      b58encode_pad(enc_seed))
+	): msg(fs.format(*i))
+
+	fs = "  {:6} {}"
+	for i in (
+		("Salt:",    ""),
+		("  b58:",      b58encode_pad(salt)),
+		("  hex:",      hexlify(salt)),
+		("Encrypted seed:", ""),
+		("  b58:",      b58encode_pad(enc_seed)),
+		("  hex:",      hexlify(enc_seed))
 	): msg(fs.format(*i))
 
 
@@ -496,8 +510,9 @@ def _compare_checksums(chksum1, desc1, chksum2, desc2):
 		return True
 	else:
 		if g.debug:
-			msg("ERROR!\nComputed checksum %s (%s) doesn't match checksum %s (%s)" \
-				% (desc1,chksum1,desc2,chksum2))
+			print \
+	"ERROR!\nComputed checksum %s (%s) doesn't match checksum %s (%s)" \
+			% (desc1,chksum1,desc2,chksum2)
 		return False
 
 def _is_hex(s):
@@ -553,7 +568,7 @@ def _check_chksum_6(chk,val,desc,infile):
 		msg("Checksum: %s. Computed value: %s" % (chk,comp_chk))
 		sys.exit(2)
 	elif g.debug:
-		msg("%s checksum passed: %s" % (desc.capitalize(),chk))
+		print "%s checksum passed: %s" % (desc.capitalize(),chk)
 
 
 def get_data_from_wallet(infile,silent=False):
@@ -716,45 +731,143 @@ def get_seed_from_wallet(
 	return decrypt_seed(enc_seed, key, metadata[0], metadata[1])
 
 
-def make_key(passwd, salt, hash_preset):
+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)
+		os.close(fd)
+	else:
+		fsize = os.stat(fname).st_size
+
+	if fsize < offset + dlen:
+		msg(
+"Destination file has length %s, too short to %s %s bytes of data at offset %s"
+			% (fsize,action,dlen,offset))
+		sys.exit(1)
+
+
+def get_hidden_incog_data(opts):
+		# Already sanity-checked:
+		fname,offset,seed_len = opts['hidden_incog_data'].split(",")
+		qmsg("Getting hidden incog data from file '%s'" % fname)
+
+		dlen = g.aesctr_iv_len + g.salt_len + (int(seed_len)/8)
+
+		fsize = check_data_fits_file_at_offset(fname,int(offset),dlen,"read")
+
+		f = os.open(fname,os.O_RDONLY)
+		os.lseek(f, int(offset), os.SEEK_SET)
+		data = os.read(f, dlen)
+		os.close(f)
+		qmsg("Data read from file '%s' at offset %s" % (fname,offset),
+				"Data read from file")
+		return data
+
+def get_seed_from_incog_wallet(
+		infile,
+		opts,
+		prompt="Enter %s wallet passphrase: " % g.proj_name_cap,
+		silent=False
+		):
+
+	what = "incognito wallet data"
+
+	if "hidden_incog_data" in opts:
+		d = get_hidden_incog_data(opts)
+	else:
+		d = get_data_from_file(infile,what)
+		# 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:]
+
+	qmsg("IV ID: %s.  Check this value if possible." % make_chksum_8(iv))
+	vmsg(cmessages['incognito_iv_id_hidden' if "hidden_incog_data" in opts
+			else 'incognito_iv_id'])
+
+	passwd = get_mmgen_passphrase(prompt,opts)
+
+	msg("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_cap, g.hash_preset))
+		if not hp:
+			hp = g.hash_preset; break
+		elif hp in g.hash_presets:
+			break
+		msg("%s: Invalid hash preset" % hp)
+
+	from hashlib import sha256
+	# IV is used BOTH to initialize counter and to salt password!
+	key = make_key(passwd, iv, hp, "wrapper key")
+	d = decrypt_seed(enc_incog_data, key, "", "", iv=int(hexlify(iv),16))
+	if d == False: sys.exit(2)
+
+	salt,enc_seed = d[0:g.salt_len], d[g.salt_len:]
 
-	vmsg_r("Hashing passphrase.  Please wait...")
+	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['incognito_key_id_hidden' if "hidden_incog_data" in opts
+			else 'incognito_key_id'])
+
+	return seed
+
+
+def make_key(passwd, salt, hash_preset, what="key"):
+
+	vmsg_r("Generating %s from passphrase.  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):
+def decrypt_seed(enc_seed, key, seed_id, key_id, iv=1):
 
-	vmsg_r("Checking key...")
-	chk = make_chksum_8(key)
-	if not _compare_checksums(chk, "of key", key_id, "in header"):
-		msg("Incorrect passphrase")
-		return False
+	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
 
-	vmsg_r("Decrypting seed with key...")
+	vmsg("Decrypting seed with key...")
 
 	from Crypto.Cipher import AES
 	from Crypto.Util import Counter
 
-	c = AES.new(key, AES.MODE_CTR,counter=Counter.new(128))
+	c = AES.new(key, AES.MODE_CTR,
+			counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
 	dec_seed = c.decrypt(enc_seed)
 
-	chk = make_chksum_8(dec_seed)
-	if _compare_checksums(chk,"of decrypted seed",seed_id,"in header"):
-		qmsg("Passphrase is OK")
-	else:
-		if not g.debug:
-			msg_r("Checking key ID...")
-			chk = make_chksum_8(key)
-			if _compare_checksums(chk, "of key", key_id, "in header"):
-				msg("Key ID is correct but decryption of seed failed")
-			else:
-				msg("Incorrect passphrase")
+	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...")
+				chk1 = make_chksum_8(key)
+				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
+			return False
+#	else:
+#		qmsg("Generated IDs (Seed/Key): %s/%s" % (chk2,chk1))
 
-	if g.debug: msg("key: %s" % hexlify(key))
+	if g.debug: print "Decrypted seed: %s" % hexlify(dec_seed)
 
 	return dec_seed
 
@@ -774,9 +887,11 @@ def get_seed(infile,opts,silent=False):
 	elif ext == g.brain_ext:        source = "brainwallet"
 	elif ext == g.seed_ext:         source = "seed"
 	elif ext == g.wallet_ext:       source = "wallet"
-	elif 'from_mnemonic' in opts: source = "mnemonic"
-	elif 'from_brain'    in opts: source = "brainwallet"
-	elif 'from_seed'     in opts: source = "seed"
+	elif ext == g.incog_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_incognito' in opts: source = "incognito wallet"
 	else:
 		if infile: msg(
 			"Invalid file extension for file: %s\nValid extensions: '.%s'" %
@@ -808,11 +923,17 @@ def get_seed(infile,opts,silent=False):
 		seed = _get_seed_from_seed_data(words)
 	elif source == "wallet":
 		seed = get_seed_from_wallet(infile, opts, silent=silent)
+	elif source == "incognito wallet":
+		seed = get_seed_from_incog_wallet(infile, opts, silent=silent)
+
 
-	if infile and not seed and (source == "seed" or source == "mnemonic"):
-		msg("Invalid %s file: %s" % (source,infile))
+	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
@@ -863,5 +984,6 @@ def do_pager(text):
 				break
 	else: print text+end
 
+
 if __name__ == "__main__":
 	print "util.py"

+ 4 - 4
scripts/bitcoind-walletunlock.py

@@ -2,17 +2,17 @@
 #
 # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
 # Copyright (C) 2013 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/>.
 """
@@ -22,7 +22,7 @@ bitcoind-walletunlock.py: Unlock a Bitcoin wallet securely
 import sys
 from mmgen.Opts import *
 from mmgen.tx import *
-from mmgen.utils import msg, my_getpass, my_raw_input
+from mmgen.util import msg, my_getpass, my_raw_input
 
 prog_name = sys.argv[0].split("/")[-1]