diff options
author | Sebastian Schweizer <sebastian@schweizer.tel> | 2016-10-17 18:05:46 +0200 |
---|---|---|
committer | Sebastian Schweizer <sebastian@schweizer.tel> | 2016-10-17 18:05:46 +0200 |
commit | f9b9cdcfd98860da76f4e17397f6ceb1fbbdffea (patch) | |
tree | 8f7c3f9c4bab5e2432a35af6c82d99c6115287a4 | |
parent | ac2b66265d5fda38ce097782d47089bc3366683d (diff) | |
download | google2fa-f9b9cdcfd98860da76f4e17397f6ceb1fbbdffea.zip google2fa-f9b9cdcfd98860da76f4e17397f6ceb1fbbdffea.tar.gz google2fa-f9b9cdcfd98860da76f4e17397f6ceb1fbbdffea.tar.bz2 |
Add functionality to prevent reuse of keys.
See the patched readme files for details.
I also added a getter for KEY_REGENERATION in order to allow using
the facade like this: $ts * Google2FA::getKeyRegenerationTime()
-rw-r--r-- | readme.md | 23 | ||||
-rw-r--r-- | src/Contracts/Google2FA.php | 23 | ||||
-rw-r--r-- | src/Google2FA.php | 44 | ||||
-rw-r--r-- | tests/spec/Google2FASpec.php | 21 |
4 files changed, 108 insertions, 3 deletions
@@ -107,6 +107,29 @@ It's really important that you keep your server time in sync with some NTP serve ntpdate ntp.ubuntu.com
+## Validation Window
+
+To avoid problms with clocks that are slightly out of sync, we do not check against the current key only but also consider `$window` keys each from the past and future. You can pass `$window` as optional third parameter to `verifyKey`, it defaults to `4`. A new key is generated every 30 seconds, so this window includes keys from the previous two and next two minutes.
+
+ $secret = Input::get('secret');
+ $window = 8; // 8 keys (respectively 4 minutes) past and future
+
+ $valid = Google2FA::verifyKey($user->google2fa_secret, $secret, $window);
+
+An attacker might be able to watch the user entering his credentials and one time key.
+Without further precautions, the key remains valid until it is no longer within the window of the server time. In order to prevent usage of a one time key that has already been used, you can utilize the `verifyKeyNewer` function.
+
+ $secret = Input::get('secret');
+ $ts = Google2FA::verifyKeyNewer($user->google2fa_secret, $secret, $user->google2fa_ts);
+ if ($ts !== false) {
+ $user->update(['google2fa_ts' => $ts]);
+ // successful
+ } else {
+ // failed
+ }
+
+Note that `$ts` either `false` (if the key is invalid or has been used before) or the provided key's unix timestamp divided by the key regeneration period of 30 seconds.
+
## Using a Bigger and Prefixing the Secret Key
Although the probability of collision of a 16 bytes (128 bits) random string is very low, you can harden it by:
diff --git a/src/Contracts/Google2FA.php b/src/Contracts/Google2FA.php index c1e4712..bae992e 100644 --- a/src/Contracts/Google2FA.php +++ b/src/Contracts/Google2FA.php @@ -71,6 +71,22 @@ interface Google2FA public function verifyKey($b32seed, $key, $window = 4, $useTimeStamp = true); /** + * Verifies a user inputted key against the current timestamp. Checks $window + * keys either side of the timestamp, but ensures that the given key is newer than + * the given oldTimestamp. Useful if you need to ensure that a single key cannot + * be used twice. + * + * @param string $b32seed + * @param string $key - User specified key + * @param int $oldTimestamp - The timestamp from the last verified key + * @param int $window + * @param bool $useTimeStamp + * + * @return bool|int - false (not verified) or the timestamp of the verified key + **/ + public function verifyKeyNewer($b32seed, $key, $oldTimestamp, $window = 4, $useTimeStamp = true); + + /** * Extracts the OTP from the SHA1 hash. * * @param string $hash @@ -112,4 +128,11 @@ interface Google2FA * @return string */ public function getQRCodeInline($company, $holder, $secret, $size = 100, $encoding = 'utf-8'); + + /** + * Get the key regeneration time in seconds. + * + * @return int + */ + public function getKeyRegenerationTime(); } diff --git a/src/Google2FA.php b/src/Google2FA.php index 4387123..844ecd5 100644 --- a/src/Google2FA.php +++ b/src/Google2FA.php @@ -229,6 +229,40 @@ class Google2FA implements Google2FAContract } /** + * Verifies a user inputted key against the current timestamp. Checks $window + * keys either side of the timestamp, but ensures that the given key is newer than + * the given oldTimestamp. Useful if you need to ensure that a single key cannot + * be used twice. + * + * @param string $b32seed + * @param string $key - User specified key + * @param int $oldTimestamp - The timestamp from the last verified key + * @param int $window + * @param bool $useTimeStamp + * + * @return bool|int - false (not verified) or the timestamp of the verified key + **/ + public function verifyKeyNewer($b32seed, $key, $oldTimestamp, $window = 4, $useTimeStamp = true) + { + $timeStamp = $this->getTimestamp(); + + if ($useTimeStamp !== true) { + $timeStamp = (int) $useTimeStamp; + } + + $binarySeed = $this->base32Decode($b32seed); + + for ($ts = max($timeStamp - $window, $oldTimestamp + 1); + $ts <= $timeStamp + $window; $ts++) { + if (hash_equals($this->oathHotp($binarySeed, $ts), $key)) { + return $ts; + } + } + + return false; + } + + /** * Extracts the OTP from the SHA1 hash. * * @param string $hash @@ -349,4 +383,14 @@ class Google2FA implements Google2FAContract return str_replace('=', '', $encoded); } + + /** + * Get the key regeneration time in seconds. + * + * @return int + */ + public function getKeyRegenerationTime() + { + return static::KEY_REGENERATION; + } } diff --git a/tests/spec/Google2FASpec.php b/tests/spec/Google2FASpec.php index 1598548..4583ea2 100644 --- a/tests/spec/Google2FASpec.php +++ b/tests/spec/Google2FASpec.php @@ -61,11 +61,26 @@ class Google2FASpec extends ObjectBehavior $this->getCurrentOtp($this->secret)->shouldHaveLength(6); } - public function it_verifies_a_key() + public function it_verifies_keys() { - // 26213400 = Human time (GMT): Sat, 31 Oct 1970 09:30:00 GMT + // $ts 26213400 with KEY_REGENERATION 30 seconds is + // timestamp 786402000, which is 1994-12-02 21:00:00 UTC + + $this->verifyKey($this->secret, '093183', 2, 26213400)->shouldBe(false); // 26213397 + $this->verifyKey($this->secret, '558854', 2, 26213400)->shouldBe(true); // 26213398 + $this->verifyKey($this->secret, '981084', 2, 26213400)->shouldBe(true); // 26213399 + $this->verifyKey($this->secret, '512396', 2, 26213400)->shouldBe(true); // 26213400 + $this->verifyKey($this->secret, '410272', 2, 26213400)->shouldBe(true); // 26213401 + $this->verifyKey($this->secret, '239815', 2, 26213400)->shouldBe(true); // 26213402 + $this->verifyKey($this->secret, '313366', 2, 26213400)->shouldBe(false); // 26213403 + } - $this->verifyKey($this->secret, '410272', 4, 26213400)->shouldBe(true); + public function it_verifies_keys_newer() + { + $this->verifyKeyNewer($this->secret, '512396', 26213401, 2, 26213400)->shouldBe(false); // 26213400 + $this->verifyKeyNewer($this->secret, '410272', 26213401, 2, 26213400)->shouldBe(false); // 26213401 + $this->verifyKeyNewer($this->secret, '239815', 26213401, 2, 26213400)->shouldBe(26213402); // 26213402 + $this->verifyKeyNewer($this->secret, '313366', 26213401, 2, 26213400)->shouldBe(false); // 26213403 } public function it_removes_invalid_chars_from_secret() |