Newer
Older
use SimpleSAML\Configuration;
* A class for cryptography-related functions.
* @package SimpleSAMLphp
class Crypto
Jaime Perez Crespo
committed
/**
* Decrypt data using AES-256-CBC and the key provided as a parameter.
Jaime Perez Crespo
committed
*
Jaime Pérez Crespo
committed
* @param string $ciphertext The HMAC of the encrypted data, the IV used and the encrypted data, concatenated.
Jaime Perez Crespo
committed
* @param string $secret The secret to use to decrypt the data.
Jaime Perez Crespo
committed
*
* @return string The decrypted data.
* @throws \InvalidArgumentException If $ciphertext is not a string.
* @throws Error\Exception If the openssl module is not loaded.
Jaime Perez Crespo
committed
*
Jaime Perez Crespo
committed
* @see \SimpleSAML\Utils\Crypto::aesDecrypt()
Jaime Perez Crespo
committed
*/
private static function aesDecryptInternal($ciphertext, $secret)
Jaime Perez Crespo
committed
{
if (!is_string($ciphertext)) {
throw new \InvalidArgumentException(
'Input parameter "$ciphertext" must be a string with more than 48 characters.'
);
}
/** @var int $len */
$len = mb_strlen($ciphertext, '8bit');
Jaime Pérez Crespo
committed
throw new \InvalidArgumentException(
'Input parameter "$ciphertext" must be a string with more than 48 characters.'
);
Jaime Perez Crespo
committed
}
if (!function_exists("openssl_decrypt")) {
throw new Error\Exception("The openssl PHP module is not loaded.");
Jaime Perez Crespo
committed
}
Jaime Pérez Crespo
committed
// derive encryption and authentication keys from the secret
$key = openssl_digest($secret, 'sha512');
Jaime Perez Crespo
committed
Jaime Pérez Crespo
committed
$hmac = mb_substr($ciphertext, 0, 32, '8bit');
$iv = mb_substr($ciphertext, 32, 16, '8bit');
$msg = mb_substr($ciphertext, 48, $len - 48, '8bit');
Jaime Pérez Crespo
committed
// authenticate the ciphertext
if (self::secureCompare(hash_hmac('sha256', $iv . $msg, substr($key, 64, 64), true), $hmac)) {
Jaime Pérez Crespo
committed
$plaintext = openssl_decrypt(
$msg,
'AES-256-CBC',
substr($key, 0, 64),
defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : 1,
Jaime Pérez Crespo
committed
$iv
);
Jaime Pérez Crespo
committed
return $plaintext;
}
}
throw new Error\Exception("Failed to decrypt ciphertext.");
Jaime Perez Crespo
committed
}
Jaime Perez Crespo
committed
Jaime Perez Crespo
committed
/**
* Decrypt data using AES-256-CBC and the system-wide secret salt as key.
Jaime Perez Crespo
committed
*
Jaime Pérez Crespo
committed
* @param string $ciphertext The HMAC of the encrypted data, the IV used and the encrypted data, concatenated.
Jaime Perez Crespo
committed
*
* @return string The decrypted data.
* @throws \InvalidArgumentException If $ciphertext is not a string.
* @throws Error\Exception If the openssl module is not loaded.
Jaime Perez Crespo
committed
*
Jaime Perez Crespo
committed
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function aesDecrypt($ciphertext)
{
return self::aesDecryptInternal($ciphertext, Config::getSecretSalt());
Jaime Perez Crespo
committed
}
/**
* Encrypt data using AES-256-CBC and the key provided as a parameter.
*
Jaime Perez Crespo
committed
* @param string $data The data to encrypt.
Jaime Perez Crespo
committed
* @param string $secret The secret to use to encrypt the data.
Jaime Perez Crespo
committed
*
Jaime Pérez Crespo
committed
* @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated.
Jaime Perez Crespo
committed
* @throws \InvalidArgumentException If $data is not a string.
* @throws Error\Exception If the openssl module is not loaded.
Jaime Perez Crespo
committed
*
Jaime Perez Crespo
committed
* @see \SimpleSAML\Utils\Crypto::aesEncrypt()
Jaime Perez Crespo
committed
*/
private static function aesEncryptInternal($data, $secret)
Jaime Perez Crespo
committed
{
if (!is_string($data)) {
Jaime Perez Crespo
committed
throw new \InvalidArgumentException('Input parameter "$data" must be a string.');
Jaime Perez Crespo
committed
}
Jaime Perez Crespo
committed
if (!function_exists("openssl_encrypt")) {
throw new Error\Exception('The openssl PHP module is not loaded.');
Jaime Perez Crespo
committed
}
Jaime Pérez Crespo
committed
// derive encryption and authentication keys from the secret
$key = openssl_digest($secret, 'sha512');
// generate a random IV
$iv = openssl_random_pseudo_bytes(16);
// encrypt the message
/** @var string|false $ciphertext */
$ciphertext = openssl_encrypt(
Jaime Pérez Crespo
committed
$data,
'AES-256-CBC',
substr($key, 0, 64),
defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : 1,
Jaime Pérez Crespo
committed
$iv
);
if ($ciphertext === false) {
throw new Error\Exception("Failed to encrypt plaintext.");
Jaime Pérez Crespo
committed
}
Jaime Perez Crespo
committed
Jaime Pérez Crespo
committed
// return the ciphertext with proper authentication
return hash_hmac('sha256', $iv . $ciphertext, substr($key, 64, 64), true) . $iv . $ciphertext;
Jaime Perez Crespo
committed
}
Jaime Perez Crespo
committed
Jaime Perez Crespo
committed
/**
* Encrypt data using AES-256-CBC and the system-wide secret salt as key.
Jaime Perez Crespo
committed
*
* @param string $data The data to encrypt.
*
Jaime Pérez Crespo
committed
* @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated.
Jaime Perez Crespo
committed
* @throws \InvalidArgumentException If $data is not a string.
* @throws Error\Exception If the openssl module is not loaded.
Jaime Perez Crespo
committed
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function aesEncrypt($data)
{
return self::aesEncryptInternal($data, Config::getSecretSalt());
Jaime Perez Crespo
committed
}
/**
* Convert data from DER to PEM encoding.
*
* @param string $der Data encoded in DER format.
* @param string $type The type of data we are encoding, as expressed by the PEM header. Defaults to "CERTIFICATE".
* @return string The same data encoded in PEM format.
* @see RFC7648 for known types and PEM format specifics.
*/
public static function der2pem($der, $type = 'CERTIFICATE')
{
return "-----BEGIN " . $type . "-----\n" .
chunk_split(base64_encode($der), 64, "\n") .
"-----END " . $type . "-----\n";
}
Jaime Perez Crespo
committed
/**
* Load a private key from metadata.
*
* This function loads a private key from a metadata array. It looks for the following elements:
* - 'privatekey': Name of a private key file in the cert-directory.
* - 'privatekey_pass': Password for the private key.
*
* It returns and array with the following elements:
* - 'PEM': Data for the private key, in PEM-format.
* - 'password': Password for the private key.
*
* @param \SimpleSAML\Configuration $metadata The metadata array the private key should be loaded from.
Jaime Perez Crespo
committed
* @param bool $required Whether the private key is required. If this is true, a
Jaime Perez Crespo
committed
* missing key will cause an exception. Defaults to false.
Jaime Perez Crespo
committed
* @param string $prefix The prefix which should be used when reading from the metadata
Jaime Perez Crespo
committed
* array. Defaults to ''.
* @param bool $full_path Whether the filename found in the configuration contains the
* full path to the private key or not. Default to false.
Jaime Perez Crespo
committed
*
* @return array|NULL Extracted private key, or NULL if no private key is present.
Jaime Perez Crespo
committed
* @throws \InvalidArgumentException If $required is not boolean or $prefix is not a string.
* @throws Error\Exception If no private key is found in the metadata, or it was not possible to load
Jaime Perez Crespo
committed
* it.
Jaime Perez Crespo
committed
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function loadPrivateKey(Configuration $metadata, $required = false, $prefix = '', $full_path = false)
Jaime Perez Crespo
committed
{
if (!is_bool($required) || !is_string($prefix) || !is_bool($full_path)) {
Jaime Perez Crespo
committed
throw new \InvalidArgumentException('Invalid input parameters.');
Jaime Perez Crespo
committed
}
$file = $metadata->getString($prefix . 'privatekey', null);
Jaime Perez Crespo
committed
if ($file === null) {
// no private key found
if ($required) {
throw new Error\Exception('No private key found in metadata.');
Jaime Perez Crespo
committed
} else {
return null;
}
}
if (!$full_path) {
$file = Config::getCertPath($file);
}
Jaime Perez Crespo
committed
$data = @file_get_contents($file);
if ($data === false) {
throw new Error\Exception('Unable to load private key from file "' . $file . '"');
Jaime Perez Crespo
committed
}
Jaime Perez Crespo
committed
'PEM' => $data,
'password' => $metadata->getString($prefix . 'privatekey_pass', null),
Jaime Perez Crespo
committed
return $ret;
}
Jaime Perez Crespo
committed
Jaime Perez Crespo
committed
/**
* Get public key or certificate from metadata.
*
* This function implements a function to retrieve the public key or certificate from a metadata array.
*
* It will search for the following elements in the metadata:
* - 'certData': The certificate as a base64-encoded string.
* - 'certificate': A file with a certificate or public key in PEM-format.
* - 'certFingerprint': The fingerprint of the certificate. Can be a single fingerprint, or an array of multiple
* valid fingerprints. (deprecated)
Jaime Perez Crespo
committed
*
* This function will return an array with these elements:
* - 'PEM': The public key/certificate in PEM-encoding.
* - 'certData': The certificate data, base64 encoded, on a single line. (Only present if this is a certificate.)
* - 'certFingerprint': Array of valid certificate fingerprints. (Deprecated. Only present if this is a
* certificate.)
Jaime Perez Crespo
committed
*
* @param \SimpleSAML\Configuration $metadata The metadata.
* @param bool $required Whether the public key is required. If this is TRUE, a missing key
Jaime Perez Crespo
committed
* will cause an exception. Default is FALSE.
Jaime Perez Crespo
committed
* @param string $prefix The prefix which should be used when reading from the metadata array.
Jaime Perez Crespo
committed
* Defaults to ''.
*
* @return array|NULL Public key or certificate data, or NULL if no public key or certificate was found.
* @throws \InvalidArgumentException If $metadata is not an instance of \SimpleSAML\Configuration, $required is not
Jaime Perez Crespo
committed
* boolean or $prefix is not a string.
* @throws Error\Exception If no public key is found in the metadata, or it was not possible to load
Jaime Perez Crespo
committed
* it.
Jaime Perez Crespo
committed
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Lasse Birnbaum Jensen
*/
public static function loadPublicKey(Configuration $metadata, $required = false, $prefix = '')
Jaime Perez Crespo
committed
{
Jaime Perez Crespo
committed
if (!is_bool($required) || !is_string($prefix)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
Jaime Perez Crespo
committed
$keys = $metadata->getPublicKeys(null, false, $prefix);
if (!empty($keys)) {
Jaime Perez Crespo
committed
foreach ($keys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
if ($key['signing'] !== true) {
continue;
}
$certData = $key['X509Certificate'];
$pem = "-----BEGIN CERTIFICATE-----\n" .
chunk_split($certData, 64) .
Jaime Perez Crespo
committed
"-----END CERTIFICATE-----\n";
$certFingerprint = strtolower(sha1(base64_decode($certData)));
Jaime Perez Crespo
committed
'certData' => $certData,
'PEM' => $pem,
'certFingerprint' => [$certFingerprint],
];
Jaime Perez Crespo
committed
}
// no valid key found
} elseif ($metadata->hasValue($prefix . 'certFingerprint')) {
Jaime Perez Crespo
committed
// we only have a fingerprint available
$fps = $metadata->getArrayizeString($prefix . 'certFingerprint');
Jaime Perez Crespo
committed
// normalize fingerprint(s) - lowercase and no colons
foreach ($fps as &$fp) {
Jaime Perez Crespo
committed
$fp = strtolower(str_replace(':', '', $fp));
}
/*
* We can't build a full certificate from a fingerprint, and may as well return an array with only the
* fingerprint(s) immediately.
*/
return ['certFingerprint' => $fps];
Jaime Perez Crespo
committed
}
// no public key/certificate available
if ($required) {
throw new Error\Exception('No public key / certificate found in metadata.');
Jaime Perez Crespo
committed
} else {
return null;
}
}
Jaime Perez Crespo
committed
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
/**
* Convert from PEM to DER encoding.
*
* @param string $pem Data encoded in PEM format.
* @return string The same data encoded in DER format.
* @throws \InvalidArgumentException If $pem is not encoded in PEM format.
* @see RFC7648 for PEM format specifics.
*/
public static function pem2der($pem)
{
$pem = trim($pem);
$begin = "-----BEGIN ";
$end = "-----END ";
$lines = explode("\n", $pem);
$last = count($lines) - 1;
if (strpos($lines[0], $begin) !== 0) {
throw new \InvalidArgumentException("pem2der: input is not encoded in PEM format.");
}
unset($lines[0]);
if (strpos($lines[$last], $end) !== 0) {
throw new \InvalidArgumentException("pem2der: input is not encoded in PEM format.");
}
unset($lines[$last]);
return base64_decode(implode($lines));
}
/**
* This function hashes a password with a given algorithm.
*
* @param string $password The password to hash.
* @param string|null $algorithm @deprecated The hashing algorithm, uppercase, optionally
* prepended with 'S' (salted). See hash_algos() for a complete list of hashing algorithms.
* @param string|null $salt @deprecated An optional salt to use.
*
* @return string The hashed password.
* @throws \InvalidArgumentException If the input parameter is not a string.
* @throws Error\Exception If the algorithm specified is not supported.
Jaime Perez Crespo
committed
*
Jaime Perez Crespo
committed
*
* @author Dyonisius Visser, TERENA <visser@terena.org>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function pwHash($password, $algorithm = null, $salt = null)
if (!is_null($algorithm)) {
// @deprecated Old-style
if (!is_string($algorithm) || !is_string($password)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// hash w/o salt
if (in_array(strtolower($algorithm), hash_algos(), true)) {
$alg_str = '{' . str_replace('SHA1', 'SHA', $algorithm) . '}'; // LDAP compatibility
$hash = hash(strtolower($algorithm), $password, true);
}
// hash w/ salt
if ($salt === null) {
// no salt provided, generate one
// default 8 byte salt, but 4 byte for LDAP SHA1 hashes
$bytes = ($algorithm == 'SSHA1') ? 4 : 8;
$salt = openssl_random_pseudo_bytes($bytes);
}
if ($algorithm[0] == 'S' && in_array(substr(strtolower($algorithm), 1), hash_algos(), true)) {
$alg = substr(strtolower($algorithm), 1); // 'sha256' etc
$alg_str = '{' . str_replace('SSHA1', 'SSHA', $algorithm) . '}'; // LDAP compatibility
$hash = hash($alg, $password . $salt, true);
return $alg_str . base64_encode($hash . $salt);
throw new Error\Exception('Hashing algorithm \'' . strtolower($algorithm) . '\' is not supported');
} else {
if (!is_string($password)) {
throw new \InvalidArgumentException('Invalid input parameter.');
} elseif (!is_string($hash = password_hash($password, PASSWORD_DEFAULT))) {
throw new \InvalidArgumentException('Error while hashing password.');
/**
* Compare two strings securely.
*
* This method checks if two strings are equal in constant time, avoiding timing attacks. Use it every time we need
* to compare a string with a secret that shouldn't be leaked, i.e. when verifying passwords, one-time codes, etc.
*
* @param string $known A known string.
* @param string $user A user-provided string to compare with the known string.
*
* @return bool True if both strings are equal, false otherwise.
*/
public static function secureCompare($known, $user)
{
/**
* This function checks if a password is valid
*
* @param string $hash The password as it appears in password file, optionally prepended with algorithm.
* @param string $password The password to check in clear.
*
* @return boolean True if the hash corresponds with the given password, false otherwise.
Jaime Perez Crespo
committed
* @throws \InvalidArgumentException If the input parameters are not strings.
* @throws Error\Exception If the algorithm specified is not supported.
Jaime Perez Crespo
committed
*
* @author Dyonisius Visser, TERENA <visser@terena.org>
*/
public static function pwValid($hash, $password)
{
if (!is_string($hash) || !is_string($password)) {
Jaime Perez Crespo
committed
throw new \InvalidArgumentException('Invalid input parameters.');
if (password_verify($password, $hash)) {
return true;
}
// return $hash === $password
// @deprecated remove everything below this line for 2.0
// match algorithm string (e.g. '{SSHA256}', '{MD5}')
if (preg_match('/^{(.*?)}(.*)$/', $hash, $matches)) {
// LDAP compatibility
$alg = preg_replace('/^(S?SHA)$/', '${1}1', $matches[1]);
// hash w/o salt
if (in_array(strtolower($alg), hash_algos(), true)) {
return self::secureCompare($hash, self::pwHash($password, $alg));
}
// hash w/ salt
if ($alg[0] === 'S' && in_array(substr(strtolower($alg), 1), hash_algos(), true)) {
$php_alg = substr(strtolower($alg), 1);
// get hash length of this algorithm to learn how long the salt is
$hash_length = strlen(hash($php_alg, '', true));
$salt = substr(base64_decode($matches[2]), $hash_length);
return self::secureCompare($hash, self::pwHash($password, $alg, $salt));
throw new Error\Exception('Hashing algorithm \'' . strtolower($alg) . '\' is not supported');
} else {
return $hash === $password;
}
}