14
14
*/
15
15
final class CookieStore implements StoreInterface
16
16
{
17
- public const USE_CRYPTO = true ;
18
17
public const KEY_HASHING_ALGO = 'sha256 ' ;
19
- public const KEY_CHUNKING_THRESHOLD = 3072 ;
18
+ public const KEY_CHUNKING_THRESHOLD = 2048 ;
20
19
public const KEY_SEPARATOR = '_ ' ;
21
20
public const VAL_CRYPTO_ALGO = 'aes-128-gcm ' ;
22
21
@@ -48,6 +47,16 @@ final class CookieStore implements StoreInterface
48
47
*/
49
48
private bool $ deferring = false ;
50
49
50
+ /**
51
+ * Determine if changes have been made since the last setState.
52
+ */
53
+ private bool $ dirty = false ;
54
+
55
+ /**
56
+ * Determine if changes have been made since the last setState.
57
+ */
58
+ private bool $ encrypt = true ;
59
+
51
60
/**
52
61
* CookieStore constructor.
53
62
*
@@ -68,13 +77,7 @@ public function __construct(
68
77
69
78
$ this ->configuration = $ configuration ;
70
79
$ this ->namespace = (string ) $ namespace ;
71
-
72
- // @phpstan-ignore-next-line
73
- if (self ::USE_CRYPTO ) {
74
- $ this ->namespace = hash (self ::KEY_HASHING_ALGO , $ this ->namespace );
75
- }
76
-
77
- $ this ->threshold = self ::KEY_CHUNKING_THRESHOLD - strlen ($ this ->namespace ) + 2 ;
80
+ $ this ->threshold = self ::KEY_CHUNKING_THRESHOLD - strlen ($ this ->namespace );
78
81
79
82
$ this ->getState ();
80
83
}
@@ -95,6 +98,25 @@ public function getThreshold(): int
95
98
return $ this ->threshold ;
96
99
}
97
100
101
+ /**
102
+ * Returns the current encryption state
103
+ */
104
+ public function getEncrypted (): bool
105
+ {
106
+ return $ this ->encrypt ;
107
+ }
108
+
109
+ /**
110
+ * Toggle the encryption state
111
+ *
112
+ * @param bool $encrypt Enable or disable cookie encryption.
113
+ */
114
+ public function setEncrypted (bool $ encrypt = true ): self
115
+ {
116
+ $ this ->encrypt = $ encrypt ;
117
+ return $ this ;
118
+ }
119
+
98
120
/**
99
121
* Defer saving state changes to destination to improve performance during blocks of changes.
100
122
*
@@ -103,14 +125,13 @@ public function getThreshold(): int
103
125
public function defer (
104
126
bool $ deferring
105
127
): void {
128
+ $ this ->deferring = $ deferring ;
129
+
106
130
// If we were deferring state saving and we've been asked to cancel that deference
107
- if ($ this -> deferring && ! $ deferring ) {
131
+ if (! $ deferring ) {
108
132
// Immediately push the state to the host device.
109
133
$ this ->setState ();
110
134
}
111
-
112
- // Update our deference state.
113
- $ this ->deferring = $ deferring ;
114
135
}
115
136
116
137
/**
@@ -125,6 +146,10 @@ public function getState(
125
146
): array {
126
147
// Overwrite our internal state with one passed (presumably during unit tests.)
127
148
if ($ state !== null ) {
149
+ if ($ this ->store !== $ state ) {
150
+ $ this ->dirty = true ;
151
+ }
152
+
128
153
return $ this ->store = $ state ;
129
154
}
130
155
@@ -151,7 +176,7 @@ public function getState(
151
176
}
152
177
153
178
// If no cookies were found, set an empty state and continue.
154
- if (mb_strlen ( $ data) === 0 ) {
179
+ if ($ data === '' ) {
155
180
return $ this ->store = [];
156
181
}
157
182
@@ -171,9 +196,16 @@ public function getState(
171
196
172
197
/**
173
198
* Push our storage state to the source for persistence.
199
+ *
200
+ * @psalm-suppress UnusedFunctionCall
174
201
*/
175
- public function setState (): self
176
- {
202
+ public function setState (
203
+ bool $ force = false
204
+ ): self {
205
+ if (!$ this ->dirty && !$ force ) {
206
+ return $ this ;
207
+ }
208
+
177
209
$ setOptions = $ this ->getCookieOptions ();
178
210
$ deleteOptions = $ this ->getCookieOptions (-1000 );
179
211
$ existing = [];
@@ -212,7 +244,7 @@ public function setState(): self
212
244
// @codeCoverageIgnoreStart
213
245
if (! defined ('AUTH0_TESTS_DIR ' )) {
214
246
/** @var array{expires?: int, path?: string, domain?: string, secure?: bool, httponly?: bool, samesite?: 'Lax'|'lax'|'None'|'none'|'Strict'|'strict', url_encode?: int} $setOptions */
215
- setcookie ($ cookieName , $ chunk , $ setOptions );
247
+ setrawcookie ($ cookieName , $ chunk , $ setOptions );
216
248
}
217
249
// @codeCoverageIgnoreEnd
218
250
@@ -233,14 +265,15 @@ public function setState(): self
233
265
// @codeCoverageIgnoreStart
234
266
if (! defined ('AUTH0_TESTS_DIR ' )) {
235
267
/** @var array{expires?: int, path?: string, domain?: string, secure?: bool, httponly?: bool, samesite?: 'Lax'|'lax'|'None'|'none'|'Strict'|'strict', url_encode?: int} $deleteOptions */
236
- setcookie ($ cookieName , '' , $ deleteOptions );
268
+ setrawcookie ($ cookieName , '' , $ deleteOptions );
237
269
}
238
270
// @codeCoverageIgnoreEnd
239
271
240
272
// Clear PHP's internal COOKIE global of the orphaned cookie.
241
273
unset($ _COOKIE [$ cookieName ]);
242
274
}
243
275
276
+ $ this ->dirty = false ;
244
277
return $ this ;
245
278
}
246
279
@@ -260,7 +293,10 @@ public function set(
260
293
[$ key , \Auth0 \SDK \Exception \ArgumentException::missing ('key ' )],
261
294
])->isString ();
262
295
263
- $ this ->store [(string ) $ key ] = $ value ;
296
+ if (! isset ($ this ->store [(string ) $ key ]) || $ this ->store [(string ) $ key ] !== $ value ) {
297
+ $ this ->store [(string ) $ key ] = $ value ;
298
+ $ this ->dirty = true ;
299
+ }
264
300
265
301
if (! $ this ->deferring ) {
266
302
$ this ->setState ();
@@ -303,7 +339,10 @@ public function delete(
303
339
[$ key , \Auth0 \SDK \Exception \ArgumentException::missing ('key ' )],
304
340
])->isString ();
305
341
306
- unset($ this ->store [(string ) $ key ]);
342
+ if (isset ($ this ->store [(string ) $ key ])) {
343
+ unset($ this ->store [(string ) $ key ]);
344
+ $ this ->dirty = true ;
345
+ }
307
346
308
347
if (! $ this ->deferring ) {
309
348
$ this ->setState ();
@@ -315,7 +354,10 @@ public function delete(
315
354
*/
316
355
public function purge (): void
317
356
{
318
- $ this ->store = [];
357
+ if ($ this ->store !== []) {
358
+ $ this ->store = [];
359
+ $ this ->dirty = true ;
360
+ }
319
361
320
362
if (! $ this ->deferring ) {
321
363
$ this ->setState ();
@@ -351,10 +393,9 @@ public function getCookieOptions(
351
393
$ options ['samesite ' ] = 'Lax ' ;
352
394
}
353
395
354
- $ domain = $ this ->configuration ->getCookieDomain () ?? $ _SERVER [ ' HTTP_HOST ' ] ?? null ;
396
+ $ domain = $ this ->configuration ->getCookieDomain () ?? null ;
355
397
356
- if ($ domain !== null ) {
357
- /** @var string $domain */
398
+ if ($ domain !== null && $ domain !== $ _SERVER ['HTTP_HOST ' ]) {
358
399
$ options ['domain ' ] = $ domain ;
359
400
}
360
401
@@ -364,47 +405,70 @@ public function getCookieOptions(
364
405
/**
365
406
* Encrypt data for safe storage format for a cookie.
366
407
*
367
- * @param array<mixed> $data Data to encrypt.
408
+ * @param array<mixed> $data Data to encrypt.
409
+ * @param array<mixed> $options Additional configuration options.
368
410
*
369
411
* @psalm-suppress TypeDoesNotContainType
370
412
*/
371
- private function encrypt (
372
- array $ data
413
+ public function encrypt (
414
+ array $ data ,
415
+ array $ options = []
373
416
): string {
374
- // @codeCoverageIgnoreStart
375
- // @phpstan-ignore-next-line
376
- if (! self ::USE_CRYPTO ) {
377
- return base64_encode (json_encode (serialize ($ data ), JSON_THROW_ON_ERROR ));
417
+ if (! $ this ->encrypt ) {
418
+ $ data = $ options ['encoded1 ' ] ?? json_encode ($ data );
419
+
420
+ if (! is_string ($ data )) {
421
+ return '' ;
422
+ }
423
+
424
+ return rawurlencode ($ data );
378
425
}
379
- // @codeCoverageIgnoreEnd
380
426
381
427
$ secret = $ this ->configuration ->getCookieSecret ();
382
- $ ivLen = openssl_cipher_iv_length (self ::VAL_CRYPTO_ALGO );
428
+ $ ivLen = $ options ['ivLen ' ] ?? openssl_cipher_iv_length (self ::VAL_CRYPTO_ALGO );
429
+ $ tag = null ;
383
430
384
431
if ($ secret === null ) {
385
432
throw \Auth0 \SDK \Exception \ConfigurationException::requiresCookieSecret ();
386
433
}
387
434
388
- // @codeCoverageIgnoreStart
389
- if ($ ivLen === false ) {
435
+ if (! is_int ($ ivLen )) {
436
+ return '' ;
437
+ }
438
+
439
+ $ iv = $ options ['iv ' ] ?? openssl_random_pseudo_bytes ($ ivLen );
440
+
441
+ if (! is_string ($ iv )) {
442
+ return '' ;
443
+ }
444
+
445
+ $ data = $ options ['encoded1 ' ] ?? json_encode ($ data );
446
+
447
+ if (! is_string ($ data )) {
390
448
return '' ;
391
449
}
392
- // @codeCoverageIgnoreEnd
393
450
394
- $ iv = openssl_random_pseudo_bytes ($ ivLen );
451
+ // Encrypt the PHP array.
452
+ $ encrypted = $ options ['encrypted ' ] ?? openssl_encrypt ($ data , self ::VAL_CRYPTO_ALGO , $ secret , 0 , $ iv , $ tag );
453
+ $ iv = $ options ['iv ' ] ?? $ iv ;
454
+ $ tag = $ options ['tag ' ] ?? $ tag ;
395
455
396
- // @codeCoverageIgnoreStart
397
- // @phpstan-ignore-next-line
398
- if ($ iv === false ) {
456
+ if (! is_string ($ encrypted )) {
399
457
return '' ;
400
458
}
401
- // @codeCoverageIgnoreEnd
402
459
403
- // Encrypt the serialized PHP array.
404
- $ encrypted = openssl_encrypt (serialize ($ data ), self ::VAL_CRYPTO_ALGO , $ secret , 0 , $ iv , $ tag );
460
+ if (! is_string ($ tag )) {
461
+ return '' ;
462
+ }
405
463
406
464
// Return a JSON encoded object containing the crypto tag and iv, and the encrypted data.
407
- return json_encode (serialize (['tag ' => base64_encode ($ tag ), 'iv ' => base64_encode ($ iv ), 'data ' => $ encrypted ]), JSON_THROW_ON_ERROR );
465
+ $ encoded = $ options ['encoded2 ' ] ?? json_encode (['tag ' => base64_encode ($ tag ), 'iv ' => base64_encode ($ iv ), 'data ' => $ encrypted ]);
466
+
467
+ if (is_string ($ encoded )) {
468
+ return rawurlencode ($ encoded );
469
+ }
470
+
471
+ return '' ;
408
472
}
409
473
410
474
/**
@@ -416,30 +480,19 @@ private function encrypt(
416
480
*
417
481
* @psalm-suppress TypeDoesNotContainType
418
482
*/
419
- private function decrypt (
483
+ public function decrypt (
420
484
string $ data
421
485
) {
422
- // @codeCoverageIgnoreStart
423
- // @phpstan-ignore-next-line
424
- if (! self ::USE_CRYPTO ) {
425
- $ returns = [];
426
- $ decoded = base64_decode ($ data , true );
427
-
428
- if (is_string ($ decoded )) {
429
- $ decoded = json_decode ($ decoded , true , 512 , JSON_THROW_ON_ERROR );
430
- }
431
-
432
- if (is_string ($ decoded )) {
433
- $ decoded = unserialize ($ decoded );
434
- }
486
+ if (! $ this ->encrypt ) {
487
+ $ decoded = rawurldecode ($ data );
488
+ $ decoded = json_decode ($ decoded , true );
435
489
436
490
if (is_array ($ decoded )) {
437
- $ returns = $ decoded ;
491
+ return $ decoded ;
438
492
}
439
493
440
- return $ returns ;
494
+ return [] ;
441
495
}
442
- // @codeCoverageIgnoreEnd
443
496
444
497
[$ data ] = Toolkit::filter ([$ data ])->string ()->trim ();
445
498
@@ -453,14 +506,11 @@ private function decrypt(
453
506
throw \Auth0 \SDK \Exception \ConfigurationException::requiresCookieSecret ();
454
507
}
455
508
456
- $ data = json_decode ((string ) $ data , true );
509
+ $ decoded = rawurldecode ((string ) $ data );
510
+ $ stripped = stripslashes ($ decoded );
511
+ $ data = json_decode ($ stripped , true , 512 );
457
512
458
- if (! is_string ($ data )) {
459
- return null ;
460
- }
461
-
462
- /** @var array{iv?: int|string|null, tag?: int|string|null, data: string} */
463
- $ data = unserialize ($ data );
513
+ /** @var array{iv?: int|string|null, tag?: int|string|null, data: string} $data */
464
514
465
515
if (! isset ($ data ['iv ' ]) || ! isset ($ data ['tag ' ]) || ! is_string ($ data ['iv ' ]) || ! is_string ($ data ['tag ' ])) {
466
516
return null ;
@@ -469,17 +519,17 @@ private function decrypt(
469
519
$ iv = base64_decode ($ data ['iv ' ], true );
470
520
$ tag = base64_decode ($ data ['tag ' ], true );
471
521
472
- if ($ iv === false || $ tag === false ) {
522
+ if (! is_string ( $ iv) || ! is_string ( $ tag) ) {
473
523
return null ;
474
524
}
475
525
476
526
$ data = openssl_decrypt ($ data ['data ' ], self ::VAL_CRYPTO_ALGO , $ secret , 0 , $ iv , $ tag );
477
527
478
- if ($ data === false ) {
528
+ if (! is_string ( $ data) ) {
479
529
return null ;
480
530
}
481
531
482
- $ data = unserialize ($ data );
532
+ $ data = json_decode ($ data, true );
483
533
484
534
/** @var array<mixed> $data */
485
535
return $ data ;
0 commit comments