You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
224 lines
6.5 KiB
PHP
224 lines
6.5 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Have I Been Pwned Password Strength Driver
|
|
*
|
|
* Driver to check passwords using HIBP:
|
|
* https://haveibeenpwned.com/Passwords
|
|
*
|
|
* This driver will return a strength of:
|
|
* 3: if the password WAS NOT found in HIBP
|
|
* 1: if the password WAS found in HIBP
|
|
* 2: if there was an ERROR retrieving data.
|
|
*
|
|
* To use this driver, configure (in ../config.inc.php):
|
|
*
|
|
* $config['password_strength_driver'] = 'pwned';
|
|
* $config['password_minimum_score'] = 3;
|
|
*
|
|
* Set the minimum score to 3 if you want to make sure that all
|
|
* passwords are successfully checked against HIBP (recommended).
|
|
*
|
|
* Set it to 2 if you still want to accept passwords in case a
|
|
* HIBP check fails for some (technical) reason.
|
|
*
|
|
* Setting the minimum score to 1 or less effectively renders
|
|
* the checks useless, as all passwords would be accepted.
|
|
* Setting it to 4 or more will effectively reject all passwords.
|
|
*
|
|
* This driver will only return a maximum score of 3 because not
|
|
* being listed in HIBP does not necessarily mean that the
|
|
* password is a good one. It is therefore recommended to also
|
|
* configure a minimum length for the password.
|
|
*
|
|
* Background reading (don't worry, your passwords are not sent anywhere):
|
|
* https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/#cloudflareprivacyandkanonymity
|
|
*
|
|
* @version 1.0
|
|
* @author Christoph Langguth
|
|
*
|
|
* Copyright (C) The Roundcube Dev Team
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see http://www.gnu.org/licenses/.
|
|
*/
|
|
|
|
class rcube_pwned_password
|
|
{
|
|
// API URL. Note: the trailing slash is mandatory.
|
|
const API_URL = 'https://api.pwnedpasswords.com/range/';
|
|
|
|
// See https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
|
|
const ENHANCED_PRIVACY_CURL = 1;
|
|
|
|
// Score constants, these directly correspond to the score that is returned.
|
|
const SCORE_LISTED = 1;
|
|
const SCORE_ERROR = 2;
|
|
const SCORE_NOT_LISTED = 3;
|
|
|
|
/**
|
|
* Rule description.
|
|
*
|
|
* @return array human-readable description of the check rule.
|
|
*/
|
|
function strength_rules()
|
|
{
|
|
$rc = rcmail::get_instance();
|
|
return array($rc->gettext('password.pwned_mustnotbedisclosed'));
|
|
}
|
|
|
|
/**
|
|
* Password strength check.
|
|
* Return values:
|
|
* 1 - if password is definitely compromised.
|
|
* 2 - if status for password can't be determined (network failures etc.)
|
|
* 3 - if password is not publicly known to be compromised.
|
|
*
|
|
* @param string $passwd Password
|
|
*
|
|
* @return array password score (1 to 3) and (optional) reason message
|
|
*/
|
|
function check_strength($passwd)
|
|
{
|
|
$score = $this->check_pwned($passwd);
|
|
$message = null;
|
|
|
|
if ($score !== self::SCORE_NOT_LISTED) {
|
|
$rc = rcmail::get_instance();
|
|
if ($score === self::SCORE_LISTED) {
|
|
$message = $rc->gettext('password.pwned_isdisclosed');
|
|
}
|
|
else {
|
|
$message = $rc->gettext('password.pwned_fetcherror');
|
|
}
|
|
}
|
|
|
|
return array($score, $message);
|
|
}
|
|
|
|
/**
|
|
* Check password using HIBP.
|
|
*
|
|
* @param string $passwd
|
|
*
|
|
* @return int score, one of the SCORE_* constants (between 1 and 3).
|
|
*/
|
|
function check_pwned($passwd)
|
|
{
|
|
// initialize with error score
|
|
$result = self::SCORE_ERROR;
|
|
|
|
if (!$this->can_retrieve()) {
|
|
// Log the fact that we cannot check because of configuration error.
|
|
rcube::raise_error("Need curl or allow_url_fopen to check password strength with 'pwned'", true, true);
|
|
}
|
|
else {
|
|
list($prefix, $suffix) = $this->hash_split($passwd);
|
|
|
|
$suffixes = $this->retrieve_suffixes(self::API_URL . $prefix);
|
|
|
|
if ($suffixes) {
|
|
$result = $this->check_suffix_in_list($suffix, $suffixes);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
function hash_split($passwd)
|
|
{
|
|
$hash = strtolower(sha1($passwd));
|
|
$prefix = substr($hash, 0, 5);
|
|
$suffix = substr($hash, 5);
|
|
|
|
return array($prefix, $suffix);
|
|
}
|
|
|
|
function can_retrieve()
|
|
{
|
|
return $this->can_curl() || $this->can_fopen();
|
|
}
|
|
|
|
function can_curl()
|
|
{
|
|
return function_exists('curl_init');
|
|
}
|
|
|
|
function can_fopen()
|
|
{
|
|
return ini_get('allow_url_fopen');
|
|
}
|
|
|
|
function retrieve_suffixes($url)
|
|
{
|
|
if ($this->can_curl()) {
|
|
return $this->retrieve_curl($url);
|
|
}
|
|
else {
|
|
return $this->retrieve_fopen($url);
|
|
}
|
|
}
|
|
|
|
function retrieve_curl($url)
|
|
{
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
|
if (self::ENHANCED_PRIVACY_CURL == 1) {
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Add-Padding: true'));
|
|
}
|
|
$output = curl_exec($ch);
|
|
curl_close($ch);
|
|
|
|
return $output;
|
|
}
|
|
|
|
function retrieve_fopen($url)
|
|
{
|
|
$output = '';
|
|
$ch = fopen($url, 'r');
|
|
while (!feof($ch)) {
|
|
$output .= fgets($ch);
|
|
}
|
|
fclose($ch);
|
|
|
|
return $output;
|
|
}
|
|
|
|
function check_suffix_in_list($candidate, $list)
|
|
{
|
|
// initialize to error in case there are no lines at all
|
|
$result = self::SCORE_ERROR;
|
|
|
|
foreach (preg_split('/[\r\n]+/', $list) as $line) {
|
|
$line = strtolower($line);
|
|
|
|
if (preg_match('/^([0-9a-f]{35}):(\d+)$/', $line, $matches)) {
|
|
if ($matches[2] > 0 && $matches[1] === $candidate) {
|
|
// more than 0 occurrences, and suffix matches
|
|
// -> password is compromised
|
|
return self::SCORE_LISTED;
|
|
}
|
|
|
|
// valid line, not matching the current password
|
|
$result = self::SCORE_NOT_LISTED;
|
|
}
|
|
else {
|
|
// invalid line
|
|
return self::SCORE_ERROR;
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
}
|