Skip to content

Commit c1d88b0

Browse files
authored
Merge pull request #79 from patchlevel/optional-encrypted-field-name
make encrypted field names optional
2 parents 8b73304 + d324ee4 commit c1d88b0

File tree

3 files changed

+97
-14
lines changed

3 files changed

+97
-14
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,10 +518,14 @@ use Patchlevel\Hydrator\Metadata\Event\EventMetadataFactory;
518518
use Patchlevel\Hydrator\MetadataHydrator;
519519

520520
$cipherKeyStore = new InMemoryCipherKeyStore();
521-
$cryptographer = PersonalDataPayloadCryptographer::createWithOpenssl($cipherKeyStore);
521+
$cryptographer = PersonalDataPayloadCryptographer::createWithOpenssl($cipherKeyStore, true);
522522
$hydrator = new MetadataHydrator(cryptographer: $cryptographer);
523523
```
524524

525+
> [!WARNING]
526+
> We recommend to use the `useEncryptedFieldName` option to recognize encrypted fields.
527+
> This allows data to be encrypted later without big troubles.
528+
525529
#### Cipher Key Store
526530

527531
The keys must be stored somewhere. For testing purposes, we offer an in-memory implementation.

src/Cryptography/PersonalDataPayloadCryptographer.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public function __construct(
2323
private readonly CipherKeyStore $cipherKeyStore,
2424
private readonly CipherKeyFactory $cipherKeyFactory,
2525
private readonly Cipher $cipher,
26-
private readonly bool $fallbackWithoutPrefix = true,
26+
private readonly bool $useEncryptedFieldName = false,
27+
private readonly bool $fallbackToFieldName = false,
2728
) {
2829
}
2930

@@ -52,11 +53,19 @@ public function encrypt(ClassMetadata $metadata, array $data): array
5253
continue;
5354
}
5455

55-
$data[$propertyMetadata->encryptedFieldName()] = $this->cipher->encrypt(
56+
$targetFieldName = $this->useEncryptedFieldName
57+
? $propertyMetadata->encryptedFieldName()
58+
: $propertyMetadata->fieldName();
59+
60+
$data[$targetFieldName] = $this->cipher->encrypt(
5661
$cipherKey,
5762
$data[$propertyMetadata->fieldName()],
5863
);
5964

65+
if (!$this->useEncryptedFieldName) {
66+
continue;
67+
}
68+
6069
unset($data[$propertyMetadata->fieldName()]);
6170
}
6271

@@ -87,10 +96,10 @@ public function decrypt(ClassMetadata $metadata, array $data): array
8796
continue;
8897
}
8998

90-
if (array_key_exists($propertyMetadata->encryptedFieldName(), $data)) {
99+
if ($this->useEncryptedFieldName && array_key_exists($propertyMetadata->encryptedFieldName(), $data)) {
91100
$rawData = $data[$propertyMetadata->encryptedFieldName()];
92101
unset($data[$propertyMetadata->encryptedFieldName()]);
93-
} elseif ($this->fallbackWithoutPrefix) {
102+
} elseif (!$this->useEncryptedFieldName || $this->fallbackToFieldName) {
94103
$rawData = $data[$propertyMetadata->fieldName()];
95104
} else {
96105
continue;
@@ -144,11 +153,15 @@ private function subjectId(ClassMetadata $metadata, array $data): string|null
144153
public static function createWithOpenssl(
145154
CipherKeyStore $cryptoStore,
146155
string $method = OpensslCipherKeyFactory::DEFAULT_METHOD,
156+
bool $useEncryptedFieldName = false,
157+
bool $fallbackToFieldName = false,
147158
): static {
148159
return new self(
149160
$cryptoStore,
150161
new OpensslCipherKeyFactory($method),
151162
new OpensslCipher(),
163+
$useEncryptedFieldName,
164+
$fallbackToFieldName,
152165
);
153166
}
154167
}

tests/Unit/Cryptography/PersonalDataPayloadCryptographerTest.php

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public function testEncryptWithMissingKey(): void
7777

7878
$result = $cryptographer->encrypt($this->metadata(PersonalDataProfileCreated::class), ['id' => 'foo', 'email' => 'info@patchlevel.de']);
7979

80-
self::assertEquals(['id' => 'foo', '!email' => 'encrypted'], $result);
80+
self::assertEquals(['id' => 'foo', 'email' => 'encrypted'], $result);
8181
}
8282

8383
public function testEncryptWithExistingKey(): void
@@ -109,6 +109,39 @@ public function testEncryptWithExistingKey(): void
109109

110110
$result = $cryptographer->encrypt($this->metadata(PersonalDataProfileCreated::class), ['id' => 'foo', 'email' => 'info@patchlevel.de']);
111111

112+
self::assertEquals(['id' => 'foo', 'email' => 'encrypted'], $result);
113+
}
114+
115+
public function testEncryptWithExistingKeyEncryptedFieldName(): void
116+
{
117+
$cipherKey = new CipherKey(
118+
'foo',
119+
'bar',
120+
'baz',
121+
);
122+
123+
$cipherKeyStore = $this->prophesize(CipherKeyStore::class);
124+
$cipherKeyStore->get('foo')->willReturn($cipherKey);
125+
$cipherKeyStore->store('foo', Argument::type(CipherKey::class))->shouldNotBeCalled();
126+
127+
$cipherKeyFactory = $this->prophesize(CipherKeyFactory::class);
128+
$cipherKeyFactory->__invoke()->shouldNotBeCalled();
129+
130+
$cipher = $this->prophesize(Cipher::class);
131+
$cipher
132+
->encrypt($cipherKey, 'info@patchlevel.de')
133+
->willReturn('encrypted')
134+
->shouldBeCalledOnce();
135+
136+
$cryptographer = new PersonalDataPayloadCryptographer(
137+
$cipherKeyStore->reveal(),
138+
$cipherKeyFactory->reveal(),
139+
$cipher->reveal(),
140+
true,
141+
);
142+
143+
$result = $cryptographer->encrypt($this->metadata(PersonalDataProfileCreated::class), ['id' => 'foo', 'email' => 'info@patchlevel.de']);
144+
112145
self::assertEquals(['id' => 'foo', '!email' => 'encrypted'], $result);
113146
}
114147

@@ -148,10 +181,9 @@ public function testDecryptWithMissingKey(): void
148181
$cipherKeyStore->reveal(),
149182
$cipherKeyFactory->reveal(),
150183
$cipher->reveal(),
151-
false,
152184
);
153185

154-
$result = $cryptographer->decrypt($this->metadata(PersonalDataProfileCreated::class), ['id' => 'foo', '!email' => 'encrypted']);
186+
$result = $cryptographer->decrypt($this->metadata(PersonalDataProfileCreated::class), ['id' => 'foo', 'email' => 'encrypted']);
155187

156188
self::assertEquals(['id' => 'foo', 'email' => new Email('unknown')], $result);
157189
}
@@ -181,15 +213,14 @@ public function testDecryptWithInvalidKey(): void
181213
$cipherKeyStore->reveal(),
182214
$cipherKeyFactory->reveal(),
183215
$cipher->reveal(),
184-
false,
185216
);
186217

187-
$result = $cryptographer->decrypt($this->metadata(PersonalDataProfileCreated::class), ['id' => 'foo', '!email' => 'encrypted']);
218+
$result = $cryptographer->decrypt($this->metadata(PersonalDataProfileCreated::class), ['id' => 'foo', 'email' => 'encrypted']);
188219

189220
self::assertEquals(['id' => 'foo', 'email' => new Email('unknown')], $result);
190221
}
191222

192-
public function testDecryptWithExistingKey(): void
223+
public function testDecryptWithValidKey(): void
193224
{
194225
$cipherKey = new CipherKey(
195226
'foo',
@@ -217,12 +248,45 @@ public function testDecryptWithExistingKey(): void
217248
false,
218249
);
219250

251+
$result = $cryptographer->decrypt($this->metadata(PersonalDataProfileCreated::class), ['id' => 'foo', 'email' => 'encrypted']);
252+
253+
self::assertEquals(['id' => 'foo', 'email' => 'info@patchlevel.de'], $result);
254+
}
255+
256+
public function testDecryptWithValidKeyAndEncryptedFieldName(): void
257+
{
258+
$cipherKey = new CipherKey(
259+
'foo',
260+
'bar',
261+
'baz',
262+
);
263+
264+
$cipherKeyStore = $this->prophesize(CipherKeyStore::class);
265+
$cipherKeyStore->get('foo')->willReturn($cipherKey);
266+
$cipherKeyStore->store('foo', Argument::type(CipherKey::class))->shouldNotBeCalled();
267+
268+
$cipherKeyFactory = $this->prophesize(CipherKeyFactory::class);
269+
$cipherKeyFactory->__invoke()->shouldNotBeCalled();
270+
271+
$cipher = $this->prophesize(Cipher::class);
272+
$cipher
273+
->decrypt($cipherKey, 'encrypted')
274+
->willReturn('info@patchlevel.de')
275+
->shouldBeCalledOnce();
276+
277+
$cryptographer = new PersonalDataPayloadCryptographer(
278+
$cipherKeyStore->reveal(),
279+
$cipherKeyFactory->reveal(),
280+
$cipher->reveal(),
281+
true,
282+
);
283+
220284
$result = $cryptographer->decrypt($this->metadata(PersonalDataProfileCreated::class), ['id' => 'foo', '!email' => 'encrypted']);
221285

222286
self::assertEquals(['id' => 'foo', 'email' => 'info@patchlevel.de'], $result);
223287
}
224288

225-
public function testDecryptWithoutPrefixField(): void
289+
public function testDecryptWithValidKeyAndEncryptedFieldNameWithoutEncryptedData(): void
226290
{
227291
$cipherKey = new CipherKey(
228292
'foo',
@@ -243,15 +307,15 @@ public function testDecryptWithoutPrefixField(): void
243307
$cipherKeyStore->reveal(),
244308
$cipherKeyFactory->reveal(),
245309
$cipher->reveal(),
246-
false,
310+
true,
247311
);
248312

249313
$result = $cryptographer->decrypt($this->metadata(PersonalDataProfileCreated::class), ['id' => 'foo', 'email' => 'info@patchlevel.de']);
250314

251315
self::assertEquals(['id' => 'foo', 'email' => 'info@patchlevel.de'], $result);
252316
}
253317

254-
public function testDecryptWithFallbackWithoutPrefix(): void
318+
public function testDecryptWithValidKeyAndEncryptedFieldNameAndFallbackFieldName(): void
255319
{
256320
$cipherKey = new CipherKey(
257321
'foo',
@@ -276,6 +340,8 @@ public function testDecryptWithFallbackWithoutPrefix(): void
276340
$cipherKeyStore->reveal(),
277341
$cipherKeyFactory->reveal(),
278342
$cipher->reveal(),
343+
true,
344+
true,
279345
);
280346

281347
$result = $cryptographer->decrypt($this->metadata(PersonalDataProfileCreated::class), ['id' => 'foo', 'email' => 'encrypted']);

0 commit comments

Comments
 (0)