From 828dc867a1f3eeb650530284ade88c1c18cfaf46 Mon Sep 17 00:00:00 2001 From: flashwave Date: Tue, 11 Jul 2023 21:59:09 +0000 Subject: [PATCH] Made the CSRF protection less needlessly complex. --- VERSION | 2 +- src/Security/CSRFP.php | 128 ++++++++++++++++----------------- src/Security/CSRFPIdentity.php | 79 -------------------- src/Security/CSRFPToken.php | 101 -------------------------- tests/CSRFPTest.php | 99 ++++++++++++------------- 5 files changed, 109 insertions(+), 300 deletions(-) delete mode 100644 src/Security/CSRFPIdentity.php delete mode 100644 src/Security/CSRFPToken.php diff --git a/VERSION b/VERSION index e5404f0..bdd2a71 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2307.102242 +0.2307.112158 diff --git a/src/Security/CSRFP.php b/src/Security/CSRFP.php index 82c1c7e..b2ac4c6 100644 --- a/src/Security/CSRFP.php +++ b/src/Security/CSRFP.php @@ -1,104 +1,104 @@ secretKey = $secretKey; + $this->identity = $identity; $this->tolerance = $tolerance; - $this->epoch = $epoch; - $this->hashAlgo = $hashAlgo; + } + + private static function time(int $time = -1): int { + return ($time < 0 ? time() : $time) - self::EPOCH; + } + + private function createHash(int $time, string $nonce): string { + return hash_hmac(self::HASH_ALGO, "{$this->identity}!{$time}!{$nonce}", $this->secretKey, true); } /** - * Gets the amount of time a token should remain valid for. + * Creates a token string. * - * @return int Token expiry tolerance. - */ - public function getTolerance(): int { - return $this->tolerance; - } - - /** - * Gets an identity instance. - * - * @param string $identity String that represents the identity. - * @return CSRFPIdentity An identity instance representing the given string. - */ - public function getIdentity(string $identity): CSRFPIdentity { - return $this->identities[$identity] ?? ($this->identities[$identity] = new CSRFPIdentity($this, $identity)); - } - - /** - * Returns the time with the offset applied. - * - * @return int Current time, with offset. - */ - public function time(): int { - return time() - $this->epoch; - } - - /** - * Creates a hash with the given arguments. - * - * @param string $identity Identity string to verify for. - * @param int $timestamp Timestamp to verify. - * @param int $tolerance Tolerance to verify. - * @return string Hash representing the given arguments. - */ - public function createHash(string $identity, int $timestamp, int $tolerance): string { - return hash_hmac($this->hashAlgo, "{$identity}!{$timestamp}!{$tolerance}", $this->secretKey, true); - } - - /** - * Creates a token string using a given identity string. - * - * @param string $identity Identity to create token for. + * @param int $time Timestamp to generate the token for, -1 (default) for now. * @return string Token string. */ - public function createToken(string $identity): string { - return $this->getIdentity($identity)->createToken(); + public function createToken(int $time = -1): string { + $time = self::time($time); + $nonce = random_bytes(self::NONCE_LENGTH); + $hash = $this->createHash($time, $nonce); + + return Serialiser::uriBase64()->serialise( + pack('V', $time) . $nonce . $hash + ); } /** - * Verifies a token string using a given identity string. + * Verifies a token string. * - * @param string $identity Identity to test the token with. * @param string $token Token to test. + * @param int $tolerance Amount of seconds for which the token can remain valid, < 0 for whatever the default value is. + * @param int $time Point in time for which to check validity for this time. * @return bool true if the token is valid, false if not. */ - public function verifyToken(string $identity, string $token): bool { - return $this->getIdentity($identity)->verifyToken(CSRFPToken::decode($token)); + public function verifyToken(string $token, int $tolerance = -1, int $time = -1): bool { + if($tolerance === 0) + return false; + if($tolerance < 0) + $tolerance = $this->tolerance; + + $token = Serialiser::uriBase64()->deserialise($token); + if(empty($token)) + return false; + + $uTime = -1; + extract(unpack('VuTime', $token)); + if($uTime < 0) + return false; + + $uNonce = substr($token, self::TIMESTAMP_LENGTH, self::NONCE_LENGTH); + if(empty($uNonce)) + return false; + + $uHash = substr($token, self::TIMESTAMP_LENGTH + self::NONCE_LENGTH, self::HASH_LENGTH); + if(empty($uHash)) + return false; + + $rTime = self::time($time); + + return ($rTime - ($uTime + $tolerance)) < 0 + && hash_equals($this->createHash($uTime, $uNonce), $uHash); } } diff --git a/src/Security/CSRFPIdentity.php b/src/Security/CSRFPIdentity.php deleted file mode 100644 index 551e642..0000000 --- a/src/Security/CSRFPIdentity.php +++ /dev/null @@ -1,79 +0,0 @@ -owner = $owner; - $this->identity = $identity; - } - - /** - * Gets a reference to the owner CSRFP object. - * - * @return CSRFP Owner object. - */ - public function getOwner(): CSRFP { - return $this->owner; - } - - /** - * Gets the string for this identity. - */ - public function getIdentity(): string { - return $this->identity; - } - - /** - * Creates a new token using this identity. - * - * @return CSRFPToken Newly created token. - */ - public function createToken(): CSRFPToken { - $timestamp = $this->owner->time(); - $tolerance = $this->owner->getTolerance(); - $hash = $this->owner->createHash($this->identity, $timestamp, $tolerance); - return new CSRFPToken($timestamp, $tolerance, $hash); - } - - /** - * Verifies a token using this identity. - * - * @param CSRFPToken $token Token to verify. - * @return bool true if the token is valid, false if not. - */ - public function verifyToken(CSRFPToken $token): bool { - $timestamp = $token->getTimestamp(); - $tolerance = $token->getTolerance(); - $tHash = $token->getHash(); - - // invalid for sure, defaults for decode failure - if($timestamp < 0 || $tolerance < 1 || empty($tHash)) - return false; - - $currentTime = $this->owner->time(); - if($currentTime < $timestamp - || $currentTime > ($timestamp + $tolerance)) - return false; - - $rHash = $this->owner->createHash($this->identity, $timestamp, $tolerance); - - return hash_equals($rHash, $tHash); - } -} diff --git a/src/Security/CSRFPToken.php b/src/Security/CSRFPToken.php deleted file mode 100644 index 4300636..0000000 --- a/src/Security/CSRFPToken.php +++ /dev/null @@ -1,101 +0,0 @@ -timestamp = $timestamp; - $this->tolerance = $tolerance; - $this->hash = $hash; - } - - /** - * Gets the timestamp value of this token. - * - * @return int Timestamp for this token. - */ - public function getTimestamp(): int { - return $this->timestamp; - } - - /** - * Gets the tolerance value of this token. - * - * @return int Tolerance for this token. - */ - public function getTolerance(): int { - return $this->tolerance; - } - - /** - * Gets the hash of this token. - * - * @return string Hash for this token. - */ - public function getHash(): string { - return $this->hash; - } - - /** - * Encodes the CSRFPToken instance as a token string. - * - * @return string CSRF prevention token string. - */ - public function encode(): string { - return Serialiser::uriBase64()->serialise(pack('Vv', $this->timestamp, $this->tolerance) . $this->hash); - } - - /** - * Decodes a token string to a CSRFPToken instance. - * - * If an invalid token is provided, no exception will be thrown. - * - * @param string $token Input token string. - * @return CSRFPToken Instance representing the provided token. - */ - public static function decode(string $token): CSRFPToken { - $token = Serialiser::uriBase64()->deserialise($token); - try { - $decode = unpack('Vtimestamp/vtolerance', $token); - } catch(ErrorException $ex) { - $decode = [ - 'timestamp' => -1, - 'tolerance' => 0, - ]; - } - // arbitrary length - $hash = substr($token, 6, 128); - return new CSRFPToken($decode['timestamp'], $decode['tolerance'], $hash); - } - - public function __toString(): string { - return $this->encode(); - } -} diff --git a/tests/CSRFPTest.php b/tests/CSRFPTest.php index 94ff606..27ce32a 100644 --- a/tests/CSRFPTest.php +++ b/tests/CSRFPTest.php @@ -1,79 +1,68 @@ assertEquals(bin2hex($csrfp1->createHash('test', 1234, 12)), '965582c3bd762c22c18f99a5733cf9922166dbd2'); - $this->assertEquals(bin2hex($csrfp2->createHash('test', 1234, 12)), '539b4e89e7313b91c66d5d18cc6e9ff6826cc7a2'); + $token1 = $csrfp1->createToken(self::TIMESTAMP_1); + $token2 = $csrfp2->createToken(self::TIMESTAMP_1); + $token3 = $csrfp1->createToken(self::TIMESTAMP_2); + $token4 = $csrfp2->createToken(self::TIMESTAMP_2); - $token1 = $csrfp1->createToken('identity'); - $token2 = $csrfp2->createToken('identity'); - $token3 = $csrfp1->createToken('other'); - $token4 = $csrfp2->createToken('other'); + $this->assertNotEquals($token1, $token2); + $this->assertNotEquals($token2, $token3); + $this->assertNotEquals($token3, $token4); + $this->assertNotEquals($token4, $token1); - $this->assertTrue($csrfp1->verifyToken('identity', $token1)); - $this->assertTrue($csrfp2->verifyToken('identity', $token2)); - $this->assertTrue($csrfp1->verifyToken('other', $token3)); - $this->assertTrue($csrfp2->verifyToken('other', $token4)); + $this->assertTrue($csrfp1->verifyToken($token1, -1, self::TIMESTAMP_1 + self::P15M)); + $this->assertTrue($csrfp2->verifyToken($token2, -1, self::TIMESTAMP_1 + self::P29M59S)); + $this->assertTrue($csrfp1->verifyToken($token3, -1, self::TIMESTAMP_2 + self::P15M)); + $this->assertTrue($csrfp2->verifyToken($token4, -1, self::TIMESTAMP_2 + self::P29M59S)); - $this->assertFalse($csrfp2->verifyToken('identity', $token1)); - $this->assertFalse($csrfp1->verifyToken('identity', $token2)); - $this->assertFalse($csrfp2->verifyToken('other', $token3)); - $this->assertFalse($csrfp1->verifyToken('other', $token4)); + $this->assertFalse($csrfp2->verifyToken($token1, -1, self::TIMESTAMP_1 + self::P15M)); + $this->assertFalse($csrfp1->verifyToken($token2, -1, self::TIMESTAMP_1 + self::P29M59S)); + $this->assertFalse($csrfp2->verifyToken($token3, -1, self::TIMESTAMP_2 + self::P15M)); + $this->assertFalse($csrfp1->verifyToken($token4, -1, self::TIMESTAMP_2 + self::P29M59S)); - $this->assertFalse($csrfp1->verifyToken('other', $token1)); - $this->assertFalse($csrfp2->verifyToken('other', $token2)); - $this->assertFalse($csrfp1->verifyToken('identity', $token3)); - $this->assertFalse($csrfp2->verifyToken('identity', $token4)); - } + $this->assertFalse($csrfp1->verifyToken($token1, -1, self::TIMESTAMP_1 + self::P30M)); + $this->assertFalse($csrfp2->verifyToken($token2, -1, self::TIMESTAMP_1 + self::P30M)); + $this->assertFalse($csrfp1->verifyToken($token3, -1, self::TIMESTAMP_2 + self::P30M)); + $this->assertFalse($csrfp2->verifyToken($token4, -1, self::TIMESTAMP_2 + self::P30M)); - public function testTokenDecode(): void { - $token1 = CSRFPToken::decode('zCM1AAgHjTdDYLEcRgg5g0NHVsu69PTKurg'); // valid - $token2 = CSRFPToken::decode('AyQ1AAgHirhWJJJnQIwYKhWaF6zfv5NkhQ0'); // valid - $token3 = CSRFPToken::decode('KJFfkd39rrkf9Gs9g90sg90g3fdskfdsk34'); // random characters - $token4 = CSRFPToken::decode('zCM1AAgHjTdDY'); // incomplete data - $token5 = CSRFPToken::decode('AyQ'); // incomplete data - $token6 = CSRFPToken::decode(''); // empty + $this->assertTrue($csrfp1->verifyToken($token1, self::P5M, self::TIMESTAMP_1 + self::P3M)); + $this->assertTrue($csrfp2->verifyToken($token2, self::P5M, self::TIMESTAMP_1 + self::P4M59S)); + $this->assertTrue($csrfp1->verifyToken($token3, self::P5M, self::TIMESTAMP_2 + self::P3M)); + $this->assertTrue($csrfp2->verifyToken($token4, self::P5M, self::TIMESTAMP_2 + self::P4M59S)); - $this->assertEquals(bin2hex($token1->getHash()), '8d374360b11c46083983434756cbbaf4f4cabab8'); - $this->assertEquals(bin2hex($token2->getHash()), '8ab856249267408c182a159a17acdfbf9364850d'); - $this->assertEquals(bin2hex($token3->getHash()), 'aeb91ff46b3d83dd2c83dd20ddf76c91f76c937e'); - $this->assertEquals(bin2hex($token4->getHash()), '8d3743'); // data may be incomplete, but there's still something - $this->assertEquals($token5->getHash(), ''); - $this->assertEquals($token6->getHash(), ''); - - $this->assertEquals($token1->getTimestamp(), 3482572); - $this->assertEquals($token2->getTimestamp(), 3482627); - $this->assertEquals($token3->getTimestamp(), 2438959400); - $this->assertEquals($token4->getTimestamp(), 3482572); - $this->assertEquals($token5->getTimestamp(), -1); - $this->assertEquals($token6->getTimestamp(), -1); - - $this->assertEquals($token1->getTolerance(), 1800); - $this->assertEquals($token2->getTolerance(), 1800); - $this->assertEquals($token3->getTolerance(), 64989); - $this->assertEquals($token4->getTolerance(), 1800); - $this->assertEquals($token5->getTolerance(), 0); - $this->assertEquals($token6->getTolerance(), 0); + $this->assertFalse($csrfp1->verifyToken($token1, self::P5M, self::TIMESTAMP_1 + self::P5M)); + $this->assertFalse($csrfp2->verifyToken($token2, self::P5M, self::TIMESTAMP_1 + self::P5M30S)); + $this->assertFalse($csrfp1->verifyToken($token3, self::P5M, self::TIMESTAMP_2 + self::P5M)); + $this->assertFalse($csrfp2->verifyToken($token4, self::P5M, self::TIMESTAMP_2 + self::P5M30S)); } }