Merge pull request #52 from dirtsimple/wp_tokens

Simplify refresh/logout handling (to fix #49, #50, and #51)
isekai
Jonathan Daggerhart 7 years ago committed by GitHub
commit e02e455965
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,434 +0,0 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class Core
{
const HEADER_VERSION_SIZE = 4;
const MINIMUM_CIPHERTEXT_SIZE = 84;
const CURRENT_VERSION = "\xDE\xF5\x02\x00";
const CIPHER_METHOD = 'aes-256-ctr';
const BLOCK_BYTE_SIZE = 16;
const KEY_BYTE_SIZE = 32;
const SALT_BYTE_SIZE = 32;
const MAC_BYTE_SIZE = 32;
const HASH_FUNCTION_NAME = 'sha256';
const ENCRYPTION_INFO_STRING = 'DefusePHP|V2|KeyForEncryption';
const AUTHENTICATION_INFO_STRING = 'DefusePHP|V2|KeyForAuthentication';
const BUFFER_BYTE_SIZE = 1048576;
const LEGACY_CIPHER_METHOD = 'aes-128-cbc';
const LEGACY_BLOCK_BYTE_SIZE = 16;
const LEGACY_KEY_BYTE_SIZE = 16;
const LEGACY_HASH_FUNCTION_NAME = 'sha256';
const LEGACY_MAC_BYTE_SIZE = 32;
const LEGACY_ENCRYPTION_INFO_STRING = 'DefusePHP|KeyForEncryption';
const LEGACY_AUTHENTICATION_INFO_STRING = 'DefusePHP|KeyForAuthentication';
/*
* V2.0 Format: VERSION (4 bytes) || SALT (32 bytes) || IV (16 bytes) ||
* CIPHERTEXT (varies) || HMAC (32 bytes)
*
* V1.0 Format: HMAC (32 bytes) || IV (16 bytes) || CIPHERTEXT (varies).
*/
/**
* Adds an integer to a block-sized counter.
*
* @param string $ctr
* @param int $inc
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function incrementCounter($ctr, $inc)
{
if (Core::ourStrlen($ctr) !== Core::BLOCK_BYTE_SIZE) {
throw new Ex\EnvironmentIsBrokenException(
'Trying to increment a nonce of the wrong size.'
);
}
if (! \is_int($inc)) {
throw new Ex\EnvironmentIsBrokenException(
'Trying to increment nonce by a non-integer.'
);
}
if ($inc < 0) {
throw new Ex\EnvironmentIsBrokenException(
'Trying to increment nonce by a negative amount.'
);
}
if ($inc > 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));
}
}
}

@ -1,372 +0,0 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
class Crypto
{
/**
* Encrypts a string with a Key.
*
* @param string $plaintext
* @param Key $key
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function encrypt($plaintext, Key $key, $raw_binary = false)
{
return self::encryptInternal(
$plaintext,
KeyOrPassword::createFromKey($key),
$raw_binary
);
}
/**
* Encrypts a string with a password, using a slow key derivation function
* to make password cracking more expensive.
*
* @param string $plaintext
* @param string $password
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function encryptWithPassword($plaintext, $password, $raw_binary = false)
{
return self::encryptInternal(
$plaintext,
KeyOrPassword::createFromPassword($password),
$raw_binary
);
}
/**
* Decrypts a ciphertext to a string with a Key.
*
* @param string $ciphertext
* @param Key $key
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @return string
*/
public static function decrypt($ciphertext, Key $key, $raw_binary = false)
{
return self::decryptInternal(
$ciphertext,
KeyOrPassword::createFromKey($key),
$raw_binary
);
}
/**
* Decrypts a ciphertext to a string with a password, using a slow key
* derivation function to make password cracking more expensive.
*
* @param string $ciphertext
* @param string $password
* @param bool $raw_binary
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @return string
*/
public static function decryptWithPassword($ciphertext, $password, $raw_binary = false)
{
return self::decryptInternal(
$ciphertext,
KeyOrPassword::createFromPassword($password),
$raw_binary
);
}
/**
* Decrypts a legacy ciphertext produced by version 1 of this library.
*
* @param string $ciphertext
* @param string $key
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*
* @return string
*/
public static function legacyDecrypt($ciphertext, $key)
{
RuntimeTests::runtimeTest();
// Extract the HMAC from the front of the ciphertext.
if (Core::ourStrlen($ciphertext) <= Core::LEGACY_MAC_BYTE_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Ciphertext is too short.'
);
}
$hmac = Core::ourSubstr($ciphertext, 0, Core::LEGACY_MAC_BYTE_SIZE);
if ($hmac === false) {
throw new Ex\EnvironmentIsBrokenException();
}
$ciphertext = Core::ourSubstr($ciphertext, Core::LEGACY_MAC_BYTE_SIZE);
if ($ciphertext === false) {
throw new Ex\EnvironmentIsBrokenException();
}
// Regenerate the same authentication sub-key.
$akey = Core::HKDF(
Core::LEGACY_HASH_FUNCTION_NAME,
$key,
Core::LEGACY_KEY_BYTE_SIZE,
Core::LEGACY_AUTHENTICATION_INFO_STRING,
null
);
if (self::verifyHMAC($hmac, $ciphertext, $akey)) {
// Regenerate the same encryption sub-key.
$ekey = Core::HKDF(
Core::LEGACY_HASH_FUNCTION_NAME,
$key,
Core::LEGACY_KEY_BYTE_SIZE,
Core::LEGACY_ENCRYPTION_INFO_STRING,
null
);
// Extract the IV from the ciphertext.
if (Core::ourStrlen($ciphertext) <= Core::LEGACY_BLOCK_BYTE_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Ciphertext is too short.'
);
}
$iv = Core::ourSubstr($ciphertext, 0, Core::LEGACY_BLOCK_BYTE_SIZE);
if ($iv === false) {
throw new Ex\EnvironmentIsBrokenException();
}
$ciphertext = Core::ourSubstr($ciphertext, Core::LEGACY_BLOCK_BYTE_SIZE);
if ($ciphertext === false) {
throw new Ex\EnvironmentIsBrokenException();
}
// Do the decryption.
$plaintext = self::plainDecrypt($ciphertext, $ekey, $iv, Core::LEGACY_CIPHER_METHOD);
return $plaintext;
} else {
throw new Ex\WrongKeyOrModifiedCiphertextException(
'Integrity check failed.'
);
}
}
/**
* Encrypts a string with either a key or a password.
*
* @param string $plaintext
* @param KeyOrPassword $secret
* @param bool $raw_binary
*
* @return string
*/
private static function encryptInternal($plaintext, KeyOrPassword $secret, $raw_binary)
{
RuntimeTests::runtimeTest();
$salt = Core::secureRandom(Core::SALT_BYTE_SIZE);
$keys = $secret->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);
}
}

@ -1,37 +0,0 @@
<?php
namespace Defuse\Crypto;
final class DerivedKeys
{
private $akey = null;
private $ekey = null;
/**
* Returns the authentication key.
*/
public function getAuthenticationKey()
{
return $this->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;
}
}

@ -1,212 +0,0 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class Encoding
{
const CHECKSUM_BYTE_SIZE = 32;
const CHECKSUM_HASH_ALGO = 'sha256';
const SERIALIZE_HEADER_BYTES = 4;
/**
* Converts a byte string to a hexadecimal string without leaking
* information through side channels.
*
* @param string $byte_string
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public static function binToHex($byte_string)
{
$hex = '';
$len = Core::ourStrlen($byte_string);
for ($i = 0; $i < $len; ++$i) {
$c = \ord($byte_string[$i]) & 0xf;
$b = \ord($byte_string[$i]) >> 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
);
}
}

@ -1,7 +0,0 @@
<?php
namespace Defuse\Crypto\Exception;
class BadFormatException extends \Defuse\Crypto\Exception\CryptoException
{
}

@ -1,7 +0,0 @@
<?php
namespace Defuse\Crypto\Exception;
class CryptoException extends \Exception
{
}

@ -1,7 +0,0 @@
<?php
namespace Defuse\Crypto\Exception;
class EnvironmentIsBrokenException extends \Defuse\Crypto\Exception\CryptoException
{
}

@ -1,7 +0,0 @@
<?php
namespace Defuse\Crypto\Exception;
class IOException extends \Defuse\Crypto\Exception\CryptoException
{
}

@ -1,7 +0,0 @@
<?php
namespace Defuse\Crypto\Exception;
class WrongKeyOrModifiedCiphertextException extends \Defuse\Crypto\Exception\CryptoException
{
}

@ -1,755 +0,0 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class File
{
/**
* Encrypts the input file, saving the ciphertext to the output file.
*
* @param string $inputFilename
* @param string $outputFilename
* @param Key $key
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
*/
public static function encryptFile($inputFilename, $outputFilename, Key $key)
{
self::encryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromKey($key)
);
}
/**
* Encrypts a file with a password, using a slow key derivation function to
* make password cracking more expensive.
*
* @param string $inputFilename
* @param string $outputFilename
* @param string $password
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
*/
public static function encryptFileWithPassword($inputFilename, $outputFilename, $password)
{
self::encryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Decrypts the input file, saving the plaintext to the output file.
*
* @param string $inputFilename
* @param string $outputFilename
* @param Key $key
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptFile($inputFilename, $outputFilename, Key $key)
{
self::decryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromKey($key)
);
}
/**
* Decrypts a file with a password, using a slow key derivation function to
* make password cracking more expensive.
*
* @param string $inputFilename
* @param string $outputFilename
* @param string $password
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptFileWithPassword($inputFilename, $outputFilename, $password)
{
self::decryptFileInternal(
$inputFilename,
$outputFilename,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Takes two resource handles and encrypts the contents of the first,
* writing the ciphertext into the second.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param Key $key
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function encryptResource($inputHandle, $outputHandle, Key $key)
{
self::encryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromKey($key)
);
}
/**
* Encrypts the contents of one resource handle into another with a
* password, using a slow key derivation function to make password cracking
* more expensive.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param string $password
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function encryptResourceWithPassword($inputHandle, $outputHandle, $password)
{
self::encryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Takes two resource handles and decrypts the contents of the first,
* writing the plaintext into the second.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param Key $key
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptResource($inputHandle, $outputHandle, Key $key)
{
self::decryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromKey($key)
);
}
/**
* Decrypts the contents of one resource into another with a password, using
* a slow key derivation function to make password cracking more expensive.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param string $password
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
* @throws Ex\WrongKeyOrModifiedCiphertextException
*/
public static function decryptResourceWithPassword($inputHandle, $outputHandle, $password)
{
self::decryptResourceInternal(
$inputHandle,
$outputHandle,
KeyOrPassword::createFromPassword($password)
);
}
/**
* Encrypts a file with either a key or a password.
*
* @param string $inputFilename
* @param string $outputFilename
* @param KeyOrPassword $secret
*
* @throws Ex\CryptoException
* @throws Ex\IOException
*/
private static function encryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret)
{
/* Open the input file. */
$if = @\fopen($inputFilename, 'rb');
if ($if === false) {
throw new Ex\IOException(
'Cannot open input file for encrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_read_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_read_buffer($if, 0);
}
/* Open the output file. */
$of = @\fopen($outputFilename, 'wb');
if ($of === false) {
\fclose($if);
throw new Ex\IOException(
'Cannot open output file for encrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_write_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_write_buffer($of, 0);
}
/* Perform the encryption. */
try {
self::encryptResourceInternal($if, $of, $secret);
} catch (Ex\CryptoException $ex) {
\fclose($if);
\fclose($of);
throw $ex;
}
/* Close the input file. */
if (\fclose($if) === false) {
\fclose($of);
throw new Ex\IOException(
'Cannot close input file after encrypting'
);
}
/* Close the output file. */
if (\fclose($of) === false) {
throw new Ex\IOException(
'Cannot close output file after encrypting'
);
}
}
/**
* Decrypts a file with either a key or a password.
*
* @param string $inputFilename
* @param string $outputFilename
* @param KeyOrPassword $secret
*
* @throws Ex\CryptoException
* @throws Ex\IOException
*/
private static function decryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret)
{
/* Open the input file. */
$if = @\fopen($inputFilename, 'rb');
if ($if === false) {
throw new Ex\IOException(
'Cannot open input file for decrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_read_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_read_buffer($if, 0);
}
/* Open the output file. */
$of = @\fopen($outputFilename, 'wb');
if ($of === false) {
\fclose($if);
throw new Ex\IOException(
'Cannot open output file for decrypting: ' .
self::getLastErrorMessage()
);
}
if (\is_callable('\\stream_set_write_buffer')) {
/* This call can fail, but the only consequence is performance. */
\stream_set_write_buffer($of, 0);
}
/* Perform the decryption. */
try {
self::decryptResourceInternal($if, $of, $secret);
} catch (Ex\CryptoException $ex) {
\fclose($if);
\fclose($of);
throw $ex;
}
/* Close the input file. */
if (\fclose($if) === false) {
\fclose($of);
throw new Ex\IOException(
'Cannot close input file after decrypting'
);
}
/* Close the output file. */
if (\fclose($of) === false) {
throw new Ex\IOException(
'Cannot close output file after decrypting'
);
}
}
/**
* Encrypts a resource with either a key or a password.
*
* @param resource $inputHandle
* @param resource $outputHandle
* @param KeyOrPassword $secret
*
* @throws Ex\EnvironmentIsBrokenException
* @throws Ex\IOException
*/
private static function encryptResourceInternal($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!'
);
}
$inputStat = \fstat($inputHandle);
$inputSize = $inputStat['size'];
$file_salt = Core::secureRandom(Core::SALT_BYTE_SIZE);
$keys = $secret->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'];
}
}
}

@ -1,84 +0,0 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class Key
{
const KEY_CURRENT_VERSION = "\xDE\xF0\x00\x00";
const KEY_BYTE_SIZE = 32;
private $key_bytes = null;
/**
* Creates new random key.
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return Key
*/
public static function createNewRandomKey()
{
return new Key(Core::secureRandom(self::KEY_BYTE_SIZE));
}
/**
* Loads a Key from its encoded form.
*
* @param string $saved_key_string
*
* @throws Ex\BadFormatException
* @throws Ex\EnvironmentIsBrokenException
*
* @return Key
*/
public static function loadFromAsciiSafeString($saved_key_string)
{
$key_bytes = Encoding::loadBytesFromChecksummedAsciiSafeString(self::KEY_CURRENT_VERSION, $saved_key_string);
return new Key($key_bytes);
}
/**
* Encodes the Key into a string of printable ASCII characters.
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return string
*/
public function saveToAsciiSafeString()
{
return Encoding::saveBytesToChecksummedAsciiSafeString(
self::KEY_CURRENT_VERSION,
$this->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;
}
}

@ -1,119 +0,0 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class KeyOrPassword
{
const PBKDF2_ITERATIONS = 100000;
const SECRET_TYPE_KEY = 1;
const SECRET_TYPE_PASSWORD = 2;
private $secret_type = null;
private $secret = null;
/**
* Initializes an instance of KeyOrPassword from a key.
*
* @param Key $key
*
* @return KeyOrPassword
*/
public static function createFromKey(Key $key)
{
return new KeyOrPassword(self::SECRET_TYPE_KEY, $key);
}
/**
* Initializes an instance of KeyOrPassword from a password.
*
* @param string $password
*
* @return KeyOrPassword
*/
public static function createFromPassword($password)
{
return new KeyOrPassword(self::SECRET_TYPE_PASSWORD, $password);
}
/**
* Derives authentication and encryption keys from the secret, using a slow
* key derivation function if the secret is a password.
*
* @param string $salt
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return DerivedKeys
*/
public function deriveKeys($salt)
{
if (Core::ourStrlen($salt) !== Core::SALT_BYTE_SIZE) {
throw new Ex\EnvironmentIsBrokenException('Bad salt.');
}
if ($this->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;
}
}

@ -1,112 +0,0 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class KeyProtectedByPassword
{
const PASSWORD_KEY_CURRENT_VERSION = "\xDE\xF1\x00\x00";
private $encrypted_key = null;
/**
* Creates a random key protected by the provided password.
*
* @param string $password
*
* @throws Ex\EnvironmentIsBrokenException
*
* @return KeyProtectedByPassword
*/
public static function createRandomPasswordProtectedKey($password)
{
$inner_key = Key::createNewRandomKey();
/* The password is hashed as a form of poor-man's domain separation
* between this use of encryptWithPassword() and other uses of
* encryptWithPassword() that the user may also be using as part of the
* same protocol. */
$encrypted_key = Crypto::encryptWithPassword(
$inner_key->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;
}
}

@ -1,242 +0,0 @@
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
/*
* We're using static class inheritance to get access to protected methods
* inside Crypto. To make it easy to know where the method we're calling can be
* found, within this file, prefix calls with `Crypto::` or `RuntimeTests::`,
* and don't use `self::`.
*/
class RuntimeTests extends Crypto
{
/**
* Runs the runtime tests.
*
* @throws Ex\EnvironmentIsBrokenException
*/
public static function runtimeTest()
{
// 0: Tests haven't been run yet.
// 1: Tests have passed.
// 2: Tests are running right now.
// 3: Tests have failed.
static $test_state = 0;
if ($test_state === 1 || $test_state === 2) {
return;
}
if ($test_state === 3) {
/* If an intermittent problem caused a test to fail previously, we
* want that to be indicated to the user with every call to this
* library. This way, if the user first does something they really
* don't care about, and just ignores all exceptions, they won't get
* screwed when they then start to use the library for something
* they do care about. */
throw new Ex\EnvironmentIsBrokenException('Tests failed previously.');
}
try {
$test_state = 2;
Core::ensureFunctionExists('openssl_get_cipher_methods');
if (\in_array(Core::CIPHER_METHOD, \openssl_get_cipher_methods()) === false) {
throw new Ex\EnvironmentIsBrokenException(
'Cipher method not supported. This is normally caused by an outdated ' .
'version of OpenSSL (and/or OpenSSL compiled for FIPS compliance). ' .
'Please upgrade to a newer version of OpenSSL that supports ' .
Core::CIPHER_METHOD . ' to use this library.'
);
}
RuntimeTests::AESTestVector();
RuntimeTests::HMACTestVector();
RuntimeTests::HKDFTestVector();
RuntimeTests::testEncryptDecrypt();
if (Core::ourStrlen(Key::createNewRandomKey()->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();
}
}
}

@ -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 );
@ -74,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', array($client_wrapper, 'ensure_tokens_still_fresh'));
}
return $client_wrapper;
@ -130,39 +127,36 @@ 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' ];
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 );
@ -179,7 +173,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 +203,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
*
@ -441,87 +420,46 @@ 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 );
$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(
'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;
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'] + $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;
}
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
*
@ -796,7 +734,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
@ -819,7 +756,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

@ -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

Loading…
Cancel
Save