diff --git a/src/Exception/ExceptionCode.php b/src/Exception/ExceptionCode.php index a2f877d..f74aef2 100644 --- a/src/Exception/ExceptionCode.php +++ b/src/Exception/ExceptionCode.php @@ -37,6 +37,7 @@ abstract class ExceptionCode const IMPOSSIBLE_CONDITION = 0x3142EE1B; const INVOKED_RAW_ON_MULTIKEY = 0x3142EE1C; const KEY_NOT_IN_KEYRING = 0x3142EE1D; + const UNDEFINED_PROPERTY = 0x3142EE1E; /** * @param int $code @@ -89,6 +90,8 @@ public static function explainErrorCode(int $code): string "and pass that instead."; case self::KEY_NOT_IN_KEYRING: return "The key you requested is not in this keyring."; + case self::UNDEFINED_PROPERTY: + return "An expected property was not defined at runtime."; default: return 'Unknown error code'; diff --git a/src/Purpose.php b/src/Purpose.php index 489fc0c..0b716c5 100644 --- a/src/Purpose.php +++ b/src/Purpose.php @@ -228,11 +228,6 @@ public static function isValid(string $rawString): bool */ public function isSendingKeyValid(SendingKey $key): bool { - /* - if ($key instanceof SendingKeyRing) { - return true; - } - */ $expectedKeyType = $this->expectedSendingKeyType(); return $key instanceof $expectedKeyType; } @@ -246,11 +241,6 @@ public function isSendingKeyValid(SendingKey $key): bool */ public function isReceivingKeyValid(ReceivingKey $key): bool { - /* - if ($key instanceof ReceivingKeyRing) { - return true; - } - */ $expectedKeyType = $this->expectedReceivingKeyType(); return $key instanceof $expectedKeyType; } diff --git a/src/SendingKeyRing.php b/src/SendingKeyRing.php index 064e062..34af2b0 100644 --- a/src/SendingKeyRing.php +++ b/src/SendingKeyRing.php @@ -2,6 +2,7 @@ declare(strict_types=1); namespace ParagonIE\Paseto; +use Exception; use ParagonIE\Paseto\Exception\InvalidKeyException; use ParagonIE\Paseto\Exception\PasetoException; use ParagonIE\Paseto\Keys\AsymmetricSecretKey; @@ -17,7 +18,6 @@ class SendingKeyRing implements KeyRingInterface, SendingKey /** @var array */ protected $keys = []; - /** * Add a key to this KeyID. * @@ -43,6 +43,7 @@ public function addKey(string $keyId, SendingKey $key): self * * @throws InvalidKeyException * @throws PasetoException + * @throws Exception */ public function deriveReceivingKeyRing(): ReceivingKeyRing { diff --git a/src/Traits/MultiKeyTrait.php b/src/Traits/MultiKeyTrait.php index 314fb7c..2017f64 100644 --- a/src/Traits/MultiKeyTrait.php +++ b/src/Traits/MultiKeyTrait.php @@ -8,26 +8,48 @@ use ParagonIE\Paseto\Exception\PasetoException; use ParagonIE\Paseto\KeyInterface; use ParagonIE\Paseto\ProtocolInterface; +use ParagonIE\Paseto\Purpose; +use ParagonIE\Paseto\ReceivingKey; +use ParagonIE\Paseto\SendingKey; +use TypeError; +use function is_null; /** * @var array $keys */ trait MultiKeyTrait { - /** @var ProtocolInterface $version */ + /** @var ?Purpose $purpose */ + protected $purpose = null; + + /** @var ?ProtocolInterface $version */ protected $version = null; /** - * The intended version for this protocol. Currently only meaningful - * in asymmetric cryptography. + * The intended version for this key. * * @return ProtocolInterface */ public function getProtocol(): ProtocolInterface { + if (is_null($this->version)) { + throw new TypeError( + "Version must not be NULL.", + ExceptionCode::UNDEFINED_PROPERTY + ); + } return $this->version; } + /** + * The intended purpose for this key. + * @return ?Purpose + */ + public function getPurpose(): ?Purpose + { + return $this->purpose; + } + /** * Throw an exception if the key is wrong. * @@ -46,12 +68,45 @@ protected function typeCheckKey(KeyInterface $key): void ExceptionCode::IMPOSSIBLE_CONDITION ); } + if (!$key instanceof $type) { throw new InvalidKeyException( "The provided key is the wrong type", ExceptionCode::PASETO_KEY_TYPE_ERROR ); } + + if (!is_null($this->version)) { + if (!($key->getProtocol() instanceof $this->version)) { + throw new InvalidKeyException( + "The provided key is for the wrong version", + ExceptionCode::WRONG_KEY_FOR_VERSION + ); + } + } + + if (!is_null($this->purpose)) { + $valid = false; + try { + if ($key instanceof ReceivingKey) { + $valid = $this->purpose->isReceivingKeyValid($key); + } elseif ($key instanceof SendingKey) { + $valid = $this->purpose->isSendingKeyValid($key); + } + } catch (TypeError $ex) { + throw new InvalidKeyException( + "The provided key is not appropriate for the expected purpose.", + ExceptionCode::PURPOSE_WRONG_FOR_KEY, + $ex + ); + } + if (!$valid) { + throw new InvalidKeyException( + "The provided key is not appropriate for the expected purpose.", + ExceptionCode::PURPOSE_WRONG_FOR_KEY + ); + } + } } /** @@ -91,6 +146,16 @@ public function raw(): string ); } + /** + * @param Purpose $purpose + * @return static + */ + public function setPurpose(Purpose $purpose): self + { + $this->purpose = $purpose; + return $this; + } + /** * @param ProtocolInterface $version * @return static diff --git a/tests/MultiKeyTest.php b/tests/MultiKeyTest.php index 544d9f3..5c6e823 100644 --- a/tests/MultiKeyTest.php +++ b/tests/MultiKeyTest.php @@ -14,15 +14,17 @@ Version3, Version4 }; -use ParagonIE\Paseto\{ - Builder, - Parser, +use ParagonIE\Paseto\{Builder, JsonToken, + Parser, ProtocolInterface, + Purpose, + ReceivingKey, ReceivingKeyRing, - SendingKeyRing -}; + SendingKey, + SendingKeyRing}; use PHPUnit\Framework\TestCase; +use TypeError; /** * @covers Builder @@ -223,4 +225,270 @@ protected function doSendingTest(ProtocolInterface $v): void $this->assertSame('foo', $parseLocal->getSubject()); $this->assertSame('foo', $parsePublic->getSubject()); } + + /** + * @return array + * @throws \Exception + */ + public function typeCheckData() + { + $v3_lk = Version3::generateSymmetricKey(); + $v3_sk = Version3::generateAsymmetricSecretKey(); + $v3_pk = $v3_sk->getPublicKey(); + $v4_lk = Version4::generateSymmetricKey(); + $v4_sk = Version4::generateAsymmetricSecretKey(); + $v4_pk = $v4_sk->getPublicKey(); + + return [ + // Receiving keys, version 3 + [ + (new ReceivingKeyRing())->setVersion(new Version3()), + $v3_lk, + false + ], [ + (new ReceivingKeyRing())->setVersion(new Version3()), + $v3_sk, + true + ], [ + (new ReceivingKeyRing())->setVersion(new Version3()), + $v3_pk, + false + ], + + // Receiving keys, version 4 + [ + (new ReceivingKeyRing())->setVersion(new Version4()), + $v4_lk, + false + ], [ + (new ReceivingKeyRing())->setVersion(new Version4()), + $v4_sk, + true + ], [ + (new ReceivingKeyRing())->setVersion(new Version4()), + $v4_pk, + false + ], + + // Sending keys, version 3 + [ + (new SendingKeyRing())->setVersion(new Version3()), + $v3_lk, + false + ], [ + (new SendingKeyRing())->setVersion(new Version3()), + $v3_sk, + false + ], [ + (new SendingKeyRing())->setVersion(new Version3()), + $v3_pk, + true + ], + // Sending keys, version 4 + [ + (new SendingKeyRing())->setVersion(new Version4()), + $v4_lk, + false + ], [ + (new SendingKeyRing())->setVersion(new Version4()), + $v4_sk, + false + ], [ + (new SendingKeyRing())->setVersion(new Version4()), + $v4_pk, + true + ], + + // Type confusion: Receiving, version 4, with v3 key + [ + (new ReceivingKeyRing())->setVersion(new Version4()), + $v3_lk, + true + ], [ + (new ReceivingKeyRing())->setVersion(new Version4()), + $v3_sk, + true + ], [ + (new ReceivingKeyRing())->setVersion(new Version4()), + $v3_pk, + true + ], + // Type confusion: Receiving, version 3, with v4 key + [ + (new ReceivingKeyRing())->setVersion(new Version3()), + $v4_lk, + true + ], [ + (new ReceivingKeyRing())->setVersion(new Version3()), + $v4_sk, + true + ], [ + (new ReceivingKeyRing())->setVersion(new Version3()), + $v4_pk, + true + ], + + // Type confusion: Sending, version 4, with v3 key + [ + (new SendingKeyRing())->setVersion(new Version4()), + $v3_lk, + true + ], [ + (new SendingKeyRing())->setVersion(new Version4()), + $v3_sk, + true + ], [ + (new SendingKeyRing())->setVersion(new Version4()), + $v3_pk, + true + ], + + // Type confusion: Sending, version 3, with v4 key + [ + (new SendingKeyRing())->setVersion(new Version3()), + $v4_lk, + true + ], [ + (new SendingKeyRing())->setVersion(new Version3()), + $v4_sk, + true + ], [ + (new SendingKeyRing())->setVersion(new Version3()), + $v4_pk, + true + ], + + // Version 3 -- purpose checks -- receiving + [ + (new ReceivingKeyRing()) + ->setPurpose(Purpose::local()) + ->setVersion(new Version3()), + $v3_lk, + false + ], [ + (new ReceivingKeyRing()) + ->setPurpose(Purpose::local()) + ->setVersion(new Version3()), + $v3_pk, + true + ], [ + (new ReceivingKeyRing()) + ->setPurpose(Purpose::public()) + ->setVersion(new Version3()), + $v3_lk, + true + ], [ + (new ReceivingKeyRing()) + ->setPurpose(Purpose::public()) + ->setVersion(new Version3()), + $v3_pk, + false + ], + + // Version 4 -- purpose checks -- receiving + [ + (new ReceivingKeyRing()) + ->setPurpose(Purpose::local()) + ->setVersion(new Version4()), + $v4_lk, + false + ], [ + (new ReceivingKeyRing()) + ->setPurpose(Purpose::local()) + ->setVersion(new Version4()), + $v4_pk, + true + ], [ + (new ReceivingKeyRing()) + ->setPurpose(Purpose::public()) + ->setVersion(new Version4()), + $v4_lk, + true + ], [ + (new ReceivingKeyRing()) + ->setPurpose(Purpose::public()) + ->setVersion(new Version4()), + $v4_pk, + false + ], + + // Version 3 -- purpose checks -- sending + [ + (new SendingKeyRing()) + ->setPurpose(Purpose::local()) + ->setVersion(new Version3()), + $v3_lk, + false + ], [ + (new SendingKeyRing()) + ->setPurpose(Purpose::local()) + ->setVersion(new Version3()), + $v3_sk, + true + ], [ + (new SendingKeyRing()) + ->setPurpose(Purpose::public()) + ->setVersion(new Version3()), + $v3_lk, + true + ], [ + (new SendingKeyRing()) + ->setPurpose(Purpose::public()) + ->setVersion(new Version3()), + $v3_sk, + false + ], + + // Version 4 -- purpose checks -- sending + [ + (new SendingKeyRing()) + ->setPurpose(Purpose::local()) + ->setVersion(new Version4()), + $v4_lk, + false + ], [ + (new SendingKeyRing()) + ->setPurpose(Purpose::local()) + ->setVersion(new Version4()), + $v4_sk, + true + ], [ + (new SendingKeyRing()) + ->setPurpose(Purpose::public()) + ->setVersion(new Version4()), + $v4_lk, + true + ], [ + (new SendingKeyRing()) + ->setPurpose(Purpose::public()) + ->setVersion(new Version4()), + $v4_sk, + false + ] + ]; + } + + /** + * @dataProvider typeCheckData + * + * @param SendingKeyRing|ReceivingKeyRing $keyring + * @param SendingKey|ReceivingKey $key + * @param bool $expectFail + * + * @psalm-suppress PossiblyInvalidArgument + * @throws PasetoException + */ + public function testTypeChecks($keyring, $key, bool $expectFail): void + { + if ($expectFail) { + $this->expectException(PasetoException::class); + } + try { + $keyring->addKey('foo', $key); + $received = $keyring->fetchKey('foo'); + $this->assertInstanceOf(get_class($key), $received); + } catch (TypeError $ex) { + throw new PasetoException('TypeError', 0, $ex); + } + } }