rFile = sys_get_temp_dir() . '/xcvm_openssl_extra_prev_' . uniqid(); OpensslExtra::usePrevFile($this->rFile); } protected function tearDown(): void { try { OpensslExtra::usePrevFile(null); } finally { @unlink($this->rFile); } } private function previous(string $rValue, int $rValidUntil): void { file_put_contents($this->rFile, json_encode(['value' => $rValue, 'valid_until' => $rValidUntil])); OpensslExtra::usePrevFile($this->rFile); } public function testATokenMintedWithTheReplacedValueOpensOnlyWhileItIsAccepted(): void { $rToken = Encryption::seal(self::PLAIN, 'k', 'old-extra'); $this->assertFalse(Encryption::readToken($rToken, 'k', OPENSSL_EXTRA, false), 'no previous value'); $this->previous('old-extra', time() + 600); $this->assertSame(self::PLAIN, Encryption::readToken($rToken, 'k', OPENSSL_EXTRA, false)); $this->previous('old-extra', time() - 1); $this->assertFalse(Encryption::readToken($rToken, 'k', OPENSSL_EXTRA, false), 'window closed'); } public function testALegacyTokenFallsBackOnlyWhereLegacyTokensAreAccepted(): void { $rToken = Encryption::encrypt(self::PLAIN, 'k', 'old-extra'); $this->previous('old-extra', time() + 600); $this->assertSame(self::PLAIN, Encryption::readToken($rToken, 'k', OPENSSL_EXTRA, true)); $this->assertFalse(Encryption::readToken($rToken, 'k', OPENSSL_EXTRA, false)); } /** * Where legacy tokens are accepted, a sealed token minted with the replaced * value must not be read as a legacy one with the current value first: that * decrypt() returns garbage instead of false about once in 256 tokens (when * the padding happens to hold). 48- and 64-byte tokens can pass CBC's length * check; 2000 of them miss none only when both sealed attempts come first. */ public function testASealedTokenWithTheReplacedValueOpensWhereLegacyTokensAreAccepted(): void { $this->previous('old-extra', time() + 600); $rMissed = 0; for ($i = 0; $i < 2000; $i++) { $rPlain = str_pad('live/' . $i . '/', ($i % 2) ? 20 : 36, 'x'); if (Encryption::readToken(Encryption::seal($rPlain, 'k', 'old-extra'), 'k', OPENSSL_EXTRA, true) !== $rPlain) { $rMissed++; } } $this->assertSame(0, $rMissed); } public function testOnlyTheOpensslExtraContextFallsBack(): void { $this->previous('old-extra', time() + 600); $this->assertFalse(Encryption::readToken(Encryption::seal(self::PLAIN, 'k', 'old-extra'), 'k', 'd', false)); $this->assertFalse(Encryption::readToken(Encryption::encrypt(self::PLAIN, 'k', 'old-extra'), 'k', 'd', true)); $this->assertFalse(Encryption::readToken(Encryption::seal(self::PLAIN, 'other-key', OPENSSL_EXTRA), 'k', OPENSSL_EXTRA, false), 'another key is still refused'); } public function testATokenMintedWithTheCurrentValueStillOpens(): void { $rSealed = Encryption::mintToken(self::PLAIN, 'k', OPENSSL_EXTRA, true); $rLegacy = Encryption::mintToken(self::PLAIN, 'k', OPENSSL_EXTRA, false); $this->assertSame(self::PLAIN, Encryption::readToken($rSealed, 'k', OPENSSL_EXTRA, false)); $this->assertSame(self::PLAIN, Encryption::readToken($rLegacy, 'k', OPENSSL_EXTRA, true)); $this->previous('old-extra', time() + 600); $this->assertSame(self::PLAIN, Encryption::readToken($rSealed, 'k', OPENSSL_EXTRA, false)); $this->assertSame(self::PLAIN, Encryption::readToken($rLegacy, 'k', OPENSSL_EXTRA, true)); } }