summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSebastian Schweizer <sebastian@schweizer.tel>2016-10-17 18:05:46 +0200
committerSebastian Schweizer <sebastian@schweizer.tel>2016-10-17 18:05:46 +0200
commitf9b9cdcfd98860da76f4e17397f6ceb1fbbdffea (patch)
tree8f7c3f9c4bab5e2432a35af6c82d99c6115287a4
parentac2b66265d5fda38ce097782d47089bc3366683d (diff)
downloadgoogle2fa-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.md23
-rw-r--r--src/Contracts/Google2FA.php23
-rw-r--r--src/Google2FA.php44
-rw-r--r--tests/spec/Google2FASpec.php21
4 files changed, 108 insertions, 3 deletions
diff --git a/readme.md b/readme.md
index afc2a94..76842a5 100644
--- a/readme.md
+++ b/readme.md
@@ -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()