Browse Source

Online signing, new commands for 'mmgen-tool', code cleanups, bugfixes

philemon 10 years ago
parent
commit
490879f968
18 changed files with 467 additions and 323 deletions
  1. 3 0
      README.md
  2. 34 30
      mmgen-addrgen
  3. 2 2
      mmgen-addrimport
  4. 15 14
      mmgen-passchg
  5. 16 11
      mmgen-tool
  6. 9 9
      mmgen-txcreate
  7. 3 3
      mmgen-txsend
  8. 46 26
      mmgen-txsign
  9. 13 2
      mmgen-walletchk
  10. 13 39
      mmgen-walletgen
  11. 44 37
      mmgen/Opts.py
  12. 28 23
      mmgen/addr.py
  13. 6 3
      mmgen/config.py
  14. 63 18
      mmgen/tool.py
  15. 56 20
      mmgen/tx.py
  16. 114 34
      mmgen/util.py
  17. 0 51
      mmgen/walletgen.py
  18. 2 1
      setup.py

+ 3 - 0
README.md

@@ -67,6 +67,8 @@ mnemonic or seed or a lost seed from the wallet or mnemonic.
 
 > #### See [Getting Started with MMGen][3]
 
+> #### [MMGen command help][6]
+
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
 [**Forum**][4] |
@@ -78,3 +80,4 @@ Donate: 15TLdmi5NYLdqmtCqczUs5pBPkJDXRs83w
 [3]: https://github.com/mmgen/mmgen/wiki/Getting-Started-with-MMGen
 [4]: https://bitcointalk.org/index.php?topic=567069.0
 [5]: https://github.com/mmgen/mmgen/wiki/MMGen-Signing-Key
+[6]: https://github.com/mmgen/mmgen/wiki/cmdhelp/Command-Help

+ 34 - 30
mmgen-addrgen

@@ -38,36 +38,40 @@ help_data = {
                   mnemonic, seed or password""".format(gen_what,g=g),
 	'usage':"[opts] [infile] <address list>",
 	'options': """
--h, --help               Print this help message{}
--d, --outdir=         d  Specify an alternate directory 'd' for output
--e, --echo-passphrase    Echo passphrase or mnemonic to screen upon entry
--H, --show-hash-presets  Show information on available hash presets
--K, --no-keyconv         Use internal libraries for address generation
-                         instead of 'keyconv'
--l, --seed-len=       N  Length of seed.  Options: {seed_lens}
-                         (default: {g.seed_len})
--p, --hash-preset=    p  Use scrypt.hash() parameters from preset 'p' when
-                         hashing password (default: '{g.hash_preset}')
--P, --passwd-file=    f  Get passphrase from file 'f'
--q, --quiet              Suppress warnings; overwrite files without
-                         prompting
--S, --stdout             Print {what} to stdout
--v, --verbose            Produce more verbose output{}
-
--b, --from-brain=    l,p Generate {what} from a user-created password,
-                         i.e. a "brainwallet", using seed length 'l' and
-                         hash preset 'p' (comma-separated)
--g, --from-incog         Generate {what} from an incognito wallet
--X, --from-incog-hex     Generate {what} from incognito hexadecimal wallet
--G, --from-incog-hidden= f,o,l  Generate {what} from incognito data in file
-                         'f' at offset 'o', with seed length of 'l'
--m, --from-mnemonic      Generate {what} from an electrum-like mnemonic
--s, --from-seed          Generate {what} from a seed in .{g.seed_ext} format
+-h, --help              Print this help message{}
+-d, --outdir=       d   Specify an alternate directory 'd' for output
+-e, --echo-passphrase   Echo passphrase or mnemonic to screen upon entry{}
+-H, --show-hash-presets Show information on available hash presets
+-K, --no-keyconv        Use internal libraries for address generation
+                        instead of 'keyconv'
+-l, --seed-len=     N   Length of seed.  Options: {seed_lens}
+                        (default: {g.seed_len})
+-p, --hash-preset=  p   Use scrypt.hash() parameters from preset 'p' when
+                        hashing password (default: '{g.hash_preset}')
+-P, --passwd-file=  f   Get passphrase from file 'f'
+-q, --quiet             Suppress warnings; overwrite files without
+                        prompting
+-S, --stdout            Print {what} to stdout
+-v, --verbose           Produce more verbose output{}
+
+-b, --from-brain=  l,p  Generate {what} from a user-created password,
+                        i.e. a "brainwallet", using seed length 'l' and
+                        hash preset 'p' (comma-separated)
+-g, --from-incog        Generate {what} from an incognito wallet
+-X, --from-incog-hex    Generate {what} from incognito hexadecimal wallet
+-G, --from-incog-hidden=f,o,l Generate {what} from incognito data in file
+                        'f' at offset 'o', with seed length of 'l'
+-m, --from-mnemonic     Generate {what} from an electrum-like mnemonic
+-s, --from-seed         Generate {what} from a seed in .{g.seed_ext} format
 """.format(
 		*(
-		   ("\n-A, --no-addresses       Print only secret keys, no addresses",
-		    "\n-x, --b16                Print secret keys in hexadecimal too")
-		if gen_what == "keys" else ("","")),
+		   (
+"\n-A, --no-addresses       Print only secret keys, no addresses",
+"\n-f, --flat-list          Produce a flat list of keys suitable for use with" +
+"\n                         'mmgen-txsign'",
+"\n-x, --b16                Print secret keys in hexadecimal too"
+			)
+		if gen_what == "keys" else ("","","")),
 		seed_lens=", ".join([str(i) for i in g.seed_lens]),
 		what=gen_what, g=g
 ),
@@ -160,9 +164,9 @@ if 'stdout' in opts:
 	if gen_what == "keys" and not g.quiet:
 		confirm = True
 	else: confirm = False
-	write_to_stdout(addr_data_str,"secret keys",confirm)
+	write_to_stdout(addr_data_str,gen_what,confirm)
 elif not sys.stdout.isatty():
-	write_to_stdout(addr_data_str,"secret keys",confirm=False)
+	write_to_stdout(addr_data_str,gen_what,confirm=False)
 else:
 	write_addr_data_to_file(seed, addr_data_str, addr_idxs, opts)
 

+ 2 - 2
mmgen-addrimport

@@ -28,8 +28,8 @@ from mmgen.tx import connect_to_bitcoind,parse_addrs_file
 
 help_data = {
 	'prog_name': sys.argv[0].split("/")[-1],
-	'desc': """Import addresses (both mmgen and non-mmgen) into a bitcoind
-                     watching wallet""",
+	'desc': """Import addresses (both {pnm} and non-{pnm}) into a bitcoind
+                     watching wallet""".format(pnm=g.proj_name),
 	'usage':"[opts] [mmgen address file]",
 	'options': """
 -h, --help        Print this help message

+ 15 - 14
mmgen-passchg

@@ -30,18 +30,20 @@ help_data = {
                   deterministic wallet""".format(g.proj_name),
 	'usage':   "[opts] [filename]",
 	'options': """
--h, --help                  Print this help message
--d, --outdir=            d  Specify an alternate directory 'd' for output
--H, --show-hash-presets     Show information on available hash presets
--k, --keep-old-passphrase   Keep old passphrase (use when changing hash
-                            strength or label only)
--L, --label=             l  Change the wallet's label to 'l'
--p, --hash-preset=       p  Change scrypt.hash() parameters to preset 'p'
-                            (default: '{g.hash_preset}')
--P, --passwd-file=       f  Get new passphrase from file 'f'
--q, --quiet                 Suppress warnings; overwrite files without
-                            prompting
--v, --verbose               Produce more verbose output
+-h, --help                Print this help message
+-d, --outdir=           d Specify an alternate directory 'd' for output
+-H, --show-hash-presets   Show information on available hash presets
+-k, --keep-old-passphrase Keep old passphrase (use when changing hash
+                          strength or label only)
+-L, --label=            l Change the wallet's label to 'l'
+-p, --hash-preset=      p Change scrypt.hash() parameters to preset 'p'
+                          (default: '{g.hash_preset}')
+-P, --passwd-file=      f Get new passphrase from file 'f'
+-r, --usr-randchars=    n Get 'n' characters of additional randomness from
+                          user (min={g.min_urandchars}, max={g.max_urandchars})
+-q, --quiet               Suppress warnings; overwrite files without
+                          prompting
+-v, --verbose             Produce more verbose output
 """.format(g=g),
 	'notes': """
 
@@ -110,9 +112,8 @@ if 'preset' in changed or 'passwd' in changed: # Update key ID, salt
 	qmsg("Will update salt and key ID")
 
 	from hashlib import sha256
-	from Crypto import Random
 
-	salt = sha256(salt + Random.new().read(128)).digest()[:g.salt_len]
+	salt = sha256(salt + get_random(128,opts)).digest()[:g.salt_len]
 	key = make_key(passwd, salt, opts['hash_preset'])
 	new_key_id = make_chksum_8(key)
 	qmsg("Key ID changed: %s -> %s" % (key_id,new_key_id))

+ 16 - 11
mmgen-tool

@@ -25,7 +25,7 @@ from hashlib import sha256
 from mmgen.Opts import *
 import mmgen.config as g
 from mmgen.util import pretty_hexdump
-from mmgen.tool import *
+import mmgen.tool as tool
 prog_name = sys.argv[0].split("/")[-1]
 
 help_data = {
@@ -33,16 +33,18 @@ help_data = {
 	'desc':    "Perform various BTC-related operations",
 	'usage':   "[opts] <command> <args>",
 	'options': """
--h, --help                 Print this help message
--q, --quiet                Produce quieter output
--v, --verbose              Produce more verbose output
-""",
+-h, --help            Print this help message
+-q, --quiet           Produce quieter output
+-r, --usr-randchars=n Get 'n' characters of additional randomness from
+                      user (min={g.min_urandchars}, max={g.max_urandchars})
+-v, --verbose         Produce more verbose output
+""".format(g=g),
 	'notes': """
 
 COMMANDS:{}
 Type '{} <command> --help for usage information on a particular
 command
-""".format(command_help,prog_name)
+""".format(tool.command_help,prog_name)
 }
 
 opts,cmd_args = parse_opts(sys.argv,help_data)
@@ -53,17 +55,20 @@ if 'verbose' in opts: g.verbose = True
 if len(cmd_args) < 1:
 	usage(help_data)
 	sys.exit(1)
-else: command = cmd_args.pop(0)
 
-if command not in commands.keys():
+command = cmd_args.pop(0)
+
+if command not in tool.commands.keys():
 	msg("'%s': No such command" % command)
 	sys.exit(1)
 
 if cmd_args and cmd_args[0] == '--help':
-	tool_usage(prog_name, command)
+	tool.tool_usage(prog_name, command)
 	sys.exit(0)
 
-args = process_args(prog_name, command, cmd_args)
+args = tool.process_args(prog_name, command, cmd_args)
+
+tool.opts = opts
 
 #print command + "(" + ", ".join(args) + ")"
-eval(command + "(" + ", ".join(args) + ")")
+eval("tool." + command + "(" + ", ".join(args) + ")")

+ 9 - 9
mmgen-txcreate

@@ -36,13 +36,13 @@ help_data = {
 	'desc':    "Create a BTC transaction with outputs to specified addresses",
 	'usage':   "[opts]  <addr,amt> ... [change addr] [addr file] ...",
 	'options': """
--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
--f, --tx-fee=          f  Transaction fee (default: {g.tx_fee} BTC)
--i, --info                Display unspent outputs and exit
--q, --quiet               Suppress warnings; overwrite files without
-                          prompting
+-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
+-f, --tx-fee=       f Transaction fee (default: {g.tx_fee} BTC)
+-i, --info            Display unspent outputs and exit
+-q, --quiet           Suppress warnings; overwrite files without
+                      prompting
 """.format(g=g),
 	'notes': """
 
@@ -52,12 +52,12 @@ via an interactive menu.
 Ages of transactions are approximate based on an average block creation
 interval of {g.mins_per_block} minutes.
 
-Addresses on the command line can be Bitcoin addresses or MMGen addresses
+Addresses on the command line can be Bitcoin addresses or {pnm} addresses
 of the form <seed ID>:<number>.
 
 To send all inputs (minus TX fee) to a single output, specify one address
 with no amount on the command line.
-""".format(g=g)
+""".format(g=g,pnm=g.proj_name)
 }
 
 opts,cmd_args = parse_opts(sys.argv,help_data)

+ 3 - 3
mmgen-txsend

@@ -34,9 +34,9 @@ help_data = {
 	'desc':    "Send a Bitcoin transaction signed by mmgen-txsign",
 	'usage':   "[opts] <signed transaction file>",
 	'options': """
--h, --help          Print this help message
--d, --outdir=    d  Specify an alternate directory 'd' for output
--q, --quiet         Suppress warnings; overwrite files without prompting
+-h, --help      Print this help message
+-d, --outdir= d Specify an alternate directory 'd' for output
+-q, --quiet     Suppress warnings; overwrite files without prompting
 """
 }
 

+ 46 - 26
mmgen-txsign

@@ -30,14 +30,19 @@ from mmgen.util import msg,qmsg
 help_data = {
 	'prog_name': sys.argv[0].split("/")[-1],
 	'desc':    "Sign Bitcoin transactions generated by mmgen-txcreate",
-	'usage':   "[opts] <transaction file>,.. [mmgen wallet/seed/words/brainwallet file]...",
+	'usage':   "[opts] <transaction file> .. [mmgen wallet/seed/words/brainwallet file] .. [addrfile] ..",
 	'options': """
 -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
 -i, --info               Display information about the transaction and exit
 -I, --tx-id              Display transaction ID and exit
--k, --keys-from-file= k  Provide additional key data from file 'k'
+-k, --keys-from-file= f  Provide additional keys for non-{pnm} addresses
+-K, --all-keys-from-file=f  Like '-k', only use the keyfile as key source
+                         for ALL inputs, including {pnm} ones.  Can be used
+                         for online signing without an {pnm} seed source.
+                         {pnm}-to-BTC mappings can optionally be verified
+                         using address file(s) listed on the command line
 -P, --passwd-file=    f  Get passphrase from file 'f'
 -q, --quiet              Suppress warnings; overwrite files without
                          prompting
@@ -49,37 +54,37 @@ help_data = {
 -g, --from-incog         Generate keys from an incognito wallet
 -X, --from-incog-hex     Generate keys from an incognito hexadecimal wallet
 -G, --from-incog-hidden= f,o,l  Generate keys from incognito data in file
-                                 'f' at offset 'o', with seed length of 'l'
+                         'f' at offset 'o', with seed length of 'l'
 -m, --from-mnemonic      Generate keys from an electrum-like mnemonic
 -s, --from-seed          Generate keys from a seed in .{g.seed_ext} format
-""".format(g=g),
+""".format(g=g,pnm=g.proj_name),
 	'notes': """
 
-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.
-For mmgen inputs, key data is generated from your seed as with the
+Transactions with either {pnm} or non-{pnm} input addresses may be signed.
+For non-{pnm} inputs, the bitcoind wallet.dat is used as the key source.
+For {pnm} inputs, key data is generated from your seed as with the
 mmgen-addrgen and mmgen-keygen utilities.
 
 Data for the --from-<what> options will be taken from a file if a second
 file is specified on the command line.  Otherwise, the user will be
 prompted to enter the data.
 
-In cases of transactions with mixed mmgen and non-mmgen inputs, non-mmgen
+In cases of transactions with mixed {pnm} and non-{pnm} inputs, non-{pnm}
 keys must be supplied in a separate file (WIF format, one key per line)
 using the '--keys-from-file' option.  Alternatively, one may get keys from
 a running bitcoind using the '--force-wallet-dat' option.  First import the
-required mmgen keys using 'bitcoind importprivkey'.
+required {pnm} keys using 'bitcoind importprivkey'.
 
-For transaction outputs that are MMGen addresses, MMGen-to-Bitcoin address
+For transaction outputs that are {pnm} addresses, {pnm}-to-Bitcoin address
 mappings are verified.  Therefore, seed material for these addresses must
-be supplied on the command line.
+be supplied on the command line (but see '--all-keys-from-file').
 
 Seed data supplied in files must have the following extensions:
    wallet:      '.{g.wallet_ext}'
    seed:        '.{g.seed_ext}'
    mnemonic:    '.{g.mn_ext}'
    brainwallet: '.{g.brain_ext}'
-""".format(g=g)
+""".format(g=g,pnm=g.proj_name)
 }
 
 opts,infiles = parse_opts(sys.argv,help_data)
@@ -87,6 +92,9 @@ opts,infiles = parse_opts(sys.argv,help_data)
 if "quiet" in opts: g.quiet = True
 if 'from_incog_hex' in opts or 'from_incog_hidden' in opts:
 	opts['from_incog'] = True
+if 'all_keys_from_file' in opts:
+	opts['keys_from_file'] = opts['all_keys_from_file']
+	opts['skip_key_preverify'] = True
 
 if not infiles: usage(help_data)
 for i in infiles: check_infile(i)
@@ -94,13 +102,16 @@ for i in infiles: check_infile(i)
 c = connect_to_bitcoind()
 
 saved_seeds = {}
-tx_files = [i for i in set(infiles) if get_extension(i) == g.rawtx_ext]
-infiles  = list(set(infiles) - set(tx_files))
+tx_files  = [i for i in set(infiles) if get_extension(i) == g.rawtx_ext]
+addrfiles = [a for a in set(infiles) if get_extension(a) == g.addrfile_ext]
+infiles  = list(set(infiles) - set(tx_files) - set(addrfiles))
 
 if not "info" in opts: do_license_msg(immed=True)
 
-keys_from_file = get_lines_from_file(opts['keys_from_file'],"key data",
-	remove_comments=True) if 'keys_from_file' in opts else []
+if 'keys_from_file' in opts:
+	keys_from_file = get_lines_from_file(opts['keys_from_file'],"key data",
+						remove_comments=True)
+else: keys_from_file = []
 
 for tx_file in tx_files:
 	m = "" if 'tx_id' in opts else "transaction data"
@@ -118,21 +129,30 @@ for tx_file in tx_files:
 		sys.exit(0)
 
 # Are inputs mmgen addresses?
-	mmgen_addrs = [i for i in inputs_data if parse_mmgen_label(i['account'])[0]]
-	other_addrs = [i for i in inputs_data if not parse_mmgen_label(i['account'])[0]]
+	mmgen_inputs = [i for i in inputs_data if parse_mmgen_label(i['account'])[0]]
+	other_inputs = [i for i in inputs_data if not parse_mmgen_label(i['account'])[0]]
+
+	if 'all_keys_from_file' in opts: other_inputs = inputs_data
 
 	keys = keys_from_file
 
-	if other_addrs and not keys and not 'use_wallet_dat' in opts:
-		missing_keys_errormsg(other_addrs)
+	if other_inputs and not keys and not 'use_wallet_dat' in opts:
+		missing_keys_errormsg(other_inputs)
 		sys.exit(2)
 
-	if other_addrs and keys and not 'skip_key_preverify' in opts:
-		a = [i['address'] for i in other_addrs]
+	if other_inputs and keys and not 'skip_key_preverify' in opts:
+		a = [i['address'] for i in other_inputs]
 		preverify_keys(a, keys)
 		opts['skip_key_preverify'] = True
 
-	check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,saved_seeds,opts)
+	if 'all_keys_from_file' in opts:
+		if addrfiles:
+			check_mmgen_to_btc_addr_mappings_addrfile(mmgen_inputs,b2m_map,addrfiles)
+		else:
+			confirm_or_exit(txmsg['skip_mapping_checks_warning'],"continue")
+	else:
+		check_mmgen_to_btc_addr_mappings(
+				mmgen_inputs,b2m_map,infiles,saved_seeds,opts)
 
 	if len(tx_files) > 1:
 		msg("\nTransaction %s/%s:" % (tx_files.index(tx_file)+1,len(tx_files)))
@@ -147,15 +167,15 @@ for tx_file in tx_files:
 		{"txid":i['txid'],"vout":i['vout'],"scriptPubKey":i['scriptPubKey']}
 			for i in inputs_data]
 
-	if mmgen_addrs:
-		ml = [i['account'].split()[0] for i in mmgen_addrs]
+	if mmgen_inputs and not 'all_keys_from_file' in opts:
+		ml = [i['account'].split()[0] for i in mmgen_inputs]
 		keys += get_keys_for_mmgen_addrs(ml,infiles,saved_seeds,opts)
 
 		if 'use_wallet_dat' in opts:
 			sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts)
 		else:
 			sig_tx = sign_transaction(c,tx_hex,sig_data,keys)
-	elif other_addrs:
+	elif other_inputs:
 		if keys:
 			sig_tx = sign_transaction(c,tx_hex,sig_data,keys)
 		else:

+ 13 - 2
mmgen-walletchk

@@ -37,14 +37,24 @@ help_data = {
 -e, --echo-passphrase  Print passphrase to screen when typing it
 -P, --passwd-file=  f  Get passphrase from file 'f'
 -q, --quiet            Suppress warnings; overwrite files without prompting
+-r, --usr-randchars= n Get 'n' characters of additional randomness from
+                       user (min={g.min_urandchars}, max={g.max_urandchars})
 -S, --stdout           Print seed or mnemonic data to standard output
 -v, --verbose          Produce more verbose output
 -g, --export-incog     Export wallet to incognito format
 -X, --export-incog-hex Export wallet to incognito hexadecimal format
--G, --export-incog-hidden= f,o  Hide incognito data in existing file 'f'
-                           at offset 'o' (comma-separated)
+-G, --export-incog-hidden=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
+""".format(g=g),
+	'notes': """
+
+Since good randomness is particularly important for incognito wallets,
+the '--usr-randchars' option is turned on by default to gather additional
+entropy from the user when one of the '--export-incog*' options is
+selected.  If you fully trust your OS's random number generator and wish
+to disable this option, then specify '-r0' on the command line.
 """
 }
 
@@ -64,6 +74,7 @@ if 'export_mnemonic' in opts:
 elif 'export_seed' in opts:
 	qmsg("Exporting seed data to file by user request")
 elif 'export_incog' in opts:
+	if opts['usr_randchars'] == -1: opts['usr_randchars'] = g.usr_randchars_dfl
 	qmsg("Exporting wallet to incognito format by user request")
 	incog_enc,seed_id,key_id,iv_id,preset = \
 		wallet_to_incog_data(cmd_args[0],opts)

+ 13 - 39
mmgen-walletgen

@@ -47,8 +47,8 @@ help_data = {
 -P, --passwd-file=      f  Get passphrase from file 'f'
 -q, --quiet                Produce quieter output; overwrite files without
                            prompting
--u, --usr-randlen=      n  Get 'n' characters of randomness from the user
-                           (default: {g.usr_randlen})
+-r, --usr-randchars=    n  Get 'n' characters of additional randomness from
+                           user (min={g.min_urandchars}, max={g.max_urandchars})
 -v, --verbose              Produce more verbose output
 
 -b, --from-brain=      l,p Generate wallet from a user-created passphrase,
@@ -71,6 +71,11 @@ trailing space are ignored.  This permits reading passphrase data from a
 multi-line file with free spacing and indentation.  This is particularly
 convenient for long brainwallet passphrases, for example.
 
+Since good randomness is particularly important when generating wallets,
+the '--usr-randchars' option is turned on by default to gather additional
+entropy from the user.  If you fully trust your OS's random number gener-
+ator and wish to disable this option, specify '-r0' on the command line.
+
 BRAINWALLET NOTE:
 
 As brainwallets require especially strong hashing to thwart dictionary
@@ -94,6 +99,7 @@ opts,cmd_args = parse_opts(sys.argv,help_data)
 if 'quiet' in opts: g.quiet = True
 if 'verbose' in opts: g.verbose = True
 if 'show_hash_presets' in opts: show_hash_presets()
+if opts['usr_randchars'] == -1: opts['usr_randchars'] = g.usr_randchars_dfl
 
 if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 
@@ -116,31 +122,6 @@ else: usage(help_data)
 
 do_license_msg()
 
-qmsg_r("Acquiring random data from your computer...")
-
-from time import sleep
-sleep(0.25)
-
-try:
-	from Crypto import Random
-	r = Random.new()
-	os_rand_data = (r.read(256),r.read(128))
-except:
-	msg("FAILED\nUnable to generate random numbers. Exiting")
-	sys.exit(2)
-
-qmsg("OK")
-
-if g.debug: display_os_random_data(os_rand_data)
-
-usr_keys,key_timings = get_random_data_from_user(opts)
-qmsg("")
-
-if g.debug: display_user_random_data(usr_keys,key_timings)
-
-usr_rand_data = sha256(usr_keys).digest() + \
-				sha256("".join(key_timings)).digest()
-
 if 'from_brain' in opts and not g.quiet:
 	confirm_or_exit(cmessages['brain_warning'].format(
 			g.proj_name, *get_from_brain_opt_params(opts)),
@@ -155,18 +136,11 @@ for i in 'from_mnemonic','from_brain','from_seed','from_incog':
 		break
 else:
 	# Truncate random data for smaller seed lengths
-	seed = os_rand_data[0] + usr_rand_data
-	seed = sha256(seed).digest()[:opts['seed_len']/8]
-
-salt = os_rand_data[1] + usr_rand_data
-salt = sha256(salt).digest()[:g.salt_len]
-
-qmsg("""
-Now you must choose a passphrase to encrypt the wallet with.  A key will be
-generated from your passphrase using a hash preset of '%s'.  Please note that
-no strength checking of passphrases is performed.  For an empty passphrase,
-just hit ENTER twice.
-""".strip() % opts['hash_preset'])
+	seed = sha256(get_random(128,opts)).digest()[:opts['seed_len']/8]
+
+salt = sha256(get_random(128,opts)).digest()[:g.salt_len]
+
+qmsg(cmessages['choose_wallet_passphrase'] % opts['hash_preset'])
 
 passwd = get_new_passphrase("{} wallet passphrase".format(g.proj_name), opts)
 

+ 44 - 37
mmgen/Opts.py

@@ -83,27 +83,38 @@ def parse_opts(argv,help_data):
 
 	lines = help_data['options'].strip().split("\n")
 	import re
-	pat = r"^-([a-zA-Z0-9]), --([a-zA-Z0-9-]{1,64})(=*) (.*)"
-	opt_data = [m.groups() for m in [re.match(pat,l) for l in lines] if m]
-
-	short_opts = "".join([d[0]+(":" if d[2] else "") for d in opt_data if d])
-	long_opts = [d[1].replace("-","_")+d[2] for d in opt_data if d]
+	pat = r"^-([a-zA-Z0-9]), --([a-zA-Z0-9-]{1,64})(=| )(.+)"
+	rep = r"-{0}, --{1}{w}{3}"
+	opt_data = [list(m.groups()) for m in [re.match(pat,l) for l in lines] if m]
+#	for o in opt_data: print o
+#	sys.exit()
+
+	for d in opt_data:
+		if d[2] == " ": d[2] = ""
+	short_opts = "".join([d[0]+d[2].replace("=",":") for d in opt_data])
+	long_opts = [d[1].replace("-","_")+d[2] for d in opt_data]
 	help_data['options'] = "\n".join(
-		["-{0}, --{1}{w} {3}".format(w=" " if m.group(3) else "", *m.groups())
+		[rep.format(w=" ", *m.groups())
 			if m else k for m,k in [(re.match(pat,l),l) for l in lines]]
 	)
 	opts,infiles = process_opts(argv,help_data,short_opts,long_opts)
 
-	if g.debug: print "processed user opts: %s" % opts
+	# check_opts() doesn't touch opts[]
+	if not check_opts(opts,long_opts): sys.exit(1)
+
+	# If unset, set these to the default values in mmgen.config:
+	for v in g.cl_override_vars:
+		if v in opts: typeconvert_override_var(opts,v)
+		else: opts[v] = eval("g."+v)
 
-	if not check_opts(opts,long_opts): sys.exit(1) # MMGen only!
+	if g.debug: print "processed opts: %s" % opts
 
 	return opts,infiles
 
 
 def show_opts_and_cmd_args(opts,cmd_args):
-	print "Processed options:     %s" % repr(opts)
-	print "Cmd args:              %s" % repr(cmd_args)
+	print "Processed options: %s" % repr(opts)
+	print "Cmd args:          %s" % repr(cmd_args)
 
 
 # Everything below here is MMGen-specific:
@@ -112,17 +123,13 @@ from mmgen.util import msg,check_infile
 
 def check_opts(opts,long_opts):
 
-	# These must be set to the default values in mmgen.config:
-	for i in g.cl_override_vars:
-		if i+"=" in long_opts:
-			set_if_unset_and_typeconvert(opts,i)
-
 	for opt,val in opts.items():
 
 		what = "parameter for '--%s' option" % opt.replace("_","-")
 
 		# Check for file existence and readability
-		if opt in ('keys_from_file','addrlist','passwd_file','keysforaddrs'):
+		if opt in ('keys_from_file','all_keys_from_file','addrlist',
+				'passwd_file','keysforaddrs'):
 			check_infile(val)  # exits on error
 			continue
 
@@ -233,10 +240,14 @@ def check_opts(opts,long_opts):
 				msg("'%s': invalid %s.  Options: %s"
 				% (val,what,", ".join(sorted(g.hash_presets.keys()))))
 				return False
-		elif opt == 'usr_randlen':
-			if val > g.max_randlen or val < g.min_randlen:
-				msg("'%s': invalid %s (must be >= %s and <= %s)"
-				% (val,what,g.min_randlen,g.max_randlen))
+		elif opt == 'usr_randchars':
+			try: v = int(val)
+			except:
+				msg("'%s': invalid value for %s (not an integer)" % (val,what))
+				return False
+			if v != 0 and not (g.min_urandchars <= v <= g.max_urandchars):
+				msg("'%s': invalid %s (must be >= %s and <= %s (or zero))"
+				% (v,what,g.min_urandchars,g.max_urandchars))
 				return False
 		else:
 			if g.debug: print "check_opts(): No test for opt '%s'" % opt
@@ -244,22 +255,18 @@ def check_opts(opts,long_opts):
 	return True
 
 
-def set_if_unset_and_typeconvert(opts,opt):
+def typeconvert_override_var(opts,opt):
 
-	if opt in g.cl_override_vars:
-		if opt not in opts:
-			# Set to similarly named default value in mmgen.config
-			opts[opt] = eval("g."+opt)
-		else:
-			vtype = type(eval("g."+opt))
-			if g.debug: print "Opt: %s, Type: %s" % (opt,vtype)
-			if   vtype == int:   f,t = int,"an integer"
-			elif vtype == str:   f,t = str,"a string"
-			elif vtype == float: f,t = float,"a float"
+	vtype = type(eval("g."+opt))
+	if g.debug: print "Override opt: %-15s [%s]" % (opt,vtype)
 
-			try:
-				opts[opt] = f(opts[opt])
-			except:
-				msg("'%s': invalid parameter for '--%s' option (not %s)" %
-						(opts[opt],opt.replace("_","-"),t))
-				sys.exit(1)
+	if   vtype == int:   f,t = int,"an integer"
+	elif vtype == str:   f,t = str,"a string"
+	elif vtype == float: f,t = float,"a float"
+
+	try:
+		opts[opt] = f(opts[opt])
+	except:
+		msg("'%s': invalid parameter for '--%s' option (not %s)" %
+				(opts[opt],opt.replace("_","-"),t))
+		sys.exit(1)

+ 28 - 23
mmgen/addr.py

@@ -93,11 +93,11 @@ def generate_addrs(seed, addrnums, opts):
 		if g.debug:
 			print "Privkey round %s:\n  hex: %s\n  wif: %s" % (i, sec, wif)
 
-		el = { 'num': i }
+		d = { 'num': i }
 
 		if not 'print_addresses_only' in opts:
-			el['sec'] = sec
-			el['wif'] = wif
+			d['sec'] = sec
+			d['wif'] = wif
 
 		if not 'no_addresses' in opts:
 			if keyconv:
@@ -107,9 +107,9 @@ def generate_addrs(seed, addrnums, opts):
 			else:
 				addr = privnum2addr(int(sec,16))
 
-			el['addr'] = addr
+			d['addr'] = addr
 
-		out.append(el)
+		out.append(d)
 
 	w = opts['gen_what']
 	if t_addrs == 1:
@@ -127,6 +127,10 @@ def generate_keys(seed, addrnums):
 
 def format_addr_data(addr_data, addr_data_chksum, seed_id, addr_idxs, opts):
 
+	if 'flat_list' in opts:
+		return "\n\n".join(["# %s:%s %s\n%s" % (seed_id,d['num'],d['addr'],d['wif'])
+			for d in addr_data])+"\n\n"
+
 	start = addr_data[0]['num']
 	end   = addr_data[-1]['num']
 
@@ -140,35 +144,35 @@ def format_addr_data(addr_data, addr_data_chksum, seed_id, addr_idxs, opts):
 			(5 if 'print_secret' in opts else 1) + len(wif_msg)
 		)
 
-	data = []
-	if not 'stdout' in opts: data.append(addrmsgs['addrfile_header'] + "\n")
-	data.append("# Address data checksum for {}[{}]: {}".format(
+	out = []
+	if not 'stdout' in opts: out.append(addrmsgs['addrfile_header'] + "\n")
+	out.append("# Address data checksum for {}[{}]: {}".format(
 				seed_id, fmt_addr_idxs(addr_idxs), addr_data_chksum))
-	data.append("# Record this value to a secure location\n")
-	data.append("%s {" % seed_id.upper())
+	out.append("# Record this value to a secure location\n")
+	out.append("%s {" % seed_id.upper())
 
-	for el in addr_data:
-		col1 = el['num']
+	for d in addr_data:
+		col1 = d['num']
 		if 'no_addresses' in opts:
 			if 'b16' in opts:
-				data.append(fa % (col1, " (hex):", el['sec']))
+				out.append(fa % (col1, " (hex):", d['sec']))
 				col1 = ""
-			data.append(fa % (col1, " (wif):", el['wif']))
-			if 'b16' in opts: data.append("")
+			out.append(fa % (col1, " (wif):", d['wif']))
+			if 'b16' in opts: out.append("")
 		elif 'print_secret' in opts:
 			if 'b16' in opts:
-				data.append(fa % (col1, "sec (hex):", el['sec']))
+				out.append(fa % (col1, "sec (hex):", d['sec']))
 				col1 = ""
-			data.append(fa % (col1, "sec"+wif_msg+":", el['wif']))
-			data.append(fa % ("",   "addr:", el['addr']))
-			data.append("")
+			out.append(fa % (col1, "sec"+wif_msg+":", d['wif']))
+			out.append(fa % ("",   "addr:", d['addr']))
+			out.append("")
 		else:
-			data.append(fa % (col1, "", el['addr']))
+			out.append(fa % (col1, "", d['addr']))
 
-	if not data[-1]: data.pop()
-	data.append("}")
+	if not out[-1]: out.pop()
+	out.append("}")
 
-	return "\n".join(data) + "\n"
+	return "\n".join(out) + "\n"
 
 
 def fmt_addr_idxs(addr_idxs):
@@ -193,6 +197,7 @@ def write_addr_data_to_file(seed, addr_data_str, addr_idxs, opts):
 
 	if 'print_addresses_only' in opts: ext = g.addrfile_ext
 	elif 'no_addresses' in opts:       ext = g.keyfile_ext
+	elif 'flat_list' in opts:          ext = g.keylist_ext
 	else:                              ext = "akeys"
 
 	if 'b16' in opts: ext = ext.replace("keys","xkeys")

+ 6 - 3
mmgen/config.py

@@ -46,11 +46,13 @@ rawtx_ext    = "raw"
 sigtx_ext    = "sig"
 addrfile_ext = "addrs"
 keyfile_ext  = "keys"
+keylist_ext  = "keylist"
+mmenc_ext    = "mmenc"
 
 default_wl    = "electrum"
 #default_wl    = "tirosh"
 
-cl_override_vars = 'seed_len','hash_preset','usr_randlen'
+cl_override_vars = 'seed_len','hash_preset','usr_randchars'
 
 seed_lens = 128,192,256
 seed_len  = 256
@@ -68,8 +70,8 @@ bogus_wallet_data = getenv("MMGEN_BOGUS_WALLET_DATA")
 
 mins_per_block = 8.5
 passwd_max_tries = 5
-max_randlen,min_randlen = 80,5
-usr_randlen = 20
+usr_randchars,usr_randchars_dfl = -1,30 # see get_random()
+max_urandchars,min_urandchars = 80,10
 salt_len    = 16
 aesctr_iv_len  = 16
 
@@ -97,6 +99,7 @@ max_addr_label_len = 32
 wallet_label_symbols = addr_label_symbols
 max_wallet_label_len = 32
 
+user_entropy = ""
 #addr_label_punc = ".","_",",","-"," ","(",")"
 #addr_label_symbols = tuple(ascii_letters + digits) + addr_label_punc
 #wallet_label_punc = addr_label_punc

+ 63 - 18
mmgen/tool.py

@@ -23,6 +23,7 @@ import sys
 import mmgen.bitcoin as bitcoin
 import binascii as ba
 
+import mmgen.config as g
 from mmgen.util import *
 from mmgen.tx import *
 
@@ -33,6 +34,7 @@ def Vmsg(s):
 def Vmsg_r(s):
 	if g.verbose: sys.stdout.write(s)
 
+opts = {}
 commands = {
 	"strtob58":     ['<string> [str]'],
 	"hextob58":     ['<hex number> [str]'],
@@ -66,6 +68,8 @@ commands = {
 	"pubkey2addr":  ['<public key in hex format> [str]'],
 	"pubkey2hexaddr": ['<public key in hex format> [str]'],
 	"privhex2addr": ['<private key in hex format> [str]','compressed [bool=False]'],
+	"encrypt":      ['<infile> [str]','outfile [str=""]','hash_preset [str="3"]'],
+	"decrypt":      ['<infile> [str]','outfile [str=""]','hash_preset [str="3"]'],
 }
 
 command_help = """
@@ -73,10 +77,10 @@ command_help = """
   hexdump      - encode data into formatted hexadecimal form (file or stdin)
   unhexdump    - decode formatted hexadecimal data (file or stdin)
 
-  MMGen-specific operations
-  id8          - generate 8-character MMGen ID checksum for file (or stdin)
-  id6          - generate 6-character MMGen ID checksum for file (or stdin)
-  check_addrfile - compute checksum and address list for MMGen address file
+  {pnm}-specific operations
+  id8          - generate 8-character {pnm} ID checksum for file (or stdin)
+  id6          - generate 6-character {pnm} ID checksum for file (or stdin)
+  check_addrfile - compute checksum and address list for {pnm} address file
 
   Bitcoin operations:
   strtob58     - convert a string to base 58
@@ -89,7 +93,7 @@ command_help = """
   hex2wif      - convert a private key from hex to WIF format
   wif2addr     - generate a Bitcoin address from a key in WIF format
   pubkey2addr  - convert Bitcoin public key to address
-  pubkey2hexaddr  - convert Bitcoin public key to address in hex format
+  pubkey2hexaddr - convert Bitcoin public key to address in hex format
   hexaddr2addr - convert Bitcoin address from hex to base58 format
   addr2hexaddr - convert Bitcoin address from base58 to hex format
   privhex2addr - generate Bitcoin address from private key in hex format
@@ -100,6 +104,14 @@ command_help = """
   sha256x2     - compute a double sha256 hash of data
   getrand      - print 'n' bytes (default 32) of random data in hex format
 
+  Encryption operations:
+  encrypt      - encrypt a file using {pnm}'s encryption suite
+  decrypt      - decrypt an {pnm}-encrypted file
+    {pnm} encryption suite:
+      * Key: Scrypt (user-configurable hash parameters, 32-byte salt)
+      * Enc: AES256_CTR, 16-byte rand IV, sha256 hash + 32-byte nonce + data
+      * The encrypted file is indistinguishable from random data
+
   Mnemonic operations (choose "electrum" (default), "tirosh" or "all"
   wordlists):
   mn_rand128   - generate random 128-bit mnemonic
@@ -109,14 +121,14 @@ command_help = """
   mn_printlist - print mnemonic wordlist
 
   Bitcoind operations (bitcoind must be running):
-  listaddresses - show MMGen addresses and their balances
+  listaddresses - show {pnm} addresses and their balances
   getbalance    - like 'bitcoind getbalance' but shows confirmed/unconfirmed,
                   spendable/unspendable
-  viewtx        - show raw transaction in human-readable form
+  viewtx        - show raw/signed {pnm} transaction in human-readable form
 
-  IMPORTANT NOTE: Though MMGen mnemonics use the Electrum wordlist, they're
+  IMPORTANT NOTE: Though {pnm} mnemonics use the Electrum wordlist, they're
   computed using a different algorithm and are NOT Electrum-compatible!
-"""
+""".format(pnm=g.proj_name)
 
 def tool_usage(prog_name, command):
 	print "USAGE: '%s %s%s'" % (prog_name, command,
@@ -216,26 +228,22 @@ def b58tohex(s,f_enc=bitcoin.b58decode, f_dec=bitcoin.b58encode):
 	dec = f_dec(ba.unhexlify(enc))
 	print_convert_results(s,enc,dec)
 
-def get_random(length):
-	from Crypto import Random
-	return Random.new().read(length)
-
 def b58randenc():
-	r = get_random(32)
+	r = get_random(32,opts)
 	enc = bitcoin.b58encode(r)
 	dec = bitcoin.b58decode(enc)
 	print_convert_results(ba.hexlify(r),enc,ba.hexlify(dec))
 
 def getrand(bytes='32'):
-	print ba.hexlify(get_random(int(bytes)))
+	print ba.hexlify(get_random(int(bytes),opts))
 
 def randwif(compressed=False):
-	r_hex = ba.hexlify(get_random(32))
+	r_hex = ba.hexlify(get_random(32,opts))
 	enc = bitcoin.hextowif(r_hex,compressed)
 	print_convert_results(r_hex,enc,"",no_recode=True)
 
 def randpair(compressed=False):
-	r_hex = ba.hexlify(get_random(32))
+	r_hex = ba.hexlify(get_random(32,opts))
 	wif = bitcoin.hextowif(r_hex,compressed)
 	addr = bitcoin.privnum2addr(int(r_hex,16),compressed)
 	Vmsg("Key (hex):  %s" % r_hex)
@@ -265,7 +273,7 @@ def get_wordlist(wordlist):
 	return el if wordlist == "electrum" else tl
 
 def do_random_mn(nbytes,wordlist):
-	r = get_random(nbytes)
+	r = get_random(nbytes,opts)
 	wlists = wordlists if wordlist == "all" else [wordlist]
 	for wl in wlists:
 		l = get_wordlist(wl)
@@ -397,3 +405,40 @@ 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)
+	else: write_to_file((outfile or infile+"."+g.mmenc_ext),salt+iv+enc_d,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:
+			import re
+			of = re.sub(r'\.%s$'%g.mmenc_ext,r'',infile)
+			if of == infile: of = infile+".dec"
+			write_to_file((outfile or of), out, True,True)
+	else:
+		msg("Incorrect passphrase or hash preset")

+ 56 - 20
mmgen/tx.py

@@ -61,7 +61,25 @@ tracking wallet, or supply an address file for it on the command line.
 	'no_spendable_outputs': """
 No spendable outputs found!  Import addresses with balances into your
 watch-only wallet using 'mmgen-addrimport' and then re-run this program.
-""".strip()
+""".strip(),
+	'mapping_error': """
+MMGen -> BTC address mappings differ!
+In transaction:      %s
+Generated from seed: %s
+""".strip(),
+	'skip_mapping_checks_warning': """
+You've chosen the '--all-keys-from-file' option.  Since all signing keys will
+be taken from this file, no {pnm} seed source will be consulted and {pnm}-to-
+BTC mapping checks cannot not be performed.  Were an attacker to compromise
+your tracking wallet or raw transaction file, he could thus cause you to spend
+coin to an unintended address.  For greater security, supply a trusted {pnm}
+address file for your output addresses on the command line.
+""".strip().format(pnm=g.proj_name),
+	'missing_mappings': """
+No information was found in the supplied address files for the following {pnm}
+addresses: %s
+The {pnm}-to-BTC mappings for these addresses cannot be verified!
+""".strip().format(pnm=g.proj_name),
 }
 
 # Deleted text:
@@ -504,7 +522,7 @@ def mmaddr2btcaddr_bitcoind(c,mmaddr,acct_data):
 	return "",""
 
 
-def mmaddr2btcaddr_addrfile(mmaddr,addr_data):
+def mmaddr2btcaddr_addrfile(mmaddr,addr_data,silent=False):
 
 	mmid,mmidx = mmaddr.split(":")
 
@@ -512,35 +530,28 @@ def mmaddr2btcaddr_addrfile(mmaddr,addr_data):
 		if mmid == ad[0]:
 			for j in ad[1]:
 				if j[0] == mmidx:
-					msg(txmsg['addrfile_warn_msg'].format(mmaddr=mmaddr))
-					if not user_confirm("Continue anyway?"):
-						sys.exit(1)
+					if not silent:
+						msg(txmsg['addrfile_warn_msg'].format(mmaddr=mmaddr))
+						if not user_confirm("Continue anyway?"):
+							sys.exit(1)
 					return j[1:] if len(j) == 3 else (j[1],"")
 
-	msg(txmsg['addrfile_fail_msg'].format(mmaddr=mmaddr))
-	sys.exit(2)
+	if silent: return "",""
+	else: msg(txmsg['addrfile_fail_msg'].format(mmaddr=mmaddr)); sys.exit(2)
 
 
-def check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,saved_seeds,opts):
-	in_maplist = [(i['account'].split()[0],i['address'])
-		for i in inputs_data if i['account']
-			and is_mmgen_addr(i['account'].split()[0])]
+def check_mmgen_to_btc_addr_mappings(mmgen_inputs,b2m_map,infiles,saved_seeds,opts):
+	in_maplist  = [(i['account'].split()[0],i['address']) for i in mmgen_inputs]
 	out_maplist = [(i[1][0],i[0]) for i in b2m_map.items()]
 
 	for maplist,label in (in_maplist,"inputs"), (out_maplist,"outputs"):
 		if not maplist: continue
 		qmsg("Checking MMGen -> BTC address mappings for %s" % label)
-		mmaddrs = [i[0] for i in maplist]
-		from copy import deepcopy
-		pairs = get_keys_for_mmgen_addrs(mmaddrs,
+		pairs = get_keys_for_mmgen_addrs([i[0] for i in maplist],
 				infiles,saved_seeds,opts,gen_pairs=True)
 		for a,b in zip(sorted(pairs),sorted(maplist)):
 			if a != b:
-				msg("""
-MMGen -> BTC address mappings differ!
-In transaction:      %s
-Generated from seed: %s
-	""".strip() % (" ".join(a)," ".join(b)))
+				msg(txmsg['mapping_error'] % (" ".join(a)," ".join(b)))
 				sys.exit(3)
 
 	qmsg("Address mappings OK")
@@ -659,7 +670,6 @@ def get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts):
 def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds,opts,gen_pairs=False):
 
 	seed_ids = list(set([i[:8] for i in mmgen_addrs]))
-	seed_ids_save = seed_ids[0:]  # deep copy
 	ret = []
 
 	for seed_id in seed_ids:
@@ -733,6 +743,7 @@ def preverify_keys(addrs_orig, keys_orig):
 		sys.exit(2)
 	else: qmsg("OK")
 
+	# Check that keys match addresses:
 	msg('Pre-verifying keys in user-supplied key list (Ctrl-C to skip)')
 
 	try:
@@ -768,3 +779,28 @@ A key file must be supplied (or use the "-w" option) for the following
 non-mmgen address%s:
 """.strip() % ("" if len(other_addrs) == 1 else "es"))
 	print "  %s" % "\n  ".join([i['address'] for i in other_addrs])
+
+
+def check_mmgen_to_btc_addr_mappings_addrfile(mmgen_inputs,b2m_map,addrfiles):
+	addr_data = [parse_addrs_file(a) for a in addrfiles]
+	in_maplist  = [(i['account'].split()[0],i['address']) for i in mmgen_inputs]
+	out_maplist = [(i[1][0],i[0]) for i in b2m_map.items()]
+
+	missing,wrong = [],[]
+	for maplist,label in (in_maplist,"inputs"), (out_maplist,"outputs"):
+		qmsg("Checking MMGen -> BTC address mappings for %s" % label)
+		for i in maplist:
+			btaddr = mmaddr2btcaddr_addrfile(i[0],addr_data,silent=True)[0]
+			if not btaddr: missing.append(i[0])
+			elif btaddr != i[1]: wrong.append((i[0],i[1],btaddr))
+
+	if wrong:
+		fs = " {:11} {:35} {}"
+		msg("ERROR: The following address mappings did not match!")
+		msg(fs.format("MMGen addr","In TX file:","In address file:"))
+		for w in wrong: msg(fs.format(*w))
+		sys.exit(3)
+
+	if missing:
+		confirm_or_exit(txmsg['missing_mappings'] % " ".join(missing),"continue")
+	else: qmsg("Address mappings OK")

+ 114 - 34
mmgen/util.py

@@ -20,8 +20,10 @@ util.py:  Shared routines for the mmgen suite
 """
 
 import sys
-import mmgen.config as g
+from hashlib import sha256
 from binascii import hexlify,unhexlify
+
+import mmgen.config as g
 from mmgen.bitcoin import b58decode_pad
 from mmgen.term import *
 
@@ -46,6 +48,60 @@ 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)
+	msg("")
+
+	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")
+	else:
+		return os_rand
+
 def my_raw_input(prompt,echo=True):
 	try:
 		if echo:
@@ -118,7 +174,21 @@ recommended to use one of the higher-numbered presets
 Remember the seed length and hash preset parameters you've specified.  To
 generate the correct keys/addresses associated with this passphrase in the
 future, you must continue using these same parameters
-"""
+""",
+	'usr_rand_notice': """
+You've chosen to not fully trust your OS's random number generator and provide
+some additional entropy of your own.  Please type %s symbols on your keyboard.
+Type slowly and choose your symbols carefully for maximum randomness.  Try to
+use both upper and lowercase as well as punctuation and numerals.  What you
+type will not be displayed on the screen.  Note that the timings between your
+keystrokes will also be used as a source of randomness.
+""",
+	'choose_wallet_passphrase': """
+Now you must choose a passphrase to encrypt the wallet with.  A key will be
+generated from your passphrase using a hash preset of '%s'.  Please note that
+no strength checking of passphrases is performed.  For an empty passphrase,
+just hit ENTER twice.
+""".strip()
 }
 
 
@@ -172,12 +242,10 @@ def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False):
 
 
 def make_chksum_8(s,sep=False):
-	from hashlib import sha256
 	s = sha256(sha256(s).digest()).hexdigest()[:8].upper()
 	return "{} {}".format(s[:4],s[4:]) if sep else s
 
 def make_chksum_6(s):
-	from hashlib import sha256
 	return sha256(s).hexdigest()[:6]
 
 
@@ -296,31 +364,36 @@ def _get_seed_from_brain_passphrase(words,opts):
 	return seed
 
 
-def encrypt_seed(seed, key, iv=1):
+def encrypt_seed(seed, key):
+	return encrypt_data(seed, key, iv=1, what="seed")
+
+def encrypt_data(data, key, iv=1, what="data"):
+	"""
+	Encrypt arbitrary data using AES256 in counter mode
 	"""
-	Encrypt a seed for an {} deterministic wallet
-	""".format(g.proj_name)
 
 	# 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_seed = c.encrypt(seed)
+	enc_data = c.encrypt(data)
 
-	vmsg_r("Performing a test decryption of the seed...")
+	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_seed = c.decrypt(enc_seed)
+	dec_data = c.decrypt(enc_data)
 
-	if dec_seed == seed: vmsg("done")
+	if dec_data == data: vmsg("done\n")
 	else:
-		msg("ERROR.\nDecrypted seed doesn't match original seed")
+		msg("ERROR.\nDecrypted %s doesn't match original %s" % (what,what))
 		sys.exit(2)
 
-	return enc_seed
+	return enc_data
 
 
 def write_to_stdout(data, what, confirm=True):
@@ -354,7 +427,9 @@ def open_file_or_exit(filename,mode):
 	return f
 
 
-def write_to_file(outfile,data,confirm=False):
+def write_to_file(outfile,data,confirm=False,verbose=False):
+
+	if verbose: qmsg("Writing data to file '%s'" % outfile)
 
 	if confirm:
 		from os import stat
@@ -396,8 +471,8 @@ def _display_control_data(label,metadata,hash_preset,salt,enc_seed):
 	from mmgen.bitcoin import b58encode_pad
 	for i in (
 		("Label:",               label),
-		("Seed ID:",             metadata[0]),
-		("Key  ID:",             metadata[1]),
+		("Seed ID:",             metadata[0].upper()),
+		("Key  ID:",             metadata[1].upper()),
 		("Seed length:",         "%s bits (%s bytes)" %
 				(metadata[2],int(metadata[2])/8)),
 		("Scrypt params:",  "Preset '%s' (%s)" % (hash_preset,
@@ -799,10 +874,9 @@ def get_seed_from_incog_wallet(
 			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))
+	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:]
@@ -820,14 +894,14 @@ def get_seed_from_incog_wallet(
 
 def make_key(passwd, salt, hash_preset, what="key"):
 
-	vmsg_r("Generating %s from passphrase.  Please wait..." % what)
+	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, iv=1):
+def decrypt_seed(enc_seed, key, seed_id, key_id):
 
 	vmsg("Checking key...")
 	chk1 = make_chksum_8(key)
@@ -836,23 +910,16 @@ def decrypt_seed(enc_seed, key, seed_id, key_id, iv=1):
 			msg("Incorrect passphrase")
 			return False
 
-	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(g.aesctr_iv_len*8,initial_value=iv))
-	dec_seed = c.decrypt(enc_seed)
+	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...")
-				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:
@@ -867,6 +934,20 @@ def decrypt_seed(enc_seed, key, seed_id, key_id, iv=1):
 	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)
@@ -1007,6 +1088,7 @@ def decode_pretty_hexdump(data):
 	lines = [re.sub('^\d+:\s+','',l) for l in data.split("\n")]
 	return unhexlify("".join(("".join(lines).split())))
 
+
 def wallet_to_incog_data(infile,opts):
 
 	d = get_data_from_wallet(infile,silent=True)
@@ -1019,16 +1101,14 @@ def wallet_to_incog_data(infile,opts):
 	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 = get_random(g.aesctr_iv_len,opts)
 	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")
-	wrap_enc = encrypt_seed(salt + enc_seed, key, iv=int(hexlify(iv),16))
+	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
 

+ 0 - 51
mmgen/walletgen.py

@@ -24,55 +24,4 @@ import mmgen.config as g
 from mmgen.util import msg, msg_r, qmsg, qmsg_r, get_char, prompt_and_get_char
 from binascii import hexlify
 
-def get_random_data_from_user(opts):
 
-	ulen = opts['usr_randlen']
-
-	if g.quiet:
-		msg("Enter %s random symbols" % ulen)
-	else:
-		msg("""
-We're going to be paranoid and not fully trust your OS's random number
-generator.  Please type %s symbols on your keyboard.  Type slowly and choose
-your symbols carefully for maximum randomness.  Try to use both upper and
-lowercase as well as punctuation and numerals.  What you type will not be
-displayed on the screen.
-""" % ulen)
-
-	prompt = "You may begin typing.  %s symbols left: "
-	msg_r(prompt % ulen)
-
-	import time
-	# time.clock() always returns zero, so we'll use time.time()
-	saved_time = time.time()
-
-	user_rand_data,intervals = "",[]
-
-	for i in range(ulen):
-		user_rand_data += get_char(immed_chars="ALL")
-		msg_r("\r" + prompt % (ulen - i - 1))
-		now = time.time()
-		intervals.append(now - saved_time)
-		saved_time = now
-
-	if g.quiet:
-		msg_r("\r")
-	else:
-		msg_r("\rThank you.  That's enough." + " "*15 + "\n\n")
-
-	prompt = "User random data successfully acquired.  Press ENTER to continue"
-	prompt_and_get_char(prompt,"",enter_ok=True)
-
-	return user_rand_data, ["{:.22f}".format(i) for i in intervals]
-
-
-def display_os_random_data(os_rand_data):
-	print "Rand1: {}\nRand2: {}".format(
-			*[hexlify(i) for i in os_rand_data])
-
-
-def display_user_random_data(user_rand_data,intervals_fmt):
-	msg("\nUser random data: " + user_rand_data)
-	msg("Keystroke time intervals:")
-	for i in range(0,len(intervals_fmt),3):
-		msg("  " + " ".join(intervals_fmt[i:i+3]))

+ 2 - 1
setup.py

@@ -3,7 +3,7 @@ from distutils.core import setup
 
 setup(
 		name         = 'mmgen',
-		version      = '0.7.5',
+		version      = '0.7.6',
 		author       = 'Philemon',
 		author_email = 'mmgen-py@yandex.com',
 		url          = 'https://github.com/mmgen/mmgen',
@@ -45,6 +45,7 @@ setup(
 		],
 		scripts=[
 			'mmgen-addrgen',
+			'mmgen-keygen',
 			'mmgen-addrimport',
 			'mmgen-passchg',
 			'mmgen-walletchk',