gettext(['name' => 'password.pwned_mustnotbedisclosed', 'vars' => ['href' => $href]])]; } /** * 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 */ public 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 [$score, $message]; } /** * Check password using HIBP. * * @param string $passwd * * @return int score, one of the SCORE_* constants (between 1 and 3) */ public function check_pwned($passwd) { // initialize with error score $result = self::SCORE_ERROR; [$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; } public function hash_split($passwd) { $hash = strtolower(sha1($passwd)); $prefix = substr($hash, 0, 5); $suffix = substr($hash, 5); return [$prefix, $suffix]; } public function retrieve_suffixes($url) { $client = password::get_http_client(); $options = ['http_errors' => true, 'headers' => []]; // @phpstan-ignore-next-line if (self::ENHANCED_PRIVACY == 1) { $options['headers']['Add-Padding'] = 'true'; } try { $response = $client->get($url, $options); return $response->getBody(); } catch (Exception $e) { rcube::raise_error("Password plugin: Error fetching {$url} : {$e->getMessage()}", true); } return null; } public 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; } }