function testPBES2Encrypt() { $cek = Util::base64url_decode('bxsZNEIdFE5csDjwQdBScKGDJDfK7LmsgReZwsMw_bY'); $password = '******'; $keys = $this->getKeySet($password); $stub = $this->getMockBuilder('SimpleJWT\\Crypt\\PBES2')->setMethods(array('generateSaltInput'))->setConstructorArgs(array('PBES2-HS256+A128KW'))->getMock(); $stub->method('generateSaltInput')->willReturn(Util::base64url_decode('2WCTcJZ1Rvd_CJuJripQ1w')); $stub->setIterations(4096); $headers = array('alg' => 'PBES2-HS256+A128KW'); $encrypted_key = $stub->encryptKey($cek, $keys, $headers); $this->assertEquals(4096, $headers['p2c']); $this->assertEquals('2WCTcJZ1Rvd_CJuJripQ1w', $headers['p2s']); $this->assertEquals('TrqXOwuNUfDV9VPTNbyGvEJ9JMjefAVn-TR1uIxR9p6hsRQh9Tk7BA', $encrypted_key); }
/** * Decodes a serialised JWT. * * @param string $token the serialised JWT * @param SimpleJWT\Keys\KeySet $keys the key set containing the key to verify the * JWT's signature * @param string $expected_alg the expected value of the `alg` parameter, which * should be agreed between the parties out-of-band * @param string $kid the ID of the key to use to verify the signature. If null, this * is automatically retrieved * @param bool|array $skip_validation an array of headers or claims that * should be ignored as part of the validation process (e.g. if expired tokens * are to be accepted), or false if all validation * is to be performed. * @param string $format the JWT serialisation format * @return JWT the decoded JWT * @throws InvalidTokenException if the token is invalid for any reason */ public static function decode($token, $keys, $expected_alg, $kid = null, $skip_validation = array(), $format = self::COMPACT_FORMAT) { if ($skip_validation === false) { $skip_validation = array(); } switch ($format) { case self::COMPACT_FORMAT: $parts = explode('.', $token, 3); if (count($parts) != 3) { throw new InvalidTokenException('Cannot decode compact serialisation', InvalidTokenException::TOKEN_PARSE_ERROR); } list($protected, $payload, $signature) = $parts; break; case self::JSON_FORMAT: $obj = json_decode($token, true); if ($obj == null) { throw new InvalidTokenException('Cannot decode JSON', InvalidTokenException::TOKEN_PARSE_ERROR); } $payload = $obj['payload']; if (isset($obj['signatures'])) { foreach ($obj['signatures'] as $signature_obj) { if (isset($signature_obj['header']['kid'])) { $target_kid = $signature_obj['header']['kid']; if ($target_kid == $kid || $keys->getById($target_kid) != null) { $unprotected = $signature_obj['header']; $protected = $signature_obj['protected']; $signature = $signature_obj['signature']; break; } } throw new InvalidTokenException('Cannot find verifiable signature', InvalidTokenException::TOKEN_PARSE_ERROR); } } else { $unprotected = $obj['header']; $protected = $obj['protected']; $signature = $obj['signature']; } break; default: throw new \InvalidArgumentException('Incorrect format'); } $headers = json_decode(Util::base64url_decode($protected), true); if ($headers == null) { throw new InvalidTokenException('Cannot decode header', InvalidTokenException::TOKEN_PARSE_ERROR); } // Process crit if (isset($headers['crit'])) { foreach ($headers['crit'] as $critical) { if (!in_array($critical, array('nbf', 'exp', 'alg', 'kid'))) { throw new InvalidTokenException('Critical header not supported: ' . $critical, InvalidTokenException::UNSUPPORTED_ERROR); } } } if (isset($unprotected)) { $headers = array_merge($headers, $unprotected); } $claims = json_decode(Util::base64url_decode($payload), true); if ($claims == null) { throw new InvalidTokenException('Cannot decode claims', InvalidTokenException::TOKEN_PARSE_ERROR); } // Check signatures if ($headers['alg'] != $expected_alg) { throw new InvalidTokenException('Unexpected algorithm', InvalidTokenException::SIGNATURE_VERIFICATION_ERROR); } $signer = AlgorithmFactory::create($expected_alg); $signing_input = $protected . '.' . $payload; try { if (isset($headers['kid'])) { $kid = $headers['kid']; } $result = $signer->verify($signature, $signing_input, $keys, $kid); } catch (KeyException $e) { throw new InvalidTokenException($e->getMessage(), InvalidTokenException::SIGNATURE_VERIFICATION_ERROR, $e); } catch (CryptException $e) { throw new InvalidTokenException($e->getMessage(), InvalidTokenException::SIGNATURE_VERIFICATION_ERROR, $e); } if (!$result) { throw new InvalidTokenException('Incorrect signature', InvalidTokenException::SIGNATURE_VERIFICATION_ERROR); } // Check time, etc $time = time(); if (isset($claims['nbf']) && !in_array('nbf', $skip_validation)) { if ($time < $claims['nbf'] - self::$TIME_ALLOWANCE) { throw new InvalidTokenException('Too early due to nbf claim', InvalidTokenException::TOO_EARLY_ERROR, null, $claims['nbf']); } } if (isset($claims['exp']) && !in_array('exp', $skip_validation)) { if ($time > $claims['exp'] + self::$TIME_ALLOWANCE) { throw new InvalidTokenException('Too late due to exp claim', InvalidTokenException::TOO_LATE_ERROR, null, $claims['exp']); } } return new JWT($headers, $claims); }
/** * Returns the symmetric key in binary representation * * @return string the key */ public function toBinary() { return Util::base64url_decode($this->data['k']); }
public function decryptAndVerify($ciphertext, $tag, $cek, $additional, $iv) { $params = self::$alg_params[$this->getAlg()]; if (strlen($cek) != $this->getCEKSize() / 8) { throw new CryptException('Incorrect key length'); } $iv = Util::base64url_decode($iv); if (strlen($iv) != $this->getIVSize() / 8) { throw new CryptException('Incorrect IV length'); } list($mac_key, $enc_key) = str_split($cek, (int) (strlen($cek) / 2)); $al = Util::packInt64(strlen($additional) * 8); $e = Util::base64url_decode($ciphertext); $m = hash_hmac($params['hash'], $additional . $iv . $e . $al, $mac_key, true); $t = substr($m, 0, $params['tag']); if (!Util::secure_compare(Util::base64url_decode($tag), $t)) { throw new CryptException('Authentication tag does not match'); } $plaintext = openssl_decrypt($e, $params['cipher'], $enc_key, OPENSSL_RAW_DATA, $iv); return $plaintext; }
public function toPEM() { if ($this->isPublic()) { $der = ASN1::encodeDER(ASN1::SEQUENCE, ASN1::encodeDER(ASN1::SEQUENCE, ASN1::encodeDER(ASN1::OID, ASN1::encodeOID(self::OID)) . ASN1::encodeDER(ASN1::NULL_TYPE), false) . ASN1::encodeDER(ASN1::BIT_STRING, chr(0x0) . ASN1::encodeDER(ASN1::SEQUENCE, ASN1::encodeDER(ASN1::INTEGER_TYPE, Util::base64url_decode($this->data['n'])) . ASN1::encodeDER(ASN1::INTEGER_TYPE, Util::base64url_decode($this->data['e'])), false)), false); return wordwrap("-----BEGIN PUBLIC KEY-----\n" . base64_encode($der) . "\n-----END PUBLIC KEY-----\n", 64, "\n", true); } else { $der = ASN1::encodeDER(ASN1::SEQUENCE, ASN1::encodeDER(ASN1::INTEGER_TYPE, chr(0)) . ASN1::encodeDER(ASN1::INTEGER_TYPE, Util::base64url_decode($this->data['n'])) . ASN1::encodeDER(ASN1::INTEGER_TYPE, Util::base64url_decode($this->data['e'])) . ASN1::encodeDER(ASN1::INTEGER_TYPE, Util::base64url_decode($this->data['d'])) . ASN1::encodeDER(ASN1::INTEGER_TYPE, Util::base64url_decode($this->data['p'])) . ASN1::encodeDER(ASN1::INTEGER_TYPE, Util::base64url_decode($this->data['q'])) . ASN1::encodeDER(ASN1::INTEGER_TYPE, Util::base64url_decode($this->data['dp'])) . ASN1::encodeDER(ASN1::INTEGER_TYPE, Util::base64url_decode($this->data['dq'])) . ASN1::encodeDER(ASN1::INTEGER_TYPE, Util::base64url_decode($this->data['qi'])), false); return wordwrap("-----BEGIN RSA PRIVATE KEY-----\n" . base64_encode($der) . "\n-----END RSA PRIVATE KEY-----\n", 64, "\n", true); } }
/** * Decrypts a JWE. * * @param string $token the serialised JWE * @param SimpleJWT\Keys\KeySet $keys the key set containing the key to verify the * JWT's signature * @param string $expected_alg the expected value of the `alg` parameter, which * should be agreed between the parties out-of-band * @param string $format the JWE serialisation format * @return JWE the decrypted JWE * @throws InvalidTokenException if the token is invalid for any reason */ public static function decrypt($token, $keys, $expected_alg, $format = self::COMPACT_FORMAT) { switch ($format) { case self::COMPACT_FORMAT: $parts = explode('.', $token, 5); if (count($parts) != 5) { throw new InvalidTokenException('Cannot decode compact serialisation', InvalidTokenException::TOKEN_PARSE_ERROR); } list($protected, $encrypted_key, $iv, $ciphertext, $tag) = $parts; break; case self::JSON_FORMAT: $obj = json_decode($token, true); if ($obj == null) { throw new InvalidTokenException('Cannot decode JSON', InvalidTokenException::TOKEN_PARSE_ERROR); } $protected = $obj['protected']; $unprotected = $obj['unprotected']; $iv = $obj['iv']; $ciphertext = $obj['ciphertext']; $tag = $obj['tag']; if (isset($obj['recipients'])) { foreach ($obj['recipients'] as $recipient) { if (isset($recipient_obj['header']['kid'])) { $target_kid = $recipient_obj['header']['kid']; if ($keys->getById($target_kid) != null) { $unprotected = isset($unprotected) ? array_merge($unprotected, $recipient_obj['header']) : $recipient_obj['header']; $encrypted_key = $recipient_obj['encrypted_key']; break; } } throw new InvalidTokenException('Cannot find verifiable signature', InvalidTokenException::TOKEN_PARSE_ERROR); } } else { $unprotected = isset($unprotected) ? array_merge($unprotected, $obj['header']) : $obj['header']; $encrypted_key = $obj['encrypted_key']; } break; default: throw new \InvalidArgumentException('Incorrect format'); } $headers = json_decode(Util::base64url_decode($protected), true); if ($headers == null) { throw new InvalidTokenException('Cannot decode header', InvalidTokenException::TOKEN_PARSE_ERROR); } if (isset($unprotected)) { $headers = array_merge($headers, $unprotected); } // Process crit if (isset($headers['crit'])) { foreach ($headers['crit'] as $critical) { if (!in_array($critical, array('alg', 'enc', 'kid', 'zip'))) { throw new InvalidTokenException('Critical header not supported: ' . $critical, InvalidTokenException::UNSUPPORTED_ERROR); } } } if (!isset($headers['alg'])) { throw new InvalidTokenException('alg parameter missing', InvalidTokenException::TOKEN_PARSE_ERROR); } if (!isset($headers['enc'])) { throw new InvalidTokenException('enc parameter missing', InvalidTokenException::TOKEN_PARSE_ERROR); } if ($headers['alg'] != $expected_alg) { throw new InvalidTokenException('Unexpected algorithm', InvalidTokenException::DECRYPTION_ERROR); } $key_enc = AlgorithmFactory::create($headers['alg']); $content_enc = AlgorithmFactory::create($headers['enc']); if ($key_enc instanceof KeyDerivationAlgorithm) { try { $agreed_key = $key_enc->deriveKey($keys, $headers, $kid); } catch (KeyException $e) { throw new InvalidTokenException($e->getMessage(), InvalidTokenException::DECRYPTION_ERROR, $e); } catch (CryptException $e) { throw new InvalidTokenException($e->getMessage(), InvalidTokenException::DECRYPTION_ERROR, $e); } if ($key_enc instanceof KeyEncryptionAlgorithm) { // Key agreement with wrapping $keys->add(new SymmetricKey(array('kty' => SymmetricKey::KTY, 'alg' => $this->headers['alg'], 'k' => Util::base64url_encode($agreed_key), 'kid' => '#key-agreement-with-wrapping'), 'php')); $kid = '#key-agreement-with-wrapping'; } else { // Direct key agreement or direct encryption $cek = $agreed_key; if ($encrypted_key != '') { throw new InvalidTokenException('encrypted key should be empty', InvalidTokenException::TOKEN_PARSE_ERROR); } } } if (!isset($cek) && $key_enc instanceof KeyEncryptionAlgorithm) { try { if (!isset($kid)) { $kid = isset($headers['kid']) ? $headers['kid'] : null; } $cek = $key_enc->decryptKey($encrypted_key, $keys, $headers, $kid); } catch (KeyException $e) { throw new InvalidTokenException($e->getMessage(), InvalidTokenException::DECRYPTION_ERROR, $e); } catch (CryptException $e) { throw new InvalidTokenException($e->getMessage(), InvalidTokenException::DECRYPTION_ERROR, $e); } } if (!$cek) { throw new InvalidTokenException('alg parameter incorrect', InvalidTokenException::TOKEN_PARSE_ERROR); } try { $plaintext = $content_enc->decryptAndVerify($ciphertext, $tag, $cek, $protected, $iv); if (isset($headers['zip'])) { switch ($headers['zip']) { case 'DEF': $plaintext = gzinflate($plaintext); break; default: throw new InvalidTokenException('Unsupported zip header:' . $headers['zip'], InvalidTokenException::UNSUPPORTED_ERROR); } } } catch (CryptException $e) { throw new InvalidTokenException($e->getMessage(), InvalidTokenException::DECRYPTION_ERROR, $e); } return new JWE($headers, $plaintext); }
private function getKeySetFromPassword($password, $headers) { $salt = $headers['alg'] . "" . Util::base64url_decode($headers['p2s']); $hash = hash_pbkdf2($this->hash_alg, $password, $salt, $headers['p2c'], $this->getAESKWKeySize() / 8, true); $keys = new KeySet(); $keys->add(new SymmetricKey($hash, 'bin')); return $keys; }
public function verify($signature, $data, $keys, $kid = null) { $key = $this->selectKey($keys, $kid, array(Key::PUBLIC_PROPERTY => true, '~use' => 'sig')); if ($key == null) { throw new KeyException('Key not found or is invalid'); } $binary = Util::base64url_decode($signature); if ($key->getKeyType() == \SimpleJWT\Keys\ECKey::KTY) { // For ECDSA signatures, OpenSSL expects a ASN.1 DER SEQUENCE list($r, $s) = str_split($binary, (int) (strlen($binary) / 2)); // Trim leading zeros $r = ltrim($r, ""); $s = ltrim($s, ""); // Convert r and s from unsigned big-endian integers to signed two's complement if (ord($r[0]) > 0x7f) { $r = "" . $r; } if (ord($s[0]) > 0x7f) { $s = "" . $s; } $binary = ASN1::encodeDER(ASN1::SEQUENCE, ASN1::encodeDER(ASN1::INTEGER_TYPE, $r) . ASN1::encodeDER(ASN1::INTEGER_TYPE, $s), false); } $result = openssl_verify($data, $binary, $key->toPEM(), 'SHA' . $this->size); switch ($result) { case 1: return true; break; case 0: return false; break; default: $messages = array(); while ($message = openssl_error_string()) { $messages[] = $message; } throw new CryptException('Cannot verify signature: ' . implode("\n", $messages)); return false; break; } }
public function toPEM() { $oid = $this->getOID($this->data['crv']); if ($oid == null) { throw new KeyException('Unrecognised EC curve'); } if ($this->isPublic()) { $der = ASN1::encodeDER(ASN1::SEQUENCE, ASN1::encodeDER(ASN1::SEQUENCE, ASN1::encodeDER(ASN1::OID, ASN1::encodeOID(self::EC_OID)) . ASN1::encodeDER(ASN1::OID, ASN1::encodeOID($oid)), false) . ASN1::encodeDER(ASN1::BIT_STRING, chr(0x0) . chr(0x4) . Util::base64url_decode($this->data['x']) . Util::base64url_decode($this->data['y'])), false); return wordwrap("-----BEGIN PUBLIC KEY-----\n" . base64_encode($der) . "\n-----END PUBLIC KEY-----\n", 64, "\n", true); } else { $der = ASN1::encodeDER(ASN1::SEQUENCE, ASN1::encodeDER(ASN1::INTEGER_TYPE, chr(0x1)) . ASN1::encodeDER(ASN1::OCTET_STRING, Util::base64url_decode($this->data['d'])) . ASN1::encodeDER(0x0, ASN1::encodeDER(ASN1::OID, ASN1::encodeOID($oid)), false, ASN1::CONTEXT_CLASS) . ASN1::encodeDER(0x1, ASN1::encodeDER(ASN1::BIT_STRING, chr(0x0) . chr(0x4) . Util::base64url_decode($this->data['x']) . Util::base64url_decode($this->data['y'])), false, ASN1::CONTEXT_CLASS), false); return wordwrap("-----BEGIN EC PRIVATE KEY-----\n" . base64_encode($der) . "\n-----END EC PRIVATE KEY-----\n", 64, "\n", true); } }
public function decryptKey($encrypted_key, $keys, $headers, $kid = null) { $key = $this->selectKey($keys, $kid, array(Key::PUBLIC_PROPERTY => false)); if ($key == null || $key->isPublic()) { throw new CryptException('Key not found or is invalid'); } $params = self::$alg_params[$this->getAlg()]; $cek = ''; if (!openssl_private_decrypt(Util::base64url_decode($encrypted_key), $cek, $key->toPEM(), $params['padding'])) { $messages = array(); while ($message = openssl_error_string()) { $messages[] = $message; } throw new CryptException('Cannot decrypt key: ' . implode("\n", $messages)); } if (isset($params['oaep'])) { // $key->getSize() ignores the first octet when calculating the key size, // therefore we need to add it back in $cek = $this->oaep_decode($cek, 1 + $key->getSize() / 8, $params['oaep']); } return $cek; }