TQ
dev.com

Blog about software development

Subscribe

A lesspass implementation in PHP

28 Mar 2017 - by 'Maurits van der Schee'

I like the idea of lesspass, a password manager without a database. I'm not 100% sure that it is secure, but I am 100% sure that passwords are a problem and that we need to solve it. This system 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.

Security and bad practices

I don't like the implementation of lesspass in a browser plugin. I don't like browser plugins at all. Not to talk about the site widget: You should never enter your master password in any site. I strongly advise you to not do that on lesspass.com either. So, many complaints, but what would I like? Well, maybe we can have an open source and standalone application that works from the system tray (or notification area) in Linux, OSX and Windows, that would be great!

PHP implementation of lesspass

To make some first steps towards such a tool I ported the JavaScript implementation to PHP7 (requires php7-gmp):

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

function getPasswordProfile($passwordProfile) {
  $defaultPasswordProfile = (object)[
    'lowercase'=>true,
    'uppercase'=>true,
    'numbers'=>true,
    'symbols'=>true,
    'digest'=>'sha256',
    'iterations'=>100000,
    'keylen'=>32,
    'length'=>16,
    'counter'=>1,
    'version'=>2
  ];
  return (object)array_merge((array)$defaultPasswordProfile,(array)$passwordProfile);
}

function generatePassword($site, $login, $masterPassword, $passwordProfile=null) {
  $passwordProfile = getPasswordProfile($passwordProfile);
  $entropy = calcEntropy($site, $login, $masterPassword, $passwordProfile);
  return renderPassword($entropy, $passwordProfile);
}

function calcEntropy($site, $login, $masterPassword, $passwordProfile) {
  $salt = $site . $login . dechex($passwordProfile->counter);
  return hash_pbkdf2($passwordProfile->digest, $masterPassword, $salt, $passwordProfile->iterations, $passwordProfile->keylen*2);
}

function getSetOfCharacters($rules=null) {
  global $characterSubsets;
  if (!$rules) {
    return $characterSubsets->lowercase . $characterSubsets->uppercase . $characterSubsets->numbers . $characterSubsets->symbols;
  }
  $setOfChars = '';
  foreach ($rules as $rule) {
    $setOfChars .= $characterSubsets->$rule;
  }
  return $setOfChars;
}

function consumeEntropy($generatedPassword, $quotient, $setOfCharacters, $maxLength) {
  if (strlen($generatedPassword) >= $maxLength) {
    return [$generatedPassword, $quotient];
  }
  list($quotient,$remainder) = gmp_div_qr($quotient, strlen($setOfCharacters));
  $generatedPassword .= $setOfCharacters[(int)$remainder];
  return consumeEntropy($generatedPassword, $quotient, $setOfCharacters, $maxLength);
}

function insertStringPseudoRandomly($generatedPassword, $entropy, $string) {
  for ($i = 0; $i < strlen($string); $i++) {
    list($quotient,$remainder) = gmp_div_qr($entropy, strlen($generatedPassword));
    $generatedPassword = substr($generatedPassword, 0, (int)$remainder) . $string[$i] . substr($generatedPassword, (int)$remainder);
    $entropy = $quotient;
  }
  return $generatedPassword;
}

function getOneCharPerRule($entropy, $rules) {
  global $characterSubsets;
  $oneCharPerRules = '';
  foreach ($rules as $rule) {
    list($value,$entropy) = consumeEntropy('', $entropy, $characterSubsets->$rule, 1);
    $oneCharPerRules .= $value;
  }
  return [$oneCharPerRules, $entropy];
}

function getConfiguredRules($passwordProfile) {
  return array_merge(array_filter(['lowercase', 'uppercase', 'numbers', 'symbols'], function ($rule) use ($passwordProfile) {
    return isset($passwordProfile->$rule) && $passwordProfile->$rule;
  }));
}

function renderPassword($entropy, $passwordProfile) {
  $rules = getConfiguredRules($passwordProfile);
  $setOfCharacters = getSetOfCharacters($rules);
  list($password,$passwordEntropy) = consumeEntropy('', gmp_init($entropy,16), $setOfCharacters, $passwordProfile->length - count($rules));
  list($charactersToAdd,$characterEntropy) = getOneCharPerRule($passwordEntropy, $rules);
  return insertStringPseudoRandomly($password, $characterEntropy, $charactersToAdd);
}

// should echo: WHLpUL)e00[iHR+w
echo generatePassword('example.org', 'contact@example.org', 'password')."\n";

There are ports to other languages as well (to Go for instance and my implementation in Python), but as far as I know this is the first implementation in PHP. As always you can find my code on my Github account.


PS: Liked this article? Please share it on Facebook, Twitter or LinkedIn.