From 7879835b4966c09185a8b80e5a8d15f8b02618b2 Mon Sep 17 00:00:00 2001 From: PJ Eby Date: Sat, 16 Dec 2017 13:18:45 -0500 Subject: [PATCH 1/5] Store refresh token in sessions, instead of a cookie --- includes/Defuse/Crypto/Core.php | 434 ---------- includes/Defuse/Crypto/Crypto.php | 372 --------- includes/Defuse/Crypto/DerivedKeys.php | 37 - includes/Defuse/Crypto/Encoding.php | 212 ----- .../Crypto/Exception/BadFormatException.php | 7 - .../Crypto/Exception/CryptoException.php | 7 - .../EnvironmentIsBrokenException.php | 7 - .../Defuse/Crypto/Exception/IOException.php | 7 - .../WrongKeyOrModifiedCiphertextException.php | 7 - includes/Defuse/Crypto/File.php | 755 ------------------ includes/Defuse/Crypto/Key.php | 84 -- includes/Defuse/Crypto/KeyOrPassword.php | 119 --- .../Defuse/Crypto/KeyProtectedByPassword.php | 112 --- includes/Defuse/Crypto/RuntimeTests.php | 242 ------ .../openid-connect-generic-client-wrapper.php | 125 +-- 15 files changed, 27 insertions(+), 2500 deletions(-) delete mode 100644 includes/Defuse/Crypto/Core.php delete mode 100644 includes/Defuse/Crypto/Crypto.php delete mode 100644 includes/Defuse/Crypto/DerivedKeys.php delete mode 100644 includes/Defuse/Crypto/Encoding.php delete mode 100644 includes/Defuse/Crypto/Exception/BadFormatException.php delete mode 100644 includes/Defuse/Crypto/Exception/CryptoException.php delete mode 100644 includes/Defuse/Crypto/Exception/EnvironmentIsBrokenException.php delete mode 100644 includes/Defuse/Crypto/Exception/IOException.php delete mode 100644 includes/Defuse/Crypto/Exception/WrongKeyOrModifiedCiphertextException.php delete mode 100644 includes/Defuse/Crypto/File.php delete mode 100644 includes/Defuse/Crypto/Key.php delete mode 100644 includes/Defuse/Crypto/KeyOrPassword.php delete mode 100644 includes/Defuse/Crypto/KeyProtectedByPassword.php delete mode 100644 includes/Defuse/Crypto/RuntimeTests.php diff --git a/includes/Defuse/Crypto/Core.php b/includes/Defuse/Crypto/Core.php deleted file mode 100644 index c489f39..0000000 --- a/includes/Defuse/Crypto/Core.php +++ /dev/null @@ -1,434 +0,0 @@ - PHP_INT_MAX - 255) { - throw new Ex\EnvironmentIsBrokenException( - 'Integer overflow may occur.' - ); - } - - /* - * We start at the rightmost byte (big-endian) - * So, too, does OpenSSL: http://stackoverflow.com/a/3146214/2224584 - */ - for ($i = Core::BLOCK_BYTE_SIZE - 1; $i >= 0; --$i) { - $sum = \ord($ctr[$i]) + $inc; - - /* Detect integer overflow and fail. */ - if (! \is_int($sum)) { - throw new Ex\EnvironmentIsBrokenException( - 'Integer overflow in CTR mode nonce increment.' - ); - } - - $ctr[$i] = \pack('C', $sum & 0xFF); - $inc = $sum >> 8; - } - return $ctr; - } - - /** - * Returns a random byte string of the specified length. - * - * @param int $octets - * - * @throws Ex\EnvironmentIsBrokenException - * - * @return string - */ - public static function secureRandom($octets) - { - self::ensureFunctionExists('random_bytes'); - try { - return \random_bytes($octets); - } catch (\Exception $ex) { - throw new Ex\EnvironmentIsBrokenException( - 'Your system does not have a secure random number generator.' - ); - } - } - - /** - * Computes the HKDF key derivation function specified in - * http://tools.ietf.org/html/rfc5869. - * - * @param string $hash Hash Function - * @param string $ikm Initial Keying Material - * @param int $length How many bytes? - * @param string $info What sort of key are we deriving? - * @param string $salt - * - * @throws Ex\EnvironmentIsBrokenException - * - * @return string - */ - public static function HKDF($hash, $ikm, $length, $info = '', $salt = null) - { - $digest_length = Core::ourStrlen(\hash_hmac($hash, '', '', true)); - - // Sanity-check the desired output length. - if (empty($length) || ! \is_int($length) || - $length < 0 || $length > 255 * $digest_length) { - throw new Ex\EnvironmentIsBrokenException( - 'Bad output length requested of HKDF.' - ); - } - - // "if [salt] not provided, is set to a string of HashLen zeroes." - if (\is_null($salt)) { - $salt = \str_repeat("\x00", $digest_length); - } - - // HKDF-Extract: - // PRK = HMAC-Hash(salt, IKM) - // The salt is the HMAC key. - $prk = \hash_hmac($hash, $ikm, $salt, true); - - // HKDF-Expand: - - // This check is useless, but it serves as a reminder to the spec. - if (Core::ourStrlen($prk) < $digest_length) { - throw new Ex\EnvironmentIsBrokenException(); - } - - // T(0) = '' - $t = ''; - $last_block = ''; - for ($block_index = 1; Core::ourStrlen($t) < $length; ++$block_index) { - // T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??) - $last_block = \hash_hmac( - $hash, - $last_block . $info . \chr($block_index), - $prk, - true - ); - // T = T(1) | T(2) | T(3) | ... | T(N) - $t .= $last_block; - } - - // ORM = first L octets of T - $orm = Core::ourSubstr($t, 0, $length); - if ($orm === false) { - throw new Ex\EnvironmentIsBrokenException(); - } - return $orm; - } - - /** - * Checks if two equal-length strings are the same without leaking - * information through side channels. - * - * @param string $expected - * @param string $given - * - * @throws Ex\EnvironmentIsBrokenException - * - * @return bool - */ - public static function hashEquals($expected, $given) - { - static $native = null; - if ($native === null) { - $native = \function_exists('hash_equals'); - } - if ($native) { - return \hash_equals($expected, $given); - } - - // We can't just compare the strings with '==', since it would make - // timing attacks possible. We could use the XOR-OR constant-time - // comparison algorithm, but that may not be a reliable defense in an - // interpreted language. So we use the approach of HMACing both strings - // with a random key and comparing the HMACs. - - // We're not attempting to make variable-length string comparison - // secure, as that's very difficult. Make sure the strings are the same - // length. - if (Core::ourStrlen($expected) !== Core::ourStrlen($given)) { - throw new Ex\EnvironmentIsBrokenException(); - } - - $blind = Core::secureRandom(32); - $message_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $given, $blind); - $correct_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $expected, $blind); - return $correct_compare === $message_compare; - } - /** - * Throws an exception if the constant doesn't exist. - * - * @param string $name - * - * @throws Ex\EnvironmentIsBrokenException - */ - public static function ensureConstantExists($name) - { - if (! \defined($name)) { - throw new Ex\EnvironmentIsBrokenException(); - } - } - - /** - * Throws an exception if the function doesn't exist. - * - * @param string $name - * - * @throws Ex\EnvironmentIsBrokenException - */ - public static function ensureFunctionExists($name) - { - if (! \function_exists($name)) { - throw new Ex\EnvironmentIsBrokenException(); - } - } - - /* - * We need these strlen() and substr() functions because when - * 'mbstring.func_overload' is set in php.ini, the standard strlen() and - * substr() are replaced by mb_strlen() and mb_substr(). - */ - - /** - * Computes the length of a string in bytes. - * - * @param string $str - * - * @throws Ex\EnvironmentIsBrokenException - * - * @return int - */ - public static function ourStrlen($str) - { - static $exists = null; - if ($exists === null) { - $exists = \function_exists('mb_strlen'); - } - if ($exists) { - $length = \mb_strlen($str, '8bit'); - if ($length === false) { - throw new Ex\EnvironmentIsBrokenException(); - } - return $length; - } else { - return \strlen($str); - } - } - - /** - * Behaves roughly like the function substr() in PHP 7 does. - * - * @param string $str - * @param int $start - * @param int $length - * - * @throws Ex\EnvironmentIsBrokenException - * - * @return string - */ - public static function ourSubstr($str, $start, $length = null) - { - static $exists = null; - if ($exists === null) { - $exists = \function_exists('mb_substr'); - } - - if ($exists) { - // mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP - // 5.3, so we have to find the length ourselves. - if (! isset($length)) { - if ($start >= 0) { - $length = Core::ourStrlen($str) - $start; - } else { - $length = -$start; - } - } - - // This is required to make mb_substr behavior identical to substr. - // Without this, mb_substr() would return false, contra to what the - // PHP documentation says (it doesn't say it can return false.) - if ($start === Core::ourStrlen($str) && $length === 0) { - return ''; - } - - if ($start > Core::ourStrlen($str)) { - return false; - } - - $substr = \mb_substr($str, $start, $length, '8bit'); - if (Core::ourStrlen($substr) !== $length) { - throw new Ex\EnvironmentIsBrokenException( - 'Your version of PHP has bug #66797. Its implementation of - mb_substr() is incorrect. See the details here: - https://bugs.php.net/bug.php?id=66797' - ); - } - return $substr; - } - - // Unlike mb_substr(), substr() doesn't accept NULL for length - if (isset($length)) { - return \substr($str, $start, $length); - } else { - return \substr($str, $start); - } - } - - /** - * Computes the PBKDF2 password-based key derivation function. - * - * The PBKDF2 function is defined in RFC 2898. Test vectors can be found in - * RFC 6070. This implementation of PBKDF2 was originally created by Taylor - * Hornby, with improvements from http://www.variations-of-shadow.com/. - * - * @param string $algorithm The hash algorithm to use. Recommended: SHA256 - * @param string $password The password. - * @param string $salt A salt that is unique to the password. - * @param int $count Iteration count. Higher is better, but slower. Recommended: At least 1000. - * @param int $key_length The length of the derived key in bytes. - * @param bool $raw_output If true, the key is returned in raw binary format. Hex encoded otherwise. - * - * @throws Ex\EnvironmentIsBrokenException - * - * @return string A $key_length-byte key derived from the password and salt. - */ - public static function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false) - { - // Type checks: - if (! \is_string($algorithm)) { - throw new \InvalidArgumentException( - 'pbkdf2(): algorithm must be a string' - ); - } - if (! \is_string($password)) { - throw new \InvalidArgumentException( - 'pbkdf2(): password must be a string' - ); - } - if (! \is_string($salt)) { - throw new \InvalidArgumentException( - 'pbkdf2(): salt must be a string' - ); - } - // Coerce strings to integers with no information loss or overflow - $count += 0; - $key_length += 0; - - $algorithm = \strtolower($algorithm); - if (! \in_array($algorithm, \hash_algos(), true)) { - throw new Ex\EnvironmentIsBrokenException( - 'Invalid or unsupported hash algorithm.' - ); - } - - // Whitelist, or we could end up with people using CRC32. - $ok_algorithms = [ - 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', - 'ripemd160', 'ripemd256', 'ripemd320', 'whirlpool', - ]; - if (! \in_array($algorithm, $ok_algorithms, true)) { - throw new Ex\EnvironmentIsBrokenException( - 'Algorithm is not a secure cryptographic hash function.' - ); - } - - if ($count <= 0 || $key_length <= 0) { - throw new Ex\EnvironmentIsBrokenException( - 'Invalid PBKDF2 parameters.' - ); - } - - if (\function_exists('hash_pbkdf2')) { - // The output length is in NIBBLES (4-bits) if $raw_output is false! - if (! $raw_output) { - $key_length = $key_length * 2; - } - return \hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output); - } - - $hash_length = Core::ourStrlen(\hash($algorithm, '', true)); - $block_count = \ceil($key_length / $hash_length); - - $output = ''; - for ($i = 1; $i <= $block_count; $i++) { - // $i encoded as 4 bytes, big endian. - $last = $salt . \pack('N', $i); - // first iteration - $last = $xorsum = \hash_hmac($algorithm, $last, $password, true); - // perform the other $count - 1 iterations - for ($j = 1; $j < $count; $j++) { - $xorsum ^= ($last = \hash_hmac($algorithm, $last, $password, true)); - } - $output .= $xorsum; - } - - if ($raw_output) { - return Core::ourSubstr($output, 0, $key_length); - } else { - return Encoding::binToHex(Core::ourSubstr($output, 0, $key_length)); - } - } -} diff --git a/includes/Defuse/Crypto/Crypto.php b/includes/Defuse/Crypto/Crypto.php deleted file mode 100644 index 3acd611..0000000 --- a/includes/Defuse/Crypto/Crypto.php +++ /dev/null @@ -1,372 +0,0 @@ -deriveKeys($salt); - $ekey = $keys->getEncryptionKey(); - $akey = $keys->getAuthenticationKey(); - $iv = Core::secureRandom(Core::BLOCK_BYTE_SIZE); - - $ciphertext = Core::CURRENT_VERSION . $salt . $iv . self::plainEncrypt($plaintext, $ekey, $iv); - $auth = \hash_hmac(Core::HASH_FUNCTION_NAME, $ciphertext, $akey, true); - $ciphertext = $ciphertext . $auth; - - if ($raw_binary) { - return $ciphertext; - } - return Encoding::binToHex($ciphertext); - } - - /** - * Decrypts a ciphertext to a string with either a key or a password. - * - * @param string $ciphertext - * @param KeyOrPassword $secret - * @param bool $raw_binary - * - * @throws Ex\EnvironmentIsBrokenException - * @throws Ex\WrongKeyOrModifiedCiphertextException - * - * @return string - */ - private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw_binary) - { - RuntimeTests::runtimeTest(); - - if (! $raw_binary) { - try { - $ciphertext = Encoding::hexToBin($ciphertext); - } catch (Ex\BadFormatException $ex) { - throw new Ex\WrongKeyOrModifiedCiphertextException( - 'Ciphertext has invalid hex encoding.' - ); - } - } - - if (Core::ourStrlen($ciphertext) < Core::MINIMUM_CIPHERTEXT_SIZE) { - throw new Ex\WrongKeyOrModifiedCiphertextException( - 'Ciphertext is too short.' - ); - } - - // Get and check the version header. - $header = Core::ourSubstr($ciphertext, 0, Core::HEADER_VERSION_SIZE); - if ($header !== Core::CURRENT_VERSION) { - throw new Ex\WrongKeyOrModifiedCiphertextException( - 'Bad version header.' - ); - } - - // Get the salt. - $salt = Core::ourSubstr( - $ciphertext, - Core::HEADER_VERSION_SIZE, - Core::SALT_BYTE_SIZE - ); - if ($salt === false) { - throw new Ex\EnvironmentIsBrokenException(); - } - - // Get the IV. - $iv = Core::ourSubstr( - $ciphertext, - Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE, - Core::BLOCK_BYTE_SIZE - ); - if ($iv === false) { - throw new Ex\EnvironmentIsBrokenException(); - } - - // Get the HMAC. - $hmac = Core::ourSubstr( - $ciphertext, - Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE, - Core::MAC_BYTE_SIZE - ); - if ($hmac === false) { - throw new Ex\EnvironmentIsBrokenException(); - } - - // Get the actual encrypted ciphertext. - $encrypted = Core::ourSubstr( - $ciphertext, - Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + - Core::BLOCK_BYTE_SIZE, - Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE - Core::SALT_BYTE_SIZE - - Core::BLOCK_BYTE_SIZE - Core::HEADER_VERSION_SIZE - ); - if ($encrypted === false) { - throw new Ex\EnvironmentIsBrokenException(); - } - - // Derive the separate encryption and authentication keys from the key - // or password, whichever it is. - $keys = $secret->deriveKeys($salt); - - if (self::verifyHMAC($hmac, $header . $salt . $iv . $encrypted, $keys->getAuthenticationKey())) { - $plaintext = self::plainDecrypt($encrypted, $keys->getEncryptionKey(), $iv, Core::CIPHER_METHOD); - return $plaintext; - } else { - throw new Ex\WrongKeyOrModifiedCiphertextException( - 'Integrity check failed.' - ); - } - } - - /** - * Raw unauthenticated encryption (insecure on its own). - * - * @param string $plaintext - * @param string $key - * @param string $iv - * - * @throws Ex\EnvironmentIsBrokenException - * - * @return string - */ - protected static function plainEncrypt($plaintext, $key, $iv) - { - Core::ensureConstantExists('OPENSSL_RAW_DATA'); - Core::ensureFunctionExists('openssl_encrypt'); - $ciphertext = \openssl_encrypt( - $plaintext, - Core::CIPHER_METHOD, - $key, - OPENSSL_RAW_DATA, - $iv - ); - - if ($ciphertext === false) { - throw new Ex\EnvironmentIsBrokenException( - 'openssl_encrypt() failed.' - ); - } - - return $ciphertext; - } - - /** - * Raw unauthenticated decryption (insecure on its own). - * - * @param string $ciphertext - * @param string $key - * @param string $iv - * @param string $cipherMethod - * - * @throws Ex\EnvironmentIsBrokenException - * - * @return string - */ - protected static function plainDecrypt($ciphertext, $key, $iv, $cipherMethod) - { - Core::ensureConstantExists('OPENSSL_RAW_DATA'); - Core::ensureFunctionExists('openssl_decrypt'); - $plaintext = \openssl_decrypt( - $ciphertext, - $cipherMethod, - $key, - OPENSSL_RAW_DATA, - $iv - ); - if ($plaintext === false) { - throw new Ex\EnvironmentIsBrokenException( - 'openssl_decrypt() failed.' - ); - } - - return $plaintext; - } - - /** - * Verifies an HMAC without leaking information through side-channels. - * - * @param string $correct_hmac - * @param string $message - * @param string $key - * - * @throws Ex\EnvironmentIsBrokenException - * - * @return bool - */ - protected static function verifyHMAC($correct_hmac, $message, $key) - { - $message_hmac = \hash_hmac(Core::HASH_FUNCTION_NAME, $message, $key, true); - return Core::hashEquals($correct_hmac, $message_hmac); - } -} diff --git a/includes/Defuse/Crypto/DerivedKeys.php b/includes/Defuse/Crypto/DerivedKeys.php deleted file mode 100644 index fcfc043..0000000 --- a/includes/Defuse/Crypto/DerivedKeys.php +++ /dev/null @@ -1,37 +0,0 @@ -akey; - } - - /** - * Returns the encryption key. - */ - public function getEncryptionKey() - { - return $this->ekey; - } - - /** - * Constructor for DerivedKeys. - * - * @param string $akey - * @param string $ekey - */ - public function __construct($akey, $ekey) - { - $this->akey = $akey; - $this->ekey = $ekey; - } -} diff --git a/includes/Defuse/Crypto/Encoding.php b/includes/Defuse/Crypto/Encoding.php deleted file mode 100644 index 6e022d7..0000000 --- a/includes/Defuse/Crypto/Encoding.php +++ /dev/null @@ -1,212 +0,0 @@ -> 4; - $hex .= \pack( - 'CC', - 87 + $b + ((($b - 10) >> 8) & ~38), - 87 + $c + ((($c - 10) >> 8) & ~38) - ); - } - return $hex; - } - - /** - * Converts a hexadecimal string into a byte string without leaking - * information through side channels. - * - * @param string $hex_string - * - * @throws Ex\BadFormatException - * @throws Ex\EnvironmentIsBrokenException - * - * @return string - */ - public static function hexToBin($hex_string) - { - $hex_pos = 0; - $bin = ''; - $hex_len = Core::ourStrlen($hex_string); - $state = 0; - $c_acc = 0; - - while ($hex_pos < $hex_len) { - $c = \ord($hex_string[$hex_pos]); - $c_num = $c ^ 48; - $c_num0 = ($c_num - 10) >> 8; - $c_alpha = ($c & ~32) - 55; - $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8; - if (($c_num0 | $c_alpha0) === 0) { - throw new Ex\BadFormatException( - 'Encoding::hexToBin() input is not a hex string.' - ); - } - $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0); - if ($state === 0) { - $c_acc = $c_val * 16; - } else { - $bin .= \pack('C', $c_acc | $c_val); - } - $state ^= 1; - ++$hex_pos; - } - return $bin; - } - - /* - * SECURITY NOTE ON APPLYING CHECKSUMS TO SECRETS: - * - * The checksum introduces a potential security weakness. For example, - * suppose we apply a checksum to a key, and that an adversary has an - * exploit against the process containing the key, such that they can - * overwrite an arbitrary byte of memory and then cause the checksum to - * be verified and learn the result. - * - * In this scenario, the adversary can extract the key one byte at - * a time by overwriting it with their guess of its value and then - * asking if the checksum matches. If it does, their guess was right. - * This kind of attack may be more easy to implement and more reliable - * than a remote code execution attack. - * - * This attack also applies to authenticated encryption as a whole, in - * the situation where the adversary can overwrite a byte of the key - * and then cause a valid ciphertext to be decrypted, and then - * determine whether the MAC check passed or failed. - * - * By using the full SHA256 hash instead of truncating it, I'm ensuring - * that both ways of going about the attack are equivalently difficult. - * A shorter checksum of say 32 bits might be more useful to the - * adversary as an oracle in case their writes are coarser grained. - * - * Because the scenario assumes a serious vulnerability, we don't try - * to prevent attacks of this style. - */ - - /** - * INTERNAL USE ONLY: Applies a version header, applies a checksum, and - * then encodes a byte string into a range of printable ASCII characters. - * - * @param string $header - * @param string $bytes - * - * @throws Ex\EnvironmentIsBrokenException - * - * @return string - */ - public static function saveBytesToChecksummedAsciiSafeString($header, $bytes) - { - // Headers must be a constant length to prevent one type's header from - // being a prefix of another type's header, leading to ambiguity. - if (Core::ourStrlen($header) !== self::SERIALIZE_HEADER_BYTES) { - throw new Ex\EnvironmentIsBrokenException( - 'Header must be ' . self::SERIALIZE_HEADER_BYTES . ' bytes.' - ); - } - - return Encoding::binToHex( - $header . - $bytes . - \hash( - self::CHECKSUM_HASH_ALGO, - $header . $bytes, - true - ) - ); - } - - /** - * INTERNAL USE ONLY: Decodes, verifies the header and checksum, and returns - * the encoded byte string. - * - * @param string $expected_header - * @param string $string - * - * @throws Ex\EnvironmentIsBrokenException - * @throws Ex\BadFormatException - * - * @return string - */ - public static function loadBytesFromChecksummedAsciiSafeString($expected_header, $string) - { - // Headers must be a constant length to prevent one type's header from - // being a prefix of another type's header, leading to ambiguity. - if (Core::ourStrlen($expected_header) !== self::SERIALIZE_HEADER_BYTES) { - throw new Ex\EnvironmentIsBrokenException( - 'Header must be 4 bytes.' - ); - } - - $bytes = Encoding::hexToBin($string); - - /* Make sure we have enough bytes to get the version header and checksum. */ - if (Core::ourStrlen($bytes) < self::SERIALIZE_HEADER_BYTES + self::CHECKSUM_BYTE_SIZE) { - throw new Ex\BadFormatException( - 'Encoded data is shorter than expected.' - ); - } - - /* Grab the version header. */ - $actual_header = Core::ourSubstr($bytes, 0, self::SERIALIZE_HEADER_BYTES); - - if ($actual_header !== $expected_header) { - throw new Ex\BadFormatException( - 'Invalid header.' - ); - } - - /* Grab the bytes that are part of the checksum. */ - $checked_bytes = Core::ourSubstr( - $bytes, - 0, - Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE - ); - - /* Grab the included checksum. */ - $checksum_a = Core::ourSubstr( - $bytes, - Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE, - self::CHECKSUM_BYTE_SIZE - ); - - /* Re-compute the checksum. */ - $checksum_b = \hash(self::CHECKSUM_HASH_ALGO, $checked_bytes, true); - - /* Check if the checksum matches. */ - if (! Core::hashEquals($checksum_a, $checksum_b)) { - throw new Ex\BadFormatException( - "Data is corrupted, the checksum doesn't match" - ); - } - - return Core::ourSubstr( - $bytes, - self::SERIALIZE_HEADER_BYTES, - Core::ourStrlen($bytes) - self::SERIALIZE_HEADER_BYTES - self::CHECKSUM_BYTE_SIZE - ); - } -} diff --git a/includes/Defuse/Crypto/Exception/BadFormatException.php b/includes/Defuse/Crypto/Exception/BadFormatException.php deleted file mode 100644 index 804d9c1..0000000 --- a/includes/Defuse/Crypto/Exception/BadFormatException.php +++ /dev/null @@ -1,7 +0,0 @@ -deriveKeys($file_salt); - $ekey = $keys->getEncryptionKey(); - $akey = $keys->getAuthenticationKey(); - - $ivsize = Core::BLOCK_BYTE_SIZE; - $iv = Core::secureRandom($ivsize); - - /* Initialize a streaming HMAC state. */ - $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); - if ($hmac === false) { - throw new Ex\EnvironmentIsBrokenException( - 'Cannot initialize a hash context' - ); - } - - /* Write the header, salt, and IV. */ - self::writeBytes( - $outputHandle, - Core::CURRENT_VERSION . $file_salt . $iv, - Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + $ivsize - ); - - /* Add the header, salt, and IV to the HMAC. */ - \hash_update($hmac, Core::CURRENT_VERSION); - \hash_update($hmac, $file_salt); - \hash_update($hmac, $iv); - - /* $thisIv will be incremented after each call to the encryption. */ - $thisIv = $iv; - - /* How many blocks do we encrypt at a time? We increment by this value. */ - $inc = Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE; - - /* Loop until we reach the end of the input file. */ - $at_file_end = false; - while (! (\feof($inputHandle) || $at_file_end)) { - /* Find out if we can read a full buffer, or only a partial one. */ - $pos = \ftell($inputHandle); - if ($pos === false) { - throw new Ex\IOException( - 'Could not get current position in input file during encryption' - ); - } - if ($pos + Core::BUFFER_BYTE_SIZE >= $inputSize) { - /* We're at the end of the file, so we need to break out of the loop. */ - $at_file_end = true; - $read = self::readBytes( - $inputHandle, - $inputSize - $pos - ); - } else { - $read = self::readBytes( - $inputHandle, - Core::BUFFER_BYTE_SIZE - ); - } - - /* Encrypt this buffer. */ - $encrypted = \openssl_encrypt( - $read, - Core::CIPHER_METHOD, - $ekey, - OPENSSL_RAW_DATA, - $thisIv - ); - - if ($encrypted === false) { - throw new Ex\EnvironmentIsBrokenException( - 'OpenSSL encryption error' - ); - } - - /* Write this buffer's ciphertext. */ - self::writeBytes($outputHandle, $encrypted, Core::ourStrlen($encrypted)); - /* Add this buffer's ciphertext to the HMAC. */ - \hash_update($hmac, $encrypted); - - /* Increment the counter by the number of blocks in a buffer. */ - $thisIv = Core::incrementCounter($thisIv, $inc); - /* WARNING: Usually, unless the file is a multiple of the buffer - * size, $thisIv will contain an incorrect value here on the last - * iteration of this loop. */ - } - - /* Get the HMAC and append it to the ciphertext. */ - $final_mac = \hash_final($hmac, true); - self::writeBytes($outputHandle, $final_mac, Core::MAC_BYTE_SIZE); - } - - /** - * Decrypts a file-backed resource with either a key or a password. - * - * @param resource $inputHandle - * @param resource $outputHandle - * @param KeyOrPassword $secret - * - * @throws Ex\EnvironmentIsBrokenException - * @throws Ex\IOException - * @throws Ex\WrongKeyOrModifiedCiphertextException - */ - public static function decryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret) - { - if (! \is_resource($inputHandle)) { - throw new Ex\IOException( - 'Input handle must be a resource!' - ); - } - if (! \is_resource($outputHandle)) { - throw new Ex\IOException( - 'Output handle must be a resource!' - ); - } - - /* Make sure the file is big enough for all the reads we need to do. */ - $stat = \fstat($inputHandle); - if ($stat['size'] < Core::MINIMUM_CIPHERTEXT_SIZE) { - throw new Ex\WrongKeyOrModifiedCiphertextException( - 'Input file is too small to have been created by this library.' - ); - } - - /* Check the version header. */ - $header = self::readBytes($inputHandle, Core::HEADER_VERSION_SIZE); - if ($header !== Core::CURRENT_VERSION) { - throw new Ex\WrongKeyOrModifiedCiphertextException( - 'Bad version header.' - ); - } - - /* Get the salt. */ - $file_salt = self::readBytes($inputHandle, Core::SALT_BYTE_SIZE); - - /* Get the IV. */ - $ivsize = Core::BLOCK_BYTE_SIZE; - $iv = self::readBytes($inputHandle, $ivsize); - - /* Derive the authentication and encryption keys. */ - $keys = $secret->deriveKeys($file_salt); - $ekey = $keys->getEncryptionKey(); - $akey = $keys->getAuthenticationKey(); - - /* We'll store the MAC of each buffer-sized chunk as we verify the - * actual MAC, so that we can check them again when decrypting. */ - $macs = []; - - /* $thisIv will be incremented after each call to the decryption. */ - $thisIv = $iv; - - /* How many blocks do we encrypt at a time? We increment by this value. */ - $inc = Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE; - - /* Get the HMAC. */ - if (\fseek($inputHandle, (-1 * Core::MAC_BYTE_SIZE), SEEK_END) === false) { - throw new Ex\IOException( - 'Cannot seek to beginning of MAC within input file' - ); - } - - /* Get the position of the last byte in the actual ciphertext. */ - $cipher_end = \ftell($inputHandle); - if ($cipher_end === false) { - throw new Ex\IOException( - 'Cannot read input file' - ); - } - /* We have the position of the first byte of the HMAC. Go back by one. */ - --$cipher_end; - - /* Read the HMAC. */ - $stored_mac = self::readBytes($inputHandle, Core::MAC_BYTE_SIZE); - - /* Initialize a streaming HMAC state. */ - $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); - if ($hmac === false) { - throw new Ex\EnvironmentIsBrokenException( - 'Cannot initialize a hash context' - ); - } - - /* Reset file pointer to the beginning of the file after the header */ - if (\fseek($inputHandle, Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { - throw new Ex\IOException( - 'Cannot read seek within input file' - ); - } - - /* Seek to the start of the actual ciphertext. */ - if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize, SEEK_CUR) === false) { - throw new Ex\IOException( - 'Cannot seek input file to beginning of ciphertext' - ); - } - - /* PASS #1: Calculating the HMAC. */ - - \hash_update($hmac, $header); - \hash_update($hmac, $file_salt); - \hash_update($hmac, $iv); - $hmac2 = \hash_copy($hmac); - - $break = false; - while (! $break) { - $pos = \ftell($inputHandle); - if ($pos === false) { - throw new Ex\IOException( - 'Could not get current position in input file during decryption' - ); - } - - /* Read the next buffer-sized chunk (or less). */ - if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) { - $break = true; - $read = self::readBytes( - $inputHandle, - $cipher_end - $pos + 1 - ); - } else { - $read = self::readBytes( - $inputHandle, - Core::BUFFER_BYTE_SIZE - ); - } - - /* Update the HMAC. */ - \hash_update($hmac, $read); - - /* Remember this buffer-sized chunk's HMAC. */ - $chunk_mac = \hash_copy($hmac); - if ($chunk_mac === false) { - throw new Ex\EnvironmentIsBrokenException( - 'Cannot duplicate a hash context' - ); - } - $macs []= \hash_final($chunk_mac); - } - - /* Get the final HMAC, which should match the stored one. */ - $final_mac = \hash_final($hmac, true); - - /* Verify the HMAC. */ - if (! Core::hashEquals($final_mac, $stored_mac)) { - throw new Ex\WrongKeyOrModifiedCiphertextException( - 'Integrity check failed.' - ); - } - - /* PASS #2: Decrypt and write output. */ - - /* Rewind to the start of the actual ciphertext. */ - if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === false) { - throw new Ex\IOException( - 'Could not move the input file pointer during decryption' - ); - } - - $at_file_end = false; - while (! $at_file_end) { - $pos = \ftell($inputHandle); - if ($pos === false) { - throw new Ex\IOException( - 'Could not get current position in input file during decryption' - ); - } - - /* Read the next buffer-sized chunk (or less). */ - if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) { - $at_file_end = true; - $read = self::readBytes( - $inputHandle, - $cipher_end - $pos + 1 - ); - } else { - $read = self::readBytes( - $inputHandle, - Core::BUFFER_BYTE_SIZE - ); - } - - /* Recalculate the MAC (so far) and compare it with the one we - * remembered from pass #1 to ensure attackers didn't change the - * ciphertext after MAC verification. */ - \hash_update($hmac2, $read); - $calc_mac = \hash_copy($hmac2); - if ($calc_mac === false) { - throw new Ex\EnvironmentIsBrokenException( - 'Cannot duplicate a hash context' - ); - } - $calc = \hash_final($calc_mac); - - if (empty($macs)) { - throw new Ex\WrongKeyOrModifiedCiphertextException( - 'File was modified after MAC verification' - ); - } elseif (! Core::hashEquals(\array_shift($macs), $calc)) { - throw new Ex\WrongKeyOrModifiedCiphertextException( - 'File was modified after MAC verification' - ); - } - - /* Decrypt this buffer-sized chunk. */ - $decrypted = \openssl_decrypt( - $read, - Core::CIPHER_METHOD, - $ekey, - OPENSSL_RAW_DATA, - $thisIv - ); - if ($decrypted === false) { - throw new Ex\EnvironmentIsBrokenException( - 'OpenSSL decryption error' - ); - } - - /* Write the plaintext to the output file. */ - self::writeBytes( - $outputHandle, - $decrypted, - Core::ourStrlen($decrypted) - ); - - /* Increment the IV by the amount of blocks in a buffer. */ - $thisIv = Core::incrementCounter($thisIv, $inc); - /* WARNING: Usually, unless the file is a multiple of the buffer - * size, $thisIv will contain an incorrect value here on the last - * iteration of this loop. */ - } - } - - /** - * Read from a stream; prevent partial reads. - * - * @param resource $stream - * @param int $num_bytes - * - * @throws Ex\IOException - * @throws Ex\EnvironmentIsBrokenException - * - * @return string - */ - public static function readBytes($stream, $num_bytes) - { - if ($num_bytes < 0) { - throw new Ex\EnvironmentIsBrokenException( - 'Tried to read less than 0 bytes' - ); - } elseif ($num_bytes === 0) { - return ''; - } - $buf = ''; - $remaining = $num_bytes; - while ($remaining > 0 && ! \feof($stream)) { - $read = \fread($stream, $remaining); - - if ($read === false) { - throw new Ex\IOException( - 'Could not read from the file' - ); - } - $buf .= $read; - $remaining -= Core::ourStrlen($read); - } - if (Core::ourStrlen($buf) !== $num_bytes) { - throw new Ex\IOException( - 'Tried to read past the end of the file' - ); - } - return $buf; - } - - /** - * Write to a stream; prevents partial writes. - * - * @param resource $stream - * @param string $buf - * @param int $num_bytes - * - * @throws Ex\IOException - * - * @return string - */ - public static function writeBytes($stream, $buf, $num_bytes = null) - { - $bufSize = Core::ourStrlen($buf); - if ($num_bytes === null) { - $num_bytes = $bufSize; - } - if ($num_bytes > $bufSize) { - throw new Ex\IOException( - 'Trying to write more bytes than the buffer contains.' - ); - } - if ($num_bytes < 0) { - throw new Ex\IOException( - 'Tried to write less than 0 bytes' - ); - } - $remaining = $num_bytes; - while ($remaining > 0) { - $written = \fwrite($stream, $buf, $remaining); - if ($written === false) { - throw new Ex\IOException( - 'Could not write to the file' - ); - } - $buf = Core::ourSubstr($buf, $written, null); - $remaining -= $written; - } - return $num_bytes; - } - - /** - * Returns the last PHP error's or warning's message string. - * - * @return string - */ - private static function getLastErrorMessage() - { - $error = error_get_last(); - if ($error === null) { - return '[no PHP error]'; - } else { - return $error['message']; - } - } -} diff --git a/includes/Defuse/Crypto/Key.php b/includes/Defuse/Crypto/Key.php deleted file mode 100644 index ca7b9b2..0000000 --- a/includes/Defuse/Crypto/Key.php +++ /dev/null @@ -1,84 +0,0 @@ -key_bytes - ); - } - - /** - * Gets the raw bytes of the key. - * - * @return string - */ - public function getRawBytes() - { - return $this->key_bytes; - } - - /** - * Constructs a new Key object from a string of raw bytes. - * - * @param string $bytes - * - * @throws Ex\EnvironmentIsBrokenException - */ - private function __construct($bytes) - { - if (Core::ourStrlen($bytes) !== self::KEY_BYTE_SIZE) { - throw new Ex\EnvironmentIsBrokenException( - 'Bad key length.' - ); - } - $this->key_bytes = $bytes; - } - -} diff --git a/includes/Defuse/Crypto/KeyOrPassword.php b/includes/Defuse/Crypto/KeyOrPassword.php deleted file mode 100644 index 2a46b71..0000000 --- a/includes/Defuse/Crypto/KeyOrPassword.php +++ /dev/null @@ -1,119 +0,0 @@ -secret_type === self::SECRET_TYPE_KEY) { - $akey = Core::HKDF( - Core::HASH_FUNCTION_NAME, - $this->secret->getRawBytes(), - Core::KEY_BYTE_SIZE, - Core::AUTHENTICATION_INFO_STRING, - $salt - ); - $ekey = Core::HKDF( - Core::HASH_FUNCTION_NAME, - $this->secret->getRawBytes(), - Core::KEY_BYTE_SIZE, - Core::ENCRYPTION_INFO_STRING, - $salt - ); - return new DerivedKeys($akey, $ekey); - } elseif ($this->secret_type === self::SECRET_TYPE_PASSWORD) { - /* Our PBKDF2 polyfill is vulnerable to a DoS attack documented in - * GitHub issue #230. The fix is to pre-hash the password to ensure - * it is short. We do the prehashing here instead of in pbkdf2() so - * that pbkdf2() still computes the function as defined by the - * standard. */ - $prehash = \hash(Core::HASH_FUNCTION_NAME, $this->secret, true); - $prekey = Core::pbkdf2( - Core::HASH_FUNCTION_NAME, - $prehash, - $salt, - self::PBKDF2_ITERATIONS, - Core::KEY_BYTE_SIZE, - true - ); - $akey = Core::HKDF( - Core::HASH_FUNCTION_NAME, - $prekey, - Core::KEY_BYTE_SIZE, - Core::AUTHENTICATION_INFO_STRING, - $salt - ); - /* Note the cryptographic re-use of $salt here. */ - $ekey = Core::HKDF( - Core::HASH_FUNCTION_NAME, - $prekey, - Core::KEY_BYTE_SIZE, - Core::ENCRYPTION_INFO_STRING, - $salt - ); - return new DerivedKeys($akey, $ekey); - } else { - throw new Ex\EnvironmentIsBrokenException('Bad secret type.'); - } - } - - /** - * Constructor for KeyOrPassword. - * - * @param int $secret_type - * @param mixed $secret (either a Key or a password string) - */ - private function __construct($secret_type, $secret) - { - $this->secret_type = $secret_type; - $this->secret = $secret; - } -} diff --git a/includes/Defuse/Crypto/KeyProtectedByPassword.php b/includes/Defuse/Crypto/KeyProtectedByPassword.php deleted file mode 100644 index b9a40f1..0000000 --- a/includes/Defuse/Crypto/KeyProtectedByPassword.php +++ /dev/null @@ -1,112 +0,0 @@ -saveToAsciiSafeString(), - \hash(Core::HASH_FUNCTION_NAME, $password, true), - true - ); - - return new KeyProtectedByPassword($encrypted_key); - } - - /** - * Loads a KeyProtectedByPassword from its encoded form. - * - * @param string $saved_key_string - * - * @throws Ex\BadFormatException - * - * @return KeyProtectedByPassword - */ - public static function loadFromAsciiSafeString($saved_key_string) - { - $encrypted_key = Encoding::loadBytesFromChecksummedAsciiSafeString( - self::PASSWORD_KEY_CURRENT_VERSION, - $saved_key_string - ); - return new KeyProtectedByPassword($encrypted_key); - } - - /** - * Encodes the KeyProtectedByPassword into a string of printable ASCII - * characters. - * - * @throws Ex\EnvironmentIsBrokenException - * - * @return string - */ - public function saveToAsciiSafeString() - { - return Encoding::saveBytesToChecksummedAsciiSafeString( - self::PASSWORD_KEY_CURRENT_VERSION, - $this->encrypted_key - ); - } - - /** - * Decrypts the protected key, returning an unprotected Key object that can - * be used for encryption and decryption. - * - * @throws Ex\EnvironmentIsBrokenException - * @throws Ex\WrongKeyOrModifiedCiphertextException - * - * @return Key - */ - public function unlockKey($password) - { - try { - $inner_key_encoded = Crypto::decryptWithPassword( - $this->encrypted_key, - \hash(Core::HASH_FUNCTION_NAME, $password, true), - true - ); - return Key::loadFromAsciiSafeString($inner_key_encoded); - } catch (Ex\BadFormatException $ex) { - /* This should never happen unless an attacker replaced the - * encrypted key ciphertext with some other ciphertext that was - * encrypted with the same password. We transform the exception type - * here in order to make the API simpler, avoiding the need to - * document that this method might throw an Ex\BadFormatException. */ - throw new Ex\WrongKeyOrModifiedCiphertextException( - "The decrypted key was found to be in an invalid format. " . - "This very likely indicates it was modified by an attacker." - ); - } - } - - /** - * Constructor for KeyProtectedByPassword. - * - * @param string $encrypted_key - */ - private function __construct($encrypted_key) - { - $this->encrypted_key = $encrypted_key; - } -} diff --git a/includes/Defuse/Crypto/RuntimeTests.php b/includes/Defuse/Crypto/RuntimeTests.php deleted file mode 100644 index 9c0ba33..0000000 --- a/includes/Defuse/Crypto/RuntimeTests.php +++ /dev/null @@ -1,242 +0,0 @@ -getRawBytes()) != Core::KEY_BYTE_SIZE) { - throw new Ex\EnvironmentIsBrokenException(); - } - - if (Core::ENCRYPTION_INFO_STRING == Core::AUTHENTICATION_INFO_STRING) { - throw new Ex\EnvironmentIsBrokenException(); - } - } catch (Ex\EnvironmentIsBrokenException $ex) { - // Do this, otherwise it will stay in the "tests are running" state. - $test_state = 3; - throw $ex; - } - - // Change this to '0' make the tests always re-run (for benchmarking). - $test_state = 1; - } - - /** - * High-level tests of Crypto operations. - * - * @throws Ex\EnvironmentIsBrokenException - */ - private static function testEncryptDecrypt() - { - $key = Key::createNewRandomKey(); - $data = "EnCrYpT EvErYThInG\x00\x00"; - - // Make sure encrypting then decrypting doesn't change the message. - $ciphertext = Crypto::encrypt($data, $key, true); - try { - $decrypted = Crypto::decrypt($ciphertext, $key, true); - } catch (Ex\WrongKeyOrModifiedCiphertextException $ex) { - // It's important to catch this and change it into a - // Ex\EnvironmentIsBrokenException, otherwise a test failure could trick - // the user into thinking it's just an invalid ciphertext! - throw new Ex\EnvironmentIsBrokenException(); - } - if ($decrypted !== $data) { - throw new Ex\EnvironmentIsBrokenException(); - } - - // Modifying the ciphertext: Appending a string. - try { - Crypto::decrypt($ciphertext . 'a', $key, true); - throw new Ex\EnvironmentIsBrokenException(); - } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ - } - - // Modifying the ciphertext: Changing an HMAC byte. - $indices_to_change = [ - 0, // The header. - Core::HEADER_VERSION_SIZE + 1, // the salt - Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + 1, // the IV - Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + Core::BLOCK_BYTE_SIZE + 1, // the ciphertext - ]; - - foreach ($indices_to_change as $index) { - try { - $ciphertext[$index] = \chr((\ord($ciphertext[$index]) + 1) % 256); - Crypto::decrypt($ciphertext, $key, true); - throw new Ex\EnvironmentIsBrokenException(); - } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ - } - } - - // Decrypting with the wrong key. - $key = Key::createNewRandomKey(); - $data = 'abcdef'; - $ciphertext = Crypto::encrypt($data, $key, true); - $wrong_key = Key::createNewRandomKey(); - try { - Crypto::decrypt($ciphertext, $wrong_key, true); - throw new Ex\EnvironmentIsBrokenException(); - } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ - } - - // Ciphertext too small. - $key = Key::createNewRandomKey(); - $ciphertext = \str_repeat('A', Core::MINIMUM_CIPHERTEXT_SIZE - 1); - try { - Crypto::decrypt($ciphertext, $key, true); - throw new Ex\EnvironmentIsBrokenException(); - } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ - } - } - - /** - * Test HKDF against test vectors. - * - * @throws Ex\EnvironmentIsBrokenException - */ - private static function HKDFTestVector() - { - // HKDF test vectors from RFC 5869 - - // Test Case 1 - $ikm = \str_repeat("\x0b", 22); - $salt = Encoding::hexToBin('000102030405060708090a0b0c'); - $info = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9'); - $length = 42; - $okm = Encoding::hexToBin( - '3cb25f25faacd57a90434f64d0362f2a' . - '2d2d0a90cf1a5a4c5db02d56ecc4c5bf' . - '34007208d5b887185865' - ); - $computed_okm = Core::HKDF('sha256', $ikm, $length, $info, $salt); - if ($computed_okm !== $okm) { - throw new Ex\EnvironmentIsBrokenException(); - } - - // Test Case 7 - $ikm = \str_repeat("\x0c", 22); - $length = 42; - $okm = Encoding::hexToBin( - '2c91117204d745f3500d636a62f64f0a' . - 'b3bae548aa53d423b0d1f27ebba6f5e5' . - '673a081d70cce7acfc48' - ); - $computed_okm = Core::HKDF('sha1', $ikm, $length, '', null); - if ($computed_okm !== $okm) { - throw new Ex\EnvironmentIsBrokenException(); - } - } - - /** - * Test HMAC against test vectors. - * - * @throws Ex\EnvironmentIsBrokenException - */ - private static function HMACTestVector() - { - // HMAC test vector From RFC 4231 (Test Case 1) - $key = \str_repeat("\x0b", 20); - $data = 'Hi There'; - $correct = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'; - if (\hash_hmac(Core::HASH_FUNCTION_NAME, $data, $key) !== $correct) { - throw new Ex\EnvironmentIsBrokenException(); - } - } - - /** - * Test AES against test vectors. - * - * @throws Ex\EnvironmentIsBrokenException - */ - private static function AESTestVector() - { - // AES CTR mode test vector from NIST SP 800-38A - $key = Encoding::hexToBin( - '603deb1015ca71be2b73aef0857d7781' . - '1f352c073b6108d72d9810a30914dff4' - ); - $iv = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'); - $plaintext = Encoding::hexToBin( - '6bc1bee22e409f96e93d7e117393172a' . - 'ae2d8a571e03ac9c9eb76fac45af8e51' . - '30c81c46a35ce411e5fbc1191a0a52ef' . - 'f69f2445df4f9b17ad2b417be66c3710' - ); - $ciphertext = Encoding::hexToBin( - '601ec313775789a5b7a7f504bbf3d228' . - 'f443e3ca4d62b59aca84e990cacaf5c5' . - '2b0930daa23de94ce87017ba2d84988d' . - 'dfc9c58db67aada613c2dd08457941a6' - ); - - $computed_ciphertext = Crypto::plainEncrypt($plaintext, $key, $iv); - if ($computed_ciphertext !== $ciphertext) { - echo \str_repeat("\n", 30); - echo \bin2hex($computed_ciphertext); - echo "\n---\n"; - echo \bin2hex($ciphertext); - echo \str_repeat("\n", 30); - throw new Ex\EnvironmentIsBrokenException(); - } - - $computed_plaintext = Crypto::plainDecrypt($ciphertext, $key, $iv, Core::CIPHER_METHOD); - if ($computed_plaintext !== $plaintext) { - throw new Ex\EnvironmentIsBrokenException(); - } - } -} diff --git a/includes/openid-connect-generic-client-wrapper.php b/includes/openid-connect-generic-client-wrapper.php index 8242fb8..135b26c 100644 --- a/includes/openid-connect-generic-client-wrapper.php +++ b/includes/openid-connect-generic-client-wrapper.php @@ -45,9 +45,6 @@ class OpenID_Connect_Generic_Client_Wrapper { static public function register( OpenID_Connect_Generic_Client $client, WP_Option_Settings $settings, WP_Option_Logger $logger ){ $client_wrapper = new self( $client, $settings, $logger ); - // remove cookies on logout - add_action( 'wp_logout', array( $client_wrapper, 'wp_logout' ) ); - // integrated logout if ( $settings->endpoint_end_session ) { add_filter( 'allowed_redirect_hosts', array( $client_wrapper, 'update_allowed_redirect_hosts' ), 99, 1 ); @@ -130,25 +127,18 @@ class OpenID_Connect_Generic_Client_Wrapper { return; } - $is_openid_connect_user = get_user_meta( wp_get_current_user()->ID, 'openid-connect-generic-user', TRUE ); - if ( empty( $is_openid_connect_user ) ) { - return; - } + $user_id = wp_get_current_user()->ID; + $manager = WP_Session_Tokens::get_instance( $user_id ); + $token = wp_get_session_token(); + $session = $manager->get( $token ); - if ( ! isset( $_COOKIE[ $this->cookie_token_refresh_key] ) ) { - wp_logout(); - $this->error_redirect( new WP_Error( 'token-refresh-cookie-missing', __( 'Single sign-on cookie missing. Please login again.' ), $_COOKIE ) ); - exit; + if ( ! isset( $session[ $this->cookie_token_refresh_key ] ) ) { + // not an OpenID-based session + return; } - $user_id = wp_get_current_user()->ID; $current_time = current_time( 'timestamp', TRUE ); - $refresh_token_info = $this->read_token_refresh_info_from_cookie( $user_id ); - - if ( ! $refresh_token_info ) { - wp_logout(); - $this->error_redirect( new WP_Error( 'token-refresh-cookie-missing', __( 'Single sign-on cookie invalid. Please login again.' ), $_COOKIE ) ); - } + $refresh_token_info = $session[ $this->cookie_token_refresh_key ]; $next_access_token_refresh_time = $refresh_token_info[ 'next_access_token_refresh_time' ]; $refresh_token = $refresh_token_info[ 'refresh_token' ]; @@ -179,7 +169,7 @@ class OpenID_Connect_Generic_Client_Wrapper { $this->error_redirect( $token_response ); } - $this->issue_token_refresh_info_cookie( $user_id, $token_response ); + $this->save_refresh_token( $manager, $token, $token_response ); } /** @@ -209,21 +199,6 @@ class OpenID_Connect_Generic_Client_Wrapper { return $this->error; } - /** - * Implements hook wp_logout - * - * Remove cookies - */ - function wp_logout() { - // set OpenID Connect user flag to false on logout to allow users to log into the same account without OpenID Connect - if( $this->settings->link_existing_users ) { - if( get_user_meta( wp_get_current_user()->ID, 'openid-connect-generic-user', TRUE ) ) - update_user_meta( wp_get_current_user()->ID, 'openid-connect-generic-user', FALSE ); - } - - setcookie( $this->cookie_token_refresh_key, false, 1, COOKIEPATH, COOKIE_DOMAIN, is_ssl() ); - } - /** * Add the end_session endpoint to WP core's whitelist of redirect hosts * @@ -446,82 +421,36 @@ class OpenID_Connect_Generic_Client_Wrapper { update_user_meta( $user->ID, 'openid-connect-generic-user', TRUE ); } + // Create the WP session, so we know its token + $expiration = time() + apply_filters( 'auth_cookie_expiration', 2 * DAY_IN_SECONDS, $user->ID, FALSE ); + $manager = WP_Session_Tokens::get_instance( $user->ID ); + $token = $manager->create( $expiration ); + + // Save the refresh token in the session + $this->save_refresh_token( $manager, $token, $token_response ); + // you did great, have a cookie! - $this->issue_token_refresh_info_cookie( $user->ID, $token_response ); - wp_set_auth_cookie( $user->ID, FALSE ); + wp_set_auth_cookie( $user->ID, FALSE, '', $token); do_action( 'wp_login', $user->user_login, $user ); } /** - * Create encrypted refresh_token cookie + * Save refresh token to WP session tokens * - * @param $user_id + * @param $manager + * @param $token * @param $token_response */ - function issue_token_refresh_info_cookie( $user_id, $token_response ) { - $cookie_value = serialize( array( + function save_refresh_token( $manager, $token, $token_response ) { + $session = $manager->get($token); + $session[$this->cookie_token_refresh_key] = array( 'next_access_token_refresh_time' => $token_response['expires_in'] + current_time( 'timestamp' , TRUE ), 'refresh_token' => isset( $token_response[ 'refresh_token' ] ) ? $token_response[ 'refresh_token' ] : false - ) ); - $key = $this->get_refresh_cookie_encryption_key( $user_id ); - $encrypted_cookie_value = \Defuse\Crypto\Crypto::encrypt( $cookie_value, $key ); - setcookie( $this->cookie_token_refresh_key, $encrypted_cookie_value, 0, COOKIEPATH, COOKIE_DOMAIN, is_ssl() ); - } - - /** - * Retrieve and decrypt refresh_token contents from user cookie - * @param $user_id - * - * @return bool|mixed - */ - function read_token_refresh_info_from_cookie( $user_id ) { - if ( ! isset( $_COOKIE[ $this->cookie_token_refresh_key ] ) ) { - return false; - } - - try { - $encrypted_cookie_value = $_COOKIE[$this->cookie_token_refresh_key]; - $key = $this->get_refresh_cookie_encryption_key( $user_id ); - $cookie_value = unserialize( \Defuse\Crypto\Crypto::decrypt($encrypted_cookie_value, $key) ); - - if ( ! isset( $cookie_value[ 'next_access_token_refresh_time' ] ) - || ! $cookie_value[ 'next_access_token_refresh_time' ] - || ! isset( $cookie_value[ 'refresh_token' ] ) ) - { - return false; - } - - return $cookie_value; - } - catch ( Exception $e ) { - $this->logger->log( $e->getMessage() ); - return false; - } + ); + $manager->update($token, $session); + return; } - /** - * Retrieve or regenerate a user's unique encryption key - * - * @param $user_id - * - * @return \Defuse\Crypto\Key - */ - function get_refresh_cookie_encryption_key( $user_id ) { - $meta_key = 'openid-connect-generic-refresh-cookie-key'; - $existing_key_string = get_user_meta( $user_id, $meta_key, true ); - - try { - $user_encryption_key = \Defuse\Crypto\Key::loadFromAsciiSafeString( $existing_key_string ); - } - catch ( Exception $e ) { - $this->logger->log( "Error loading user {$user_id} refresh token cookie key, generating new: " . $e->getMessage() ); - $user_encryption_key = \Defuse\Crypto\Key::createNewRandomKey(); - update_user_meta( $user_id, $meta_key, $user_encryption_key->saveToAsciiSafeString() ); - } - - return $user_encryption_key; - } - /** * Get the user that has meta data matching a * From 134d0116b89af4d8691f98edd303938ac7073207 Mon Sep 17 00:00:00 2001 From: PJ Eby Date: Sat, 16 Dec 2017 13:24:52 -0500 Subject: [PATCH 2/5] Drop unnecessary user_meta keys --- includes/openid-connect-generic-client-wrapper.php | 7 ------- openid-connect-generic.php | 2 -- 2 files changed, 9 deletions(-) diff --git a/includes/openid-connect-generic-client-wrapper.php b/includes/openid-connect-generic-client-wrapper.php index 135b26c..072441b 100644 --- a/includes/openid-connect-generic-client-wrapper.php +++ b/includes/openid-connect-generic-client-wrapper.php @@ -416,11 +416,6 @@ class OpenID_Connect_Generic_Client_Wrapper { update_user_meta( $user->ID, 'openid-connect-generic-last-id-token-claim', $id_token_claim ); update_user_meta( $user->ID, 'openid-connect-generic-last-user-claim', $user_claim ); - // if we're allowing users to use WordPress and OpenID Connect, we need to set this to true at every login - if( $this->settings->link_existing_users ) { - update_user_meta( $user->ID, 'openid-connect-generic-user', TRUE ); - } - // Create the WP session, so we know its token $expiration = time() + apply_filters( 'auth_cookie_expiration', 2 * DAY_IN_SECONDS, $user->ID, FALSE ); $manager = WP_Session_Tokens::get_instance( $user->ID ); @@ -725,7 +720,6 @@ class OpenID_Connect_Generic_Client_Wrapper { $user = get_user_by( 'id', $uid ); // save some meta data about this new user for the future - add_user_meta( $user->ID, 'openid-connect-generic-user', TRUE, TRUE ); add_user_meta( $user->ID, 'openid-connect-generic-subject-identity', (string) $subject_identity, TRUE ); // log the results @@ -748,7 +742,6 @@ class OpenID_Connect_Generic_Client_Wrapper { */ function update_existing_user( $uid, $subject_identity ) { // add the OpenID Connect meta data - add_user_meta( $uid, 'openid-connect-generic-user', TRUE, TRUE ); add_user_meta( $uid, 'openid-connect-generic-subject-identity', (string) $subject_identity, TRUE ); // allow plugins / themes to take action on user update diff --git a/openid-connect-generic.php b/openid-connect-generic.php index 01dd127..c9747d8 100644 --- a/openid-connect-generic.php +++ b/openid-connect-generic.php @@ -28,12 +28,10 @@ Notes - openid-connect-generic-redirect-user-back - 2 args: $redirect_url, $user. Allows interruption of redirect during login. User Meta - - openid-connect-generic-user - (bool) if the user was created by this plugin - openid-connect-generic-subject-identity - the identity of the user provided by the idp - openid-connect-generic-last-id-token-claim - the user's most recent id_token claim, decoded - openid-connect-generic-last-user-claim - the user's most recent user_claim - openid-connect-generic-last-token-response - the user's most recent token response - - openid-connect-generic-refresh-cookie-key - encryption key used to secure refresh token info in cookie Options - openid_connect_generic_settings - plugin settings From caf132449900241341110ce76ed105d7eaa41033 Mon Sep 17 00:00:00 2001 From: PJ Eby Date: Sat, 16 Dec 2017 13:52:07 -0500 Subject: [PATCH 3/5] Defer logouts to after all WP plugins initialize --- includes/openid-connect-generic-client-wrapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/openid-connect-generic-client-wrapper.php b/includes/openid-connect-generic-client-wrapper.php index 072441b..ad83301 100644 --- a/includes/openid-connect-generic-client-wrapper.php +++ b/includes/openid-connect-generic-client-wrapper.php @@ -71,7 +71,7 @@ class OpenID_Connect_Generic_Client_Wrapper { // verify token for any logged in user if ( is_user_logged_in() ) { - $client_wrapper->ensure_tokens_still_fresh(); + add_action( 'wp_loaded', [$client_wrapper, 'ensure_tokens_still_fresh']); } return $client_wrapper; From 662e9fb25bbc58788bd4caf9b4fec364229adac2 Mon Sep 17 00:00:00 2001 From: PJ Eby Date: Sat, 16 Dec 2017 17:17:25 -0500 Subject: [PATCH 4/5] Track refresh token expiration (fixes #51) --- .../openid-connect-generic-client-wrapper.php | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/includes/openid-connect-generic-client-wrapper.php b/includes/openid-connect-generic-client-wrapper.php index ad83301..579f274 100644 --- a/includes/openid-connect-generic-client-wrapper.php +++ b/includes/openid-connect-generic-client-wrapper.php @@ -141,18 +141,22 @@ class OpenID_Connect_Generic_Client_Wrapper { $refresh_token_info = $session[ $this->cookie_token_refresh_key ]; $next_access_token_refresh_time = $refresh_token_info[ 'next_access_token_refresh_time' ]; - $refresh_token = $refresh_token_info[ 'refresh_token' ]; if ( $current_time < $next_access_token_refresh_time ) { return; } - if ( ! $refresh_token ) { + $refresh_token = $refresh_token_info[ 'refresh_token' ]; + $refresh_expires = $refresh_token_info[ 'refresh_expires' ]; + + if ( ! $refresh_token || ( $refresh_expires && $current_time > $refresh_expires ) ) { wp_logout(); if ( $this->settings->redirect_on_logout ) { $this->error_redirect( new WP_Error( 'access-token-expired', __( 'Session expired. Please login again.' ) ) ); } + + return; } $token_result = $this->client->request_new_tokens( $refresh_token ); @@ -438,10 +442,20 @@ class OpenID_Connect_Generic_Client_Wrapper { */ function save_refresh_token( $manager, $token, $token_response ) { $session = $manager->get($token); + $now = current_time( 'timestamp' , TRUE ); $session[$this->cookie_token_refresh_key] = array( - 'next_access_token_refresh_time' => $token_response['expires_in'] + current_time( 'timestamp' , TRUE ), - 'refresh_token' => isset( $token_response[ 'refresh_token' ] ) ? $token_response[ 'refresh_token' ] : false + 'next_access_token_refresh_time' => $token_response['expires_in'] + $now, + 'refresh_token' => isset( $token_response[ 'refresh_token' ] ) ? $token_response[ 'refresh_token' ] : false, + 'refresh_expires' => false, ); + if ( isset( $token_response[ 'refresh_expires_in' ] ) ) { + $refresh_expires_in = $token_response[ 'refresh_expires_in' ]; + if ($refresh_expires_in > 0) { + // leave enough time for the actual refresh request to go through + $refresh_expires = $now + $refresh_expires_in - $this->alter_http_request_timeout(5); + $session[$this->cookie_token_refresh_key]['refresh_expires'] = $refresh_expires; + } + } $manager->update($token, $session); return; } From b14554e8fe2c4d7a37fa588a53bb093c525ac9f2 Mon Sep 17 00:00:00 2001 From: PJ Eby Date: Sat, 16 Dec 2017 17:30:42 -0500 Subject: [PATCH 5/5] WP still supports PHP 5.2.4 :( --- includes/openid-connect-generic-client-wrapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/openid-connect-generic-client-wrapper.php b/includes/openid-connect-generic-client-wrapper.php index 579f274..7f07ed4 100644 --- a/includes/openid-connect-generic-client-wrapper.php +++ b/includes/openid-connect-generic-client-wrapper.php @@ -71,7 +71,7 @@ class OpenID_Connect_Generic_Client_Wrapper { // verify token for any logged in user if ( is_user_logged_in() ) { - add_action( 'wp_loaded', [$client_wrapper, 'ensure_tokens_still_fresh']); + add_action( 'wp_loaded', array($client_wrapper, 'ensure_tokens_still_fresh')); } return $client_wrapper;