primo commit

This commit is contained in:
2024-12-17 17:34:10 +01:00
commit e650f8df99
16435 changed files with 2451012 additions and 0 deletions

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
final class A128CBCHS256 extends AESCBCHS
{
public function getCEKSize(): int
{
return 256;
}
public function name(): string
{
return 'A128CBC-HS256';
}
protected function getHashAlgorithm(): string
{
return 'sha256';
}
protected function getMode(): string
{
return 'aes-128-cbc';
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
final class A128GCM extends AESGCM
{
public function getCEKSize(): int
{
return 128;
}
public function name(): string
{
return 'A128GCM';
}
protected function getMode(): string
{
return 'aes-128-gcm';
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
final class A192CBCHS384 extends AESCBCHS
{
public function getCEKSize(): int
{
return 384;
}
public function name(): string
{
return 'A192CBC-HS384';
}
protected function getHashAlgorithm(): string
{
return 'sha384';
}
protected function getMode(): string
{
return 'aes-192-cbc';
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
final class A192GCM extends AESGCM
{
public function getCEKSize(): int
{
return 192;
}
public function name(): string
{
return 'A192GCM';
}
protected function getMode(): string
{
return 'aes-192-gcm';
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
final class A256CBCHS512 extends AESCBCHS
{
public function getCEKSize(): int
{
return 512;
}
public function name(): string
{
return 'A256CBC-HS512';
}
protected function getHashAlgorithm(): string
{
return 'sha512';
}
protected function getMode(): string
{
return 'aes-256-cbc';
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
final class A256GCM extends AESGCM
{
public function getCEKSize(): int
{
return 256;
}
public function name(): string
{
return 'A256GCM';
}
protected function getMode(): string
{
return 'aes-256-gcm';
}
}

View File

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Encryption\Algorithm\ContentEncryptionAlgorithm;
use RuntimeException;
use function extension_loaded;
use const OPENSSL_RAW_DATA;
abstract class AESCBCHS implements ContentEncryptionAlgorithm
{
public function __construct()
{
if (! extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
}
public function allowedKeyTypes(): array
{
return []; //Irrelevant
}
public function encryptContent(
string $data,
string $cek,
string $iv,
?string $aad,
string $encoded_protected_header,
?string &$tag = null
): string {
$k = mb_substr($cek, $this->getCEKSize() / 16, null, '8bit');
$result = openssl_encrypt($data, $this->getMode(), $k, OPENSSL_RAW_DATA, $iv);
if ($result === false) {
throw new RuntimeException('Unable to encrypt the content');
}
$tag = $this->calculateAuthenticationTag($result, $cek, $iv, $aad, $encoded_protected_header);
return $result;
}
public function decryptContent(
string $data,
string $cek,
string $iv,
?string $aad,
string $encoded_protected_header,
string $tag
): string {
if (! $this->isTagValid($data, $cek, $iv, $aad, $encoded_protected_header, $tag)) {
throw new RuntimeException('Unable to decrypt or to verify the tag.');
}
$k = mb_substr($cek, $this->getCEKSize() / 16, null, '8bit');
$result = openssl_decrypt($data, $this->getMode(), $k, OPENSSL_RAW_DATA, $iv);
if ($result === false) {
throw new RuntimeException('Unable to decrypt or to verify the tag.');
}
return $result;
}
public function getIVSize(): int
{
return 128;
}
protected function calculateAuthenticationTag(
string $encrypted_data,
string $cek,
string $iv,
?string $aad,
string $encoded_header
): string {
$calculated_aad = $encoded_header;
if ($aad !== null) {
$calculated_aad .= '.' . Base64UrlSafe::encodeUnpadded($aad);
}
$mac_key = mb_substr($cek, 0, $this->getCEKSize() / 16, '8bit');
$auth_data_length = mb_strlen($encoded_header, '8bit');
$secured_input = implode('', [
$calculated_aad,
$iv,
$encrypted_data,
pack('N2', ($auth_data_length / 2_147_483_647) * 8, ($auth_data_length % 2_147_483_647) * 8),
]);
$hash = hash_hmac($this->getHashAlgorithm(), $secured_input, $mac_key, true);
return mb_substr($hash, 0, mb_strlen($hash, '8bit') / 2, '8bit');
}
protected function isTagValid(
string $encrypted_data,
string $cek,
string $iv,
?string $aad,
string $encoded_header,
string $authentication_tag
): bool {
return hash_equals(
$authentication_tag,
$this->calculateAuthenticationTag($encrypted_data, $cek, $iv, $aad, $encoded_header)
);
}
abstract protected function getHashAlgorithm(): string;
abstract protected function getMode(): string;
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Encryption\Algorithm\ContentEncryptionAlgorithm;
use RuntimeException;
use function extension_loaded;
use const OPENSSL_RAW_DATA;
abstract class AESGCM implements ContentEncryptionAlgorithm
{
public function __construct()
{
if (! extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
}
public function allowedKeyTypes(): array
{
return []; //Irrelevant
}
public function encryptContent(
string $data,
string $cek,
string $iv,
?string $aad,
string $encoded_protected_header,
?string &$tag = null
): string {
$calculated_aad = $encoded_protected_header;
if ($aad !== null) {
$calculated_aad .= '.' . Base64UrlSafe::encodeUnpadded($aad);
}
$tag = '';
$result = openssl_encrypt($data, $this->getMode(), $cek, OPENSSL_RAW_DATA, $iv, $tag, $calculated_aad);
if ($result === false) {
throw new RuntimeException('Unable to encrypt the content');
}
return $result;
}
public function decryptContent(
string $data,
string $cek,
string $iv,
?string $aad,
string $encoded_protected_header,
string $tag
): string {
$calculated_aad = $encoded_protected_header;
if ($aad !== null) {
$calculated_aad .= '.' . Base64UrlSafe::encodeUnpadded($aad);
}
$result = openssl_decrypt($data, $this->getMode(), $cek, OPENSSL_RAW_DATA, $iv, $tag, $calculated_aad);
if ($result === false) {
throw new RuntimeException('Unable to decrypt the content');
}
return $result;
}
public function getIVSize(): int
{
return 96;
}
abstract protected function getMode(): string;
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm;
use Jose\Component\Core\Algorithm;
interface ContentEncryptionAlgorithm extends Algorithm
{
/**
* This method encrypts the data using the given CEK, IV, AAD and protected header. The variable $tag is populated
* on success.
*
* @param string $data The data to encrypt
* @param string $cek The content encryption key
* @param string $iv The Initialization Vector
* @param string|null $aad Additional Additional Authenticated Data
* @param string $encoded_protected_header The Protected Header encoded in Base64Url
* @param string $tag Tag
*/
public function encryptContent(
string $data,
string $cek,
string $iv,
?string $aad,
string $encoded_protected_header,
?string &$tag = null
): string;
/**
* This method tries to decrypt the data using the given CEK, IV, AAD, protected header and tag.
*
* @param string $data The data to decrypt
* @param string $cek The content encryption key
* @param string $iv The Initialization Vector
* @param string|null $aad Additional Additional Authenticated Data
* @param string $encoded_protected_header The Protected Header encoded in Base64Url
* @param string $tag Tag
*/
public function decryptContent(
string $data,
string $cek,
string $iv,
?string $aad,
string $encoded_protected_header,
string $tag
): string;
/**
* Returns the size of the IV used by this encryption method.
*/
public function getIVSize(): int;
/**
* Returns the size of the CEK used by this encryption method.
*/
public function getCEKSize(): int;
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
final class A128GCMKW extends AESGCMKW
{
public function name(): string
{
return 'A128GCMKW';
}
protected function getKeySize(): int
{
return 128;
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A128KW as Wrapper;
use AESKW\Wrapper as WrapperInterface;
final class A128KW extends AESKW
{
public function name(): string
{
return 'A128KW';
}
protected function getWrapper(): WrapperInterface
{
return new Wrapper();
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
final class A192GCMKW extends AESGCMKW
{
public function name(): string
{
return 'A192GCMKW';
}
protected function getKeySize(): int
{
return 192;
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A192KW as Wrapper;
use AESKW\Wrapper as WrapperInterface;
final class A192KW extends AESKW
{
public function name(): string
{
return 'A192KW';
}
protected function getWrapper(): WrapperInterface
{
return new Wrapper();
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
final class A256GCMKW extends AESGCMKW
{
public function name(): string
{
return 'A256GCMKW';
}
protected function getKeySize(): int
{
return 256;
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A256KW as Wrapper;
use AESKW\Wrapper as WrapperInterface;
final class A256KW extends AESKW
{
public function name(): string
{
return 'A256KW';
}
protected function getWrapper(): WrapperInterface
{
return new Wrapper();
}
}

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\Wrapper as WrapperInterface;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
use RuntimeException;
use function extension_loaded;
use function in_array;
use function is_string;
use const OPENSSL_RAW_DATA;
abstract class AESGCMKW implements KeyWrapping
{
public function __construct()
{
if (! extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
if (! interface_exists(WrapperInterface::class)) {
throw new RuntimeException('Please install "spomky-labs/aes-key-wrap" to use AES-KW algorithms');
}
}
public function allowedKeyTypes(): array
{
return ['oct'];
}
/**
* @param array<string, mixed> $completeHeader
* @param array<string, mixed> $additionalHeader
*/
public function wrapKey(JWK $key, string $cek, array $completeHeader, array &$additionalHeader): string
{
$kek = $this->getKey($key);
$iv = random_bytes(96 / 8);
$additionalHeader['iv'] = Base64UrlSafe::encodeUnpadded($iv);
$mode = sprintf('aes-%d-gcm', $this->getKeySize());
$tag = '';
$encrypted_cek = openssl_encrypt($cek, $mode, $kek, OPENSSL_RAW_DATA, $iv, $tag, '');
if ($encrypted_cek === false) {
throw new RuntimeException('Unable to encrypt the CEK');
}
$additionalHeader['tag'] = Base64UrlSafe::encodeUnpadded($tag);
return $encrypted_cek;
}
/**
* @param array<string, mixed> $completeHeader
*/
public function unwrapKey(JWK $key, string $encrypted_cek, array $completeHeader): string
{
$kek = $this->getKey($key);
(isset($completeHeader['iv']) && is_string($completeHeader['iv'])) || throw new InvalidArgumentException(
'Parameter "iv" is missing.'
);
(isset($completeHeader['tag']) && is_string($completeHeader['tag'])) || throw new InvalidArgumentException(
'Parameter "tag" is missing.'
);
$tag = Base64UrlSafe::decodeNoPadding($completeHeader['tag']);
$iv = Base64UrlSafe::decodeNoPadding($completeHeader['iv']);
$mode = sprintf('aes-%d-gcm', $this->getKeySize());
$cek = openssl_decrypt($encrypted_cek, $mode, $kek, OPENSSL_RAW_DATA, $iv, $tag, '');
if ($cek === false) {
throw new RuntimeException('Unable to decrypt the CEK');
}
return $cek;
}
public function getKeyManagementMode(): string
{
return self::MODE_WRAP;
}
protected function getKey(JWK $key): string
{
if (! in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
if (! $key->has('k')) {
throw new InvalidArgumentException('The key parameter "k" is missing.');
}
$k = $key->get('k');
if (! is_string($k)) {
throw new InvalidArgumentException('The key parameter "k" is invalid.');
}
return Base64UrlSafe::decodeNoPadding($k);
}
abstract protected function getKeySize(): int;
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\Wrapper as WrapperInterface;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
use RuntimeException;
use function in_array;
use function is_string;
abstract class AESKW implements KeyWrapping
{
public function __construct()
{
if (! interface_exists(WrapperInterface::class)) {
throw new RuntimeException('Please install "spomky-labs/aes-key-wrap" to use AES-KW algorithms');
}
}
public function allowedKeyTypes(): array
{
return ['oct'];
}
/**
* @param array<string, mixed> $completeHeader
* @param array<string, mixed> $additionalHeader
*/
public function wrapKey(JWK $key, string $cek, array $completeHeader, array &$additionalHeader): string
{
$k = $this->getKey($key);
$wrapper = $this->getWrapper();
return $wrapper::wrap($k, $cek);
}
/**
* @param array<string, mixed> $completeHeader
*/
public function unwrapKey(JWK $key, string $encrypted_cek, array $completeHeader): string
{
$k = $this->getKey($key);
$wrapper = $this->getWrapper();
return $wrapper::unwrap($k, $encrypted_cek);
}
public function getKeyManagementMode(): string
{
return self::MODE_WRAP;
}
abstract protected function getWrapper(): WrapperInterface;
private function getKey(JWK $key): string
{
if (! in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
if (! $key->has('k')) {
throw new InvalidArgumentException('The key parameter "k" is missing.');
}
$k = $key->get('k');
if (! is_string($k)) {
throw new InvalidArgumentException('The key parameter "k" is invalid.');
}
return Base64UrlSafe::decodeNoPadding($k);
}
}

View File

@ -0,0 +1,316 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Brick\Math\BigInteger;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Core\Util\Ecc\Curve;
use Jose\Component\Core\Util\Ecc\EcDH;
use Jose\Component\Core\Util\Ecc\NistCurve;
use Jose\Component\Core\Util\Ecc\PrivateKey;
use Jose\Component\Core\Util\ECKey;
use Jose\Component\Encryption\Algorithm\KeyEncryption\Util\ConcatKDF;
use RuntimeException;
use Throwable;
use function array_key_exists;
use function extension_loaded;
use function function_exists;
use function in_array;
use function is_array;
use function is_string;
abstract class AbstractECDH implements KeyAgreement
{
public function allowedKeyTypes(): array
{
return ['EC', 'OKP'];
}
/**
* @param array<string, mixed> $complete_header
* @param array<string, mixed> $additional_header_values
*/
public function getAgreementKey(
int $encryptionKeyLength,
string $algorithm,
JWK $recipientKey,
?JWK $senderKey,
array $complete_header = [],
array &$additional_header_values = []
): string {
if ($recipientKey->has('d')) {
[$public_key, $private_key] = $this->getKeysFromPrivateKeyAndHeader($recipientKey, $complete_header);
} else {
[$public_key, $private_key] = $this->getKeysFromPublicKey(
$recipientKey,
$senderKey,
$additional_header_values
);
}
$agreed_key = $this->calculateAgreementKey($private_key, $public_key);
$apu = array_key_exists('apu', $complete_header) ? $complete_header['apu'] : '';
is_string($apu) || throw new InvalidArgumentException('Invalid APU.');
$apv = array_key_exists('apv', $complete_header) ? $complete_header['apv'] : '';
is_string($apv) || throw new InvalidArgumentException('Invalid APU.');
return ConcatKDF::generate($agreed_key, $algorithm, $encryptionKeyLength, $apu, $apv);
}
public function getKeyManagementMode(): string
{
return self::MODE_AGREEMENT;
}
protected function calculateAgreementKey(JWK $private_key, JWK $public_key): string
{
$crv = $public_key->get('crv');
if (! is_string($crv)) {
throw new InvalidArgumentException('Invalid key parameter "crv"');
}
switch ($crv) {
case 'P-256':
case 'P-384':
case 'P-521':
$curve = $this->getCurve($crv);
if (function_exists('openssl_pkey_derive')) {
try {
$publicPem = ECKey::convertPublicKeyToPEM($public_key);
$privatePem = ECKey::convertPrivateKeyToPEM($private_key);
$res = openssl_pkey_derive($publicPem, $privatePem, $curve->getSize());
if ($res === false) {
throw new RuntimeException('Unable to derive the key');
}
return $res;
} catch (Throwable) {
//Does nothing. Will fallback to the pure PHP function
}
}
$x = $public_key->get('x');
if (! is_string($x)) {
throw new InvalidArgumentException('Invalid key parameter "x"');
}
$y = $public_key->get('y');
if (! is_string($y)) {
throw new InvalidArgumentException('Invalid key parameter "y"');
}
$d = $private_key->get('d');
if (! is_string($d)) {
throw new InvalidArgumentException('Invalid key parameter "d"');
}
$rec_x = $this->convertBase64ToBigInteger($x);
$rec_y = $this->convertBase64ToBigInteger($y);
$sen_d = $this->convertBase64ToBigInteger($d);
$priv_key = PrivateKey::create($sen_d);
$pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y);
return $this->convertDecToBin(EcDH::computeSharedKey($curve, $pub_key, $priv_key));
case 'X25519':
$this->checkSodiumExtensionIsAvailable();
$x = $public_key->get('x');
if (! is_string($x)) {
throw new InvalidArgumentException('Invalid key parameter "x"');
}
$d = $private_key->get('d');
if (! is_string($d)) {
throw new InvalidArgumentException('Invalid key parameter "d"');
}
$sKey = Base64UrlSafe::decodeNoPadding($d);
$recipientPublickey = Base64UrlSafe::decodeNoPadding($x);
return sodium_crypto_scalarmult($sKey, $recipientPublickey);
default:
throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv));
}
}
/**
* @param array<string, mixed> $additional_header_values
* @return JWK[]
*/
protected function getKeysFromPublicKey(
JWK $recipient_key,
?JWK $senderKey,
array &$additional_header_values
): array {
$this->checkKey($recipient_key, false);
$public_key = $recipient_key;
$crv = $public_key->get('crv');
if (! is_string($crv)) {
throw new InvalidArgumentException('Invalid key parameter "crv"');
}
$private_key = match ($crv) {
'P-256', 'P-384', 'P-521' => $senderKey ?? ECKey::createECKey($crv),
'X25519' => $senderKey ?? $this->createOKPKey('X25519'),
default => throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv)),
};
$epk = $private_key->toPublic()
->all();
$additional_header_values['epk'] = $epk;
return [$public_key, $private_key];
}
/**
* @param array<string, mixed> $complete_header
* @return JWK[]
*/
protected function getKeysFromPrivateKeyAndHeader(JWK $recipient_key, array $complete_header): array
{
$this->checkKey($recipient_key, true);
$private_key = $recipient_key;
$public_key = $this->getPublicKey($complete_header);
if ($private_key->get('crv') !== $public_key->get('crv')) {
throw new InvalidArgumentException('Curves are different');
}
return [$public_key, $private_key];
}
/**
* @param array<string, mixed> $complete_header
*/
private function getPublicKey(array $complete_header): JWK
{
if (! isset($complete_header['epk'])) {
throw new InvalidArgumentException('The header parameter "epk" is missing.');
}
if (! is_array($complete_header['epk'])) {
throw new InvalidArgumentException('The header parameter "epk" is not an array of parameters');
}
$public_key = new JWK($complete_header['epk']);
$this->checkKey($public_key, false);
return $public_key;
}
private function checkKey(JWK $key, bool $is_private): void
{
if (! in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
foreach (['x', 'crv'] as $k) {
if (! $key->has($k)) {
throw new InvalidArgumentException(sprintf('The key parameter "%s" is missing.', $k));
}
}
$crv = $key->get('crv');
if (! is_string($crv)) {
throw new InvalidArgumentException('Invalid key parameter "crv"');
}
switch ($crv) {
case 'P-256':
case 'P-384':
case 'P-521':
if (! $key->has('y')) {
throw new InvalidArgumentException('The key parameter "y" is missing.');
}
break;
case 'X25519':
break;
default:
throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv));
}
if ($is_private === true && ! $key->has('d')) {
throw new InvalidArgumentException('The key parameter "d" is missing.');
}
}
private function getCurve(string $crv): Curve
{
return match ($crv) {
'P-256' => NistCurve::curve256(),
'P-384' => NistCurve::curve384(),
'P-521' => NistCurve::curve521(),
default => throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv)),
};
}
private function convertBase64ToBigInteger(string $value): BigInteger
{
$data = unpack('H*', Base64UrlSafe::decodeNoPadding($value));
if (! is_array($data) || ! isset($data[1]) || ! is_string($data[1])) {
throw new InvalidArgumentException('Unable to convert base64 to integer');
}
return BigInteger::fromBase($data[1], 16);
}
private function convertDecToBin(BigInteger $dec): string
{
if ($dec->compareTo(BigInteger::zero()) < 0) {
throw new InvalidArgumentException('Unable to convert negative integer to string');
}
$hex = $dec->toBase(16);
if (mb_strlen($hex, '8bit') % 2 !== 0) {
$hex = '0' . $hex;
}
$bin = hex2bin($hex);
if ($bin === false) {
throw new InvalidArgumentException('Unable to convert integer to string');
}
return $bin;
}
/**
* @param string $curve The curve
*/
private function createOKPKey(string $curve): JWK
{
$this->checkSodiumExtensionIsAvailable();
switch ($curve) {
case 'X25519':
$keyPair = sodium_crypto_box_keypair();
$d = sodium_crypto_box_secretkey($keyPair);
$x = sodium_crypto_box_publickey($keyPair);
break;
case 'Ed25519':
$keyPair = sodium_crypto_sign_keypair();
$secret = sodium_crypto_sign_secretkey($keyPair);
$secretLength = mb_strlen($secret, '8bit');
$d = mb_substr($secret, 0, -$secretLength / 2, '8bit');
$x = sodium_crypto_sign_publickey($keyPair);
break;
default:
throw new InvalidArgumentException(sprintf('Unsupported "%s" curve', $curve));
}
return new JWK([
'kty' => 'OKP',
'crv' => $curve,
'x' => Base64UrlSafe::encodeUnpadded($x),
'd' => Base64UrlSafe::encodeUnpadded($d),
]);
}
private function checkSodiumExtensionIsAvailable(): void
{
if (! extension_loaded('sodium')) {
throw new RuntimeException('The extension "sodium" is not available. Please install it to use this method');
}
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\Wrapper as WrapperInterface;
use RuntimeException;
abstract class AbstractECDHAESKW implements KeyAgreementWithKeyWrapping
{
public function __construct()
{
if (! interface_exists(WrapperInterface::class)) {
throw new RuntimeException('Please install "spomky-labs/aes-key-wrap" to use AES-KW algorithms');
}
}
public function allowedKeyTypes(): array
{
return ['EC', 'OKP'];
}
public function getKeyManagementMode(): string
{
return self::MODE_WRAP;
}
abstract protected function getWrapper(): WrapperInterface;
abstract protected function getKeyLength(): int;
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
use function in_array;
use function is_string;
final class Dir implements DirectEncryption
{
public function getCEK(JWK $key): string
{
if (! in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
if (! $key->has('k')) {
throw new InvalidArgumentException('The key parameter "k" is missing.');
}
$k = $key->get('k');
if (! is_string($k)) {
throw new InvalidArgumentException('The key parameter "k" is invalid.');
}
return Base64UrlSafe::decodeNoPadding($k);
}
public function name(): string
{
return 'dir';
}
public function allowedKeyTypes(): array
{
return ['oct'];
}
public function getKeyManagementMode(): string
{
return self::MODE_DIRECT;
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
interface DirectEncryption extends KeyEncryptionAlgorithm
{
/**
* Returns the CEK.
*
* @param JWK $key The key used to get the CEK
*/
public function getCEK(JWK $key): string;
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
final class ECDHES extends AbstractECDH
{
public function name(): string
{
return 'ECDH-ES';
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A128KW as Wrapper;
final class ECDHESA128KW extends ECDHESAESKW
{
public function name(): string
{
return 'ECDH-ES+A128KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getKeyLength(): int
{
return 128;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A192KW as Wrapper;
final class ECDHESA192KW extends ECDHESAESKW
{
public function name(): string
{
return 'ECDH-ES+A192KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getKeyLength(): int
{
return 192;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A256KW as Wrapper;
final class ECDHESA256KW extends ECDHESAESKW
{
public function name(): string
{
return 'ECDH-ES+A256KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getKeyLength(): int
{
return 256;
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Core\JWK;
abstract class ECDHESAESKW extends AbstractECDHAESKW
{
/**
* @param array<string, mixed> $complete_header
* @param array<string, mixed> $additional_header_values
*/
public function wrapAgreementKey(
JWK $recipientKey,
?JWK $senderKey,
string $cek,
int $encryption_key_length,
array $complete_header,
array &$additional_header_values
): string {
$ecdh_es = new ECDHES();
$agreement_key = $ecdh_es->getAgreementKey(
$this->getKeyLength(),
$this->name(),
$recipientKey->toPublic(),
$senderKey,
$complete_header,
$additional_header_values
);
$wrapper = $this->getWrapper();
return $wrapper::wrap($agreement_key, $cek);
}
/**
* @param array<string, mixed> $complete_header
*/
public function unwrapAgreementKey(
JWK $recipientKey,
?JWK $senderKey,
string $encrypted_cek,
int $encryption_key_length,
array $complete_header
): string {
$ecdh_es = new ECDHES();
$agreement_key = $ecdh_es->getAgreementKey(
$this->getKeyLength(),
$this->name(),
$recipientKey,
$senderKey,
$complete_header
);
$wrapper = $this->getWrapper();
return $wrapper::unwrap($agreement_key, $encrypted_cek);
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Core\JWK;
use LogicException;
final class ECDHSS extends AbstractECDH
{
public function name(): string
{
return 'ECDH-SS';
}
/**
* @param array<string, mixed> $complete_header
* @param array<string, mixed> $additional_header_values
*/
public function getAgreementKey(
int $encryptionKeyLength,
string $algorithm,
JWK $recipientKey,
?JWK $senderKey,
array $complete_header = [],
array &$additional_header_values = []
): string {
if ($senderKey === null) {
throw new LogicException('The sender key shall be set');
}
$agreedKey = parent::getAgreementKey(
$encryptionKeyLength,
$algorithm,
$recipientKey,
$senderKey,
$complete_header,
$additional_header_values
);
unset($additional_header_values['epk']);
return $agreedKey;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A128KW as Wrapper;
final class ECDHSSA128KW extends ECDHSSAESKW
{
public function name(): string
{
return 'ECDH-SS+A128KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getKeyLength(): int
{
return 128;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A192KW as Wrapper;
final class ECDHSSA192KW extends ECDHSSAESKW
{
public function name(): string
{
return 'ECDH-SS+A192KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getKeyLength(): int
{
return 192;
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A256KW as Wrapper;
final class ECDHSSA256KW extends ECDHSSAESKW
{
/**
* NOTE: the return name was modified
*/
public function name(): string
{
return 'ECDH-SS+A256KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getKeyLength(): int
{
return 256;
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Core\JWK;
abstract class ECDHSSAESKW extends AbstractECDHAESKW
{
/**
* @param array<string, mixed> $complete_header
* @param array<string, mixed> $additional_header_values
*/
public function wrapAgreementKey(
JWK $recipientKey,
?JWK $senderKey,
string $cek,
int $encryption_key_length,
array $complete_header,
array &$additional_header_values
): string {
$ecdh_ss = new ECDHSS();
$agreement_key = $ecdh_ss->getAgreementKey(
$this->getKeyLength(),
$this->name(),
$recipientKey->toPublic(),
$senderKey,
$complete_header,
$additional_header_values
);
$wrapper = $this->getWrapper();
return $wrapper::wrap($agreement_key, $cek);
}
/**
* @param array<string, mixed> $complete_header
*/
public function unwrapAgreementKey(
JWK $recipientKey,
?JWK $senderKey,
string $encrypted_cek,
int $encryption_key_length,
array $complete_header
): string {
$ecdh_ss = new ECDHSS();
$agreement_key = $ecdh_ss->getAgreementKey(
$this->getKeyLength(),
$this->name(),
$recipientKey,
$senderKey,
$complete_header
);
$wrapper = $this->getWrapper();
return $wrapper::unwrap($agreement_key, $encrypted_cek);
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
interface KeyAgreement extends KeyEncryptionAlgorithm
{
/**
* Computes the agreement key.
*
* @param array<string, mixed> $completeHeader
* @param array<string, mixed> $additionalHeaderValues
*/
public function getAgreementKey(
int $encryptionKeyLength,
string $algorithm,
JWK $recipientKey,
?JWK $senderKey,
array $completeHeader = [],
array &$additionalHeaderValues = []
): string;
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
interface KeyAgreementWithKeyWrapping extends KeyEncryptionAlgorithm
{
/**
* Compute and wrap the agreement key.
*
* @param JWK $recipientKey The receiver's key
* @param string $cek The CEK to wrap
* @param int $encryption_key_length Size of the key expected for the algorithm used for data encryption
* @param array<string, mixed> $complete_header The complete header of the JWT
* @param array<string, mixed> $additional_header_values Set additional header values if needed
*/
public function wrapAgreementKey(
JWK $recipientKey,
?JWK $senderKey,
string $cek,
int $encryption_key_length,
array $complete_header,
array &$additional_header_values
): string;
/**
* Unwrap and compute the agreement key.
*
* @param JWK $recipientKey The receiver's key
* @param string $encrypted_cek The encrypted CEK
* @param int $encryption_key_length Size of the key expected for the algorithm used for data encryption
* @param array<string, mixed> $complete_header The complete header of the JWT
*
* @return string The decrypted CEK
*/
public function unwrapAgreementKey(
JWK $recipientKey,
?JWK $senderKey,
string $encrypted_cek,
int $encryption_key_length,
array $complete_header
): string;
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
interface KeyEncryption extends KeyEncryptionAlgorithm
{
/**
* Encrypt the CEK.
*
* @param JWK $key The key used to wrap the CEK
* @param string $cek The CEK to encrypt
* @param array<string, mixed> $completeHeader The complete header of the JWT
* @param array<string, mixed> $additionalHeader Additional header
*/
public function encryptKey(JWK $key, string $cek, array $completeHeader, array &$additionalHeader): string;
/**
* Decrypt de CEK.
*
* @param JWK $key The key used to wrap the CEK
* @param string $encrypted_cek The CEK to decrypt
* @param array<string, mixed> $header The complete header of the JWT
*/
public function decryptKey(JWK $key, string $encrypted_cek, array $header): string;
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
interface KeyWrapping extends KeyEncryptionAlgorithm
{
/**
* Encrypt the CEK.
*
* @param JWK $key The key used to wrap the CEK
* @param string $cek The CEK to encrypt
* @param array<string, mixed> $completeHeader The complete header of the JWT
* @param array<string, mixed> $additionalHeader The complete header of the JWT
*/
public function wrapKey(JWK $key, string $cek, array $completeHeader, array &$additionalHeader): string;
/**
* Decrypt de CEK.
*
* @param JWK $key The key used to wrap the CEK
* @param string $encrypted_cek The CEK to decrypt
* @param array<string, mixed> $completeHeader The complete header of the JWT
*/
public function unwrapKey(JWK $key, string $encrypted_cek, array $completeHeader): string;
}

View File

@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A128KW;
use AESKW\A192KW;
use AESKW\A256KW;
use AESKW\Wrapper as WrapperInterface;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
use RuntimeException;
use function in_array;
use function is_int;
use function is_string;
abstract class PBES2AESKW implements KeyWrapping
{
public function __construct(
private readonly int $salt_size = 64,
private readonly int $nb_count = 4096
) {
if (! interface_exists(WrapperInterface::class)) {
throw new RuntimeException('Please install "spomky-labs/aes-key-wrap" to use AES-KW algorithms');
}
}
public function allowedKeyTypes(): array
{
return ['oct'];
}
/**
* @param array<string, mixed> $completeHeader
* @param array<string, mixed> $additionalHeader
*/
public function wrapKey(JWK $key, string $cek, array $completeHeader, array &$additionalHeader): string
{
$password = $this->getKey($key);
$this->checkHeaderAlgorithm($completeHeader);
$wrapper = $this->getWrapper();
$hash_algorithm = $this->getHashAlgorithm();
$key_size = $this->getKeySize();
$salt = random_bytes($this->salt_size);
// We set header parameters
$additionalHeader['p2s'] = Base64UrlSafe::encodeUnpadded($salt);
$additionalHeader['p2c'] = $this->nb_count;
$derived_key = hash_pbkdf2(
$hash_algorithm,
$password,
$completeHeader['alg'] . "\x00" . $salt,
$this->nb_count,
$key_size,
true
);
return $wrapper::wrap($derived_key, $cek);
}
/**
* @param array<string, mixed> $completeHeader
*/
public function unwrapKey(JWK $key, string $encrypted_cek, array $completeHeader): string
{
$password = $this->getKey($key);
$this->checkHeaderAlgorithm($completeHeader);
$this->checkHeaderAdditionalParameters($completeHeader);
$wrapper = $this->getWrapper();
$hash_algorithm = $this->getHashAlgorithm();
$key_size = $this->getKeySize();
$p2s = $completeHeader['p2s'];
is_string($p2s) || throw new InvalidArgumentException('Invalid salt.');
$salt = $completeHeader['alg'] . "\x00" . Base64UrlSafe::decodeNoPadding($p2s);
$count = $completeHeader['p2c'];
is_int($count) || throw new InvalidArgumentException('Invalid counter.');
$derived_key = hash_pbkdf2($hash_algorithm, $password, $salt, $count, $key_size, true);
return $wrapper::unwrap($derived_key, $encrypted_cek);
}
public function getKeyManagementMode(): string
{
return self::MODE_WRAP;
}
protected function getKey(JWK $key): string
{
if (! in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
if (! $key->has('k')) {
throw new InvalidArgumentException('The key parameter "k" is missing.');
}
$k = $key->get('k');
if (! is_string($k)) {
throw new InvalidArgumentException('The key parameter "k" is invalid.');
}
return Base64UrlSafe::decodeNoPadding($k);
}
/**
* @param array<string, mixed> $header
*/
protected function checkHeaderAlgorithm(array $header): void
{
if (! isset($header['alg'])) {
throw new InvalidArgumentException('The header parameter "alg" is missing.');
}
if (! is_string($header['alg'])) {
throw new InvalidArgumentException('The header parameter "alg" is not valid.');
}
}
/**
* @param array<string, mixed> $header
*/
protected function checkHeaderAdditionalParameters(array $header): void
{
if (! isset($header['p2s'])) {
throw new InvalidArgumentException('The header parameter "p2s" is missing.');
}
if (! is_string($header['p2s'])) {
throw new InvalidArgumentException('The header parameter "p2s" is not valid.');
}
if (! isset($header['p2c'])) {
throw new InvalidArgumentException('The header parameter "p2c" is missing.');
}
if (! is_int($header['p2c']) || $header['p2c'] <= 0) {
throw new InvalidArgumentException('The header parameter "p2c" is not valid.');
}
}
abstract protected function getWrapper(): A256KW|A128KW|A192KW;
abstract protected function getHashAlgorithm(): string;
abstract protected function getKeySize(): int;
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A128KW as Wrapper;
final class PBES2HS256A128KW extends PBES2AESKW
{
public function name(): string
{
return 'PBES2-HS256+A128KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getHashAlgorithm(): string
{
return 'sha256';
}
protected function getKeySize(): int
{
return 16;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A192KW as Wrapper;
final class PBES2HS384A192KW extends PBES2AESKW
{
public function name(): string
{
return 'PBES2-HS384+A192KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getHashAlgorithm(): string
{
return 'sha384';
}
protected function getKeySize(): int
{
return 24;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A256KW as Wrapper;
final class PBES2HS512A256KW extends PBES2AESKW
{
public function name(): string
{
return 'PBES2-HS512+A256KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getHashAlgorithm(): string
{
return 'sha512';
}
protected function getKeySize(): int
{
return 32;
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\RSAKey;
use Jose\Component\Encryption\Algorithm\KeyEncryption\Util\RSACrypt;
use function in_array;
abstract class RSA implements KeyEncryption
{
public function allowedKeyTypes(): array
{
return ['RSA'];
}
/**
* @param array<string, mixed> $completeHeader
* @param array<string, mixed> $additionalHeader
*/
public function encryptKey(JWK $key, string $cek, array $completeHeader, array &$additionalHeader): string
{
$this->checkKey($key);
$pub = RSAKey::toPublic(RSAKey::createFromJWK($key));
return RSACrypt::encrypt($pub, $cek, $this->getEncryptionMode(), $this->getHashAlgorithm());
}
/**
* @param array<string, mixed> $header
*/
public function decryptKey(JWK $key, string $encrypted_cek, array $header): string
{
$this->checkKey($key);
if (! $key->has('d')) {
throw new InvalidArgumentException('The key is not a private key');
}
$priv = RSAKey::createFromJWK($key);
return RSACrypt::decrypt($priv, $encrypted_cek, $this->getEncryptionMode(), $this->getHashAlgorithm());
}
public function getKeyManagementMode(): string
{
return self::MODE_ENCRYPT;
}
protected function checkKey(JWK $key): void
{
if (! in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
}
abstract protected function getEncryptionMode(): int;
abstract protected function getHashAlgorithm(): ?string;
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Encryption\Algorithm\KeyEncryption\Util\RSACrypt;
final class RSA15 extends RSA
{
public function name(): string
{
return 'RSA1_5';
}
protected function getEncryptionMode(): int
{
return RSACrypt::ENCRYPTION_PKCS1;
}
protected function getHashAlgorithm(): ?string
{
return null;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Encryption\Algorithm\KeyEncryption\Util\RSACrypt;
final class RSAOAEP extends RSA
{
public function name(): string
{
return 'RSA-OAEP';
}
protected function getEncryptionMode(): int
{
return RSACrypt::ENCRYPTION_OAEP;
}
protected function getHashAlgorithm(): string
{
return 'sha1';
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Encryption\Algorithm\KeyEncryption\Util\RSACrypt;
final class RSAOAEP256 extends RSA
{
public function getEncryptionMode(): int
{
return RSACrypt::ENCRYPTION_OAEP;
}
public function getHashAlgorithm(): string
{
return 'sha256';
}
public function name(): string
{
return 'RSA-OAEP-256';
}
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption\Util;
use InvalidArgumentException;
use Jose\Component\Core\Util\Base64UrlSafe;
use const STR_PAD_LEFT;
/**
* @internal
*
* @see https://tools.ietf.org/html/rfc7518#section-4.6.2
*/
final class ConcatKDF
{
/**
* Key Derivation Function.
*
* @param string $Z Shared secret
* @param string $algorithm Encryption algorithm
* @param int $encryption_key_size Size of the encryption key
* @param string $apu Agreement PartyUInfo (information about the producer)
* @param string $apv Agreement PartyVInfo (information about the recipient)
*/
public static function generate(
string $Z,
string $algorithm,
int $encryption_key_size,
string $apu = '',
string $apv = ''
): string {
$apu = ! self::isEmpty($apu) ? Base64UrlSafe::decodeNoPadding($apu) : '';
$apv = ! self::isEmpty($apv) ? Base64UrlSafe::decodeNoPadding($apv) : '';
$encryption_segments = [
self::toInt32Bits(1), // Round number 1
$Z, // Z (shared secret)
self::toInt32Bits(mb_strlen($algorithm, '8bit')) . $algorithm, // Size of algorithm's name and algorithm
self::toInt32Bits(mb_strlen($apu, '8bit')) . $apu, // PartyUInfo
self::toInt32Bits(mb_strlen($apv, '8bit')) . $apv, // PartyVInfo
self::toInt32Bits($encryption_key_size), // SuppPubInfo (the encryption key size)
'', // SuppPrivInfo
];
$input = implode('', $encryption_segments);
$hash = hash('sha256', $input, true);
return mb_substr($hash, 0, $encryption_key_size / 8, '8bit');
}
/**
* Convert an integer into a 32 bits string.
*
* @param int $value Integer to convert
*/
private static function toInt32Bits(int $value): string
{
$result = hex2bin(str_pad(dechex($value), 8, '0', STR_PAD_LEFT));
if ($result === false) {
throw new InvalidArgumentException('Invalid result');
}
return $result;
}
private static function isEmpty(?string $value): bool
{
return $value === null || $value === '';
}
}

View File

@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption\Util;
use InvalidArgumentException;
use Jose\Component\Core\Util\BigInteger;
use Jose\Component\Core\Util\Hash;
use Jose\Component\Core\Util\RSAKey;
use LogicException;
use RuntimeException;
use function chr;
use function count;
use function ord;
use const STR_PAD_LEFT;
/**
* @internal
*/
final class RSACrypt
{
/**
* Optimal Asymmetric Encryption Padding (OAEP).
*/
public const ENCRYPTION_OAEP = 1;
/**
* Use PKCS#1 padding.
*/
public const ENCRYPTION_PKCS1 = 2;
public static function encrypt(RSAKey $key, string $data, int $mode, ?string $hash = null): string
{
switch ($mode) {
case self::ENCRYPTION_OAEP:
if ($hash === null) {
throw new LogicException('Hash shall be defined for RSA OAEP cyphering');
}
return self::encryptWithRSAOAEP($key, $data, $hash);
case self::ENCRYPTION_PKCS1:
return self::encryptWithRSA15($key, $data);
default:
throw new InvalidArgumentException('Unsupported mode.');
}
}
public static function decrypt(RSAKey $key, string $plaintext, int $mode, ?string $hash = null): string
{
switch ($mode) {
case self::ENCRYPTION_OAEP:
if ($hash === null) {
throw new LogicException('Hash shall be defined for RSA OAEP cyphering');
}
return self::decryptWithRSAOAEP($key, $plaintext, $hash);
case self::ENCRYPTION_PKCS1:
return self::decryptWithRSA15($key, $plaintext);
default:
throw new InvalidArgumentException('Unsupported mode.');
}
}
public static function encryptWithRSA15(RSAKey $key, string $data): string
{
$mLen = mb_strlen($data, '8bit');
if ($mLen > $key->getModulusLength() - 11) {
throw new InvalidArgumentException('Message too long');
}
$psLen = $key->getModulusLength() - $mLen - 3;
$ps = '';
while (mb_strlen($ps, '8bit') !== $psLen) {
$temp = random_bytes($psLen - mb_strlen($ps, '8bit'));
$temp = str_replace("\x00", '', $temp);
$ps .= $temp;
}
$type = 2;
$data = chr(0) . chr($type) . $ps . chr(0) . $data;
$binaryData = BigInteger::createFromBinaryString($data);
$c = self::getRSAEP($key, $binaryData);
return self::convertIntegerToOctetString($c, $key->getModulusLength());
}
public static function decryptWithRSA15(RSAKey $key, string $c): string
{
if (mb_strlen($c, '8bit') !== $key->getModulusLength()) {
throw new InvalidArgumentException('Unable to decrypt');
}
$c = BigInteger::createFromBinaryString($c);
$m = self::getRSADP($key, $c);
$em = self::convertIntegerToOctetString($m, $key->getModulusLength());
if (ord($em[0]) !== 0 || ord($em[1]) > 2) {
throw new InvalidArgumentException('Unable to decrypt');
}
$ps = mb_substr($em, 2, (int) mb_strpos($em, chr(0), 2, '8bit') - 2, '8bit');
$m = mb_substr($em, mb_strlen($ps, '8bit') + 3, null, '8bit');
if (mb_strlen($ps, '8bit') < 8) {
throw new InvalidArgumentException('Unable to decrypt');
}
return $m;
}
/**
* Encryption.
*/
public static function encryptWithRSAOAEP(RSAKey $key, string $plaintext, string $hash_algorithm): string
{
/** @var Hash $hash */
$hash = Hash::$hash_algorithm();
$length = $key->getModulusLength() - 2 * $hash->getLength() - 2;
if ($length <= 0) {
throw new RuntimeException();
}
$splitPlaintext = mb_str_split($plaintext, $length, '8bit');
$ciphertext = '';
foreach ($splitPlaintext as $m) {
$ciphertext .= self::encryptRSAESOAEP($key, $m, $hash);
}
return $ciphertext;
}
/**
* Decryption.
*/
public static function decryptWithRSAOAEP(RSAKey $key, string $ciphertext, string $hash_algorithm): string
{
if ($key->getModulusLength() <= 0) {
throw new RuntimeException('Invalid modulus length');
}
$hash = Hash::$hash_algorithm();
$splitCiphertext = mb_str_split($ciphertext, $key->getModulusLength(), '8bit');
$splitCiphertext[count($splitCiphertext) - 1] = str_pad(
$splitCiphertext[count($splitCiphertext) - 1],
$key->getModulusLength(),
chr(0),
STR_PAD_LEFT
);
$plaintext = '';
foreach ($splitCiphertext as $c) {
$temp = self::getRSAESOAEP($key, $c, $hash);
$plaintext .= $temp;
}
return $plaintext;
}
private static function convertIntegerToOctetString(BigInteger $x, int $xLen): string
{
$x = $x->toBytes();
if (mb_strlen($x, '8bit') > $xLen) {
throw new RuntimeException('Invalid length.');
}
return str_pad($x, $xLen, chr(0), STR_PAD_LEFT);
}
/**
* Octet-String-to-Integer primitive.
*/
private static function convertOctetStringToInteger(string $x): BigInteger
{
return BigInteger::createFromBinaryString($x);
}
/**
* RSA EP.
*/
private static function getRSAEP(RSAKey $key, BigInteger $m): BigInteger
{
if ($m->compare(BigInteger::createFromDecimal(0)) < 0 || $m->compare($key->getModulus()) > 0) {
throw new RuntimeException();
}
return RSAKey::exponentiate($key, $m);
}
/**
* RSA DP.
*/
private static function getRSADP(RSAKey $key, BigInteger $c): BigInteger
{
if ($c->compare(BigInteger::createFromDecimal(0)) < 0 || $c->compare($key->getModulus()) > 0) {
throw new RuntimeException();
}
return RSAKey::exponentiate($key, $c);
}
/**
* MGF1.
*/
private static function getMGF1(string $mgfSeed, int $maskLen, Hash $mgfHash): string
{
$t = '';
$count = ceil($maskLen / $mgfHash->getLength());
for ($i = 0; $i < $count; ++$i) {
$c = pack('N', $i);
$t .= $mgfHash->hash($mgfSeed . $c);
}
return mb_substr($t, 0, $maskLen, '8bit');
}
/**
* RSAES-OAEP-ENCRYPT.
*/
private static function encryptRSAESOAEP(RSAKey $key, string $m, Hash $hash): string
{
$mLen = mb_strlen($m, '8bit');
$lHash = $hash->hash('');
$ps = str_repeat(chr(0), $key->getModulusLength() - $mLen - 2 * $hash->getLength() - 2);
$db = $lHash . $ps . chr(1) . $m;
$seed = random_bytes($hash->getLength());
$dbMask = self::getMGF1($seed, $key->getModulusLength() - $hash->getLength() - 1, $hash/*MGF*/);
$maskedDB = $db ^ $dbMask;
$seedMask = self::getMGF1($maskedDB, $hash->getLength(), $hash/*MGF*/);
$maskedSeed = $seed ^ $seedMask;
$em = chr(0) . $maskedSeed . $maskedDB;
$m = self::convertOctetStringToInteger($em);
$c = self::getRSAEP($key, $m);
return self::convertIntegerToOctetString($c, $key->getModulusLength());
}
/**
* RSAES-OAEP-DECRYPT.
*/
private static function getRSAESOAEP(RSAKey $key, string $c, Hash $hash): string
{
$c = self::convertOctetStringToInteger($c);
$m = self::getRSADP($key, $c);
$em = self::convertIntegerToOctetString($m, $key->getModulusLength());
$lHash = $hash->hash('');
$maskedSeed = mb_substr($em, 1, $hash->getLength(), '8bit');
$maskedDB = mb_substr($em, $hash->getLength() + 1, null, '8bit');
$seedMask = self::getMGF1($maskedDB, $hash->getLength(), $hash/*MGF*/);
$seed = $maskedSeed ^ $seedMask;
$dbMask = self::getMGF1($seed, $key->getModulusLength() - $hash->getLength() - 1, $hash/*MGF*/);
$db = $maskedDB ^ $dbMask;
$lHash2 = mb_substr($db, 0, $hash->getLength(), '8bit');
$m = mb_substr($db, $hash->getLength(), null, '8bit');
if (! hash_equals($lHash, $lHash2)) {
throw new RuntimeException();
}
$m = ltrim($m, chr(0));
if (ord($m[0]) !== 1) {
throw new RuntimeException();
}
return mb_substr($m, 1, null, '8bit');
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm;
use Jose\Component\Core\Algorithm;
interface KeyEncryptionAlgorithm extends Algorithm
{
public const MODE_DIRECT = 'dir';
public const MODE_ENCRYPT = 'enc';
public const MODE_WRAP = 'wrap';
public const MODE_AGREEMENT = 'agree';
/**
* Returns the key management mode used by the key encryption algorithm.
*/
public function getKeyManagementMode(): string;
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Compression;
/**
* @deprecated This class is deprecated and will be removed in v4.0. Compression is not recommended for JWE.
*/
interface CompressionMethod
{
/**
* Returns the name of the method.
*/
public function name(): string;
/**
* Compress the data. Throws an exception in case of failure.
*
* @param string $data The data to compress
*/
public function compress(string $data): string;
/**
* Uncompress the data. Throws an exception in case of failure.
*
* @param string $data The data to uncompress
*/
public function uncompress(string $data): string;
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Compression;
use InvalidArgumentException;
use function array_key_exists;
/**
* @deprecated This class is deprecated and will be removed in v4.0. Compression is not recommended for JWE.
*/
class CompressionMethodManager
{
/**
* @var CompressionMethod[]
*/
private array $compressionMethods = [];
/**
* @param CompressionMethod[] $methods
*/
public function __construct(iterable $methods = [])
{
foreach ($methods as $method) {
$this->add($method);
}
}
/**
* Returns true if the givn compression method is supported.
*/
public function has(string $name): bool
{
return array_key_exists($name, $this->compressionMethods);
}
/**
* This method returns the compression method with the given name. Throws an exception if the method is not
* supported.
*
* @param string $name The name of the compression method
*/
public function get(string $name): CompressionMethod
{
if (! $this->has($name)) {
throw new InvalidArgumentException(sprintf('The compression method "%s" is not supported.', $name));
}
return $this->compressionMethods[$name];
}
/**
* Returns the list of compression method names supported by the manager.
*
* @return string[]
*/
public function list(): array
{
return array_keys($this->compressionMethods);
}
/**
* Add the given compression method to the manager.
*/
protected function add(CompressionMethod $compressionMethod): void
{
$name = $compressionMethod->name();
$this->compressionMethods[$name] = $compressionMethod;
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Compression;
use InvalidArgumentException;
/**
* @deprecated This class is deprecated and will be removed in v4.0. Compression is not recommended for JWE.
*/
class CompressionMethodManagerFactory
{
/**
* @var CompressionMethod[]
*/
private array $compressionMethods = [];
/**
* This method adds a compression method to this factory. The method is uniquely identified by an alias. This allows
* the same method to be added twice (or more) using several configuration options.
*/
public function add(string $alias, CompressionMethod $compressionMethod): void
{
$this->compressionMethods[$alias] = $compressionMethod;
}
/**
* Returns the list of compression method aliases supported by the factory.
*
* @return string[]
*/
public function aliases(): array
{
return array_keys($this->compressionMethods);
}
/**
* Returns all compression methods supported by this factory.
*
* @return CompressionMethod[]
*/
public function all(): array
{
return $this->compressionMethods;
}
/**
* Creates a compression method manager using the compression methods identified by the given aliases. If one of the
* aliases does not exist, an exception is thrown.
*
* @param string[] $aliases
*/
public function create(array $aliases): CompressionMethodManager
{
$compressionMethods = [];
foreach ($aliases as $alias) {
if (! isset($this->compressionMethods[$alias])) {
throw new InvalidArgumentException(sprintf(
'The compression method with the alias "%s" is not supported.',
$alias
));
}
$compressionMethods[] = $this->compressionMethods[$alias];
}
return new CompressionMethodManager($compressionMethods);
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Compression;
use InvalidArgumentException;
use Throwable;
use function is_string;
/**
* @deprecated This class is deprecated and will be removed in v4.0. Compression is not recommended for JWE.
*/
final class Deflate implements CompressionMethod
{
private int $compressionLevel = -1;
public function __construct(int $compressionLevel = -1)
{
if ($compressionLevel < -1 || $compressionLevel > 9) {
throw new InvalidArgumentException(
'The compression level can be given as 0 for no compression up to 9 for maximum compression. If -1 given, the default compression level will be the default compression level of the zlib library.'
);
}
$this->compressionLevel = $compressionLevel;
}
public function name(): string
{
return 'DEF';
}
public function compress(string $data): string
{
try {
$bin = gzdeflate($data, $this->getCompressionLevel());
if (! is_string($bin)) {
throw new InvalidArgumentException('Unable to encode the data');
}
return $bin;
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Unable to compress data.', $throwable->getCode(), $throwable);
}
}
public function uncompress(string $data): string
{
try {
$bin = gzinflate($data);
if (! is_string($bin)) {
throw new InvalidArgumentException('Unable to encode the data');
}
return $bin;
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Unable to uncompress data.', $throwable->getCode(), $throwable);
}
}
private function getCompressionLevel(): int
{
return $this->compressionLevel;
}
}

View File

@ -0,0 +1,218 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption;
use InvalidArgumentException;
use Jose\Component\Core\JWT;
use function array_key_exists;
use function count;
class JWE implements JWT
{
private ?string $payload = null;
public function __construct(
private readonly ?string $ciphertext,
private readonly string $iv,
private readonly string $tag,
private readonly ?string $aad = null,
private readonly array $sharedHeader = [],
private readonly array $sharedProtectedHeader = [],
private readonly ?string $encodedSharedProtectedHeader = null,
private readonly array $recipients = []
) {
}
public function getPayload(): ?string
{
return $this->payload;
}
/**
* Set the payload. This method is immutable and a new object will be returned.
*/
public function withPayload(string $payload): self
{
$clone = clone $this;
$clone->payload = $payload;
return $clone;
}
/**
* Returns the number of recipients associated with the JWS.
*/
public function countRecipients(): int
{
return count($this->recipients);
}
/**
* Returns true is the JWE has already been encrypted.
*/
public function isEncrypted(): bool
{
return $this->getCiphertext() !== null;
}
/**
* Returns the recipients associated with the JWS.
*
* @return Recipient[]
*/
public function getRecipients(): array
{
return $this->recipients;
}
/**
* Returns the recipient object at the given index.
*/
public function getRecipient(int $id): Recipient
{
if (! isset($this->recipients[$id])) {
throw new InvalidArgumentException('The recipient does not exist.');
}
return $this->recipients[$id];
}
/**
* Returns the ciphertext. This method will return null is the JWE has not yet been encrypted.
*
* @return string|null The ciphertext
*/
public function getCiphertext(): ?string
{
return $this->ciphertext;
}
/**
* Returns the Additional Authentication Data if available.
*/
public function getAAD(): ?string
{
return $this->aad;
}
/**
* Returns the Initialization Vector if available.
*/
public function getIV(): ?string
{
return $this->iv;
}
/**
* Returns the tag if available.
*/
public function getTag(): ?string
{
return $this->tag;
}
/**
* Returns the encoded shared protected header.
*/
public function getEncodedSharedProtectedHeader(): string
{
return $this->encodedSharedProtectedHeader ?? '';
}
/**
* Returns the shared protected header.
*/
public function getSharedProtectedHeader(): array
{
return $this->sharedProtectedHeader;
}
/**
* Returns the shared protected header parameter identified by the given key. Throws an exception is the the
* parameter is not available.
*
* @param string $key The key
*
* @return mixed|null
*/
public function getSharedProtectedHeaderParameter(string $key)
{
if (! $this->hasSharedProtectedHeaderParameter($key)) {
throw new InvalidArgumentException(sprintf('The shared protected header "%s" does not exist.', $key));
}
return $this->sharedProtectedHeader[$key];
}
/**
* Returns true if the shared protected header has the parameter identified by the given key.
*
* @param string $key The key
*/
public function hasSharedProtectedHeaderParameter(string $key): bool
{
return array_key_exists($key, $this->sharedProtectedHeader);
}
/**
* Returns the shared header.
*/
public function getSharedHeader(): array
{
return $this->sharedHeader;
}
/**
* Returns the shared header parameter identified by the given key. Throws an exception is the the parameter is not
* available.
*
* @param string $key The key
*
* @return mixed|null
*/
public function getSharedHeaderParameter(string $key)
{
if (! $this->hasSharedHeaderParameter($key)) {
throw new InvalidArgumentException(sprintf('The shared header "%s" does not exist.', $key));
}
return $this->sharedHeader[$key];
}
/**
* Returns true if the shared header has the parameter identified by the given key.
*
* @param string $key The key
*/
public function hasSharedHeaderParameter(string $key): bool
{
return array_key_exists($key, $this->sharedHeader);
}
/**
* This method splits the JWE into a list of JWEs. It is only useful when the JWE contains more than one recipient
* (JSON General Serialization).
*
* @return JWE[]
*/
public function split(): array
{
$result = [];
foreach ($this->recipients as $recipient) {
$result[] = new self(
$this->ciphertext,
$this->iv,
$this->tag,
$this->aad,
$this->sharedHeader,
$this->sharedProtectedHeader,
$this->encodedSharedProtectedHeader,
[$recipient]
);
}
return $result;
}
}

View File

@ -0,0 +1,618 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption;
use InvalidArgumentException;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Core\Util\KeyChecker;
use Jose\Component\Encryption\Algorithm\ContentEncryptionAlgorithm;
use Jose\Component\Encryption\Algorithm\KeyEncryption\DirectEncryption;
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyAgreement;
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyAgreementWithKeyWrapping;
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyEncryption;
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyWrapping;
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
use Jose\Component\Encryption\Compression\CompressionMethod;
use Jose\Component\Encryption\Compression\CompressionMethodManager;
use LogicException;
use RuntimeException;
use function array_key_exists;
use function count;
use function is_string;
class JWEBuilder
{
protected ?JWK $senderKey = null;
protected ?string $payload = null;
protected ?string $aad = null;
protected array $recipients = [];
protected array $sharedProtectedHeader = [];
protected array $sharedHeader = [];
private ?CompressionMethod $compressionMethod = null;
private ?string $keyManagementMode = null;
private ?ContentEncryptionAlgorithm $contentEncryptionAlgorithm = null;
private readonly AlgorithmManager $keyEncryptionAlgorithmManager;
private readonly AlgorithmManager $contentEncryptionAlgorithmManager;
public function __construct(
AlgorithmManager $algorithmManager,
null|AlgorithmManager $contentEncryptionAlgorithmManager = null,
private readonly null|CompressionMethodManager $compressionManager = null
) {
if ($compressionManager !== null) {
trigger_deprecation(
'web-token/jwt-library',
'3.3.0',
'The parameter "$compressionManager" is deprecated and will be removed in 4.0.0. Compression is not recommended for JWE. Please set "null" instead.'
);
}
if ($contentEncryptionAlgorithmManager !== null) {
trigger_deprecation(
'web-token/jwt-library',
'3.3.0',
'The parameter "$contentEncryptionAlgorithmManager" is deprecated and will be removed in 4.0.0. Please set all algorithms in the first argument and set "null" instead.'
);
$this->keyEncryptionAlgorithmManager = $algorithmManager;
$this->contentEncryptionAlgorithmManager = $contentEncryptionAlgorithmManager;
} else {
$keyEncryptionAlgorithms = [];
$contentEncryptionAlgorithms = [];
foreach ($algorithmManager->all() as $algorithm) {
if ($algorithm instanceof KeyEncryptionAlgorithm) {
$keyEncryptionAlgorithms[] = $algorithm;
}
if ($algorithm instanceof ContentEncryptionAlgorithm) {
$contentEncryptionAlgorithms[] = $algorithm;
}
}
$this->keyEncryptionAlgorithmManager = new AlgorithmManager($keyEncryptionAlgorithms);
$this->contentEncryptionAlgorithmManager = new AlgorithmManager($contentEncryptionAlgorithms);
}
}
/**
* Reset the current data.
*/
public function create(): self
{
$this->senderKey = null;
$this->payload = null;
$this->aad = null;
$this->recipients = [];
$this->sharedProtectedHeader = [];
$this->sharedHeader = [];
$this->compressionMethod = null;
$this->keyManagementMode = null;
return $this;
}
/**
* Returns the key encryption algorithm manager.
*/
public function getKeyEncryptionAlgorithmManager(): AlgorithmManager
{
return $this->keyEncryptionAlgorithmManager;
}
/**
* Returns the content encryption algorithm manager.
*/
public function getContentEncryptionAlgorithmManager(): AlgorithmManager
{
return $this->contentEncryptionAlgorithmManager;
}
/**
* Returns the compression method manager.
* @deprecated This method is deprecated and will be removed in v4.0. Compression is not recommended for JWE.
*/
public function getCompressionMethodManager(): null|CompressionMethodManager
{
return $this->compressionManager;
}
/**
* Set the payload of the JWE to build.
*/
public function withPayload(string $payload): self
{
$clone = clone $this;
$clone->payload = $payload;
return $clone;
}
/**
* Set the Additional Authenticated Data of the JWE to build.
*/
public function withAAD(?string $aad): self
{
$clone = clone $this;
$clone->aad = $aad;
return $clone;
}
/**
* Set the shared protected header of the JWE to build.
*/
public function withSharedProtectedHeader(array $sharedProtectedHeader): self
{
$this->checkDuplicatedHeaderParameters($sharedProtectedHeader, $this->sharedHeader);
foreach ($this->recipients as $recipient) {
$this->checkDuplicatedHeaderParameters($sharedProtectedHeader, $recipient->getHeader());
}
$clone = clone $this;
$clone->sharedProtectedHeader = $sharedProtectedHeader;
return $clone;
}
/**
* Set the shared header of the JWE to build.
*/
public function withSharedHeader(array $sharedHeader): self
{
$this->checkDuplicatedHeaderParameters($this->sharedProtectedHeader, $sharedHeader);
foreach ($this->recipients as $recipient) {
$this->checkDuplicatedHeaderParameters($sharedHeader, $recipient->getHeader());
}
$clone = clone $this;
$clone->sharedHeader = $sharedHeader;
return $clone;
}
/**
* Adds a recipient to the JWE to build.
*/
public function addRecipient(JWK $recipientKey, array $recipientHeader = []): self
{
$this->checkDuplicatedHeaderParameters($this->sharedProtectedHeader, $recipientHeader);
$this->checkDuplicatedHeaderParameters($this->sharedHeader, $recipientHeader);
$clone = clone $this;
$completeHeader = array_merge($clone->sharedHeader, $recipientHeader, $clone->sharedProtectedHeader);
$clone->checkAndSetContentEncryptionAlgorithm($completeHeader);
$keyEncryptionAlgorithm = $clone->getKeyEncryptionAlgorithm($completeHeader);
if ($clone->keyManagementMode === null) {
$clone->keyManagementMode = $keyEncryptionAlgorithm->getKeyManagementMode();
} else {
if (! $clone->areKeyManagementModesCompatible(
$clone->keyManagementMode,
$keyEncryptionAlgorithm->getKeyManagementMode()
)) {
throw new InvalidArgumentException('Foreign key management mode forbidden.');
}
}
$compressionMethod = $clone->getCompressionMethod($completeHeader);
if ($compressionMethod !== null) {
if ($clone->compressionMethod === null) {
$clone->compressionMethod = $compressionMethod;
} elseif ($clone->compressionMethod->name() !== $compressionMethod->name()) {
throw new InvalidArgumentException('Incompatible compression method.');
}
}
if ($compressionMethod === null && $clone->compressionMethod !== null) {
throw new InvalidArgumentException('Inconsistent compression method.');
}
$clone->checkKey($keyEncryptionAlgorithm, $recipientKey);
$clone->recipients[] = [
'key' => $recipientKey,
'header' => $recipientHeader,
'key_encryption_algorithm' => $keyEncryptionAlgorithm,
];
return $clone;
}
//TODO: Verify if the key is compatible with the key encryption algorithm like is done to the ECDH-ES
/**
* Set the sender JWK to be used instead of the internal generated JWK
*/
public function withSenderKey(JWK $senderKey): self
{
$clone = clone $this;
$completeHeader = array_merge($clone->sharedHeader, $clone->sharedProtectedHeader);
$keyEncryptionAlgorithm = $clone->getKeyEncryptionAlgorithm($completeHeader);
if ($clone->keyManagementMode === null) {
$clone->keyManagementMode = $keyEncryptionAlgorithm->getKeyManagementMode();
} else {
if (! $clone->areKeyManagementModesCompatible(
$clone->keyManagementMode,
$keyEncryptionAlgorithm->getKeyManagementMode()
)) {
throw new InvalidArgumentException('Foreign key management mode forbidden.');
}
}
$clone->checkKey($keyEncryptionAlgorithm, $senderKey);
$clone->senderKey = $senderKey;
return $clone;
}
/**
* Builds the JWE.
*/
public function build(): JWE
{
if ($this->payload === null) {
throw new LogicException('Payload not set.');
}
if (count($this->recipients) === 0) {
throw new LogicException('No recipient.');
}
$additionalHeader = [];
$cek = $this->determineCEK($additionalHeader);
$recipients = [];
foreach ($this->recipients as $recipient) {
$recipient = $this->processRecipient($recipient, $cek, $additionalHeader);
$recipients[] = $recipient;
}
if ((is_countable($additionalHeader) ? count($additionalHeader) : 0) !== 0 && count($this->recipients) === 1) {
$sharedProtectedHeader = array_merge($additionalHeader, $this->sharedProtectedHeader);
} else {
$sharedProtectedHeader = $this->sharedProtectedHeader;
}
$encodedSharedProtectedHeader = count($sharedProtectedHeader) === 0 ? '' : Base64UrlSafe::encodeUnpadded(
JsonConverter::encode($sharedProtectedHeader)
);
[$ciphertext, $iv, $tag] = $this->encryptJWE($cek, $encodedSharedProtectedHeader);
return new JWE(
$ciphertext,
$iv,
$tag,
$this->aad,
$this->sharedHeader,
$sharedProtectedHeader,
$encodedSharedProtectedHeader,
$recipients
);
}
private function checkAndSetContentEncryptionAlgorithm(array $completeHeader): void
{
$contentEncryptionAlgorithm = $this->getContentEncryptionAlgorithm($completeHeader);
if ($this->contentEncryptionAlgorithm === null) {
$this->contentEncryptionAlgorithm = $contentEncryptionAlgorithm;
} elseif ($contentEncryptionAlgorithm->name() !== $this->contentEncryptionAlgorithm->name()) {
throw new InvalidArgumentException('Inconsistent content encryption algorithm');
}
}
private function processRecipient(array $recipient, string $cek, array &$additionalHeader): Recipient
{
$completeHeader = array_merge($this->sharedHeader, $recipient['header'], $this->sharedProtectedHeader);
$keyEncryptionAlgorithm = $recipient['key_encryption_algorithm'];
if (! $keyEncryptionAlgorithm instanceof KeyEncryptionAlgorithm) {
throw new InvalidArgumentException('The key encryption algorithm is not valid');
}
$encryptedContentEncryptionKey = $this->getEncryptedKey(
$completeHeader,
$cek,
$keyEncryptionAlgorithm,
$additionalHeader,
$recipient['key'],
$recipient['sender_key'] ?? $this->senderKey ?? null
);
$recipientHeader = $recipient['header'];
if ((is_countable($additionalHeader) ? count($additionalHeader) : 0) !== 0 && count($this->recipients) !== 1) {
$recipientHeader = array_merge($recipientHeader, $additionalHeader);
$additionalHeader = [];
}
return new Recipient($recipientHeader, $encryptedContentEncryptionKey);
}
private function encryptJWE(string $cek, string $encodedSharedProtectedHeader): array
{
if (! $this->contentEncryptionAlgorithm instanceof ContentEncryptionAlgorithm) {
throw new InvalidArgumentException('The content encryption algorithm is not valid');
}
$iv_size = $this->contentEncryptionAlgorithm->getIVSize();
$iv = $this->createIV($iv_size);
$payload = $this->preparePayload();
$tag = null;
$ciphertext = $this->contentEncryptionAlgorithm->encryptContent(
$payload ?? '',
$cek,
$iv,
$this->aad,
$encodedSharedProtectedHeader,
$tag
);
return [$ciphertext, $iv, $tag];
}
private function preparePayload(): ?string
{
$prepared = $this->payload;
if ($this->compressionMethod === null) {
return $prepared;
}
return $this->compressionMethod->compress($prepared ?? '');
}
private function getEncryptedKey(
array $completeHeader,
string $cek,
KeyEncryptionAlgorithm $keyEncryptionAlgorithm,
array &$additionalHeader,
JWK $recipientKey,
?JWK $senderKey
): ?string {
if ($keyEncryptionAlgorithm instanceof KeyEncryption) {
return $this->getEncryptedKeyFromKeyEncryptionAlgorithm(
$completeHeader,
$cek,
$keyEncryptionAlgorithm,
$recipientKey,
$additionalHeader
);
}
if ($keyEncryptionAlgorithm instanceof KeyWrapping) {
return $this->getEncryptedKeyFromKeyWrappingAlgorithm(
$completeHeader,
$cek,
$keyEncryptionAlgorithm,
$recipientKey,
$additionalHeader
);
}
if ($keyEncryptionAlgorithm instanceof KeyAgreementWithKeyWrapping) {
return $this->getEncryptedKeyFromKeyAgreementAndKeyWrappingAlgorithm(
$completeHeader,
$cek,
$keyEncryptionAlgorithm,
$additionalHeader,
$recipientKey,
$senderKey
);
}
if ($keyEncryptionAlgorithm instanceof KeyAgreement) {
return null;
}
if ($keyEncryptionAlgorithm instanceof DirectEncryption) {
return null;
}
throw new InvalidArgumentException('Unsupported key encryption algorithm.');
}
private function getEncryptedKeyFromKeyAgreementAndKeyWrappingAlgorithm(
array $completeHeader,
string $cek,
KeyAgreementWithKeyWrapping $keyEncryptionAlgorithm,
array &$additionalHeader,
JWK $recipientKey,
?JWK $senderKey
): string {
if ($this->contentEncryptionAlgorithm === null) {
throw new InvalidArgumentException('Invalid content encryption algorithm');
}
return $keyEncryptionAlgorithm->wrapAgreementKey(
$recipientKey,
$senderKey,
$cek,
$this->contentEncryptionAlgorithm->getCEKSize(),
$completeHeader,
$additionalHeader
);
}
private function getEncryptedKeyFromKeyEncryptionAlgorithm(
array $completeHeader,
string $cek,
KeyEncryption $keyEncryptionAlgorithm,
JWK $recipientKey,
array &$additionalHeader
): string {
return $keyEncryptionAlgorithm->encryptKey($recipientKey, $cek, $completeHeader, $additionalHeader);
}
private function getEncryptedKeyFromKeyWrappingAlgorithm(
array $completeHeader,
string $cek,
KeyWrapping $keyEncryptionAlgorithm,
JWK $recipientKey,
array &$additionalHeader
): string {
return $keyEncryptionAlgorithm->wrapKey($recipientKey, $cek, $completeHeader, $additionalHeader);
}
private function checkKey(KeyEncryptionAlgorithm $keyEncryptionAlgorithm, JWK $recipientKey): void
{
if ($this->contentEncryptionAlgorithm === null) {
throw new InvalidArgumentException('Invalid content encryption algorithm');
}
KeyChecker::checkKeyUsage($recipientKey, 'encryption');
if ($keyEncryptionAlgorithm->name() !== 'dir') {
KeyChecker::checkKeyAlgorithm($recipientKey, $keyEncryptionAlgorithm->name());
} else {
KeyChecker::checkKeyAlgorithm($recipientKey, $this->contentEncryptionAlgorithm->name());
}
}
private function determineCEK(array &$additionalHeader): string
{
if ($this->contentEncryptionAlgorithm === null) {
throw new InvalidArgumentException('Invalid content encryption algorithm');
}
switch ($this->keyManagementMode) {
case KeyEncryption::MODE_ENCRYPT:
case KeyEncryption::MODE_WRAP:
return $this->createCEK($this->contentEncryptionAlgorithm->getCEKSize());
case KeyEncryption::MODE_AGREEMENT:
if (count($this->recipients) !== 1) {
throw new LogicException(
'Unable to encrypt for multiple recipients using key agreement algorithms.'
);
}
$recipientKey = $this->recipients[0]['key'];
$senderKey = $this->recipients[0]['sender_key'] ?? null;
$algorithm = $this->recipients[0]['key_encryption_algorithm'];
if (! $algorithm instanceof KeyAgreement) {
throw new InvalidArgumentException('Invalid content encryption algorithm');
}
$completeHeader = array_merge(
$this->sharedHeader,
$this->recipients[0]['header'],
$this->sharedProtectedHeader
);
return $algorithm->getAgreementKey(
$this->contentEncryptionAlgorithm->getCEKSize(),
$this->contentEncryptionAlgorithm->name(),
$recipientKey,
$senderKey,
$completeHeader,
$additionalHeader
);
case KeyEncryption::MODE_DIRECT:
if (count($this->recipients) !== 1) {
throw new LogicException(
'Unable to encrypt for multiple recipients using key agreement algorithms.'
);
}
/** @var JWK $key */
$key = $this->recipients[0]['key'];
if ($key->get('kty') !== 'oct') {
throw new RuntimeException('Wrong key type.');
}
$k = $key->get('k');
if (! is_string($k)) {
throw new RuntimeException('Invalid key.');
}
return Base64UrlSafe::decodeNoPadding($k);
default:
throw new InvalidArgumentException(sprintf(
'Unsupported key management mode "%s".',
$this->keyManagementMode
));
}
}
private function getCompressionMethod(array $completeHeader): ?CompressionMethod
{
if ($this->compressionManager === null || ! array_key_exists('zip', $completeHeader)) {
return null;
}
return $this->compressionManager->get($completeHeader['zip']);
}
private function areKeyManagementModesCompatible(string $current, string $new): bool
{
$agree = KeyEncryptionAlgorithm::MODE_AGREEMENT;
$dir = KeyEncryptionAlgorithm::MODE_DIRECT;
$enc = KeyEncryptionAlgorithm::MODE_ENCRYPT;
$wrap = KeyEncryptionAlgorithm::MODE_WRAP;
$supportedKeyManagementModeCombinations = [
$enc . $enc => true,
$enc . $wrap => true,
$wrap . $enc => true,
$wrap . $wrap => true,
$agree . $agree => false,
$agree . $dir => false,
$agree . $enc => false,
$agree . $wrap => false,
$dir . $agree => false,
$dir . $dir => false,
$dir . $enc => false,
$dir . $wrap => false,
$enc . $agree => false,
$enc . $dir => false,
$wrap . $agree => false,
$wrap . $dir => false,
];
if (array_key_exists($current . $new, $supportedKeyManagementModeCombinations)) {
return $supportedKeyManagementModeCombinations[$current . $new];
}
return false;
}
private function createCEK(int $size): string
{
return random_bytes($size / 8);
}
private function createIV(int $size): string
{
return random_bytes($size / 8);
}
private function getKeyEncryptionAlgorithm(array $completeHeader): KeyEncryptionAlgorithm
{
if (! isset($completeHeader['alg'])) {
throw new InvalidArgumentException('Parameter "alg" is missing.');
}
$keyEncryptionAlgorithm = $this->keyEncryptionAlgorithmManager->get($completeHeader['alg']);
if (! $keyEncryptionAlgorithm instanceof KeyEncryptionAlgorithm) {
throw new InvalidArgumentException(sprintf(
'The key encryption algorithm "%s" is not supported or not a key encryption algorithm instance.',
$completeHeader['alg']
));
}
return $keyEncryptionAlgorithm;
}
private function getContentEncryptionAlgorithm(array $completeHeader): ContentEncryptionAlgorithm
{
if (! isset($completeHeader['enc'])) {
throw new InvalidArgumentException('Parameter "enc" is missing.');
}
$contentEncryptionAlgorithm = $this->contentEncryptionAlgorithmManager->get($completeHeader['enc']);
if (! $contentEncryptionAlgorithm instanceof ContentEncryptionAlgorithm) {
throw new InvalidArgumentException(sprintf(
'The content encryption algorithm "%s" is not supported or not a content encryption algorithm instance.',
$completeHeader['enc']
));
}
return $contentEncryptionAlgorithm;
}
private function checkDuplicatedHeaderParameters(array $header1, array $header2): void
{
$inter = array_intersect_key($header1, $header2);
if (count($inter) !== 0) {
throw new InvalidArgumentException(sprintf(
'The header contains duplicated entries: %s.',
implode(', ', array_keys($inter))
));
}
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption;
use Jose\Component\Core\AlgorithmManagerFactory;
use Jose\Component\Encryption\Compression\CompressionMethodManagerFactory;
class JWEBuilderFactory
{
public function __construct(
private readonly AlgorithmManagerFactory $algorithmManagerFactory,
private readonly null|CompressionMethodManagerFactory $compressionMethodManagerFactory = null
) {
if ($compressionMethodManagerFactory !== null) {
trigger_deprecation(
'web-token/jwt-library',
'3.3.0',
'The parameter "$compressionMethodManagerFactory" is deprecated and will be removed in 4.0.0. Compression is not recommended for JWE. Please set "null" instead.'
);
}
}
/**
* Creates a JWE Builder object using the given key encryption algorithms, content encryption algorithms and
* compression methods.
*
* @param array<string> $encryptionAlgorithms
* @param null|array<string> $contentEncryptionAlgorithm
* @param null|string[] $compressionMethods
*/
public function create(
array $encryptionAlgorithms,
null|array $contentEncryptionAlgorithm = null,
null|array $compressionMethods = null
): JWEBuilder {
if ($contentEncryptionAlgorithm !== null) {
trigger_deprecation(
'web-token/jwt-library',
'3.3.0',
'The parameter "$contentEncryptionAlgorithm" is deprecated and will be removed in 4.0.0. Please set "null" instead.'
);
$encryptionAlgorithms = array_merge($encryptionAlgorithms, $contentEncryptionAlgorithm);
}
$encryptionAlgorithmManager = $this->algorithmManagerFactory->create($encryptionAlgorithms);
$compressionMethodManager = $compressionMethods === null ? null : $this->compressionMethodManagerFactory?->create(
$compressionMethods
);
return new JWEBuilder($encryptionAlgorithmManager, null, $compressionMethodManager);
}
}

View File

@ -0,0 +1,324 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption;
use InvalidArgumentException;
use Jose\Component\Core\Algorithm;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Core\Util\KeyChecker;
use Jose\Component\Encryption\Algorithm\ContentEncryptionAlgorithm;
use Jose\Component\Encryption\Algorithm\KeyEncryption\DirectEncryption;
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyAgreement;
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyAgreementWithKeyWrapping;
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyEncryption;
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyWrapping;
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
use Jose\Component\Encryption\Compression\CompressionMethodManager;
use Throwable;
use function array_key_exists;
use function is_string;
class JWEDecrypter
{
private readonly AlgorithmManager $keyEncryptionAlgorithmManager;
private readonly AlgorithmManager $contentEncryptionAlgorithmManager;
public function __construct(
AlgorithmManager $algorithmManager,
null|AlgorithmManager $contentEncryptionAlgorithmManager,
private readonly null|CompressionMethodManager $compressionMethodManager = null
) {
if ($compressionMethodManager !== null) {
trigger_deprecation(
'web-token/jwt-library',
'3.3.0',
'The parameter "$compressionMethodManager" is deprecated and will be removed in 4.0.0. Compression is not recommended for JWE. Please set "null" instead.'
);
}
if ($contentEncryptionAlgorithmManager !== null) {
trigger_deprecation(
'web-token/jwt-library',
'3.3.0',
'The parameter "$contentEncryptionAlgorithmManager" is deprecated and will be removed in 4.0.0. Please set all algorithms in the first argument and set "null" instead.'
);
$this->keyEncryptionAlgorithmManager = $algorithmManager;
$this->contentEncryptionAlgorithmManager = $contentEncryptionAlgorithmManager;
} else {
$keyEncryptionAlgorithms = [];
$contentEncryptionAlgorithms = [];
foreach ($algorithmManager->all() as $key => $algorithm) {
if ($algorithm instanceof KeyEncryptionAlgorithm) {
$keyEncryptionAlgorithms[$key] = $algorithm;
}
if ($algorithm instanceof ContentEncryptionAlgorithm) {
$contentEncryptionAlgorithms[$key] = $algorithm;
}
}
$this->keyEncryptionAlgorithmManager = new AlgorithmManager($keyEncryptionAlgorithms);
$this->contentEncryptionAlgorithmManager = new AlgorithmManager($contentEncryptionAlgorithms);
}
}
/**
* Returns the key encryption algorithm manager.
*/
public function getKeyEncryptionAlgorithmManager(): AlgorithmManager
{
return $this->keyEncryptionAlgorithmManager;
}
/**
* Returns the content encryption algorithm manager.
*/
public function getContentEncryptionAlgorithmManager(): AlgorithmManager
{
return $this->contentEncryptionAlgorithmManager;
}
/**
* Returns the compression method manager.
* @deprecated This method is deprecated and will be removed in v4.0. Compression is not recommended for JWE.
*/
public function getCompressionMethodManager(): null|CompressionMethodManager
{
return $this->compressionMethodManager;
}
/**
* This method will try to decrypt the given JWE and recipient using a JWK.
*
* @param JWE $jwe A JWE object to decrypt
* @param JWK $jwk The key used to decrypt the input
* @param int $recipient The recipient used to decrypt the token
*/
public function decryptUsingKey(JWE &$jwe, JWK $jwk, int $recipient, ?JWK $senderKey = null): bool
{
$jwkset = new JWKSet([$jwk]);
return $this->decryptUsingKeySet($jwe, $jwkset, $recipient, $senderKey);
}
/**
* This method will try to decrypt the given JWE and recipient using a JWKSet.
*
* @param JWE $jwe A JWE object to decrypt
* @param JWKSet $jwkset The key set used to decrypt the input
* @param JWK $jwk The key used to decrypt the token in case of success
* @param int $recipient The recipient used to decrypt the token in case of success
*/
public function decryptUsingKeySet(
JWE &$jwe,
JWKSet $jwkset,
int $recipient,
?JWK &$jwk = null,
?JWK $senderKey = null
): bool {
if ($jwkset->count() === 0) {
throw new InvalidArgumentException('No key in the key set.');
}
if ($jwe->getPayload() !== null) {
return true;
}
if ($jwe->countRecipients() === 0) {
throw new InvalidArgumentException('The JWE does not contain any recipient.');
}
$plaintext = $this->decryptRecipientKey($jwe, $jwkset, $recipient, $jwk, $senderKey);
if ($plaintext !== null) {
$jwe = $jwe->withPayload($plaintext);
return true;
}
return false;
}
private function decryptRecipientKey(
JWE $jwe,
JWKSet $jwkset,
int $i,
?JWK &$successJwk = null,
?JWK $senderKey = null
): ?string {
$recipient = $jwe->getRecipient($i);
$completeHeader = array_merge(
$jwe->getSharedProtectedHeader(),
$jwe->getSharedHeader(),
$recipient->getHeader()
);
$this->checkCompleteHeader($completeHeader);
$key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($completeHeader);
$content_encryption_algorithm = $this->getContentEncryptionAlgorithm($completeHeader);
$this->checkIvSize($jwe->getIV(), $content_encryption_algorithm->getIVSize());
foreach ($jwkset as $recipientKey) {
try {
KeyChecker::checkKeyUsage($recipientKey, 'decryption');
if ($key_encryption_algorithm->name() !== 'dir') {
KeyChecker::checkKeyAlgorithm($recipientKey, $key_encryption_algorithm->name());
} else {
KeyChecker::checkKeyAlgorithm($recipientKey, $content_encryption_algorithm->name());
}
$cek = $this->decryptCEK(
$key_encryption_algorithm,
$content_encryption_algorithm,
$recipientKey,
$senderKey,
$recipient,
$completeHeader
);
$this->checkCekSize($cek, $key_encryption_algorithm, $content_encryption_algorithm);
$payload = $this->decryptPayload($jwe, $cek, $content_encryption_algorithm, $completeHeader);
$successJwk = $recipientKey;
return $payload;
} catch (Throwable) {
//We do nothing, we continue with other keys
continue;
}
}
return null;
}
private function checkCekSize(
string $cek,
KeyEncryptionAlgorithm $keyEncryptionAlgorithm,
ContentEncryptionAlgorithm $algorithm
): void {
if ($keyEncryptionAlgorithm instanceof DirectEncryption || $keyEncryptionAlgorithm instanceof KeyAgreement) {
return;
}
if (mb_strlen($cek, '8bit') * 8 !== $algorithm->getCEKSize()) {
throw new InvalidArgumentException('Invalid CEK size');
}
}
private function checkIvSize(?string $iv, int $requiredIvSize): void
{
if ($iv === null && $requiredIvSize !== 0) {
throw new InvalidArgumentException('Invalid IV size');
}
if (is_string($iv) && mb_strlen($iv, '8bit') !== $requiredIvSize / 8) {
throw new InvalidArgumentException('Invalid IV size');
}
}
private function decryptCEK(
Algorithm $key_encryption_algorithm,
ContentEncryptionAlgorithm $content_encryption_algorithm,
JWK $recipientKey,
?JWK $senderKey,
Recipient $recipient,
array $completeHeader
): string {
if ($key_encryption_algorithm instanceof DirectEncryption) {
return $key_encryption_algorithm->getCEK($recipientKey);
}
if ($key_encryption_algorithm instanceof KeyAgreement) {
return $key_encryption_algorithm->getAgreementKey(
$content_encryption_algorithm->getCEKSize(),
$content_encryption_algorithm->name(),
$recipientKey,
$senderKey,
$completeHeader
);
}
if ($key_encryption_algorithm instanceof KeyAgreementWithKeyWrapping) {
return $key_encryption_algorithm->unwrapAgreementKey(
$recipientKey,
$senderKey,
$recipient->getEncryptedKey() ?? '',
$content_encryption_algorithm->getCEKSize(),
$completeHeader
);
}
if ($key_encryption_algorithm instanceof KeyEncryption) {
return $key_encryption_algorithm->decryptKey(
$recipientKey,
$recipient->getEncryptedKey() ?? '',
$completeHeader
);
}
if ($key_encryption_algorithm instanceof KeyWrapping) {
return $key_encryption_algorithm->unwrapKey(
$recipientKey,
$recipient->getEncryptedKey() ?? '',
$completeHeader
);
}
throw new InvalidArgumentException('Unsupported CEK generation');
}
private function decryptPayload(
JWE $jwe,
string $cek,
ContentEncryptionAlgorithm $content_encryption_algorithm,
array $completeHeader
): string {
$payload = $content_encryption_algorithm->decryptContent(
$jwe->getCiphertext() ?? '',
$cek,
$jwe->getIV() ?? '',
$jwe->getAAD(),
$jwe->getEncodedSharedProtectedHeader(),
$jwe->getTag() ?? ''
);
return $this->decompressIfNeeded($payload, $completeHeader);
}
private function decompressIfNeeded(string $payload, array $completeHeaders): string
{
if ($this->compressionMethodManager === null || ! array_key_exists('zip', $completeHeaders)) {
return $payload;
}
$compression_method = $this->compressionMethodManager->get($completeHeaders['zip']);
return $compression_method->uncompress($payload);
}
private function checkCompleteHeader(array $completeHeaders): void
{
foreach (['enc', 'alg'] as $key) {
if (! isset($completeHeaders[$key])) {
throw new InvalidArgumentException(sprintf("Parameter '%s' is missing.", $key));
}
}
}
private function getKeyEncryptionAlgorithm(array $completeHeaders): KeyEncryptionAlgorithm
{
$key_encryption_algorithm = $this->keyEncryptionAlgorithmManager->get($completeHeaders['alg']);
if (! $key_encryption_algorithm instanceof KeyEncryptionAlgorithm) {
throw new InvalidArgumentException(sprintf(
'The key encryption algorithm "%s" is not supported or does not implement KeyEncryptionAlgorithm interface.',
$completeHeaders['alg']
));
}
return $key_encryption_algorithm;
}
private function getContentEncryptionAlgorithm(array $completeHeader): ContentEncryptionAlgorithm
{
$content_encryption_algorithm = $this->contentEncryptionAlgorithmManager->get($completeHeader['enc']);
if (! $content_encryption_algorithm instanceof ContentEncryptionAlgorithm) {
throw new InvalidArgumentException(sprintf(
'The key encryption algorithm "%s" is not supported or does not implement the ContentEncryption interface.',
$completeHeader['enc']
));
}
return $content_encryption_algorithm;
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption;
use Jose\Component\Core\AlgorithmManagerFactory;
use Jose\Component\Encryption\Compression\CompressionMethodManagerFactory;
class JWEDecrypterFactory
{
public function __construct(
private readonly AlgorithmManagerFactory $algorithmManagerFactory,
private readonly null|CompressionMethodManagerFactory $compressionMethodManagerFactory = null
) {
if ($compressionMethodManagerFactory !== null) {
trigger_deprecation(
'web-token/jwt-library',
'3.3.0',
'The parameter "$compressionMethodManagerFactory" is deprecated and will be removed in 4.0.0. Compression is not recommended for JWE. Please set "null" instead.'
);
}
}
/**
* Creates a JWE Decrypter object using the given key encryption algorithms, content encryption algorithms and
* compression methods.
*
* @param string[] $encryptionAlgorithms
* @param null|string[] $contentEncryptionAlgorithms
* @param null|string[] $compressionMethods
*/
public function create(
array $encryptionAlgorithms,
null|array $contentEncryptionAlgorithms = null,
null|array $compressionMethods = null
): JWEDecrypter {
if ($contentEncryptionAlgorithms !== null) {
$encryptionAlgorithms = array_merge($encryptionAlgorithms, $contentEncryptionAlgorithms);
}
$algorithmManager = $this->algorithmManagerFactory->create($encryptionAlgorithms);
$compressionMethodManager = $compressionMethods === null ? null : $this->compressionMethodManagerFactory?->create(
$compressionMethods
);
return new JWEDecrypter($algorithmManager, null, $compressionMethodManager);
}
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption;
use Jose\Component\Checker\HeaderCheckerManager;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Encryption\Serializer\JWESerializerManager;
use RuntimeException;
use Throwable;
/**
* @see \Jose\Tests\Component\Encryption\JWELoaderTest
*/
class JWELoader
{
public function __construct(
private readonly JWESerializerManager $serializerManager,
private readonly JWEDecrypter $jweDecrypter,
private readonly ?HeaderCheckerManager $headerCheckerManager
) {
}
/**
* Returns the JWE Decrypter object.
*/
public function getJweDecrypter(): JWEDecrypter
{
return $this->jweDecrypter;
}
/**
* Returns the header checker manager if set.
*/
public function getHeaderCheckerManager(): ?HeaderCheckerManager
{
return $this->headerCheckerManager;
}
/**
* Returns the serializer manager.
*/
public function getSerializerManager(): JWESerializerManager
{
return $this->serializerManager;
}
/**
* This method will try to load and decrypt the given token using a JWK. If succeeded, the methods will populate the
* $recipient variable and returns the JWE.
*/
public function loadAndDecryptWithKey(string $token, JWK $key, ?int &$recipient): JWE
{
$keyset = new JWKSet([$key]);
return $this->loadAndDecryptWithKeySet($token, $keyset, $recipient);
}
/**
* This method will try to load and decrypt the given token using a JWKSet. If succeeded, the methods will populate
* the $recipient variable and returns the JWE.
*/
public function loadAndDecryptWithKeySet(string $token, JWKSet $keyset, ?int &$recipient): JWE
{
try {
$jwe = $this->serializerManager->unserialize($token);
$nbRecipients = $jwe->countRecipients();
for ($i = 0; $i < $nbRecipients; ++$i) {
if ($this->processRecipient($jwe, $keyset, $i)) {
$recipient = $i;
return $jwe;
}
}
} catch (Throwable) {
// Nothing to do. Exception thrown just after
}
throw new RuntimeException('Unable to load and decrypt the token.');
}
private function processRecipient(JWE &$jwe, JWKSet $keyset, int $recipient): bool
{
try {
if ($this->headerCheckerManager !== null) {
$this->headerCheckerManager->check($jwe, $recipient);
}
return $this->jweDecrypter->decryptUsingKeySet($jwe, $keyset, $recipient);
} catch (Throwable) {
return false;
}
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption;
use Jose\Component\Checker\HeaderCheckerManagerFactory;
use Jose\Component\Encryption\Serializer\JWESerializerManagerFactory;
class JWELoaderFactory
{
public function __construct(
private readonly JWESerializerManagerFactory $jweSerializerManagerFactory,
private readonly JWEDecrypterFactory $jweDecrypterFactory,
private readonly ?HeaderCheckerManagerFactory $headerCheckerManagerFactory
) {
}
/**
* Creates a JWELoader using the given serializer aliases, encryption algorithm aliases, compression method aliases
* and header checker aliases.
*/
public function create(
array $serializers,
array $encryptionAlgorithms,
null|array $contentEncryptionAlgorithms = null,
null|array $compressionMethods = null,
array $headerCheckers = []
): JWELoader {
if ($contentEncryptionAlgorithms !== null) {
$encryptionAlgorithms = array_merge($encryptionAlgorithms, $contentEncryptionAlgorithms);
}
$serializerManager = $this->jweSerializerManagerFactory->create($serializers);
$jweDecrypter = $this->jweDecrypterFactory->create($encryptionAlgorithms, null, $compressionMethods);
if ($this->headerCheckerManagerFactory !== null) {
$headerCheckerManager = $this->headerCheckerManagerFactory->create($headerCheckers);
} else {
$headerCheckerManager = null;
}
return new JWELoader($serializerManager, $jweDecrypter, $headerCheckerManager);
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption;
use Jose\Component\Checker\TokenTypeSupport;
use Jose\Component\Core\JWT;
final class JWETokenSupport implements TokenTypeSupport
{
public function supports(JWT $jwt): bool
{
return $jwt instanceof JWE;
}
/**
* @param array<string, mixed> $protectedHeader
* @param array<string, mixed> $unprotectedHeader
*/
public function retrieveTokenHeaders(JWT $jwt, int $index, array &$protectedHeader, array &$unprotectedHeader): void
{
if (! $jwt instanceof JWE) {
return;
}
$protectedHeader = $jwt->getSharedProtectedHeader();
$unprotectedHeader = $jwt->getSharedHeader();
$recipient = $jwt->getRecipient($index)
->getHeader();
$unprotectedHeader = array_merge($unprotectedHeader, $recipient);
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption;
use InvalidArgumentException;
use function array_key_exists;
/**
* @internal
*/
final class Recipient
{
public function __construct(
private readonly array $header,
private readonly ?string $encryptedKey
) {
}
/**
* Returns the recipient header.
*/
public function getHeader(): array
{
return $this->header;
}
/**
* Returns the value of the recipient header parameter with the specified key.
*
* @param string $key The key
*
* @return mixed|null
*/
public function getHeaderParameter(string $key)
{
if (! $this->hasHeaderParameter($key)) {
throw new InvalidArgumentException(sprintf('The header "%s" does not exist.', $key));
}
return $this->header[$key];
}
/**
* Returns true if the recipient header contains the parameter with the specified key.
*
* @param string $key The key
*/
public function hasHeaderParameter(string $key): bool
{
return array_key_exists($key, $this->header);
}
/**
* Returns the encrypted key.
*/
public function getEncryptedKey(): ?string
{
return $this->encryptedKey;
}
}

View File

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Serializer;
use InvalidArgumentException;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Encryption\JWE;
use Jose\Component\Encryption\Recipient;
use LogicException;
use Throwable;
use function count;
use function is_array;
final class CompactSerializer implements JWESerializer
{
public const NAME = 'jwe_compact';
public function displayName(): string
{
return 'JWE Compact';
}
public function name(): string
{
return self::NAME;
}
public function serialize(JWE $jwe, ?int $recipientIndex = null): string
{
if ($recipientIndex === null) {
$recipientIndex = 0;
}
$recipient = $jwe->getRecipient($recipientIndex);
$this->checkHasNoAAD($jwe);
$this->checkHasSharedProtectedHeader($jwe);
$this->checkRecipientHasNoHeader($jwe, $recipientIndex);
return sprintf(
'%s.%s.%s.%s.%s',
$jwe->getEncodedSharedProtectedHeader(),
Base64UrlSafe::encodeUnpadded($recipient->getEncryptedKey() ?? ''),
Base64UrlSafe::encodeUnpadded($jwe->getIV() ?? ''),
Base64UrlSafe::encodeUnpadded($jwe->getCiphertext() ?? ''),
Base64UrlSafe::encodeUnpadded($jwe->getTag() ?? '')
);
}
public function unserialize(string $input): JWE
{
$parts = explode('.', $input);
if (count($parts) !== 5) {
throw new InvalidArgumentException('Unsupported input');
}
try {
$encodedSharedProtectedHeader = $parts[0];
$sharedProtectedHeader = JsonConverter::decode(
Base64UrlSafe::decodeNoPadding($encodedSharedProtectedHeader)
);
if (! is_array($sharedProtectedHeader)) {
throw new InvalidArgumentException('Unsupported input.');
}
$encryptedKey = $parts[1] === '' ? null : Base64UrlSafe::decodeNoPadding($parts[1]);
$iv = Base64UrlSafe::decodeNoPadding($parts[2]);
$ciphertext = Base64UrlSafe::decodeNoPadding($parts[3]);
$tag = Base64UrlSafe::decodeNoPadding($parts[4]);
return new JWE(
$ciphertext,
$iv,
$tag,
null,
[],
$sharedProtectedHeader,
$encodedSharedProtectedHeader,
[new Recipient([], $encryptedKey)]
);
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Unsupported input', $throwable->getCode(), $throwable);
}
}
private function checkHasNoAAD(JWE $jwe): void
{
if ($jwe->getAAD() !== null) {
throw new LogicException('This JWE has AAD and cannot be converted into Compact JSON.');
}
}
private function checkRecipientHasNoHeader(JWE $jwe, int $id): void
{
if (count($jwe->getSharedHeader()) !== 0 || count($jwe->getRecipient($id)->getHeader()) !== 0) {
throw new LogicException(
'This JWE has shared header parameters or recipient header parameters and cannot be converted into Compact JSON.'
);
}
}
private function checkHasSharedProtectedHeader(JWE $jwe): void
{
if (count($jwe->getSharedProtectedHeader()) === 0) {
throw new LogicException(
'This JWE does not have shared protected header parameters and cannot be converted into Compact JSON.'
);
}
}
}

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Serializer;
use InvalidArgumentException;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Encryption\JWE;
use Jose\Component\Encryption\Recipient;
use function array_key_exists;
use function count;
use function is_array;
final class JSONFlattenedSerializer implements JWESerializer
{
public const NAME = 'jwe_json_flattened';
public function displayName(): string
{
return 'JWE JSON Flattened';
}
public function name(): string
{
return self::NAME;
}
public function serialize(JWE $jwe, ?int $recipientIndex = null): string
{
if ($recipientIndex === null) {
$recipientIndex = 0;
}
$recipient = $jwe->getRecipient($recipientIndex);
$data = [
'ciphertext' => Base64UrlSafe::encodeUnpadded($jwe->getCiphertext() ?? ''),
'iv' => Base64UrlSafe::encodeUnpadded($jwe->getIV() ?? ''),
'tag' => Base64UrlSafe::encodeUnpadded($jwe->getTag() ?? ''),
];
if ($jwe->getAAD() !== null) {
$data['aad'] = Base64UrlSafe::encodeUnpadded($jwe->getAAD());
}
if (count($jwe->getSharedProtectedHeader()) !== 0) {
$data['protected'] = $jwe->getEncodedSharedProtectedHeader();
}
if (count($jwe->getSharedHeader()) !== 0) {
$data['unprotected'] = $jwe->getSharedHeader();
}
if (count($recipient->getHeader()) !== 0) {
$data['header'] = $recipient->getHeader();
}
if ($recipient->getEncryptedKey() !== null) {
$data['encrypted_key'] = Base64UrlSafe::encodeUnpadded($recipient->getEncryptedKey());
}
return JsonConverter::encode($data);
}
public function unserialize(string $input): JWE
{
$data = JsonConverter::decode($input);
if (! is_array($data)) {
throw new InvalidArgumentException('Unsupported input.');
}
$this->checkData($data);
$ciphertext = Base64UrlSafe::decodeNoPadding($data['ciphertext']);
$iv = Base64UrlSafe::decodeNoPadding($data['iv']);
$tag = Base64UrlSafe::decodeNoPadding($data['tag']);
$aad = array_key_exists('aad', $data) ? Base64UrlSafe::decodeNoPadding($data['aad']) : null;
[$encodedSharedProtectedHeader, $sharedProtectedHeader, $sharedHeader] = $this->processHeaders($data);
$encryptedKey = array_key_exists('encrypted_key', $data) ? Base64UrlSafe::decodeNoPadding(
$data['encrypted_key']
) : null;
$header = array_key_exists('header', $data) ? $data['header'] : [];
return new JWE(
$ciphertext,
$iv,
$tag,
$aad,
$sharedHeader,
$sharedProtectedHeader,
$encodedSharedProtectedHeader,
[new Recipient($header, $encryptedKey)]
);
}
private function checkData(?array $data): void
{
if ($data === null || ! isset($data['ciphertext']) || isset($data['recipients'])) {
throw new InvalidArgumentException('Unsupported input.');
}
}
private function processHeaders(array $data): array
{
$encodedSharedProtectedHeader = array_key_exists('protected', $data) ? $data['protected'] : null;
$sharedProtectedHeader = $encodedSharedProtectedHeader ? JsonConverter::decode(
Base64UrlSafe::decodeNoPadding($encodedSharedProtectedHeader)
) : [];
$sharedHeader = $data['unprotected'] ?? [];
return [$encodedSharedProtectedHeader, $sharedProtectedHeader, $sharedHeader];
}
}

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Serializer;
use InvalidArgumentException;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Encryption\JWE;
use Jose\Component\Encryption\Recipient;
use LogicException;
use function array_key_exists;
use function count;
use function is_array;
final class JSONGeneralSerializer implements JWESerializer
{
public const NAME = 'jwe_json_general';
public function displayName(): string
{
return 'JWE JSON General';
}
public function name(): string
{
return self::NAME;
}
public function serialize(JWE $jwe, ?int $recipientIndex = null): string
{
if ($jwe->countRecipients() === 0) {
throw new LogicException('No recipient.');
}
$data = [
'ciphertext' => Base64UrlSafe::encodeUnpadded($jwe->getCiphertext() ?? ''),
'iv' => Base64UrlSafe::encodeUnpadded($jwe->getIV() ?? ''),
'tag' => Base64UrlSafe::encodeUnpadded($jwe->getTag() ?? ''),
];
if ($jwe->getAAD() !== null) {
$data['aad'] = Base64UrlSafe::encodeUnpadded($jwe->getAAD());
}
if (count($jwe->getSharedProtectedHeader()) !== 0) {
$data['protected'] = $jwe->getEncodedSharedProtectedHeader();
}
if (count($jwe->getSharedHeader()) !== 0) {
$data['unprotected'] = $jwe->getSharedHeader();
}
$data['recipients'] = [];
foreach ($jwe->getRecipients() as $recipient) {
$temp = [];
if (count($recipient->getHeader()) !== 0) {
$temp['header'] = $recipient->getHeader();
}
if ($recipient->getEncryptedKey() !== null) {
$temp['encrypted_key'] = Base64UrlSafe::encodeUnpadded($recipient->getEncryptedKey());
}
$data['recipients'][] = $temp;
}
return JsonConverter::encode($data);
}
public function unserialize(string $input): JWE
{
$data = JsonConverter::decode($input);
if (! is_array($data)) {
throw new InvalidArgumentException('Unsupported input.');
}
$this->checkData($data);
$ciphertext = Base64UrlSafe::decodeNoPadding($data['ciphertext']);
$iv = Base64UrlSafe::decodeNoPadding($data['iv']);
$tag = Base64UrlSafe::decodeNoPadding($data['tag']);
$aad = array_key_exists('aad', $data) ? Base64UrlSafe::decodeNoPadding($data['aad']) : null;
[$encodedSharedProtectedHeader, $sharedProtectedHeader, $sharedHeader] = $this->processHeaders($data);
$recipients = [];
foreach ($data['recipients'] as $recipient) {
[$encryptedKey, $header] = $this->processRecipient($recipient);
$recipients[] = new Recipient($header, $encryptedKey);
}
return new JWE(
$ciphertext,
$iv,
$tag,
$aad,
$sharedHeader,
$sharedProtectedHeader,
$encodedSharedProtectedHeader,
$recipients
);
}
private function checkData(?array $data): void
{
if ($data === null || ! isset($data['ciphertext']) || ! isset($data['recipients'])) {
throw new InvalidArgumentException('Unsupported input.');
}
}
private function processRecipient(array $recipient): array
{
$encryptedKey = array_key_exists('encrypted_key', $recipient) ? Base64UrlSafe::decodeNoPadding(
$recipient['encrypted_key']
) : null;
$header = array_key_exists('header', $recipient) ? $recipient['header'] : [];
return [$encryptedKey, $header];
}
private function processHeaders(array $data): array
{
$encodedSharedProtectedHeader = array_key_exists('protected', $data) ? $data['protected'] : null;
$sharedProtectedHeader = $encodedSharedProtectedHeader ? JsonConverter::decode(
Base64UrlSafe::decodeNoPadding($encodedSharedProtectedHeader)
) : [];
$sharedHeader = array_key_exists('unprotected', $data) ? $data['unprotected'] : [];
return [$encodedSharedProtectedHeader, $sharedProtectedHeader, $sharedHeader];
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Serializer;
use Jose\Component\Encryption\JWE;
interface JWESerializer
{
/**
* The name of the serialization method.
*/
public function name(): string;
/**
* Display name of the serialization method.
*/
public function displayName(): string;
/**
* Converts a JWE into a string. If the JWE is designed for multiple recipients and the serializer only supports one
* recipient, the recipient index has to be set.
*/
public function serialize(JWE $jws, ?int $recipientIndex = null): string;
/**
* Loads data and return a JWE object. Throws an exception in case of failure.
*
* @param string $input A string that represents a JWE
*/
public function unserialize(string $input): JWE;
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Serializer;
use InvalidArgumentException;
use Jose\Component\Encryption\JWE;
class JWESerializerManager
{
/**
* @var JWESerializer[]
*/
private array $serializers = [];
/**
* @param JWESerializer[] $serializers
*/
public function __construct(iterable $serializers)
{
foreach ($serializers as $serializer) {
$this->add($serializer);
}
}
/**
* Return the serializer names supported by the manager.
*
* @return string[]
*/
public function names(): array
{
return array_keys($this->serializers);
}
/**
* Converts a JWE into a string. Throws an exception if none of the serializer was able to convert the input.
*/
public function serialize(string $name, JWE $jws, ?int $recipientIndex = null): string
{
if (! isset($this->serializers[$name])) {
throw new InvalidArgumentException(sprintf('Unsupported serializer "%s".', $name));
}
return $this->serializers[$name]->serialize($jws, $recipientIndex);
}
/**
* Loads data and return a JWE object. Throws an exception if none of the serializer was able to convert the input.
*
* @param string $input A string that represents a JWE
* @param string|null $name the name of the serializer if the input is unserialized
*/
public function unserialize(string $input, ?string &$name = null): JWE
{
foreach ($this->serializers as $serializer) {
try {
$jws = $serializer->unserialize($input);
$name = $serializer->name();
return $jws;
} catch (InvalidArgumentException) {
continue;
}
}
throw new InvalidArgumentException('Unsupported input.');
}
/**
* Adds a serializer to the manager.
*/
private function add(JWESerializer $serializer): void
{
$this->serializers[$serializer->name()] = $serializer;
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Serializer;
use InvalidArgumentException;
class JWESerializerManagerFactory
{
/**
* @var JWESerializer[]
*/
private array $serializers = [];
/**
* Creates a serializer manager factory using the given serializers.
*
* @param string[] $names
*/
public function create(array $names): JWESerializerManager
{
$serializers = [];
foreach ($names as $name) {
if (! isset($this->serializers[$name])) {
throw new InvalidArgumentException(sprintf('Unsupported serializer "%s".', $name));
}
$serializers[] = $this->serializers[$name];
}
return new JWESerializerManager($serializers);
}
/**
* Return the serializer names supported by the manager.
*
* @return string[]
*/
public function names(): array
{
return array_keys($this->serializers);
}
/**
* Returns all serializers supported by this factory.
*
* @return JWESerializer[]
*/
public function all(): array
{
return $this->serializers;
}
/**
* Adds a serializer to the manager.
*/
public function add(JWESerializer $serializer): void
{
$this->serializers[$serializer->name()] = $serializer;
}
}