<?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 { }