The original implementation of mmgen-passgen, using sha256x10(seed+id_str)

Superseded by the current implementation using sha256x10(hmac(seed,id_str,hashlib.sha256))
This commit is contained in:
philemon 2017-07-07 16:09:28 +03:00
commit 85cf5b3cbb
Signed by untrusted user who does not match committer: mmgen
GPG key ID: 62DBE9E5212F05BE
19 changed files with 697 additions and 301 deletions

25
mmgen-passgen Executable file
View file

@ -0,0 +1,25 @@
#!/usr/bin/env python
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""
mmgen-passgen: Generate a series or range of passwords from an MMGen
deterministic wallet
"""
from mmgen.main import launch
launch("passgen")

View file

@ -45,7 +45,7 @@ package on your system or specify the secp256k1 library.
def _test_for_secp256k1(silent=False):
no_secp256k1_errmsg = """
secp256k1 library unavailable. Will use '{kconv}', or failing that, the (slow)
internal ECDSA library for address generation.
native Python ECDSA library for address generation.
""".format(kconv=g.keyconv_exec)
try:
from mmgen.secp256k1 import priv2pub
@ -94,7 +94,7 @@ def _keygen_selector(generator=None):
else:
if opt.key_generator == 3 and _test_for_secp256k1(): return 2
elif opt.key_generator in (2,3) and _test_for_keyconv(): return 1
msg('Using (slow) internal ECDSA library for address generation')
msg('Using (slow) native Python ECDSA library for address generation')
return 0
def get_wif2addr_f(generator=None):
@ -107,21 +107,22 @@ def get_privhex2addr_f(generator=None):
class AddrListEntry(MMGenListItem):
attrs = 'idx','addr','label','wif','sec'
label = MMGenListItemAttr('label','MMGenAddrLabel')
idx = MMGenListItemAttr('idx','AddrIdx')
class AddrListChksum(str,Hilite):
color = 'pink'
trunc_ok = False
def __new__(cls,addrlist):
lines=[' '.join([str(e.idx),e.addr]+([e.wif] if addrlist.has_keys else []))
for e in addrlist.data]
els = ['addr','wif'] if addrlist.has_keys else ['sec'] if addrlist.gen_passwds else ['addr']
lines = [' '.join([str(e.idx)] + [getattr(e,f) for f in els]) for e in addrlist.data]
# print '[{}]'.format(' '.join(lines))
return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True))
class AddrListID(str,Hilite):
class AddrListID(unicode,Hilite):
color = 'green'
trunc_ok = False
def __new__(cls,addrlist):
def __new__(cls,addrlist,fmt_str=None):
try: int(addrlist.data[0].idx)
except:
s = '(no idxs)'
@ -136,8 +137,8 @@ class AddrListID(str,Hilite):
if prev != ret[-1]: ret += '-', prev
ret += ',', i
prev = i
s = ''.join([str(i) for i in ret])
return str.__new__(cls,'%s[%s]' % (addrlist.seed_id,s))
s = ''.join([unicode(i) for i in ret])
return unicode.__new__(cls,fmt_str.format(s) if fmt_str else '{}[{}]'.format(addrlist.seed_id,s))
class AddrList(MMGenObject): # Address info for a single seed ID
msgs = {
@ -158,11 +159,13 @@ Record this checksum: it will be used to verify the address file in the future
Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
""".strip().format(pnm=pnm)
}
main_key = 'addr'
data_desc = 'address'
file_desc = 'addresses'
gen_desc = 'address'
gen_desc = 'address'
gen_desc_pl = 'es'
gen_addrs = True
gen_passwds = False
gen_keys = False
has_keys = False
ext = 'addrs'
@ -199,17 +202,15 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
self.fmt_data = ''
self.id_str = None
self.chksum = None
self.id_str = AddrListID(self)
if type(self) == KeyList:
self.id_str = AddrListID(self)
return
if type(self) == KeyList: return
if do_chksum:
self.chksum = AddrListChksum(self)
if chksum_only:
Msg(self.chksum)
else:
self.id_str = AddrListID(self)
qmsg('Checksum for %s data %s: %s' %
(self.data_desc,self.id_str.hl(),self.chksum.hl()))
qmsg(self.msgs[('check_chksum','record_chksum')[src=='gen']])
@ -225,6 +226,8 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
self.seed_id = SeedID(seed=seed)
seed = seed.get_data()
seed = self.cook_seed(seed)
if self.gen_addrs:
privhex2addr_f = get_privhex2addr_f()
@ -238,7 +241,8 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
pos += 1
qmsg_r('\rGenerating %s #%s (%s of %s)' % (self.gen_desc,num,pos,t_addrs))
if not g.debug:
qmsg_r('\rGenerating %s #%s (%s of %s)' % (self.gen_desc,num,pos,t_addrs))
e = AddrListEntry(idx=num)
@ -252,19 +256,27 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
e.wif = hex2wif(sec,compressed=False)
if opt.b16: e.sec = sec
if self.gen_passwds:
e.sec = self.make_passwd(sec)
dmsg('Key {:>03}: {}'.format(pos,sec))
out.append(e)
qmsg('\r%s: %s %s%s generated%s' % (
self.seed_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15))
return out
def encrypt(self):
def chk_addr_or_pw(self,addr): return is_btc_addr(addr)
def cook_seed(self,seed): return seed
def encrypt(self,desc='new key list'):
from mmgen.crypto import mmgen_encrypt
self.fmt_data = mmgen_encrypt(self.fmt_data,'new key list','')
self.fmt_data = mmgen_encrypt(self.fmt_data.encode('utf8'),desc,'')
self.ext += '.'+g.mmenc_ext
def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False):
fn = '{}.{}'.format(self.id_str,self.ext)
fn = u'{}.{}'.format(self.id_str,self.ext)
ask_tty = self.has_keys and not opt.quiet
write_data_to_file(fn,self.fmt_data,self.file_desc,ask_tty=ask_tty,binary=binary)
@ -359,23 +371,31 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
if not getattr(e,key):
die(3,'missing %s in addr data' % desc)
if type(self) != KeyList: check_attrs('addr','addresses')
if type(self) not in (KeyList,PasswordList): check_attrs('addr','addresses')
if self.has_keys:
if opt.b16: check_attrs('sec','hex keys')
check_attrs('wif','wif keys')
out = [self.msgs['file_header']+'\n']
if self.chksum:
out.append('# {} data checksum for {}: {}'.format(
self.data_desc.capitalize(),self.id_str,self.chksum))
out.append(u'# {} data checksum for {}: {}'.format(
capfirst(self.data_desc),self.id_str,self.chksum))
out.append('# Record this value to a secure location.\n')
out.append('%s {' % self.seed_id)
if type(self) == PasswordList:
out.append(u'{} {} {}:{} {{'.format(
self.seed_id,self.pw_id_str,self.pw_fmt,self.pw_len))
else:
out.append('{} {{'.format(self.seed_id))
fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx))
for e in self.data:
c = ' '+e.label if enable_comments and e.label else ''
if type(self) == KeyList:
out.append(fs.format(e.idx, 'wif: '+e.wif,c))
elif type(self) == PasswordList:
out.append(fs.format(e.idx, e.sec, c))
else: # First line with idx
out.append(fs.format(e.idx, e.addr,c))
if self.has_keys:
@ -391,18 +411,20 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
return 'Key-address file has odd number of lines'
ret = []
while lines:
l = lines.pop(0)
d = l.split(None,2)
if not is_mmgen_idx(d[0]):
return "'%s': invalid address num. in line: '%s'" % (d[0],l)
if not is_btc_addr(d[1]):
return "'%s': invalid Bitcoin address" % d[1]
if not self.chk_addr_or_pw(d[1]):
return "'{}': invalid {}".format(d[1],self.data_desc)
if len(d) != 3: d.append('')
a = AddrListEntry(idx=int(d[0]),addr=d[1],label=d[2])
a = AddrListEntry(**{'idx':int(d[0]),self.main_key:d[1],'label':d[2]})
if self.has_keys:
l = lines.pop(0)
@ -430,30 +452,40 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
def parse_file(self,fn,buf=[],exit_on_error=True):
def do_error(msg):
if exit_on_error: die(3,msg)
msg(msg)
return False
lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True)
try:
sid,obrace = lines[0].split()
except:
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(sid):
errmsg = "'%s': invalid Seed ID" % sid
else:
ret = self.parse_file_body(lines[1:-1])
if type(ret) == list:
return sid,ret
else:
errmsg = ret
if len(lines) < 3:
return do_error("Too few lines in address file (%s)" % len(lines))
if exit_on_error: die(3,errmsg)
msg(errmsg)
return False
ls = lines[0].split()
ls_len = (2,4)[type(self)==PasswordList]
if len(ls) != ls_len:
return do_error("Invalid first line for {} file: '{}'".format(self.gen_desc,lines[0]))
if ls[-1] != '{':
return do_error("'%s': invalid first line" % ls)
if lines[-1] != '}':
return do_error("'%s': invalid last line" % lines[-1])
if not is_mmgen_seed_id(ls[0]):
return do_error("'%s': invalid Seed ID" % ls[0])
if type(self) == PasswordList:
self.pw_id_str = MMGenPWIDString(ls[1])
ss = ls[2].split(':')
if len(ss) != 2:
return do_error("'%s': invalid password length specifier (must contain colon)" % ls[2])
self.set_pw_fmt(ss[0])
self.set_pw_len(ss[1])
ret = self.parse_file_body(lines[1:-1])
if type(ret) != list:
return do_error(ret)
return ls[0],ret
class KeyAddrList(AddrList):
data_desc = 'key-address'
@ -483,6 +515,131 @@ class KeyList(AddrList):
has_keys = True
ext = 'keys'
class PasswordList(AddrList):
msgs = {
'file_header': """
# {pnm} password file
#
# This file is editable.
# Everything following a hash symbol '#' is a comment and ignored by {pnm}.
# A text label of {n} characters or less may be added to the right of each
# password. The label may contain any printable ASCII symbol.
#
""".strip().format(n=MMGenAddrLabel.max_len,pnm=pnm),
'record_chksum': """
Record this checksum: it will be used to verify the password file in the future
""".strip()
}
main_key = 'sec'
data_desc = 'password'
file_desc = 'passwords'
gen_desc = 'password'
gen_desc_pl = 's'
gen_addrs = False
gen_keys = False
gen_passwds = True
has_keys = False
ext = 'pws'
pw_len = None
pw_fmt = None
pw_info = {
'base58': { 'min_len': 8 , 'max_len': 36 ,'dfl_len': 20, 'desc': 'base-58 password' },
'base32': { 'min_len': 10 ,'max_len': 42 ,'dfl_len': 24, 'desc': 'base-32 password' }
}
cook_hash_rounds = 10 # not too many rounds, so hand decoding can still be feasible
def __init__(self,
seed=None,
addr_idxs=None,
pw_id_str=None,
pw_len=None,
infile=None,
chksum_only=False,
pw_fmt=None,
chk_params_only=False
):
self.update_msgs()
if infile:
(self.seed_id,self.data) = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len
else:
for k in seed,addr_idxs: assert chk_params_only or k
for k in pw_id_str,pw_fmt: assert k
self.pw_id_str = MMGenPWIDString(pw_id_str)
self.set_pw_fmt(pw_fmt)
self.set_pw_len(pw_len)
if chk_params_only: return
self.seed_id = seed.sid
self.data = self.generate(seed,addr_idxs)
self.num_addrs = len(self.data)
self.fmt_data = ''
self.chksum = AddrListChksum(self)
if chksum_only:
Msg(self.chksum)
else:
self.id_str = AddrListID(self,fmt_str=u'{}-{}-{}-{}[{{}}]'.format(
self.seed_id,self.pw_id_str,self.pw_fmt,self.pw_len))
qmsg(u'Checksum for {} data {}: {}'.format(self.data_desc,self.id_str.hl(),self.chksum.hl()))
qmsg(self.msgs[('record_chksum','check_chksum')[bool(infile)]])
def set_pw_fmt(self,pw_fmt):
assert pw_fmt in self.pw_info
self.pw_fmt = pw_fmt
def chk_pw_len(self,passwd=None):
if passwd is None:
assert self.pw_len
pw_len = self.pw_len
fs = '{l}: invalid user-requested length for {b} ({c}{m})'
else:
pw_len = len(passwd)
fs = '{pw}: {b} has invalid length {l} ({c}{m} characters)'
d = self.pw_info[self.pw_fmt]
if pw_len > d['max_len']:
die(2,fs.format(l=pw_len,b=d['desc'],c='>',m=d['max_len'],pw=passwd))
elif pw_len < d['min_len']:
die(2,fs.format(l=pw_len,b=d['desc'],c='<',m=d['min_len'],pw=passwd))
def set_pw_len(self,pw_len):
assert self.pw_fmt in self.pw_info
d = self.pw_info[self.pw_fmt]
if pw_len is None:
self.pw_len = d['dfl_len']
return
if not is_int(pw_len):
die(2,"'{}': invalid user-requested password length (not an integer)".format(pw_len,d['desc']))
self.pw_len = int(pw_len)
self.chk_pw_len()
def make_passwd(self,hex_sec):
assert self.pw_fmt in self.pw_info
from mmgen.bitcoin import b58a
alpha,base = ((b58a,58),(b32a,32))[self.pw_fmt=='base32']
# we take least significant part
return ''.join(baseconv.fromhex(base,hex_sec,alpha,pad=self.pw_len))[-self.pw_len:]
def chk_addr_or_pw(self,pw):
if not (is_b58_str,is_b32_str)[self.pw_fmt=='base32'](pw):
msg('Password is not a valid {} string'.format(self.pw_fmt))
return False
if len(pw) != self.pw_len:
msg('Password has incorrect length ({} != {})'.format(len(pw),self.pw_len))
return False
return True
def cook_seed(self,seed):
from mmgen.crypto import sha256_rounds
# Changing either pw_fmt or pw_len will cause a different, unrelated set of passwords to
# be generated: this is what we want
cseed = '{}{}:{}:{}'.format(seed,self.pw_fmt,self.pw_len,self.pw_id_str.encode('utf8'))
dmsg('Cooked seed: {}\nSeed len: {}'.format(repr(cseed),len(cseed)))
return sha256_rounds(cseed,self.cook_hash_rounds)
class AddrData(MMGenObject):
msgs = {

View file

@ -53,6 +53,12 @@ keystrokes will also be used as a source of randomness.
# """.strip(),
}
def sha256_rounds(s,n):
assert is_int(n) and n > 0
for i in range(n):
s = sha256(s).digest()
return s
def encrypt_seed(seed, key):
return encrypt_data(seed, key, iv=1, desc='seed')
@ -237,7 +243,7 @@ def mmgen_decrypt(data,desc='data',hash_preset=''):
m = ('user-requested','default')[hp=='3']
qmsg("Using %s hash preset of '%s'" % (m,hp))
passwd = get_mmgen_passphrase(desc)
key = make_key(passwd, salt, hp)
key = make_key(passwd,salt,hp)
dec_d = decrypt_data(enc_d, key, int(hexlify(iv),16), desc)
if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
vmsg('OK')

View file

@ -74,13 +74,13 @@ def find_files_in_dir(ftype,fdir,no_dups=False):
die(3,"'{}': not a recognized file type".format(ftype))
try: dirlist = os.listdir(fdir)
except: die(3,"ERROR: unable to read directory '{}'".format(fdir))
except: die(3,u"ERROR: unable to read directory '{}'".format(fdir))
matches = [l for l in dirlist if l[-len(ftype.ext)-1:]=='.'+ftype.ext]
if no_dups:
if len(matches) > 1:
die(1,"ERROR: more than one {} file in directory '{}'".format(ftype.__name__,fdir))
die(1,u"ERROR: more than one {} file in directory '{}'".format(ftype.__name__,fdir))
return os.path.join(fdir,matches[0]) if len(matches) else None
else:
return [os.path.join(fdir,m) for m in matches]

View file

@ -22,6 +22,16 @@ main.py - Script launcher for the MMGen suite
def launch(what):
def my_dec(a):
try:
return a.decode('utf8')
except:
sys.stderr.write("Argument '{}' is not a valid UTF-8 string".format(a))
sys.exit(2)
import sys
sys.argv = [my_dec(a) for a in sys.argv]
if what in ('walletgen','walletchk','walletconv','passchg'):
what = 'wallet'
if what == 'keygen': what = 'addrgen'
@ -30,7 +40,7 @@ def launch(what):
except: # Windows
__import__('mmgen.main_' + what)
else:
import sys,os,atexit
import os,atexit
if sys.stdin.isatty():
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)

View file

@ -28,23 +28,24 @@ from mmgen.seed import SeedSource
if sys.argv[0].split('-')[-1] == 'keygen':
gen_what = 'keys'
gen_desc = 'secret keys'
opt_filter = None
note1 = """
By default, both addresses and secret keys are generated.
""".strip()
note2 = 'By default, both addresses and secret keys are generated.\n\n'
else:
gen_what = 'addresses'
gen_desc = 'addresses'
opt_filter = 'hbcdeiHOKlpzPqrSv-'
note1 = """
If available, the external 'keyconv' program will be used for address
generation.
note2 = ''
note1 = """
If available, the secp256k1 library will be used for address generation.
""".strip()
opts_data = {
'sets': [('print_checksum',True,'quiet',True)],
'desc': """Generate a range or list of {what} from an {pnm} wallet,
mnemonic, seed or password""".format(what=gen_what,pnm=g.proj_name),
'usage':'[opts] [infile] <range or list of address indexes>',
'desc': """Generate a range or list of {desc} from an {pnm} wallet,
mnemonic, seed or brainwallet""".format(desc=gen_desc,pnm=g.proj_name),
'usage':'[opts] [seed source] <index list or range(s)>',
'options': """
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
@ -80,9 +81,16 @@ opts_data = {
),
'notes': """
Address indexes are given in a comma-separated list and/or hyphen-separated ranges.
{n}
NOTES FOR THIS COMMAND
Address indexes are given as a comma-separated list and/or hyphen-separated
range(s).
{n2}{n1}
NOTES FOR ALL GENERATOR COMMANDS
{o.pw_note}
@ -91,7 +99,7 @@ Address indexes are given in a comma-separated list and/or hyphen-separated rang
FMT CODES:
{f}
""".format(
n=note1,
n1=note1,n2=note2,
f='\n '.join(SeedSource.format_fmt_codes().splitlines()),
o=opts
)

138
mmgen/main_passgen.py Executable file
View file

@ -0,0 +1,138 @@
#!/usr/bin/env python
#
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
mmgen-passgen: Generate a series or range of passwords from an MMGen
deterministic wallet
"""
from mmgen.common import *
from mmgen.crypto import *
from mmgen.addr import PasswordList,AddrIdxList
from mmgen.seed import SeedSource
from mmgen.obj import MMGenPWIDString
opts_data = {
'sets': [('print_checksum',True,'quiet',True)],
'desc': """Generate a range or list of passwords from an {pnm} wallet,
mnemonic, seed or brainwallet for the given ID string""".format(pnm=g.proj_name),
'usage':'[opts] [seed source] <ID string> <index list or range(s)>',
'options': """
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-b, --base32 Generate passwords in Base32 format instead of Base58
-d, --outdir= d Output files to directory 'd' instead of working dir
-e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry
-i, --in-fmt= f Input is from wallet format 'f' (see FMT CODES below)
-H, --hidden-incog-input-params=f,o Read hidden incognito data from file
'f' at offset 'o' (comma-separated)
-O, --old-incog-fmt Specify old-format incognito input
-L, --passwd-len= l Specify length of generated passwords
(default: {p} chars [base58], {q} chars [base32])
-l, --seed-len= l Specify wallet seed length of 'l' bits. This option
is required only for brainwallet and incognito inputs
with non-standard (< {g.seed_len}-bit) seed lengths
-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
for password hashing (default: '{g.hash_preset}')
-z, --show-hash-presets Show information on available hash presets
-P, --passwd-file= f Get wallet passphrase from file 'f'
-q, --quiet Produce quieter output; suppress some warnings
-r, --usr-randchars=n Get 'n' characters of additional randomness from user
(min={g.min_urandchars}, max={g.max_urandchars}, default={g.usr_randchars})
-S, --stdout Print passwords to stdout
-v, --verbose Produce more verbose output
""".format(
seed_lens=', '.join([str(i) for i in g.seed_lens]),
pnm=g.proj_name,
kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
g=g,
p=PasswordList.pw_info['base58']['dfl_len'],
q=PasswordList.pw_info['base32']['dfl_len']
),
'notes': """
NOTES FOR THIS COMMAND
ID string must be a valid UTF-8 string not longer than {ml} characters and
not containing the symbols '{fs}'.
Password indexes are given as a comma-separated list and/or hyphen-separated
range(s).
Changing either the password format (base32,base58) or length alters the seed
and thus generates a completely new set of passwords.
EXAMPLE:
Generate ten base58 passwords of length {dfl58} for Alice's email account:
{g.prog_name} alice@nowhere.com 1-10
Generate ten base58 passwords of length 16 for Alice's email account:
{g.prog_name} -L16 alice@nowhere.com 1-10
Generate ten base32 passwords of length {dfl32} for Alice's email account:
{g.prog_name} -b alice@nowhere.com 1-10
The three sets of passwords are completely unrelated to each other, so
Alice doesn't need to worry about password reuse.
NOTES FOR ALL GENERATOR COMMANDS
{o.pw_note}
{o.bw_note}
FMT CODES:
{f}
""".format(
f='\n '.join(SeedSource.format_fmt_codes().splitlines()),
o=opts,g=g,
ml=MMGenPWIDString.max_len,
dfl58=PasswordList.pw_info['base58']['dfl_len'],
dfl32=PasswordList.pw_info['base32']['dfl_len'],
fs="', '".join(MMGenPWIDString.forbidden)
)
}
cmd_args = opts.init(opts_data,add_opts=['b16'])
if len(cmd_args) < 2: opts.usage()
idxs = AddrIdxList(fmt_str=cmd_args.pop())
pw_id_str = cmd_args.pop()
sf = get_seed_file(cmd_args,1)
pw_fmt = ('base58','base32')[bool(opt.base32)]
PasswordList(pw_id_str=pw_id_str,pw_len=opt.passwd_len,pw_fmt=pw_fmt,chk_params_only=True)
do_license_msg()
ss = SeedSource(sf)
al = PasswordList(seed=ss.seed,addr_idxs=idxs,pw_id_str=pw_id_str,pw_len=opt.passwd_len,pw_fmt=pw_fmt)
al.format()
if keypress_confirm('Encrypt password list?'):
al.encrypt(desc='password list')
al.write_to_file(binary=True)
else:
al.write_to_file()

View file

@ -17,7 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
obj.py: The MMGenObject class and methods
obj.py: MMGen native classes
"""
from decimal import *
@ -26,72 +26,39 @@ lvl = 0
class MMGenObject(object):
# Pretty-print any object of type MMGenObject, recursing into sub-objects
def __str__(self):
global lvl
indent = lvl * ' '
def fix_linebreaks(v,fixed_indent=None):
if '\n' in v:
i = indent+' ' if fixed_indent == None else fixed_indent*' '
return '\n'+i + v.replace('\n','\n'+i)
else: return repr(v)
def conv(v,col_w):
vret = ''
if type(v) in (str,unicode):
from string import printable
if not (set(list(v)) <= set(list(printable))):
vret = repr(v)
# Pretty-print any object of type MMGenObject, recursing into sub-objects - WIP
def pprint(self): print self.pformat()
def pformat(self,lvl=0):
def do_list(out,e,lvl=0):
add_spc = False
if e and type(e[0]) not in (str,unicode):
out.append('\n')
for i in e:
if hasattr(i,'pformat'):
out.append('{:>{l}}{}'.format('',i.pformat(lvl=lvl+1),l=(lvl+1)*8))
elif type(i) in (str,unicode):
add_spc = True
out.append(u' {}'.format(repr(i)))
elif type(i) == list:
out.append(u'{:>{l}}{:16}'.format('','<'+type(i).__name__+'>',l=(lvl*8)+4))
do_list(out,i,lvl=lvl)
else:
vret = fix_linebreaks(v,fixed_indent=0)
elif type(v) in (int,long,BTCAmt):
vret = str(v)
elif type(v) == dict:
sep = '\n{}{}'.format(indent,' '*4)
cw = (max(len(k) for k in v) if v else 0) + 2
t = sep.join(['{:<{w}}: {}'.format(
repr(k),
(fix_linebreaks(v[k],fixed_indent=0) if type(v[k]) == str else v[k]),
w=cw)
for k in sorted(v)])
vret = '{' + sep + t + '\n' + indent + '}'
elif type(v) in (list,tuple):
sep = '\n{}{}'.format(indent,' '*4)
t = ' '.join([repr(e) for e in sorted(v)])
o,c = (('(',')'),('[',']'))[type(v)==list]
vret = o + sep + t + '\n' + indent + c
elif repr(v)[:14] == '<bound method ':
vret = ' '.join(repr(v).split()[0:3]) + '>'
# vret = repr(v)
return vret or type(v)
out.append(u'{:>{l}}{:16} {}\n'.format('','<'+type(i).__name__+'>',repr(i),l=(lvl*8)+8))
if not e: out.append('{}\n'.format(repr(e)))
if add_spc: out.append('\n')
out = []
def f(k): return k[:2] != '__'
keys = filter(f, self.__dict__.keys())
col_w = max(len(k) for k in keys) if keys else 1
fs = '{}%-{}s: %s'.format(indent,col_w)
methods = [k for k in keys if repr(getattr(self,k))[:14] == '<bound method ']
def f(k): return repr(getattr(self,k))[:14] == '<bound method '
methods = filter(f,keys)
def f(k): return repr(getattr(self,k))[:7] == '<mmgen.'
objects = filter(f,keys)
other = list(set(keys) - set(methods) - set(objects))
for k in sorted(methods) + sorted(other) + sorted(objects):
val = getattr(self,k)
if str(type(val))[:13] == "<class 'mmgen": # recurse into sub-objects
out.append('\n%s%s (%s):' % (indent,k,type(val)))
lvl += 1
out.append(unicode(getattr(self,k))+'\n')
lvl -= 1
out.append(u'<{}>\n'.format(type(self).__name__))
d = self.__dict__
for k in d:
e = getattr(self,k)
if type(e) == list:
out.append(u'{:>{l}}{:<10} {:16}'.format('',k,'<'+type(e).__name__+'>',l=(lvl*8)+4))
do_list(out,e,lvl=lvl)
elif hasattr(e,'pformat') and type(e) != type:
out.append(u'{:>{l}}{:10} {}'.format('',k,e.pformat(lvl=lvl+1),l=(lvl*8)+4))
else:
out.append(fs % (k, conv(val,col_w)))
return repr(self) + '\n ' + '\n '.join(out)
out.append(u'{:>{l}}{:<10} {:16} {}\n'.format('',k,'<'+type(e).__name__+'>',repr(e),l=(lvl*8)+4))
return ''.join(out)
# Descriptor: https://docs.python.org/2/howto/descriptor.html
class MMGenListItemAttr(object):
@ -124,7 +91,7 @@ class MMGenListItem(MMGenObject):
"'{}': attribute '{}' in instance of class '{}' cannot be reassigned".format(
val,attr,type(self).__name__)
attrs_base = ('attrs','attrs_priv','attrs_reassign','attrs_base','attr_error','set_error','__dict__')
attrs_base = ('attrs','attrs_priv','attrs_reassign','attrs_base','attr_error','set_error','__dict__','pformat')
def __init__(self,*args,**kwargs):
if args:
@ -432,36 +399,55 @@ class BitcoinTxID(MMGenTxID):
class MMGenLabel(unicode,Hilite,InitErrors):
color = 'pink'
allowed = u''
allowed = []
forbidden = []
max_len = 0
min_len = 0
desc = 'label'
def __new__(cls,s,on_fail='die',msg=None):
cls.arg_chk(cls,on_fail)
for k in cls.forbidden,cls.allowed:
assert type(k) == list
for ch in k: assert type(ch) == unicode and len(ch) == 1
try:
s = s.decode('utf8').strip()
s = s.strip()
if type(s) != unicode:
s = s.decode('utf8')
except:
m = "'%s: value is not a valid UTF-8 string" % s
m = "'%s': value is not a valid UTF-8 string" % s
else:
from mmgen.util import capfirst
if len(s) > cls.max_len:
m = '%s too long (>%s symbols)' % (cls.desc.capitalize(),cls.max_len)
elif cls.allowed and not set(list(s)).issubset(set(list(cls.allowed))):
m = '%s contains non-permitted symbols: %s' % (cls.desc.capitalize(),
' '.join(set(list(s)) - set(list(cls.allowed))))
m = u"'{}': {} too long (>{} symbols)".format(s,capfirst(cls.desc),cls.max_len)
elif len(s) < cls.min_len:
m = u"'{}': {} too short (<{} symbols)".format(s,capfirst(cls.desc),cls.min_len)
elif cls.allowed and not set(list(s)).issubset(set(cls.allowed)):
m = u"{} '{}' contains non-allowed symbols: {}".format(capfirst(cls.desc),s,
' '.join(set(list(s)) - set(cls.allowed)))
elif cls.forbidden and any([ch in s for ch in cls.forbidden]):
m = u"{} '{}' contains one of these forbidden symbols: '{}'".format(capfirst(cls.desc),s,
"', '".join(cls.forbidden))
else:
return unicode.__new__(cls,s)
return cls.init_fail((msg+'\n' if msg else '') + m,on_fail)
class MMGenWalletLabel(MMGenLabel):
max_len = 48
allowed = [chr(i+32) for i in range(95)]
allowed = [unichr(i+32) for i in range(95)]
desc = 'wallet label'
class MMGenAddrLabel(MMGenLabel):
max_len = 32
allowed = [chr(i+32) for i in range(95)]
allowed = [unichr(i+32) for i in range(95)]
desc = 'address label'
class MMGenTXLabel(MMGenLabel):
max_len = 72
desc = 'transaction label'
class MMGenPWIDString(MMGenLabel):
max_len = 256
min_len = 1
desc = 'password ID string'
forbidden = list(u' :/\\')

View file

@ -104,7 +104,7 @@ class BitcoinRPCConnection(object):
'Authorization': 'Basic {}'.format(base64.b64encode(self.auth_str))
})
except Exception as e:
return die_maybe(None,2,'%s\nUnable to connect to bitcoind' % e)
return die_maybe(None,2,'{}\nUnable to connect to bitcoind at {}:{}'.format(e,self.host,self.port))
r = hc.getresponse() # returns HTTPResponse instance

View file

@ -370,38 +370,15 @@ class Mnemonic (SeedSourceUnenc):
@staticmethod
def _hex2mn_pad(hexnum): return len(hexnum) * 3 / 8
@staticmethod
def baseNtohex(base,words_arg,wl,pad=0): # accepts both string and list input
words = words_arg
if type(words) not in (list,tuple):
words = tuple(words.strip())
if not set(words).issubset(set(wl)):
die(2,'{} is not in base-{} format'.format(repr(words_arg),base))
deconv = [wl.index(words[::-1][i])*(base**i)
for i in range(len(words))]
ret = ('{:0%sx}' % pad).format(sum(deconv))
return ('','0')[len(ret) % 2] + ret
@staticmethod
def hextobaseN(base,hexnum,wl,pad=0):
hexnum = hexnum.strip()
if not is_hexstring(hexnum):
die(2,"'%s': not a hexadecimal number" % hexnum)
num,ret = int(hexnum,16),[]
while num:
ret.append(num % base)
num /= base
return [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]]
@classmethod
def hex2mn(cls,hexnum,wordlist):
wl = cls.get_wordlist(wordlist)
return cls.hextobaseN(cls.mn_base,hexnum,wl,cls._hex2mn_pad(hexnum))
return baseconv.fromhex(cls.mn_base,hexnum,wl,cls._hex2mn_pad(hexnum))
@classmethod
def mn2hex(cls,mn,wordlist):
wl = cls.get_wordlist(wordlist)
return cls.baseNtohex(cls.mn_base,mn,wl,cls._mn2hex_pad(mn))
return baseconv.tohex(cls.mn_base,mn,wl,cls._mn2hex_pad(mn))
@classmethod
def get_wordlist(cls,wordlist=None):
@ -433,9 +410,9 @@ class Mnemonic (SeedSourceUnenc):
def _format(self):
wl = self.get_wordlist()
seed_hex = self.seed.hexdata
mn = self.hextobaseN(self.mn_base,seed_hex,wl,self._hex2mn_pad(seed_hex))
mn = baseconv.fromhex(self.mn_base,seed_hex,wl,self._hex2mn_pad(seed_hex))
ret = self.baseNtohex(self.mn_base,mn,wl,self._mn2hex_pad(mn))
ret = baseconv.tohex(self.mn_base,mn,wl,self._mn2hex_pad(mn))
# Internal error, so just die on fail
compare_or_die(ret,'recomputed seed',
seed_hex,'original',e='Internal error')
@ -458,9 +435,9 @@ class Mnemonic (SeedSourceUnenc):
msg('Invalid mnemonic: word #%s is not in the wordlist' % n)
return False
seed_hex = self.baseNtohex(self.mn_base,mn,wl,self._mn2hex_pad(mn))
seed_hex = baseconv.tohex(self.mn_base,mn,wl,self._mn2hex_pad(mn))
ret = self.hextobaseN(self.mn_base,seed_hex,wl,self._hex2mn_pad(seed_hex))
ret = baseconv.fromhex(self.mn_base,seed_hex,wl,self._hex2mn_pad(seed_hex))
# Internal error, so just die
compare_or_die(' '.join(ret),'recomputed mnemonic',
@ -503,7 +480,7 @@ class SeedFile (SeedSourceUnenc):
msg("'%s': invalid checksum format in %s" % (a, desc))
return False
if not is_b58string(b):
if not is_b58_str(b):
msg("'%s': not a base 58 string, in %s" % (b, desc))
return False
@ -557,7 +534,7 @@ class HexSeedFile (SeedSourceUnenc):
msg("'%s': invalid checksum format in %s" % (chk, desc))
return False
if not is_hexstring(hstr):
if not is_hex_str(hstr):
msg("'%s': not a hexadecimal string, in %s" % (hstr, desc))
return False

View file

@ -33,13 +33,13 @@ from collections import OrderedDict
cmd_data = OrderedDict([
('help', ['<tool command> [str]']),
('usage', ['<tool command> [str]']),
('strtob58', ['<string> [str-]']),
('strtob58', ['<string> [str-]','pad [int=0]']),
('b58tostr', ['<b58 number> [str-]']),
('hextob58', ['<hex number> [str-]']),
('b58tohex', ['<b58 number> [str-]']),
('hextob58', ['<hex number> [str-]','pad [int=0]']),
('b58tohex', ['<b58 number> [str-]','pad [int=0]']),
('b58randenc', []),
('b32tohex', ['<b32 num> [str-]']),
('hextob32', ['<hex num> [str-]']),
('b32tohex', ['<b32 num> [str-]','pad [int=0]']),
('hextob32', ['<hex num> [str-]','pad [int=0]']),
('randhex', ['nbytes [int=32]']),
('id8', ['<infile> [str]']),
('id6', ['<infile> [str]']),
@ -80,6 +80,7 @@ cmd_data = OrderedDict([
('remove_label', ['<{} address> [str]'.format(pnm)]),
('addrfile_chksum', ['<{} addr file> [str]'.format(pnm)]),
('keyaddrfile_chksum', ['<{} addr file> [str]'.format(pnm)]),
('passwdfile_chksum', ['<{} password file> [str]'.format(pnm)]),
('find_incog_data', ['<file or device name> [str]','<Incog ID> [str]','keep_searching [bool=False]']),
('encrypt', ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
@ -137,6 +138,7 @@ cmd_help = """
remove_label - remove descriptive label for {pnm} address in tracking wallet
addrfile_chksum - compute checksum for {pnm} address file
keyaddrfile_chksum - compute checksum for {pnm} key-address file
passwdfile_chksum - compute checksum for {pnm} password 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)
@ -213,6 +215,7 @@ def process_args(prog_name, command, cmd_args):
tool_usage(prog_name,command)
def conv_type(arg,arg_name,arg_type):
if arg_type == 'str': arg_type = 'unicode'
if arg_type == 'bool':
if arg.lower() in ('true','yes','1','on'): arg = True
elif arg.lower() in ('false','no','0','off'): arg = False
@ -266,32 +269,6 @@ def unhexdump(infile):
sys.stdout.write(decode_pretty_hexdump(
get_data_from_file(infile,dash=True,silent=True)))
def strtob58(s):
enc = mmb.b58encode(s)
dec = mmb.b58decode(enc)
print_convert_results(s,enc,dec,'str')
def hextob58(s,f_enc=mmb.b58encode, f_dec=mmb.b58decode):
s = s.strip()
enc = f_enc(ba.unhexlify(s))
dec = ba.hexlify(f_dec(enc))
print_convert_results(s,enc,dec,'hex')
def b58tohex(s,f_enc=mmb.b58decode, f_dec=mmb.b58encode):
s = s.strip()
tmp = f_enc(s)
if tmp == False: die(1,"Unable to decode string '%s'" % s)
enc = ba.hexlify(tmp)
dec = f_dec(ba.unhexlify(enc))
print_convert_results(s,enc,dec,'b58')
def b58tostr(s,f_enc=mmb.b58decode, f_dec=mmb.b58encode):
s = s.strip()
enc = f_enc(s)
if enc == False: die(1,"Unable to decode string '%s'" % s)
dec = f_dec(enc)
print_convert_results(s,enc,dec,'b58')
def b58randenc():
r = get_random(32)
enc = mmb.b58encode(r)
@ -331,7 +308,7 @@ def do_random_mn(nbytes,wordlist):
Vmsg('Seed: %s' % hexrand)
for wlname in ([wordlist],wordlists)[wordlist=='all']:
if wordlist == 'all':
Msg('%s mnemonic:' % (wlname.capitalize()))
Msg('%s mnemonic:' % (capfirst(wlname)))
mn = Mnemonic.hex2mn(hexrand,wordlist=wlname)
Msg(' '.join(mn))
@ -340,20 +317,28 @@ def mn_rand192(wordlist=dfl_wordlist): do_random_mn(24,wordlist)
def mn_rand256(wordlist=dfl_wordlist): do_random_mn(32,wordlist)
def hex2mn(s,wordlist=dfl_wordlist):
mn = Mnemonic.hex2mn(s,wordlist)
Msg(' '.join(mn))
Msg(' '.join(Mnemonic.hex2mn(s,wordlist)))
def mn2hex(s,wordlist=dfl_wordlist):
hexnum = Mnemonic.mn2hex(s.split(),wordlist)
Msg(hexnum)
Msg(Mnemonic.mn2hex(s.split(),wordlist))
def b32tohex(s):
b32a = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
Msg(Mnemonic.baseNtohex(32,s.upper(),b32a))
def strtob58(s,pad=None):
Msg(''.join(baseconv.fromhex(58,ba.hexlify(s),mmb.b58a,pad)))
def hextob32(s):
b32a = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
Msg(''.join(Mnemonic.hextobaseN(32,s,b32a)))
def b58tostr(s):
Msg(ba.unhexlify(baseconv.tohex(58,s,mmb.b58a)))
def b58tohex(s,pad=None):
Msg(baseconv.tohex(58,s,mmb.b58a,pad))
def hextob58(s,pad=None):
Msg(''.join(baseconv.fromhex(58,s,mmb.b58a,pad)))
def b32tohex(s,pad=None):
Msg(baseconv.tohex(32,s.upper(),b32a,pad))
def hextob32(s,pad=None):
Msg(''.join(baseconv.fromhex(32,s,b32a,pad)))
def mn_stats(wordlist=dfl_wordlist):
Mnemonic.check_wordlist(wordlist)
@ -506,6 +491,10 @@ def keyaddrfile_chksum(infile):
from mmgen.addr import KeyAddrList
KeyAddrList(infile,chksum_only=True)
def passwdfile_chksum(infile):
from mmgen.addr import PasswordList
PasswordList(infile=infile,chksum_only=True)
def hexreverse(s):
Msg(ba.hexlify(ba.unhexlify(s.strip())[::-1]))

View file

@ -107,7 +107,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
def sort_info(self,include_group=True):
ret = ([],['Reverse'])[self.reverse]
ret.append(self.sort_key.capitalize().replace('Mmid','MMGenID'))
ret.append(capfirst(self.sort_key).replace('Mmid','MMGenID'))
if include_group and self.group and (self.sort_key in ('addr','txid','mmid')):
ret.append('Grouped')
return ret

View file

@ -31,10 +31,6 @@ def is_mmgen_idx(s): return AddrIdx(s,on_fail='silent')
def is_mmgen_id(s): return MMGenID(s,on_fail='silent')
def is_btc_addr(s): return BTCAddr(s,on_fail='silent')
def is_b58_str(s):
from mmgen.bitcoin import b58a
return set(list(s)) <= set(b58a)
def is_wif(s):
if s == '': return False
from mmgen.bitcoin import wif2hex
@ -376,7 +372,7 @@ class MMGenTX(MMGenObject):
self.view(pager=reply in 'Vv',terse=reply in 'Tt')
def view(self,pager=False,pause=True,terse=False):
o = self.format_view(terse=terse)
o = self.format_view(terse=terse).encode('utf8')
if pager: do_pager(o)
else:
sys.stdout.write(o)
@ -480,9 +476,8 @@ class MMGenTX(MMGenObject):
ts = len(self.hex)/2 if self.hex else 'unknown'
out += 'Transaction size: estimated - {}, actual - {}\n'.format(self.get_size(),ts)
# only tx label may contain non-ascii chars
# encode() is necessary for test suite with PopenSpawn
return out.encode('utf8')
# TX label might contain non-ascii chars
return out
def parse_tx_file(self,infile):

View file

@ -26,50 +26,42 @@ from binascii import hexlify,unhexlify
from string import hexdigits
from mmgen.color import *
def msg(s): sys.stderr.write(s+'\n')
def msg_r(s): sys.stderr.write(s)
def Msg(s): sys.stdout.write(s + '\n')
def Msg_r(s): sys.stdout.write(s)
def msgred(s): sys.stderr.write(red(s+'\n'))
def msg(s): sys.stderr.write(s.encode('utf8') + '\n')
def msg_r(s): sys.stderr.write(s.encode('utf8'))
def Msg(s): sys.stdout.write(s.encode('utf8') + '\n')
def Msg_r(s): sys.stdout.write(s.encode('utf8'))
def msgred(s): msg(red(s))
def mmsg(*args):
for d in args:
sys.stdout.write(repr(d)+'\n')
for d in args: Msg(repr(d))
def mdie(*args):
for d in args:
sys.stdout.write(repr(d)+'\n')
sys.exit()
mmsg(*args); sys.exit()
def die_wait(delay,ev=0,s=''):
assert type(delay) == int
assert type(ev) == int
if s: sys.stderr.write(s+'\n')
if s: msg(s)
time.sleep(delay)
sys.exit(ev)
def die_pause(ev=0,s=''):
assert type(ev) == int
if s: sys.stderr.write(s+'\n')
if s: msg(s)
raw_input('Press ENTER to exit')
sys.exit(ev)
def die(ev=0,s=''):
assert type(ev) == int
if s: sys.stderr.write(s+'\n')
if s: msg(s)
sys.exit(ev)
def Die(ev=0,s=''):
assert type(ev) == int
if s: sys.stdout.write(s+'\n')
if s: Msg(s)
sys.exit(ev)
def pp_format(d):
import pprint
return pprint.PrettyPrinter(indent=4).pformat(d)
def pp_die(d):
import pprint
die(1,pprint.PrettyPrinter(indent=4).pformat(d))
def pp_msg(d):
import pprint
msg(pprint.PrettyPrinter(indent=4).pformat(d))
def pp_die(d): die(1,pp_format(d))
def pp_msg(d): msg(pp_format(d))
def set_for_type(val,refval,desc,invert_bool=False,src=None):
src_str = (''," in '{}'".format(src))[bool(src)]
@ -120,24 +112,24 @@ def check_or_create_dir(path):
from mmgen.opts import opt
def qmsg(s,alt=False):
def qmsg(s,alt=None):
if opt.quiet:
if alt != False: sys.stderr.write(alt + '\n')
else: sys.stderr.write(s + '\n')
def qmsg_r(s,alt=False):
if alt != None: msg(alt)
else: msg(s)
def qmsg_r(s,alt=None):
if opt.quiet:
if alt != False: sys.stderr.write(alt)
else: sys.stderr.write(s)
if alt != None: msg_r(alt)
else: msg_r(s)
def vmsg(s,force=False):
if opt.verbose or force: sys.stderr.write(s + '\n')
if opt.verbose or force: msg(s)
def vmsg_r(s,force=False):
if opt.verbose or force: sys.stderr.write(s)
if opt.verbose or force: msg_r(s)
def Vmsg(s,force=False):
if opt.verbose or force: sys.stdout.write(s + '\n')
if opt.verbose or force: Msg(s)
def Vmsg_r(s,force=False):
if opt.verbose or force: sys.stdout.write(s)
if opt.verbose or force: Msg_r(s)
def dmsg(s):
if opt.debug: sys.stdout.write(s + '\n')
if opt.debug: msg(s)
def suf(arg,suf_type):
t = type(arg)
@ -170,7 +162,7 @@ def make_chksum_8(s,sep=False):
s = sha256(sha256(s).digest()).hexdigest()[:8].upper()
return '{} {}'.format(s[:4],s[4:]) if sep else s
def make_chksum_6(s): return sha256(s).hexdigest()[:6]
def is_chksum_6(s): return len(s) == 6 and is_hexstring_lc(s)
def is_chksum_6(s): return len(s) == 6 and is_hex_str_lc(s)
def make_iv_chksum(s): return sha256(s).hexdigest()[:8].upper()
@ -212,35 +204,55 @@ def secs_to_hms(secs):
def secs_to_ms(secs):
return '{:02d}:{:02d}'.format(secs/60, secs % 60)
def _is_whatstring(s,chars):
return set(list(s)) <= set(chars)
def is_int(s):
try:
int(s)
int(str(s))
return True
except:
return False
def is_hexstring(s):
return _is_whatstring(s.lower(),hexdigits.lower())
def is_hexstring_lc(s):
return _is_whatstring(s,hexdigits.lower())
def is_hexstring_uc(s):
return _is_whatstring(s,hexdigits.upper())
def is_b58string(s):
# https://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet
# https://tools.ietf.org/html/rfc4648
b32a = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
def is_b32_str(s): return set(list(s)) <= set(list(b32a))
def is_hex_str(s): return set(list(s.lower())) <= set(list(hexdigits.lower()))
def is_hex_str_lc(s): return set(list(s)) <= set(list(hexdigits.lower()))
def is_hex_str_uc(s): return set(list(s)) <= set(list(hexdigits.upper()))
def is_b58_str(s):
from mmgen.bitcoin import b58a
return _is_whatstring(s,b58a)
return set(list(s)) <= set(b58a)
def is_utf8(s):
try: s.decode('utf8')
def is_ascii(s,enc='ascii'):
try: s.decode(enc)
except: return False
else: return True
else: return True
def is_ascii(s):
try: s.decode('ascii')
except: return False
else: return True
def is_utf8(s): return is_ascii(s,enc='utf8')
class baseconv(object):
@staticmethod
def tohex(base,words,wl,pad=None): # accepts both string and list input
if type(words) not in (list,tuple):
words = tuple(words.strip())
if not set(words).issubset(set(wl)):
die(2,'{} is not in base-{} format'.format(repr(words_arg),base))
deconv = [wl.index(words[::-1][i])*(base**i)
for i in range(len(words))]
ret = ('{:0{w}x}'.format(sum(deconv),w=pad or 0))
return ('','0')[len(ret) % 2] + ret
@staticmethod
def fromhex(base,hexnum,wl,pad=None):
assert len(wl) == base
hexnum = hexnum.strip()
if not is_hex_str(hexnum):
die(2,"'%s': not a hexadecimal number" % hexnum)
num,ret = int(hexnum,16),[]
while num:
ret.append(num % base)
num /= base
return [wl[n] for n in [0] * ((pad or 0)-len(ret)) + ret[::-1]]
def match_ext(addr,ext):
return addr.split('.')[-1] == ext
@ -444,6 +456,9 @@ def write_data_to_file(
if ask_write_default_yes == False or ask_write_prompt:
ask_write = True
if not binary and type(data) == unicode:
data = data.encode('utf8')
def do_stdout():
qmsg('Output to STDOUT requested')
if sys.stdout.isatty():
@ -537,7 +552,7 @@ def get_words(infile,desc,prompt):
def mmgen_decrypt_file_maybe(fn,desc=''):
d = get_data_from_file(fn,desc,binary=True)
have_enc_ext = get_extension(fn) == g.mmenc_ext
if have_enc_ext or not is_ascii(d):
if have_enc_ext or not is_utf8(d):
m = ('Attempting to decrypt','Decrypting')[have_enc_ext]
msg("%s %s '%s'" % (m,desc,fn))
from mmgen.crypto import mmgen_decrypt_retry
@ -547,7 +562,9 @@ def mmgen_decrypt_file_maybe(fn,desc=''):
def get_lines_from_file(fn,desc='',trim_comments=False):
dec = mmgen_decrypt_file_maybe(fn,desc)
ret = dec.decode('utf8').splitlines() # DOS-safe
return remove_comments(ret) if trim_comments else ret
if trim_comments: ret = remove_comments(ret)
vmsg(u"Got {} lines from file '{}'".format(len(ret),fn))
return ret
def get_data_from_user(desc='data',silent=False):
data = my_raw_input('Enter %s: ' % desc, echo=opt.echo_passphrase)
@ -593,7 +610,7 @@ def my_raw_input(prompt,echo=True,insert_txt='',use_readline=True):
from mmgen.term import kb_hold_protect
kb_hold_protect()
if echo or not sys.stdin.isatty():
reply = raw_input(prompt)
reply = raw_input(prompt.encode('utf8'))
else:
from getpass import getpass
reply = getpass(prompt)

View file

@ -16,16 +16,48 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys,os,subprocess
from shutil import copy2
_gvi = subprocess.check_output(['gcc','--version']).splitlines()[0]
have_mingw64 = 'x86_64' in _gvi and 'MinGW' in _gvi
have_arm = subprocess.check_output(['uname','-m']).strip() == 'aarch64'
# Zipfile module under Windows (MinGW) can't handle UTF-8 filenames.
# Move it so that distutils will use the 'zip' utility instead.
def divert_zipfile_module():
msg1 = 'Unable to divert zipfile module. UTF-8 filenames may be broken in the Python archive.'
def return_warn(m):
sys.stderr.write('WARNING: {}\n'.format(m))
return False
dirname = os.path.dirname(sys.modules['os'].__file__)
if not dirname: return return_warn(msg1)
stem = os.path.join(dirname,'zipfile')
a,b = stem+'.py',stem+'-is-broken.py'
try: os.stat(a)
except: return
try:
sys.stderr.write('moving {} -> {}\n'.format(a,b))
os.rename(a,b)
except:
return return_warn(msg1)
else:
try:
os.unlink(stem+'.pyc')
os.unlink(stem+'.pyo')
except:
pass
if have_mingw64:
# import zipfile
# sys.exit()
divert_zipfile_module()
from distutils.core import setup,Extension
from distutils.command.build_ext import build_ext
from distutils.command.install_data import install_data
import sys,os
from shutil import copy2
import subprocess as sp
_gvi = sp.check_output(['gcc','--version']).splitlines()[0]
have_mingw64 = 'x86_64' in _gvi and 'MinGW' in _gvi
have_arm = sp.check_output(['uname','-m']).strip() == 'aarch64'
# install extension module in repository after building
class my_build_ext(build_ext):
@ -100,6 +132,7 @@ setup(
'mmgen.main',
'mmgen.main_wallet',
'mmgen.main_addrgen',
'mmgen.main_passgen',
'mmgen.main_addrimport',
'mmgen.main_txcreate',
'mmgen.main_txbump',
@ -116,6 +149,7 @@ setup(
scripts=[
'mmgen-addrgen',
'mmgen-keygen',
'mmgen-passgen',
'mmgen-addrimport',
'mmgen-passchg',
'mmgen-walletchk',

View file

@ -121,8 +121,12 @@ if a and b:
gen_a = get_privhex2addr_f(generator=a)
gen_b = get_privhex2addr_f(generator=b)
compressed = False
for i in range(1,rounds+1):
qmsg_r('\rRound %s/%s ' % (i,rounds))
last_t = time.time()
for i in range(rounds):
if time.time() - last_t >= 0.1:
qmsg_r('\rRound %s/%s ' % (i+1,rounds))
last_t = time.time()
sec = hexlify(os.urandom(32))
wif = hex2wif(sec,compressed=compressed)
a_addr = gen_a(sec,compressed)
@ -132,6 +136,7 @@ if a and b:
match_error(sec,wif,a_addr,b_addr,a,b)
if a != 2 and b != 2:
compressed = not compressed
qmsg_r('\rRound %s/%s ' % (i+1,rounds))
qmsg(green(('\n','')[bool(opt.verbose)] + 'OK'))
elif a and not fh:
@ -139,24 +144,27 @@ elif a and not fh:
qmsg(green(m.format(g.key_generators[a-1])))
from mmgen.addr import get_privhex2addr_f
gen_a = get_privhex2addr_f(generator=a)
import time
start = time.time()
from struct import pack,unpack
seed = os.urandom(28)
print 'Incrementing key with each round'
print 'Starting key:', hexlify(seed+pack('I',0))
compressed = False
import time
start = last_t = time.time()
for i in range(rounds):
qmsg_r('\rRound %s/%s ' % (i+1,rounds))
if time.time() - last_t >= 0.1:
qmsg_r('\rRound %s/%s ' % (i+1,rounds))
last_t = time.time()
sec = hexlify(seed+pack('I',i))
wif = hex2wif(sec,compressed=compressed)
a_addr = gen_a(sec,compressed)
vmsg('\nkey: %s\naddr: %s\n' % (wif,a_addr))
if a != 2:
compressed = not compressed
elapsed = int(time.time() - start)
qmsg('')
qmsg('%s addresses generated in %s second%s' % (rounds,elapsed,('s','')[elapsed==1]))
qmsg_r('\rRound %s/%s ' % (i+1,rounds))
qmsg('\n{} addresses generated in {:.2f} seconds'.format(rounds,time.time()-start))
elif a and dump:
m = "Comparing output of address generator '{}' against wallet dump '{}'"
qmsg(green(m.format(g.key_generators[a-1],cmd_args[1])))

View file

@ -0,0 +1,17 @@
# MMGen password file
#
# This file is editable.
# Everything following a hash symbol '#' is a comment and ignored by MMGen.
# A text label of 32 characters or less may be added to the right of each
# password. The label may contain any printable ASCII symbol.
#
# Password data checksum for 98831F3A-фубар@crypto.org-base58-20[1,4,9-11,1100]: 7723 735B 2CBB 2571
# Record this value to a secure location.
98831F3A фубар@crypto.org base58:20 {
1 7ds9PiQt1poHpknpQyNg
4 Dp4s9nWuzCFsdy39p6tk
9 3pPEHJdeF4vid8D7vea4
10 iX5q85oD9hnNfg219ztp
11 vqgKETaoP8yxVUuHYgkf
1100 89i9jmt7s6Nh5PNLdRJH
}

View file

@ -280,6 +280,8 @@ cfgs = {
'ref_bw_seed_id': '33F10310',
'addrfile_chk': ('B230 7526 638F 38CB','B64D 7327 EF2A 60FE')[g.testnet],
'keyaddrfile_chk': ('CF83 32FB 8A8B 08E2','FEBF 7878 97BB CC35')[g.testnet],
'passfile_chk': '3EA0 A3C9 DA28 5126',
'passfile32_chk': 'EF67 D0BE 4B24 9B4F',
'wpasswd': 'reference password',
'ref_wallet': 'FE3C6545-D782B529[128,1].mmdat',
'ic_wallet': 'FE3C6545-E29303EA-5E229E30[128,1].mmincog',
@ -291,6 +293,7 @@ cfgs = {
'tmpdir': os.path.join('test','tmp6'),
'kapasswd': '',
'addr_idx_list': '1010,500-501,31-33,1,33,500,1011', # 8 addresses
'pass_idx_list': '1,4,9-11,1100',
'dep_generators': {
'mmdat': 'refwalletgen1',
pwfile: 'refwalletgen1',
@ -306,6 +309,8 @@ cfgs = {
'ref_bw_seed_id': 'CE918388',
'addrfile_chk': ('8C17 A5FA 0470 6E89','0A59 C8CD 9439 8B81')[g.testnet],
'keyaddrfile_chk': ('9648 5132 B98E 3AD9','2F72 C83F 44C5 0FAC')[g.testnet],
'passfile_chk': '000C 7711 CD45 C5BE',
'passfile32_chk': 'AFEC 54A1 7D79 1866',
'wpasswd': 'reference password',
'ref_wallet': '1378FC64-6F0F9BB4[192,1].mmdat',
'ic_wallet': '1378FC64-2907DE97-F980D21F[192,1].mmincog',
@ -317,6 +322,7 @@ cfgs = {
'tmpdir': os.path.join('test','tmp7'),
'kapasswd': '',
'addr_idx_list': '1010,500-501,31-33,1,33,500,1011', # 8 addresses
'pass_idx_list': '1,4,9-11,1100',
'dep_generators': {
'mmdat': 'refwalletgen2',
pwfile: 'refwalletgen2',
@ -332,13 +338,16 @@ cfgs = {
'ref_bw_seed_id': 'B48CD7FC',
'addrfile_chk': ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E')[g.testnet],
'keyaddrfile_chk': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2')[g.testnet],
'passfile_chk': '54B1 A5BE 9F07 1FDD',
'passfile32_chk': '072A 4A13 FB64 B64B',
'wpasswd': 'reference password',
'ref_wallet': '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.testnet]),
'ref_addrfile': '98831F3A[1,31-33,500-501,1010-1011]{}.addrs'.format(tn_desc),
'ref_keyaddrfile': '98831F3A[1,31-33,500-501,1010-1011]{}.akeys.mmenc'.format(tn_desc),
'ref_passwdfile': '98831F3A-фубар@crypto.org-base58-20[1,4,9-11,1100].pws',
'ref_addrfile_chksum': ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E')[g.testnet],
'ref_keyaddrfile_chksum': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2')[g.testnet],
'ref_passwdfile_chksum': '7723 735B 2CBB 2571',
# 'ref_fake_unspent_data':'98831F3A_unspent.json',
'ref_tx_file': 'FFB367[1.234]{}.rawtx'.format(tn_desc),
'ic_wallet': '98831F3A-5482381C-18460FB1[256,1].mmincog',
@ -350,6 +359,8 @@ cfgs = {
'tmpdir': os.path.join('test','tmp8'),
'kapasswd': '',
'addr_idx_list': '1010,500-501,31-33,1,33,500,1011', # 8 addresses
'pass_idx_list': '1,4,9-11,1100',
'dep_generators': {
'mmdat': 'refwalletgen3',
pwfile: 'refwalletgen3',
@ -470,13 +481,16 @@ cmd_group['ref'] = (
# generating new reference ('abc' brainwallet) files:
('refwalletgen', ([],'gen new refwallet')),
('refaddrgen', (['mmdat',pwfile],'new refwallet addr chksum')),
('refkeyaddrgen', (['mmdat',pwfile],'new refwallet key-addr chksum'))
('refkeyaddrgen', (['mmdat',pwfile],'new refwallet key-addr chksum')),
('refpasswdgen', (['mmdat',pwfile],'new refwallet passwd file chksum')),
('ref_b32passwdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (base32)')),
)
# misc. saved reference data
cmd_group['ref_other'] = (
('ref_addrfile_chk', 'saved reference address file'),
('ref_keyaddrfile_chk','saved reference key-address file'),
('ref_passwdfile_chk', 'saved reference password file'),
# Create the fake inputs:
# ('txcreate8', 'transaction creation (8)'),
('ref_tx_chk', 'saved reference tx file'),
@ -1351,17 +1365,20 @@ class MMGenTestSuite(object):
have_dfl_wallet = False
if not ia: ok()
def addrgen(self,name,wf,pf=None,check_ref=False):
add_args = ([],['-q'] + ([],['-P',pf])[bool(pf)])[ia]
t = MMGenExpect(name,'mmgen-addrgen', add_args +
['-d',cfg['tmpdir']] + ([],[wf])[bool(wf)] + [cfg['addr_idx_list']])
def addrgen(self,name,wf,pf=None,check_ref=False,ftype='addr',id_str=None,extra_args=[]):
ftype,chkfile = ((ftype,'{}file_chk'.format(ftype)),('pass','passfile32_chk'))[ftype=='pass32']
add_args = extra_args + ([],['-q'] + ([],['-P',pf])[bool(pf)])[ia]
dlist = [id_str] if id_str else []
t = MMGenExpect(name,'mmgen-{}gen'.format(ftype), add_args +
['-d',cfg['tmpdir']] + ([],[wf])[bool(wf)] + dlist + [cfg['{}_idx_list'.format(ftype)]])
if ia: return
t.license()
t.passphrase('MMGen wallet',cfg['wpasswd'])
t.expect('Passphrase is OK')
chk = t.expect_getend(r'Checksum for address data .*?: ',regex=True)
desc = ('address','password')[ftype=='pass']
chk = t.expect_getend(r'Checksum for {} data .*?: '.format(desc),regex=True)
if check_ref:
refcheck('address data checksum',chk,cfg['addrfile_chk'])
refcheck('address data checksum',chk,cfg[chkfile])
return
t.written_to_file('Addresses',oo=True)
t.ok()
@ -1702,6 +1719,13 @@ class MMGenTestSuite(object):
def refkeyaddrgen(self,name,wf,pf):
self.keyaddrgen(name,wf,pf,check_ref=True)
def refpasswdgen(self,name,wf,pf):
self.addrgen(name,wf,pf,check_ref=True,ftype='pass',id_str='alice@crypto.org')
def ref_b32passwdgen(self,name,wf,pf):
ea = ['--base32','--passwd-len','17']
self.addrgen(name,wf,pf,check_ref=True,ftype='pass32',id_str='фубар@crypto.org',extra_args=ea)
def txsign_keyaddr(self,name,keyaddr_file,txfile):
t = MMGenExpect(name,'mmgen-txsign', ['-d',cfg['tmpdir'],'-M',keyaddr_file,txfile])
t.license()
@ -2016,6 +2040,9 @@ class MMGenTestSuite(object):
def ref_keyaddrfile_chk(self,name):
self.ref_addrfile_chk(name,ftype='keyaddr')
def ref_passwdfile_chk(self,name):
self.ref_addrfile_chk(name,ftype='passwd')
# def txcreate8(self,name,addrfile):
# self.txcreate_common(name,sources=['8'])
@ -2171,6 +2198,8 @@ class MMGenTestSuite(object):
'ref_brain_chk',
'ref_hincog_chk',
'refkeyaddrgen',
'refpasswdgen',
'ref_b32passwdgen'
):
for i in ('1','2','3'):
locals()[k+i] = locals()[k]

View file

@ -147,7 +147,7 @@ if opt.list_cmds:
import binascii
from mmgen.test import *
from mmgen.tx import is_wif,is_btc_addr,is_b58_str
from mmgen.tx import is_wif,is_btc_addr
class MMGenToolTestSuite(object):