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;
}