diff --git a/mmgen-passgen b/mmgen-passgen new file mode 100755 index 00000000..0216710e --- /dev/null +++ b/mmgen-passgen @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2017 Philemon +# +# 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 . + +""" +mmgen-passgen: Generate a series or range of passwords from an MMGen + deterministic wallet +""" + +from mmgen.main import launch +launch("passgen") diff --git a/mmgen/addr.py b/mmgen/addr.py index c65f4d06..db452e44 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -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 = { diff --git a/mmgen/crypto.py b/mmgen/crypto.py index 9d2a4e0e..d68c05eb 100755 --- a/mmgen/crypto.py +++ b/mmgen/crypto.py @@ -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') diff --git a/mmgen/filename.py b/mmgen/filename.py index c7951a65..edb94638 100755 --- a/mmgen/filename.py +++ b/mmgen/filename.py @@ -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] diff --git a/mmgen/main.py b/mmgen/main.py index 1fa1cb47..1aaac2d7 100755 --- a/mmgen/main.py +++ b/mmgen/main.py @@ -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) diff --git a/mmgen/main_addrgen.py b/mmgen/main_addrgen.py index 4932ea6b..e53f0ded 100755 --- a/mmgen/main_addrgen.py +++ b/mmgen/main_addrgen.py @@ -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] ', + '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] ', '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 ) diff --git a/mmgen/main_passgen.py b/mmgen/main_passgen.py new file mode 100755 index 00000000..4f0593f8 --- /dev/null +++ b/mmgen/main_passgen.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2017 Philemon +# +# 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 . + +""" +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] ', + '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() diff --git a/mmgen/obj.py b/mmgen/obj.py index e6af715c..bd12e1cf 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -17,7 +17,7 @@ # along with this program. If not, see . """ -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] == '' -# 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] == '\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' :/\\') diff --git a/mmgen/rpc.py b/mmgen/rpc.py index fd51a049..2b2d2461 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -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 diff --git a/mmgen/seed.py b/mmgen/seed.py index 62c1473e..03e0ca28 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -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 diff --git a/mmgen/tool.py b/mmgen/tool.py index 78d4bea6..24abe045 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -33,13 +33,13 @@ from collections import OrderedDict cmd_data = OrderedDict([ ('help', [' [str]']), ('usage', [' [str]']), - ('strtob58', [' [str-]']), + ('strtob58', [' [str-]','pad [int=0]']), ('b58tostr', [' [str-]']), - ('hextob58', [' [str-]']), - ('b58tohex', [' [str-]']), + ('hextob58', [' [str-]','pad [int=0]']), + ('b58tohex', [' [str-]','pad [int=0]']), ('b58randenc', []), - ('b32tohex', [' [str-]']), - ('hextob32', [' [str-]']), + ('b32tohex', [' [str-]','pad [int=0]']), + ('hextob32', [' [str-]','pad [int=0]']), ('randhex', ['nbytes [int=32]']), ('id8', [' [str]']), ('id6', [' [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', [' [str]',' [str]','keep_searching [bool=False]']), ('encrypt', [' [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])) diff --git a/mmgen/tw.py b/mmgen/tw.py index bcbba093..a6e5ab56 100755 --- a/mmgen/tw.py +++ b/mmgen/tw.py @@ -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 diff --git a/mmgen/tx.py b/mmgen/tx.py index c9062fad..2deabcb4 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -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): diff --git a/mmgen/util.py b/mmgen/util.py index 30fa6219..2cf48a48 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -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) diff --git a/setup.py b/setup.py index 456f3ba4..62e189be 100755 --- a/setup.py +++ b/setup.py @@ -16,16 +16,48 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +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', diff --git a/test/gentest.py b/test/gentest.py index 78e86848..4e0184bd 100755 --- a/test/gentest.py +++ b/test/gentest.py @@ -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]))) diff --git a/test/ref/98831F3A-фубар@crypto.org-base58-20[1,4,9-11,1100].pws b/test/ref/98831F3A-фубар@crypto.org-base58-20[1,4,9-11,1100].pws new file mode 100644 index 00000000..b2c070f8 --- /dev/null +++ b/test/ref/98831F3A-фубар@crypto.org-base58-20[1,4,9-11,1100].pws @@ -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 +} diff --git a/test/test.py b/test/test.py index e45f6ec0..38f9f0d6 100755 --- a/test/test.py +++ b/test/test.py @@ -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] diff --git a/test/tooltest.py b/test/tooltest.py index b0d21c59..72638a1f 100755 --- a/test/tooltest.py +++ b/test/tooltest.py @@ -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):