44
55namespace Patchlevel \Hydrator \Cryptography ;
66
7+ use Closure ;
8+ use Patchlevel \Hydrator \Cryptography \Cipher \Cipher ;
9+ use Patchlevel \Hydrator \Cryptography \Cipher \CipherKeyFactory ;
10+ use Patchlevel \Hydrator \Cryptography \Cipher \DecryptionFailed ;
11+ use Patchlevel \Hydrator \Cryptography \Cipher \OpensslCipher ;
12+ use Patchlevel \Hydrator \Cryptography \Cipher \OpensslCipherKeyFactory ;
13+ use Patchlevel \Hydrator \Cryptography \Store \CipherKeyNotExists ;
14+ use Patchlevel \Hydrator \Cryptography \Store \CipherKeyStore ;
715use Patchlevel \Hydrator \Metadata \ClassMetadata ;
816use Patchlevel \Hydrator \Middleware \Middleware ;
917use Patchlevel \Hydrator \Middleware \Stack ;
18+ use Stringable ;
19+
20+ use function array_key_exists ;
21+ use function assert ;
22+ use function is_array ;
23+ use function is_int ;
24+ use function is_string ;
1025
1126final class CryptographyMiddleware implements Middleware
1227{
28+ private const DEFAULT_ENCRYPTED_FIELD_NAME_PREFIX = '! ' ;
29+
1330 public function __construct (
14- private readonly PayloadCryptographer $ cryptography ,
31+ private readonly Cipher $ cipher ,
32+ private readonly CipherKeyStore $ cipherKeyStore ,
33+ private readonly CipherKeyFactory $ cipherKeyFactory ,
34+ private readonly string |null $ encryptedFieldNamePrefix = self ::DEFAULT_ENCRYPTED_FIELD_NAME_PREFIX ,
1535 ) {
1636 }
1737
@@ -26,9 +46,72 @@ public function __construct(
2646 */
2747 public function hydrate (ClassMetadata $ metadata , array $ data , array $ context , Stack $ stack ): object
2848 {
49+ $ subjectIds = $ context [SubjectIds::class] ?? new SubjectIds ();
50+
51+ assert ($ subjectIds instanceof SubjectIds);
52+
53+ $ mapping = $ metadata ->extras [SubjectIdFieldMapping::class] ?? null ;
54+
55+ if ($ mapping instanceof SubjectIdFieldMapping) {
56+ $ subjectIds = $ this ->resolveSubjectIds ($ metadata , $ mapping , $ data )
57+ ->merge ($ subjectIds );
58+ }
59+
60+ $ context [SubjectIds::class] = $ subjectIds ;
61+
62+ foreach ($ metadata ->properties as $ propertyMetadata ) {
63+ $ sensitiveDataInfo = $ propertyMetadata ->extras [SensitiveDataInfo::class] ?? null ;
64+
65+ if (!$ sensitiveDataInfo instanceof SensitiveDataInfo) {
66+ continue ;
67+ }
68+
69+ $ subjectId = $ subjectIds ->get ($ sensitiveDataInfo ->subjectIdName );
70+
71+ try {
72+ $ cipherKey = $ this ->cipherKeyStore ->get ($ subjectId );
73+ } catch (CipherKeyNotExists ) {
74+ $ cipherKey = null ;
75+ }
76+
77+ if (
78+ $ this ->encryptedFieldNamePrefix && array_key_exists (
79+ $ this ->encryptedFieldNamePrefix . $ propertyMetadata ->fieldName ,
80+ $ data ,
81+ )
82+ ) {
83+ $ rawData = $ data [$ this ->encryptedFieldNamePrefix . $ propertyMetadata ->fieldName ];
84+ unset($ data [$ this ->encryptedFieldNamePrefix . $ propertyMetadata ->fieldName ]);
85+ } elseif (!$ this ->encryptedFieldNamePrefix ) {
86+ $ rawData = $ data [$ propertyMetadata ->fieldName ];
87+ } else {
88+ continue ;
89+ }
90+
91+ if (!is_string ($ rawData )) {
92+ $ data [$ propertyMetadata ->fieldName ] = $ rawData ;
93+
94+ continue ;
95+ }
96+
97+ if (!$ cipherKey ) {
98+ $ data [$ propertyMetadata ->fieldName ] = $ this ->fallback ($ sensitiveDataInfo , $ subjectId , $ rawData );
99+ continue ;
100+ }
101+
102+ try {
103+ $ data [$ propertyMetadata ->fieldName ] = $ this ->cipher ->decrypt (
104+ $ cipherKey ,
105+ $ rawData ,
106+ );
107+ } catch (DecryptionFailed ) {
108+ $ data [$ propertyMetadata ->fieldName ] = $ this ->fallback ($ sensitiveDataInfo , $ subjectId , $ rawData );
109+ }
110+ }
111+
29112 return $ stack ->next ()->hydrate (
30113 $ metadata ,
31- $ this -> cryptography -> decrypt ( $ metadata , $ data) ,
114+ $ data ,
32115 $ context ,
33116 $ stack ,
34117 );
@@ -45,9 +128,119 @@ public function hydrate(ClassMetadata $metadata, array $data, array $context, St
45128 */
46129 public function extract (ClassMetadata $ metadata , object $ object , array $ context , Stack $ stack ): array
47130 {
48- return $ this ->cryptography ->encrypt (
49- $ metadata ,
50- $ stack ->next ()->extract ($ metadata , $ object , $ context , $ stack ),
131+ $ subjectIds = $ context [SubjectIds::class] ?? new SubjectIds ();
132+
133+ assert ($ subjectIds instanceof SubjectIds);
134+
135+ $ mapping = $ metadata ->extras [SubjectIdFieldMapping::class] ?? null ;
136+
137+ if ($ mapping instanceof SubjectIdFieldMapping) {
138+ $ subjectIds = $ this ->resolveSubjectIds ($ metadata , $ mapping , $ object )
139+ ->merge ($ subjectIds );
140+ }
141+
142+ $ context [SubjectIds::class] = $ subjectIds ;
143+
144+ $ data = $ stack ->next ()->extract ($ metadata , $ object , $ context , $ stack );
145+
146+ foreach ($ metadata ->properties as $ propertyMetadata ) {
147+ $ sensitiveDataInfo = $ propertyMetadata ->extras [SensitiveDataInfo::class] ?? null ;
148+
149+ if (!$ sensitiveDataInfo instanceof SensitiveDataInfo) {
150+ continue ;
151+ }
152+
153+ $ subjectId = $ subjectIds ->get ($ sensitiveDataInfo ->subjectIdName );
154+
155+ try {
156+ $ cipherKey = $ this ->cipherKeyStore ->get ($ subjectId );
157+ } catch (CipherKeyNotExists ) {
158+ $ cipherKey = ($ this ->cipherKeyFactory )();
159+ $ this ->cipherKeyStore ->store ($ subjectId , $ cipherKey );
160+ }
161+
162+ $ targetFieldName = $ this ->encryptedFieldNamePrefix
163+ ? $ this ->encryptedFieldNamePrefix . $ propertyMetadata ->fieldName
164+ : $ propertyMetadata ->fieldName ;
165+
166+ $ data [$ targetFieldName ] = $ this ->cipher ->encrypt (
167+ $ cipherKey ,
168+ $ data [$ propertyMetadata ->fieldName ],
169+ );
170+
171+ if (!$ this ->encryptedFieldNamePrefix ) {
172+ continue ;
173+ }
174+
175+ unset($ data [$ propertyMetadata ->fieldName ]);
176+ }
177+
178+ return $ data ;
179+ }
180+
181+ /** @param array<string, mixed>|object $data */
182+ private function resolveSubjectIds (
183+ ClassMetadata $ metadata ,
184+ SubjectIdFieldMapping $ mapping ,
185+ array |object $ data ,
186+ ): SubjectIds {
187+ $ result = [];
188+
189+ foreach ($ mapping ->nameToField as $ name => $ fieldName ) {
190+ if (is_array ($ data )) {
191+ if (!array_key_exists ($ fieldName , $ data )) {
192+ throw new MissingSubjectIdField ($ metadata ->className , $ fieldName );
193+ }
194+
195+ $ subjectId = $ data [$ fieldName ];
196+ } else {
197+ $ property = $ metadata ->propertyForField ($ fieldName );
198+
199+ if ($ property ->normalizer ) {
200+ $ subjectId = $ property ->normalizer ->normalize ($ property ->getValue ($ data ));
201+ } else {
202+ $ subjectId = $ property ->getValue ($ data );
203+ }
204+ }
205+
206+ if (is_int ($ subjectId )) {
207+ $ subjectId = (string )$ subjectId ;
208+ }
209+
210+ if ($ subjectId instanceof Stringable) {
211+ $ subjectId = $ subjectId ->__toString ();
212+ }
213+
214+ if (!is_string ($ subjectId )) {
215+ throw new UnsupportedSubjectId ($ metadata ->className , $ fieldName , $ subjectId );
216+ }
217+
218+ $ result [$ name ] = $ subjectId ;
219+ }
220+
221+ return new SubjectIds ($ result );
222+ }
223+
224+ private function fallback (SensitiveDataInfo $ sensitiveDataInfo , string $ subjectId , mixed $ rawData ): mixed
225+ {
226+ if ($ sensitiveDataInfo ->fallback instanceof Closure) {
227+ return ($ sensitiveDataInfo ->fallback )($ subjectId , $ rawData );
228+ }
229+
230+ return $ sensitiveDataInfo ->fallback ;
231+ }
232+
233+ /** @param non-empty-string $method */
234+ public static function createWithOpenssl (
235+ CipherKeyStore $ cryptoStore ,
236+ string $ method = OpensslCipherKeyFactory::DEFAULT_METHOD ,
237+ string |null $ encryptedFieldNamePrefix = self ::DEFAULT_ENCRYPTED_FIELD_NAME_PREFIX ,
238+ ): static {
239+ return new self (
240+ new OpensslCipher (),
241+ $ cryptoStore ,
242+ new OpensslCipherKeyFactory ($ method ),
243+ $ encryptedFieldNamePrefix ,
51244 );
52245 }
53246}
0 commit comments