* New incognito format with checksum for password verification. Old format

continues to be supported with '--old-incog-fmt' option

* mmgen-txsign: '--mmgen-keys-from-file' option (supersedes
  '--all-keys-from-file' option) allows offline signing of transactions with
  both MMGen and non-MMGen inputs.  Instead of a flat keylist, a key-address
  file (the output of 'mmgen-keygen'), optionally encrypted, is used both as a
  key source and to verify MMGen-to-BTC mappings for both inputs and outputs,
  eliminating the need for an additional address file.

* mmgen-addrimport: '--keyaddr-file' option allows using key-address file
  (possibly encrypted) as an address source.
This commit is contained in:
The MMGen Project 2014-08-20 20:34:29 +04:00
commit 4378ac0db9
19 changed files with 1255 additions and 1205 deletions

View file

@ -57,7 +57,7 @@ def parse_opts(argv,help_data):
('outdir', 'export_incog_hidden'),
('from_incog_hidden','from_incog','from_seed','from_mnemonic','from_brain'),
('export_incog','export_incog_hex','export_incog_hidden','export_mnemonic',
'export_seed'),
'export_seed'),
('quiet','verbose')
): warn_incompatible_opts(opts,l)
@ -65,12 +65,14 @@ def parse_opts(argv,help_data):
if not check_opts(opts,long_opts): sys.exit(1)
# If unset, set these to default values in mmgen.config:
for v in g.cl_override_vars:
for v in g.dfl_vars:
if v in opts: typeconvert_override_var(opts,v)
else: opts[v] = eval("g."+v)
if "verbose" in opts: g.verbose = True
if "quiet" in opts: g.quiet = True
# Opposite of above: if set, override the default values in mmgen.config:
if 'no_keyconv' in opts: g.no_keyconv = opts['no_keyconv']
if 'verbose' in opts: g.verbose = opts['verbose']
if 'quiet' in opts: g.quiet = opts['quiet']
if g.debug: print "opts after typeconvert: %s" % opts
@ -123,8 +125,8 @@ def check_opts(opts,long_opts):
what = "parameter for '--%s' option" % opt.replace("_","-")
# Check for file existence and readability
if opt in ('keys_from_file','all_keys_from_file','addrlist',
'passwd_file','keysforaddrs'):
if opt in ('keys_from_file','mmgen_keys_from_file',
'passwd_file','keysforaddrs','comment_file'):
check_infile(val) # exits on error
continue
@ -170,7 +172,7 @@ def check_opts(opts,long_opts):
if not opt_is_in_list(val,g.hash_presets.keys(),what): return False
elif opt == 'usr_randchars':
if not opt_is_int(val,what): return False
if val == '0': return True
if val == '0': continue
if not opt_compares(val,">=",g.min_urandchars,what): return False
if not opt_compares(val,"<=",g.max_urandchars,what): return False
else:
@ -187,6 +189,7 @@ def typeconvert_override_var(opts,opt):
if vtype == int: f,t = int,"an integer"
elif vtype == str: f,t = str,"a string"
elif vtype == float: f,t = float,"a float"
elif vtype == bool: f,t = bool,"a boolean value"
try:
opts[opt] = f(opts[opt])

View file

@ -58,10 +58,10 @@ def test_for_keyconv():
return True
def generate_addrs(seed, addrnums, opts):
def generate_addrs(seed, addrnums, opts, seed_id=""):
if 'addrs' in opts['gen_what']:
if 'no_keyconv' in opts or test_for_keyconv() == False:
if 'a' in opts['gen_what']:
if g.no_keyconv or test_for_keyconv() == False:
msg("Using (slow) internal ECDSA library for address generation")
from mmgen.bitcoin import privnum2addr
keyconv = False
@ -69,18 +69,21 @@ def generate_addrs(seed, addrnums, opts):
from subprocess import Popen, PIPE
keyconv = "keyconv"
fmt = "num addr" if opts['gen_what'] == ["addrs"] else (
"num sec wif" if opts['gen_what'] == ["keys"] else "num sec wif addr")
fmt = "num sec wif addr" if 'ka' in opts['gen_what'] else (
"num sec wif" if 'k' in opts['gen_what'] else "num addr")
from collections import namedtuple
addrinfo = namedtuple("addrinfo",fmt)
addrinfo_args = "%s" % ",".join(fmt.split())
addrnums = sorted(set(addrnums)) # don't trust the calling function
t_addrs,num,pos,out = len(addrnums),0,0,[]
addrnums.sort() # needed only if caller didn't sort
ws,wp = ('key','keys') if 'keys' in opts['gen_what'] \
else ('address','addresses')
w = {
'ka': ('key/address pair','s'),
'k': ('key','s'),
'a': ('address','es')
}[opts['gen_what']]
while pos != t_addrs:
seed = sha512(seed).digest()
@ -91,50 +94,51 @@ def generate_addrs(seed, addrnums, opts):
pos += 1
qmsg_r("\rGenerating %s #%s (%s of %s)" % (ws,num,pos,t_addrs))
qmsg_r("\rGenerating %s #%s (%s of %s)" % (w[0],num,pos,t_addrs))
# Secret key is double sha256 of seed hash round /num/
sec = sha256(sha256(seed).digest()).hexdigest()
wif = numtowif(int(sec,16))
if 'addrs' in opts['gen_what']: addr = \
if 'a' in opts['gen_what']: addr = \
Popen([keyconv, wif], stdout=PIPE).stdout.readline().split()[1] \
if keyconv else privnum2addr(int(sec,16))
out.append(eval("addrinfo("+addrinfo_args+")"))
qmsg("\rGenerated %s %s%s"%(t_addrs, (ws if t_addrs == 1 else wp), " "*15))
m = w[0] if t_addrs == 1 else w[0]+w[1]
if seed_id:
qmsg("\r%s: %s %s generated%s" % (seed_id,t_addrs,m," "*15))
else:
qmsg("\rGenerated %s %s%s" % (t_addrs,m," "*15))
return out
def format_addr_data(addr_data, addr_data_chksum, seed_id, addr_idxs, opts):
if 'flat_list' in opts:
return "\n\n".join(["# {}:{d.num} {d.addr}\n{d.wif}".format(seed_id,d=d)
for d in addr_data])+"\n\n"
fs = " {:<%s} {}" % len(str(addr_data[-1].num))
if 'addrs' not in opts['gen_what']: out = []
else:
if 'a' in opts['gen_what']:
out = [] if 'stdout' in opts else [addrmsgs['addrfile_header']+"\n"]
out.append("# Address data checksum for {}[{}]: {}".format(
seed_id, fmt_addr_idxs(addr_idxs), addr_data_chksum))
w = "Key-address" if 'k' in opts['gen_what'] else "Address"
out.append("# {} data checksum for {}[{}]: {}".format(
w, seed_id, fmt_addr_idxs(addr_idxs), addr_data_chksum))
out.append("# Record this value to a secure location\n")
else: out = []
out.append("%s {" % seed_id.upper())
for d in addr_data:
if 'addrs' in opts['gen_what']: # First line with number
if 'a' in opts['gen_what']: # First line with number
out.append(fs.format(d.num, d.addr))
else:
out.append(fs.format(d.num, "wif: "+d.wif))
if 'keys' in opts['gen_what']: # Subsequent lines
if 'k' in opts['gen_what']: # Subsequent lines
if 'b16' in opts:
out.append(fs.format("", "hex: "+d.sec))
if 'addrs' in opts['gen_what']:
if 'a' in opts['gen_what']:
out.append(fs.format("", "wif: "+d.wif))
out.append("}")

View file

@ -52,65 +52,56 @@ b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
# The "zero address":
# 1111111111111111111114oLvT2 (use step2 = ("0" * 40) to generate)
#
def pubhex2hexaddr(pubhex):
step1 = sha256(unhexlify(pubhex)).digest()
return hashlib_new('ripemd160',step1).hexdigest()
def hexaddr2addr(hexaddr):
def hexaddr2addr(hexaddr, vers_num='00'):
# See above:
extra_ones = (len(hexaddr) - len(hexaddr.lstrip("0"))) / 2
step1 = sha256(unhexlify('00'+hexaddr)).digest()
hexaddr2 = vers_num + hexaddr
step1 = sha256(unhexlify(hexaddr2)).digest()
step2 = sha256(step1).hexdigest()
pubkey = int(hexaddr + step2[:8], 16)
return "1" + ("1" * extra_ones) + _numtob58(pubkey)
def pubhex2addr(pubhex):
return hexaddr2addr(pubhex2hexaddr(pubhex))
pubkey = hexaddr2 + step2[:8]
lzeroes = (len(hexaddr2) - len(hexaddr2.lstrip("0"))) / 2
return ("1" * lzeroes) + _numtob58(int(pubkey,16))
def verify_addr(addr,verbose=False,return_hex=False):
if addr[0] != "1":
if verbose: print "%s: Invalid address" % addr
return False
for vers_num,ldigit in ('00','1'),('05','3'):
if addr[0] != ldigit: continue
num = _b58tonum(addr)
if num == False: break
addr_hex = "{:050x}".format(num)
if addr_hex[:2] != vers_num: continue
step1 = sha256(unhexlify(addr_hex[:42])).digest()
step2 = sha256(step1).hexdigest()
if step2[:8] == addr_hex[42:]:
return addr_hex[2:42] if return_hex else True
else:
if verbose: print "Invalid checksum in address '%s'" % addr
break
num = _b58tonum(addr[1:])
if num == False: return False
addr_hex = hex(num)[2:].rstrip("L").zfill(48)
if verbose: print "Invalid address '%s'" % addr
return False
step1 = sha256(unhexlify('00'+addr_hex[:40])).digest()
step2 = sha256(step1).hexdigest()
if step2[:8] != addr_hex[40:]:
if verbose: print "Invalid checksum in address %s" % ("1" + addr)
return False
return addr_hex[:40] if return_hex else True
# Reworked code from here:
def _numtob58(num):
b58conv,i = [],0
while True:
n = num / (58**i); i += 1
if not n: break
b58conv.append(b58a[n % 58])
return ''.join(b58conv)[::-1]
ret = []
while num:
ret.append(b58a[num % 58])
num /= 58
return ''.join(ret)[::-1]
def _b58tonum(b58num):
for i in b58num:
if not i in b58a:
print "Invalid symbol in b58 number: '%s'" % i
return False
b58deconv = []
b58num_r = b58num[::-1]
for i in range(len(b58num)):
idx = b58a.index(b58num_r[i])
b58deconv.append(idx * (58**i))
return sum(b58deconv)
if not i in b58a: return False
return sum([b58a.index(n) * (58**i) for i,n in enumerate(list(b58num[::-1]))])
def numtowif(numpriv):
step1 = '80'+hex(numpriv)[2:].rstrip('L').zfill(64)
step1 = '80' + "{:064x}".format(numpriv)
step2 = sha256(unhexlify(step1)).digest()
step3 = sha256(step2).hexdigest()
key = step1 + step3[:8]
@ -131,8 +122,8 @@ def b58decode(b58num):
# Zap all spaces:
num = _b58tonum(b58num.translate(None,' \t\n\r'))
if num == False: return False
out = hex(num)[2:].rstrip('L')
return unhexlify("0" + out if len(out) % 2 else out)
out = "{:x}".format(num)
return unhexlify("0"*(len(out)%2) + out)
# These yield bytewise equivalence in our special cases:
@ -165,7 +156,7 @@ def wiftohex(wifpriv,compressed=False):
idx = 68 if compressed else 66
num = _b58tonum(wifpriv)
if num == False: return False
key = hex(num)[2:].rstrip('L')
key = "{:x}".format(num)
if compressed and key[66:68] != '01': return False
round1 = sha256(unhexlify(key[:idx])).digest()
round2 = sha256(round1).hexdigest()
@ -182,16 +173,10 @@ def privnum2pubhex(numpriv,compressed=False):
pko = ecdsa.SigningKey.from_secret_exponent(numpriv,secp256k1)
pubkey = hexlify(pko.get_verifying_key().to_string())
if compressed:
p = '03' if pubkey[-1] in "13579bdf" else '02'
p = '02' if pubkey[-1] in "02468ace" else '03'
return p+pubkey[:64]
else:
return '04'+pubkey
def privnum2addr(numpriv,compressed=False):
return pubhex2addr(privnum2pubhex(numpriv,compressed))
# Used only in test suite. To check validity, recode with numtowif()
def wiftonum(wifpriv):
num = _b58tonum(wifpriv)
if num == False: return False
return (num % (1<<288)) >> 32
return hexaddr2addr(pubhex2hexaddr(privnum2pubhex(numpriv,compressed)))

View file

@ -28,7 +28,8 @@ email = "<mmgen-py@yandex.com>"
Cdates = '2013-2014'
version = '0.7.7'
quiet,verbose = False,False
quiet,verbose,no_keyconv = False,False,False
min_screen_width = 80
max_tx_comment_len = 72
@ -52,18 +53,18 @@ sigtx_ext = "sig"
addrfile_ext = "addrs"
addrfile_chksum_ext = "chk"
keyfile_ext = "keys"
keylist_ext = "keylist"
keyaddrfile_ext = "akeys"
mmenc_ext = "mmenc"
default_wl = "electrum"
#default_wl = "tirosh"
cl_override_vars = 'seed_len','hash_preset','usr_randchars'
dfl_vars = "seed_len","hash_preset","usr_randchars"
seed_lens = 128,192,256
seed_len = 256
mnemonic_lens = [i / 32 * 3 for i in seed_lens]
mn_lens = [i / 32 * 3 for i in seed_lens]
http_timeout = 30

View file

@ -28,13 +28,41 @@ import mmgen.config as g
from mmgen.util import *
from mmgen.term import get_char
crmsg = {
'incog_iv_id': """
Check that the generated Incog ID above is correct.
If it's not, then your incognito data is incorrect or corrupted.
""",
'incog_iv_id_hidden': """
Check that the generated Incog ID above is correct.
If it's not, then your incognito data is incorrect or corrupted,
or you've supplied an incorrect offset.
""",
'usr_rand_notice': """
You've chosen to not fully trust your OS's random number generator and provide
some additional entropy of your own. Please type %s symbols on your keyboard.
Type slowly and choose your symbols carefully for maximum randomness. Try to
use both upper and lowercase as well as punctuation and numerals. What you
type will not be displayed on the screen. Note that the timings between your
keystrokes will also be used as a source of randomness.
""",
'incorrect_incog_passphrase_try_again': """
Incorrect passphrase, hash preset, or maybe old-format incog wallet.
Try again? (Y)es, (n)o, (m)ore information:
""".strip(),
'confirm_seed_id': """
If the seed ID above is correct but you're seeing this message, then you need
to exit and re-run the program with the '--old-incog-fmt' option.
""".strip(),
}
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...")
vmsg_r("Checking key...")
chk1 = make_chksum_8(key)
if key_id:
if not compare_checksums(chk1, "of key", key_id, "in header"):
@ -56,12 +84,14 @@ def decrypt_seed(enc_seed, key, seed_id, key_id):
else:
msg("Incorrect passphrase")
vmsg("")
return False
# else:
# qmsg("Generated IDs (Seed/Key): %s/%s" % (chk2,chk1))
if g.debug: print "Decrypted seed: %s" % hexlify(dec_seed)
vmsg("OK")
return dec_seed
@ -97,7 +127,7 @@ def encrypt_data(data, key, iv=1, what="data", verify=True):
def decrypt_data(enc_data, key, iv=1, what="data"):
vmsg("Decrypting %s with key..." % what)
vmsg_r("Decrypting %s with key..." % what)
from Crypto.Cipher import AES
from Crypto.Util import Counter
@ -119,10 +149,10 @@ def scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32):
return scrypt.hash(passwd, salt, 2**N, r, p, buflen=buflen)
def make_key(passwd, salt, hash_preset, what="key", verbose=False):
def make_key(passwd, salt, hash_preset, what="encryption key", verbose=False):
if g.verbose or verbose:
msg_r("Generating %s. Please wait..." % what)
msg_r("Generating %s from passphrase.\nPlease wait..." % what)
key = scrypt_hash_passphrase(passwd, salt, hash_preset)
if g.verbose or verbose:
msg("done")
@ -133,7 +163,7 @@ def make_key(passwd, salt, hash_preset, what="key", verbose=False):
def get_random_data_from_user(uchars):
if g.quiet: msg("Enter %s random symbols" % uchars)
else: msg(cmessages['usr_rand_notice'] % uchars)
else: msg(crmsg['usr_rand_notice'] % uchars)
prompt = "You may begin typing. %s symbols left: "
msg_r(prompt % uchars)
@ -187,26 +217,60 @@ def get_random(length,opts):
def get_seed_from_wallet(
infile,
opts,
prompt="{} wallet".format(g.proj_name),
prompt_info="{} wallet".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)
if g.debug: display_control_data(*wdata)
passwd = get_mmgen_passphrase(prompt,opts)
padd = " "+infile if g.quiet else ""
passwd = get_mmgen_passphrase(prompt_info+padd,opts)
key = make_key(passwd, salt, hash_preset)
return decrypt_seed(enc_seed, key, metadata[0], metadata[1])
def get_hidden_incog_data(opts):
# Already sanity-checked:
fname,offset,seed_len = opts['from_incog_hidden'].split(",")
qmsg("Getting hidden incog data from file '%s'" % fname)
z = 0 if 'old_incog_fmt' in opts else 8
dlen = g.aesctr_iv_len + g.salt_len + (int(seed_len)/8) + z
fsize = check_data_fits_file_at_offset(fname,int(offset),dlen,"read")
import os
f = os.open(fname,os.O_RDONLY)
os.lseek(f, int(offset), os.SEEK_SET)
data = os.read(f, dlen)
os.close(f)
qmsg("Data read from file '%s' at offset %s" % (fname,offset),
"Data read from file")
return data
def confirm_old_format():
while True:
reply = get_char(
crmsg['incorrect_incog_passphrase_try_again']+" ").strip("\n\r")
if not reply: msg(""); return False
elif reply in 'yY': msg(""); return False
elif reply in 'nN': msg("\nExiting at user request"); sys.exit(1)
elif reply in 'mM': msg(""); return True
else:
if g.verbose: msg("\nInvalid reply")
else: msg_r("\r")
def get_seed_from_incog_wallet(
infile,
opts,
prompt_what="{} incognito wallet".format(g.proj_name),
prompt_info="{} incognito wallet".format(g.proj_name),
silent=False,
hex_input=False
):
@ -224,75 +288,66 @@ def get_seed_from_incog_wallet(
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]
z = 0 if 'old_incog_fmt' in opts else 8
valid_dlens = [i/8 + g.aesctr_iv_len + g.salt_len + z for i in g.seed_lens]
# New fmt: [56, 64, 72]. Old fmt: [48, 56, 64].
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]))
)
vn = [i/8 + g.aesctr_iv_len + g.salt_len + 8 for i in g.seed_lens]
if len(d) in vn:
msg("Re-run the program without the '--old-incog-fmt' option")
sys.exit()
else: 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)))
incog_id = make_iv_chksum(iv)
msg("Incog ID: %s (IV ID: %s)" % (incog_id,make_chksum_8(iv)))
qmsg("Check the applicable value against your records.")
vmsg(cmessages['incog_iv_id_hidden' if "from_incog_hidden" in opts
vmsg(crmsg['incog_iv_id_hidden' if "from_incog_hidden" in opts
else 'incog_iv_id'])
passwd = get_mmgen_passphrase(prompt_what,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)
passwd = get_mmgen_passphrase(prompt_info+" "+incog_id,opts)
# 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)
qmsg("Configured hash presets: %s" % " ".join(sorted(g.hash_presets)))
hp = get_hash_preset_from_user(what="incog wallet")
salt,enc_seed = d[0:g.salt_len], d[g.salt_len:]
# 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")
key = make_key(passwd, salt, hp, "main key")
vmsg("Key ID: %s" % make_chksum_8(key))
salt,enc_seed = d[0:g.salt_len], d[g.salt_len:]
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'])
key = make_key(passwd, salt, hp, "main key")
vmsg("Key ID: %s" % make_chksum_8(key))
seed = decrypt_seed(enc_seed, key, "", "")
old_fmt_sid = make_chksum_8(seed)
def confirm_correct_seed_id(sid):
m = "Seed ID: %s. Is the Seed ID correct?" % sid
return keypress_confirm(m, True)
if 'old_incog_fmt' in opts:
if confirm_correct_seed_id(old_fmt_sid):
break
else:
chk,seed_maybe = seed[:8],seed[8:]
if sha256(seed_maybe).digest()[:8] == chk:
msg("Passphrase and hash preset are correct")
seed = seed_maybe
break
elif confirm_old_format():
if confirm_correct_seed_id(old_fmt_sid):
break
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("{} wallet".format(g.proj_name),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):
def _get_seed(infile,opts,silent=False,seed_id=""):
ext = get_extension(infile)
@ -313,8 +368,9 @@ def get_seed(infile,opts,silent=False):
else: msg("No seed source type specified and no file supplied")
sys.exit(2)
seed_id_str = " for seed ID "+seed_id if seed_id else ""
if source == "mnemonic":
prompt = "Enter mnemonic: "
prompt = "Enter mnemonic%s: " % seed_id_str
words = get_words(infile,"mnemonic data",prompt,opts)
wl = get_default_wordlist()
from mmgen.mnemonic import get_seed_from_mnemonic
@ -323,17 +379,17 @@ def get_seed(infile,opts,silent=False):
if 'from_brain' not in opts:
msg("'--from-brain' parameters must be specified for brainwallet file")
sys.exit(2)
prompt = "Enter brainwallet passphrase: "
prompt = "Enter brainwallet passphrase%s: " % seed_id_str
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
prompt = "Enter seed%s in %s format: " % (seed_id_str,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
h = ext == g.incog_hex_ext or 'from_incog_hex' in opts
seed = get_seed_from_incog_wallet(infile, opts, silent=silent, hex_input=h)
@ -348,10 +404,10 @@ def get_seed(infile,opts,silent=False):
# Repeat if entered data is invalid
def get_seed_retry(infile,opts):
def get_seed_retry(infile,opts,seed_id=""):
silent = False
while True:
seed = get_seed(infile,opts,silent=silent)
seed = _get_seed(infile,opts,silent=silent,seed_id=seed_id)
silent = True
if seed: return seed
@ -385,7 +441,7 @@ def mmgen_encrypt(data,what="data",hash_preset='',opts={}):
return salt+iv+enc_d
def mmgen_decrypt(data,what="data",hash_preset='',opts={}):
def mmgen_decrypt(data,what="data",hash_preset=""):
dstart = salt_len + g.aesctr_iv_len
salt,iv,enc_d = data[:salt_len],data[salt_len:dstart],data[dstart:]
vmsg("Preparing to decrypt %s" % what)
@ -396,8 +452,14 @@ def mmgen_decrypt(data,what="data",hash_preset='',opts={}):
key = make_key(passwd, salt, hp)
dec_d = decrypt_data(enc_d, key, int(hexlify(iv),16), what)
if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
vmsg("Success. Passphrase and hash preset are correct")
vmsg("OK")
return dec_d[sha256_len+nonce_len:]
else:
msg("Incorrect passphrase or hash preset")
return False
def mmgen_decrypt_retry(d,what="data"):
while True:
d_dec = mmgen_decrypt(d,what)
if d_dec: return d_dec
msg("Trying again...")

View file

@ -41,3 +41,7 @@ def main(progname):
sys.stderr.write("\nUser interrupt\n")
termios.tcsetattr(fd, termios.TCSADRAIN, old)
sys.exit(1)
except EOFError:
sys.stderr.write("\nEnd of file\n")
termios.tcsetattr(fd, termios.TCSADRAIN, old)
sys.exit(1)

View file

@ -42,10 +42,10 @@ help_data = {
-h, --help Print this help message{}
-d, --outdir= d Specify an alternate directory 'd' for output
-c, --save-checksum Save address list checksum to file
-e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry{}
-e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry
-H, --show-hash-presets Show information on available hash presets
-K, --no-keyconv Use internal libraries for address generation
instead of 'keyconv'
-K, --no-keyconv Force use of internal libraries for address gener-
ation, even if 'keyconv' is available
-l, --seed-len= N Length of seed. Options: {seed_lens}
(default: {g.seed_len})
-p, --hash-preset= p Use scrypt.hash() parameters from preset 'p' when
@ -63,17 +63,16 @@ help_data = {
-X, --from-incog-hex Generate {what} from incognito hexadecimal wallet
-G, --from-incog-hidden=f,o,l Generate {what} from incognito data in file
'f' at offset 'o', with seed length of 'l'
-o, --old-incog-fmt Use old (pre-0.7.8) incog format
-m, --from-mnemonic Generate {what} from an electrum-like mnemonic
-s, --from-seed Generate {what} from a seed in .{g.seed_ext} format
""".format(
*(
(
(
"\n-A, --no-addresses Print only secret keys, no addresses",
"\n-f, --flat-list Produce a flat list of keys suitable for use with" +
"\n '{}-txsign'".format(g.proj_name.lower()),
"\n-x, --b16 Print secret keys in hexadecimal too"
)
if what == "keys" else ("","","")),
if what == "keys" else ("","")),
seed_lens=", ".join([str(i) for i in g.seed_lens]),
what=what, g=g
),
@ -109,6 +108,13 @@ invocations with that passphrase
if what == "keys" else "")
}
wmsg = {
'unencrypted_secret_keys': """
This program generates secret keys from your {} seed, outputting them in
UNENCRYPTED form. Generate only the key(s) you need and guard them carefully.
""".format(g.proj_name),
}
opts,cmd_args = parse_opts(sys.argv,help_data)
if 'show_hash_presets' in opts: show_hash_presets()
@ -129,7 +135,7 @@ elif len(cmd_args) == 2:
check_infile(infile)
else: usage(help_data)
addr_idxs = parse_address_list(addr_idx_arg)
addr_idxs = parse_addr_idxs(addr_idx_arg)
if not addr_idxs: sys.exit(2)
@ -137,38 +143,44 @@ do_license_msg()
# Interact with user:
if what == "keys" and not g.quiet:
confirm_or_exit(cmessages['unencrypted_secret_keys'], 'continue')
confirm_or_exit(wmsg['unencrypted_secret_keys'], 'continue')
# Generate data:
seed = get_seed_retry(infile,opts)
seed_id = make_chksum_8(seed)
for l in (
('flat_list', 'no_addresses'),
('flat_list', 'b16'),
): warn_incompatible_opts(opts,l)
opts['gen_what'] = "a" if what == "addresses" else (
"k" if 'no_addresses' in opts else "ka")
opts['gen_what'] = \
["addrs"] if what == "addresses" else (
["keys"] if 'no_addresses' in opts else ["addrs","keys"])
addr_data = generate_addrs(seed, addr_idxs, opts)
addr_data_chksum = make_addr_data_chksum([(a.num,a.addr)
for a in addr_data]) if 'addrs' in opts['gen_what'] else ""
addr_data_str = format_addr_data(
addr_data = generate_addrs(seed, addr_idxs, opts)
if 'a' in opts['gen_what']:
if 'k' in opts['gen_what']:
def l(a): return ( a.num, (a.addr,"",a.wif) )
keys = True
else:
def l(a): return ( a.num, (a.addr,) )
keys = False
addr_data_chksum = make_addr_data_chksum([l(a) for a in addr_data],keys)
else:
addr_data_chksum = ""
addr_data_str = format_addr_data(
addr_data, addr_data_chksum, seed_id, addr_idxs, opts)
outfile_base = "{}[{}]".format(seed_id, fmt_addr_idxs(addr_idxs))
if 'addrs' in opts['gen_what']:
qmsg("Checksum for address data %s: %s" % (outfile_base,addr_data_chksum))
if 'a' in opts['gen_what']:
w = "key-address" if 'k' in opts['gen_what'] else "address"
qmsg("Checksum for %s data %s: %s" % (w,outfile_base,addr_data_chksum))
if 'save_checksum' in opts:
write_to_file(outfile_base+"."+g.addrfile_chksum_ext,
addr_data_chksum+"\n",opts,"address data checksum",True,True,False)
addr_data_chksum+"\n",opts,"%s data checksum" % w,True,True,False)
else:
qmsg("This checksum will be used to verify the address file in the future.")
qmsg("This checksum will be used to verify the %s file in the future."%w)
qmsg("Record it to a safe location.")
if 'flat_list' in opts and keypress_confirm("Encrypt key list?"):
if 'k' in opts['gen_what'] and keypress_confirm("Encrypt key list?"):
addr_data_str = mmgen_encrypt(addr_data_str,"key list","",opts)
enc_ext = "." + g.mmenc_ext
else: enc_ext = ""
@ -178,13 +190,11 @@ if 'stdout' in opts or not sys.stdout.isatty():
if enc_ext and sys.stdout.isatty():
msg("Cannot write encrypted data to screen. Exiting")
sys.exit(2)
c = True if (what == "keys" and not g.quiet and sys.stdout.isatty()) else False
write_to_stdout(addr_data_str,what,c)
write_to_stdout(addr_data_str,what,
(what=="keys"and not g.quiet and sys.stdout.isatty()))
else:
confirm_overwrite = False if g.quiet else True
outfile = "%s.%s%s" % (outfile_base, (
g.keylist_ext if 'flat_list' in opts else (
g.keyfile_ext if opts['gen_what'] == ["keys"] else (
g.addrfile_ext if opts['gen_what'] == ["addrs"] else "akeys"))), enc_ext)
write_to_file(outfile,addr_data_str,opts,what,confirm_overwrite,True)
g.keyaddrfile_ext if "ka" in opts['gen_what'] else (
g.keyfile_ext if "k" in opts['gen_what'] else
g.addrfile_ext)), enc_ext)
write_to_file(outfile,addr_data_str,opts,what,not g.quiet,True)

View file

@ -24,7 +24,7 @@ import sys
from mmgen.Opts import *
from mmgen.license import *
from mmgen.util import *
from mmgen.tx import connect_to_bitcoind,parse_addrs_file
from mmgen.tx import connect_to_bitcoind,parse_addrfile,parse_keyaddr_file
help_data = {
'prog_name': g.prog_name,
@ -32,38 +32,44 @@ help_data = {
watching wallet""".format(pnm=g.proj_name),
'usage':"[opts] [mmgen address file]",
'options': """
-h, --help Print this help message
-l, --addrlist= f Import the non-mmgen Bitcoin addresses listed in file 'f'
-q, --quiet Suppress warnings
-r, --rescan Rescan the blockchain. Required if address to import is
on the blockchain and has a balance. Rescanning is slow.
-h, --help Print this help message
-l, --addrlist Address source is a flat list of addresses
-k, --keyaddr-file Address source is a key-address file
-q, --quiet Suppress warnings
-r, --rescan Rescan the blockchain. Required if address to import is
on the blockchain and has a balance. Rescanning is slow.
"""
}
opts,cmd_args = parse_opts(sys.argv,help_data)
if len(cmd_args) != 1 and not 'addrlist' in opts:
msg("You must specify an mmgen address list (and/or non-mmgen addresses with the '--addrlist' option)")
sys.exit(1)
if cmd_args:
check_infile(cmd_args[0])
seed_id,addr_data = parse_addrs_file(cmd_args[0])
if len(cmd_args) == 1:
infile = cmd_args[0]
check_infile(infile)
if 'addrlist' in opts:
lines = get_lines_from_file(infile,"non-{} addresses".format(g.proj_name),
trim_comments=True)
addr_list = [(None,l) for l in lines]
seed_id = ""
else:
addr_data = {}
pf = parse_keyaddr_file if 'keyaddr_file' in opts else parse_addrfile
pf(infile,addr_data)
seed_id = addr_data.keys()[0]
e = addr_data[seed_id]
addr_list = [(k,e[k][0],e[k][1]) for k in e.keys()]
else:
seed_id,addr_data = "",[]
if 'addrlist' in opts:
lines = get_lines_from_file(opts['addrlist'],"non-mmgen addresses",
trim_comments=True)
addr_data += [(None,l) for l in lines]
msg_r("You must specify an mmgen address list (or a list of ")
msg("non-%s addresses with\nthe '--addrlist' option)" % g.proj_name)
sys.exit(1)
from mmgen.bitcoin import verify_addr
qmsg_r("Validating addresses...")
for i in addr_data:
for n,i in enumerate(addr_list,1):
if not verify_addr(i[1],verbose=True):
msg("%s: invalid address" % i)
sys.exit(2)
qmsg("OK")
qmsg("OK. %s addresses%s" % (n," from seed ID "+seed_id if seed_id else ""))
import mmgen.config as g
g.http_timeout = 3600
@ -94,8 +100,9 @@ def import_address(addr,label,rescan):
err_flag = True
w1 = len(str(len(addr_data))) * 2 + 2
w2 = len(str(max([i[0] for i in addr_data if i[0]]))) + 12
w1 = len(str(len(addr_list))) * 2 + 2
w2 = "" if 'addrlist' in opts else \
len(str(max([i[0] for i in addr_list if i[0]]))) + 12 \
if "rescan" in opts:
import threading
@ -105,10 +112,9 @@ else:
msg_fmt = "\r%-" + str(w1) + "s %-34s %-" + str(w2) + "s"
msg("Importing addresses")
for n,i in enumerate(addr_data):
for n,i in enumerate(addr_list):
if i[0]:
comment = " " + i[2] if len(i) == 3 else ""
label = "%s:%s%s" % (seed_id,i[0],comment)
label = "%s:%s%s" % (seed_id,i[0], (" "+i[2] if i[2] else ""))
else: label = "non-mmgen"
if "rescan" in opts:
@ -123,7 +129,7 @@ for n,i in enumerate(addr_data):
elapsed = int(time.time() - start)
msg_r(msg_fmt % (
secs_to_hms(elapsed),
("%s/%s:" % (n+1,len(addr_data))),
("%s/%s:" % (n+1,len(addr_list))),
i[1], "(" + label + ")"
)
)
@ -134,7 +140,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))),
msg_r(msg_fmt % (("%s/%s:" % (n+1,len(addr_list))),
i[1], "(" + label + ")"))
if err_flag: msg("\nImport failed"); sys.exit(2)
msg(" - OK")

View file

@ -1671,7 +1671,7 @@ data = "\n".join(data) + "\n"
# Output data
if 'stdout' in opts or not sys.stdout.isatty():
c = False if ('addrs' in opts or not sys.stdout.isatty()) else True
write_to_stdout(data,"secret keys",c)
conf = not ('addrs' in opts or not sys.stdout.isatty())
write_to_stdout(data,"secret keys",conf)
else:
write_walletdat_dump_to_file(wallet_id, data, len_arg, ext, what, opts)

View file

@ -28,7 +28,6 @@ import mmgen.config as g
from mmgen.Opts import *
from mmgen.license import *
from mmgen.tx import *
from mmgen.util import msg, msg_r, keypress_confirm
help_data = {
'prog_name': g.prog_name,
@ -43,6 +42,7 @@ help_data = {
-i, --info Display unspent outputs and exit
-q, --quiet Suppress warnings; overwrite files without
prompting
-v, --verbose Produce more verbose output
""".format(g=g),
'notes': """
@ -60,40 +60,300 @@ with no amount on the command line.
""".format(g=g,pnm=g.proj_name)
}
wmsg = {
'too_many_acct_addresses': """
ERROR: More than one address found for account: "%s".
Your "wallet.dat" file appears to have been altered by a non-{pnm} program.
Please restore your tracking wallet from a backup or create a new one and
re-import your addresses.
""".strip().format(pnm=g.proj_name),
'addr_in_addrfile_only': """
Warning: output address {mmgenaddr} is not in the tracking wallet, which means
its balance will not be tracked. You're strongly advised to import the address
into your tracking wallet before broadcasting this transaction.
""".strip(),
'addr_not_found': """
No data for MMgen address {mmgenaddr} could be found in either the tracking
wallet or the supplied address file. Please import this address into your
tracking wallet, or supply an address file for it on the command line.
""".strip(),
'addr_not_found_no_addrfile': """
No data for MMgen address {mmgenaddr} could be found in the tracking wallet.
Please import this address into your tracking wallet or supply an address file
for it on the command line.
""".strip(),
'no_spendable_outputs': """
No spendable outputs found! Import addresses with balances into your
watch-only wallet using '{pnm}-addrimport' and then re-run this program.
""".strip().format(pnm=g.proj_name.lower()),
'mixed_inputs': """
NOTE: This transaction uses a mixture of both mmgen and non-mmgen inputs, which
makes the signing process more complicated. When signing the transaction, keys
for the non-{pnm} inputs must be supplied to '{pnl}-txsign' in a file with the
'--keys-from-file' option.
Selected mmgen inputs: %s
""".strip().format(pnm=g.proj_name,pnl=g.proj_name.lower()),
'not_enough_btc': """
Not enough BTC in the inputs for this transaction (%s BTC)
""".strip(),
'throwaway_change': """
ERROR: This transaction produces change (%s BTC); however, no change address
was specified.
""".strip(),
}
def format_unspent_outputs_for_printing(out,sort_info,total):
pfs = " %-4s %-67s %-34s %-12s %-13s %-8s %-10s %s"
pout = [pfs % ("Num","TX id,Vout","Address","MMgen ID",
"Amount (BTC)","Conf.","Age (days)", "Comment")]
for n,i in enumerate(out):
addr = "=" if i.skip == "addr" and "grouped" in sort_info else i.address
tx = " " * 63 + "=" \
if i.skip == "txid" and "grouped" in sort_info else str(i.txid)
s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr,
i.mmid,i.amt,i.confirmations,i.days,i.label)
pout.append(s.rstrip())
return \
"Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n".format(
make_timestr(), " ".join(sort_info), "\n".join(pout), total
)
def sort_and_view(unspent,opts):
def s_amt(i): return i.amount
def s_txid(i): return "%s %03s" % (i.txid,i.vout)
def s_addr(i): return i.address
def s_age(i): return i.confirmations
def s_mmgen(i):
m = parse_mmgen_label(i.account)[0]
if m: return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *m.split(":"))
else: return "G" + i.account
sort,group,show_days,show_mmaddr,reverse = "age",False,False,True,True
unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default
total = trim_exponent(sum([i.amount for i in unspent]))
max_acct_len = max([len(i.account) for i in unspent])
hdr_fmt = "UNSPENT OUTPUTS (sort order: %s) Total BTC: %s"
options_msg = """
Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
""".strip()
prompt = \
"('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
from copy import deepcopy
from mmgen.term import get_terminal_size
write_to_file_msg = ""
msg("")
while True:
cols = get_terminal_size()[0]
if cols < g.min_screen_width:
msg("%s-txcreate requires a screen at least %s characters wide" %
(g.proj_name.lower(),g.min_screen_width))
sys.exit(2)
addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46)
acct_w = min(max_acct_len, max(24,int(addr_w-10)))
btaddr_w = addr_w - acct_w - 1
tx_w = max(11,min(64, cols-addr_w-32))
txdots = "..." if tx_w < 64 else ""
fs = " %-4s %-" + str(tx_w) + "s %-2s %-" + str(addr_w) + "s %-13s %-s"
table_hdr = fs % ("Num","TX id Vout","","Address","Amount (BTC)",
"Age(d)" if show_days else "Conf.")
unsp = deepcopy(unspent)
for i in unsp: i.skip = ""
if group and (sort == "address" or sort == "txid"):
for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
if sort == "address" and a.address == b.address: b.skip = "addr"
elif sort == "txid" and a.txid == b.txid: b.skip = "txid"
for i in unsp:
amt = str(trim_exponent(i.amount))
lfill = 3 - len(amt.split(".")[0]) if "." in amt else 3 - len(amt)
i.amt = " "*lfill + amt
i.days = int(i.confirmations * g.mins_per_block / (60*24))
i.age = i.days if show_days else i.confirmations
i.mmid,i.label = parse_mmgen_label(i.account)
if i.skip == "addr":
i.addr = "|" + "." * 33
else:
if show_mmaddr:
dots = ".." if btaddr_w < len(i.address) else ""
i.addr = "%s%s %s" % (
i.address[:btaddr_w-len(dots)],
dots,
i.account[:acct_w])
else:
i.addr = i.address
i.tx = " " * (tx_w-4) + "|..." if i.skip == "txid" \
else i.txid[:tx_w-len(txdots)]+txdots
sort_info = ["reverse"] if reverse else []
sort_info.append(sort if sort else "unsorted")
if group and (sort == "address" or sort == "txid"):
sort_info.append("grouped")
out = [hdr_fmt % (" ".join(sort_info), total), table_hdr]
out += [fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,i.age)
for n,i in enumerate(unsp)]
msg("\n".join(out) +"\n\n" + write_to_file_msg + options_msg)
write_to_file_msg = ""
skip_prompt = False
while True:
reply = get_char(prompt, immed_chars="atDdAMrgmeqpvw")
if reply == 'a': unspent.sort(key=s_amt); sort = "amount"
elif reply == 't': unspent.sort(key=s_txid); sort = "txid"
elif reply == 'D': show_days = not show_days
elif reply == 'd': unspent.sort(key=s_addr); sort = "address"
elif reply == 'A': unspent.sort(key=s_age); sort = "age"
elif reply == 'M':
unspent.sort(key=s_mmgen); sort = "mmgen"
show_mmaddr = True
elif reply == 'r':
unspent.reverse()
reverse = not reverse
elif reply == 'g': group = not group
elif reply == 'm': show_mmaddr = not show_mmaddr
elif reply == 'e': pass
elif reply == 'q': pass
elif reply == 'p':
d = format_unspent_outputs_for_printing(unsp,sort_info,total)
of = "listunspent[%s].out" % ",".join(sort_info)
write_to_file(of, d, opts,"",False,False)
write_to_file_msg = "Data written to '%s'\n\n" % of
elif reply == 'v':
do_pager("\n".join(out))
continue
elif reply == 'w':
data = format_unspent_outputs_for_printing(unsp,sort_info,total)
do_pager(data)
continue
else:
msg("\nInvalid input")
continue
break
msg("\n")
if reply == 'q': break
return tuple(unspent)
def select_outputs(unspent,prompt):
while True:
reply = my_raw_input(prompt).strip()
if not reply: continue
selected = parse_addr_idxs(reply,sep=None)
if not selected: continue
if selected[-1] > len(unspent):
msg("Inputs must be less than %s" % len(unspent))
continue
return selected
def get_acct_data_from_wallet(c,acct_data):
# acct_data is global object initialized by caller
vmsg_r("Getting account data from wallet...")
accts,i = c.listaccounts(minconf=0,includeWatchonly=True),0
for acct in accts:
ma,comment = parse_mmgen_label(acct)
if ma:
i += 1
addrlist = c.getaddressesbyaccount(acct)
if len(addrlist) != 1:
msg(wmsg['too_many_acct_addresses'] % acct)
sys.exit(2)
seed_id,idx = ma.split(":")
if seed_id not in acct_data:
acct_data[seed_id] = {}
acct_data[seed_id][idx] = (addrlist[0],comment)
vmsg("%s %s addresses found, %s accounts total" % (i,g.proj_name,len(accts)))
def mmaddr2btcaddr_unspent(unspent,mmaddr):
vmsg_r("Searching for {g.proj_name} address {m} in wallet...".format(g=g,m=mmaddr))
m = [u for u in unspent if u.account.split()[0] == mmaddr]
if len(m) == 0:
vmsg("not found")
return "",""
elif len(m) > 1:
msg(wmsg['too_many_acct_addresses'] % acct); sys.exit(2)
else:
vmsg("success (%s)" % m[0].address)
return m[0].address, split2(m[0].account)[1]
sys.exit()
def mmaddr2btcaddr(c,mmaddr,acct_data,addr_data,b2m_map):
# assume mmaddr has already been checked
if not acct_data: get_acct_data_from_wallet(c,acct_data)
btcaddr,comment = mmaddr2btcaddr_addrdata(mmaddr,acct_data,"wallet")
# btcaddr,comment = mmaddr2btcaddr_unspent(us,mmaddr)
if not btcaddr:
if addr_data:
btcaddr,comment = mmaddr2btcaddr_addrdata(mmaddr,addr_data,"addr file")
if btcaddr:
msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr))
if not keypress_confirm("Continue anyway?"):
sys.exit(1)
else:
msg(wmsg['addr_not_found'].format(mmgenaddr=mmaddr))
sys.exit(2)
else:
msg(wmsg['addr_not_found_no_addrfile'].format(mmgenaddr=mmaddr))
sys.exit(2)
b2m_map[btcaddr] = mmaddr,comment
return btcaddr
opts,cmd_args = parse_opts(sys.argv,help_data)
if g.debug: show_opts_and_cmd_args(opts,cmd_args)
if 'comment_file' in opts:
comment = get_tx_comment_from_file(opts['comment_file'])
c = connect_to_bitcoind()
if not 'info' in opts:
do_license_msg(immed=True)
tx_out,addr_data,b2m_map,acct_data,change_addr = {},[],{},[],""
tx_out,addr_data,b2m_map,acct_data,change_addr = {},{},{},{},""
addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext]
cmd_args = set(cmd_args) - set(addrfiles)
for a in addrfiles:
check_infile(a)
addr_data.append(parse_addrs_file(a))
def mmaddr2btcaddr(c,mmaddr,acct_data,addr_data,b2m_map):
# assume mmaddr has already been checked
btcaddr,label = mmaddr2btcaddr_bitcoind(c,mmaddr,acct_data)
if not btcaddr:
if addr_data:
btcaddr,label = mmaddr2btcaddr_addrfile(mmaddr,addr_data)
else:
msg(txmsg['addrfile_no_data_msg'] % mmaddr)
sys.exit(2)
b2m_map[btcaddr] = mmaddr,label
return btcaddr
parse_addrfile(a,addr_data)
for a in cmd_args:
if "," in a:
a1,a2 = a.split(",")
a1,a2 = split2(a,",")
if is_btc_addr(a1):
btcaddr = a1
elif is_mmgen_addr(a1):
@ -131,16 +391,13 @@ if not 'info' in opts:
if g.debug: show_opts_and_cmd_args(opts,cmd_args)
#write_to_file("bogus_unspent.json", repr(us), opts); sys.exit()
#if False:
if g.bogus_wallet_data:
import mmgen.rpc
if g.bogus_wallet_data: # for debugging purposes only
us = eval(get_data_from_file(g.bogus_wallet_data))
else:
us = c.listunspent()
# write_to_file("bogus_unspent.json", repr(us), opts); sys.exit()
if not us: msg(txmsg['no_spendable_outputs']); sys.exit(2)
if not us: msg(wmsg['no_spendable_outputs']); sys.exit(2)
unspent = sort_and_view(us,opts)
@ -165,7 +422,7 @@ while True:
mmaddrs.discard("")
if mmaddrs and len(mmaddrs) < len(sel_unspent):
msg(txmsg['mixed_inputs'] % ", ".join(sorted(mmaddrs)))
msg(wmsg['mixed_inputs'] % ", ".join(sorted(mmaddrs)))
if not keypress_confirm("Accept?"):
continue
@ -177,10 +434,10 @@ while True:
if keypress_confirm(prompt,default_yes=True):
break
else:
msg(txmsg['not_enough_btc'] % change)
msg(wmsg['not_enough_btc'] % change)
if change > 0 and not change_addr:
msg(txmsg['throwaway_change'] % change)
msg(wmsg['throwaway_change'] % change)
sys.exit(2)
if change_addr in tx_out and not change:
@ -198,8 +455,6 @@ if g.debug:
print "tx_out:", repr(tx_out)
if 'comment_file' in opts:
comment = get_tx_comment_from_file(opts['comment_file'])
if comment == False: sys.exit(2)
if keypress_confirm("Edit comment?",False):
comment = get_tx_comment_from_user(comment)
else:
@ -219,7 +474,7 @@ metadata = tx_id, amt, make_timestamp()
if reply and reply in "YyVv":
view_tx_data(c,[i.__dict__ for i in sel_unspent],tx_hex,b2m_map,
comment,metadata,True if reply in "Vv" else False)
comment,metadata,reply in "Vv")
prompt = "Save transaction?"
if keypress_confirm(prompt,default_yes=True):

View file

@ -51,7 +51,7 @@ do_license_msg()
tx_data = get_lines_from_file(infile,"signed transaction data")
metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_data(tx_data,infile)
metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,infile)
qmsg("Signed transaction file '%s' is valid" % infile)
@ -60,8 +60,7 @@ c = connect_to_bitcoind()
prompt = "View transaction data? (y)es, (N)o, (v)iew in pager"
reply = prompt_and_get_char(prompt,"YyNnVv",enter_ok=True)
if reply and reply in "YyVv":
view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,
pager=True if reply in "Vv" else False)
view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,reply in "Vv")
if keypress_confirm("Edit transaction comment?"):
comment = get_tx_comment_from_user(comment)

View file

@ -26,7 +26,6 @@ import mmgen.config as g
from mmgen.Opts import *
from mmgen.license import *
from mmgen.tx import *
from mmgen.util import msg,qmsg
help_data = {
'prog_name': g.prog_name,
@ -39,16 +38,18 @@ help_data = {
-i, --info Display information about the transaction and exit
-I, --tx-id Display transaction ID and exit
-k, --keys-from-file= f Provide additional keys for non-{pnm} addresses
-K, --all-keys-from-file=f Like '-k', only use the keyfile as key source
for ALL inputs, including {pnm} ones. Can be used
for online signing without an {pnm} seed source.
{pnm}-to-BTC mappings can optionally be verified
using address file(s) listed on the command line
-K, --no-keyconv Force use of internal libraries for address gener-
ation, even if 'keyconv' is available
-M, --mmgen-keys-from-file=f Provide keys for {pnm} addresses in a key-
address file (output of '{pnl}-keygen'). Permits
online signing without an {pnm} seed source.
The key-address file is also used to verify
{pnm}-to-BTC mappings, so its checksum should
be recorded by the user.
-P, --passwd-file= f Get MMGen wallet or bitcoind passphrase from file 'f'
-q, --quiet Suppress warnings; overwrite files without
prompting
-v, --verbose Produce more verbose output
-V, --skip-key-preverify Skip optional key pre-verification step
-b, --from-brain= l,p Generate keys from a user-created password,
i.e. a "brainwallet", using seed length 'l' and
hash preset 'p'
@ -57,9 +58,10 @@ help_data = {
-X, --from-incog-hex Generate keys from an incognito hexadecimal wallet
-G, --from-incog-hidden= f,o,l Generate keys from incognito data in file
'f' at offset 'o', with seed length of 'l'
-o, --old-incog-fmt Use old (pre-0.7.8) incog format
-m, --from-mnemonic Generate keys from an electrum-like mnemonic
-s, --from-seed Generate keys from a seed in .{g.seed_ext} format
""".format(g=g,pnm=g.proj_name),
""".format(g=g,pnm=g.proj_name,pnl=g.proj_name.lower()),
'notes': """
Transactions with either {pnm} or non-{pnm} input addresses may be signed.
@ -89,17 +91,204 @@ Seed data supplied in files must have the following extensions:
""".format(g=g,pnm=g.proj_name,pnl=g.proj_name.lower())
}
wmsg = {
'mm2btc_mapping_error': """
MMGen -> BTC address mappings differ!
From %-18s %s -> %s
From %-18s %s -> %s
""".strip(),
'removed_dups': """
Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
""".strip().format(pnm=g.proj_name),
}
def get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts):
if seed_id in saved_seeds.keys():
return saved_seeds[seed_id]
from mmgen.crypto import get_seed_retry
while True:
if infiles:
seed = get_seed_retry(infiles.pop(0),opts)
elif "from_brain" in opts or "from_mnemonic" in opts \
or "from_seed" in opts or "from_incog" in opts:
qmsg("Need seed data for seed ID %s" % seed_id)
seed = get_seed_retry("",opts,seed_id)
msg("User input produced seed ID %s" % make_chksum_8(seed))
else:
msg("ERROR: No seed source found for seed ID: %s" % seed_id)
sys.exit(2)
sid = make_chksum_8(seed)
saved_seeds[sid] = seed
if sid == seed_id: return seed
def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds,opts):
seed_ids = set([i[:8] for i in mmgen_addrs])
vmsg("Need seed%s: %s" % (suf(seed_ids,"k")," ".join(seed_ids)))
d = []
from mmgen.addr import generate_addrs
for seed_id in seed_ids:
# Returns only if seed is found
seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts)
addr_nums = [int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id]
# num sec wif addr
d += [("{}:{}".format(seed_id,r.num),r.addr,r.wif)
for r in generate_addrs(seed,addr_nums,{'gen_what':"ka"},seed_id)]
return d
def sign_transaction(c,tx_hex,tx_num_str,sig_data,keys=None):
if keys:
qmsg("Passing %s key%s to bitcoind" % (len(keys),suf(keys,"k")))
if g.debug: print "Keys:\n %s" % "\n ".join(keys)
msg_r("Signing transaction{}...".format(tx_num_str))
from mmgen.rpc import exceptions
try:
sig_tx = c.signrawtransaction(tx_hex,sig_data,keys)
except exceptions.InvalidAddressOrKey:
msg("failed\nInvalid address or key")
sys.exit(3)
return sig_tx
def sign_tx_with_bitcoind_wallet(c,tx_hex,tx_num_str,sig_data,keys,opts):
try:
sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys)
except:
from mmgen.rpc import exceptions
msg("Using keys in wallet.dat as per user request")
prompt = "Enter passphrase for bitcoind wallet: "
while True:
passwd = get_bitcoind_passphrase(prompt,opts)
try:
c.walletpassphrase(passwd, 9999)
except exceptions.WalletPassphraseIncorrect:
msg("Passphrase incorrect")
else:
msg("Passphrase OK"); break
sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys)
msg("Locking wallet")
try:
c.walletlock()
except:
msg("Failed to lock wallet")
return sig_tx
def check_maps_from_seeds(maplist,label,infiles,saved_seeds,opts,return_keys=False):
if not maplist: return []
qmsg("Checking MMGen -> BTC address mappings for %ss (from seeds)" % label)
d = get_keys_for_mmgen_addrs(maplist.keys(),infiles,saved_seeds,opts)
# 0=mmaddr 1=addr 2=wif
m = dict([(e[0],e[1]) for e in d])
for a,b in zip(sorted(m),sorted(maplist)):
if a != b:
al,bl = "generated seed:","tx file:"
msg(wmsg['mm2btc_mapping_error'] % (al,a,m[a],bl,b,maplist[b]))
sys.exit(3)
if return_keys:
ret = [e[2] for e in d]
vmsg("Added %s wif key%s from seeds" % (len(ret),suf(ret,"k")))
return ret
def missing_keys_errormsg(addrs):
print """
A key file must be supplied (or use the '--use-wallet-dat' option)
for the following non-{} address{}:\n {}""".format(
g.proj_name,suf(addrs,"a"),"\n ".join(addrs)).strip()
def parse_mmgen_keyaddr_file(opts):
adata = {}
parse_keyaddr_file(opts['mmgen_keys_from_file'],adata)
for sid in adata.keys(): # one seed id, one loop
idxs = adata[sid]
count = len(idxs.keys())
vmsg("Found %s wif key%s for seed ID %s" % (count,suf(count,"k"),sid))
# idx: (0=addr, 1=comment 2=wif) -> mmaddr: (0=addr, 1=wif)
return dict([("{}:{}".format(sid,k),(idxs[k][0],idxs[k][2]))
for k in idxs.keys()])
def parse_keylist(opts,from_file):
fn = opts['keys_from_file']
d = get_data_from_file(fn,"non-%s keylist" % g.proj_name)
enc_ext = get_extension(fn) == g.mmenc_ext
if enc_ext or not is_utf8(d):
if not enc_ext: qmsg("Keylist file appears to be encrypted")
from crypto import mmgen_decrypt_retry
d = mmgen_decrypt_retry(d,"encrypted keylist")
# Check for duplication with key-address file
keys_all = set(remove_comments(d.split("\n")))
d = from_file['mmdata']
kawifs = [d[k][1] for k in d.keys()]
keys = [k for k in keys_all if k not in kawifs]
removed = len(keys_all) - len(keys)
if removed: vmsg(wmsg['removed_dups'] % (removed,suf(removed,"k")))
addrs = []
wif2addr_f = get_wif2addr_f()
for n,k in enumerate(keys,1):
qmsg_r("\rGenerating addresses from keylist: %s/%s" % (n,len(keys)))
addrs.append(wif2addr_f(k))
qmsg("\rGenerated addresses from keylist: %s/%s " % (n,len(keys)))
return dict(zip(addrs,keys))
# Check inputs and outputs maps against key-address file, deleting entries:
def check_maps_from_kafile(imap,what,kadata,return_keys=False):
qmsg("Checking MMGen -> BTC address mappings for %ss (from key-address file)" % what)
ret = []
for k in imap.keys():
if k in kadata.keys():
if kadata[k][0] == imap[k]:
del imap[k]
ret += [kadata[k][1]]
else:
kl,il = "key-address file:","tx file:"
msg(wmsg['mm2btc_mapping_error']%(kl,k,kadata[k][0],il,k,imap[k]))
sys.exit(2)
if ret: vmsg("Removed %s address%s from %ss map" % (len(ret),suf(ret,"a"),what))
if return_keys:
vmsg("Added %s wif key%s from %ss map" % (len(ret),suf(ret,"k"),what))
return ret
def get_keys_from_keylist(kldata,other_addrs):
ret = []
for addr in other_addrs[:]:
if addr in kldata.keys():
ret += [kldata[addr]]
other_addrs.remove(addr)
vmsg("Added %s wif key%s from user-supplied keylist" %
(len(ret),suf(ret,"k")))
return ret
opts,infiles = parse_opts(sys.argv,help_data)
for l in (
('tx_id', 'info'),
('keys_from_file','all_keys_from_file')
('tx_id', 'info')
): warn_incompatible_opts(opts,l)
if 'from_incog_hex' in opts or 'from_incog_hidden' in opts:
opts['from_incog'] = True
if 'all_keys_from_file' in opts:
opts['keys_from_file'] = opts['all_keys_from_file']
if not infiles: usage(help_data)
for i in infiles: check_infile(i)
@ -108,24 +297,15 @@ c = connect_to_bitcoind()
saved_seeds = {}
tx_files = [i for i in set(infiles) if get_extension(i) == g.rawtx_ext]
addrfiles = [a for a in set(infiles) if get_extension(a) == g.addrfile_ext]
seed_files = list(set(infiles) - set(tx_files) - set(addrfiles))
seed_files = list(set(infiles) - set(tx_files))
if not "info" in opts: do_license_msg(immed=True)
from_file = { 'mmdata':{}, 'kldata':{} }
if 'mmgen_keys_from_file' in opts:
from_file['mmdata'] = parse_mmgen_keyaddr_file(opts) or {}
if 'keys_from_file' in opts:
fn = opts['keys_from_file']
d = get_data_from_file(fn,"keylist")
if get_extension(fn) == g.mmenc_ext or not \
is_btc_key(remove_comments(d.split("\n"))[0][:55]):
qmsg("Keylist appears to be encrypted")
from mmgen.crypto import mmgen_decrypt
while True:
d_dec = mmgen_decrypt(d,"encrypted keylist","",opts)
if d_dec: d = d_dec; break
msg("Trying again...")
keys_from_file = remove_comments(d.split("\n"))
else: keys_from_file = []
from_file['kldata'] = parse_keylist(opts,from_file) or {}
tx_num_str = ""
for tx_num,tx_file in enumerate(tx_files,1):
@ -136,7 +316,7 @@ for tx_num,tx_file in enumerate(tx_files,1):
m = "" if 'tx_id' in opts else "transaction data"
tx_data = get_lines_from_file(tx_file,m)
metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_data(tx_data,tx_file)
metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,tx_file)
qmsg("Successfully opened transaction file '%s'" % tx_file)
if 'tx_id' in opts:
@ -147,56 +327,47 @@ for tx_num,tx_file in enumerate(tx_files,1):
view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata)
sys.exit(0)
# Are inputs mmgen addresses?
mmgen_inputs = [i for i in inputs_data if parse_mmgen_label(i['account'])[0]]
other_inputs = [i for i in inputs_data if not parse_mmgen_label(i['account'])[0]]
if 'all_keys_from_file' in opts: other_inputs = inputs_data
keys = keys_from_file
if other_inputs and not keys and not 'use_wallet_dat' in opts:
missing_keys_errormsg(other_inputs)
sys.exit(2)
if other_inputs and keys and not 'skip_key_preverify' in opts:
addrs = [i['address'] for i in other_inputs]
mm_inputs = mmgen_inputs if 'all_keys_from_file' in opts else []
preverify_keys(addrs, keys, mm_inputs)
opts['skip_key_preverify'] = True
if 'all_keys_from_file' in opts:
if addrfiles:
check_mmgen_to_btc_addr_mappings_addrfile(mmgen_inputs,b2m_map,addrfiles)
else:
confirm_or_exit(txmsg['skip_mapping_checks_warning'],"continue")
else:
check_mmgen_to_btc_addr_mappings(
mmgen_inputs,b2m_map,seed_files,saved_seeds,opts)
p = "View data for transaction{}? (y)es, (N)o, (v)iew in pager"
reply = prompt_and_get_char(p.format(tx_num_str),"YyNnVv",enter_ok=True)
if reply and reply in "YyVv":
view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,
True if reply in "Vv" else False)
view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,reply in "Vv")
# Start
other_addrs = list(set([i['address'] for i in inputs_data
if not parse_mmgen_label(i['account'])[0]]))
keys = get_keys_from_keylist(from_file['kldata'],other_addrs)
if other_addrs and not 'use_wallet_dat' in opts:
missing_keys_errormsg(other_addrs)
sys.exit(2)
imap = dict([(i['account'].split()[0],i['address']) for i in inputs_data
if parse_mmgen_label(i['account'])[0]])
omap = dict([(j[0],i) for i,j in b2m_map.items()])
sids = set([i[:8] for i in imap.keys()])
keys += check_maps_from_kafile(imap,"input",from_file['mmdata'],True)
check_maps_from_kafile(omap,"output",from_file['mmdata'])
keys += check_maps_from_seeds(imap,"input",seed_files,saved_seeds,opts,True)
check_maps_from_seeds(omap,"output",seed_files,saved_seeds,opts)
extra_sids = set(saved_seeds.keys()) - sids
if extra_sids:
msg("Unused seed ID%s: %s" %
(suf(extra_sids,"k")," ".join(extra_sids)))
# Begin signing
sig_data = [
{"txid":i['txid'],"vout":i['vout'],"scriptPubKey":i['scriptPubKey']}
for i in inputs_data]
if mmgen_inputs and not 'all_keys_from_file' in opts:
ml = [i['account'].split()[0] for i in mmgen_inputs]
keys += get_keys_for_mmgen_addrs(ml,seed_files,saved_seeds,opts)
if 'use_wallet_dat' in opts:
sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,tx_num_str,sig_data,keys,opts)
else:
sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys)
elif other_inputs:
if keys:
sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys)
else:
sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,tx_num_str,sig_data,keys,opts)
if 'use_wallet_dat' in opts:
sig_tx = sign_tx_with_bitcoind_wallet(
c,tx_hex,tx_num_str,sig_data,keys,opts)
else:
sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys)
if sig_tx['complete']:
msg("OK")

View file

@ -25,7 +25,7 @@ import sys
import mmgen.config as g
from mmgen.Opts import *
from mmgen.util import *
from mmgen.crypto import get_seed_retry,wallet_to_incog_data
from mmgen.crypto import *
help_data = {
'prog_name': g.prog_name,
@ -47,6 +47,7 @@ help_data = {
-X, --export-incog-hex Export wallet to incognito hexadecimal format
-G, --export-incog-hidden=f,o Hide incognito data in existing file 'f'
at offset 'o' (comma-separated)
-o, --old-incog-fmt Use old (pre-0.7.8) incog format
-m, --export-mnemonic Export the wallet's mnemonic to file
-s, --export-seed Export the wallet's seed to file
""".format(g=g),
@ -60,6 +61,54 @@ to disable this option, then specify '-r0' on the command line.
"""
}
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]
while True:
passwd = get_mmgen_passphrase("{} wallet".format(g.proj_name),opts)
key = make_key(passwd, salt, preset, "main key")
seed = decrypt_seed(enc_seed, key, seed_id, key_id)
if seed: break
iv = get_random(g.aesctr_iv_len,opts)
iv_id = make_iv_chksum(iv)
msg("Incog ID: %s" % iv_id)
if not 'old_incog_fmt' in opts:
salt = get_random(g.salt_len,opts)
key = make_key(passwd, salt, preset, "incog wallet key")
key_id = make_chksum_8(key)
from hashlib import sha256
chk = sha256(seed).digest()[:8]
enc_seed = encrypt_data(chk+seed, key, 1, "seed")
# IV is used BOTH to initialize counter and to salt password!
key = make_key(passwd, iv, preset, "incog 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 export_to_hidden_incog(incog_enc,opts):
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(outfile,int(offset),len(incog_enc),"write")
if not g.quiet: confirm_or_exit("","alter file '%s'" % outfile)
import os
f = os.open(outfile,os.O_RDWR)
os.lseek(f, int(offset), os.SEEK_SET)
os.write(f, incog_enc)
os.close(f)
msg("Data written to file '%s' at offset %s" %
(os.path.relpath(outfile),offset))
opts,cmd_args = parse_opts(sys.argv,help_data)
if 'export_incog_hidden' in opts or 'export_incog_hex' in opts:
@ -82,7 +131,8 @@ elif 'export_incog' in opts:
if "export_incog_hidden" in opts:
export_to_hidden_incog(incog_enc,opts)
else:
seed_len = (len(incog_enc)-g.salt_len-g.aesctr_iv_len)*8
z = 0 if 'old_incog_fmt' in opts else 8
seed_len = (len(incog_enc)-g.salt_len-g.aesctr_iv_len-z)*8
fn = "%s-%s-%s[%s,%s].%s" % (
seed_id, key_id, iv_id, seed_len, preset,
g.incog_hex_ext if "export_incog_hex" in opts else g.incog_ext
@ -102,8 +152,7 @@ else:
if 'export_mnemonic' in opts:
wl = get_default_wordlist()
from mmgen.mnemonic import get_mnemonic_from_seed
p = True if g.debug else False
mn = get_mnemonic_from_seed(seed, wl, g.default_wl, print_info=p)
mn = get_mnemonic_from_seed(seed, wl, g.default_wl, g.debug)
fn = "%s.%s" % (make_chksum_8(seed).upper(), g.mn_ext)
write_to_file_or_stdout(fn, " ".join(mn)+"\n", opts, "mnemonic data")

View file

@ -55,6 +55,9 @@ help_data = {
i.e. a "brainwallet", using seed length 'l' and
hash preset 'p' (comma-separated)
-g, --from-incog Generate wallet from an incognito-format wallet
-G, --from-incog-hidden= f,o,l Generate keys from incognito data in file
'f' at offset 'o', with seed length of 'l'
-o, --old-incog-fmt Use old (pre-0.7.8) incog format
-m, --from-mnemonic Generate wallet from an Electrum-like mnemonic
-s, --from-seed Generate wallet from a seed in .{g.seed_ext} format
""".format(seed_lens=",".join([str(i) for i in g.seed_lens]), g=g),
@ -94,6 +97,30 @@ in all future invocations with that passphrase.
""".format(g=g)
}
wmsg = {
'choose_wallet_passphrase': """
You must choose a passphrase to encrypt the wallet with. A key will be
generated from your passphrase using a hash preset of '%s'. Please note that
no strength checking of passphrases is performed. For an empty passphrase,
just hit ENTER twice.
""".strip(),
'brain_warning': """
############################## EXPERTS ONLY! ##############################
A brainwallet will be secure only if you really know what you're doing and
have put much care into its creation. {} assumes no responsibility for
coins stolen as a result of a poorly crafted brainwallet passphrase.
A key will be generated from your passphrase using the parameters requested
by you: seed length {}, hash preset '{}'. For brainwallets it's highly
recommended to use one of the higher-numbered presets
Remember the seed length and hash preset parameters you've specified. To
generate the correct keys/addresses associated with this passphrase in the
future, you must continue using these same parameters
""",
}
opts,cmd_args = parse_opts(sys.argv,help_data)
if 'show_hash_presets' in opts: show_hash_presets()
@ -121,16 +148,17 @@ else: usage(help_data)
do_license_msg()
if 'from_brain' in opts and not g.quiet:
confirm_or_exit(cmessages['brain_warning'].format(
confirm_or_exit(wmsg['brain_warning'].format(
g.proj_name, *get_from_brain_opt_params(opts)),
"continue")
for i in 'from_mnemonic','from_brain','from_seed','from_incog':
if infile or (i in opts):
seed = get_seed_retry(infile,opts)
if "from_incog" in opts or get_extension(infile) == g.incog_ext:
qmsg(cmessages['incog'] % make_chksum_8(seed))
else: qmsg("")
# if "from_incog" in opts or get_extension(infile) == g.incog_ext:
# qmsg(cmessages['incog'] % make_chksum_8(seed))
# else: qmsg("")
qmsg("")
break
else:
# Truncate random data for smaller seed lengths
@ -138,7 +166,7 @@ else:
salt = sha256(get_random(128,opts)).digest()[:g.salt_len]
qmsg(cmessages['choose_wallet_passphrase'] % opts['hash_preset'])
qmsg(wmsg['choose_wallet_passphrase'] % opts['hash_preset'])
passwd = get_new_passphrase("new {} wallet".format(g.proj_name), opts)

View file

@ -17,9 +17,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
mnemonic.py: Mnemomic routines for the MMGen suite
mnemonic.py: Mnemonic routines for the MMGen suite
"""
import sys
from mmgen.util import msg,msg_r,make_chksum_8
import mmgen.config as g
wl_checksums = {
"electrum": '5ca31424',
"tirosh": '1a5faeff'
@ -27,70 +31,77 @@ wl_checksums = {
# These are the only base-1626 specific configs:
mn_base = 1626
def mn_fill(mn): return len(mn) * 8 / 3
def mn_len(hexnum): return len(hexnum) * 3 / 8
def mn2hex_pad(mn): return len(mn) * 8 / 3
def hex2mn_pad(hexnum): return len(hexnum) * 3 / 8
import sys
from mmgen.util import msg,make_chksum_8
import mmgen.config as g
# These universal base-conversion routines work for any base
def baseNtohex(base,words,wordlist,fill=0):
# Universal base-conversion routines:
def baseNtohex(base,words,wordlist,pad=0):
deconv = \
[wordlist.index(words[::-1][i])*(base**i) for i in range(len(words))]
return hex(sum(deconv))[2:].rstrip('L').zfill(fill)
return ("{:0%sx}"%pad).format(sum(deconv))
def hextobaseN(base,hexnum,wordlist,mn_len):
num = int(hexnum,16)
return [wordlist[num / (base**i) % base] for i in range(mn_len)][::-1]
def hextobaseN(base,hexnum,wordlist,pad=0):
num,ret = int(hexnum,16),[]
while num:
ret.append(num % base)
num /= base
return [wordlist[n] for n in [0] * (pad-len(ret)) + ret[::-1]]
def get_seed_from_mnemonic(mn,wl):
def get_seed_from_mnemonic(mn,wl,silent=False,label=""):
if len(mn) not in g.mnemonic_lens:
msg("Bad mnemonic (%i words). Allowed numbers of words: %s" %
(len(mn),", ".join([str(i) for i in g.mnemonic_lens])))
return False
if len(mn) not in g.mn_lens:
msg("Invalid mnemonic (%i words). Allowed numbers of words: %s" %
(len(mn),", ".join([str(i) for i in g.mn_lens])))
sys.exit(3)
for n,w in enumerate(mn,1):
if w not in wl:
msg("Bad mnemonic: word number %s is not in the wordlist" % n)
return False
msg("Invalid mnemonic: word #%s is not in the wordlist" % n)
sys.exit(3)
from binascii import unhexlify
seed = unhexlify(baseNtohex(mn_base,mn,wl,mn_fill(mn)))
msg("Valid mnemomic for seed ID %s" % make_chksum_8(seed))
hseed = baseNtohex(mn_base,mn,wl,mn2hex_pad(mn))
return seed
rev = hextobaseN(mn_base,hseed,wl,hex2mn_pad(hseed))
if rev != mn:
msg("ERROR: mnemonic recomputed from seed not the same as original")
msg("Recomputed mnemonic:\n%s" % " ".join(rev))
sys.exit(3)
if not silent:
msg("Valid mnemonic for seed ID %s" % make_chksum_8(unhexlify(hseed)))
return unhexlify(hseed)
def get_mnemonic_from_seed(seed, wl, label, print_info=False):
def get_mnemonic_from_seed(seed, wl, label="", verbose=False):
if len(seed)*8 not in g.seed_lens:
msg("%s: invalid seed length" % len(seed))
sys.exit(3)
from binascii import hexlify
if print_info:
msg("Wordlist: %s" % label.capitalize())
msg("Seed length: %s bits" % (len(seed) * 8))
msg("Seed: %s" % hexlify(seed))
hseed = hexlify(seed)
mn = hextobaseN(mn_base,hseed,wl,mn_len(hseed))
mn = hextobaseN(mn_base,hseed,wl,hex2mn_pad(hseed))
if print_info:
if verbose:
msg("Wordlist: %s" % label.capitalize())
msg("Seed length: %s bits" % (len(seed) * 8))
msg("Seed: %s" % hseed)
msg("mnemonic (%s words):\n%s" % (len(mn), " ".join(mn)))
if int(baseNtohex(mn_base,mn,wl,mn_fill(mn)),16) != int(hexlify(seed),16):
rev = baseNtohex(mn_base,mn,wl,mn2hex_pad(mn))
if rev != hseed:
msg("ERROR: seed recomputed from wordlist not the same as original seed!")
msg("Recomputed seed %s" % baseNtohex(mn_base,mn,wl,mn_fill(mn)))
msg("Original seed: %s" % hseed)
msg("Recomputed seed: %s" % rev)
sys.exit(3)
return mn
def check_wordlist(wl_str,label):
wl = wl_str.strip().split("\n")
def check_wordlist(wl,label):
print "Wordlist: %s" % label.capitalize()

View file

@ -22,26 +22,14 @@ bitcoin.py: Test suite for mmgen.bitcoin module
import mmgen.bitcoin as b
from mmgen.util import msg
from mmgen.tests.test import *
from binascii import hexlify, unhexlify
from binascii import hexlify
import sys
def b58_randenc():
r = get_random(24)
r_enc = b.b58encode(r)
print "Data (hex): %s" % hexlify(r)
print "Base 58: %s" % r_enc
r_dec = b.b58decode(r_enc)
print "Decoded data: %s" % hexlify(r_dec)
if r_dec != r:
print "ERROR! Decoded data doesn't match original"
sys.exit(9)
def keyconv_compare_randloop(loops, quiet=False):
for i in range(1,int(loops)+1):
wif = numtowif_rand(quiet=True)
wif = _numtowif_rand(quiet=True)
if not quiet: sys.stderr.write("-- %s --\n" % i)
ret = keyconv_compare(wif,quiet)
@ -55,7 +43,6 @@ def keyconv_compare_randloop(loops, quiet=False):
else:
print "%s iterations completed" % i
def keyconv_compare(wif,quiet=False):
do_msg = nomsg if quiet else msg
do_msg("WIF: %s" % wif)
@ -81,135 +68,24 @@ def keyconv_compare(wif,quiet=False):
def _do_hextowif(hex_in,quiet=False):
do_msg = nomsg if quiet else msg
do_msg("Input: %s" % hex_in)
wif = numtowif(int(hex_in,16))
wif = b.numtowif(int(hex_in,16))
do_msg("WIF encoded: %s" % wif)
wif_dec = wiftohex(wif)
wif_dec = b.wiftohex(wif)
do_msg("WIF decoded: %s" % wif_dec)
if hex_in != wif_dec:
print "ERROR! Decoded data doesn't match original data"
sys.exit(9)
return wif
def hextowiftopubkey(hex_in,quiet=False):
if len(hex_in) != 64:
print "Input must be a hex number 64 bits in length (%s input)" \
% len(hex_in)
sys.exit(2)
wif = _do_hextowif(hex_in,quiet=quiet)
keyconv_compare(wif)
def numtowif_rand(quiet=False):
def _numtowif_rand(quiet=False):
r_hex = hexlify(get_random(32))
return _do_hextowif(r_hex,quiet)
def strtob58(s,quiet=False):
print "Input: %s" % s
s_enc = b.b58encode(s)
print "Encoded data: %s" % s_enc
s_dec = b.b58decode(s_enc)
print "Decoded data: %s" % s_dec
test_equality(s,s_dec,[""],quiet)
def hextob58(s_in,f_enc=b.b58encode, f_dec=b.b58decode, quiet=False):
do_msg = nomsg if quiet else msg
do_msg("Input: %s" % s_in)
s_bin = unhexlify(s_in)
s_enc = f_enc(s_bin)
do_msg("Encoded data: %s" % s_enc)
s_dec = hexlify(f_dec(s_enc))
do_msg("Recoded data: %s" % s_dec)
test_equality(s_in,s_dec,["0"],quiet)
def b58tohex(s_in,f_dec=b.b58decode, f_enc=b.b58encode,quiet=False):
print "Input: %s" % s_in
s_dec = f_dec(s_in)
print "Decoded data: %s" % hexlify(s_dec)
s_enc = f_enc(s_dec)
print "Recoded data: %s" % s_enc
test_equality(s_in,s_enc,["1"],quiet)
def hextob58_pad(s_in, quiet=False):
hextob58(s_in,f_enc=b.b58encode_pad, f_dec=b.b58decode_pad, quiet=quiet)
def b58tohex_pad(s_in, quiet=False):
b58tohex(s_in,f_dec=b.b58decode_pad, f_enc=b.b58encode_pad, quiet=quiet)
def hextob58_pad_randloop(loops, quiet=False):
for i in range(1,int(loops)+1):
r = hexlify(get_random(32))
hextob58(r,f_enc=b.b58encode_pad, f_dec=b.b58decode_pad, quiet=quiet)
if not quiet: print
if not i % 100 and quiet:
sys.stderr.write("\riteration: %i " % i)
sys.stderr.write("\r%s iterations completed\n" % i)
def test_wiftohex(s_in,f_dec=b.wiftohex,f_enc=b.numtowif):
print "Input: %s" % s_in
s_dec = f_dec(s_in)
print "Decoded data: %s" % s_dec
s_enc = f_enc(int(s_dec,16))
print "Recoded data: %s" % s_enc
def hextosha256(s_in):
print "Entered data: %s" % s_in
s_enc = sha256(unhexlify(s_in)).hexdigest()
print "Encoded data: %s" % s_enc
def pubhextoaddr(s_in):
print "Entered data: %s" % s_in
s_enc = b.pubhex2addr(s_in)
print "Encoded data: %s" % s_enc
def hextowif_comp(s_in):
print "Entered data: %s" % s_in
s_enc = b.hextowif(s_in,compressed=True)
print "Encoded data: %s" % s_enc
s_dec = b.wiftohex(s_enc,compressed=True)
print "Decoded data: %s" % s_dec
def wiftohex_comp(s_in):
print "Entered data: %s" % s_in
s_enc = b.wiftohex(s_in,compressed=True)
print "Encoded data: %s" % s_enc
s_dec = b.hextowif(s_enc,compressed=True)
print "Decoded data: %s" % s_dec
def privhextoaddr_comp(hexpriv):
print b.privnum2addr(int(hexpriv,16),compressed=True)
def wiftoaddr_comp(s_in):
print "Entered data: %s" % s_in
s_enc = b.wiftohex(s_in,compressed=True)
print "Encoded data: %s" % s_enc
s_enc = b.privnum2addr(int(s_enc,16),compressed=True)
print "Encoded data: %s" % s_enc
tests = {
"keyconv_compare": ['wif [str]','quiet [bool=False]'],
"keyconv_compare_randloop": ['iterations [int]','quiet [bool=False]'],
"b58_randenc": ['quiet [bool=False]'],
"strtob58": ['string [str]','quiet [bool=False]'],
"hextob58": ['hexnum [str]','quiet [bool=False]'],
"b58tohex": ['b58num [str]','quiet [bool=False]'],
"hextob58_pad": ['hexnum [str]','quiet [bool=False]'],
"b58tohex_pad": ['b58num [str]','quiet [bool=False]'],
"hextob58_pad_randloop": ['iterations [int]','quiet [bool=False]'],
"test_wiftohex": ['wif [str]', 'quiet [bool=False]'],
"numtowif_rand": ['quiet [bool=False]'],
"hextosha256": ['hexnum [str]','quiet [bool=False]'],
"hextowiftopubkey": ['hexnum [str]','quiet [bool=False]'],
"pubhextoaddr": ['hexnum [str]','quiet [bool=False]'],
"hextowif_comp": ['hexnum [str]'],
"wiftohex_comp": ['wif [str]'],
"privhextoaddr_comp": ['hexnum [str]'],
"wiftoaddr_comp": ['wif [str]'],
}
args = process_test_args(sys.argv, tests)

View file

@ -39,9 +39,9 @@ def Vmsg_r(s):
opts = {}
commands = {
"strtob58": ['<string> [str]'],
"b58tostr": ['<b58 number> [str]'],
"hextob58": ['<hex number> [str]'],
"b58tohex": ['<b58 number> [str]'],
"b58tostr": ['<b58 number> [str]'],
"b58randenc": [],
"randhex": ['nbytes [int=32]'],
"randwif": ['compressed [bool=False]'],
@ -51,6 +51,8 @@ commands = {
"hex2wif": ['<private key in hex format> [str]', 'compressed [bool=False]'],
"hexdump": ['<infile> [str]', 'cols [int=8]', 'line_nums [bool=True]'],
"unhexdump": ['<infile> [str]'],
"hex2mn": ['<hexadecimal string> [str]','wordlist [str="electrum"]'],
"mn2hex": ['<mnemonic> [str]', 'wordlist [str="electrum"]'],
"mn_rand128": ['wordlist [str="electrum"]'],
"mn_rand192": ['wordlist [str="electrum"]'],
"mn_rand256": ['wordlist [str="electrum"]'],
@ -59,10 +61,11 @@ commands = {
"id8": ['<infile> [str]'],
"id6": ['<infile> [str]'],
"str2id6": ['<string (spaces are ignored)> [str]'],
"listaddresses":['minconf [int=1]', 'showempty [bool=False]'],
"listaddresses":['minconf [int=1]','showempty [bool=False]','pager [bool=False]'],
"getbalance": ['minconf [int=1]'],
"viewtx": ['<MMGen tx file> [str]','pager [bool=False]'],
"check_addrfile": ['<MMGen addr file> [str]'],
"addrfile_chksum": ['<MMGen addr file> [str]'],
"keyaddrfile_chksum": ['<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]',
@ -122,8 +125,9 @@ command_help = """
* 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
addrfile_chksum - compute checksum for {pnm} address file
keyaddrfile_chksum - compute checksum for {pnm} key file
find_incog_data - Use an Incog ID to find hidden incognito wallet data
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
@ -135,6 +139,8 @@ command_help = """
mn_rand256 - generate random 256-bit mnemonic
mn_stats - show stats for mnemonic wordlist
mn_printlist - print mnemonic wordlist
hex2mn - convert a 16, 24 or 32-byte number in hex format to a mnemonic
mn2hex - convert a 12, 18 or 24-word mnemonic to a number in hex format
IMPORTANT NOTE: Though {pnm} mnemonics use the Electrum wordlist, they're
computed using a different algorithm and are NOT Electrum-compatible!
@ -286,37 +292,45 @@ def get_wordlist(wordlist):
Msg('"%s": invalid wordlist. Valid choices: %s' %
(wordlist,'"'+'" "'.join(wordlists)+'"'))
sys.exit(1)
return el if wordlist == "electrum" else tl
return (el if wordlist == "electrum" else tl).strip().split("\n")
def do_random_mn(nbytes,wordlist):
r = get_random(nbytes,opts)
wlists = wordlists if wordlist == "all" else [wordlist]
for wl in wlists:
l = get_wordlist(wl)
if wl == wlists[0]: Vmsg("Seed: %s" % ba.hexlify(r))
mn = get_mnemonic_from_seed(r,l.strip().split("\n"),
wordlist,print_info=False)
Vmsg("%s wordlist mnemonic:" % (wl.capitalize()))
Vmsg("Seed: %s" % ba.hexlify(r))
for wlname in (wordlists if wordlist == "all" else [wordlist]):
wl = get_wordlist(wlname)
mn = get_mnemonic_from_seed(r,wl,wordlist)
Vmsg("%s wordlist mnemonic:" % (wlname.capitalize()))
print " ".join(mn)
def mn_rand128(wordlist="electrum"): do_random_mn(16,wordlist)
def mn_rand192(wordlist="electrum"): do_random_mn(24,wordlist)
def mn_rand256(wordlist="electrum"): do_random_mn(32,wordlist)
def hex2mn(s,wordlist="electrum"):
import mmgen.mnemonic
wl = get_wordlist(wordlist)
print " ".join(get_mnemonic_from_seed(ba.unhexlify(s), wl, wordlist))
def mn2hex(s,wordlist="electrum"):
import mmgen.mnemonic
wl = get_wordlist(wordlist)
print ba.hexlify(get_seed_from_mnemonic(s.split(),wl,True))
def mn_stats(wordlist="electrum"):
l = get_wordlist(wordlist)
check_wordlist(l,wordlist)
def mn_printlist(wordlist="electrum"):
l = get_wordlist(wordlist)
print "%s" % l.strip()
wl = get_wordlist(wordlist)
print "\n".join(wl)
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):
def listaddresses(minconf=1,showempty=False,pager=False):
from mmgen.tx import connect_to_bitcoind,trim_exponent,is_mmgen_addr
c = connect_to_bitcoind()
@ -349,7 +363,7 @@ def listaddresses(minconf=1,showempty=False):
max([len(k) for k in addrs.keys()]),
max([len(str(addrs[k][1])) for k in addrs.keys()])
)
print fs % ("ADDRESS","COMMENT","BALANCE")
out = [ fs % ("ADDRESS","COMMENT","BALANCE") ]
def s_mmgen(ma):
return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *ma.split("_"))
@ -357,9 +371,13 @@ def listaddresses(minconf=1,showempty=False):
old_sid = ""
for k in sorted(addrs.keys(),key=s_mmgen):
sid,num = k.split("_")
if old_sid and old_sid != sid: print
if old_sid and old_sid != sid: out.append("")
old_sid = sid
print fs % (sid+":"+num, addrs[k][1], trim_exponent(addrs[k][0]))
out.append(fs % (sid+":"+num, addrs[k][1], trim_exponent(addrs[k][0])))
o = "\n".join(out)
if pager: do_pager(o)
else: print o
def getbalance(minconf=1):
@ -390,10 +408,11 @@ def viewtx(infile,pager=False):
c = connect_to_bitcoind()
tx_data = get_lines_from_file(infile,"transaction data")
metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_data(tx_data,infile)
metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,infile)
view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager)
def check_addrfile(infile): parse_addrs_file(infile)
def addrfile_chksum(infile): parse_addrfile(infile,{})
def keyaddrfile_chksum(infile): parse_keyaddr_file(infile,{})
def hexreverse(hex_str):
print ba.hexlify(decode_pretty_hexdump(hex_str)[::-1])
@ -418,7 +437,7 @@ def pubkey2hexaddr(pubkeyhex):
print bitcoin.pubhex2hexaddr(pubkeyhex)
def pubkey2addr(pubkeyhex):
print bitcoin.pubhex2addr(pubkeyhex)
print bitcoin.hexaddr2addr(bitcoin.pubhex2hexaddr(pubkeyhex))
def privhex2addr(privkeyhex,compressed=False):
print bitcoin.privnum2addr(int(privkeyhex,16),compressed)
@ -444,7 +463,7 @@ def encrypt(infile,outfile="",hash_preset=''):
def decrypt(infile,outfile="",hash_preset=''):
enc_d = get_data_from_file(infile,"encrypted data")
while True:
dec_d = mmgen_decrypt(enc_d,"user data","",opts)
dec_d = mmgen_decrypt(enc_d,"user data")
if dec_d: break
msg("Trying again...")
if outfile == '-':
@ -463,6 +482,10 @@ 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)
for ch in iv_id:
if ch not in "0123456789ABCDEF":
msg("'%s': invalid Incog ID" % iv_id)
sys.exit(2)
while True:
d = os.read(f,bsize)
if not d: break

View file

@ -26,82 +26,7 @@ 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)",
'throwaway_change': """
ERROR: This transaction produces change (%s BTC); however, no change address
was specified.
""".strip(),
'mixed_inputs': """
NOTE: This transaction uses a mixture of both mmgen and non-mmgen inputs, which
makes the signing process more complicated. When signing the transaction, keys
for the non-mmgen inputs must be supplied in a separate file using either the
'-k' or '-K' option to '{}-txsign'.
Selected mmgen inputs: %s""".format(g.proj_name.lower()),
'too_many_acct_addresses': """
ERROR: More than one address found for account: "%s".
The tracking "wallet.dat" file appears to have been altered by a non-{g.proj_name}
program. Please restore "wallet.dat" from a backup or create a new wallet
and re-import your addresses.""".strip().format(g=g),
'addrfile_no_data_msg': """
No data found for MMgen address '%s'. Please import this address into
your tracking wallet, or supply an address file for it on the command line.
""".strip(),
'addrfile_warn_msg': """
Warning: output address '{mmaddr}' is not in the tracking wallet, which means
its balance will not be tracked. You're strongly advised to import the address
into your tracking wallet before broadcasting this transaction.
""".strip(),
'addrfile_fail_msg': """
No data for MMgen address '{mmaddr}' could be found in either the tracking
wallet or the supplied address file. Please import this address into your
tracking wallet, or supply an address file for it on the command line.
""".strip(),
'no_spendable_outputs': """
No spendable outputs found! Import addresses with balances into your
watch-only wallet using '{}-addrimport' and then re-run this program.
""".strip().format(g.proj_name.lower()),
'mapping_error': """
MMGen -> BTC address mappings differ!
In transaction: %s
Generated from seed: %s
""".strip(),
'skip_mapping_checks_warning': """
You've chosen the '--all-keys-from-file' option. Since all signing keys will
be taken from this file, no {pnm} seed source will be consulted and {pnm}-to-
BTC mapping checks cannot not be performed. Were an attacker to compromise
your tracking wallet or raw transaction file, he could thus cause you to spend
coin to an unintended address. For greater security, supply a trusted {pnm}
address file for your output addresses on the command line.
""".strip().format(pnm=g.proj_name),
'missing_mappings': """
No information was found in the supplied address files for the following {pnm}
addresses: %s
The {pnm}-to-BTC mappings for these addresses cannot be verified!
""".strip().format(pnm=g.proj_name),
}
def connect_to_bitcoind():
host,port,user,passwd = "localhost",8332,"rpcuser","rpcpassword"
cfg = get_bitcoind_cfg_options((user,passwd))
import mmgen.rpc.connection
f = mmgen.rpc.connection.BitcoinConnection
try:
c = f(cfg[user],cfg[passwd],host,port)
except:
msg("Unable to establish RPC connection with bitcoind")
sys.exit(2)
return c
from mmgen.term import do_pager
def trim_exponent(n):
'''Remove exponent and trailing zeros.
@ -109,8 +34,6 @@ def trim_exponent(n):
d = Decimal(n)
return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
def is_btc_amt(amt):
# amt must be a string!
@ -134,197 +57,81 @@ def is_btc_amt(amt):
return trim_exponent(ret)
def normalize_btc_amt(amt):
# amt must be a string!
ret = is_btc_amt(amt)
if ret: return ret
else: sys.exit(3)
def get_bitcoind_cfg_options(cfg_keys):
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])
for k in set(cfg_keys) - set(cfg.keys()):
msg("Configuration option '%s' must be set in %s" % (k,cfg_file))
sys.exit(2)
return cfg
def format_unspent_outputs_for_printing(out,sort_info,total):
pfs = " %-4s %-67s %-34s %-12s %-13s %-8s %-10s %s"
pout = [pfs % ("Num","TX id,Vout","Address","MMgen ID",
"Amount (BTC)","Conf.","Age (days)", "Comment")]
for n,i in enumerate(out):
addr = "=" if i.skip == "addr" and "grouped" in sort_info else i.address
tx = " " * 63 + "=" \
if i.skip == "txid" and "grouped" in sort_info else str(i.txid)
s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr,
i.mmid,i.amt,i.confirmations,i.days,i.label)
pout.append(s.rstrip())
return \
"Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n".format(
make_timestr(), " ".join(sort_info), "\n".join(pout), total
)
def sort_and_view(unspent,opts):
def s_amt(i): return i.amount
def s_txid(i): return "%s %03s" % (i.txid,i.vout)
def s_addr(i): return i.address
def s_age(i): return i.confirmations
def s_mmgen(i):
m = parse_mmgen_label(i.account)[0]
if m: return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *m.split(":"))
else: return "G" + i.account
sort,group,show_days,show_mmaddr,reverse = "age",False,False,True,True
unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default
total = trim_exponent(sum([i.amount for i in unspent]))
max_acct_len = max([len(i.account) for i in unspent])
hdr_fmt = "UNSPENT OUTPUTS (sort order: %s) Total BTC: %s"
options_msg = """
Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
""".strip()
prompt = \
"('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
from copy import deepcopy
from mmgen.term import get_terminal_size
write_to_file_msg = ""
msg("")
while True:
cols = get_terminal_size()[0]
if cols < g.min_screen_width:
msg("%s-txcreate requires a screen at least %s characters wide" %
(g.proj_name.lower(),g.min_screen_width))
sys.exit(2)
addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46)
acct_w = min(max_acct_len, max(24,int(addr_w-10)))
btaddr_w = addr_w - acct_w - 1
tx_w = max(11,min(64, cols-addr_w-32))
txdots = "..." if tx_w < 64 else ""
fs = " %-4s %-" + str(tx_w) + "s %-2s %-" + str(addr_w) + "s %-13s %-s"
table_hdr = fs % ("Num","TX id Vout","","Address","Amount (BTC)",
"Age(d)" if show_days else "Conf.")
unsp = deepcopy(unspent)
for i in unsp: i.skip = ""
if group and (sort == "address" or sort == "txid"):
for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
if sort == "address" and a.address == b.address: b.skip = "addr"
elif sort == "txid" and a.txid == b.txid: b.skip = "txid"
for i in unsp:
amt = str(trim_exponent(i.amount))
lfill = 3 - len(amt.split(".")[0]) if "." in amt else 3 - len(amt)
i.amt = " "*lfill + amt
i.days = int(i.confirmations * g.mins_per_block / (60*24))
i.age = i.days if show_days else i.confirmations
i.mmid,i.label = parse_mmgen_label(i.account)
if i.skip == "addr":
i.addr = "|" + "." * 33
else:
if show_mmaddr:
dots = ".." if btaddr_w < len(i.address) else ""
i.addr = "%s%s %s" % (
i.address[:btaddr_w-len(dots)],
dots,
i.account[:acct_w])
else:
i.addr = i.address
i.tx = " " * (tx_w-4) + "|..." if i.skip == "txid" \
else i.txid[:tx_w-len(txdots)]+txdots
sort_info = ["reverse"] if reverse else []
sort_info.append(sort if sort else "unsorted")
if group and (sort == "address" or sort == "txid"):
sort_info.append("grouped")
out = [hdr_fmt % (" ".join(sort_info), total), table_hdr]
out += [fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,i.age)
for n,i in enumerate(unsp)]
msg("\n".join(out) +"\n\n" + write_to_file_msg + options_msg)
write_to_file_msg = ""
skip_prompt = False
while True:
reply = get_char(prompt, immed_chars="atDdAMrgmeqpvw")
if reply == 'a': unspent.sort(key=s_amt); sort = "amount"
elif reply == 't': unspent.sort(key=s_txid); sort = "txid"
elif reply == 'D': show_days = False if show_days else True
elif reply == 'd': unspent.sort(key=s_addr); sort = "address"
elif reply == 'A': unspent.sort(key=s_age); sort = "age"
elif reply == 'M':
unspent.sort(key=s_mmgen); sort = "mmgen"
show_mmaddr = True
elif reply == 'r':
unspent.reverse()
reverse = False if reverse else True
elif reply == 'g': group = False if group else True
elif reply == 'm': show_mmaddr = False if show_mmaddr else True
elif reply == 'e': pass
elif reply == 'q': pass
elif reply == 'p':
d = format_unspent_outputs_for_printing(unsp,sort_info,total)
of = "listunspent[%s].out" % ",".join(sort_info)
write_to_file(of, d, opts,"",False,False)
write_to_file_msg = "Data written to '%s'\n\n" % of
elif reply == 'v':
do_pager("\n".join(out))
continue
elif reply == 'w':
data = format_unspent_outputs_for_printing(unsp,sort_info,total)
do_pager(data)
continue
else:
msg("\nInvalid input")
continue
break
msg("\n")
if reply == 'q': break
return tuple(unspent)
def parse_mmgen_label(s,check_label_len=False):
l = split2(s)
if not is_mmgen_addr(l[0]): return "",s
if check_label_len: check_addr_label(l[1])
return tuple(l)
def is_mmgen_seed_id(s):
import re
return re.match(r"^[0123456789ABCDEF]{8}$",s) is not None
def is_mmgen_idx(s):
import re
m = g.mmgen_idx_max_digits
return re.match(r"^[0123456789]{1,"+str(m)+r"}$",s) is not None
def is_mmgen_addr(s):
seed_id,idx = split2(s,":")
return is_mmgen_seed_id(seed_id) and is_mmgen_idx(idx)
def is_btc_addr(s):
from mmgen.bitcoin import verify_addr
return verify_addr(s)
def is_b58_str(s):
from mmgen.bitcoin import b58a
for ch in s:
if ch not in b58a: return False
return True
def is_btc_key(s):
if s == "": return False
compressed = not s[0] == '5'
from mmgen.bitcoin import wiftohex
return wiftohex(s,compressed) is not False
def wiftoaddr(s):
if s == "": return False
compressed = not s[0] == '5'
from mmgen.bitcoin import wiftohex,privnum2addr
hex_key = wiftohex(s,compressed)
if not hex_key: return False
return privnum2addr(int(hex_key,16),compressed)
def is_valid_tx_comment(s, verbose=True):
if len(s) > g.max_tx_comment_len:
if verbose: msg("Invalid transaction comment (longer than %s characters)" %
g.max_tx_comment_len)
return False
try: s.decode("utf8")
except:
if verbose: msg("Invalid transaction comment (not UTF-8)")
return False
else: return True
def check_addr_label(label):
if len(label) > g.max_addr_label_len:
msg("'%s': overlong label (length must be <=%s)" %
(label,g.max_addr_label_len))
sys.exit(3)
for ch in label:
if ch not in g.addr_label_symbols:
msg("""
"%s": illegal character in label "%s".
Only ASCII printable characters are permitted.
""".strip() % (ch,label))
sys.exit(3)
def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False):
@ -343,7 +150,7 @@ def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False):
total_in += j['amount']
addr = j['address']
mmid,label = parse_mmgen_label(j['account']) \
if 'account' in j else ("","")
if 'account' in j else ("","")
mmid_str = ((34-len(addr))*" " + " (%s)" % mmid) if mmid else ""
for d in (
@ -378,10 +185,13 @@ def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False):
o = out.encode("utf8")
if pager: do_pager(o)
else: print "\n"+o
else:
print "\n"+o
get_char("Press any key to continue: ")
msg("")
def parse_tx_data(tx_data,infile):
def parse_tx_file(tx_data,infile):
err_str,err_fmt = "","Invalid %s in transaction file"
@ -424,164 +234,34 @@ def parse_tx_data(tx_data,infile):
return metadata.split(),tx_hex,inputs_data,outputs_data,comment
def select_outputs(unspent,prompt):
while True:
reply = my_raw_input(prompt).strip()
if not reply: continue
from mmgen.util import parse_address_list
selected = parse_address_list(reply,sep=None)
if not selected: continue
if selected[-1] > len(unspent):
msg("Inputs must be less than %s" % len(unspent))
continue
return selected
def is_mmgen_seed_id(s):
import re
return True if re.match(r"^[0123456789ABCDEF]{8}$",s) else False
def is_mmgen_idx(s):
import re
m = g.mmgen_idx_max_digits
return True if re.match(r"^[0123456789]{1,"+str(m)+r"}$",s) else False
def is_mmgen_addr(s):
seed_id,idx = split2(s,":")
return is_mmgen_seed_id(seed_id) and is_mmgen_idx(idx)
def is_btc_addr(s):
from mmgen.bitcoin import verify_addr
return verify_addr(s)
def is_b58_str(s):
from mmgen.bitcoin import b58a
for ch in s:
if ch not in b58a: return False
return True
def is_btc_key(s):
if s == "": return False
compressed = False if s[0] == '5' else True
from mmgen.bitcoin import wiftohex
return True if wiftohex(s,compressed) else False
def mmaddr2btcaddr_bitcoind(c,mmaddr,acct_data):
# Don't want to create a new object, so use append()
if not acct_data:
for i in c.listaccounts(minconf=0,includeWatchonly=True):
acct_data.append(i)
for acct in acct_data:
m,comment = parse_mmgen_label(acct)
if m == mmaddr:
addrlist = c.getaddressesbyaccount(acct)
if len(addrlist) == 1:
return addrlist[0],comment
else:
msg(txmsg['too_many_acct_addresses'] % acct); sys.exit(2)
return "",""
def get_wif2addr_f():
if g.no_keyconv: return wiftoaddr
from mmgen.addr import test_for_keyconv
return wiftoaddr_keyconv if test_for_keyconv() else wiftoaddr
def mmaddr2btcaddr_addrfile(mmaddr,addr_data,silent=False):
mmid,mmidx = mmaddr.split(":")
for ad in addr_data:
if mmid == ad[0]:
for j in ad[1]:
if j[0] == mmidx:
if not silent:
msg(txmsg['addrfile_warn_msg'].format(mmaddr=mmaddr))
if not keypress_confirm("Continue anyway?"):
sys.exit(1)
return j[1:] if len(j) == 3 else (j[1],"")
if silent: return "",""
else:
msg(txmsg['addrfile_fail_msg'].format(mmaddr=mmaddr))
sys.exit(2)
def check_mmgen_to_btc_addr_mappings(mmgen_inputs,b2m_map,infiles,saved_seeds,opts):
in_maplist = [(i['account'].split()[0],i['address']) for i in mmgen_inputs]
out_maplist = [(i[1][0],i[0]) for i in b2m_map.items()]
for maplist,label in (in_maplist,"inputs"), (out_maplist,"outputs"):
if not maplist: continue
qmsg("Checking MMGen -> BTC address mappings for %s" % label)
pairs = get_keys_for_mmgen_addrs([i[0] for i in maplist],
infiles,saved_seeds,opts,gen_pairs=True)
for a,b in zip(sorted(pairs),sorted(maplist)):
if a != b:
msg(txmsg['mapping_error'] % (" ".join(a)," ".join(b)))
sys.exit(3)
qmsg("Address mappings OK")
def check_addr_label(label):
if len(label) > g.max_addr_label_len:
msg("'%s': overlong label (length must be <=%s)" %
(label,g.max_addr_label_len))
sys.exit(3)
for ch in label:
if ch not in g.addr_label_symbols:
msg("""
"%s": illegal character in label "%s".
Only ASCII printable characters are permitted.
""".strip() % (ch,label))
sys.exit(3)
def make_addr_data_chksum(addr_data):
def make_addr_data_chksum(adata,keys=False):
nchars = 24
return make_chksum_N(
" ".join(["{} {}".format(*d[:2]) for d in addr_data]), nchars, sep=True
)
return make_chksum_N(" ".join([" ".join(
[str(n),d[0],d[2]] if keys else [str(n),d[0]]
) for n,d in adata]), nchars, sep=True)
def check_addr_data_hash(seed_id,addr_data):
def get_addr_data_hash(e,keys=False):
def s_addrdata(a): return int(a[0])
addr_data_chksum = make_addr_data_chksum(sorted(addr_data,key=s_addrdata))
from mmgen.addr import fmt_addr_idxs
fl = fmt_addr_idxs([int(a[0]) for a in addr_data])
qmsg_r("Computed checksum for addr data {}[{}]: ".format(seed_id,fl))
msg(addr_data_chksum)
qmsg("Check this value against your records")
adata = [(k,e[k]) for k in e.keys()]
return make_addr_data_chksum(sorted(adata,key=s_addrdata),keys)
def parse_addrs_file(f):
lines = get_lines_from_file(f,"address data",trim_comments=True)
def _parse_addrfile_body(lines,keys=False,check=False):
try:
seed_id,obrace = lines[0].split()
except:
msg("Invalid first line: '%s'" % lines[0])
sys.exit(3)
cbrace = lines[-1]
if obrace != '{':
msg("'%s': invalid first line" % lines[0])
elif cbrace != '}':
msg("'%s': invalid last line" % cbrace)
elif not is_mmgen_seed_id(seed_id):
msg("'%s': invalid Seed ID" % seed_id)
else:
addr_data = []
for i in lines[1:-1]:
d = i.split(None,2)
def parse_addr_lines(lines):
ret = []
for l in lines:
d = l.split(None,2)
if not is_mmgen_idx(d[0]):
msg("'%s': invalid address num. in line: %s" % (d[0],d))
msg("'%s': invalid address num. in line: '%s'" % (d[0],l))
sys.exit(3)
if not is_btc_addr(d[1]):
@ -589,210 +269,112 @@ def parse_addrs_file(f):
sys.exit(3)
if len(d) == 3:
check_addr_label(d[2])
addr_data.append(tuple(d))
check_addr_data_hash(seed_id,addr_data)
return seed_id,addr_data
sys.exit(3)
def sign_transaction(c,tx_hex,tx_num_str,sig_data,keys=None):
if keys:
qmsg("%s keys total" % len(keys))
if g.debug: print "Keys:\n %s" % "\n ".join(keys)
msg_r("Signing transaction{}...".format(tx_num_str))
from mmgen.rpc import exceptions
try:
sig_tx = c.signrawtransaction(tx_hex,sig_data,keys)
except exceptions.InvalidAddressOrKey:
msg("failed\nInvalid address or key")
sys.exit(3)
return sig_tx
def get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts):
if seed_id in saved_seeds.keys():
return saved_seeds[seed_id]
while True:
if infiles:
seed = get_seed_retry(infiles.pop(0),opts)
elif "from_brain" in opts or "from_mnemonic" in opts \
or "from_seed" in opts or "from_incog" in opts:
msg("Need data for seed ID %s" % seed_id)
seed = get_seed_retry("",opts)
msg("User input produced seed ID %s" % make_chksum_8(seed))
else:
msg("ERROR: No seed source found for seed ID: %s" % seed_id)
sys.exit(2)
s_id = make_chksum_8(seed)
saved_seeds[s_id] = seed
if s_id == seed_id: return seed
def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds,opts,gen_pairs=False):
seed_ids = list(set([i[:8] for i in mmgen_addrs]))
ret = []
for seed_id in seed_ids:
# Returns only if seed is found
seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts)
addr_ids = [int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id]
from mmgen.addr import generate_addrs
if gen_pairs:
ret += [("{}:{}".format(seed_id,i.num),i.addr)
for i in generate_addrs(seed,addr_ids,
{'gen_what':["addrs"]})]
else:
ret += [i.wif for i in generate_addrs(seed,addr_ids,
{'gen_what':["keys"]})]
return ret
def sign_tx_with_bitcoind_wallet(c,tx_hex,tx_num_str,sig_data,keys,opts):
try:
sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys)
except:
from mmgen.rpc import exceptions
msg("Using keys in wallet.dat as per user request")
prompt = "Enter passphrase for bitcoind wallet: "
while True:
passwd = get_bitcoind_passphrase(prompt,opts)
try:
c.walletpassphrase(passwd, 9999)
except exceptions.WalletPassphraseIncorrect:
msg("Passphrase incorrect")
comment = d[2]
check_addr_label(comment)
else:
msg("Passphrase OK"); break
comment = ""
sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys)
ret.append((d[0],d[1],comment))
msg("Locking wallet")
try:
c.walletlock()
except:
msg("Failed to lock wallet")
return ret
return sig_tx
def parse_key_lines(lines):
ret = []
for l in lines:
d = l.split(None,2)
if d[0] != "wif:":
msg("Invalid key line in file: '%s'" % l)
sys.exit(3)
if not is_btc_key(d[1]):
msg("'%s': invalid Bitcoin key" % d[1])
sys.exit(3)
ret.append(d[1])
return ret
z = len(lines) / 2
if keys:
adata = parse_addr_lines([lines[i*2] for i in range(z)])
kdata = parse_key_lines([lines[i*2+1] for i in range(z)])
if len(adata) != len(kdata):
msg("Odd number of lines in key file")
sys.exit(2)
if check or keypress_confirm("Check key-to-address validity?"):
wif2addr_f = get_wif2addr_f()
for i in range(z):
msg_r("\rVerifying keys %s/%s" % (i+1,z))
if adata[i][1] != wif2addr_f(kdata[i]):
msg("Key doesn't match address!\n %s\n %s" %
kdata[i],adata[i][1])
sys.exit(2)
msg(" - done")
return [adata[i] + (kdata[i],) for i in range(z)]
else:
return parse_addr_lines(lines)
def preverify_keys(addrs_in, keys_in, mm_inputs):
def parse_addrfile(f,addr_data,keys=False):
return parse_addrfile_lines(
get_lines_from_file(f,"address data",trim_comments=True),
addr_data,keys)
addrs,keys = set(addrs_in),set(keys_in)
import mmgen.bitcoin as b
qmsg_r('Checking that user-supplied key list contains valid keys...')
invalid_keys = [k for k in keys if not is_btc_key(k)]
if invalid_keys:
s = "" if len(invalid_keys) == 1 else "s"
msg("\n%s/%s invalid key%s in keylist!\n" % (len(invalid_keys),len(keys),s))
sys.exit(2)
qmsg("OK")
# Check that keys match addresses:
msg('Pre-verifying keys in user-supplied key list (Ctrl-C to skip)')
def parse_addrfile_lines(lines,addr_data,keys=False,exit_on_error=True):
try:
for n,k in enumerate(keys,1):
msg_r("\rkey %s of %s" % (n,len(keys)))
c = False if k[0] == '5' else True
hexkey = b.wiftohex(k,compressed=c)
addr = b.privnum2addr(int(hexkey,16),compressed=c)
if addr in addrs:
addrs.remove(addr)
if not addrs: break
except KeyboardInterrupt:
msg("\nSkipping")
else:
msg("")
if addrs:
mms = dict([(i['address'],i['account'].split()[0])
for i in mm_inputs if i['address'] in addrs])
s = "" if len(addrs) == 1 else "es"
msg(
"Cannot sign transaction. No keys found for the following address%s:"%s)
for a in sorted(addrs):
print " %s%s" % (a, " ({})".format(mms[a]) if a in mms else "")
sys.exit(2)
else:
extra_keys = len(keys) - len(set(addrs_in))
if extra_keys > 0:
s = "" if extra_keys == 1 else "s"
msg("%s extra key%s found" % (extra_keys,s))
def missing_keys_errormsg(other_addrs):
msg("""
A key file must be supplied (or use the "-w" option) for the following
non-mmgen address%s:
""".strip() % ("" if len(other_addrs) == 1 else "es"))
print " %s" % "\n ".join([i['address'] for i in other_addrs])
def check_mmgen_to_btc_addr_mappings_addrfile(mmgen_inputs,b2m_map,addrfiles):
addr_data = [parse_addrs_file(a) for a in addrfiles]
in_maplist = [(i['account'].split()[0],i['address']) for i in mmgen_inputs]
out_maplist = [(i[1][0],i[0]) for i in b2m_map.items()]
missing,wrong = [],[]
for maplist,label in (in_maplist,"inputs"), (out_maplist,"outputs"):
qmsg("Checking MMGen -> BTC address mappings for %s" % label)
for i in maplist:
btaddr = mmaddr2btcaddr_addrfile(i[0],addr_data,silent=True)[0]
if not btaddr: missing.append(i[0])
elif btaddr != i[1]: wrong.append((i[0],i[1],btaddr))
if wrong:
fs = " {:11} {:35} {}"
msg("ERROR: The following address mappings did not match!")
msg(fs.format("MMGen addr","In TX file:","In address file:"))
for w in wrong: msg(fs.format(*w))
sys.exit(3)
if missing:
confirm_or_exit(txmsg['missing_mappings'] %
" ".join(missing),"continue")
else: qmsg("Address mappings OK")
def is_valid_tx_comment(s, verbose=True):
if len(s) > g.max_tx_comment_len:
if verbose: msg("Invalid transaction comment (longer than %s characters)" %
g.max_tx_comment_len)
return False
try: s.decode("utf8")
seed_id,obrace = lines[0].split()
except:
if verbose: msg("Invalid transaction comment (not UTF-8)")
errmsg = "Invalid first line: '%s'" % lines[0]
else:
cbrace = lines[-1]
if obrace != '{':
errmsg = "'%s': invalid first line" % lines[0]
elif cbrace != '}':
errmsg = "'%s': invalid last line" % cbrace
elif not is_mmgen_seed_id(seed_id):
errmsg = "'%s': invalid Seed ID" % seed_id
else:
ldata = _parse_addrfile_body(lines[1:-1],keys)
if seed_id not in addr_data: addr_data[seed_id] = {}
for l in ldata:
addr_data[seed_id][l[0]] = l[1:]
chk = get_addr_data_hash(addr_data[seed_id],keys)
from mmgen.addr import fmt_addr_idxs
fl = fmt_addr_idxs([int(i) for i in addr_data[seed_id].keys()])
w = "key" if keys else "addr"
qmsg_r("Computed checksum for "+w+" data ",w.capitalize()+" checksum ")
msg("{}[{}]: {}".format(seed_id,fl,chk))
qmsg("Check this value against your records")
return True
if exit_on_error:
msg(errmsg)
sys.exit(3)
else:
return False
else: return True
def parse_keyaddr_file(infile,addr_data):
d = get_data_from_file(infile,"%s key-address file data" % g.proj_name)
enc_ext = get_extension(infile) == g.mmenc_ext
if enc_ext or not is_utf8(d):
m = "Decrypting" if enc_ext else "Attempting to decrypt"
msg("%s key-address file %s" % (m,infile))
from crypto import mmgen_decrypt_retry
d = mmgen_decrypt_retry(d,"key-address file")
parse_addrfile_lines(remove_comments(d.split("\n")),addr_data,True,False)
def get_tx_comment_from_file(infile):
s = get_data_from_file(infile,"transaction comment")
if is_valid_tx_comment(s, verbose=True):
return s.decode("utf8").strip()
else: return False
else:
sys.exit(2)
def get_tx_comment_from_user(comment=""):
try:
while True:
s = my_raw_input("Comment: ",insert_txt=comment.encode("utf8"))
@ -800,12 +382,68 @@ def get_tx_comment_from_user(comment=""):
if is_valid_tx_comment(s, verbose=True):
return s.decode("utf8")
except KeyboardInterrupt:
msg("User interrupt")
return False
msg("User interrupt")
return False
def make_tx_data(metadata_fmt, tx_hex, inputs_data, b2m_map, comment):
from mmgen.bitcoin import b58encode
s = (b58encode(comment.encode("utf8")),) if comment else ()
lines = (metadata_fmt, tx_hex, repr(inputs_data), repr(b2m_map)) + s
return "\n".join(lines)+"\n"
def mmaddr2btcaddr_addrdata(mmaddr,addr_data,source=""):
seed_id,idx = mmaddr.split(":")
if seed_id in addr_data:
if idx in addr_data[seed_id]:
vmsg("%s -> %s%s" % (mmaddr,addr_data[seed_id][idx][0],
" (from "+source+")" if source else ""))
return addr_data[seed_id][idx]
return "",""
def get_bitcoind_cfg_options(cfg_keys):
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])
for k in set(cfg_keys) - set(cfg.keys()):
msg("Configuration option '%s' must be set in %s" % (k,cfg_file))
sys.exit(2)
return cfg
def connect_to_bitcoind():
host,port,user,passwd = "localhost",8332,"rpcuser","rpcpassword"
cfg = get_bitcoind_cfg_options((user,passwd))
import mmgen.rpc.connection
f = mmgen.rpc.connection.BitcoinConnection
try:
c = f(cfg[user],cfg[passwd],host,port)
except:
msg("Unable to establish RPC connection with bitcoind")
sys.exit(2)
return c
def wiftoaddr_keyconv(wif):
from subprocess import Popen, PIPE
if wif[0] == '5':
return Popen(["keyconv", wif],
stdout=PIPE).stdout.readline().split()[1]
else:
return wiftoaddr(wif)

View file

@ -28,74 +28,33 @@ import mmgen.config as g
def msg(s): sys.stderr.write(s + "\n")
def msg_r(s): sys.stderr.write(s)
def qmsg(s,alt=""):
def qmsg(s,alt=False):
if g.quiet:
if alt: sys.stderr.write(alt + "\n")
if alt != False: sys.stderr.write(alt + "\n")
else: sys.stderr.write(s + "\n")
def qmsg_r(s,alt=""):
def qmsg_r(s,alt=False):
if g.quiet:
if alt: sys.stderr.write(alt)
if alt != False: sys.stderr.write(alt)
else: sys.stderr.write(s)
def vmsg(s):
if g.verbose: sys.stderr.write(s + "\n")
def vmsg_r(s):
if g.verbose: sys.stderr.write(s)
cmessages = {
'null': "",
'incog_iv_id': """
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 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.
""",
'incog_key_id': """
Check that the generated seed ID is correct. If it's not, then your
password or hash preset is incorrect or incognito data is corrupted.
""",
'incog_key_id_hidden': """
Check that the generated seed ID is correct. If it's not, then your
password or hash preset is incorrect or incognito data is corrupted.
If the key ID is correct but the seed ID is not, then you might have
chosen an incorrect seed length.
""",
'unencrypted_secret_keys': """
This program generates secret keys from your {} seed, outputting them in
UNENCRYPTED form. Generate only the key(s) you need and guard them carefully.
""".format(g.proj_name),
'brain_warning': """
############################## EXPERTS ONLY! ##############################
def suf(arg,what):
t = type(arg)
if t == int:
n = arg
elif t == list or t == tuple or t == set:
n = len(arg)
else:
msg("%s: invalid parameter" % arg)
return ""
A brainwallet will be secure only if you really know what you're doing and
have put much care into its creation. {} assumes no responsibility for
coins stolen as a result of a poorly crafted brainwallet passphrase.
A key will be generated from your passphrase using the parameters requested
by you: seed length {}, hash preset '{}'. For brainwallets it's highly
recommended to use one of the higher-numbered presets
Remember the seed length and hash preset parameters you've specified. To
generate the correct keys/addresses associated with this passphrase in the
future, you must continue using these same parameters
""",
'usr_rand_notice': """
You've chosen to not fully trust your OS's random number generator and provide
some additional entropy of your own. Please type %s symbols on your keyboard.
Type slowly and choose your symbols carefully for maximum randomness. Try to
use both upper and lowercase as well as punctuation and numerals. What you
type will not be displayed on the screen. Note that the timings between your
keystrokes will also be used as a source of randomness.
""",
'choose_wallet_passphrase': """
Now you must choose a passphrase to encrypt the wallet with. A key will be
generated from your passphrase using a hash preset of '%s'. Please note that
no strength checking of passphrases is performed. For an empty passphrase,
just hit ENTER twice.
""".strip()
}
if what in "a":
return "" if n == 1 else "es"
if what in "k":
return "" if n == 1 else "s"
def get_extension(f):
import os
@ -138,6 +97,11 @@ def _is_hex(s):
except: return False
else: return True
def is_utf8(s):
try: s.decode("utf8")
except: return False
else: return True
def match_ext(addr,ext):
return addr.split(".")[-1] == ext
@ -149,7 +113,7 @@ 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)) +
("" if (line_nums == False or i % cols) else "{:06x}: ".format(i*gw)) +
hexlify(data[i*gw:i*gw+gw]) +
(" " if (i+1) % cols else "\n")
for i in range(len(data)/gw + r)
@ -158,7 +122,8 @@ def pretty_hexdump(data,gw=2,cols=8,line_nums=False):
def decode_pretty_hexdump(data):
import re
lines = [re.sub('^\d+:\s+','',l) for l in data.split("\n")]
from string import hexdigits
lines = [re.sub('^['+hexdigits+']+:\s+','',l) for l in data.split("\n")]
return unhexlify("".join(("".join(lines).split())))
def get_hash_params(hash_preset):
@ -237,7 +202,6 @@ 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)
@ -258,7 +222,7 @@ def make_full_path(outdir,outfile):
# os.path.join() doesn't work?
def parse_address_list(arg,sep=","):
def parse_addr_idxs(arg,sep=","):
ret = []
@ -278,7 +242,7 @@ def parse_address_list(arg,sep=","):
if end < beg:
msg("'%s-%s': end of range less than beginning" % (beg,end))
return False
for k in range(beg,end+1): ret.append(k)
ret.extend(range(beg,end+1))
else:
msg("'%s': invalid argument for address range" % i)
return False
@ -327,11 +291,8 @@ def confirm_or_false(message, question, expect="YES"):
p = question+" "+conf_msg if question[0].isupper() else \
"Are you sure you want to %s?\n%s" % (question,conf_msg)
ret = True if my_raw_input(p).strip() == expect else False
vmsg("")
return ret
return my_raw_input(p).strip() == expect
def write_to_stdout(data, what, confirm=True):
@ -341,7 +302,9 @@ 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'" % os.path.relpath(of))
of_maybe = os.path.relpath(of)
of = of if of_maybe.find(os.path.pardir) == 0 else of_maybe
msg("Redirecting output to file '%s'" % of)
except:
msg("Redirecting output to file")
sys.stdout.write(data)
@ -383,8 +346,7 @@ def write_to_file_or_stdout(outfile, data, opts, what="data"):
if 'stdout' in opts or not sys.stdout.isatty():
write_to_stdout(data, what, confirm=True)
else:
confirm_overwrite = False if g.quiet else True
write_to_file(outfile,data,opts,what,confirm_overwrite,True)
write_to_file(outfile,data,opts,what,not g.quiet,True)
from mmgen.bitcoin import b58decode_pad,b58encode_pad
@ -441,10 +403,9 @@ def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts):
seed_id,key_id,seed_len,hash_preset,g.wallet_ext)
d = "\n".join((chk,)+lines)+"\n"
confirm_overwrite = False if g.quiet else True
write_to_file(outfile,d,opts,"wallet",confirm_overwrite,True)
write_to_file(outfile,d,opts,"wallet",not g.quiet,True)
if g.verbose:
if g.debug:
display_control_data(label,metadata,hash_preset,salt,enc_seed)
@ -542,8 +503,7 @@ def get_data_from_wallet(infile,silent=False):
def _get_words_from_user(prompt, opts):
# split() also strips
words = my_raw_input(prompt,
echo=True if 'echo_passphrase' in opts else False).split()
words = my_raw_input(prompt, echo='echo_passphrase' in opts).split()
if g.debug: print "Sanitized input: [%s]" % " ".join(words)
return words
@ -610,7 +570,7 @@ def get_seed_from_seed_data(words):
msg("Invalid b58 number: %s" % val)
return False
vmsg("%s data produces seed ID: %s" % (g.seed_ext,make_chksum_8(seed)))
msg("Valid seed data for seed ID %s" % make_chksum_8(seed))
return seed
else:
msg("Invalid checksum for {} seed".format(g.proj_name))
@ -643,8 +603,7 @@ def get_bitcoind_passphrase(prompt,opts):
return get_data_from_file(opts['passwd_file'],
"passphrase").strip("\r\n")
else:
return my_raw_input(prompt,
echo=True if 'echo_passphrase' in opts else False)
return my_raw_input(prompt, echo='echo_passphrase' in opts)
def check_data_fits_file_at_offset(fname,offset,dlen,action):
@ -664,43 +623,11 @@ def check_data_fits_file_at_offset(fname,offset,dlen,action):
sys.exit(1)
def get_hidden_incog_data(opts):
# Already sanity-checked:
fname,offset,seed_len = opts['from_incog_hidden'].split(",")
qmsg("Getting hidden incog data from file '%s'" % fname)
dlen = g.aesctr_iv_len + g.salt_len + (int(seed_len)/8)
fsize = check_data_fits_file_at_offset(fname,int(offset),dlen,"read")
f = os.open(fname,os.O_RDONLY)
os.lseek(f, int(offset), os.SEEK_SET)
data = os.read(f, dlen)
os.close(f)
qmsg("Data read from file '%s' at offset %s" % (fname,offset),
"Data read from file")
return data
def export_to_hidden_incog(incog_enc,opts):
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(outfile,int(offset),len(incog_enc),"write")
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)
msg("Data written to file '%s' at offset %s" %
(os.path.relpath(outfile),offset))
from mmgen.term import kb_hold_protect,get_char
def get_hash_preset_from_user(hp='3',what="data"):
p = "Enter hash preset for %s, or ENTER to accept the default ('%s'): " \
% (what,hp)
% (what,hp)
while True:
ret = my_raw_input(p)
if ret:
@ -761,5 +688,3 @@ def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False):
if verbose: msg("\nInvalid reply")
else: msg_r("\r")