|
<?php |
|
|
|
/** |
|
* Copyright (c) 2015 Miloslav Hůla (https://github.com/milo) |
|
*/ |
|
|
|
namespace Milo; |
|
|
|
|
|
final class Crypt |
|
{ |
|
const |
|
CIPHER_METHOD = 'aes-256-ctr', |
|
CRYPT_KEY_LENGTH = 32, |
|
HASH_ALGORITHM = 'sha256', |
|
HASH_LENGTH = 64; |
|
|
|
|
|
public function __construct() |
|
{ |
|
if (PHP_VERSION_ID < 50600) { |
|
throw new CryptException('PHP 5.6.0 or newer is required.'); |
|
} elseif (!extension_loaded('openssl')) { |
|
throw new CryptException('Missing OpenSSL PHP module.'); |
|
} elseif (!in_array(self::CIPHER_METHOD, openssl_get_cipher_methods(), true)) { |
|
throw new CryptException('Missing OpenSSL encryption method ' . self::CIPHER_METHOD . '.'); |
|
} |
|
} |
|
|
|
|
|
/** |
|
* @param string |
|
* @param string |
|
* @param string |
|
* @param string |
|
* @return string |
|
* @throws CryptException |
|
*/ |
|
public function encryptBinary($text, $encryptKey, $signatureKey, $iv = null) |
|
{ |
|
if (strlen($encryptKey) !== self::CRYPT_KEY_LENGTH) { |
|
throw new CryptException('Encryption key needs to be ' . self::CRYPT_KEY_LENGTH . ' bytes long, but ' . strlen($encryptKey) . ' given.'); |
|
} |
|
|
|
if ($iv === null) { |
|
$iv = self::randomBytes(openssl_cipher_iv_length(self::CIPHER_METHOD)); |
|
} elseif (($ivLen = strlen($iv)) !== ($ivNeedLen = openssl_cipher_iv_length(self::CIPHER_METHOD))) { |
|
throw new CryptException("IV needs to be exactly $ivNeedLen bytes long, $ivLen given."); |
|
} |
|
|
|
$ciphered = $iv . openssl_encrypt( |
|
$text, |
|
self::CIPHER_METHOD, |
|
$encryptKey, |
|
OPENSSL_RAW_DATA, |
|
$iv |
|
); |
|
|
|
return hash_hmac(self::HASH_ALGORITHM, $ciphered, $signatureKey) . $ciphered; |
|
} |
|
|
|
|
|
/** |
|
* @param string |
|
* @param string |
|
* @param string |
|
* @param string |
|
* @return string |
|
* @throws CryptException |
|
*/ |
|
public function encrypt($text, $encryptKey, $signatureKey, $iv = null) |
|
{ |
|
return base64_encode($this->encryptBinary($text, $encryptKey, $signatureKey, $iv)); |
|
} |
|
|
|
|
|
/** |
|
* @param string |
|
* @param string |
|
* @param string |
|
* @return string |
|
* @throws CryptException |
|
*/ |
|
public function decryptBinary($text, $encryptKey, $signatureKey) |
|
{ |
|
if (strlen($text) < self::HASH_LENGTH) { |
|
throw new CryptException('Signed encrypted message is only ' . strlen($text) . ' bytes long, expected more.'); |
|
} |
|
|
|
$hmac = substr($text, 0, self::HASH_LENGTH); |
|
$text = substr($text, self::HASH_LENGTH); |
|
|
|
if (!hash_equals(hash_hmac(self::HASH_ALGORITHM, $text, $signatureKey), $hmac)) { |
|
throw new CryptException('Message signature does not match.'); |
|
} |
|
|
|
$ivLength = openssl_cipher_iv_length(self::CIPHER_METHOD); |
|
if (strlen($text) < $ivLength) { |
|
throw new CryptException('Encrypted message is only ' . strlen($text) . ' bytes long, expected more.'); |
|
} |
|
|
|
$iv = substr($text, 0, $ivLength); |
|
$text = substr($text, $ivLength); |
|
|
|
$decrypted = openssl_decrypt( |
|
$text, |
|
self::CIPHER_METHOD, |
|
$encryptKey, |
|
OPENSSL_RAW_DATA, |
|
$iv |
|
); |
|
|
|
if ($decrypted === false) { |
|
throw new CryptException('Message decryption failed.'); |
|
} |
|
|
|
return $decrypted; |
|
} |
|
|
|
|
|
/** |
|
* @param string |
|
* @param string |
|
* @param string |
|
* @return string |
|
* @throws CryptException |
|
*/ |
|
public function decrypt($text, $encryptKey, $signatureKey) |
|
{ |
|
return $this->decryptBinary(base64_decode($text), $encryptKey, $signatureKey); |
|
} |
|
|
|
|
|
private static function randomBytes($length) |
|
{ |
|
if ($length < 1) { |
|
throw new CryptException('Length must be greater then zero.'); |
|
} |
|
|
|
if (!defined('PHP_WINDOWS_VERSION_BUILD') && is_readable('/dev/urandom')) { |
|
return file_get_contents('/dev/urandom', false, null, -1, $length); |
|
} |
|
|
|
$bytes = openssl_random_pseudo_bytes($length, $secure); |
|
if ($secure !== true) { |
|
throw new CryptException('Random bytes are not cryptographically strong.'); |
|
} |
|
|
|
return $bytes; |
|
} |
|
} |
|
|
|
|
|
class CryptException extends \RuntimeException |
|
{ |
|
} |