<?php
declare(strict_types=1);
namespace ParagonIE\Paseto;
use ParagonIE\ConstantTime\{
Base64UrlSafe,
Binary
};
use ParagonIE\Paseto\Exception\PasetoException;
/**
* Class Util
* @package ParagonIE\Paseto
*/
abstract class Util
{
/**
* Computes the HKDF key derivation function specified in
* http://tools.ietf.org/html/rfc5869.
*
* Adapted from defuse/php-encryption
* @ref https://github.com/defuse/php-encryption/blob/aa72b8bc85311dbcc56c080823f0be12d78331c7/src/Core.php#L116-L190
*
* @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|null $salt
*
* @return string
* @psalm-suppress MixedInferredReturnType This always returns a string!
* @throws PasetoException
* @throws \TypeError
*/
public static function HKDF(
string $hash,
string $ikm,
int $length,
string $info = '',
string $salt = null
): string {
static $nativeHKDF = null;
if ($nativeHKDF === null) {
$nativeHKDF = \is_callable('\\hash_hkdf');
}
if ($nativeHKDF) {
/**
* @psalm-suppress UndefinedFunction
* This is wrapped in an is_callable() check.
*/
return (string) \hash_hkdf($hash, $ikm, $length, $info, $salt ?? '');
}
$digest_length = Binary::safeStrlen(
\hash_hmac($hash, '', '', true)
);
// Sanity-check the desired output length.
if (empty($length) || $length < 0 || $length > 255 * $digest_length) {
throw new PasetoException(
'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 (Binary::safeStrlen($prk) < $digest_length) {
throw new PasetoException(
'An unexpected condition occurred in the HKDF internals'
);
}
// T(0) = ''
$t = '';
$last_block = '';
for ($block_index = 1; Binary::safeStrlen($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
/** @var string $orm */
$orm = Binary::safeSubstr($t, 0, $length);
return (string) $orm;
}
/**
* Format the Additional Associated Data.
*
* Prefix with the length (64-bit unsigned little-endian integer)
* followed by each message. This provides a more explicit domain
* separation between each piece of the message.
*
* Each length is masked with PHP_INT_MAX using bitwise AND (&) to
* clear out the MSB of the total string length.
*
* @param string ...$pieces
* @return string
*/
public static function preAuthEncode(string ...$pieces): string
{
$accumulator = \ParagonIE_Sodium_Core_Util::store64_le(\count($pieces) & PHP_INT_MAX);
foreach ($pieces as $piece) {
$len = Binary::safeStrlen($piece);
$accumulator .= \ParagonIE_Sodium_Core_Util::store64_le($len & PHP_INT_MAX);
$accumulator .= $piece;
}
return $accumulator;
}
/**
* If a footer was included with the message, first verify that
* it's equivalent to the one we expect, then remove it from the
* token payload.
*
* @param string $payload
* @return string
* @throws \TypeError
*/
public static function extractFooter(string $payload): string
{
/** @var array<int, string> $pieces */
$pieces = \explode('.', $payload);
if (\count($pieces) > 3) {
return Base64UrlSafe::decode((string) \array_pop($pieces));
}
return '';
}
/**
* If a footer was included with the message, first verify that
* it's equivalent to the one we expect, then remove it from the
* token payload.
*
* @param string $payload
* @return string
* @throws \TypeError
*/
public static function removeFooter(string $payload): string
{
$pieces = \explode('.', $payload);
if (\count($pieces) > 3) {
return \implode('.', \array_slice($pieces, 0, 3));
}
return $payload;
}
/**
* If a footer was included with the message, first verify that
* it's equivalent to the one we expect, then remove it from the
* token payload.
*
* @param string $payload
* @param string $footer
* @return string
* @throws PasetoException
* @throws \TypeError
*/
public static function validateAndRemoveFooter(
string $payload,
string $footer = ''
): string {
if (empty($footer)) {
return $payload;
}
$footer = Base64UrlSafe::encodeUnpadded($footer);
$payload_len = Binary::safeStrlen($payload);
$footer_len = Binary::safeStrlen($footer) + 1;
$trailing = Binary::safeSubstr(
$payload,
$payload_len - $footer_len,
$footer_len
);
if (!\hash_equals('.' . $footer, $trailing)) {
throw new PasetoException('Invalid message footer');
}
return Binary::safeSubstr($payload, 0, $payload_len - $footer_len);
}
}
|