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
Python's 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?
To solve this I persisted the services in a sqlite
database. Of course, I used the peewee ORM
to work with these Service objects. Here is what the Service
model looks like:
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 Service
class:
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.
Comments (1)
Commenting has been closed.
Mapio | may 03 2013, at 04:28am
I choose a similar (albeit much simpler path) with
http://documentup.com/mapio/nswp…
just to suggest that adding a Chrome extension really made my day!