blob: 7f0b41289da25dcc5b2d090691821bf7996cb1bf [file] [log] [blame]
<?php
namespace LdapRecord\Models\Attributes;
use InvalidArgumentException;
use LdapRecord\LdapRecordException;
use ReflectionMethod;
class Password
{
const CRYPT_SALT_TYPE_MD5 = 1;
const CRYPT_SALT_TYPE_SHA256 = 5;
const CRYPT_SALT_TYPE_SHA512 = 6;
/**
* Make an encoded password for transmission over LDAP.
*
* @param string $password
*
* @return string
*/
public static function encode($password)
{
return iconv('UTF-8', 'UTF-16LE', '"'.$password.'"');
}
/**
* Make a salted md5 password.
*
* @param string $password
* @param null|string $salt
*
* @return string
*/
public static function smd5($password, $salt = null)
{
return '{SMD5}'.static::makeHash($password, 'md5', null, $salt ?? random_bytes(4));
}
/**
* Make a salted SHA password.
*
* @param string $password
* @param null|string $salt
*
* @return string
*/
public static function ssha($password, $salt = null)
{
return '{SSHA}'.static::makeHash($password, 'sha1', null, $salt ?? random_bytes(4));
}
/**
* Make a salted SSHA256 password.
*
* @param string $password
* @param null|string $salt
*
* @return string
*/
public static function ssha256($password, $salt = null)
{
return '{SSHA256}'.static::makeHash($password, 'hash', 'sha256', $salt ?? random_bytes(4));
}
/**
* Make a salted SSHA384 password.
*
* @param string $password
* @param null|string $salt
*
* @return string
*/
public static function ssha384($password, $salt = null)
{
return '{SSHA384}'.static::makeHash($password, 'hash', 'sha384', $salt ?? random_bytes(4));
}
/**
* Make a salted SSHA512 password.
*
* @param string $password
* @param null|string $salt
*
* @return string
*/
public static function ssha512($password, $salt = null)
{
return '{SSHA512}'.static::makeHash($password, 'hash', 'sha512', $salt ?? random_bytes(4));
}
/**
* Make a non-salted SHA password.
*
* @param string $password
*
* @return string
*/
public static function sha($password)
{
return '{SHA}'.static::makeHash($password, 'sha1');
}
/**
* Make a non-salted SHA256 password.
*
* @param string $password
*
* @return string
*/
public static function sha256($password)
{
return '{SHA256}'.static::makeHash($password, 'hash', 'sha256');
}
/**
* Make a non-salted SHA384 password.
*
* @param string $password
*
* @return string
*/
public static function sha384($password)
{
return '{SHA384}'.static::makeHash($password, 'hash', 'sha384');
}
/**
* Make a non-salted SHA512 password.
*
* @param string $password
*
* @return string
*/
public static function sha512($password)
{
return '{SHA512}'.static::makeHash($password, 'hash', 'sha512');
}
/**
* Make a non-salted md5 password.
*
* @param string $password
*
* @return string
*/
public static function md5($password)
{
return '{MD5}'.static::makeHash($password, 'md5');
}
/**
* Crypt password with an MD5 salt.
*
* @param string $password
* @param string $salt
*
* @return string
*/
public static function md5Crypt($password, $salt = null)
{
return '{CRYPT}'.static::makeCrypt($password, static::CRYPT_SALT_TYPE_MD5, $salt);
}
/**
* Crypt password with a SHA256 salt.
*
* @param string $password
* @param string $salt
*
* @return string
*/
public static function sha256Crypt($password, $salt = null)
{
return '{CRYPT}'.static::makeCrypt($password, static::CRYPT_SALT_TYPE_SHA256, $salt);
}
/**
* Crypt a password with a SHA512 salt.
*
* @param string $password
* @param string $salt
*
* @return string
*/
public static function sha512Crypt($password, $salt = null)
{
return '{CRYPT}'.static::makeCrypt($password, static::CRYPT_SALT_TYPE_SHA512, $salt);
}
/**
* Make a new password hash.
*
* @param string $password The password to make a hash of.
* @param string $method The hash function to use.
* @param string|null $algo The algorithm to use for hashing.
* @param string|null $salt The salt to append onto the hash.
*
* @return string
*/
protected static function makeHash($password, $method, $algo = null, $salt = null)
{
$params = $algo ? [$algo, $password.$salt] : [$password.$salt];
return base64_encode(pack('H*', call_user_func($method, ...$params)).$salt);
}
/**
* Make a hashed password.
*
* @param string $password
* @param int $type
* @param null|string $salt
*
* @return string
*/
protected static function makeCrypt($password, $type, $salt = null)
{
return crypt($password, $salt ?? static::makeCryptSalt($type));
}
/**
* Make a salt for the crypt() method using the given type.
*
* @param int $type
*
* @return string
*/
protected static function makeCryptSalt($type)
{
[$prefix, $length] = static::makeCryptPrefixAndLength($type);
$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
while (strlen($prefix) < $length) {
$prefix .= substr($chars, random_int(0, strlen($chars) - 1), 1);
}
return $prefix;
}
/**
* Determine the crypt prefix and length.
*
* @param int $type
*
* @throws InvalidArgumentException
*
* @return array
*/
protected static function makeCryptPrefixAndLength($type)
{
switch ($type) {
case static::CRYPT_SALT_TYPE_MD5:
return ['$1$', 12];
case static::CRYPT_SALT_TYPE_SHA256:
return ['$5$', 16];
case static::CRYPT_SALT_TYPE_SHA512:
return ['$6$', 16];
default:
throw new InvalidArgumentException("Invalid crypt type [$type].");
}
}
/**
* Attempt to retrieve the hash method used for the password.
*
* @param string $password
*
* @return string|void
*/
public static function getHashMethod($password)
{
if (! preg_match('/^\{(\w+)\}/', $password, $matches)) {
return;
}
return $matches[1];
}
/**
* Attempt to retrieve the hash method and algorithm used for the password.
*
* @param string $password
*
* @return array|void
*/
public static function getHashMethodAndAlgo($password)
{
if (! preg_match('/^\{(\w+)\}\$([0-9a-z]{1})\$/', $password, $matches)) {
return;
}
return [$matches[1], $matches[2]];
}
/**
* Attempt to retrieve a salt from the encrypted password.
*
* @throws LdapRecordException
*
* @return string
*/
public static function getSalt($encryptedPassword)
{
// crypt() methods.
if (preg_match('/^\{(\w+)\}(\$.*\$).*$/', $encryptedPassword, $matches)) {
return $matches[2];
}
// All other methods.
if (preg_match('/{([^}]+)}(.*)/', $encryptedPassword, $matches)) {
return substr(base64_decode($matches[2]), -4);
}
throw new LdapRecordException('Could not extract salt from encrypted password.');
}
/**
* Determine if the hash method requires a salt to be given.
*
* @param string $method
*
* @throws \ReflectionException
*
* @return bool
*/
public static function hashMethodRequiresSalt($method): bool
{
$parameters = (new ReflectionMethod(static::class, $method))->getParameters();
foreach ($parameters as $parameter) {
if ($parameter->name === 'salt') {
return true;
}
}
return false;
}
}