diff --git a/mmgen-addrgen b/mmgen-addrgen index 7d48a180..92970255 100755 --- a/mmgen-addrgen +++ b/mmgen-addrgen @@ -66,6 +66,9 @@ help_data = { -b, --from-brain l,p Generate {W} from a user-created password, i.e. a "brainwallet", using seed length 'l' and hash preset 'p' (comma-separated) +-g, --from-incognito Generate {W} from an incognito-format wallet +-G, --hidden-incog-data f,o,l Generate {W} from incognito data in file + 'f' at offset 'o', with seed length of 'l' -m, --from-mnemonic Generate {W} from an electrum-like mnemonic -s, --from-seed Generate {W} from a seed in .{S} format @@ -107,12 +110,14 @@ in all future invocations with that passphrase ) } -short_opts = ["h","A","d:","e","H","K","l:","p:","P:","q","S", - "v","x","b:","m","s"] +short_opts = ["h","A","d:","e", + "H","K","l:","p:", + "P:","q","S","v","x","b:", + "g","G:","m","s"] long_opts = ["help","no_addresses","outdir=","echo_passphrase", "show_hash_presets","no_keyconv","seed_len=","hash_preset=", "passwd_file=","quiet","stdout","verbose","b16","from_brain=", - "from_mnemonic","from_seed"] + "from_incognito","hidden_incog_data=","from_mnemonic","from_seed"] if invoked_as == "addrgen": for i in "A","x": short_opts.remove(i) @@ -123,6 +128,7 @@ opts,cmd_args = process_opts(sys.argv,help_data,"".join(short_opts),long_opts) if 'show_hash_presets' in opts: show_hash_presets() if 'quiet' in opts: g.quiet = True if 'verbose' in opts: g.verbose = True +if 'hidden_incog_data' in opts: opts['from_incognito'] = True opts['gen_what'] = gen_what @@ -131,9 +137,10 @@ check_opts(opts,long_opts) if g.debug: show_opts_and_cmd_args(opts,cmd_args) if len(cmd_args) == 1 and ( - 'from_mnemonic' in opts or - 'from_brain' in opts or - 'from_seed' in opts + 'from_mnemonic' in opts + or 'from_brain' in opts + or 'from_seed' in opts + or 'from_incognito' in opts ): infile,addr_list_arg = "",cmd_args[0] elif len(cmd_args) == 2: @@ -160,6 +167,7 @@ else: seed = get_seed_retry(infile,opts) seed_id = make_chksum_8(seed) + addr_data = generate_addrs(seed, addr_list, opts) addr_data_str = format_addr_data(addr_data, seed_id, opts) diff --git a/mmgen-txsign b/mmgen-txsign index 4daf172c..9635307d 100755 --- a/mmgen-txsign +++ b/mmgen-txsign @@ -46,9 +46,10 @@ help_data = { -b, --from-brain l,p Generate keys from a user-created password, i.e. a "brainwallet", using seed length 'l' and hash preset 'p' +-w, --use-wallet-dat Get keys from a running bitcoind +-g, --from-incognito Generate keys from an incognito-format wallet -m, --from-mnemonic Generate keys from an electrum-like mnemonic -s, --from-seed Generate keys from a seed in .{} format --w, --use-wallet-dat Get keys from a running bitcoind Transactions with either mmgen or non-mmgen input addresses may be signed. For non-mmgen inputs, the bitcoind wallet.dat is used as the key source. @@ -77,10 +78,11 @@ Seed data supplied in files must have the following extensions: """.format(g.seed_ext,g.wallet_ext,g.seed_ext,g.mn_ext,g.brain_ext) } -short_opts = "hd:eiIk:P:qVb:msw" +short_opts = "hd:eiIk:P:qVb:wgms" long_opts = "help","outdir=","echo_passphrase","info","tx_id",\ "keys_from_file=","passwd_file=","quiet","skip_key_preverify",\ - "from_brain=","from_mnemonic","from_seed","use_wallet_dat" + "from_brain=","use_wallet_dat",\ + "from_incognito","from_mnemonic","from_seed" opts,infiles = process_opts(sys.argv,help_data,short_opts,long_opts) if "quiet" in opts: g.quiet = True diff --git a/mmgen-walletchk b/mmgen-walletchk index 9c5da705..8ac55dcb 100755 --- a/mmgen-walletchk +++ b/mmgen-walletchk @@ -35,22 +35,27 @@ help_data = { -h, --help Print this help message -d, --outdir d Specify an alternate directory 'd' for output -e, --echo-passphrase Print passphrase to screen when typing it --m, --export-mnemonic Export the wallet's mnemonic to file -P, --passwd-file f Get passphrase from file 'f' -q, --quiet Suppress warnings; overwrite files without prompting --s, --export-seed Export the wallet's seed to file -S, --stdout Print seed or mnemonic data to standard output -v, --verbose Produce more verbose output +-g, --export-incognito Export wallet to incognito format +-G, --hide-incog-data f,o Hide incognito data in existing file 'f' + at offset 'o' (comma-separated) +-m, --export-mnemonic Export the wallet's mnemonic to file +-s, --export-seed Export the wallet's seed to file """ } -short_opts = "hd:emP:qsSv" -long_opts = "help","outdir=","echo_passphrase","export_mnemonic",\ - "passwd_file=","quiet","export_seed","stdout","verbose" +short_opts = "hd:eP:qSvgG:ms" +long_opts = "help","outdir=","echo_passphrase","passwd_file=","quiet",\ + "stdout","verbose",\ + "export_incognito","hide_incog_data=","export_mnemonic","export_seed" opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) if 'quiet' in opts: g.quiet = True if 'verbose' in opts: g.verbose = True +if 'hide_incog_data' in opts: opts['export_incognito'] = True # Argument sanity checks and processing: check_opts(opts,long_opts) @@ -61,20 +66,69 @@ check_infile(cmd_args[0]) if 'export_mnemonic' in opts: qmsg("Exporting mnemonic data to file by user request") -if 'export_seed' in opts: +elif 'export_seed' in opts: qmsg("Exporting seed data to file by user request") +elif 'export_incognito' in opts: + qmsg("Exporting wallet to incognito format by user request") + + d = get_data_from_wallet(cmd_args[0],silent=True) + seed_id,key_id,preset,salt,enc_seed = \ + d[1][0], d[1][1], d[2].split(":")[0], d[3], d[4] + + passwd = get_mmgen_passphrase("Enter mmgen passphrase: ",opts) + key = make_key(passwd, salt, preset, "main key") + # We don't need the seed; just do this to verify password. + if decrypt_seed(enc_seed, key, seed_id, key_id) == False: + sys.exit(2) + + from Crypto import Random + iv = Random.new().read(g.aesctr_iv_len) + iv_id = make_chksum_8(iv) + qmsg("IV ID: %s" % iv_id) + + from binascii import hexlify + from hashlib import sha256 + # IV is used BOTH to initialize counter and to salt password! + key = make_key(passwd, iv, preset, "wrapper key") + incog_enc = encrypt_seed(salt + enc_seed, key, iv=int(hexlify(iv),16)) + + if "hide_incog_data" in opts: + fname,offset = opts['hide_incog_data'].split(",") # Already sanity-checked + offset = int(offset) + + check_data_fits_file_at_offset(fname,offset,len(iv + incog_enc),"write") + + if not g.quiet: confirm_or_exit("","alter file '%s'" % fname) + f = os.open(fname,os.O_RDWR) + os.lseek(f, offset, os.SEEK_SET) + os.write(f, iv + incog_enc) + os.close(f) + qmsg("Data written to file '%s' at offset %s" % (fname,offset), + "Data written to file") + else: + fn = "%s-%s-%s[%s,%s].%s" % (seed_id, key_id, iv_id, + len(enc_seed)*8, preset, g.incog_ext) + export_to_file(fn, iv + incog_enc, "incognito wallet data", opts) + + sys.exit() seed = get_seed_from_wallet(cmd_args[0], opts) if seed: qmsg("Wallet is OK") +else: + msg("Error opening wallet") + sys.exit(2) if 'export_mnemonic' in opts: wl = get_default_wordlist() - from mmgen.mnemonic import get_mnemonic_from_seed p = True if g.debug else False mn = get_mnemonic_from_seed(seed, wl, g.default_wl, print_info=p) + fn = "%s.%s" % (make_chksum_8(seed).upper(), g.mn_ext) + export_to_file(fn, " ".join(mn)+"\n", "mnemonic data", opts) - write_mnemonic(mn, seed, opts) - -if 'export_seed' in opts: - write_seed(seed, opts) +elif 'export_seed' in opts: + from mmgen.bitcoin import b58encode_pad + data = col4(b58encode_pad(seed)) + chk = make_chksum_6(b58encode_pad(seed)) + fn = "%s.%s" % (make_chksum_8(seed).upper(), g.seed_ext) + export_to_file(fn, "%s %s\n" % (chk,data), "seed data", opts) diff --git a/mmgen-walletgen b/mmgen-walletgen index 519f6989..58efb972 100755 --- a/mmgen-walletgen +++ b/mmgen-walletgen @@ -54,6 +54,7 @@ help_data = { -b, --from-brain l,p Generate wallet from a user-created passphrase, i.e. a "brainwallet", using seed length 'l' and hash preset 'p' (comma-separated) +-g, --from-incognito Generate wallet from an incognito-format wallet -m, --from-mnemonic Generate wallet from an Electrum-like mnemonic -s, --from-seed Generate wallet from a seed in .{S} format @@ -93,10 +94,10 @@ in all future invocations with that passphrase. ) } -short_opts = "hd:eHl:L:p:P:qu:vb:ms" +short_opts = "hd:eHl:L:p:P:qu:vb:gms" long_opts = "help","outdir=","echo_passphrase","show_hash_presets","seed_len=",\ - "label=","hash_preset=","passwd_file=","quiet","usr_randlen=",\ - "verbose","from_brain=","from_mnemonic","from_seed" + "label=","hash_preset=","passwd_file=","quiet","usr_randlen=","verbose",\ + "from_brain=","from_incognito","from_mnemonic","from_seed" opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) if 'quiet' in opts: g.quiet = True @@ -111,7 +112,7 @@ if len(cmd_args) == 1: infile = cmd_args[0] check_infile(infile) ext = infile.split(".")[-1] - ok_exts = g.seed_ext, g.mn_ext, g.brain_ext + ok_exts = g.seedfile_exts for e in ok_exts: if e == ext: break else: @@ -151,10 +152,13 @@ if g.debug: display_user_random_data(usr_keys,key_timings) usr_rand_data = sha256(usr_keys).digest() + \ sha256("".join(key_timings)).digest() -for i in 'from_mnemonic','from_brain','from_seed': +for i in 'from_mnemonic','from_brain','from_seed','from_incognito': if infile or (i in opts): seed = get_seed_retry(infile,opts) - qmsg(""); break + if "from_incognito" in opts or get_extension(infile) == g.incog_ext: + qmsg(cmessages['incognito'] % make_chksum_8(seed)) + else: qmsg("") + break else: # Truncate random data for smaller seed lengths seed = os_rand_data[0] + usr_rand_data diff --git a/mmgen/Opts.py b/mmgen/Opts.py index 3bbc195d..25b65ce6 100755 --- a/mmgen/Opts.py +++ b/mmgen/Opts.py @@ -18,7 +18,6 @@ import sys, getopt import mmgen.config as g -from mmgen.util import msg,check_infile def usage(hd): print "USAGE: %s %s" % (hd['prog_name'], hd['usage']) @@ -70,6 +69,15 @@ def process_opts(argv,help_data,short_opts,long_opts): return opts,args +def show_opts_and_cmd_args(opts,cmd_args): + print "Processed options: %s" % repr(opts) + print "Cmd args: %s" % repr(cmd_args) + + +# Everything below here is MMGen-specific: + +from mmgen.util import msg,check_infile + def check_opts(opts,long_opts): # These must be set to the default values in mmgen.config: @@ -91,6 +99,7 @@ def check_opts(opts,long_opts): if opt == 'outdir': what = "output directory" import re, os, stat + # TODO Non-portable: d = re.sub(r'/*$','', val) opts[opt] = d @@ -121,12 +130,61 @@ def check_opts(opts,long_opts): "%s": illegal character in label. Only ASCII characters are permitted. """.strip() % ch) sys.exit(1) + elif opt == 'hide_incog_data' or opt == 'hidden_incog_data': + try: + if opt == 'hide_incog_data': + outfile,offset = val.split(",") + else: + outfile,offset,seed_len = val.split(",") + except: + msg("'%s': invalid %s" % (val,what)) + sys.exit(1) + + try: + o = int(offset) + except: + msg("'%s': invalid 'o' %s (not an integer)" % (offset,what)) + sys.exit(1) + + if o < 0: + msg("'%s': invalid 'o' %s (less than zero)" % (offset,what)) + sys.exit(1) + + if opt == 'hidden_incog_data': + try: + sl = int(seed_len) + except: + msg("'%s': invalid 'l' %s (not an integer)" % (sl,what)) + sys.exit(1) + + if sl not in g.seed_lens: + msg("'%s': invalid 'l' %s (valid choices: %s)" % + (sl,what," ".join(str(i) for i in g.seed_lens))) + sys.exit(1) + + import os, stat + try: mode = os.stat(outfile).st_mode + except: + msg("Unable to stat requested %s '%s'" % (what,outfile)) + sys.exit(1) + + if not (stat.S_ISREG(mode) or stat.S_ISBLK(mode)): + msg("Requested %s '%s' is not a file or block device" % + (what,outfile)) + sys.exit(1) + + ac,m = (os.W_OK,"writ") \ + if "hide_incog_data" in opts else (os.R_OK,"read") + if not os.access(outfile, ac): + msg("Requested %s '%s' is un%sable by you" % (what,outfile,m)) + sys.exit(1) + elif opt == 'from_brain': try: l,p = val.split(",") except: msg("'%s': invalid %s" % (val,what)) - sys.exit(1) + sys.exit(2) try: int(l) @@ -162,11 +220,6 @@ def check_opts(opts,long_opts): if g.debug: print "check_opts(): No test for opt '%s'" % opt -def show_opts_and_cmd_args(opts,cmd_args): - print "Processed options: %s" % repr(opts) - print "Cmd args: %s" % repr(cmd_args) - - def set_if_unset_and_typeconvert(opts,opt): if opt in g.cl_override_vars: diff --git a/mmgen/addr.py b/mmgen/addr.py index e3510fd1..fd9a25c2 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -63,6 +63,9 @@ def generate_addrs(seed, addrnums, opts): while a: seed = sha512(seed).digest() i += 1 # round /i/ + + if g.debug: print "Seed round %s: %s" % (i, hexlify(seed)) + if i < a[0]: continue a.pop(0) @@ -74,6 +77,9 @@ def generate_addrs(seed, addrnums, opts): sec = sha256(sha256(seed).digest()).hexdigest() wif = numtowif(int(sec,16)) + if g.debug: + print "Privkey round %s:\n hex: %s\n wif: %s" % (i, sec, wif) + el = { 'num': i } if not 'print_addresses_only' in opts: @@ -140,7 +146,7 @@ def format_addr_data(addrlist, seed_chksum, opts): # Everything following a hash symbol '#' is a comment and ignored by {}. # A text label of {} characters or less may be added to the right of each # address, and it will be appended to the bitcoind wallet label upon import. -# The label may contain printable ASCII symbols. +# The label may contain any printable ASCII symbol. """.strip().format(g.proj_name_cap,g.max_addr_label_len) data = [] diff --git a/mmgen/config.py b/mmgen/config.py index 8844cd1b..850dec20 100755 --- a/mmgen/config.py +++ b/mmgen/config.py @@ -32,8 +32,9 @@ wallet_ext = "mmdat" seed_ext = "mmseed" mn_ext = "mmwords" brain_ext = "mmbrain" +incog_ext = "mmincog" -seedfile_exts = wallet_ext, seed_ext, mn_ext, brain_ext +seedfile_exts = wallet_ext, seed_ext, mn_ext, brain_ext, incog_ext addrfile_ext = "addrs" keyfile_ext = "keys" @@ -53,13 +54,15 @@ http_timeout = 30 keyconv_exec = "keyconv" from os import getenv -debug = True if getenv("MMGEN_DEBUG") else False +debug = True if getenv("MMGEN_DEBUG") else False +no_license = True if getenv("MMGEN_NOLICENSE") else False mins_per_block = 8.5 passwd_max_tries = 5 max_randlen,min_randlen = 80,5 usr_randlen = 20 salt_len = 16 +aesctr_iv_len = 16 hash_preset = '3' hash_presets = { @@ -72,6 +75,7 @@ hash_presets = { '4': [15, 8, 12], '5': [16, 8, 16], '6': [17, 8, 20], + '7': [18, 8, 24], } mmgen_idx_max_digits = 7 diff --git a/mmgen/license.py b/mmgen/license.py index afc54276..0bc72a60 100755 --- a/mmgen/license.py +++ b/mmgen/license.py @@ -587,7 +587,7 @@ copy of the Program in return for a fee. def do_license_msg(immed=False): import mmgen.config as g - if g.quiet: return + if g.quiet or g.no_license: return msg(gpl['warning']) prompt = "%s " % gpl['prompt'].strip() diff --git a/mmgen/tx.py b/mmgen/tx.py index ff67bf3e..971e5b5e 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -584,7 +584,7 @@ Generated from seed: %s """.strip() % (" ".join(a)," ".join(b))) sys.exit(3) - qmsg("Address mappings OK\n") + qmsg("Address mappings OK") def check_addr_label(label): @@ -671,14 +671,14 @@ def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,seeds,opts,gen_pairs=False): while seed_ids: if seeds_keys: - seed = seeds[seeds_keys.pop()] + seed = seeds[seeds_keys.pop(0)] else: infile = False if infiles: - infile = infiles.pop() + infile = infiles.pop(0) seed = get_seed_retry(infile,opts) elif "from_brain" in opts or "from_mnemonic" in opts \ - or "from_seed" in opts: + or "from_seed" in opts or "from_incognito" in opts: msg("Need data for seed ID %s" % seed_ids[0]) seed = get_seed_retry("",opts) else: @@ -707,8 +707,14 @@ def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,seeds,opts,gen_pairs=False): else: msg(" for ID %s" % seed_id) else: msg("Seed source produced an invalid seed ID (%s)" % seed_id) - if infile: - msg("Invalid input file: %s" % infile) + if "from_incognito" in opts or infile.split(".")[-1] == g.incog_ext: + msg( +"""Incorrect hash preset, password or incognito wallet data + +Trying again...""") + infiles.insert(0,infile) # ugly! + elif infile: + msg("Invalid input file '%s'" % infile) sys.exit(2) return ret diff --git a/mmgen/util.py b/mmgen/util.py index aca04486..c58a8b3f 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -27,10 +27,14 @@ from mmgen.term import * def msg(s): sys.stderr.write(s + "\n") def msg_r(s): sys.stderr.write(s) -def qmsg(s): - if not g.quiet: sys.stderr.write(s + "\n") -def qmsg_r(s): - if not g.quiet: sys.stderr.write(s) +def qmsg(s,alt=""): + if g.quiet: + if alt: sys.stderr.write(alt + "\n") + else: sys.stderr.write(s + "\n") +def qmsg_r(s,alt=""): + if g.quiet: + if alt: sys.stderr.write(alt) + else: sys.stderr.write(s) def vmsg(s): if g.verbose: sys.stderr.write(s + "\n") def vmsg_r(s): @@ -38,6 +42,8 @@ def vmsg_r(s): def bail(): sys.exit(9) +def get_extension(f): return f.split(".")[-1] + def my_raw_input(prompt,echo=True,allowed_chars=""): try: if echo: @@ -96,6 +102,25 @@ def show_hash_presets(): cmessages = { 'null': "", + 'incognito_iv_id': """ + If you know your IV ID, check it against the value above. If it's + incorrect, then your incognito data is invalid. +""", + 'incognito_iv_id_hidden': """ + If you know your IV ID, check it against the value above. If it's + incorrect, then your incognito data is invalid or you've supplied + an incorrect offset. +""", + 'incognito_key_id': """ + Check that the generated seed ID is correct. If it's not, then your + password or hash preset is incorrect or incognito data is corrupted. +""", + 'incognito_key_id_hidden': """ + Check that the generated seed ID is correct. If it's not, then your + password or hash preset is incorrect or incognito data is corrupted. + If the key ID is correct but the seed ID is not, then you might have + chosen an incorrect seed length. +""", 'unencrypted_secret_keys': """ This program generates secret keys from your {} seed, outputting them in UNENCRYPTED form. Generate only the key(s) you need and guard them carefully. @@ -136,7 +161,7 @@ def confirm_or_exit(message, question, expect="YES"): msg("Exiting at user request") sys.exit(2) - msg("") + vmsg("") def user_confirm(prompt,default_yes=False,verbose=False): @@ -265,6 +290,9 @@ def get_new_passphrase(what, opts): def _scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32): + # Buflen arg is for brainwallets only, which use this function to generate + # the seed directly. + N,r,p = _get_hash_params(hash_preset) import scrypt @@ -288,7 +316,7 @@ def _get_seed_from_brain_passphrase(words,opts): return seed -def encrypt_seed(seed, key): +def encrypt_seed(seed, key, iv=1): """ Encrypt a seed for an {} deterministic wallet """.format(g.proj_name_cap) @@ -297,12 +325,14 @@ def encrypt_seed(seed, key): from Crypto.Cipher import AES from Crypto.Util import Counter - c = AES.new(key, AES.MODE_CTR,counter=Counter.new(128)) + c = AES.new(key, AES.MODE_CTR, + counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv)) enc_seed = c.encrypt(seed) vmsg_r("Performing a test decryption of the seed...") - c = AES.new(key, AES.MODE_CTR,counter=Counter.new(128)) + c = AES.new(key, AES.MODE_CTR, + counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv)) dec_seed = c.decrypt(enc_seed) if dec_seed == seed: vmsg("done") @@ -366,60 +396,44 @@ def write_to_file(outfile,data,confirm=False): f.close -def write_seed(seed, opts): - - outfile = "%s.%s" % (make_chksum_8(seed).upper(),g.seed_ext) - if 'outdir' in opts: - outfile = "%s/%s" % (opts['outdir'], outfile) - - from mmgen.bitcoin import b58encode_pad - data = col4(b58encode_pad(seed)) - chk = make_chksum_6(b58encode_pad(seed)) - - o = "%s %s\n" % (chk,data) +def export_to_file(outfile, data, label, opts): if 'stdout' in opts: - write_to_stdout(o,"seed data",confirm=True) + write_to_stdout(data, label, confirm=True) elif not sys.stdout.isatty(): - write_to_stdout(o,"seed data",confirm=False) + write_to_stdout(data, label, confirm=False) else: - write_to_file(outfile,o) - msg("%s data saved to file '%s'" % ("Seed",outfile)) - - -def write_mnemonic(mn, seed, opts): - - outfile = "%s.%s" % (make_chksum_8(seed).upper(),g.mn_ext) - if 'outdir' in opts: - outfile = "%s/%s" % (opts['outdir'], outfile) - - o = " ".join(mn) + "\n" - - if 'stdout' in opts: - write_to_stdout(o,"mnemonic data",confirm=True) - elif not sys.stdout.isatty(): - write_to_stdout(o,"mnemonic data",confirm=False) - else: - write_to_file(outfile,o) - msg("%s data saved to file '%s'" % ("Mnemonic",outfile)) + if 'outdir' in opts: + outfile = "%s/%s" % (opts['outdir'], outfile) + write_to_file(outfile, data, confirm=False if g.quiet else True) + msg("%s saved to file '%s'" % (label.capitalize(), outfile)) def _display_control_data(label,metadata,hash_preset,salt,enc_seed): msg("WALLET DATA") - fs = " {:25} {}" + fs = " {:18} {}" pw_empty = "yes" if metadata[3] == "E" else "no" from mmgen.bitcoin import b58encode_pad for i in ( ("Label:", label), ("Seed ID:", metadata[0]), ("Key ID:", metadata[1]), - ("Seed length:", metadata[2]), - ("Scrypt hash params:", "Preset '%s' (%s)" % (hash_preset, - " ".join([str(i) for i in _get_hash_params(hash_preset)]))), - ("Passphrase is empty:", pw_empty), + ("Seed length:", "%s bits (%s bytes)" % + (metadata[2],int(metadata[2])/8)), + ("Scrypt params:", "Preset '%s' (%s)" % (hash_preset, + " ".join([str(i) for i in _get_hash_params(hash_preset)]))), + ("Passphrase empty?", pw_empty.capitalize()), ("Timestamp:", "%s UTC" % metadata[4]), - ("Salt:", b58encode_pad(salt)), - ("Encrypted seed:", b58encode_pad(enc_seed)) + ): msg(fs.format(*i)) + + fs = " {:6} {}" + for i in ( + ("Salt:", ""), + (" b58:", b58encode_pad(salt)), + (" hex:", hexlify(salt)), + ("Encrypted seed:", ""), + (" b58:", b58encode_pad(enc_seed)), + (" hex:", hexlify(enc_seed)) ): msg(fs.format(*i)) @@ -496,8 +510,9 @@ def _compare_checksums(chksum1, desc1, chksum2, desc2): return True else: if g.debug: - msg("ERROR!\nComputed checksum %s (%s) doesn't match checksum %s (%s)" \ - % (desc1,chksum1,desc2,chksum2)) + print \ + "ERROR!\nComputed checksum %s (%s) doesn't match checksum %s (%s)" \ + % (desc1,chksum1,desc2,chksum2) return False def _is_hex(s): @@ -553,7 +568,7 @@ def _check_chksum_6(chk,val,desc,infile): msg("Checksum: %s. Computed value: %s" % (chk,comp_chk)) sys.exit(2) elif g.debug: - msg("%s checksum passed: %s" % (desc.capitalize(),chk)) + print "%s checksum passed: %s" % (desc.capitalize(),chk) def get_data_from_wallet(infile,silent=False): @@ -716,45 +731,143 @@ def get_seed_from_wallet( return decrypt_seed(enc_seed, key, metadata[0], metadata[1]) -def make_key(passwd, salt, hash_preset): +def check_data_fits_file_at_offset(fname,offset,dlen,action): + # TODO: Check for Windows + import os, stat + if stat.S_ISBLK(os.stat(fname).st_mode): + fd = os.open(fname, os.O_RDONLY) + fsize = os.lseek(fd, 0, os.SEEK_END) + os.close(fd) + else: + fsize = os.stat(fname).st_size - vmsg_r("Hashing passphrase. Please wait...") + if fsize < offset + dlen: + msg( +"Destination file has length %s, too short to %s %s bytes of data at offset %s" + % (fsize,action,dlen,offset)) + sys.exit(1) + + +def get_hidden_incog_data(opts): + # Already sanity-checked: + fname,offset,seed_len = opts['hidden_incog_data'].split(",") + qmsg("Getting hidden incog data from file '%s'" % fname) + + dlen = g.aesctr_iv_len + g.salt_len + (int(seed_len)/8) + + fsize = check_data_fits_file_at_offset(fname,int(offset),dlen,"read") + + f = os.open(fname,os.O_RDONLY) + os.lseek(f, int(offset), os.SEEK_SET) + data = os.read(f, dlen) + os.close(f) + qmsg("Data read from file '%s' at offset %s" % (fname,offset), + "Data read from file") + return data + +def get_seed_from_incog_wallet( + infile, + opts, + prompt="Enter %s wallet passphrase: " % g.proj_name_cap, + silent=False + ): + + what = "incognito wallet data" + + if "hidden_incog_data" in opts: + d = get_hidden_incog_data(opts) + else: + d = get_data_from_file(infile,what) + # File could be of invalid length, so check: + valid_dlens = [i/8 + g.aesctr_iv_len + g.salt_len for i in g.seed_lens] + if len(d) not in valid_dlens: + qmsg("Invalid incognito file size: %s. Valid sizes (in bytes): %s" % + (len(d), " ".join([str(i) for i in valid_dlens])) + ) + return False + + iv, enc_incog_data = d[0:g.aesctr_iv_len], d[g.aesctr_iv_len:] + + qmsg("IV ID: %s. Check this value if possible." % make_chksum_8(iv)) + vmsg(cmessages['incognito_iv_id_hidden' if "hidden_incog_data" in opts + else 'incognito_iv_id']) + + passwd = get_mmgen_passphrase(prompt,opts) + + msg("Configured hash presets: %s" % " ".join(sorted(g.hash_presets))) + while True: + p = "Enter hash preset for %s wallet (default='%s'): " + hp = my_raw_input(p % (g.proj_name_cap, g.hash_preset)) + if not hp: + hp = g.hash_preset; break + elif hp in g.hash_presets: + break + msg("%s: Invalid hash preset" % hp) + + from hashlib import sha256 + # IV is used BOTH to initialize counter and to salt password! + key = make_key(passwd, iv, hp, "wrapper key") + d = decrypt_seed(enc_incog_data, key, "", "", iv=int(hexlify(iv),16)) + if d == False: sys.exit(2) + + salt,enc_seed = d[0:g.salt_len], d[g.salt_len:] + + key = make_key(passwd, salt, hp, "main key") + vmsg("Key ID: %s" % make_chksum_8(key)) + + seed = decrypt_seed(enc_seed, key, "", "") + qmsg("Seed ID: %s. Check that this value is correct." % make_chksum_8(seed)) + vmsg(cmessages['incognito_key_id_hidden' if "hidden_incog_data" in opts + else 'incognito_key_id']) + + return seed + + +def make_key(passwd, salt, hash_preset, what="key"): + + vmsg_r("Generating %s from passphrase. Please wait..." % what) key = _scrypt_hash_passphrase(passwd, salt, hash_preset) vmsg("done") + if g.debug: print "Key: %s" % hexlify(key) return key -def decrypt_seed(enc_seed, key, seed_id, key_id): +def decrypt_seed(enc_seed, key, seed_id, key_id, iv=1): - vmsg_r("Checking key...") - chk = make_chksum_8(key) - if not _compare_checksums(chk, "of key", key_id, "in header"): - msg("Incorrect passphrase") - return False + vmsg("Checking key...") + chk1 = make_chksum_8(key) + if key_id: + if not _compare_checksums(chk1, "of key", key_id, "in header"): + msg("Incorrect passphrase") + return False - vmsg_r("Decrypting seed with key...") + vmsg("Decrypting seed with key...") from Crypto.Cipher import AES from Crypto.Util import Counter - c = AES.new(key, AES.MODE_CTR,counter=Counter.new(128)) + c = AES.new(key, AES.MODE_CTR, + counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv)) dec_seed = c.decrypt(enc_seed) - chk = make_chksum_8(dec_seed) - if _compare_checksums(chk,"of decrypted seed",seed_id,"in header"): - qmsg("Passphrase is OK") - else: - if not g.debug: - msg_r("Checking key ID...") - chk = make_chksum_8(key) - if _compare_checksums(chk, "of key", key_id, "in header"): - msg("Key ID is correct but decryption of seed failed") - else: - msg("Incorrect passphrase") + chk2 = make_chksum_8(dec_seed) + if seed_id: + if _compare_checksums(chk2,"of decrypted seed",seed_id,"in header"): + qmsg("Passphrase is OK") + else: + if not g.debug: + msg_r("Checking key ID...") + chk1 = make_chksum_8(key) + if _compare_checksums(chk1, "of key", key_id, "in header"): + msg("Key ID is correct but decryption of seed failed") + else: + msg("Incorrect passphrase") - return False + return False +# else: +# qmsg("Generated IDs (Seed/Key): %s/%s" % (chk2,chk1)) - if g.debug: msg("key: %s" % hexlify(key)) + if g.debug: print "Decrypted seed: %s" % hexlify(dec_seed) return dec_seed @@ -774,9 +887,11 @@ def get_seed(infile,opts,silent=False): elif ext == g.brain_ext: source = "brainwallet" elif ext == g.seed_ext: source = "seed" elif ext == g.wallet_ext: source = "wallet" - elif 'from_mnemonic' in opts: source = "mnemonic" - elif 'from_brain' in opts: source = "brainwallet" - elif 'from_seed' in opts: source = "seed" + elif ext == g.incog_ext: source = "incognito wallet" + elif 'from_mnemonic' in opts: source = "mnemonic" + elif 'from_brain' in opts: source = "brainwallet" + elif 'from_seed' in opts: source = "seed" + elif 'from_incognito' in opts: source = "incognito wallet" else: if infile: msg( "Invalid file extension for file: %s\nValid extensions: '.%s'" % @@ -808,11 +923,17 @@ def get_seed(infile,opts,silent=False): seed = _get_seed_from_seed_data(words) elif source == "wallet": seed = get_seed_from_wallet(infile, opts, silent=silent) + elif source == "incognito wallet": + seed = get_seed_from_incog_wallet(infile, opts, silent=silent) - if infile and not seed and (source == "seed" or source == "mnemonic"): - msg("Invalid %s file: %s" % (source,infile)) + + if infile and not seed and ( + source == "seed" or source == "mnemonic" or source == "incognito wallet"): + msg("Invalid %s file '%s'" % (source,infile)) sys.exit(2) + if g.debug: print "Seed: %s" % hexlify(seed) + return seed # Repeat if entered data is invalid @@ -863,5 +984,6 @@ def do_pager(text): break else: print text+end + if __name__ == "__main__": print "util.py" diff --git a/scripts/bitcoind-walletunlock.py b/scripts/bitcoind-walletunlock.py index 3ecbcc48..c1641a36 100755 --- a/scripts/bitcoind-walletunlock.py +++ b/scripts/bitcoind-walletunlock.py @@ -2,17 +2,17 @@ # # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution # Copyright (C) 2013 by philemon -# +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ @@ -22,7 +22,7 @@ bitcoind-walletunlock.py: Unlock a Bitcoin wallet securely import sys from mmgen.Opts import * from mmgen.tx import * -from mmgen.utils import msg, my_getpass, my_raw_input +from mmgen.util import msg, my_getpass, my_raw_input prog_name = sys.argv[0].split("/")[-1]