From 490879f96856068bc2b120536098a04d557f59cb Mon Sep 17 00:00:00 2001 From: philemon Date: Wed, 30 Jul 2014 00:48:45 +0400 Subject: [PATCH] Online signing, new commands for 'mmgen-tool', code cleanups, bugfixes --- README.md | 3 + mmgen-addrgen | 62 +++++++++--------- mmgen-addrimport | 4 +- mmgen-passchg | 29 ++++----- mmgen-tool | 27 ++++---- mmgen-txcreate | 18 +++--- mmgen-txsend | 6 +- mmgen-txsign | 72 +++++++++++++-------- mmgen-walletchk | 15 ++++- mmgen-walletgen | 48 ++++---------- mmgen/Opts.py | 79 ++++++++++++----------- mmgen/addr.py | 51 ++++++++------- mmgen/config.py | 9 ++- mmgen/tool.py | 81 ++++++++++++++++++------ mmgen/tx.py | 76 ++++++++++++++++------ mmgen/util.py | 154 ++++++++++++++++++++++++++++++++++----------- mmgen/walletgen.py | 51 --------------- setup.py | 3 +- 18 files changed, 466 insertions(+), 322 deletions(-) diff --git a/README.md b/README.md index b57a0429..0c02f32e 100644 --- a/README.md +++ b/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 diff --git a/mmgen-addrgen b/mmgen-addrgen index 82409b60..23ac14e0 100755 --- a/mmgen-addrgen +++ b/mmgen-addrgen @@ -38,36 +38,40 @@ help_data = { mnemonic, seed or password""".format(gen_what,g=g), 'usage':"[opts] [infile]
", '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{} +-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 +-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) diff --git a/mmgen-addrimport b/mmgen-addrimport index 1757315d..10b0ac23 100755 --- a/mmgen-addrimport +++ b/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 diff --git a/mmgen-passchg b/mmgen-passchg index 2b64cbf5..7165532e 100755 --- a/mmgen-passchg +++ b/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)) diff --git a/mmgen-tool b/mmgen-tool index da66e9b8..f902cdbe 100755 --- a/mmgen-tool +++ b/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] ", '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 '{} --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) + ")") diff --git a/mmgen-txcreate b/mmgen-txcreate index 6d5f7e47..e81bd0c7 100755 --- a/mmgen-txcreate +++ b/mmgen-txcreate @@ -36,13 +36,13 @@ help_data = { 'desc': "Create a BTC transaction with outputs to specified addresses", 'usage': "[opts] ... [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 :. 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) diff --git a/mmgen-txsend b/mmgen-txsend index 1f159d50..75c0018e 100755 --- a/mmgen-txsend +++ b/mmgen-txsend @@ -34,9 +34,9 @@ help_data = { 'desc': "Send a Bitcoin transaction signed by mmgen-txsign", 'usage': "[opts] ", '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 """ } diff --git a/mmgen-txsign b/mmgen-txsign index 98025c21..7bd71485 100755 --- a/mmgen-txsign +++ b/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] ,.. [mmgen wallet/seed/words/brainwallet file]...", + 'usage': "[opts] .. [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- 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: diff --git a/mmgen-walletchk b/mmgen-walletchk index 815bbb19..ac938d00 100755 --- a/mmgen-walletchk +++ b/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) diff --git a/mmgen-walletgen b/mmgen-walletgen index 38380ce0..91d0a15f 100755 --- a/mmgen-walletgen +++ b/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] + seed = sha256(get_random(128,opts)).digest()[:opts['seed_len']/8] -salt = os_rand_data[1] + usr_rand_data -salt = sha256(salt).digest()[:g.salt_len] +salt = sha256(get_random(128,opts)).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']) +qmsg(cmessages['choose_wallet_passphrase'] % opts['hash_preset']) passwd = get_new_passphrase("{} wallet passphrase".format(g.proj_name), opts) diff --git a/mmgen/Opts.py b/mmgen/Opts.py index aaf74f8a..b940c2d4 100755 --- a/mmgen/Opts.py +++ b/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] + 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() - 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] + 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 not check_opts(opts,long_opts): sys.exit(1) # MMGen only! + # 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 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) diff --git a/mmgen/addr.py b/mmgen/addr.py index 2dd92524..e9298507 100755 --- a/mmgen/addr.py +++ b/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") diff --git a/mmgen/config.py b/mmgen/config.py index 0f98ba4a..5b87a31d 100755 --- a/mmgen/config.py +++ b/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 diff --git a/mmgen/tool.py b/mmgen/tool.py index dfb976a5..2e2d7bb6 100755 --- a/mmgen/tool.py +++ b/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": [' [str]'], "hextob58": [' [str]'], @@ -66,6 +68,8 @@ commands = { "pubkey2addr": [' [str]'], "pubkey2hexaddr": [' [str]'], "privhex2addr": [' [str]','compressed [bool=False]'], + "encrypt": [' [str]','outfile [str=""]','hash_preset [str="3"]'], + "decrypt": [' [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") diff --git a/mmgen/tx.py b/mmgen/tx.py index fd25af0d..2c6dd8d9 100755 --- a/mmgen/tx.py +++ b/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") diff --git a/mmgen/util.py b/mmgen/util.py index 7bc2a3d3..31446d37 100755 --- a/mmgen/util.py +++ b/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 - 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...") + vmsg("Encrypting %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) + enc_data = c.encrypt(data) - if dec_seed == seed: vmsg("done") + vmsg_r("Performing a test decryption of the %s..." % what) + + c = AES.new(key, AES.MODE_CTR, + counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv)) + dec_data = c.decrypt(enc_data) + + if dec_data == data: vmsg("done\n") else: - msg("ERROR.\nDecrypted 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 diff --git a/mmgen/walletgen.py b/mmgen/walletgen.py index f3b3a02e..b82051bf 100755 --- a/mmgen/walletgen.py +++ b/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])) diff --git a/setup.py b/setup.py index 3e571923..cf03e1a5 100755 --- a/setup.py +++ b/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',