2019-08-01 14:47:12 +05:30
|
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace api\components\Tokens;
|
|
|
|
|
2019-08-02 05:59:20 +05:30
|
|
|
use Carbon\Carbon;
|
2019-08-01 14:47:12 +05:30
|
|
|
use Exception;
|
|
|
|
use Lcobucci\JWT\Builder;
|
|
|
|
use Lcobucci\JWT\Parser;
|
|
|
|
use Lcobucci\JWT\Token;
|
2019-12-11 16:46:11 +05:30
|
|
|
use ParagonIE\ConstantTime\Base64UrlSafe;
|
2019-08-02 21:46:34 +05:30
|
|
|
use Webmozart\Assert\Assert;
|
2019-08-01 14:47:12 +05:30
|
|
|
use yii\base\Component as BaseComponent;
|
|
|
|
|
|
|
|
class Component extends BaseComponent {
|
|
|
|
|
|
|
|
private const PREFERRED_ALGORITHM = 'ES256';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
2019-12-11 16:54:31 +05:30
|
|
|
* @deprecated In earlier versions of the application, JWT were signed by a synchronous encryption algorithm.
|
|
|
|
* Now asynchronous encryption is used instead, and this logic is saved for a transitional period.
|
|
|
|
* I think it can be safely removed, but I'll not do it yet, because at the time of writing the comment
|
|
|
|
* there were enough changes in the code already.
|
2019-08-01 14:47:12 +05:30
|
|
|
*/
|
|
|
|
public $hmacKey;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
public $publicKeyPath;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
public $privateKeyPath;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string|null
|
|
|
|
*/
|
|
|
|
public $privateKeyPass;
|
|
|
|
|
2019-12-04 23:40:15 +05:30
|
|
|
/**
|
2019-12-10 04:08:09 +05:30
|
|
|
* @var string
|
2019-12-04 23:40:15 +05:30
|
|
|
*/
|
|
|
|
public $encryptionKey;
|
|
|
|
|
2019-08-01 14:47:12 +05:30
|
|
|
/**
|
|
|
|
* @var AlgorithmsManager|null
|
|
|
|
*/
|
|
|
|
private $algorithmManager;
|
|
|
|
|
2019-08-02 21:46:34 +05:30
|
|
|
public function init(): void {
|
|
|
|
parent::init();
|
|
|
|
Assert::notEmpty($this->hmacKey, 'hmacKey must be set');
|
|
|
|
Assert::notEmpty($this->privateKeyPath, 'privateKeyPath must be set');
|
|
|
|
Assert::notEmpty($this->publicKeyPath, 'publicKeyPath must be set');
|
2019-12-04 23:40:15 +05:30
|
|
|
Assert::notEmpty($this->encryptionKey, 'encryptionKey must be set');
|
2019-08-02 21:46:34 +05:30
|
|
|
}
|
|
|
|
|
2019-08-01 14:47:12 +05:30
|
|
|
public function create(array $payloads = [], array $headers = []): Token {
|
2019-08-02 05:59:20 +05:30
|
|
|
$now = Carbon::now();
|
2019-12-06 21:01:04 +05:30
|
|
|
$builder = (new Builder())->issuedAt($now->getTimestamp());
|
|
|
|
if (isset($payloads['exp'])) {
|
|
|
|
$builder->expiresAt($payloads['exp']);
|
|
|
|
}
|
|
|
|
|
2019-08-01 14:47:12 +05:30
|
|
|
foreach ($payloads as $claim => $value) {
|
2019-12-04 23:40:15 +05:30
|
|
|
$builder->withClaim($claim, $this->prepareValue($value));
|
2019-08-01 14:47:12 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($headers as $claim => $value) {
|
2019-12-04 23:40:15 +05:30
|
|
|
$builder->withHeader($claim, $this->prepareValue($value));
|
2019-08-01 14:47:12 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
/** @noinspection PhpUnhandledExceptionInspection */
|
|
|
|
$algorithm = $this->getAlgorithmManager()->get(self::PREFERRED_ALGORITHM);
|
|
|
|
|
|
|
|
return $builder->getToken($algorithm->getSigner(), $algorithm->getPrivateKey());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $jwt
|
|
|
|
*
|
|
|
|
* @return Token
|
|
|
|
* @throws \InvalidArgumentException
|
|
|
|
*/
|
|
|
|
public function parse(string $jwt): Token {
|
|
|
|
return (new Parser())->parse($jwt);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function verify(Token $token): bool {
|
|
|
|
try {
|
|
|
|
$algorithm = $this->getAlgorithmManager()->get($token->getHeader('alg'));
|
|
|
|
return $token->verify($algorithm->getSigner(), $algorithm->getPublicKey());
|
|
|
|
} catch (Exception $e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-05 22:07:46 +05:30
|
|
|
public function encryptValue(string $rawValue): string {
|
|
|
|
/** @noinspection PhpUnhandledExceptionInspection */
|
|
|
|
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
2019-12-11 16:46:11 +05:30
|
|
|
$cipher = Base64UrlSafe::encodeUnpadded($nonce . sodium_crypto_secretbox($rawValue, $nonce, $this->encryptionKey));
|
2019-12-05 22:07:46 +05:30
|
|
|
sodium_memzero($rawValue);
|
|
|
|
|
|
|
|
return $cipher;
|
|
|
|
}
|
|
|
|
|
2019-12-04 23:40:15 +05:30
|
|
|
public function decryptValue(string $encryptedValue): string {
|
2019-12-11 16:46:11 +05:30
|
|
|
$decoded = Base64UrlSafe::decode($encryptedValue);
|
2019-12-05 22:07:46 +05:30
|
|
|
Assert::true(mb_strlen($decoded, '8bit') >= (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES));
|
|
|
|
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
|
|
|
|
$cipherText = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
|
|
|
|
|
|
|
|
$rawValue = sodium_crypto_secretbox_open($cipherText, $nonce, $this->encryptionKey);
|
|
|
|
Assert::true($rawValue !== false);
|
|
|
|
sodium_memzero($cipherText);
|
|
|
|
|
|
|
|
return $rawValue;
|
2019-12-04 23:40:15 +05:30
|
|
|
}
|
|
|
|
|
2019-08-01 14:47:12 +05:30
|
|
|
private function getAlgorithmManager(): AlgorithmsManager {
|
|
|
|
if ($this->algorithmManager === null) {
|
|
|
|
$this->algorithmManager = new AlgorithmsManager([
|
|
|
|
new Algorithms\HS256($this->hmacKey),
|
|
|
|
new Algorithms\ES256(
|
|
|
|
"file://{$this->privateKeyPath}",
|
|
|
|
$this->privateKeyPass,
|
|
|
|
"file://{$this->publicKeyPath}"
|
|
|
|
),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->algorithmManager;
|
|
|
|
}
|
|
|
|
|
2019-12-04 23:40:15 +05:30
|
|
|
private function prepareValue($value) {
|
|
|
|
if ($value instanceof EncryptedValue) {
|
2019-12-05 22:07:46 +05:30
|
|
|
return $this->encryptValue($value->getValue());
|
2019-12-04 23:40:15 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
return $value;
|
|
|
|
}
|
|
|
|
|
2019-08-01 14:47:12 +05:30
|
|
|
}
|