From 26aa2eb64b20dc456b9ead01a77e32dd69735ab1 Mon Sep 17 00:00:00 2001 From: philemon Date: Mon, 4 Aug 2014 22:07:47 +0400 Subject: [PATCH] New 'crypto.py' module for higher-level functions from 'util.py'. 'mmgen-keygen', 'mmgen-txsign': encrypted keylist support --- mmgen-addrgen | 16 +- mmgen-addrimport | 2 +- mmgen-passchg | 1 + mmgen-pywallet | 6 +- mmgen-txsign | 16 +- mmgen-walletchk | 1 + mmgen-walletgen | 1 + mmgen/Opts.py | 4 +- mmgen/crypto.py | 394 ++++++++++++++++++++++++ mmgen/license.py | 5 +- mmgen/term.py | 51 +++- mmgen/tool.py | 61 ++-- mmgen/tx.py | 4 +- mmgen/util.py | 760 ++++++++++++----------------------------------- 14 files changed, 689 insertions(+), 633 deletions(-) create mode 100755 mmgen/crypto.py diff --git a/mmgen-addrgen b/mmgen-addrgen index eeaf315c..e46a1863 100755 --- a/mmgen-addrgen +++ b/mmgen-addrgen @@ -19,7 +19,7 @@ """ mmgen-addrgen: Generate a list or range of addresses from a mmgen deterministic wallet. - Call as 'btc-keygen' to allow key generation as well. + Call as 'btc-keygen' to allow key generation. """ import sys @@ -28,6 +28,7 @@ import mmgen.config as g from mmgen.Opts import * from mmgen.license import * from mmgen.util import * +from mmgen.crypto import * from mmgen.addr import * from mmgen.tx import make_addr_data_chksum @@ -149,7 +150,7 @@ seed_id = make_chksum_8(seed) for l in ( ('flat_list', 'no_addresses'), ('flat_list', 'b16'), -): check_incompatible_opts(opts,l) +): warn_incompatible_opts(opts,l) opts['gen_what'] = \ ("addrs") if what == "addresses" else ( @@ -163,6 +164,17 @@ addr_data_str = format_addr_data( outfile_base = "{}[{}]".format(seed_id, fmt_addr_idxs(addr_idxs)) +if 'flat_list' in opts: + confirm = False if g.quiet else True + outfile = "%s.%s" % (outfile_base,g.keylist_ext) + if (user_confirm("Encrypt key list?")): + enc_data = mmgen_encrypt(addr_data_str,"",opts) + outfile += "."+g.mmenc_ext + write_to_file(outfile,enc_data,opts,"encrypted key list",confirm,True) + else: + write_to_file(outfile,addr_data_str,opts,"key list",confirm,True) + sys.exit() + # Output data: if 'stdout' in opts: confirm = True if (what == "keys" and not g.quiet) else False diff --git a/mmgen-addrimport b/mmgen-addrimport index 2165f502..d827789c 100755 --- a/mmgen-addrimport +++ b/mmgen-addrimport @@ -56,7 +56,7 @@ else: if 'addrlist' in opts: lines = get_lines_from_file(opts['addrlist'],"non-mmgen addresses", - remove_comments=True) + trim_comments=True) addr_data += [(None,l) for l in lines] from mmgen.bitcoin import verify_addr diff --git a/mmgen-passchg b/mmgen-passchg index 74d65826..ae3858a5 100755 --- a/mmgen-passchg +++ b/mmgen-passchg @@ -23,6 +23,7 @@ mmgen-passchg: Change a mmgen deterministic wallet's passphrase, label or import sys from mmgen.Opts import * from mmgen.util import * +from mmgen.crypto import * import mmgen.config as g help_data = { diff --git a/mmgen-pywallet b/mmgen-pywallet index 50ea3898..452940ed 100755 --- a/mmgen-pywallet +++ b/mmgen-pywallet @@ -86,8 +86,8 @@ help_data = { } opts,cmd_args = parse_opts(sys.argv,help_data) -from mmgen.Opts import check_incompatible_opts -check_incompatible_opts(opts,('json','keys','addrs','keysforaddrs')) +from mmgen.Opts import warn_incompatible_opts +warn_incompatible_opts(opts,('json','keys','addrs','keysforaddrs')) if len(cmd_args) == 1: from mmgen.util import check_infile @@ -1653,7 +1653,7 @@ elif 'addrs' in opts: elif 'keysforaddrs' in opts: from mmgen.util import get_lines_from_file - usr_addrs = set(get_lines_from_file(opts['keysforaddrs'],"addresses",remove_comments=True)) + usr_addrs = set(get_lines_from_file(opts['keysforaddrs'],"addresses",trim_comments=True)) data = [i['sec'] for i in json_db['keys'] if i['addr'] in usr_addrs] ext,what = "keys","private keys" if len(data) < len(usr_addrs): diff --git a/mmgen-txsign b/mmgen-txsign index 6e4cd4c8..55facf58 100755 --- a/mmgen-txsign +++ b/mmgen-txsign @@ -92,7 +92,7 @@ opts,infiles = parse_opts(sys.argv,help_data) for l in ( ('tx_id', 'info'), ('keys_from_file','all_keys_from_file') -): check_incompatible_opts(opts,l) +): warn_incompatible_opts(opts,l) if "quiet" in opts: g.quiet = True if 'from_incog_hex' in opts or 'from_incog_hidden' in opts: @@ -114,8 +114,18 @@ infiles = list(set(infiles) - set(tx_files) - set(addrfiles)) if not "info" in opts: do_license_msg(immed=True) if 'keys_from_file' in opts: - keys_from_file = get_lines_from_file(opts['keys_from_file'],"key data", - remove_comments=True) + from mmgen.crypto import mmgen_decrypt + fn = opts['keys_from_file'] + if get_extension(fn) == g.mmenc_ext: + enc_d = get_data_from_file(fn,"encrypted keylist") + dec_d = mmgen_decrypt(enc_d,"",opts) + if dec_d: + keys_from_file = remove_comments(dec_d.split("\n")) + else: + msg("Decryption of encrypted keylist failed") + sys.exit(2) + else: + keys_from_file = get_lines_from_file(fn,"key data",trim_comments=True) else: keys_from_file = [] for tx_file in tx_files: diff --git a/mmgen-walletchk b/mmgen-walletchk index 16b2ea31..637fe834 100755 --- a/mmgen-walletchk +++ b/mmgen-walletchk @@ -24,6 +24,7 @@ import sys import mmgen.config as g from mmgen.Opts import * from mmgen.util import * +from mmgen.crypto import get_seed_from_wallet,wallet_to_incog_data help_data = { 'prog_name': g.prog_name, diff --git a/mmgen-walletgen b/mmgen-walletgen index 09d90dbd..ddeeb01c 100755 --- a/mmgen-walletgen +++ b/mmgen-walletgen @@ -26,6 +26,7 @@ import mmgen.config as g from mmgen.Opts import * from mmgen.license import * from mmgen.util import * +from mmgen.crypto import * help_data = { 'prog_name': g.prog_name, diff --git a/mmgen/Opts.py b/mmgen/Opts.py index ab42bb22..7d57998e 100755 --- a/mmgen/Opts.py +++ b/mmgen/Opts.py @@ -29,7 +29,7 @@ def print_version_info(): Copyright (C) {g.Cdates} by {g.author} {g.email}. """.format(g=g).strip() -def check_incompatible_opts(opts,incompat_list): +def warn_incompatible_opts(opts,incompat_list): bad = [k for k in opts.keys() if k in incompat_list] if len(bad) > 1: msg("Mutually exclusive options: %s" % " ".join( @@ -55,7 +55,7 @@ def parse_opts(argv,help_data): ('export_incog','export_incog_hex','export_incog_hidden','export_mnemonic', 'export_seed'), ('quiet','verbose') - ): check_incompatible_opts(opts,l) + ): warn_incompatible_opts(opts,l) # check_opts() doesn't touch opts[] if not check_opts(opts,long_opts): sys.exit(1) diff --git a/mmgen/crypto.py b/mmgen/crypto.py new file mode 100755 index 00000000..0a258dfd --- /dev/null +++ b/mmgen/crypto.py @@ -0,0 +1,394 @@ +#!/usr/bin/env python +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C) 2013-2014 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 . +""" +crypto.py: Cryptographic and related routines for the mmgen-tool utility +""" + +import sys +from binascii import hexlify +from hashlib import sha256 + +import mmgen.config as g +from mmgen.util import * +from mmgen.term import get_char + +def encrypt_seed(seed, key): + return encrypt_data(seed, key, iv=1, what="seed") + + +def decrypt_seed(enc_seed, key, seed_id, key_id): + + 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 + + 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...") + 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 +# else: +# qmsg("Generated IDs (Seed/Key): %s/%s" % (chk2,chk1)) + + if g.debug: print "Decrypted seed: %s" % hexlify(dec_seed) + + return dec_seed + + +def encrypt_data(data, key, iv=1, what="data", verify=True): + """ + Encrypt arbitrary data using AES256 in counter mode + """ + + # 192-bit seed is 24 bytes -> not multiple of 16. Must use MODE_CTR + from Crypto.Cipher import AES + from Crypto.Util import Counter + + vmsg("Encrypting %s" % what) + + c = AES.new(key, AES.MODE_CTR, + counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv)) + enc_data = c.encrypt(data) + + if verify: + 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 %s doesn't match original %s" % (what,what)) + sys.exit(2) + + return enc_data + + +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 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 + return scrypt.hash(passwd, salt, 2**N, r, p, buflen=buflen) + + +def make_key(passwd, salt, hash_preset, what="key"): + + 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 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) + + 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",verify=False) + else: + return os_rand + + +def get_seed_from_wallet( + infile, + opts, + prompt="Enter {} wallet passphrase: ".format(g.proj_name), + silent=False + ): + + wdata = get_data_from_wallet(infile,silent=silent) + label,metadata,hash_preset,salt,enc_seed = wdata + + if g.verbose: display_control_data(*wdata) + + passwd = get_mmgen_passphrase(prompt,opts) + + key = make_key(passwd, salt, hash_preset) + + return decrypt_seed(enc_seed, key, metadata[0], metadata[1]) + + +def get_seed_from_incog_wallet( + infile, + opts, + prompt="Enter {} wallet passphrase: ".format(g.proj_name), + silent=False, + hex_input=False + ): + + what = "incognito wallet data" + + if "from_incog_hidden" in opts: + d = get_hidden_incog_data(opts) + else: + d = get_data_from_file(infile,what) + if hex_input: + try: + d = unhexlify("".join(d.split()).strip()) + except: + msg("Data in file '%s' is not in hexadecimal format" % infile) + sys.exit(2) + # 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:] + + msg("Incog ID: %s (IV ID: %s)" % (make_iv_chksum(iv),make_chksum_8(iv))) + qmsg("Check the applicable value against your records.") + vmsg(cmessages['incog_iv_id_hidden' if "from_incog_hidden" in opts + else 'incog_iv_id']) + + passwd = get_mmgen_passphrase(prompt,opts) + + qmsg("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, g.hash_preset)) + if not hp: + hp = g.hash_preset; break + elif hp in g.hash_presets: + break + msg("%s: Invalid hash preset" % hp) + + # IV is used BOTH to initialize counter and to salt password! + key = make_key(passwd, iv, hp, "wrapper key") + 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:] + + 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['incog_key_id_hidden' if "from_incog_hidden" in opts + else 'incog_key_id']) + + return seed + + +def wallet_to_incog_data(infile,opts): + + d = get_data_from_wallet(infile,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) + + iv = get_random(g.aesctr_iv_len,opts) + iv_id = make_iv_chksum(iv) + msg("Incog ID: %s" % iv_id) + + # IV is used BOTH to initialize counter and to salt password! + key = make_key(passwd, iv, preset, "wrapper key") + 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 + + +def get_seed(infile,opts,silent=False): + + ext = get_extension(infile) + + if ext == g.mn_ext: source = "mnemonic" + elif ext == g.brain_ext: source = "brainwallet" + elif ext == g.seed_ext: source = "seed" + elif ext == g.wallet_ext: source = "wallet" + elif ext == g.incog_ext: source = "incognito wallet" + elif ext == g.incog_hex_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_incog' in opts: source = "incognito wallet" + else: + if infile: msg( + "Invalid file extension for file: %s\nValid extensions: '.%s'" % + (infile, "', '.".join(g.seedfile_exts))) + else: msg("No seed source type specified and no file supplied") + sys.exit(2) + + if source == "mnemonic": + prompt = "Enter mnemonic: " + words = get_words(infile,"mnemonic data",prompt,opts) + wl = get_default_wordlist() + from mmgen.mnemonic import get_seed_from_mnemonic + seed = get_seed_from_mnemonic(words,wl) + elif source == "brainwallet": + if 'from_brain' not in opts: + msg("'--from-brain' parameters must be specified for brainwallet file") + sys.exit(2) + prompt = "Enter brainwallet passphrase: " + words = get_words(infile,"brainwallet data",prompt,opts) + seed = _get_seed_from_brain_passphrase(words,opts) + elif source == "seed": + prompt = "Enter seed in %s format: " % g.seed_ext + words = get_words(infile,"seed data",prompt,opts) + seed = get_seed_from_seed_data(words) + elif source == "wallet": + seed = get_seed_from_wallet(infile, opts, silent=silent) + elif source == "incognito wallet": + h = True if ext == g.incog_hex_ext or 'from_incog_hex' in opts else False + seed = get_seed_from_incog_wallet(infile, opts, silent=silent, hex_input=h) + + + 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 +def get_seed_retry(infile,opts): + silent = False + while True: + seed = get_seed(infile,opts,silent=silent) + silent = True + if seed: return seed + + +def _get_seed_from_brain_passphrase(words,opts): + bp = " ".join(words) + if g.debug: print "Sanitized brain passphrase: %s" % bp + seed_len,hash_preset = get_from_brain_opt_params(opts) + if g.debug: print "Brainwallet l = %s, p = %s" % (seed_len,hash_preset) + vmsg_r("Hashing brainwallet data. Please wait...") + # Use buflen arg of scrypt.hash() to get seed of desired length + seed = scrypt_hash_passphrase(bp, "", hash_preset, buflen=seed_len/8) + vmsg("Done") + return seed + + +# Vars for mmgen_*crypt functions only +salt_len,sha256_len,nonce_len = 32,32,32 + +def mmgen_encrypt(data,hash_preset,opts): + 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) + enc_d = encrypt_data(sha256(nonce+data).digest() + nonce + data, key, + int(hexlify(iv),16)) + return salt+iv+enc_d + + +def mmgen_decrypt(data,hash_preset,opts): + dstart = salt_len + g.aesctr_iv_len + salt,iv,enc_d = data[:salt_len],data[salt_len:dstart],data[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(hexlify(iv),16)) + if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest(): + return dec_d[sha256_len+nonce_len:] + else: + msg("Incorrect passphrase or hash preset") + return False diff --git a/mmgen/license.py b/mmgen/license.py index 5a9e1537..76cd0313 100755 --- a/mmgen/license.py +++ b/mmgen/license.py @@ -20,7 +20,8 @@ license.py: Show the license """ import sys -from mmgen.util import msg, msg_r, get_char +from mmgen.util import msg, msg_r +from mmgen.term import get_char import mmgen.config as g gpl = { @@ -595,7 +596,7 @@ def do_license_msg(immed=False): while True: reply = get_char(prompt, immed_chars="wc" if immed else "") if reply == 'w': - from mmgen.util import do_pager + from mmgen.term import do_pager do_pager(gpl['conditions']) elif reply == 'c': msg(""); break diff --git a/mmgen/term.py b/mmgen/term.py index c9cf24e8..9a3116f2 100755 --- a/mmgen/term.py +++ b/mmgen/term.py @@ -20,9 +20,7 @@ term.py: Terminal-handling routines for the mmgen suite """ import sys, os, struct - -def msg(s): sys.stderr.write(s + "\n") -def msg_r(s): sys.stderr.write(s) +from mmgen.util import msg, msg_r def _kb_hold_protect_unix(): @@ -122,7 +120,6 @@ def _get_terminal_size_linux(): def ioctl_GWINSZ(fd): try: import fcntl - import termios cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) return cr except: @@ -165,18 +162,23 @@ def _get_terminal_size_mswin(): except: return 80,25 +def mswin_dummy_flush(fd,termconst): pass + try: import tty, termios from select import select get_char = _get_keypress_unix kb_hold_protect = _kb_hold_protect_unix get_terminal_size = _get_terminal_size_linux + myflush = termios.tcflush +# call: myflush(sys.stdin, termios.TCIOFLUSH) except: try: import msvcrt, time get_char = _get_keypress_mswin kb_hold_protect = _kb_hold_protect_mswin get_terminal_size = _get_terminal_size_mswin + myflush = mswin_dummy_flush except: if not sys.platform.startswith("linux") \ and not sys.platform.startswith("win"): @@ -186,5 +188,42 @@ except: msg("Unable to set terminal mode") sys.exit(2) -if __name__ == "__main__": - print "columns: {}, rows: {}".format(*get_terminal_size()) + +def do_pager(text): + + pagers = ["less","more"] + shell = False + + from os import environ + +# Hack for MS Windows command line (i.e. non CygWin) environment +# When 'shell' is true, Windows aborts the calling program if executable +# not found. +# When 'shell' is false, an exception is raised, invoking the fallback +# 'print' instead of the pager. +# We risk assuming that "more" will always be available on a stock +# Windows installation. + if sys.platform.startswith("win") and 'HOME' not in environ: + shell = True + pagers = ["more"] + + if 'PAGER' in environ and environ['PAGER'] != pagers[0]: + pagers = [environ['PAGER']] + pagers + + for pager in pagers: + end = "" if pager == "less" else "\n(end of text)\n" + try: + from subprocess import Popen, PIPE, STDOUT + p = Popen([pager], stdin=PIPE, shell=shell) + except: pass + else: + try: + p.communicate(text+end+"\n") + except KeyboardInterrupt: + # Has no effect. Why? + if pager != "less": + msg("\n(User interrupt)\n") + finally: + msg_r("\r") + break + else: print text+end diff --git a/mmgen/tool.py b/mmgen/tool.py index 5c0f242e..13cb802f 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -24,6 +24,7 @@ import mmgen.bitcoin as bitcoin import binascii as ba import mmgen.config as g +from mmgen.crypto import * from mmgen.util import * from mmgen.tx import * @@ -55,7 +56,8 @@ commands = { "mn_printlist": ['wordlist [str="electrum"]'], "id8": [' [str]'], "id6": [' [str]'], - "listaddresses": ['minconf [int=1]', 'showempty [bool=False]'], + "str2id6": [' [str]'], + "listaddresses":['minconf [int=1]', 'showempty [bool=False]'], "getbalance": ['minconf [int=1]'], "viewtx": [' [str]'], "check_addrfile": [' [str]'], @@ -119,8 +121,9 @@ command_help = """ {pnm}-specific operations: check_addrfile - compute checksum and address list for {pnm} address file find_incog_data - Use an Incog ID to find hidden incognito wallet data - id6 - generate 6-character {pnm} ID checksum for file (or stdin) - id8 - generate 8-character {pnm} ID checksum for file (or stdin) + id6 - generate 6-character {pnm} ID for a file (or stdin) + id8 - generate 8-character {pnm} ID for a file (or stdin) + str2id6 - generate 6-character {pnm} ID for a string, ignoring spaces Mnemonic operations (choose "electrum" (default), "tirosh" or "all" wordlists): @@ -301,6 +304,7 @@ def mn_printlist(wordlist="electrum"): def id8(infile): print make_chksum_8(get_data_from_file(infile,dash=True)) def id6(infile): print make_chksum_6(get_data_from_file(infile,dash=True)) +def str2id6(s): print make_chksum_6("".join(s.split())) # List MMGen addresses and their balances: def listaddresses(minconf=1,showempty=False): @@ -410,48 +414,31 @@ 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) + data = get_data_from_file(infile,"data for encryption") + enc_d = mmgen_encrypt(data,hash_preset,opts) + if outfile == '-': + write_to_stdout(enc_d,"encrypted data",confirm=True) else: if not outfile: outfile = os.path.basename(infile) + "." + g.mmenc_ext - write_to_file(outfile, salt+iv+enc_d, opts,"encrypted data",True,True) + write_to_file(outfile, enc_d, opts,"encrypted data",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: - if not outfile: - outfile = os.path.basename(infile) - if outfile[-len(g.mmenc_ext)-1:] == "."+g.mmenc_ext: - outfile = outfile[:-len(g.mmenc_ext)-1] - else: - outfile = outfile + ".dec" - write_to_file(outfile, out, opts,"decrypted data",True,True) + enc_d = get_data_from_file(infile,"encrypted data") + dec_d = mmgen_decrypt(enc_d,hash_preset,opts) + if outfile == '-': + write_to_stdout(dec_d,"decrypted data",confirm=True) else: - msg("Incorrect passphrase or hash preset") + if not outfile: + outfile = os.path.basename(infile) + if outfile[-len(g.mmenc_ext)-1:] == "."+g.mmenc_ext: + outfile = outfile[:-len(g.mmenc_ext)-1] + else: + outfile = outfile + ".dec" + write_to_file(outfile, dec_d, opts,"decrypted data",True,True) def find_incog_data(filename,iv_id,keep_searching=False): diff --git a/mmgen/tx.py b/mmgen/tx.py index ed7ca856..eb76b930 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -25,6 +25,8 @@ from decimal import Decimal import mmgen.config as g from mmgen.util import * +from mmgen.crypto import get_seed_retry +from mmgen.term import do_pager,get_char txmsg = { 'not_enough_btc': "Not enough BTC in the inputs for this transaction (%s BTC)", @@ -538,7 +540,7 @@ def check_addr_data_hash(seed_id,addr_data): def parse_addrs_file(f): - lines = get_lines_from_file(f,"address data",remove_comments=True) + lines = get_lines_from_file(f,"address data",trim_comments=True) try: seed_id,obrace = lines[0].split() diff --git a/mmgen/util.py b/mmgen/util.py index 0b0f13c5..a9a992fa 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ -util.py: Shared routines for the mmgen suite +util.py: Low-level routines imported by other modules for the MMGen suite """ import sys @@ -24,8 +24,6 @@ from hashlib import sha256 from binascii import hexlify,unhexlify import mmgen.config as g -from mmgen.bitcoin import b58decode_pad,b58encode_pad -from mmgen.term import * def msg(s): sys.stderr.write(s + "\n") def msg_r(s): sys.stderr.write(s) @@ -42,97 +40,6 @@ def vmsg(s): def vmsg_r(s): if g.verbose: sys.stderr.write(s) -def bail(): sys.exit(9) - -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) - - 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",verify=False) - else: - return os_rand - -def my_raw_input(prompt,echo=True): - try: - if echo: - reply = raw_input(prompt) - else: - from getpass import getpass - reply = getpass(prompt) - except KeyboardInterrupt: - msg("\nUser interrupt") - sys.exit(1) - - kb_hold_protect() - return reply - - -def _get_hash_params(hash_preset): - if hash_preset in g.hash_presets: - return g.hash_presets[hash_preset] # N,p,r,buflen - else: # Shouldn't be here - msg("%s: invalid 'hash_preset' value" % hash_preset) - sys.exit(3) - - -def show_hash_presets(): - fs = " {:<7} {:<6} {:<3} {}" - msg("Available parameters for scrypt.hash():") - msg(fs.format("Preset","N","r","p")) - for i in sorted(g.hash_presets.keys()): - msg(fs.format("'%s'" % i, *g.hash_presets[i])) - msg("N = memory usage (power of two), p = iterations (rounds)") - sys.exit(0) - - cmessages = { 'null': "", 'incog_iv_id': """ @@ -189,71 +96,113 @@ just hit ENTER twice. """.strip() } - -def confirm_or_exit(message, question, expect="YES"): - - vmsg("") - - m = message.strip() - if m: msg(m) - - conf_msg = "Type uppercase '%s' to confirm: " % expect - - p = question+" "+conf_msg if question[0].isupper() else \ - "Are you sure you want to %s?\n%s" % (question,conf_msg) - - if my_raw_input(p).strip() != expect: - msg("Exiting at user request") - sys.exit(2) - - vmsg("") - - -def user_confirm(prompt,default_yes=False,verbose=False): - - q = "(Y/n)" if default_yes else "(y/N)" - - while True: - reply = get_char("%s %s: " % (prompt, q)).strip("\n\r") - - if not reply: - if default_yes: msg(""); return True - else: msg(""); return False - elif reply in 'yY': msg(""); return True - elif reply in 'nN': msg(""); return False - else: - if verbose: msg("\nInvalid reply") - else: msg_r("\r") - - -def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False): - - while True: - reply = get_char("%s: " % prompt).strip("\n\r") - - if reply in chars or (enter_ok and not reply): - msg("") - return reply - - if verbose: msg("\nInvalid reply") - else: msg_r("\r") - +def get_extension(f): + import os + return os.path.splitext(f)[1][1:] def make_chksum_N(s,n,sep=False): if n%4 or not (4 <= n <= 64): return False s = sha256(sha256(s).digest()).hexdigest().upper() sep = " " if sep else "" return sep.join([s[i*4:i*4+4] for i in range(n/4)]) - def make_chksum_8(s,sep=False): s = sha256(sha256(s).digest()).hexdigest()[:8].upper() return "{} {}".format(s[:4],s[4:]) if sep else s +def make_chksum_6(s): return sha256(s).hexdigest()[:6] +def make_iv_chksum(s): return sha256(s).hexdigest()[:8].upper() -def make_chksum_6(s): - return sha256(s).hexdigest()[:6] +def splitN(s,n,sep=None): # always return an n-element list + ret = s.split(sep,n-1) + return ret + ["" for i in range(n-len(ret))] +def split2(s,sep=None): return splitN(s,2,sep) # always return a 2-element list +def split3(s,sep=None): return splitN(s,3,sep) # always return a 3-element list -def make_iv_chksum(s): - return sha256(s).hexdigest()[:8].upper() +def col4(s): + nondiv = 1 if len(s) % 4 else 0 + return " ".join([s[4*i:4*i+4] for i in range(len(s)/4 + nondiv)]) + +def make_timestamp(): + import time + tv = time.gmtime(time.time())[:6] + return "{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}".format(*tv) +def make_timestr(): + import time + tv = time.gmtime(time.time())[:6] + return "{:04d}/{:02d}/{:02d} {:02d}:{:02d}:{:02d}".format(*tv) +def secs_to_hms(secs): + return "{:02d}:{:02d}:{:02d}".format(secs/3600, (secs/60) % 60, secs % 60) + +def _is_hex(s): + try: int(s,16) + except: return False + else: return True + +def match_ext(addr,ext): + return addr.split(".")[-1] == ext + +def get_from_brain_opt_params(opts): + l,p = opts['from_brain'].split(",") + return(int(l),p) + +def pretty_hexdump(data,gw=2,cols=8,line_nums=False): + r = 1 if len(data) % gw else 0 + return "".join( + [ + ("" if (line_nums == False or i % cols) else "%03i: " % (i/cols)) + + hexlify(data[i*gw:i*gw+gw]) + + (" " if (i+1) % cols else "\n") + for i in range(len(data)/gw + r) + ] + ).rstrip() + +def decode_pretty_hexdump(data): + import re + lines = [re.sub('^\d+:\s+','',l) for l in data.split("\n")] + return unhexlify("".join(("".join(lines).split()))) + +def get_hash_params(hash_preset): + if hash_preset in g.hash_presets: + return g.hash_presets[hash_preset] # N,p,r,buflen + else: # Shouldn't be here + msg("%s: invalid 'hash_preset' value" % hash_preset) + sys.exit(3) + +def show_hash_presets(): + fs = " {:<7} {:<6} {:<3} {}" + msg("Available parameters for scrypt.hash():") + msg(fs.format("Preset","N","r","p")) + for i in sorted(g.hash_presets.keys()): + msg(fs.format("'%s'" % i, *g.hash_presets[i])) + msg("N = memory usage (power of two), p = iterations (rounds)") + sys.exit(0) + +def compare_checksums(chksum1, desc1, chksum2, desc2): + + if chksum1.lower() == chksum2.lower(): + vmsg("OK (%s)" % chksum1.upper()) + return True + else: + if g.debug: + print \ + "ERROR!\nComputed checksum %s (%s) doesn't match checksum %s (%s)" \ + % (desc1,chksum1,desc2,chksum2) + return False + +def get_default_wordlist(): + + wl_id = g.default_wl + if wl_id == "electrum": from mmgen.mn_electrum import electrum_words as wl + elif wl_id == "tirosh": from mmgen.mn_tirosh import tirosh_words as wl + return wl.strip().split("\n") + +def open_file_or_exit(filename,mode): + try: + f = open(filename, mode) + except: + what = "reading" if mode == 'r' else "writing" + msg("Unable to open file '%s' for %s" % (filename,what)) + sys.exit(2) + return f def check_file_type_and_access(fname,ftype): @@ -287,6 +236,7 @@ def check_infile(f): return check_file_type_and_access(f,"input file") def check_outfile(f): return check_file_type_and_access(f,"output file") def check_outdir(f): return check_file_type_and_access(f,"directory") + def _validate_addr_num(n): try: n = int(n) @@ -301,6 +251,12 @@ def _validate_addr_num(n): return n +def make_full_path(outdir,outfile): + import os + return os.path.normpath(os.sep.join([outdir, os.path.basename(outfile)])) +# os.path.join() doesn't work? + + def parse_address_list(arg,sep=","): ret = [] @@ -354,65 +310,23 @@ def get_new_passphrase(what, opts): return pw -def _scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32): +def confirm_or_exit(message, question, expect="YES"): - # Buflen arg is for brainwallets only, which use this function to generate - # the seed directly. + vmsg("") - N,r,p = _get_hash_params(hash_preset) + m = message.strip() + if m: msg(m) - import scrypt - return scrypt.hash(passwd, salt, 2**N, r, p, buflen=buflen) + conf_msg = "Type uppercase '%s' to confirm: " % expect + p = question+" "+conf_msg if question[0].isupper() else \ + "Are you sure you want to %s?\n%s" % (question,conf_msg) -def get_from_brain_opt_params(opts): - l,p = opts['from_brain'].split(",") - return(int(l),p) + if my_raw_input(p).strip() != expect: + msg("Exiting at user request") + sys.exit(2) - -def _get_seed_from_brain_passphrase(words,opts): - bp = " ".join(words) - if g.debug: print "Sanitized brain passphrase: %s" % bp - seed_len,hash_preset = get_from_brain_opt_params(opts) - if g.debug: print "Brainwallet l = %s, p = %s" % (seed_len,hash_preset) - vmsg_r("Hashing brainwallet data. Please wait...") - # Use buflen arg of scrypt.hash() to get seed of desired length - seed = _scrypt_hash_passphrase(bp, "", hash_preset, buflen=seed_len/8) - vmsg("Done") - return seed - - -def encrypt_seed(seed, key): - return encrypt_data(seed, key, iv=1, what="seed") - -def encrypt_data(data, key, iv=1, what="data", verify=True): - """ - Encrypt arbitrary data using AES256 in counter mode - """ - - # 192-bit seed is 24 bytes -> not multiple of 16. Must use MODE_CTR - from Crypto.Cipher import AES - from Crypto.Util import Counter - - vmsg("Encrypting %s" % what) - - c = AES.new(key, AES.MODE_CTR, - counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv)) - enc_data = c.encrypt(data) - - if verify: - 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 %s doesn't match original %s" % (what,what)) - sys.exit(2) - - return enc_data + vmsg("") def write_to_stdout(data, what, confirm=True): @@ -422,36 +336,12 @@ def write_to_stdout(data, what, confirm=True): try: import os of = os.readlink("/proc/%d/fd/1" % os.getpid()) - msg("Redirecting output to file '%s'" % of) + msg("Redirecting output to file '%s'" % os.path.relpath(of)) except: msg("Redirecting output to file") sys.stdout.write(data) -def get_default_wordlist(): - - wl_id = g.default_wl - if wl_id == "electrum": from mmgen.mn_electrum import electrum_words as wl - elif wl_id == "tirosh": from mmgen.mn_tirosh import tirosh_words as wl - return wl.strip().split("\n") - - -def open_file_or_exit(filename,mode): - try: - f = open(filename, mode) - except: - what = "reading" if mode == 'r' else "writing" - msg("Unable to open file '%s' for %s" % (filename,what)) - sys.exit(2) - return f - - -def make_full_path(outdir,outfile): - import os - return os.path.normpath(os.sep.join([outdir, os.path.basename(outfile)])) -# os.path.join() doesn't work? - - def write_to_file(outfile,data,opts,what="data",confirm=False,verbose=False): if 'outdir' in opts: outfile = make_full_path(opts['outdir'],outfile) @@ -487,11 +377,12 @@ def export_to_file(outfile, data, opts, what="data"): write_to_file(outfile,data,opts,what,c,True) -def _display_control_data(label,metadata,hash_preset,salt,enc_seed): +from mmgen.bitcoin import b58decode_pad,b58encode_pad + +def display_control_data(label,metadata,hash_preset,salt,enc_seed): msg("WALLET DATA") 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].upper()), @@ -499,7 +390,7 @@ def _display_control_data(label,metadata,hash_preset,salt,enc_seed): ("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)]))), + " ".join([str(i) for i in get_hash_params(hash_preset)]))), ("Passphrase empty?", pw_empty.capitalize()), ("Timestamp:", "%s UTC" % metadata[4]), ): msg(fs.format(*i)) @@ -515,31 +406,6 @@ def _display_control_data(label,metadata,hash_preset,salt,enc_seed): ): msg(fs.format(*i)) -def splitN(s,n,sep=None): # always return an n-element list - ret = s.split(sep,n-1) - return ret + ["" for i in range(n-len(ret))] - -def split2(s,sep=None): return splitN(s,2,sep) # always return a 2-element list -def split3(s,sep=None): return splitN(s,3,sep) # always return a 3-element list - -def col4(s): - nondiv = 1 if len(s) % 4 else 0 - return " ".join([s[4*i:4*i+4] for i in range(len(s)/4 + nondiv)]) - -def make_timestamp(): - import time - tv = time.gmtime(time.time())[:6] - return "{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}".format(*tv) - -def make_timestr(): - import time - tv = time.gmtime(time.time())[:6] - return "{:04d}/{:02d}/{:02d} {:02d}:{:02d}:{:02d}".format(*tv) - -def secs_to_hms(secs): - return "{:02d}:{:02d}:{:02d}".format(secs/3600, (secs/60) % 60, secs % 60) - - def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts): seed_id = make_chksum_8(seed) @@ -555,7 +421,7 @@ def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts): lines = ( label, "{} {} {} {} {}".format(*metadata), - "{}: {} {} {}".format(hash_preset,*_get_hash_params(hash_preset)), + "{}: {} {} {}".format(hash_preset,*get_hash_params(hash_preset)), "{} {}".format(make_chksum_6(sf), col4(sf)), "{} {}".format(make_chksum_6(esf), col4(esf)) ) @@ -569,29 +435,9 @@ def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts): write_to_file(outfile,d,opts,"wallet",c,True) if g.verbose: - _display_control_data(label,metadata,hash_preset,salt,enc_seed) + display_control_data(label,metadata,hash_preset,salt,enc_seed) -def _compare_checksums(chksum1, desc1, chksum2, desc2): - - if chksum1.lower() == chksum2.lower(): - vmsg("OK (%s)" % chksum1.upper()) - return True - else: - if g.debug: - print \ - "ERROR!\nComputed checksum %s (%s) doesn't match checksum %s (%s)" \ - % (desc1,chksum1,desc2,chksum2) - return False - -def _is_hex(s): - try: int(s,16) - except: return False - else: return True - -def match_ext(addr,ext): - return addr.split(".")[-1] == ext - def _check_mmseed_format(words): valid = False @@ -664,7 +510,7 @@ def get_data_from_wallet(infile,silent=False): hash_preset = hd[0][:-1] hash_params = [int(i) for i in hd[1:]] - if hash_params != _get_hash_params(hash_preset): + if hash_params != get_hash_params(hash_preset): msg("Hash parameters '%s' don't match hash preset '%s'" % (" ".join(hash_params), hash_preset)) sys.exit(9) @@ -702,22 +548,29 @@ def _get_words_from_file(infile,what): return words -def get_lines_from_file(infile,what="",remove_comments=False): +def get_words(infile,what,prompt,opts): + if infile: + return _get_words_from_file(infile,what) + else: + return _get_words_from_user(prompt,opts) + +def remove_comments(lines): + import re + # re.sub(pattern, repl, string, count=0, flags=0) + ret = [] + for i in lines: + i = re.sub('#.*','',i,1) + i = re.sub('\s+$','',i) + if i: ret.append(i) + return ret + +def get_lines_from_file(infile,what="",trim_comments=False): if what != "": qmsg("Getting %s from file '%s'" % (what,infile)) f = open_file_or_exit(infile,'r') - lines = f.read().splitlines(); f.close() - if remove_comments: - import re - # re.sub(pattern, repl, string, count=0, flags=0) - ret = [] - for i in lines: - i = re.sub('#.*','',i,1) - i = re.sub('\s+$','',i) - if i: ret.append(i) - return ret - else: - return lines + lines = f.read().splitlines() + f.close() + return remove_comments(lines) if trim_comments else lines def get_data_from_file(infile,what="data",dash=False): @@ -729,7 +582,7 @@ def get_data_from_file(infile,what="data",dash=False): return data -def _get_seed_from_seed_data(words): +def get_seed_from_seed_data(words): if not _check_mmseed_format(words): msg("Invalid %s data" % g.seed_ext) @@ -741,7 +594,7 @@ def _get_seed_from_seed_data(words): chk = make_chksum_6(seed_b58) vmsg_r("Validating %s checksum..." % g.seed_ext) - if _compare_checksums(chk, "from seed", stored_chk, "from input"): + if compare_checksums(chk, "from seed", stored_chk, "from input"): seed = b58decode_pad(seed_b58) if seed == False: msg("Invalid b58 number: %s" % val) @@ -782,25 +635,6 @@ def get_bitcoind_passphrase(prompt,opts): echo=True if 'echo_passphrase' in opts else False) -def get_seed_from_wallet( - infile, - opts, - prompt="Enter {} wallet passphrase: ".format(g.proj_name), - silent=False - ): - - wdata = get_data_from_wallet(infile,silent=silent) - label,metadata,hash_preset,salt,enc_seed = wdata - - if g.verbose: _display_control_data(*wdata) - - passwd = get_mmgen_passphrase(prompt,opts) - - key = make_key(passwd, salt, hash_preset) - - return decrypt_seed(enc_seed, key, metadata[0], metadata[1]) - - def check_data_fits_file_at_offset(fname,offset,dlen,action): # TODO: Check for Windows import os, stat @@ -835,236 +669,6 @@ def get_hidden_incog_data(opts): "Data read from file") return data -def get_seed_from_incog_wallet( - infile, - opts, - prompt="Enter {} wallet passphrase: ".format(g.proj_name), - silent=False, - hex_input=False - ): - - what = "incognito wallet data" - - if "from_incog_hidden" in opts: - d = get_hidden_incog_data(opts) - else: - d = get_data_from_file(infile,what) - if hex_input: - try: - d = unhexlify("".join(d.split()).strip()) - except: - msg("Data in file '%s' is not in hexadecimal format" % infile) - sys.exit(2) - # 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:] - - msg("Incog ID: %s (IV ID: %s)" % (make_iv_chksum(iv),make_chksum_8(iv))) - qmsg("Check the applicable value against your records.") - vmsg(cmessages['incog_iv_id_hidden' if "from_incog_hidden" in opts - else 'incog_iv_id']) - - passwd = get_mmgen_passphrase(prompt,opts) - - qmsg("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, g.hash_preset)) - if not hp: - hp = g.hash_preset; break - elif hp in g.hash_presets: - break - msg("%s: Invalid hash preset" % hp) - - # IV is used BOTH to initialize counter and to salt password! - key = make_key(passwd, iv, hp, "wrapper key") - 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:] - - 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['incog_key_id_hidden' if "from_incog_hidden" in opts - else 'incog_key_id']) - - return seed - - -def make_key(passwd, salt, hash_preset, what="key"): - - 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): - - 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 - - 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...") - 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 -# else: -# qmsg("Generated IDs (Seed/Key): %s/%s" % (chk2,chk1)) - - if g.debug: print "Decrypted seed: %s" % hexlify(dec_seed) - - 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) - else: - return _get_words_from_user(prompt,opts) - - -def get_seed(infile,opts,silent=False): - - ext = get_extension(infile) - - if ext == g.mn_ext: source = "mnemonic" - elif ext == g.brain_ext: source = "brainwallet" - elif ext == g.seed_ext: source = "seed" - elif ext == g.wallet_ext: source = "wallet" - elif ext == g.incog_ext: source = "incognito wallet" - elif ext == g.incog_hex_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_incog' in opts: source = "incognito wallet" - else: - if infile: msg( - "Invalid file extension for file: %s\nValid extensions: '.%s'" % - (infile, "', '.".join(g.seedfile_exts))) - else: msg("No seed source type specified and no file supplied") - sys.exit(2) - - if source == "mnemonic": - prompt = "Enter mnemonic: " - words = _get_words(infile,"mnemonic data",prompt,opts) - wl = get_default_wordlist() - from mmgen.mnemonic import get_seed_from_mnemonic - seed = get_seed_from_mnemonic(words,wl) - elif source == "brainwallet": - if 'from_brain' not in opts: - msg("'--from-brain' parameters must be specified for brainwallet file") - sys.exit(2) - prompt = "Enter brainwallet passphrase: " - words = _get_words(infile,"brainwallet data",prompt,opts) - seed = _get_seed_from_brain_passphrase(words,opts) - elif source == "seed": - prompt = "Enter seed in %s format: " % g.seed_ext - words = _get_words(infile,"seed data",prompt,opts) - seed = _get_seed_from_seed_data(words) - elif source == "wallet": - seed = get_seed_from_wallet(infile, opts, silent=silent) - elif source == "incognito wallet": - h = True if ext == g.incog_hex_ext or 'from_incog_hex' in opts else False - seed = get_seed_from_incog_wallet(infile, opts, silent=silent, hex_input=h) - - - 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 -def get_seed_retry(infile,opts): - silent = False - while True: - seed = get_seed(infile,opts,silent=silent) - silent = True - if seed: return seed - - -def do_pager(text): - - pagers = ["less","more"] - shell = False - - from os import environ - -# Hack for MS Windows command line (i.e. non CygWin) environment -# When 'shell' is true, Windows aborts the calling program if executable -# not found. -# When 'shell' is false, an exception is raised, invoking the fallback -# 'print' instead of the pager. -# We risk assuming that "more" will always be available on a stock -# Windows installation. - if sys.platform.startswith("win") and 'HOME' not in environ: - shell = True - pagers = ["more"] - - if 'PAGER' in environ and environ['PAGER'] != pagers[0]: - pagers = [environ['PAGER']] + pagers - - for pager in pagers: - end = "" if pager == "less" else "\n(end of text)\n" - try: - from subprocess import Popen, PIPE, STDOUT - p = Popen([pager], stdin=PIPE, shell=shell) - except: pass - else: - try: - p.communicate(text+end+"\n") - except KeyboardInterrupt: - # Has no effect. Why? - if pager != "less": - msg("\n(User interrupt)\n") - finally: - msg_r("\r") - break - else: print text+end - def export_to_hidden_incog(incog_enc,opts): outfile,offset = opts['export_incog_hidden'].split(",") #Already sanity-checked @@ -1081,46 +685,50 @@ def export_to_hidden_incog(incog_enc,opts): (os.path.relpath(outfile),offset)) -def pretty_hexdump(data,gw=2,cols=8,line_nums=False): - r = 1 if len(data) % gw else 0 - return "".join( - [ - ("" if (line_nums == False or i % cols) else "%03i: " % (i/cols)) + - hexlify(data[i*gw:i*gw+gw]) + - (" " if (i+1) % cols else "\n") - for i in range(len(data)/gw + r) - ] - ).rstrip() +from mmgen.term import kb_hold_protect,get_char -def decode_pretty_hexdump(data): - import re - lines = [re.sub('^\d+:\s+','',l) for l in data.split("\n")] - return unhexlify("".join(("".join(lines).split()))) +def my_raw_input(prompt,echo=True): + try: + if echo: + reply = raw_input(prompt) + else: + from getpass import getpass + reply = getpass(prompt) + except KeyboardInterrupt: + msg("\nUser interrupt") + sys.exit(1) + + kb_hold_protect() + return reply -def wallet_to_incog_data(infile,opts): +def user_confirm(prompt,default_yes=False,verbose=False): - d = get_data_from_wallet(infile,silent=True) - seed_id,key_id,preset,salt,enc_seed = \ - d[1][0], d[1][1], d[2].split(":")[0], d[3], d[4] + q = "(Y/n)" if default_yes else "(y/N)" - 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) + while True: + reply = get_char("%s %s: " % (prompt, q)).strip("\n\r") - iv = get_random(g.aesctr_iv_len,opts) - iv_id = make_iv_chksum(iv) - msg("Incog ID: %s" % iv_id) - - # IV is used BOTH to initialize counter and to salt password! - key = make_key(passwd, iv, preset, "wrapper key") - 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 + if not reply: + if default_yes: msg(""); return True + else: msg(""); return False + elif reply in 'yY': msg(""); return True + elif reply in 'nN': msg(""); return False + else: + if verbose: msg("\nInvalid reply") + else: msg_r("\r") + + +def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False): + + while True: + reply = get_char("%s: " % prompt).strip("\n\r") + + if reply in chars or (enter_ok and not reply): + msg("") + return reply + + if verbose: msg("\nInvalid reply") + else: msg_r("\r") -if __name__ == "__main__": - print "util.py"