PaivanaCookie.php (3017B)
1 <?php 2 3 /** 4 * @file 5 * Helper for the Turnstile Paivana access cookie. 6 */ 7 8 namespace Drupal\taler_turnstile; 9 10 use Drupal\Core\PrivateKey; 11 use Symfony\Component\HttpFoundation\Cookie; 12 use Symfony\Component\HttpFoundation\Request; 13 14 /** 15 * Mints and verifies the cookie that grants access to a previously 16 * paid-for fulfillment URL. The cookie is bound to the visitor's IP 17 * address and the fulfillment URL, and carries its own expiration in 18 * its prefix: 19 * 20 * value := <cur_time>-<base64url(HMAC-SHA256(secret, cur_time||url||ip))> 21 * 22 * Mirrors the paivana-httpd implementation but uses HMAC-SHA256 with 23 * Drupal's private_key as keying material so we do not need a new 24 * config storage for a server-side secret. 25 */ 26 class PaivanaCookie { 27 28 /** 29 * Cookie name; must match the constant exported from the .module file. 30 */ 31 const COOKIE_NAME = 'taler_turnstile_paivana'; 32 33 /** 34 * @var \Drupal\Core\PrivateKey 35 */ 36 protected $privateKey; 37 38 public function __construct(PrivateKey $private_key) { 39 $this->privateKey = $private_key; 40 } 41 42 43 /** 44 * Compute the keyed hash for ($cur_time, $website, $client_addr). 45 * Returned as URL-safe base64 without padding. 46 */ 47 protected function hash(int $cur_time, string $website, string $client_addr): string { 48 $msg = pack('NN', 0, $cur_time) . $website . "\0" . $client_addr; 49 $raw = hash_hmac('sha256', $msg, $this->privateKey->get(), TRUE); 50 return rtrim(strtr(base64_encode($raw), '+/', '-_'), '='); 51 } 52 53 54 /** 55 * Build a cookie carrying access for $website, valid until 56 * now + $lifetime_seconds. Cookie is bound to the request's 57 * client address. 58 * 59 * @return \Symfony\Component\HttpFoundation\Cookie 60 */ 61 public function mint(Request $request, string $website, int $lifetime_seconds): Cookie { 62 $expires = time() + $lifetime_seconds; 63 $client_addr = $request->getClientIp() ?? ''; 64 $value = $expires . '-' . $this->hash($expires, $website, $client_addr); 65 return Cookie::create( 66 self::COOKIE_NAME, 67 $value, 68 $expires, 69 '/', 70 NULL, 71 $request->isSecure(), 72 TRUE, 73 FALSE, 74 Cookie::SAMESITE_LAX 75 ); 76 } 77 78 79 /** 80 * Check whether the request carries a valid Turnstile cookie for 81 * $website. Returns TRUE only if the cookie's MAC matches and its 82 * timestamp is still in the future. 83 */ 84 public function verify(Request $request, string $website): bool { 85 $value = $request->cookies->get(self::COOKIE_NAME); 86 if (! is_string($value) || $value === '') { 87 return FALSE; 88 } 89 $dash = strpos($value, '-'); 90 if ($dash === FALSE) { 91 return FALSE; 92 } 93 $ts_part = substr($value, 0, $dash); 94 $mac_part = substr($value, $dash + 1); 95 if (! ctype_digit($ts_part)) { 96 return FALSE; 97 } 98 $ts = (int) $ts_part; 99 if ($ts <= time()) { 100 return FALSE; 101 } 102 $client_addr = $request->getClientIp() ?? ''; 103 $expected = $this->hash($ts, $website, $client_addr); 104 return hash_equals($expected, $mac_part); 105 } 106 107 }