0
0
mirror of https://github.com/postfixadmin/postfixadmin.git synced 2024-09-19 11:12:15 +02:00

Merge remote-tracking branch 'origin/master' into issue-327-collation-schema-update

This commit is contained in:
David Goodwin 2021-10-27 22:04:36 +01:00
commit df76f7b4ae
12 changed files with 721 additions and 115 deletions

View File

@ -3,7 +3,38 @@ name: GitHubBuild
on: [push]
jobs:
build:
lint_etc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.4
tools: composer
- name: run install.sh
run: /bin/bash install.sh
- name: check composer
run: composer validate
# Needing to 'update' here isn't ideal, but we need to cope with tests that run under different PHP versions :-/
- name: Install dependencies
run: composer update --prefer-dist -n
- name: check formatting
run: composer check-format
- name: touch config.local.php
run: touch config.local.php
- name: psalm static analysis
run: composer psalm
testsuite:
needs: [lint_etc]
runs-on: ubuntu-latest
strategy:
matrix:
@ -18,11 +49,8 @@ jobs:
php-version: ${{ matrix.php-versions }}
tools: composer
- name: Validate composer.json and composer.lock
run: composer validate
- name: setup templates_c
run: mkdir templates_c || true
- name: run insall.sh
run: /bin/bash install.sh
- name: touch config.local.php
run: touch config.local.php && php -v
@ -31,18 +59,10 @@ jobs:
run: composer install --prefer-dist -n
- name: Build/test
run: composer build
run: composer test
- name: build coveralls coverage
run: php -m xdebug.mode=coverage vendor/bin/phpunit tests
- name: Coveralls
run: vendor/bin/php-coveralls ./clover.xml || true
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
post_build:
needs: [build]
build_coverage_report:
needs: [testsuite]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -53,14 +73,14 @@ jobs:
php-version: '7.4'
tools: composer
- name: setup templates_c
run: mkdir templates_c || true
- name: run insall.sh
run: /bin/bash install.sh
- name: touch config.local.php
run: touch config.local.php && php -v
- name: Install dependencies
run: composer install --prefer-dist -n
run: composer update --prefer-dist -n
- name: build coveralls coverage
run: php -d xdebug.mode=coverage vendor/bin/phpunit tests

2
.gitignore vendored
View File

@ -1,7 +1,7 @@
/config.local.php
/templates_c/*.tpl.php
/templates_c/*menu.conf.php
/vendor/
/.php_cs.cache
/.idea
/vendor
/composer.lock

View File

@ -6,6 +6,9 @@ They should not be stored in plain text.
Whatever format you choose will need to be supported by your IMAP server (and whatever provides SASL auth for Postfix)
If you can, use a format that includes a different salt per password (e.g. one of the crypt variants, like Blowfish (BLF-CRYPT) or Argon2I/Argon2ID).
Try and avoid formats that are unsalted hashes (md5, SHA1) as these offer minimal protection in the event of a data leak.
## Configuration
See config.inc.php (or config.local.php) and look for
@ -20,6 +23,49 @@ This document is probably not complete.
It possibly provides better documentation than was present before. This may not say much.
Supported hash formats include :
* MD5-CRYPT (aka MD5),
* SHA1,
* SHA1-CRYPT,
* SSHA (4 char salted sha1),
* BLF-CRYPT (Blowfish),
* SHA512,
* SHA512-CRYPT,
* ARGON2I,
* ARGON2ID,
* SHA256,
* SHA256-CRYPT,
* PLAIN-MD5 (aka md5)
* CRYPT
Historically PostfixAdmin has supported all dovecot algorithms (methods) by using the 'doveadm' system binary. As of XXXXXXX, we attempted to use a native/PHP implementation for a number of these to remove issues caused by use of proc_open / dovecot file permissions etc (see e.g. #379).
It's recommended you use the algorithm/mechanism from your MTA, and configure PostfixAdmin with the same value prefixed by the MTA name -
For example, if dovecot has `default_pass_scheme = SHA256` use `$CONF['encrypt'] = 'dovecot:SHA256'; ` in PostfixAdmin.
| Dovecot pass scheme | PostfixAdmin `$CONF['encrypt']` setting |
|---------------------|-----------------------------------------|
| SHA256 | dovecot:SHA256 |
| SHA256-CRYPT.B64 | dovecot:SHA256-CRYPT.B64 |
| SHA256-CRYPT | dovecot:SHA256-CRYPT |
| SHA512-CRYPT | dovecot:SHA512-CRYPT |
| ARGON2I | dovecot:ARGON2I |
| ARGON2ID | dovecot:ARGON2ID |
| Courier Example | PostfixAdmin |
|-----------------|--------------|
| md5 | courier:md5 |
| md5raw | courier:md5raw |
| sha1 | courier:sha1 |
| ssha | courier:ssha |
| sha256 | courier:sha256 |
### cleartext
No hashing. May be useful for debugging.
@ -58,9 +104,9 @@ You should not use this (it does not offer a high level of security), but is pro
Uses PHP's crypt function.
Probably throws an E_NOTICE. Avoid?
Probably throws an E_NOTICE.
example : `$1$tWgqTIuF$1HFciCXrhVpACGjBMxNr/0`
Example : `$1$tWgqTIuF$1HFciCXrhVpACGjBMxNr/0`
### authlib
@ -86,16 +132,18 @@ Presumably weak.
Uses sha1, base64 encoded. Unsalted. Avoid.
### dovecot:CRYPT-METHOD
### dovecot:METHOD
Uses dovecot binary to produce hash.
May use dovecot binary to produce hash, if the format you request isn't in PFACrypt::DOVECOT_NATIVE
Pros -
Using a format that PostfixAdmin doesn't support natively has the following pros/cons :
#### Pros
* Minimal dependency on PostfixAdmin / PHP code.
* Hash should definitely work with dovecot!
Cons -
#### Cons
* file permissions and/or execution of doveadm by the web server may be problematic.
* requires: proc_open(...) - which might be blocked by e.g. safemode.

View File

@ -41,10 +41,10 @@ DOCUMENTS/ folder.
(if you installed PostfixAdmin as RPM or DEB package, you can obviously skip this step.)
Assuming we are installing Postfixadmin into /srv/postfixadmin, then something like this should work. Please check https://github.com/postfixadmin/postfixadmin/releases to get the latest stable release first (the 3.2.4 version/url below is probably stale)
Assuming we are installing Postfixadmin into /srv/postfixadmin, then something like this should work. Please check https://github.com/postfixadmin/postfixadmin/releases to get the latest stable release first (the 3.2.10 version/url below is probably stale)
$ cd /srv/
$ wget -O postfixadmin.tgz https://github.com/postfixadmin/postfixadmin/archive/postfixadmin-3.2.4.tar.gz
$ wget -O postfixadmin.tgz https://github.com/postfixadmin/postfixadmin/archive/postfixadmin-3.2.10.tar.gz
$ tar -zxvf postfixadmin.tgz
$ mv postfixadmin-postfixadmin-3.2 postfixadmin
@ -53,7 +53,20 @@ Alternatively :
$ cd /srv
$ git clone https://github.com/postfixadmin/postfixadmin.git
$ cd postfixadmin
$ git checkout postfixadmin-3.2.4
$ git checkout postfixadmin-3.2.10
If you're happy to try out newer functionality and perhaps hit unfixed bugs, you can try the 'master' branch by a `git checkout master` (or don't run the final git checkout in the above list).
If you're using the 'master' branch, you'll need to also run :
```bash
/bin/bash install.sh
```
Which will
* install the 'composer' tool locally (composer.phar) and
* download dependent PHP libraries.
* create a templates_c directory if one does not exist.
2. Setup Web Server
-------------------

View File

@ -1,4 +1,7 @@
<?php
require_once(dirname(__FILE__) . '/vendor/autoload.php');
/**
* Postfix Admin
*
@ -35,23 +38,6 @@ if (!defined('POSTFIXADMIN')) {
$incpath = dirname(__FILE__);
/**
* @param string $class
* __autoload implementation, for use with spl_autoload_register().
*/
function postfixadmin_autoload($class)
{
$PATH = dirname(__FILE__) . '/model/' . $class . '.php';
if (is_file($PATH)) {
require_once($PATH);
return true;
}
return false;
}
spl_autoload_register('postfixadmin_autoload');
if (!is_file("$incpath/config.inc.php")) {
die("config.inc.php is missing!");
}

7
composer-update.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
# for github :
composer update --no-dev
# for local testing/dev:
# composer update

View File

@ -3,6 +3,9 @@
"description": "web based administration interface for Postfix mail servers",
"type": "project",
"license": "GPL-2.0",
"config": {
"preferred-install":"dist"
},
"scripts": {
"build" : [
"@check-format",
@ -19,7 +22,8 @@
"psalm": "@php ./vendor/bin/psalm --no-cache --show-info=false "
},
"require": {
"php": ">=7.0"
"php": ">=7.2",
"postfixadmin/password-hashing": "^0.0.1"
},
"require-dev": {
"ext-mysqli": "*",
@ -33,6 +37,7 @@
"shardj/zf1-future" : "^1.12"
},
"autoload": {
"classmap" : [ "model/" ],
"files": [
"config.inc.php",
"functions.inc.php",

View File

@ -187,7 +187,7 @@ $CONF['smtp_sendmail_tls'] = 'NO';
// authlib = support for courier-authlib style passwords - also set $CONF['authlib_default_flavor']
// dovecot:CRYPT-METHOD = use dovecotpw -s 'CRYPT-METHOD'. Example: dovecot:CRAM-MD5
// php_crypt:CRYPT-METHOD:DIFFICULTY:PREFIX = use PHP built in crypt()-function. Example: php_crypt:SHA512:50000
// - php_crypt CRYPT-METHOD: Supported values are DES, MD5, BLOWFISH, SHA256, SHA512
// - php_crypt CRYPT-METHOD: Supported values are DES, MD5, BLOWFISH, SHA256, SHA512 (default)
// - php_crypt DIFFICULTY: Larger value is more secure, but uses more CPU and time for each login.
// - php_crypt DIFFICULTY: Set this according to your CPU processing power.
// - php_crypt DIFFICULTY: Supported values are BLOWFISH:4-31, SHA256:1000-999999999, SHA512:1000-999999999
@ -197,7 +197,7 @@ $CONF['smtp_sendmail_tls'] = 'NO';
// - you'll need at least dovecot 2.1 for salted passwords ('doveadm pw' 2.0.x doesn't support the '-t' option)
// - dovecot 2.0.0 - 2.0.7 is not supported
// - php_crypt PREFIX: hash has specified prefix - example: php_crypt:SHA512::{SHA256-CRYPT}
// sha512.b64 - {SHA512-CRYPT.B64} (base64 encoded sha512) (no dovecot dependency; should support migration from md5crypt)
// sha512.b64 - {SHA512-CRYPT.B64} (base64 encoded sha512 crypt) (no dovecot dependency; should support migration from md5crypt)
$CONF['encrypt'] = 'php_crypt';
// In what flavor should courier-authlib style passwords be encrypted?

View File

@ -936,6 +936,7 @@ function validate_password($password)
* @param string $pw
* @param string $pw_db - encrypted hash
* @return string crypt'ed password, should equal $pw_db if $pw matches the original
* @deprecated
*/
function _pacrypt_md5crypt($pw, $pw_db = '')
{
@ -952,6 +953,7 @@ function _pacrypt_md5crypt($pw, $pw_db = '')
/**
* @todo fix this to not throw an E_NOTICE or deprecate/remove.
* @deprecated
*/
function _pacrypt_crypt($pw, $pw_db = '')
{
@ -1291,65 +1293,44 @@ function _php_crypt_random_string($characters, $length)
* @param string $pw_db optional encrypted password
* @return string encrypted password - if this matches $pw_db then the original password is $pw.
*/
function pacrypt($pw, $pw_db="")
function pacrypt($pw, $pw_db = "")
{
global $CONF;
switch ($CONF['encrypt']) {
case 'md5crypt':
return _pacrypt_md5crypt($pw, $pw_db);
case 'md5':
return md5($pw);
case 'system':
return _pacrypt_crypt($pw, $pw_db);
case 'cleartext':
return $pw;
case 'mysql_encrypt':
return _pacrypt_mysql_encrypt($pw, $pw_db);
case 'authlib':
return _pacrypt_authlib($pw, $pw_db);
case 'sha512.b64':
return _pacrypt_sha512_b64($pw, $pw_db);
$mechanism = $CONF['encrypt'] ?? 'CRYPT';
$mechanism = strtoupper($mechanism);
$crypts = ['PHP_CRYPT', 'MD5CRYPT', 'PHP_CRYPT:DES', 'PHP_CRYPT:MD5', 'PHP_CRYPT:SHA256'];
if (in_array($mechanism, $crypts)) {
$mechanism = 'CRYPT';
}
if ($mechanism == 'PHP_CRYPT:SHA512') {
$mechanism = 'SHA512-CRYPT';
}
if (preg_match("/^dovecot:/", $CONF['encrypt'])) {
return _pacrypt_dovecot($pw, $pw_db);
if ($mechanism == 'SHA512.B64') {
// postfixadmin incorrectly uses this as a SHA512-CRYPT.B64
$mechanism = 'SHA512-CRYPT.B64';
}
if (preg_match('/^DOVECOT:(.*)$/i', $mechanism, $matches)) {
$mechanism = strtoupper($matches[1]);
}
if (substr($CONF['encrypt'], 0, 9) === 'php_crypt') {
return _pacrypt_php_crypt($pw, $pw_db);
if (preg_match('/^COURIER:(.*)$/i', $mechanism, $matches)) {
$mechanism = strtoupper($mechanism);
}
if (empty($pw_db)) {
$pw_db = null;
}
throw new Exception('unknown/invalid $CONF["encrypt"] setting: ' . $CONF['encrypt']);
}
/**
* @see https://github.com/postfixadmin/postfixadmin/issues/58
*/
function _pacrypt_sha512_b64($pw, $pw_db="")
{
if (!function_exists('random_bytes') || !function_exists('crypt') || !defined('CRYPT_SHA512') || !function_exists('mb_substr')) {
throw new Exception("sha512.b64 not supported!");
if ($mechanism == 'AUTHLIB') {
return _pacrypt_authlib($pw, $pw_db);
}
if (!$pw_db) {
$salt = mb_substr(rtrim(base64_encode(random_bytes(16)),'='),0,16,'8bit');
return '{SHA512-CRYPT.B64}'.base64_encode(crypt($pw,'$6$'.$salt));
}
$password="#Thepasswordcannotbeverified";
if (strncmp($pw_db,'{SHA512-CRYPT.B64}',18)==0) {
$dcpwd = base64_decode(mb_substr($pw_db,18,null,'8bit'),true);
if ($dcpwd !== false && !empty($dcpwd) && strncmp($dcpwd,'$6$',3)==0) {
$password = '{SHA512-CRYPT.B64}'.base64_encode(crypt($pw,$dcpwd));
}
} elseif (strncmp($pw_db,'{MD5-CRYPT}',11)==0) {
$dcpwd = mb_substr($pw_db,11,null,'8bit');
if (!empty($dcpwd) && strncmp($dcpwd,'$1$',3)==0) {
$password = '{MD5-CRYPT}'.crypt($pw,$dcpwd);
}
}
return $password;
$hasher = new \PostfixAdmin\PasswordHashing\Crypt($mechanism);
return $hasher->crypt($pw, $pw_db);
}
/**
@ -1360,6 +1341,7 @@ function _pacrypt_sha512_b64($pw, $pw_db="")
* @param string $salt (optional)
* @param string $magic (optional)
* @return string hashed password in crypt format.
* @deprecated see PFACrypt::cryptMd5() (note this returns {MD5} prefix
*/
function md5crypt($pw, $salt="", $magic="")
{

62
install.sh Normal file
View File

@ -0,0 +1,62 @@
#!/bin/bash
set -eu
# PostfixAdmin install script.
# 1. Downloads 'composer.phar' to the current directory.
# 2. Runs 'php composer.phar install' which should install required runtime libraries for Postfixadmin
# 3. Runs 'mkdir templates_c && chmod 777 templates_c'
PATH=/bin:/usr/bin:/usr/local/bin
export PATH
COMPOSER_URL=https://getcomposer.org/download/latest-stable/composer.phar
type php >/dev/null 2>&1 || { echo >&2 "I require php but it's not installed. Aborting."; exit 1; }
cd "$(dirname "$0")"
# Check for $(pwd)/composer.phar
echo " * Checking for composer.phar "
if [ ! -f composer.phar ]; then
echo " * Trying to download composer.phar from $COMPOSER_URL "
# try and download it one way or another
if [ -x /usr/bin/wget ]; then
wget -q -O composer.phar $COMPOSER_URL
else
if [ -x /usr/bin/curl ]; then
curl -o composer.phar $COMPOSER_URL
else
echo " ** Could not find wget or curl; please download $COMPOSER_URL to pwd" >/dev/stderr
exit 1
fi
fi
fi
echo " * Running composer install --no-dev"
php composer.phar install --prefer-dist -n --no-dev
if [ ! -d templates_c ]; then
mkdir -p templates_c && chmod 777 templates_c
echo
echo " Warning: "
echo " templates_c directory didn't exist, now created."
echo
echo " You should change the ownership and reduce permissions on templates_c to 750. "
echo " The ownership needs to match the user used to execute PHP scripts, perhaps 'www-data' or 'httpd'"
echo
echo " e.g. chown www-data templates_c && chmod 750 templates_c"
echo
fi
echo
echo "Please continue configuration / setup within your web browser. "
echo "See also : https://github.com/postfixadmin/postfixadmin/blob/master/INSTALL.TXT#L58 "
echo

359
model/PFACrypt.php Normal file
View File

@ -0,0 +1,359 @@
<?php
class PFACrypt
{
private $algorithm;
const DOVECOT_NATIVE = [
'SHA1', 'SHA1.HEX', 'SHA1.B64',
'SSHA',
'BLF-CRYPT', 'BLF-CRYPT.B64',
'SHA512-CRYPT', 'SHA512-CRYPT.B64',
'ARGON2I',
'ARGON2I.B64',
'ARGON2ID',
'ARGON2ID.B64',
'SHA256', 'SHA256-CRYPT', 'SHA256-CRYPT.B64',
'SHA512', 'SHA512.B64',
'MD5-CRYPT',
'PLAIN-MD5',
'CRYPT',
];
public function __construct(string $algorithm)
{
$this->algorithm = $algorithm;
}
/**
* When called with a 'pw' and a 'pw_db' (hash from e.g. database).
*
* If the return value matches $pw_db, then the plain text password ('pw') is correct.
*
* @param string $pw - plain text password
* @param string $pw_db - hash from e.g. database (what we're comparing $pw to).
* @return string if $pw is correct (hashes to $pw_db) then we return $pw_db. Else we return a new hash.
*
* @throws Exception
*/
public function pacrypt(string $pw, string $pw_db = ''): string
{
$algorithm = strtoupper($this->algorithm);
// try and 'upgrade' some dovecot commands to use local algorithms (rather tnan a dependency on the dovecot binary).
if (preg_match('/^DOVECOT:/i', $algorithm)) {
$tmp = preg_replace('/^DOVECOT:/i', '', $algorithm);
if (in_array($tmp, self::DOVECOT_NATIVE)) {
$algorithm = $tmp;
} else {
error_log("Warning: using algorithm that requires proc_open: $algorithm, consider using one of : " . implode(', ', self::DOVECOT_NATIVE));
}
}
if (!empty($pw_db) && preg_match('/^{([0-9a-z-\.]+)}/i', $pw_db, $matches)) {
$method_in_hash = $matches[1];
if ('COURIER:' . strtoupper($method_in_hash) == $algorithm) {
// don't try and be clever.
} elseif ($algorithm != $method_in_hash) {
error_log("Hey, you fed me a password using {$method_in_hash}, but the system is configured to use {$algorithm}");
$algorithm = $method_in_hash;
}
}
if ($algorithm == 'SHA512CRYPT.B64') {
$algorithm = 'SHA512-CRYPT.B64';
}
switch ($algorithm) {
case 'SHA1':
case 'SHA1.B64':
case 'SHA1.HEX':
return $this->hashSha1($pw, $pw_db, $algorithm);
case 'BLF-CRYPT':
case 'BLF-CRYPT.B64':
return $this->blowfishCrypt($pw, $pw_db, $algorithm);
case 'SHA512-CRYPT':
case 'SHA512-CRYPT.B64':
return $this->sha512Crypt($pw, $pw_db, $algorithm);
case 'ARGON2I':
case 'ARGON2I.B64':
return $this->argon2iCrypt($pw, $pw_db, $algorithm);
case 'ARGON2ID':
case 'ARGON2ID.B64':
return $this->argon2idCrypt($pw, $pw_db, $algorithm);
case 'SSHA':
case 'COURIER:SSHA':
return $this->hashSha1Salted($pw, $pw_db);
case 'SHA256':
case 'COURIER:SHA256':
return $this->hashSha256($pw);
case 'SHA256-CRYPT':
case 'SHA256-CRYPT.B64':
return $this->sha256Crypt($pw, $pw_db, $algorithm);
case 'SHA512':
case 'SHA512B.b64':
case 'SHA512.B64':
return $this->hashSha512($pw, $algorithm);
case 'PLAIN-MD5': // {PLAIN-MD5} prefix
case 'MD5': // no prefix
return $this->hashMd5($pw, $algorithm); // this is hex encoded.
case 'COURIER:MD5':
return '{MD5}' . base64_encode(md5($pw, true)); // seems to need to be base64 encoded.
case 'COURIER:MD5RAW':
return '{MD5RAW}' . md5($pw);
case 'MD5-CRYPT':
return $this->cryptMd5($pw, $pw_db, $algorithm);
case 'CRYPT':
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{CRYPT}/', '', $pw_db);
}
if (empty($pw_db)) {
$pw_db = '$2y$10$' . substr(sha1(random_bytes(8)), 0, 22);
}
return '{CRYPT}' . crypt($pw, $pw_db);
case 'SYSTEM':
return crypt($pw, $pw_db);
case 'CLEAR':
case 'PLAIN':
case 'CLEARTEXT':
if (!empty($pw_db)) {
if ($pw_db == "{{$algorithm}}$pw") {
return $pw_db;
}
return $pw;
}
return '{' . $algorithm . '}' . $pw;
case 'MYSQL_ENCRYPT':
return _pacrypt_mysql_encrypt($pw, $pw_db);
// these are all supported by the above (SHA,
case 'AUTHLIB':
return _pacrypt_authlib($pw, $pw_db);
}
if (preg_match("/^DOVECOT:/", $algorithm)) {
return _pacrypt_dovecot($pw, $pw_db);
}
if (substr($algorithm, 0, 9) === 'PHP_CRYPT') {
return _pacrypt_php_crypt($pw, $pw_db);
}
throw new Exception('unknown/invalid $CONF["encrypt"] setting: ' . $algorithm);
}
public function hashSha1(string $pw, string $pw_db = '', string $algorithm = 'SHA1'): string
{
$hash = hash('sha1', $pw, true);
if (preg_match('/\.HEX$/', $algorithm)) {
$hash = bin2hex($hash);
} else {
$hash = base64_encode($hash);
}
return "{{$algorithm}}{$hash}";
}
public function hashSha1Salted(string $pw, string $pw_db = ''): string
{
if (empty($pw_db)) {
$salt = base64_encode(random_bytes(3)); // 4 char salt.
} else {
$salt = substr(base64_decode(substr($pw_db, 6)), 20);
}
return '{SSHA}' . base64_encode(sha1($pw . $salt, true) . $salt);
}
public function hashSha512(string $pw, string $algorithm = 'SHA512')
{
$prefix = '{SHA512}';
if ($algorithm == 'SHA512.B64' || $algorithm == 'sha512b.b64') {
$prefix = '{SHA512.B64}';
}
return $prefix . base64_encode(hash('sha512', $pw, true));
}
public function hashMd5(string $pw, string $algorithm = 'PLAIN-MD5'): string
{
if ($algorithm == 'PLAIN-MD5') {
return '{PLAIN-MD5}' . md5($pw);
}
return md5($pw);
}
public function hashSha256(string $pw): string
{
return '{SHA256}' . base64_encode(hash('sha256', $pw, true));
}
public function cryptMd5(string $pw, string $pw_db = '', $algorithm = 'MD5-CRYPT')
{
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{MD5.*}/', '', $pw_db);
}
if (empty($pw_db)) {
$pw_db = '$1$' . substr(sha1(random_bytes(8)), 0, 16);
}
return "{{$algorithm}}" . crypt($pw, $pw_db);
}
public function blowfishCrypt(string $pw, string $pw_db = '', string $algorithm = 'BLF-CRYPT'): string
{
if (!empty($pw_db)) {
if ($algorithm == 'BLF-CRYPT') {
$pw_db = preg_replace('/^{BLF-CRYPT}/', '', $pw_db);
}
if ($algorithm == 'BLF-CRYPT.B64') {
$pw_db = base64_decode(preg_replace('/^{BLF-CRYPT.B64}/', '', $pw_db));
}
$hash = crypt($pw, $pw_db);
if ($algorithm == 'BLF-CRYPT.B64') {
$hash = base64_encode($hash);
}
return "{{$algorithm}}{$hash}";
}
$r = password_hash($pw, PASSWORD_BCRYPT);
if (!is_string($r)) {
throw new \RuntimeException("Failed to generate password");
}
if ($algorithm == 'BLF-CRYPT.B64') {
return '{BLF-CRYPT.B64}' . base64_encode($r);
}
return '{BLF-CRYPT}' . $r;
}
public function sha256Crypt(string $pw, string $pw_db = '', string $algorithm = 'SHA256-CRYPT'): string
{
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{SHA256-CRYPT(\.B64)?}/', '', $pw_db);
if ($algorithm == 'SHA256-CRYPT.B64') {
$pw_db = base64_decode($pw_db);
}
}
if (empty($pw_db)) {
$pw_db = '$5$' . substr(sha1(random_bytes(8)), 0, 16);
}
$hash = crypt($pw, $pw_db);
if ($algorithm == 'SHA256-CRYPT.B64') {
return '{SHA256-CRYPT.B64}' . base64_encode($hash);
}
return "{SHA256-CRYPT}" . $hash;
}
public function sha512Crypt(string $pw, string $pw_db = '', $algorithm = 'SHA512-CRYPT'): string
{
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{SHA512-CRYPT(\.B64)?}/', '', $pw_db);
if ($algorithm == 'SHA512-CRYPT.B64') {
$pw_db = base64_decode($pw_db);
}
}
if (empty($pw_db)) {
$pw_db = '$6$' . substr(sha1(random_bytes(8)), 0, 16);
}
$hash = crypt($pw, $pw_db);
if ($algorithm == 'SHA512-CRYPT.B64') {
$hash = base64_encode($hash);
return "{SHA512-CRYPT.B64}{$hash}";
}
return "{SHA512-CRYPT}$hash";
}
public function argon2ICrypt(string $pw, string $pw_db = '', $algorithm = 'ARGON2I'): string
{
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{ARGON2I(\.B64)?}/', '', $pw_db);
$orig_pwdb = $pw_db;
if ($algorithm == 'ARGON2I.B64') {
$pw_db = base64_decode($pw_db);
}
if (password_verify($pw, $pw_db)) {
return "{{$algorithm}}" . $orig_pwdb;
}
$hash = password_hash($pw, PASSWORD_ARGON2I);
if ($algorithm == 'ARGON2I') {
return '{ARGON2I}' . $hash;
}
return '{ARGON2I.B64}' . base64_encode($hash);
;
}
$hash = password_hash($pw, PASSWORD_ARGON2I);
if ($algorithm == 'ARGON2I') {
return '{ARGON2I}' . $hash;
}
return "{ARGON2I.B64}" . base64_encode($hash);
}
public function argon2idCrypt(string $pw, string $pw_db = '', string $algorithm = 'ARGON2ID'): string
{
if (!defined('PASSWORD_ARGON2ID')) {
throw new \Exception("Requires PHP 7.3+");
}
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{ARGON2ID(\.B64)?}/', '', $pw_db);
$orig_pwdb = $pw_db;
if ($algorithm == 'ARGON2ID.B64') {
$pw_db = base64_decode($pw_db);
}
if (password_verify($pw, $pw_db)) {
return "{{$algorithm}}" . $orig_pwdb;
}
$hash = password_hash($pw, PASSWORD_ARGON2ID);
if ($algorithm == 'ARGON2ID') {
return '{ARGON2ID}' . $hash;
}
// if($algorithm == 'ARGON2ID.B64') {
return '{ARGON2ID.B64}' . base64_encode($hash);
}
$hash = password_hash($pw, PASSWORD_ARGON2ID);
if ($algorithm == 'ARGON2ID') {
return '{ARGON2ID}' . $hash;
}
return '{ARGON2ID.B64}' . base64_encode($hash);
}
}

View File

@ -1,11 +1,15 @@
<?php
require_once(__DIR__ . '/../model/PFACrypt.php');
class PaCryptTest extends \PHPUnit\Framework\TestCase
{
public function testMd5Crypt()
{
$hash = _pacrypt_md5crypt('test', '');
$h = new PFACrypt('MD5-CRYPT');
$this->assertNotEmpty($hash);
$this->assertNotEquals('test', $hash);
@ -68,6 +72,7 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase
public function testPacryptDovecot()
{
global $CONF;
if (!file_exists('/usr/bin/doveadm')) {
$this->markTestSkipped("No /usr/bin/doveadm");
}
@ -78,7 +83,6 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase
$expected_hash = '{SHA1}qUqP5cyxm6YcTAhz05Hph5gvu9M=';
$this->assertEquals($expected_hash, _pacrypt_dovecot('test', ''));
$this->assertEquals($expected_hash, _pacrypt_dovecot('test', $expected_hash));
// This should also work.
@ -89,7 +93,27 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase
$this->assertNotEquals($sha512, _pacrypt_dovecot('foobarbaz', $sha512));
}
public function testPhpCrypt()
{
$config = Config::getInstance();
Config::write('encrypt', 'php_crypt');
$CONF = Config::getInstance()->getAll();
$sha512_crypt = '$6$ijF8bgunALqnEHTo$LHVa6XQBpM5Gt16RMFQuXqrGAS0y0ymaLS8pnkeVUTSx3t2DrGqWwRj6q4ef3V3SWYkb5xkuN9bv7joxNd8kA1';
$enc = _pacrypt_php_crypt('foo', $sha512_crypt);
$this->assertEquals($enc, $sha512_crypt);
$fail = _pacrypt_php_crypt('bar', $sha512_crypt);
$this->assertNotEquals($fail, $sha512_crypt);
}
public function testPhpCryptMd5()
{
$config = Config::getInstance();
Config::write('encrypt', 'php_crypt:MD5');
@ -155,29 +179,129 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase
$this->assertFalse(strcmp($str1, $str2) == 0 && strcmp($str1, $str3) == 0);
}
public function testSha512B64()
public function testNewDovecotStuff()
{
$str1 = _pacrypt_sha512_b64('test', '');
$str2 = _pacrypt_sha512_b64('test', '');
global $CONF;
$this->assertNotEmpty($str1);
$this->assertNotEmpty($str2);
$this->assertNotEquals($str1, $str2); // should have different salts
// should all be from 'test123', generated via dovecot.
$algo_to_example = [
'SHA1' => '{SHA1}cojt0Pw//L6ToM8G41aOKFIWh7w=',
'SHA1.B64' => '{SHA1.B64}cojt0Pw//L6ToM8G41aOKFIWh7w=',
'BLF-CRYPT' => '{BLF-CRYPT}$2y$05$cEEZv2h/NtLXII.emi2TP.rMZyB7VRSkyToXWBqqz6cXDoyay166q',
'BLF-CRYPT.B64' => '{BLF-CRYPT.B64}JDJ5JDA1JEhlR0lBeGFHR2tNUGxjRWpyeFc0eU9oRjZZZ1NuTWVOTXFxNWp4bmFwVjUwdGU3c2x2L1VT',
'SHA512-CRYPT' => '{SHA512-CRYPT}$6$MViNQUSbWyXWL9wZ$63VsBU2a/ZFb9f/dK4EmaXABE9jAcNltR7y6a2tXLKoV5F5jMezno.2KpmtD3U0FDjfa7A.pkCluVMlZJ.F64.',
'SHA512-CRYPT.B64' => '{SHA512-CRYPT.B64}JDYkR2JwY3NiZXNMWk9DdERXbiRYdXlhdEZTdy9oa3lyUFE0d24wenpGQTZrSlpTUE9QVWdPcjVRUC40bTRMTjEzdy81aWMvWTdDZllRMWVqSWlhNkd3Q2Z0ZnNjZEFpam9OWjl3OU5tLw==',
'SHA512' => '{SHA512}2u9JU7l4M2XK1mFSI3IFBsxGxRZ80Wq1APpZeqCP+WTrJPsZaH8012Zfd4/LbFNY/ApbgeFmLPkPc6JnHFP5kQ==',
$actualHash = '{SHA512-CRYPT.B64}JDYkM2NWcFM1WFNlUHl5MzdwSiRZWW80d0FmeWg5MXpxcS4uY3dtYUR1Y1RodTJGTDY1NHpXNUNvRU0wT3hXVFFzZkxIZ1JJSTZmT281OVpDUWJOTTF2L0JXajloME0vVjJNbENNMUdwLg==';
// postfixadmin 'incorrectly' classes sha512.b64 as a sha512-crypted string that's b64 encoded.
// really SHA512.B64 should be base64_encode(hash('sha512', 'something', true));
'SHA512.B64' => '{SHA512-CRYPT.B64}JDYkMDBpOFJXQ0JwMlFMMDlobCRFMVFWLzJjbENPbEo4OTg0SjJyY1oxeXNTaFJIYVhJeVdFTDdHRGl3aHliYkhQUHBUQjZTM0lFMlYya2ZXczZWbHY0aDVNa3N0anpud0xuRTBWZVRELw==',
'CRYPT' => '{CRYPT}$2y$05$ORqzr0AagWr25v3ixHD5QuMXympIoNTbipEFZz6aAmovGNoij2vDO',
'MD5-CRYPT' => '{MD5-CRYPT}$1$AIjpWveQ$2s3eEAbZiqkJhMYUIVR240',
'PLAIN-MD5' => '{PLAIN-MD5}cc03e747a6afbbcbf8be7668acfebee5',
'SSHA' => '{SSHA}ZkqrSEAhvd0FTHaK1IxAQCRa5LWbxGQY',
'PLAIN' => '{PLAIN}test123',
'CLEAR' => '{CLEAR}test123',
'CLEARTEXT' => '{CLEARTEXT}test123',
'ARGON2I' => '{ARGON2I}$argon2i$v=19$m=32768,t=4,p=1$xoOcAGa27k0Sr6ZPbA9ODw$wl/KAZVmJooD/35IFG5oGwyQiAREXrLss5BPS1PDKfA',
'ARGON2ID' => '{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$eaXP376O9/VxleLw9OQIxg$jOoDyECeRRV4eta3eSN/j0RdBgqaA1VBGAA/pbviI20',
'ARGON2ID.B64' => '{ARGON2ID.B64}JGFyZ29uMmlkJHY9MTkkbT02NTUzNix0PTMscD0xJEljdG9DWko1T04zWlYzM3I0TVMrNEEkMUVtNTJRWkdsRlJzNnBsRXpwVmtMeVd4dVNPRUZ2dUZnaVNhTmNlb08rOA==',
'SHA256' => '{SHA256}7NcYcNGWMxapfjrDQIyYNa2M8PPBvHA1J8MCZVNPda4=',
'SHA256-CRYPT' => '{SHA256-CRYPT}$5$CFly6wzfn2az3U8j$EhfQPTdjpMGAisfCjCKektLke5GGEmtdLVaCZSmsKw2',
'SHA256-CRYPT.B64' => '{SHA256-CRYPT.B64}JDUkUTZZS1ZzZS5sSVJoLndodCR6TWNOUVFVVkhtTmM1ME1SQk9TR3BEeGpRY2M1TzJTQ1lkbWhPN1YxeHlD',
];
$check = _pacrypt_sha512_b64('test', $actualHash);
// php 7.3 and below do not support these.
if (phpversion() < '7.3') {
unset($algo_to_example['ARGON2ID']);
unset($algo_to_example['ARGON2ID.B64']);
}
$this->assertTrue(hash_equals($check, $actualHash));
foreach ($algo_to_example as $algorithm => $example_hash) {
$CONF['encrypt'] = $algorithm;
$pfa_new_hash = pacrypt('test123');
$str3 = _pacrypt_sha512_b64('foo', '');
$pacrypt_check = pacrypt('test123', $example_hash);
$pacrypt_sanity = pacrypt('zzzzzzz', $example_hash);
$this->assertNotEmpty($str3);
$this->assertNotEquals($example_hash, $pacrypt_sanity, "Should not match, zzzz password. $algorithm / $pacrypt_sanity");
$this->assertFalse(hash_equals('test', $str3));
$this->assertEquals($example_hash, $pacrypt_check, "Should match, algorithm: $algorithm generated:{$pacrypt_check} vs example:{$example_hash}");
$this->assertTrue(hash_equals(_pacrypt_sha512_b64('foo', $str3), $str3));
$new_new = pacrypt('test123', $pfa_new_hash);
$this->assertEquals($pfa_new_hash, $new_new, "Trying: $algorithm => gave: $new_new with $pfa_new_hash ... ");
}
}
public function testSha512CryptB64()
{
$c = new PFACrypt('SHA512CRYPT.B64');
// "SHA512-CRYPT.B64": "{SHA512-CRYPT.B64}JDYkR2JwY3NiZXNMWk9DdERXbiRYdXlhdEZTdy9oa3lyUFE0d24wenpGQTZrSlpTUE9QVWdPcjVRUC40bTRMTjEzdy81aWMvWTdDZllRMWVqSWlhNkd3Q2Z0ZnNjZEFpam9OWjl3OU5tLw==",
$crypt = '{SHA512-CRYPT.B64}JDYkR2JwY3NiZXNMWk9DdERXbiRYdXlhdEZTdy9oa3lyUFE0d24wenpGQTZrSlpTUE9QVWdPcjVRUC40bTRMTjEzdy81aWMvWTdDZllRMWVqSWlhNkd3Q2Z0ZnNjZEFpam9OWjl3OU5tLw==';
$this->assertEquals($crypt, $c->pacrypt('test123', $crypt));
}
public function testWeCopeWithDifferentMethodThanConfigured()
{
$c = new PFACrypt('MD5-CRYPT');
$md5Crypt = '{MD5-CRYPT}$1$AIjpWveQ$2s3eEAbZiqkJhMYUIVR240';
$this->assertEquals($md5Crypt, $c->pacrypt('test123', $md5Crypt));
$c = new PFACrypt('SHA1');
$this->assertEquals($md5Crypt, $c->pacrypt('test123', $md5Crypt));
$sha1Crypt = '{SHA1}cojt0Pw//L6ToM8G41aOKFIWh7w=';
$this->assertEquals($sha1Crypt, $c->pacrypt('test123', $sha1Crypt));
}
public function testSomeCourierHashes()
{
global $CONF;
$options = [
'courier:md5' => '{MD5}zAPnR6avu8v4vnZorP6+5Q==',
'courier:md5raw' => '{MD5RAW}cc03e747a6afbbcbf8be7668acfebee5',
'courier:ssha' => '{SSHA}pJTac1QSIHoi0qBPdqnBvgPdjfFtDRVY',
'courier:sha256' => '{SHA256}7NcYcNGWMxapfjrDQIyYNa2M8PPBvHA1J8MCZVNPda4=',
];
foreach ($options as $algorithm => $example_hash) {
$CONF['encrypt'] = $algorithm;
$pacrypt_check = pacrypt('test123', $example_hash);
$pacrypt_sanity = pacrypt('zzzzz', $example_hash);
$pfa_new_hash = pacrypt('test123');
$this->assertNotEquals($pacrypt_sanity, $pfa_new_hash);
$this->assertNotEquals($pacrypt_sanity, $example_hash);
$this->assertEquals($example_hash, $pacrypt_check, "Should match, algorithm: $algorithm generated:{$pacrypt_check} vs example:{$example_hash}");
$new = pacrypt('test123', $pfa_new_hash);
$this->assertEquals($new, $pfa_new_hash, "Trying: $algorithm => gave: $new with $pfa_new_hash");
}
}
public function testWeSupportWhatWeSayWeDo()
{
foreach (PFACrypt::DOVECOT_NATIVE as $algorithm) {
if (phpversion() < 7.3 && ($algorithm == 'ARGON2ID' || $algorithm == 'ARGON2ID.B64')) {
continue; // needs PHP7.3+
}
$c = new PFACrypt($algorithm);
$hash1 = $c->pacrypt('test123');
$this->assertEquals($hash1, $c->pacrypt('test123', $hash1));
$this->assertNotEquals($hash1, $c->pacrypt('9999test9999', $hash1));
}
}
public function testObviousMechs()
@ -190,13 +314,13 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase
'cleartext' => 'test123',
'mysql_encrypt' => '$6$$KMCDSuWNoVgNrK5P1zDS12ZZt.LV4z9v9NtD0AG0T5Rv/n0wWVvZmHMSKKZQciP7lrqrlbrBrBd4lhBSGy1BU0',
'authlib' => '{md5raw}cc03e747a6afbbcbf8be7668acfebee5',
'php_crypt:SHA512' => '$6$IeqpXtDIXF09ADdc$IsE.SSK3zuwtS9fdWZ0oVxXQjPDj834xqxTiv3Qfidq3AbAjPb0DNyI28JyzmDVlbfC9uSfNxD9RUyeO1.7FV/',
'php_crypt:SHA512' => '{SHA512-CRYPT}$6$IeqpXtDIXF09ADdc$IsE.SSK3zuwtS9fdWZ0oVxXQjPDj834xqxTiv3Qfidq3AbAjPb0DNyI28JyzmDVlbfC9uSfNxD9RUyeO1.7FV/',
'php_crypt:DES' => 'VXAXutUnpVYg6',
'php_crypt:MD5' => '$1$rGTbP.KE$wimpECWs/wQa7rnSwCmHU.',
'php_crypt:SHA256' => '$5$UaZs6ZuaLkVPx3bM$4JwAqdphXVutFYw7COgAkp/vj09S1DfjIftxtjqDrr/',
'sha512.b64' => '{SHA512-CRYPT.B64}JDYkMDBpOFJXQ0JwMlFMMDlobCRFMVFWLzJjbENPbEo4OTg0SjJyY1oxeXNTaFJIYVhJeVdFTDdHRGl3aHliYkhQUHBUQjZTM0lFMlYya2ZXczZWbHY0aDVNa3N0anpud0xuRTBWZVRELw==',
];
foreach ($mechs as $mech => $example_hash) {
if ($mech == 'mysql_encrypt' && Config::read_string('database_type') != 'mysql') {
continue;