Skip to content

Commit

Permalink
mmgen-passgen: generate passwords from your MMGen seed
Browse files Browse the repository at this point in the history
   - frees you from having to back up or remember passwords
   - generate passwords in either base32 or base58 format
   - user-configurable password length
   - password generation is keyed to an ID string, allowing you to generate
     separate sets of passwords for each online account

   EXAMPLE:
     mmgen-passgen yourname@nowhere.com 1-10
     (generates ten passwords for your email account using your default wallet)

   For more examples and detailed usage information, type 'mmgen-passgen --help'

   This is the new implementation using hmac.  It supersedes commit 85cf5b3

Mnemonic entry mode:
   - Word-by-word mnemonic entry at the prompt
   - Each word is checked individually, user is re-prompted in case of error
   - Activated automatically if stdin is a terminal
  • Loading branch information
mmgen committed Jul 7, 2017
1 parent 85cf5b3 commit fde885f
Show file tree
Hide file tree
Showing 13 changed files with 617 additions and 282 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

Install required Debian/Ubuntu packages:

$ sudo apt-get install python-pip python-dev python-pexpect python-ecdsa python-scrypt libssl-dev git autoconf libtool wipe
$ sudo apt-get install python-pip python-dev python-pexpect python-ecdsa python-scrypt libssl-dev git autoconf libtool wipe python-setuptools

Install the Python Cryptography Toolkit:

$ sudo pip install pycrypto
$ sudo -H pip install pycrypto

Install the secp256k1 library:

Expand All @@ -16,20 +16,24 @@ Install the secp256k1 library:
$ ./configure
$ make
$ sudo make install
$ sudo ldconfig
$ cd ..

Install MMGen:

$ git clone https://github.com/mmgen/mmgen.git
$ cd mmgen
$ git checkout -b stable stable_linux
$ sudo ./setup.py install
$ cd ..

Install vanitygen (optional):

$ sudo apt-get install libpcre3-dev
$ git clone https://github.com/samr7/vanitygen.git
$ cd vanitygen; make
(copy the "keyconv" executable to your execution path)
$ cd ..

Install bitcoind:

Expand Down
4 changes: 4 additions & 0 deletions doc/wiki/install-mswin/Install-MMGen-on-Microsoft-Windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ it still requires patience, and the user experience is less than optimal.
You're urged to use the prebuilt [MMGenLive][20] USB image instead. It's now
the preferred way for all non-Linux users to run MMGen.***

***Windows XP warning: MMGen is no longer officially supported on Windows XP due
to incompatibilities with the Crypto.Random module. The scripts run, but the
security of your random numbers cannot be guaranteed. Use at your own risk.***

Install MMGen on Windows by completing the following three steps:

>> 1. Install MinGW and MSYS ([WinXP][03]|[>=Win7][01]), if you haven't already;
Expand Down
464 changes: 361 additions & 103 deletions doc/wiki/using-mmgen/Getting-Started-with-MMGen.md

Large diffs are not rendered by default.

46 changes: 20 additions & 26 deletions mmgen/addr.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
addr.py: Address generation/display routines for the MMGen suite
"""

from hashlib import sha256, sha512
from hashlib import sha256,sha512
from binascii import hexlify,unhexlify
from mmgen.common import *
from mmgen.bitcoin import privnum2addr,hex2wif,wif2hex
from mmgen.obj import *
Expand Down Expand Up @@ -83,7 +84,6 @@ def _privhex2addr_keyconv(privhex,compressed=False):
def _privhex2addr_secp256k1(privhex,compressed=False):
from mmgen.secp256k1 import priv2pub
from mmgen.bitcoin import hexaddr2addr,pubhex2hexaddr
from binascii import hexlify,unhexlify
pubkey = priv2pub(unhexlify(privhex),int(compressed))
return hexaddr2addr(pubhex2hexaddr(hexlify(pubkey)))

Expand Down Expand Up @@ -275,10 +275,10 @@ def encrypt(self,desc='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):
def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None):
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)
write_data_to_file(fn,self.fmt_data,desc or self.file_desc,ask_tty=ask_tty,binary=binary)

def idxs(self):
return [e.idx for e in self.data]
Expand Down Expand Up @@ -543,35 +543,27 @@ class PasswordList(AddrList):
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' }
'b58': { 'min_len': 8 , 'max_len': 36 ,'dfl_len': 20, 'desc': 'base-58 password' },
'b32': { '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
):
def __init__(self,infile=None,seed=None,pw_idxs=None,pw_id_str=None,pw_len=None,pw_fmt=None,
chksum_only=False,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 seed,pw_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.data = self.generate(seed,pw_idxs)

self.num_addrs = len(self.data)
self.fmt_data = ''
Expand Down Expand Up @@ -618,13 +610,11 @@ def set_pw_len(self,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:]
return ''.join(baseconv.fromhex(hex_sec,self.pw_fmt,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):
if not (is_b58_str,is_b32_str)[self.pw_fmt=='b32'](pw):
msg('Password is not a valid {} string'.format(self.pw_fmt))
return False
if len(pw) != self.pw_len:
Expand All @@ -634,10 +624,14 @@ def chk_addr_or_pw(self,pw):

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)))
# Changing either pw_fmt, pw_len or id_str will cause a different, unrelated set of
# passwords to be generated: this is what we want
fid_str = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str.encode('utf8'))
dmsg(u'Full ID string: {}'.format(fid_str.decode('utf8')))
# Original implementation was 'cseed = seed + fid_str'; hmac was not used
import hmac
cseed = hmac.new(seed,fid_str,sha256).digest()
dmsg('Seed: {}\nCooked seed: {}\nCooked seed len: {}'.format(hexlify(seed),hexlify(cseed),len(cseed)))
return sha256_rounds(cseed,self.cook_hash_rounds)


Expand Down
2 changes: 1 addition & 1 deletion mmgen/globalvars.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def die(ev=0,s=''):
user_entropy = ''
hash_preset = '3'
usr_randchars = 30
stdin_tty = bool(sys.stdin.isatty() or os.getenv('MMGEN_TEST_SUITE'))

max_tx_fee = BTCAmt('0.01')
tx_fee_adj = 1.0
Expand Down Expand Up @@ -144,7 +145,6 @@ def die(ev=0,s=''):
min_urandchars = 10

seed_lens = 128,192,256
mn_lens = [i / 32 * 3 for i in seed_lens]

mmenc_ext = 'mmenc'
salt_len = 16
Expand Down
2 changes: 1 addition & 1 deletion mmgen/main_addrgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,6 @@

if al.gen_keys and keypress_confirm('Encrypt key list?'):
al.encrypt()
al.write_to_file(binary=True)
al.write_to_file(binary=True,desc='encrypted '+al.file_desc)
else:
al.write_to_file()
38 changes: 21 additions & 17 deletions mmgen/main_passgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
from mmgen.seed import SeedSource
from mmgen.obj import MMGenPWIDString

dfl_len = {
'b58': PasswordList.pw_info['b58']['dfl_len'],
'b32': PasswordList.pw_info['b32']['dfl_len']
}

opts_data = {
'sets': [('print_checksum',True,'quiet',True)],
'desc': """Generate a range or list of passwords from an {pnm} wallet,
Expand All @@ -43,7 +48,9 @@
'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])
(default: {d58} chars [base58], {d32} chars [base32]).
An argument of 'h' will generate passwords of half
the default length.
-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
Expand All @@ -58,11 +65,8 @@
-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']
g=g,pnm=g.proj_name,d58=dfl_len['b58'],d32=dfl_len['b32'],
kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)])
),
'notes': """
Expand All @@ -79,13 +83,13 @@
and thus generates a completely new set of passwords.
EXAMPLE:
Generate ten base58 passwords of length {dfl58} for Alice's email account:
Generate ten base58 passwords of length {d58} 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:
Generate ten base32 passwords of length {d32} 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
Expand All @@ -102,10 +106,8 @@
{f}
""".format(
f='\n '.join(SeedSource.format_fmt_codes().splitlines()),
o=opts,g=g,
o=opts,g=g,d58=dfl_len['b58'],d32=dfl_len['b32'],
ml=MMGenPWIDString.max_len,
dfl58=PasswordList.pw_info['base58']['dfl_len'],
dfl32=PasswordList.pw_info['base32']['dfl_len'],
fs="', '".join(MMGenPWIDString.forbidden)
)
}
Expand All @@ -114,25 +116,27 @@

if len(cmd_args) < 2: opts.usage()

idxs = AddrIdxList(fmt_str=cmd_args.pop())
pw_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)]
pw_fmt = ('b58','b32')[bool(opt.base32)]

pw_len = (opt.passwd_len,dfl_len[pw_fmt]/2)[opt.passwd_len in ('h','H')]

PasswordList(pw_id_str=pw_id_str,pw_len=opt.passwd_len,pw_fmt=pw_fmt,chk_params_only=True)
PasswordList(pw_id_str=pw_id_str,pw_len=pw_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 = PasswordList(seed=ss.seed,pw_idxs=pw_idxs,pw_id_str=pw_id_str,pw_len=pw_len,pw_fmt=pw_fmt)

al.format()

if keypress_confirm('Encrypt password list?'):
al.encrypt(desc='password list')
al.write_to_file(binary=True)
al.write_to_file(binary=True,desc='encrypted password list')
else:
al.write_to_file()
al.write_to_file(desc='password list')
Loading

0 comments on commit fde885f

Please sign in to comment.