|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace Utopia\Pay\Validator\Stripe; |
| 4 | + |
| 5 | +// header |
| 6 | +// t=1723597289,v1=f53b5765cc9847786d33f8f96d9e22c0d08967271a734b1a69327e22ecf1bc73,v0=353c23cbcfc17f983e3089a339d2004174ee472df39e61d7e52805008ffad044 |
| 7 | +// secret |
| 8 | +// whsec_2FMR5OjJa6Czcj3G07HvMGjLsw8uw3dQ |
| 9 | +class Webhook |
| 10 | +{ |
| 11 | + public const DEFAULT_TOLERANCE = 300; |
| 12 | + |
| 13 | + public const EXPECTED_SCHEME = 'v1'; |
| 14 | + |
| 15 | + private static $isHashEqualsAvailable = null; |
| 16 | + |
| 17 | + /** |
| 18 | + * Verifies the signature header sent by Stripe. Throws an |
| 19 | + * Exception\SignatureVerificationException exception if the verification fails for |
| 20 | + * any reason. |
| 21 | + * |
| 22 | + * @param string $payload the payload sent by Stripe |
| 23 | + * @param string $header the contents of the signature header sent by |
| 24 | + * Stripe |
| 25 | + * @param string $secret secret used to generate the signature |
| 26 | + * @param int $tolerance maximum difference allowed between the header's |
| 27 | + * timestamp and the current time |
| 28 | + * @return bool |
| 29 | + */ |
| 30 | + public function isValid($payload, $header, $secret, $tolerance = null) |
| 31 | + { |
| 32 | + // Extract timestamp and signatures from header |
| 33 | + $timestamp = $this->getTimestamp($header); |
| 34 | + $signatures = $this->getSignatures($header, self::EXPECTED_SCHEME); |
| 35 | + if (-1 === $timestamp) { |
| 36 | + return false; |
| 37 | + } |
| 38 | + if (empty($signatures)) { |
| 39 | + return false; |
| 40 | + } |
| 41 | + |
| 42 | + // Check if expected signature is found in list of signatures from |
| 43 | + // header |
| 44 | + $signedPayload = "{$timestamp}.{$payload}"; |
| 45 | + $expectedSignature = $this->computeSignature($signedPayload, $secret); |
| 46 | + $signatureFound = false; |
| 47 | + foreach ($signatures as $signature) { |
| 48 | + if ($this->secureCompare($expectedSignature, $signature)) { |
| 49 | + $signatureFound = true; |
| 50 | + |
| 51 | + break; |
| 52 | + } |
| 53 | + } |
| 54 | + if (! $signatureFound) { |
| 55 | + return false; |
| 56 | + } |
| 57 | + |
| 58 | + // Check if timestamp is within tolerance |
| 59 | + if (($tolerance > 0) && (\abs(\time() - $timestamp) > $tolerance)) { |
| 60 | + return false; |
| 61 | + } |
| 62 | + |
| 63 | + return true; |
| 64 | + } |
| 65 | + |
| 66 | + public function secureCompare($a, $b) |
| 67 | + { |
| 68 | + if (null === self::$isHashEqualsAvailable) { |
| 69 | + self::$isHashEqualsAvailable = \function_exists('hash_equals'); |
| 70 | + } |
| 71 | + |
| 72 | + if (self::$isHashEqualsAvailable) { |
| 73 | + return \hash_equals($a, $b); |
| 74 | + } |
| 75 | + if (\strlen($a) !== \strlen($b)) { |
| 76 | + return false; |
| 77 | + } |
| 78 | + |
| 79 | + $result = 0; |
| 80 | + for ($i = 0; $i < \strlen($a); $i++) { |
| 81 | + $result |= \ord($a[$i]) ^ \ord($b[$i]); |
| 82 | + } |
| 83 | + |
| 84 | + return 0 === $result; |
| 85 | + } |
| 86 | + |
| 87 | + /** |
| 88 | + * Extracts the timestamp in a signature header. |
| 89 | + * |
| 90 | + * @param string $header the signature header |
| 91 | + * @return int the timestamp contained in the header, or -1 if no valid |
| 92 | + * timestamp is found |
| 93 | + */ |
| 94 | + private function getTimestamp($header) |
| 95 | + { |
| 96 | + $items = \explode(',', $header); |
| 97 | + |
| 98 | + foreach ($items as $item) { |
| 99 | + $itemParts = \explode('=', $item, 2); |
| 100 | + if ('t' === $itemParts[0]) { |
| 101 | + if (! \is_numeric($itemParts[1])) { |
| 102 | + return -1; |
| 103 | + } |
| 104 | + |
| 105 | + return (int) ($itemParts[1]); |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + return -1; |
| 110 | + } |
| 111 | + |
| 112 | + /** |
| 113 | + * Extracts the signatures matching a given scheme in a signature header. |
| 114 | + * |
| 115 | + * @param string $header the signature header |
| 116 | + * @param string $scheme the signature scheme to look for |
| 117 | + * @return array the list of signatures matching the provided scheme |
| 118 | + */ |
| 119 | + private function getSignatures($header, $scheme) |
| 120 | + { |
| 121 | + $signatures = []; |
| 122 | + $items = \explode(',', $header); |
| 123 | + |
| 124 | + foreach ($items as $item) { |
| 125 | + $itemParts = \explode('=', $item, 2); |
| 126 | + if (\trim($itemParts[0]) === $scheme) { |
| 127 | + $signatures[] = $itemParts[1]; |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + return $signatures; |
| 132 | + } |
| 133 | + |
| 134 | + /** |
| 135 | + * Computes the signature for a given payload and secret. |
| 136 | + * |
| 137 | + * The current scheme used by Stripe ("v1") is HMAC/SHA-256. |
| 138 | + * |
| 139 | + * @param string $payload the payload to sign |
| 140 | + * @param string $secret the secret used to generate the signature |
| 141 | + * @return string the signature as a string |
| 142 | + */ |
| 143 | + private function computeSignature($payload, $secret) |
| 144 | + { |
| 145 | + return \hash_hmac('sha256', $payload, $secret); |
| 146 | + } |
| 147 | +} |
0 commit comments