Browse Source

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.

philemon 10 years ago
parent
commit
e328a6a24b
9 changed files with 204 additions and 88 deletions
  1. 1 0
      MANIFEST
  2. 2 1
      mmgen-addrgen
  3. 3 5
      mmgen-addrimport
  4. 1 1
      mmgen-tool
  5. 11 12
      mmgen/Opts.py
  6. 144 36
      mmgen/tool.py
  7. 7 8
      mmgen/tx.py
  8. 34 24
      mmgen/util.py
  9. 1 1
      setup.py

+ 1 - 0
MANIFEST

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

+ 2 - 1
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.")

+ 3 - 5
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")

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

+ 11 - 12
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':

+ 144 - 36
mmgen/tool.py

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

+ 7 - 8
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):
 

+ 34 - 24
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")

+ 1 - 1
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',