From a3c4bd873158590f018a502c9f96c942b413ddb9 Mon Sep 17 00:00:00 2001 From: philemon Date: Mon, 14 Jul 2014 18:31:00 +0400 Subject: [PATCH] New feature: export wallet to incognito format Incognito wallet is 48, 56 or 64 bytes of apparently random data. Allows user to hide wallet data in a pre-existing file or on a disk partition (preferably filled in advance with random data). Can be used to hide wallet securely in unencrypted cloud storage or on paper, without revealing the nature of the data. Data may be written at a user-specified offset into the file or partition, in which case user must remember the offset. Each export operation uses a new random init vector to create different data each time. This allows the user to hide wallets at different locations on the Net without detection. User must remember hash preset in addition to passphrase (though trial and error can be used if it's forgotten). Fully integrated with address generation and tx signing operations. --- mmgen-addrgen | 20 ++- mmgen-txsign | 8 +- mmgen-walletchk | 76 +++++++-- mmgen-walletgen | 16 +- mmgen/Opts.py | 67 +++++++- mmgen/addr.py | 8 +- mmgen/config.py | 8 +- mmgen/license.py | 2 +- mmgen/tx.py | 18 +- mmgen/util.py | 278 ++++++++++++++++++++++--------- scripts/bitcoind-walletunlock.py | 8 +- 11 files changed, 384 insertions(+), 125 deletions(-) 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]