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:

  1. Supply a service, i.e. reddit
  2. Have in mind a master password
  3. 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:

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:

  1. Did I enter "reddit" or "reddit.com" as the service?
  2. What if the service requires letters and numbers, but no symbols?
  3. 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:

  1. Use SSL as you will, at some point, be POSTing your master password
  2. 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)

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!


Commenting has been closed.