Creating a personal password manager
My password "system" used to be that I had three different passwords, all of which were variations on the same theme. I maintained a list of sites I had accounts on and for each site gave a hint which of the three passwords I used. What a terrible scheme.
A couple weeks ago I decided to do something about it. I wanted, above all, to only have to remember a single password. Being lately security-conscious, I also recognized the need for a unique password on every site.
In this post I'll show how I used python to create a password management system that allows me to use a single "master" password to generate unique passwords for all the sites and services I use.
Hashing to the rescue
I realized I could use a hash function to transform the unique combination of a service (like "reddit") and my master password ("p@ssw0rd") to create a unique password that would not be susceptible to dictionary-style cracking. Using python, this was super easy.
The way it works is:
- Supply a service, i.e. reddit
- Have in mind a master password
- Combine the service and the master password, run it through a hashing function, and treat the output as the real password.
The first version I wrote was a simple command-line utility. I realized that I would need something web-based so I could generate passwords from my phone, but for simplicity I'll just describe the basic python code and leave it to you to build a webservice if you're interested (you could use flask!).
Generating unique passwords
hashlib module provides implementations of several cryptographic
hash functions, including SHA-256. A
first stab at generating the password might look like this:
>>> from hashlib import sha256 >>> def get_hexdigest(salt, password): ... return sha256(salt + password).hexdigest()
Here is what it looks like when I try to generate my password for reddit:
>>> get_hexdigest('reddit', 'p@ssw0rd') 'afb0e072194a3f4e8ed39290e16bc2f458426714fc308dc28565d6205453eaa5'
OK, cool, this is a good start -- the
get_hexdigest function takes two
strings and generates a hex representation of the hash. Since the master password
and service name may be guessable, I want to add just a little extra magic to
the mix in the form of a secret key, which will be combined with the service
name and master password.
Here is the "improved" version:
def get_hexdigest(salt, plaintext): return sha256(salt + plaintext).hexdigest() SECRET_KEY = 's3cr3t' def make_password(plaintext, service): salt = get_hexdigest(SECRET_KEY, service)[:20] hsh = get_hexdigest(salt, plaintext) return ''.join((salt, hsh))
This is an improvement, but there are still problems. It generates really long passwords that are made of '0-9' and 'a-f' lowercase. Since some sites require a mixture of upper and lower-case, numbers or symbols, we need to convert this hex digest into something suitable for a password.
The solution I came up with was to convert the hexadecimal representation into a base-N number, then treat the digits of the base-N number as indices into an alphabet of valid characters. This alphabet would contain all characters that comprise a valid password and might be:
- All upper and lower-case characters
- All digits
- The symbols
This comes out to a total of 74 characters. Here is how I convert the hexadecimal number into a base-74 representation using my special alphabet:
ALPHABET = ('abcdefghijklmnopqrstuvwxyz' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' '0123456789!@#$%^&*()-_') def password(plaintext, service, length=10, alphabet=ALPHABET): raw_hexdigest = make_password(plaintext, service) # Convert the hexdigest into decimal num = int(raw_hexdigest, 16) # What base will we convert `num` into? num_chars = len(alphabet) # Build up the new password one "digit" at a time, # up to a certain length chars =  while len(chars) < length: num, idx = divmod(num, num_chars) chars.append(alphabet[idx]) return ''.join(chars)
Here is what the generated passwords look like now:
>>> password('p@ssw0rd', 'reddit') '&7Ulu!UXdj'
Note that even one small change will completely alter the generated password:
>>> password('p@ssw0rD', 'reddit') '^Zv3a&uxCG' >>> password('p@ssw0rd', 'Reddit') '_!p(v0mS*V'
Making it usable
This seems like a pretty good scheme, but there are a couple problems:
- Did I enter "reddit" or "reddit.com" as the service?
- What if the service requires letters and numbers, but no symbols?
- What if the service has odd password length requirements?
from peewee import * db = SqliteDatabase('accounts.db') class Service(Model): name = CharField() length = IntegerField(default=8) symbols = BooleanField(default=True) alphabet = CharField(default='') class Meta: database = db # create the table if it does not exist db.create_table(Service, True)
As you can see from the class definition, I am storing the name of the
service, how long the generated password should be,
whether we can include symbols, and lastly an override in case we want to
specify a custom "alphabet" from which to generate the passwords. I've also
implemented some convenience methods on the
class Service(Model): name = CharField() length = IntegerField(default=8) symbols = BooleanField(default=True) alphabet = CharField(default='') class Meta: database = db def get_alphabet(self): if self.alphabet: return self.alphabet alpha = ('abcdefghijklmnopqrstuvwxyz' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' '0123456789') if self.symbols: alpha += '!@#$%^&*()-_' return alpha def password(self, plaintext): return password(plaintext, self.name, self.length, self.get_alphabet()) @classmethod def search(cls, q): # A convenient way to search for services by name. return cls.select().where(cls.name ** ('%%%s%%' % q))
This is how I would interact with the new service class:
>>> reddit = Service.create(name='reddit', length=8, symbols=True) >>> reddit.password('p@ssw0rd') '&7Ulu!UX'
Turning it into a web-service
I didn't realize how much I use the phone to browse the web until I implemented this password scheme. At first it was a pain trying to get my phone's browser logged into the various sites I used.
I decided to build out a web-based interface which allows me to manage my list of services and generate passwords. If you do this, here are a couple recommendations:
- Use SSL as you will, at some point, be POSTing your master password
- Display the generated password in a textbox, pre-selected for easy copy/paste
Thanks for reading
Thanks for taking the time to read this post, I hope it was interesting. Please feel free to leave any suggestions or comments below.
If you're interested in more projects like this, check out the saturday-morning hack posts.
Commenting has been closed.