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.
This commit is contained in:
philemon 2014-08-01 23:08:31 +04:00
commit e328a6a24b
9 changed files with 204 additions and 88 deletions

View file

@ -2,6 +2,7 @@
__init__.py
mmgen-addrgen
mmgen-addrimport
mmgen-keygen
mmgen-passchg
mmgen-pywallet
mmgen-tool

View file

@ -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.")

View file

@ -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")

View file

@ -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] <command> <args>",
'usage': "[opts] <command> <command args>",
'options': """
-d, --outdir= d Specify an alternate directory 'd' for output
-h, --help Print this help message

View file

@ -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':

View file

@ -40,7 +40,7 @@ commands = {
"hextob58": ['<hex number> [str]'],
"b58tohex": ['<b58 number> [str]'],
"b58randenc": [],
"getrand": ['bytes [int=32]'],
"randhex": ['nbytes [int=32]'],
"randwif": ['compressed [bool=False]'],
"randpair": ['compressed [bool=False]'],
"wif2hex": ['<wif> [str]', 'compressed [bool=False]'],
@ -58,7 +58,8 @@ commands = {
"listaddresses": ['minconf [int=1]', 'showempty [bool=False]'],
"getbalance": ['minconf [int=1]'],
"viewtx": ['<MMGen tx file> [str]'],
"check_addrfile": ['<MMGen addr file> [str]'],
"check_addrfile": ['<MMGen addr file> [str]'],
"find_incog_data": ['<file or device name> [str]','<Incog ID> [str]','keep_searching [bool=False]'],
"hexreverse": ['<hexadecimal string> [str]'],
"sha256x2": ['<str, hexstr or filename> [str]',
'hex_input [bool=False]','file_input [bool=False]'],
@ -70,48 +71,57 @@ commands = {
"privhex2addr": ['<private key in hex format> [str]','compressed [bool=False]'],
"encrypt": ['<infile> [str]','outfile [str=""]','hash_preset [str="3"]'],
"decrypt": ['<infile> [str]','outfile [str=""]','hash_preset [str="3"]'],
"rand2file": ['<outfile> [str]','<nbytes> [str]','threads [int=4]'],
"bytespec": ['<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)

View file

@ -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):

View file

@ -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")

View file

@ -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',