From e328a6a24bb9a5d05ae848ca1007dcb09f12c14b Mon Sep 17 00:00:00 2001 From: philemon Date: Fri, 1 Aug 2014 23:08:31 +0400 Subject: [PATCH] Added features: * 'mmgen-tool': file encryption utility with strong encryption * 'mmgen-tool': find hidden incognito data in file using the Incog ID * User may now supply additional entropy in all cases where random data is needed. This user entropy (typed symbols + keystroke intervals) is hashed into a key with Scrypt and used to encrypt all random data produced during the session by the OS. --- MANIFEST | 1 + mmgen-addrgen | 3 +- mmgen-addrimport | 8 +-- mmgen-tool | 2 +- mmgen/Opts.py | 23 +++--- mmgen/tool.py | 180 +++++++++++++++++++++++++++++++++++++---------- mmgen/tx.py | 15 ++-- mmgen/util.py | 58 ++++++++------- setup.py | 2 +- 9 files changed, 204 insertions(+), 88 deletions(-) diff --git a/MANIFEST b/MANIFEST index 56a9426d..7f3bf5f4 100644 --- a/MANIFEST +++ b/MANIFEST @@ -2,6 +2,7 @@ __init__.py mmgen-addrgen mmgen-addrimport +mmgen-keygen mmgen-passchg mmgen-pywallet mmgen-tool diff --git a/mmgen-addrgen b/mmgen-addrgen index ab4e0198..8d4176b0 100755 --- a/mmgen-addrgen +++ b/mmgen-addrgen @@ -182,4 +182,5 @@ if not 'no_addresses' in opts: a = "address data checksum" write_to_file(outfile_base+".chk",addr_data_chksum,opts,a,confirm,True) else: - qmsg("Save this information to a secure location") + qmsg("This checksum will be used to verify the address file in the future.") + qmsg("Record it to a safe location.") diff --git a/mmgen-addrimport b/mmgen-addrimport index bebd9cf0..2165f502 100755 --- a/mmgen-addrimport +++ b/mmgen-addrimport @@ -83,6 +83,7 @@ and has a balance, you must exit the program now and rerun it using the '--rescan' option. Otherwise you may ignore this message and continue. """.strip() +if g.quiet: m = "" confirm_or_exit(m, "continue", expect="YES") err_flag = False @@ -135,10 +136,7 @@ for n,i in enumerate(addr_data): break else: import_address(i[1],label,rescan=False) - msg_r(msg_fmt % ( - ("%s/%s:" % (n+1,len(addr_data))), - i[1], "(" + label + ")" - ) - ) + msg_r(msg_fmt % (("%s/%s:" % (n+1,len(addr_data))), + i[1], "(" + label + ")")) if err_flag: msg("\nImport failed"); sys.exit(2) msg(" - OK") diff --git a/mmgen-tool b/mmgen-tool index e84a07e0..8c13eedb 100755 --- a/mmgen-tool +++ b/mmgen-tool @@ -30,7 +30,7 @@ from mmgen.util import pretty_hexdump help_data = { 'prog_name': g.prog_name, 'desc': "Perform various BTC-related operations", - 'usage': "[opts] ", + 'usage': "[opts] ", 'options': """ -d, --outdir= d Specify an alternate directory 'd' for output -h, --help Print this help message diff --git a/mmgen/Opts.py b/mmgen/Opts.py index b940c2d4..d0154b15 100755 --- a/mmgen/Opts.py +++ b/mmgen/Opts.py @@ -23,16 +23,17 @@ def usage(hd): print "USAGE: %s %s" % (hd['prog_name'], hd['usage']) sys.exit(2) -def print_version_info(progname): +def print_version_info(): # MMGen only print """ -'{}' version {g.version}. Part of the {g.proj_name} suite. +'{g.prog_name}' version {g.version}. Part of the {g.proj_name} suite. Copyright (C) {g.Cdates} by {g.author} {g.email}. -""".format(progname, g=g).strip() +""".format(g=g).strip() -def print_help(progname,help_data): - pn_len = str(len(progname)+2) - print (" %-"+pn_len+"s %s") % (progname.upper()+":", help_data['desc'].strip()) - print (" %-"+pn_len+"s %s %s")%("USAGE:", progname, help_data['usage'].strip()) +def print_help(help_data): + pn = help_data['prog_name'] + pn_len = str(len(pn)+2) + print (" %-"+pn_len+"s %s") % (pn.upper()+":", help_data['desc'].strip()) + print (" %-"+pn_len+"s %s %s")%("USAGE:", pn, help_data['usage'].strip()) sep = "\n " print " OPTIONS:"+sep+"%s" % sep.join(help_data['options'].strip().split("\n")) if "notes" in help_data: @@ -41,10 +42,8 @@ def print_help(progname,help_data): def process_opts(argv,help_data,short_opts,long_opts): - progname = argv[0].split("/")[-1] - if len(argv) == 2 and argv[1] == '--version': # MMGen only! - print_version_info(progname); sys.exit() + print_version_info(); sys.exit() if g.debug: print "Short opts: %s" % repr(short_opts) @@ -63,7 +62,7 @@ def process_opts(argv,help_data,short_opts,long_opts): else: short_opts_l += i for opt, arg in cl_opts: - if opt in ("-h","--help"): print_help(progname,help_data); sys.exit() + if opt in ("-h","--help"): print_help(help_data); sys.exit() elif opt[:2] == "--" and opt[2:] in long_opts: opts[opt[2:].replace("-","_")] = True elif opt[:2] == "--" and opt[2:]+"=" in long_opts: @@ -143,7 +142,7 @@ def check_opts(opts,long_opts): msg("Requested %s '%s' is unwritable by you" % (what,val)) return False else: - msg("Requested %s '%s' doen not exist" % (what,val)) + msg("Requested %s '%s' does not exist" % (what,val)) return False elif opt == 'label': diff --git a/mmgen/tool.py b/mmgen/tool.py index 181edfcd..ac266c10 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -40,7 +40,7 @@ commands = { "hextob58": [' [str]'], "b58tohex": [' [str]'], "b58randenc": [], - "getrand": ['bytes [int=32]'], + "randhex": ['nbytes [int=32]'], "randwif": ['compressed [bool=False]'], "randpair": ['compressed [bool=False]'], "wif2hex": [' [str]', 'compressed [bool=False]'], @@ -58,7 +58,8 @@ commands = { "listaddresses": ['minconf [int=1]', 'showempty [bool=False]'], "getbalance": ['minconf [int=1]'], "viewtx": [' [str]'], - "check_addrfile": [' [str]'], + "check_addrfile": [' [str]'], + "find_incog_data": [' [str]',' [str]','keep_searching [bool=False]'], "hexreverse": [' [str]'], "sha256x2": [' [str]', 'hex_input [bool=False]','file_input [bool=False]'], @@ -70,48 +71,57 @@ commands = { "privhex2addr": [' [str]','compressed [bool=False]'], "encrypt": [' [str]','outfile [str=""]','hash_preset [str="3"]'], "decrypt": [' [str]','outfile [str=""]','hash_preset [str="3"]'], + "rand2file": [' [str]',' [str]','threads [int=4]'], + "bytespec": [' [str]'], } command_help = """ - File operations - hexdump - encode data into formatted hexadecimal form (file or stdin) - unhexdump - decode formatted hexadecimal data (file or stdin) - - {pnm}-specific operations - id8 - generate 8-character {pnm} ID checksum for file (or stdin) - id6 - generate 6-character {pnm} ID checksum for file (or stdin) - check_addrfile - compute checksum and address list for {pnm} address file - - Bitcoin operations: - strtob58 - convert a string to base 58 - hextob58 - convert a hexadecimal number to base 58 - b58tohex - convert a base 58 number to hexadecimal + Bitcoin address/key operations (compressed addresses supported): + addr2hexaddr - convert Bitcoin address from base58 to hex format b58randenc - generate a random 32-byte number and convert it to base 58 - randwif - generate a random private key in WIF format - randpair - generate a random private key/address pair - wif2hex - convert a private key from WIF to hex format + b58tohex - convert a base 58 number to hexadecimal hex2wif - convert a private key from hex to WIF format - wif2addr - generate a Bitcoin address from a key in WIF format + hexaddr2addr - convert Bitcoin address from hex to base58 format + hextob58 - convert a hexadecimal number to base 58 + privhex2addr - generate Bitcoin address from private key in hex format pubkey2addr - convert Bitcoin public key to address pubkey2hexaddr - convert Bitcoin public key to address in hex format - hexaddr2addr - convert Bitcoin address from hex to base58 format - addr2hexaddr - convert Bitcoin address from base58 to hex format - privhex2addr - generate Bitcoin address from private key in hex format + randpair - generate a random private key/address pair + randwif - generate a random private key in WIF format + strtob58 - convert a string to base 58 + wif2addr - generate a Bitcoin address from a key in WIF format + wif2hex - convert a private key from WIF to hex format - Miscellaneous operations: - hexreverse - reverse bytes of a hexadecimal string + Wallet/TX operations (bitcoind must be running): + getbalance - like 'bitcoind getbalance' but shows confirmed/unconfirmed, + spendable/unspendable balances for individual {pnm} wallets + listaddresses - list {pnm} addresses and their balances + viewtx - show raw/signed {pnm} transaction in human-readable form + + General utilities: + bytespec - convert a byte specifier such as '1GB' into a plain integer + hexdump - encode data into formatted hexadecimal form (file or stdin) hexlify - display string in hexadecimal format + hexreverse - reverse bytes of a hexadecimal string + rand2file - write 'n' bytes of random data to specified file + randhex - print 'n' bytes (default 32) of random data in hex format sha256x2 - compute a double sha256 hash of data - getrand - print 'n' bytes (default 32) of random data in hex format + unhexdump - decode formatted hexadecimal data (file or stdin) - Encryption operations: - encrypt - encrypt a file using {pnm}'s encryption suite - decrypt - decrypt an {pnm}-encrypted file + File encryption: + encrypt - encrypt a file + decrypt - decrypt a file {pnm} encryption suite: * Key: Scrypt (user-configurable hash parameters, 32-byte salt) * Enc: AES256_CTR, 16-byte rand IV, sha256 hash + 32-byte nonce + data * The encrypted file is indistinguishable from random data + {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) + Mnemonic operations (choose "electrum" (default), "tirosh" or "all" wordlists): mn_rand128 - generate random 128-bit mnemonic @@ -120,12 +130,6 @@ command_help = """ mn_stats - show stats for mnemonic wordlist mn_printlist - print mnemonic wordlist - Bitcoind operations (bitcoind must be running): - listaddresses - show {pnm} addresses and their balances - getbalance - like 'bitcoind getbalance' but shows confirmed/unconfirmed, - spendable/unspendable - viewtx - show raw/signed {pnm} transaction in human-readable form - IMPORTANT NOTE: Though {pnm} mnemonics use the Electrum wordlist, they're computed using a different algorithm and are NOT Electrum-compatible! """.format(pnm=g.proj_name) @@ -234,8 +238,8 @@ def b58randenc(): dec = bitcoin.b58decode(enc) print_convert_results(ba.hexlify(r),enc,ba.hexlify(dec)) -def getrand(bytes='32'): - print ba.hexlify(get_random(int(bytes),opts)) +def randhex(nbytes='32'): + print ba.hexlify(get_random(int(nbytes),opts)) def randwif(compressed=False): r_hex = ba.hexlify(get_random(32,opts)) @@ -443,3 +447,107 @@ def decrypt(infile,outfile="",hash_preset=''): write_to_file((outfile or of),out,opts,"decrypted data",True,True) else: msg("Incorrect passphrase or hash preset") + + +def find_incog_data(filename,iv_id,keep_searching=False): + ivsize,bsize,mod = g.aesctr_iv_len,4096,4096*8 + n,carry = 0," "*ivsize + f = os.open(filename,os.O_RDONLY) + while True: + d = os.read(f,bsize) + if not d: break + d = carry + d + for i in range(bsize): + if sha256(d[i:i+ivsize]).hexdigest()[:8].upper() == iv_id: + if n+i < ivsize: continue + msg("\rIncog data for ID %s found at offset %s" % + (iv_id,n+i-ivsize)) + if not keep_searching: sys.exit(0) + carry = d[len(d)-ivsize:] + n += bsize + if not n % mod: msg_r("\rSearched: %s bytes" % n) + + msg("") + os.close(f) + +# From "man dd": +# c=1, w=2, b=512, kB=1000, K=1024, MB=1000*1000, M=1024*1024, +# GB=1000*1000*1000, G=1024*1024*1024, and so on for T, P, E, Z, Y. + +def parse_nbytes(nbytes): + import re + m = re.match(r'([0123456789]+)(.*)',nbytes) + smap = ("c",1),("w",2),("b",512),("kB",1000),("K",1024),("MB",1000*1000),\ + ("M",1024*1024),("GB",1000*1000*1000),("G",1024*1024*1024) + if m: + if m.group(2): + for k,v in smap: + if k == m.group(2): + return int(m.group(1)) * v + else: + msg("Valid byte specifiers: '%s'" % "' '".join([i[0] for i in smap])) + else: + return int(nbytes) + + msg("'%s': invalid byte specifier" % nbytes) + sys.exit(1) + + +def rand2file(outfile, nbytes, threads=4): + nbytes = parse_nbytes(nbytes) + from Crypto import Random + rh = Random.new() + from Queue import Queue + from threading import Thread + bsize = 2**20 + roll = bsize * 4 + if 'outdir' in opts: outfile = make_full_path(opts['outdir'],outfile) + f = open(outfile,"w") + + from Crypto.Cipher import AES + from Crypto.Util import Counter + + key = get_random(32,opts) + + def encrypt_worker(wid): + while True: + i,d = q1.get() + c = AES.new(key, AES.MODE_CTR, + counter=Counter.new(g.aesctr_iv_len*8,initial_value=i)) + enc_data = c.encrypt(d) + q2.put(enc_data) + q1.task_done() + + def output_worker(): + while True: + data = q2.get() + f.write(data) + q2.task_done() + + q1 = Queue() + for i in range(max(1,threads-2)): + t = Thread(target=encrypt_worker, args=(i,)) + t.daemon = True + t.start() + + q2 = Queue() + t = Thread(target=output_worker) + t.daemon = True + t.start() + + i = 1; rbytes = nbytes + while rbytes > 0: + d = rh.read(min(bsize,rbytes)) + q1.put((i,d)) + rbytes -= bsize + i += 1 + if not (bsize*i) % roll: + msg_r("\rRead: %s bytes" % (bsize*i)) + + msg("\rRead: %s bytes" % nbytes) + qmsg("\r%s bytes written to file '%s'" % (nbytes,outfile)) + q1.join() + q2.join() + f.close() + +def bytespec(s): print parse_nbytes(s) diff --git a/mmgen/tx.py b/mmgen/tx.py index 9bb09b7f..01f9fb77 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -141,18 +141,17 @@ def normalize_btc_amt(amt): def get_bitcoind_cfg_options(cfg_keys): - if "HOME" in os.environ: - data_dir = ".bitcoin" - cfg_file = "%s/%s/%s" % (os.environ["HOME"], data_dir, "bitcoin.conf") - elif "HOMEPATH" in os.environ: - # Windows: - data_dir = r"Application Data\Bitcoin" - cfg_file = "%s\%s\%s" % (os.environ["HOMEPATH"],data_dir,"bitcoin.conf") + if "HOME" in os.environ: # Linux + homedir,datadir = os.environ["HOME"],".bitcoin" + elif "HOMEPATH" in os.environ: # Windows: + homedir,data_dir = os.environ["HOMEPATH"],r"Application Data\Bitcoin" else: msg("Neither $HOME nor %HOMEPATH% are set") msg("Don't know where to look for 'bitcoin.conf'") sys.exit(3) + cfg_file = os.sep.join((homedir, datadir, "bitcoin.conf")) + cfg = dict([(k,v) for k,v in [split2(line.translate(None,"\t "),"=") for line in get_lines_from_file(cfg_file)] if k in cfg_keys]) @@ -535,7 +534,7 @@ def check_addr_data_hash(seed_id,addr_data): fl = fmt_addr_idxs([int(a[0]) for a in addr_data]) msg("Computed checksum for addr data {}[{}]: {}".format( seed_id,fl,addr_data_chksum)) - msg("Check this value against your records") + qmsg("Check this value against your records") def parse_addrs_file(f): diff --git a/mmgen/util.py b/mmgen/util.py index 303d97c0..f096bd90 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -80,7 +80,6 @@ def get_random_data_from_user(uchars): prompt = "User random data successfully acquired. Press ENTER to continue" prompt_and_get_char(prompt,"",enter_ok=True) - msg("") return key_data+"".join(fmt_time_data) @@ -97,7 +96,7 @@ def get_random(length,opts): else: kwhat += "saved user entropy" key = make_key(g.user_entropy, "", '2', what=kwhat) - return encrypt_data(os_rand,key,what="random data") + return encrypt_data(os_rand,key,what="random data",verify=False) else: return os_rand @@ -137,11 +136,11 @@ def show_hash_presets(): cmessages = { 'null': "", 'incog_iv_id': """ - If you know your IV ID, check it against the value above. If it's + If you know your Incog ID, check it against the value above. If it's incorrect, then your incognito data is invalid. """, 'incog_iv_id_hidden': """ - If you know your IV ID, check it against the value above. If it's + If you know your Incog ID, check it against the value above. If it's incorrect, then your incognito data is invalid or you've supplied an incorrect offset. """, @@ -253,6 +252,9 @@ def make_chksum_8(s,sep=False): def make_chksum_6(s): return sha256(s).hexdigest()[:6] +def make_iv_chksum(s): + return sha256(s).hexdigest()[:8].upper() + def check_infile(f): @@ -372,7 +374,7 @@ def _get_seed_from_brain_passphrase(words,opts): def encrypt_seed(seed, key): return encrypt_data(seed, key, iv=1, what="seed") -def encrypt_data(data, key, iv=1, what="data"): +def encrypt_data(data, key, iv=1, what="data", verify=True): """ Encrypt arbitrary data using AES256 in counter mode """ @@ -387,16 +389,17 @@ def encrypt_data(data, key, iv=1, what="data"): counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv)) enc_data = c.encrypt(data) - vmsg_r("Performing a test decryption of the %s..." % what) + 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) + 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) + 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 @@ -432,9 +435,15 @@ def open_file_or_exit(filename,mode): 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 = "%s/%s" % (opts['outdir'], outfile) + if 'outdir' in opts: outfile = make_full_path(opts['outdir'],outfile) if confirm: from os import stat @@ -456,7 +465,6 @@ def write_to_file(outfile,data,opts,what="data",confirm=False,verbose=False): if verbose: msg("%s written to file '%s'" % (what.capitalize(),outfile)) - def export_to_file(outfile, data, opts, what="data"): if 'stdout' in opts: @@ -846,7 +854,8 @@ def get_seed_from_incog_wallet( 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)) + 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']) @@ -1047,17 +1056,18 @@ def do_pager(text): def export_to_hidden_incog(incog_enc,opts): - fname,offset = opts['export_incog_hidden'].split(",") #Already sanity-checked + outfile,offset = opts['export_incog_hidden'].split(",") #Already sanity-checked + if 'outdir' in opts: outfile = make_full_path(opts['outdir'],outfile) - check_data_fits_file_at_offset(fname,int(offset),len(incog_enc),"write") + check_data_fits_file_at_offset(outfile,int(offset),len(incog_enc),"write") - if not g.quiet: confirm_or_exit("","alter file '%s'" % fname) - f = os.open(fname,os.O_RDWR) + if not g.quiet: confirm_or_exit("","alter file '%s'" % outfile) + f = os.open(outfile,os.O_RDWR) os.lseek(f, int(offset), os.SEEK_SET) os.write(f, incog_enc) os.close(f) - qmsg("Data written to file '%s' at offset %s" % (fname,offset), - "Data written to file") + msg("Data written to file '%s' at offset %s" % + (os.path.relpath(outfile),offset)) def pretty_hexdump(data,gw=2,cols=8,line_nums=False): @@ -1090,8 +1100,8 @@ def wallet_to_incog_data(infile,opts): sys.exit(2) iv = get_random(g.aesctr_iv_len,opts) - iv_id = make_chksum_8(iv) - qmsg("IV ID: %s" % iv_id) + 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") diff --git a/setup.py b/setup.py index 6cdfbe18..0580fe97 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup( name = 'mmgen', - version = '0.7.6a', + version = '0.7.7', author = 'Philemon', author_email = 'mmgen-py@yandex.com', url = 'https://github.com/mmgen/mmgen',