diff --git a/composer.json b/composer.json index a687e7b9..02779f54 100644 --- a/composer.json +++ b/composer.json @@ -23,11 +23,14 @@ "php": ">=5.6.0", "ext-json": "*", "guzzlehttp/guzzle": "^6.2.1 || ^7.0.1", - "phpseclib/phpseclib": "^2.0.11", - "gree/jose": "^2.2.1" + "phpseclib/phpseclib": "^3.0.0" }, "autoload": { - "psr-4": { "Hyperwallet\\": "src/Hyperwallet", "ComposerScript\\" : "src/ComposerScript" } + "psr-4": { + "Hyperwallet\\": "src/Hyperwallet", + "ComposerScript\\": "src/ComposerScript", + "Services\\": "src/Services" + } }, "autoload-dev" : { "psr-4": { "Hyperwallet\\Tests\\" : "tests/Hyperwallet/Tests", "ComposerScript\\" : "src/ComposerScript" } @@ -36,8 +39,5 @@ "phpunit/phpunit": "^5.7 || ^7.0.0 || ^9.0", "phake/phake": "^2.3 || ^4.2", "php-coveralls/php-coveralls": "^2.5" - }, - "scripts": { - "post-install-cmd": "ComposerScript\\RsaOaep256AlgorithmInstaller::install" } } diff --git a/src/Hyperwallet/Util/HyperwalletEncryption.php b/src/Hyperwallet/Util/HyperwalletEncryption.php index 3cc5cbe6..f0ed2ee5 100644 --- a/src/Hyperwallet/Util/HyperwalletEncryption.php +++ b/src/Hyperwallet/Util/HyperwalletEncryption.php @@ -1,29 +1,24 @@ clientPrivateKeySetLocation = $clientPrivateKeySetLocation; $this->hyperwalletKeySetLocation = $hyperwalletKeySetLocation; $this->encryptionAlgorithm = $encryptionAlgorithm; $this->signAlgorithm = $signAlgorithm; $this->encryptionMethod = $encryptionMethod; $this->jwsExpirationMinutes = $jwsExpirationMinutes; - file_put_contents($this->getVendorPath() . "/gree/jose/src/JOSE/JWE.php", file_get_contents(__DIR__ . "/../../JWE")); } /** @@ -111,7 +110,8 @@ public function __construct($clientPrivateKeySetLocation, $hyperwalletKeySetLoca * * @throws HyperwalletException */ - public function encrypt($body) { + public function encrypt($body) + { $privateJwsKey = $this->getPrivateJwsKey(); $jws = new JOSE_JWS(new JOSE_JWT($body)); $jws->header['exp'] = $this->getSignatureExpirationTime(); @@ -133,7 +133,8 @@ public function encrypt($body) { * * @throws HyperwalletException */ - public function decrypt($body) { + public function decrypt($body) + { $privateJweKey = $this->getPrivateJweKey(); $jwe = JOSE_JWT::decode($body); $decryptedBody = $jwe->decrypt($privateJweKey); @@ -152,7 +153,8 @@ public function decrypt($body) { * * @throws HyperwalletException */ - private function getPrivateJwsKey() { + private function getPrivateJwsKey() + { $privateKeyData = $this->getJwk($this->clientPrivateKeySetLocation, $this->signAlgorithm); $this->jwsKid = $privateKeyData['kid']; return $this->getPrivateKey($privateKeyData); @@ -165,7 +167,8 @@ private function getPrivateJwsKey() { * * @throws HyperwalletException */ - private function getPublicJweKey() { + private function getPublicJweKey() + { $publicKeyData = $this->getJwk($this->hyperwalletKeySetLocation, $this->encryptionAlgorithm); $this->jweKid = $publicKeyData['kid']; return $this->getPublicKey($this->convertPrivateKeyToPublic($publicKeyData)); @@ -178,7 +181,8 @@ private function getPublicJweKey() { * * @throws HyperwalletException */ - private function getPrivateJweKey() { + private function getPrivateJweKey() + { $privateKeyData = $this->getJwk($this->clientPrivateKeySetLocation, $this->encryptionAlgorithm); return $this->getPrivateKey($privateKeyData); } @@ -190,7 +194,8 @@ private function getPrivateJweKey() { * * @throws HyperwalletException */ - private function getPublicJwsKey() { + private function getPublicJwsKey() + { $publicKeyData = $this->getJwk($this->hyperwalletKeySetLocation, $this->signAlgorithm); return $this->getPublicKey($this->convertPrivateKeyToPublic($publicKeyData)); } @@ -201,31 +206,24 @@ private function getPublicJwsKey() { * @param array $privateKeyData The JWK key data * @return RSA */ - private function getPrivateKey($privateKeyData) { - $n = $this->keyParamToBigInteger($privateKeyData['n']); - $e = $this->keyParamToBigInteger($privateKeyData['e']); - $d = $this->keyParamToBigInteger($privateKeyData['d']); - $p = $this->keyParamToBigInteger($privateKeyData['p']); - $q = $this->keyParamToBigInteger($privateKeyData['q']); - $qi = $this->keyParamToBigInteger($privateKeyData['qi']); - $dp = $this->keyParamToBigInteger($privateKeyData['dp']); - $dq = $this->keyParamToBigInteger($privateKeyData['dq']); - $primes = array($p, $q); - $exponents = array($dp, $dq); - $coefficients = array($qi, $qi); - array_unshift($primes, "phoney"); - unset($primes[0]); - array_unshift($exponents, "phoney"); - unset($exponents[0]); - array_unshift($coefficients, "phoney"); - unset($coefficients[0]); + private function getPrivateKey($privateKeyData) + { + $pemData = RSA::load([ + 'e' => $this->keyParamToBigInteger($privateKeyData['e']), + 'n' => $this->keyParamToBigInteger($privateKeyData['n']), + 'd' => $this->keyParamToBigInteger($privateKeyData['d']), + 'p' => $this->keyParamToBigInteger($privateKeyData['p']), + 'q' => $this->keyParamToBigInteger($privateKeyData['q']), + 'dp' => $this->keyParamToBigInteger($privateKeyData['dp']), + 'dq' => $this->keyParamToBigInteger($privateKeyData['dq']), + 'qi' => $this->keyParamToBigInteger($privateKeyData['qi']), + ]); + + $privateKey = RSA::loadPrivateKey($pemData->toString('PKCS1')); - $pemData = (new RSA())->_convertPrivateKey($n, $e, $d, $primes, $exponents, $coefficients); - $privateKey = new RSA(); - $privateKey->loadKey($pemData); if ($privateKeyData['alg'] == 'RSA-OAEP-256') { - $privateKey->setHash('sha256'); - $privateKey->setMGFHash('sha256'); +// $privateKey->setHash('sha256'); +// $privateKey->setMGFHash('sha256'); } return $privateKey; } @@ -236,8 +234,9 @@ private function getPrivateKey($privateKeyData) { * @param string $param base 64 encoded string * @return BigInteger */ - private function keyParamToBigInteger($param) { - return new BigInteger('0x' . bin2hex(JOSE_URLSafeBase64::decode($param)), 16); + private function keyParamToBigInteger($param) + { + return new BigInteger('0x' . bin2hex(URLSafeBase64::decode($param)), 16); } /** @@ -246,12 +245,13 @@ private function keyParamToBigInteger($param) { * @param array $publicKeyData The JWK key data * @return RSA */ - private function getPublicKey($publicKeyData) { + private function getPublicKey($publicKeyData) + { $publicKeyRaw = new JOSE_JWK($publicKeyData); $publicKey = $publicKeyRaw->toKey(); if ($publicKeyData['alg'] == 'RSA-OAEP-256') { - $publicKey->setHash('sha256'); - $publicKey->setMGFHash('sha256'); +// $publicKey->setHash('sha256'); +// $publicKey->setMGFHash('sha256'); } return $publicKey; } @@ -265,8 +265,9 @@ private function getPublicKey($publicKeyData) { * * @throws HyperwalletException */ - private function getJwk($keySetLocation, $alg) { - if (filter_var($keySetLocation, FILTER_VALIDATE_URL) === FALSE) { + private function getJwk($keySetLocation, $alg) + { + if (filter_var($keySetLocation, FILTER_VALIDATE_URL) === false) { if (!file_exists($keySetLocation)) { throw new HyperwalletException("Wrong JWK key set location path = " . $keySetLocation); } @@ -283,8 +284,9 @@ private function getJwk($keySetLocation, $alg) { * * @throws HyperwalletException */ - private function findJwkByAlgorithm($jwkSetArray, $alg) { - foreach($jwkSetArray['keys'] as $jwk) { + private function findJwkByAlgorithm($jwkSetArray, $alg) + { + foreach ($jwkSetArray['keys'] as $jwk) { if ($alg == $jwk['alg']) { return $jwk; } @@ -298,7 +300,8 @@ private function findJwkByAlgorithm($jwkSetArray, $alg) { * @param string $jwk JWK key * @return array */ - private function convertPrivateKeyToPublic($jwk) { + private function convertPrivateKeyToPublic($jwk) + { if (isset($jwk['d'])) { unset($jwk['d']); } @@ -325,7 +328,8 @@ private function convertPrivateKeyToPublic($jwk) { * * @return integer */ - private function getSignatureExpirationTime() { + private function getSignatureExpirationTime() + { date_default_timezone_set("UTC"); $secondsInMinute = 60; return time() + $this->jwsExpirationMinutes * $secondsInMinute; @@ -338,15 +342,16 @@ private function getSignatureExpirationTime() { * * @throws HyperwalletException */ - public function checkJwsExpiration($header) { - if(!isset($header['exp'])) { + public function checkJwsExpiration($header) + { + if (!isset($header['exp'])) { throw new HyperwalletException('While trying to verify JWS signature no [exp] header is found'); } $exp = $header['exp']; - if(!is_numeric($exp)) { + if (!is_numeric($exp)) { throw new HyperwalletException('Wrong value in [exp] header of JWS signature, must be integer'); } - if((int)time() > (int)$exp) { + if ((int)time() > (int)$exp) { throw new HyperwalletException('JWS signature has expired, checked by [exp] JWS header'); } } @@ -358,10 +363,11 @@ public function checkJwsExpiration($header) { * * @throws HyperwalletException */ - public function getVendorPath() { + public function getVendorPath() + { $reflector = new \ReflectionClass(ClassLoader::class); - $vendorPath = preg_replace('/^(.*)\/composer\/ClassLoader\.php$/', '$1', $reflector->getFileName() ); - if($vendorPath && is_dir($vendorPath)) { + $vendorPath = preg_replace('/^(.*)\/composer\/ClassLoader\.php$/', '$1', $reflector->getFileName()); + if ($vendorPath && is_dir($vendorPath)) { return $vendorPath . '/'; } throw new HyperwalletException('Failed to find a vendor path'); diff --git a/src/Services/Jose/Exception.php b/src/Services/Jose/Exception.php new file mode 100644 index 00000000..90271d28 --- /dev/null +++ b/src/Services/Jose/Exception.php @@ -0,0 +1,8 @@ +header['typ']); } + /** + * @throws EncryptionFailed + * @throws UnexpectedAlgorithm + * @throws DecryptionFailed + */ function encrypt($public_key_or_secret, $algorithm = 'RSA1_5', $encryption_method = 'A128CBC-HS256') { $this->header['alg'] = $algorithm; $this->header['enc'] = $encryption_method; @@ -44,6 +54,10 @@ function encrypt($public_key_or_secret, $algorithm = 'RSA1_5', $encryption_metho return $this; } + /** + * @throws UnexpectedAlgorithm + * @throws DecryptionFailed + */ function decrypt($private_key_or_secret) { $this->decryptContentEncryptionKey($private_key_or_secret); $this->deriveEncryptionAndMacKeys(); @@ -68,10 +82,8 @@ private function rsa($public_or_private_key, $padding_mode) { } else if ($public_or_private_key instanceof RSA) { $rsa = $public_or_private_key; } else { - $rsa = new RSA(); - $rsa->loadKey($public_or_private_key); + } - $rsa->setEncryptionMode($padding_mode); return $rsa; } @@ -79,13 +91,13 @@ private function cipher() { switch ($this->header['enc']) { case 'A128GCM': case 'A256GCM': - throw new JOSE_Exception_UnexpectedAlgorithm('Algorithm not supported'); + throw new UnexpectedAlgorithm('Algorithm not supported'); case 'A128CBC-HS256': case 'A256CBC-HS512': - $cipher = new AES(AES::MODE_CBC); + $cipher = new AES('cbc'); break; default: - throw new JOSE_Exception_UnexpectedAlgorithm('Unknown algorithm'); + throw new UnexpectedAlgorithm('Unknown algorithm'); } switch ($this->header['enc']) { case 'A128GCM': @@ -94,10 +106,10 @@ private function cipher() { break; case 'A256GCM': case 'A256CBC-HS512': - $cipher->setBlockLength(256); +// $cipher->setBlockLength(256); break; default: - throw new JOSE_Exception_UnexpectedAlgorithm('Unknown algorithm'); + throw new UnexpectedAlgorithm('Unknown algorithm'); } return $cipher; } @@ -110,15 +122,14 @@ private function generateIv() { switch ($this->header['enc']) { case 'A128GCM': case 'A128CBC-HS256': - case 'A256CBC-HS512': $this->iv = $this->generateRandomBytes(128 / 8); break; case 'A256GCM': - //case 'A256CBC-HS512': - $this->iv = $this->generateRandomBytes(256 / 8); + case 'A256CBC-HS512': + $this->iv = $this->generateRandomBytes(128 / 8); break; default: - throw new JOSE_Exception_UnexpectedAlgorithm('Unknown algorithm'); + throw new UnexpectedAlgorithm('Unknown algorithm'); } } @@ -136,7 +147,7 @@ private function generateContentEncryptionKey($public_key_or_secret) { $this->content_encryption_key = $this->generateRandomBytes(512 / 8); break; default: - throw new JOSE_Exception_UnexpectedAlgorithm('Unknown algorithm'); + throw new UnexpectedAlgorithm('Unknown algorithm'); } } } @@ -147,8 +158,8 @@ private function encryptContentEncryptionKey($public_key_or_secret) { $rsa = $this->rsa($public_key_or_secret, RSA::ENCRYPTION_PKCS1); $this->jwe_encrypted_key = $rsa->encrypt($this->content_encryption_key); break; - case 'RSA-OAEP-256': case 'RSA-OAEP': + case 'RSA-OAEP-256': $rsa = $this->rsa($public_key_or_secret, RSA::ENCRYPTION_OAEP); $this->jwe_encrypted_key = $rsa->encrypt($this->content_encryption_key); break; @@ -160,12 +171,12 @@ private function encryptContentEncryptionKey($public_key_or_secret) { case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A256KW': - throw new JOSE_Exception_UnexpectedAlgorithm('Algorithm not supported'); + throw new UnexpectedAlgorithm('Algorithm not supported'); default: - throw new JOSE_Exception_UnexpectedAlgorithm('Unknown algorithm'); + throw new UnexpectedAlgorithm('Unknown algorithm'); } if (!$this->jwe_encrypted_key) { - throw new JOSE_Exception_EncryptionFailed('Master key encryption failed'); + throw new EncryptionFailed('Master key encryption failed'); } } @@ -190,9 +201,9 @@ private function decryptContentEncryptionKey($private_key_or_secret) { case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A256KW': - throw new JOSE_Exception_UnexpectedAlgorithm('Algorithm not supported'); + throw new UnexpectedAlgorithm('Algorithm not supported'); default: - throw new JOSE_Exception_UnexpectedAlgorithm('Unknown algorithm'); + throw new UnexpectedAlgorithm('Unknown algorithm'); } if (!$this->content_encryption_key) { # NOTE: @@ -217,10 +228,10 @@ private function deriveEncryptionAndMacKeys() { $this->deriveEncryptionAndMacKeysCBC(512); break; default: - throw new JOSE_Exception_UnexpectedAlgorithm('Unknown algorithm'); + throw new UnexpectedAlgorithm('Unknown algorithm'); } if (!$this->encryption_key || !$this->mac_key) { - throw new JOSE_Exception_DecryptionFailed('Encryption/Mac key derivation failed'); + throw new DecryptionFailed('Encryption/Mac key derivation failed'); } } @@ -235,7 +246,7 @@ private function encryptCipherText() { $cipher->setIV($this->iv); $this->cipher_text = $cipher->encrypt($this->plain_text); if (!$this->cipher_text) { - throw new JOSE_Exception_DecryptionFailed('Payload encryption failed'); + throw new DecryptionFailed('Payload encryption failed'); } } @@ -245,7 +256,7 @@ private function decryptCipherText() { $cipher->setIV($this->iv); $this->plain_text = $cipher->decrypt($this->cipher_text); if (!$this->plain_text) { - throw new JOSE_Exception_DecryptionFailed('Payload decryption failed'); + throw new DecryptionFailed('Payload decryption failed'); } } @@ -257,13 +268,13 @@ private function calculateAuthenticationTag($use_raw = false) { switch ($this->header['enc']) { case 'A128GCM': case 'A256GCM': - throw new JOSE_Exception_UnexpectedAlgorithm('Algorithm not supported'); + throw new UnexpectedAlgorithm('Algorithm not supported'); case 'A128CBC-HS256': return $this->calculateAuthenticationTagCBC(256); case 'A256CBC-HS512': return $this->calculateAuthenticationTagCBC(512); default: - throw new JOSE_Exception_UnexpectedAlgorithm('Unknown algorithm'); + throw new UnexpectedAlgorithm('Unknown algorithm'); } } @@ -290,7 +301,7 @@ private function checkAuthenticationTag() { if (hash_equals($this->authentication_tag, $this->calculateAuthenticationTag())) { return true; } else { - throw new JOSE_Exception_UnexpectedAlgorithm('Invalid authentication tag'); + throw new UnexpectedAlgorithm('Invalid authentication tag'); } } } diff --git a/src/Services/Jose/JOSE_JWK.php b/src/Services/Jose/JOSE_JWK.php new file mode 100644 index 00000000..0bd3d997 --- /dev/null +++ b/src/Services/Jose/JOSE_JWK.php @@ -0,0 +1,110 @@ +components = $components; + if (!array_key_exists('kid', $this->components)) { + $this->components['kid'] = $this->thumbprint(); + } + } + + private function keyParamToBigInteger($param) + { + return new BigInteger('0x' . bin2hex(URLSafeBase64::decode($param)), 16); + } + + function toKey() + { + switch ($this->components['kty']) { + case 'RSA': + $pemData = RSA::load([ + 'e' => $this->keyParamToBigInteger($this->components['e']), + 'n' => $this->keyParamToBigInteger($this->components['n']), + ]); + + if (array_key_exists('d', $this->components)) { + throw new UnexpectedAlgorithm('RSA private key isn\'t supported'); + } else { + $pem_string = RSA::loadPublicKey($pemData->toString('PKCS1')); + } + return RSA::load($pem_string); + default: + throw new UnexpectedAlgorithm('Unknown key type'); + } + } + + function thumbprint($hash_algorithm = 'sha256') + { + $hash = new Hash($hash_algorithm); + return URLSafeBase64::encode( + $hash->hash( + json_encode($this->normalized()) + ) + ); + } + + private function normalized() + { + switch ($this->components['kty']) { + case 'RSA': + return array( + 'e' => $this->components['e'], + 'kty' => $this->components['kty'], + 'n' => $this->components['n'] + ); + default: + throw new UnexpectedAlgorithm('Unknown key type'); + } + } + + function toString() + { + return json_encode($this->components); + } + + function __toString() + { + return $this->toString(); + } + + static function encode($key, $extra_components = array()) + { + switch (get_class($key)) { + case 'phpseclib\Crypt\RSA': + $components = array( + 'kty' => 'RSA', + 'e' => URLSafeBase64::encode($key->publicExponent->toBytes()), + 'n' => URLSafeBase64::encode($key->modulus->toBytes()) + ); + if ($key->exponent != $key->publicExponent) { + $components = array_merge($components, array( + 'd' => URLSafeBase64::encode($key->exponent->toBytes()) + )); + } + return new self(array_merge($components, $extra_components)); + default: + throw new UnexpectedAlgorithm('Unknown key type'); + } + } + + static function decode($components) + { + $jwk = new self($components); + return $jwk->toKey(); + } +} diff --git a/src/Services/Jose/JOSE_JWS.php b/src/Services/Jose/JOSE_JWS.php new file mode 100644 index 00000000..401265fe --- /dev/null +++ b/src/Services/Jose/JOSE_JWS.php @@ -0,0 +1,154 @@ +header = $jwt->header; + $this->claims = $jwt->claims; + $this->signature = $jwt->signature; + $this->raw = $jwt->raw; + } + + function toJson($syntax = 'flattened') { + if ($syntax == 'flattened') { + $components = array( + 'protected' => $this->compact((object) $this->header), + 'payload' => $this->compact((object) $this->claims), + 'signature' => $this->compact($this->signature) + ); + } else { + $components = array( + 'payload' => $this->compact((object) $this->claims), + 'signatures' => array( + 'protected' => $this->compact((object) $this->header), + 'signature' => $this->compact($this->signature) + ) + ); + } + return json_encode($components); + } + + function sign($private_key_or_secret, $algorithm = 'HS256') { + $this->header['alg'] = $algorithm; + if ( + $private_key_or_secret instanceof JOSE_JWK && + !array_key_exists('kid', $this->header) && + array_key_exists('kid', $private_key_or_secret->components) + ) { + $this->header['kid'] = $private_key_or_secret->components['kid']; + } + $this->signature = $this->_sign($private_key_or_secret); + if (!$this->signature) { + throw new Exception('Signing failed because of unknown reason'); + } + return $this; + } + + function verify($public_key_or_secret, $alg = null) { + if ($this->_verify($public_key_or_secret, $alg)) { + return $this; + } else { + throw new VerificationFailed('Signature verification failed'); + } + } + + private function rsa($public_or_private_key, $padding_mode) { + if ($public_or_private_key instanceof JOSE_JWK) { + $rsa = $public_or_private_key->toKey(); + } else if ($public_or_private_key instanceof RSA) { + $rsa = $public_or_private_key; + } else { + $rsa = RSA::loadPrivateKey($public_or_private_key); + } + return $rsa; + } + + private function digest() { + switch ($this->header['alg']) { + case 'HS256': + case 'RS256': + case 'ES256': + case 'PS256': + return 'sha256'; + case 'HS384': + case 'RS384': + case 'ES384': + case 'PS384': + return 'sha384'; + case 'HS512': + case 'RS512': + case 'ES512': + case 'PS512': + return 'sha512'; + default: + throw new UnexpectedAlgorithm('Unknown algorithm'); + } + } + + private function _sign($private_key_or_secret) { + $signature_base_string = implode('.', array( + $this->compact((object) $this->header), + $this->compact((object) $this->claims) + )); + switch ($this->header['alg']) { + case 'HS256': + case 'HS384': + case 'HS512': + return hash_hmac($this->digest(), $signature_base_string, $private_key_or_secret, true); + case 'RS256': + case 'RS384': + case 'RS512': + return $this->rsa($private_key_or_secret, RSA::SIGNATURE_PKCS1)->sign($signature_base_string); + case 'ES256': + case 'ES384': + case 'ES512': + throw new UnexpectedAlgorithm('Algorithm not supported'); + case 'PS256': + case 'PS384': + case 'PS512': + return $this->rsa($private_key_or_secret, RSA::SIGNATURE_PSS)->sign($signature_base_string); + default: + throw new UnexpectedAlgorithm('Unknown algorithm'); + } + } + + private function _verify($public_key_or_secret, $expected_alg = null) { + $segments = explode('.', $this->raw); + $signature_base_string = implode('.', array($segments[0], $segments[1])); + if (!$expected_alg) { + $expected_alg = $this->header['alg']; + $using_autodetected_alg = true; + } + switch ($expected_alg) { + case 'HS256': + case 'HS384': + case 'HS512': + if (isset($using_autodetected_alg)) { + throw new UnexpectedAlgorithm( + 'HMAC algs MUST be explicitly specified as $expected_alg' + ); + } + $hmac_hash = hash_hmac($this->digest(), $signature_base_string, $public_key_or_secret, true); + return hash_equals($this->signature, $hmac_hash); + case 'RS256': + case 'RS384': + case 'RS512': + return $this->rsa($public_key_or_secret, RSA::SIGNATURE_PKCS1)->verify($signature_base_string, $this->signature); + case 'ES256': + case 'ES384': + case 'ES512': + throw new UnexpectedAlgorithm('Algorithm not supported'); + case 'PS256': + case 'PS384': + case 'PS512': + return $this->rsa($public_key_or_secret, RSA::SIGNATURE_PSS)->verify($signature_base_string, $this->signature); + default: + throw new UnexpectedAlgorithm('Unknown algorithm'); + } + } +} diff --git a/src/Services/Jose/JOSE_JWT.php b/src/Services/Jose/JOSE_JWT.php new file mode 100644 index 00000000..3a21eef6 --- /dev/null +++ b/src/Services/Jose/JOSE_JWT.php @@ -0,0 +1,106 @@ + 'JWT', + 'alg' => 'none' + ); + var $claims = array(); + var $signature = ''; + var $raw; + + function __construct($claims = array()) { + $this->claims = $claims; + } + + function toString() { + return implode('.', array( + $this->compact((object) $this->header), + $this->compact((object) $this->claims), + $this->compact($this->signature) + )); + } + + function __toString() { + return $this->toString(); + } + + function sign($private_key_or_secret, $algorithm = 'HS256') { + $jws = $this->toJWS(); + $jws->sign($private_key_or_secret, $algorithm); + return $jws; + } + + function verify($public_key_or_secret, $alg = null) { + $jws = $this->toJWS(); + $jws->verify($public_key_or_secret, $alg); + return $jws; + } + + function encrypt($public_key_or_secret, $algorithm = 'RSA1_5', $encryption_method = 'A128CBC-HS256') { + $jwe = new JOSE_JWE($this); + $jwe->encrypt($public_key_or_secret, $algorithm, $encryption_method); + return $jwe; + } + + static function encode($claims) { + return new self($claims); + } + + static function decode($jwt_string) { + $segments = explode('.', $jwt_string); + switch (count($segments)) { + case 3: + $jwt = new self(); + $jwt->raw = $jwt_string; + $jwt->header = (array) $jwt->extract($segments[0]); + $jwt->claims = (array) $jwt->extract($segments[1]); + $jwt->signature = $jwt->extract($segments[2], 'as_binary'); + return $jwt; + case 5: + $jwe = new JOSE_JWE($jwt_string); + $jwe->auth_data = $segments[0]; + $jwe->header = (array) $jwe->extract($segments[0]); + $jwe->jwe_encrypted_key = $jwe->extract($segments[1], 'as_binary'); + $jwe->iv = $jwe->extract($segments[2], 'as_binary'); + $jwe->cipher_text = $jwe->extract($segments[3], 'as_binary'); + $jwe->authentication_tag = $jwe->extract($segments[4], 'as_binary'); + return $jwe; + default: + throw new InvalidFormat('JWT should have exact 3 or 5 segments'); + } + } + + protected function compact($segment) { + if (is_object($segment)) { + $stringified = str_replace("\/", "/", json_encode($segment)); + } else { + $stringified = $segment; + } + if ($stringified === 'null' && $segment !== null) { // shouldn't happen, just for safe + throw new InvalidFormat('Compact seriarization failed'); + } + return URLSafeBase64::encode($stringified); + } + + protected function extract($segment, $as_binary = false) { + $stringified = URLSafeBase64::decode($segment); + if ($as_binary) { + $extracted = $stringified; + } else { + $extracted = json_decode($stringified); + if ($stringified !== 'null' && $extracted === null) { + throw new InvalidFormat('Compact de-serialization failed'); + } + } + return $extracted; + } + + private function toJWS() { + return new JOSE_JWS($this); + } +} diff --git a/src/Services/Jose/URLSafeBase64.php b/src/Services/Jose/URLSafeBase64.php new file mode 100644 index 00000000..8c365a68 --- /dev/null +++ b/src/Services/Jose/URLSafeBase64.php @@ -0,0 +1,18 @@ +