TQ
dev.com

Blog about software development

Subscribe to RSS

A lesspass implementation in Python

30 Mar 2017 - by 'Maurits van der Schee'

Lesspass is a password manager without a database. Although I'm not 100% sure that it is secure, I am 100% sure that passwords are a problem that needs to be solved. Lesspass allows you to generate a password from a site name and a master password with certain characteristics. To do so it applies a 100000 iteration pbkdf2 algorithm using a SHA256 hash. It sounds good to me and I like the way that that is supposed to work.

Didn't you write a port to PHP?

Yes, there are ports to other languages as well (to Go for instance) and I recently did a port to PHP. Fortunately the original code has some functional and unit tests, so it is quite easy to check whether or not your implementation works correctly.

Python implementation of lesspass

In order to make it easy for others to create lesspass based tools I implemented the core of lesspass to Python. I made the code so, that it runs both on Python 2 as on Python 3. I tried to use as little dependencies as I could and of course I implemented all tests. This is the code:

import hashlib
import binascii

CHARACTER_SUBSETS = {
    'lowercase': 'abcdefghijklmnopqrstuvwxyz',
    'uppercase': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
    'numbers': '0123456789',
    'symbols': '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
}

def get_password_profile(password_profile):
    default_password_profile = {
        'lowercase': True,
        'uppercase': True,
        'numbers': True,
        'symbols': True,
        'digest': 'sha256',
        'iterations': 100000,
        'keylen': 32,
        'length': 16,
        'counter': 1,
        'version': 2
    }
    result = default_password_profile.copy()
    if password_profile != None:
        result.update(password_profile)
    return result

def generate_password(site, login, master_password, password_profile=None):
    password_profile = get_password_profile(password_profile)
    entropy = calc_entropy(site, login, master_password, password_profile)
    return render_password(entropy, password_profile)

def calc_entropy(site, login, master_password, password_profile):
    salt = site + login + hex(password_profile['counter'])[2:]
    return binascii.hexlify(hashlib.pbkdf2_hmac(
        password_profile['digest'],
        master_password.encode('utf-8'),
        salt.encode('utf-8'),
        password_profile['iterations'],
        password_profile['keylen']
    ))

def get_set_of_characters(rules=None):
    if rules is None:
        return (
            CHARACTER_SUBSETS['lowercase'] +
            CHARACTER_SUBSETS['uppercase'] +
            CHARACTER_SUBSETS['numbers'] +
            CHARACTER_SUBSETS['symbols']
        )
    set_of_chars = ''
    for rule in rules:
        set_of_chars += CHARACTER_SUBSETS[rule]
    return set_of_chars

def consume_entropy(generated_password, quotient, set_of_characters, max_length):
    if len(generated_password) >= max_length:
        return [generated_password, quotient]
    quotient, remainder = divmod(quotient, len(set_of_characters))
    generated_password += set_of_characters[remainder]
    return consume_entropy(generated_password, quotient, set_of_characters, max_length)

def insert_string_pseudo_randomly(generated_password, entropy, string):
    for letter in string:
        quotient, remainder = divmod(entropy, len(generated_password))
        generated_password = (
            generated_password[:remainder] +
            letter +
            generated_password[remainder:]
        )
        entropy = quotient
    return generated_password

def get_one_char_per_rule(entropy, rules):
    one_char_per_rules = ''
    for rule in rules:
        value, entropy = consume_entropy('', entropy, CHARACTER_SUBSETS[rule], 1)
        one_char_per_rules += value
    return [one_char_per_rules, entropy]

def get_configured_rules(password_profile):
    rules = ['lowercase', 'uppercase', 'numbers', 'symbols']
    return [rule for rule in rules if rule in password_profile and password_profile[rule]]

def render_password(entropy, password_profile):
    rules = get_configured_rules(password_profile)
    set_of_characters = get_set_of_characters(rules)
    password, password_entropy = consume_entropy(
        '',
        int(entropy, 16),
        set_of_characters,
        password_profile['length'] - len(rules)
    )
    characters_to_add, character_entropy = get_one_char_per_rule(password_entropy, rules)
    return insert_string_pseudo_randomly(password, character_entropy, characters_to_add)

# should print: WHLpUL)e00[iHR+w
print generate_password('example.org', 'contact@example.org', 'password')

As always you can find my code on my Github account.