From bdc788c76d96abeb8727cf7e21214983431281c5 Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 3 Apr 2026 11:04:00 +0200 Subject: [PATCH] add cryptography store cache --- .../Store/Psr16CacheStoreDecorator.php | 73 ++++++++++ .../Store/Psr6CacheStoreDecorator.php | 76 ++++++++++ .../Store/Psr16CacheStoreDecoratorTest.php | 117 ++++++++++++++++ .../Store/Psr6CacheStoreDecoratorTest.php | 131 ++++++++++++++++++ 4 files changed, 397 insertions(+) create mode 100644 src/Extension/Cryptography/Store/Psr16CacheStoreDecorator.php create mode 100644 src/Extension/Cryptography/Store/Psr6CacheStoreDecorator.php create mode 100644 tests/Unit/Extension/Cryptography/Store/Psr16CacheStoreDecoratorTest.php create mode 100644 tests/Unit/Extension/Cryptography/Store/Psr6CacheStoreDecoratorTest.php diff --git a/src/Extension/Cryptography/Store/Psr16CacheStoreDecorator.php b/src/Extension/Cryptography/Store/Psr16CacheStoreDecorator.php new file mode 100644 index 0000000..cf95b00 --- /dev/null +++ b/src/Extension/Cryptography/Store/Psr16CacheStoreDecorator.php @@ -0,0 +1,73 @@ +cache->get($key); + + if ($entry instanceof CipherKey) { + return $entry; + } + + $entry = $this->cipherKeyStore->currentKeyFor($subjectId); + + $this->cache->set($key, $entry, $this->ttl); + + return $entry; + } + + public function get(string $id): CipherKey + { + $key = 'id:' . $id; + $entry = $this->cache->get($key); + + if ($entry instanceof CipherKey) { + return $entry; + } + + $entry = $this->cipherKeyStore->get($id); + + $this->cache->set($key, $entry, $this->ttl); + + return $entry; + } + + public function store(CipherKey $key): void + { + $this->cipherKeyStore->store($key); + + $this->cache->set('id:' . $key->id, $key, $this->ttl); + $this->cache->set('subjectId:' . $key->subjectId, $key, $this->ttl); + } + + public function remove(string $id): void + { + $this->cipherKeyStore->remove($id); + + $this->cache->delete('id:' . $id); + } + + public function removeWithSubjectId(string $subjectId): void + { + $this->cipherKeyStore->removeWithSubjectId($subjectId); + + $this->cache->delete('subjectId:' . $subjectId); + } +} diff --git a/src/Extension/Cryptography/Store/Psr6CacheStoreDecorator.php b/src/Extension/Cryptography/Store/Psr6CacheStoreDecorator.php new file mode 100644 index 0000000..c8ca045 --- /dev/null +++ b/src/Extension/Cryptography/Store/Psr6CacheStoreDecorator.php @@ -0,0 +1,76 @@ +cache->getItem($key); + $entry = $item->get(); + + if ($item->isHit() && $entry instanceof CipherKey) { + return $entry; + } + + $entry = $this->cipherKeyStore->currentKeyFor($subjectId); + + $item->set($entry); + $item->expiresAfter($this->expiresAfter); + $this->cache->save($item); + + return $entry; + } + + public function get(string $id): CipherKey + { + $key = 'id:' . $id; + $item = $this->cache->getItem($key); + $entry = $item->get(); + + if ($item->isHit() && $entry instanceof CipherKey) { + return $entry; + } + + $entry = $this->cipherKeyStore->get($id); + + $item->set($entry); + $item->expiresAfter($this->expiresAfter); + $this->cache->save($item); + + return $entry; + } + + public function store(CipherKey $key): void + { + $this->cipherKeyStore->store($key); + } + + public function remove(string $id): void + { + $this->cipherKeyStore->remove($id); + + $this->cache->deleteItem('id:' . $id); + } + + public function removeWithSubjectId(string $subjectId): void + { + $this->cipherKeyStore->removeWithSubjectId($subjectId); + + $this->cache->deleteItem('subjectId:' . $subjectId); + } +} diff --git a/tests/Unit/Extension/Cryptography/Store/Psr16CacheStoreDecoratorTest.php b/tests/Unit/Extension/Cryptography/Store/Psr16CacheStoreDecoratorTest.php new file mode 100644 index 0000000..bee4764 --- /dev/null +++ b/tests/Unit/Extension/Cryptography/Store/Psr16CacheStoreDecoratorTest.php @@ -0,0 +1,117 @@ +createKey(); + + $cache = $this->createMock(CacheInterface::class); + $cache->expects(self::once())->method('get')->with('subjectId:subject-1')->willReturn($key); + $cache->expects(self::never())->method('set'); + + $innerStore = $this->createMock(CipherKeyStore::class); + $innerStore->expects(self::never())->method('currentKeyFor'); + + $store = new Psr16CacheStoreDecorator($innerStore, $cache); + + self::assertSame($key, $store->currentKeyFor('subject-1')); + } + + public function testCurrentKeyForWithCacheMiss(): void + { + $key = $this->createKey(); + + $cache = $this->createMock(CacheInterface::class); + $cache->expects(self::once())->method('get')->with('subjectId:subject-1')->willReturn(null); + $cache->expects(self::once())->method('set')->with('subjectId:subject-1', $key, 42); + + $innerStore = $this->createMock(CipherKeyStore::class); + $innerStore->expects(self::once())->method('currentKeyFor')->with('subject-1')->willReturn($key); + + $store = new Psr16CacheStoreDecorator($innerStore, $cache, 42); + + self::assertSame($key, $store->currentKeyFor('subject-1')); + } + + public function testGetWithCacheMiss(): void + { + $key = $this->createKey(); + + $cache = $this->createMock(CacheInterface::class); + $cache->expects(self::once())->method('get')->with('id:key-1')->willReturn(false); + $cache->expects(self::once())->method('set')->with('id:key-1', $key, null); + + $innerStore = $this->createMock(CipherKeyStore::class); + $innerStore->expects(self::once())->method('get')->with('key-1')->willReturn($key); + + $store = new Psr16CacheStoreDecorator($innerStore, $cache); + + self::assertSame($key, $store->get('key-1')); + } + + public function testStoreWritesInnerStoreAndBothCacheEntries(): void + { + $key = $this->createKey(); + + $cache = $this->createMock(CacheInterface::class); + $cache->expects(self::exactly(2))->method('set')->willReturnMap([ + ['id:key-1', $key, 17, true], + ['subjectId:subject-1', $key, 17, true], + ]); + + $innerStore = $this->createMock(CipherKeyStore::class); + $innerStore->expects(self::once())->method('store')->with($key); + + $store = new Psr16CacheStoreDecorator($innerStore, $cache, 17); + $store->store($key); + } + + public function testRemoveDeletesIdCacheEntry(): void + { + $cache = $this->createMock(CacheInterface::class); + $cache->expects(self::once())->method('delete')->with('id:key-1'); + + $innerStore = $this->createMock(CipherKeyStore::class); + $innerStore->expects(self::once())->method('remove')->with('key-1'); + + $store = new Psr16CacheStoreDecorator($innerStore, $cache); + $store->remove('key-1'); + } + + public function testRemoveWithSubjectIdDeletesSubjectCacheEntry(): void + { + $cache = $this->createMock(CacheInterface::class); + $cache->expects(self::once())->method('delete')->with('subjectId:subject-1'); + + $innerStore = $this->createMock(CipherKeyStore::class); + $innerStore->expects(self::once())->method('removeWithSubjectId')->with('subject-1'); + + $store = new Psr16CacheStoreDecorator($innerStore, $cache); + $store->removeWithSubjectId('subject-1'); + } + + private function createKey(): CipherKey + { + return new CipherKey( + 'key-1', + 'subject-1', + 'secret', + 'aes-256-gcm', + new DateTimeImmutable(), + ); + } +} diff --git a/tests/Unit/Extension/Cryptography/Store/Psr6CacheStoreDecoratorTest.php b/tests/Unit/Extension/Cryptography/Store/Psr6CacheStoreDecoratorTest.php new file mode 100644 index 0000000..99632d9 --- /dev/null +++ b/tests/Unit/Extension/Cryptography/Store/Psr6CacheStoreDecoratorTest.php @@ -0,0 +1,131 @@ +createKey(); + + $item = $this->createMock(CacheItemInterface::class); + $item->expects(self::once())->method('get')->willReturn($key); + $item->expects(self::once())->method('isHit')->willReturn(true); + + $cache = $this->createMock(CacheItemPoolInterface::class); + $cache->expects(self::once())->method('getItem')->with('subjectId:subject-1')->willReturn($item); + + $innerStore = $this->createMock(CipherKeyStore::class); + $innerStore->expects(self::never())->method('currentKeyFor'); + + $store = new Psr6CacheStoreDecorator($innerStore, $cache); + + self::assertSame($key, $store->currentKeyFor('subject-1')); + } + + public function testCurrentKeyForWithCacheMiss(): void + { + $key = $this->createKey(); + + $item = $this->createMock(CacheItemInterface::class); + $item->expects(self::once())->method('get')->willReturn(null); + $item->expects(self::once())->method('isHit')->willReturn(false); + $item->expects(self::once())->method('set')->with($key)->willReturnSelf(); + $item->expects(self::once())->method('expiresAfter')->with(42)->willReturnSelf(); + + $cache = $this->createMock(CacheItemPoolInterface::class); + $cache->expects(self::once())->method('getItem')->with('subjectId:subject-1')->willReturn($item); + $cache->expects(self::once())->method('save')->with($item); + + $innerStore = $this->createMock(CipherKeyStore::class); + $innerStore->expects(self::once())->method('currentKeyFor')->with('subject-1')->willReturn($key); + + $store = new Psr6CacheStoreDecorator($innerStore, $cache, 42); + + self::assertSame($key, $store->currentKeyFor('subject-1')); + } + + public function testGetWithCacheMiss(): void + { + $key = $this->createKey(); + + $item = $this->createMock(CacheItemInterface::class); + $item->expects(self::once())->method('get')->willReturn(null); + $item->expects(self::once())->method('isHit')->willReturn(false); + $item->expects(self::once())->method('set')->with($key)->willReturnSelf(); + $item->expects(self::once())->method('expiresAfter')->with(null)->willReturnSelf(); + + $cache = $this->createMock(CacheItemPoolInterface::class); + $cache->expects(self::once())->method('getItem')->with('id:key-1')->willReturn($item); + $cache->expects(self::once())->method('save')->with($item); + + $innerStore = $this->createMock(CipherKeyStore::class); + $innerStore->expects(self::once())->method('get')->with('key-1')->willReturn($key); + + $store = new Psr6CacheStoreDecorator($innerStore, $cache); + + self::assertSame($key, $store->get('key-1')); + } + + public function testStoreDelegatesToInnerStore(): void + { + $key = $this->createKey(); + + $cache = $this->createMock(CacheItemPoolInterface::class); + $cache->expects(self::never())->method('getItem'); + $cache->expects(self::never())->method('save'); + + $innerStore = $this->createMock(CipherKeyStore::class); + $innerStore->expects(self::once())->method('store')->with($key); + + $store = new Psr6CacheStoreDecorator($innerStore, $cache); + $store->store($key); + } + + public function testRemoveDeletesIdCacheEntry(): void + { + $cache = $this->createMock(CacheItemPoolInterface::class); + $cache->expects(self::once())->method('deleteItem')->with('id:key-1'); + + $innerStore = $this->createMock(CipherKeyStore::class); + $innerStore->expects(self::once())->method('remove')->with('key-1'); + + $store = new Psr6CacheStoreDecorator($innerStore, $cache); + $store->remove('key-1'); + } + + public function testRemoveWithSubjectIdDeletesSubjectCacheEntry(): void + { + $cache = $this->createMock(CacheItemPoolInterface::class); + $cache->expects(self::once())->method('deleteItem')->with('subjectId:subject-1'); + + $innerStore = $this->createMock(CipherKeyStore::class); + $innerStore->expects(self::once())->method('removeWithSubjectId')->with('subject-1'); + + $store = new Psr6CacheStoreDecorator($innerStore, $cache); + $store->removeWithSubjectId('subject-1'); + } + + private function createKey(): CipherKey + { + return new CipherKey( + 'key-1', + 'subject-1', + 'secret', + 'aes-256-gcm', + new DateTimeImmutable(), + ); + } +}