diff --git a/README.md b/README.md
index ba6cfea..ac9f9f3 100644
--- a/README.md
+++ b/README.md
@@ -180,11 +180,11 @@ You can easily implement your own `RNGProvider` by simply implementing the `IRNG
### Time providers
-Another set of providers in this library are the Time Providers; this library provides three 'built-in' ones. The default Time Provider used is the [`LocalMachineTimeProvider`](lib/Providers/Time/LocalMachineTimeProvider.php); this provider simply returns the output of `Time()` and is *highly recommended* as default provider. The [`HttpTimeProvider`](lib/Providers/Time/HttpTimeProvider.php) executes a `HEAD` request against a given webserver (default: google.com) and tries to extract the `Date:`-HTTP header and returns it's date. Other url's/domains can be used by specifying the url in the constructor. The final Time Provider is the [`ConvertUnixTimeDotComTimeProvider`](lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php) which does a HTTP request to `convert-unix-time.com/api` and decodes the `JSON` result to retrieve the time.
+Another set of providers in this library are the Time Providers; this library provides three 'built-in' ones. The default Time Provider used is the [`LocalMachineTimeProvider`](lib/Providers/Time/LocalMachineTimeProvider.php); this provider simply returns the output of `Time()` and is *highly recommended* as default provider. The [`HttpTimeProvider`](lib/Providers/Time/HttpTimeProvider.php) executes a `HEAD` request against a given webserver (default: google.com) and tries to extract the `Date:`-HTTP header and returns it's date. Other url's/domains can be used by specifying the url in the constructor. The final Time Provider is the [`NTPTimeProvider`](lib/Providers/Time/NTPTimeProvider.php) which does an NTP request to a specified NTP server.
You can easily implement your own `TimeProvider` by simply implementing the `ITimeProvider` interface.
-As to *why* these Time Providers are implemented: it allows the TwoFactorAuth library to ensure the hosts time is correct (or rather: within a margin). You can use the `ensureCorrectTime()` method to ensure the hosts time is correct. By default this method will compare the hosts time (returned by calling `time()` on the `LocalMachineTimeProvider`) to Google's and convert-unix-time.com's current time. You can pass an array of `ITimeProvider`s and specify the `leniency` (second argument) allowed (default: 5 seconds). The method will throw when the TwoFactorAuth's timeprovider (which can be any `ITimeProvider`, see constructor) differs more than the given amount of seconds from any of the given `ITimeProviders`. We advise to call this method sparingly when relying on 3rd parties (which both the `HttpTimeProvider` and `ConvertUnixTimeDotComTimeProvider` do) or, if you need to ensure time is correct on a (very) regular basis to implement an `ITimeProvider` that is more efficient than the 'built-in' ones (like use a GPS signal). The `ensureCorrectTime()` method is mostly to be used to make sure the server is configured correctly.
+As to *why* these Time Providers are implemented: it allows the TwoFactorAuth library to ensure the hosts time is correct (or rather: within a margin). You can use the `ensureCorrectTime()` method to ensure the hosts time is correct. By default this method will compare the hosts time (returned by calling `time()` on the `LocalMachineTimeProvider`) to Google's and convert-unix-time.com's current time. You can pass an array of `ITimeProvider`s and specify the `leniency` (second argument) allowed (default: 5 seconds). The method will throw when the TwoFactorAuth's timeprovider (which can be any `ITimeProvider`, see constructor) differs more than the given amount of seconds from any of the given `ITimeProviders`. We advise to call this method sparingly when relying on 3rd parties (which both the `HttpTimeProvider` and `NTPTimeProvider` do) or, if you need to ensure time is correct on a (very) regular basis to implement an `ITimeProvider` that is more efficient than the 'built-in' ones (like use a GPS signal). The `ensureCorrectTime()` method is mostly to be used to make sure the server is configured correctly.
## Integrations
diff --git a/TwoFactorAuth.phpproj b/TwoFactorAuth.phpproj
index 7fa2a58..c5df35a 100644
--- a/TwoFactorAuth.phpproj
+++ b/TwoFactorAuth.phpproj
@@ -38,10 +38,10 @@
-
+
diff --git a/lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php b/lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php
deleted file mode 100644
index 9a775fc..0000000
--- a/lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php
+++ /dev/null
@@ -1,15 +0,0 @@
-timestamp))
- throw new \TimeException('Unable to retrieve time from convert-unix-time.com');
- return $json->timestamp;
- }
-}
\ No newline at end of file
diff --git a/lib/Providers/Time/NTPTimeProvider.php b/lib/Providers/Time/NTPTimeProvider.php
new file mode 100644
index 0000000..862de79
--- /dev/null
+++ b/lib/Providers/Time/NTPTimeProvider.php
@@ -0,0 +1,52 @@
+host = $host;
+
+ if (!is_int($port) || $port <= 0 || $port > 65535)
+ throw new \TimeException('Port must be 0 < port < 65535');
+ $this->port = $port;
+
+ if (!is_int($timeout) || $timeout < 0)
+ throw new \TimeException('Timeout must be >= 0');
+ $this->timeout = $timeout;
+ }
+
+ public function getTime() {
+ try {
+ /* Create a socket and connect to NTP server */
+ $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
+ socket_connect($sock, $this->host, $this->port);
+
+ /* Send request */
+ $msg = "\010" . str_repeat("\0", 47);
+ socket_send($sock, $msg, strlen($msg), 0);
+
+ /* Receive response and close socket */
+ socket_recv($sock, $recv, 48, MSG_WAITALL);
+ socket_close($sock);
+
+ /* Interpret response */
+ $data = unpack('N12', $recv);
+ $timestamp = sprintf('%u', $data[9]);
+
+ /* NTP is number of seconds since 0000 UT on 1 January 1900 Unix time is seconds since 0000 UT on 1 January 1970 */
+ return $timestamp - 2208988800;
+ }
+ catch (Exception $ex) {
+ throw new \TimeException(sprintf('Unable to retrieve time from %s (%s)', $this->host, $ex->getMessage()));
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/TwoFactorAuth.php b/lib/TwoFactorAuth.php
index e6a1fa9..594d42b 100644
--- a/lib/TwoFactorAuth.php
+++ b/lib/TwoFactorAuth.php
@@ -79,16 +79,17 @@ class TwoFactorAuth
/**
* Check if the code is correct. This will accept codes starting from ($discrepancy * $period) sec ago to ($discrepancy * period) sec from now
*/
- public function verifyCode($secret, $code, $discrepancy = 1, $time = null)
+ public function verifyCode($secret, $code, $discrepancy = 1, $time = null, &$timeslice = 0)
{
- $result = false;
$timetamp = $this->getTime($time);
// To keep safe from timing-attachs we iterate *all* possible codes even though we already may have verified a code is correct
- for ($i = -$discrepancy; $i <= $discrepancy; $i++)
- $result |= $this->codeEquals($this->getCode($secret, $timetamp + ($i * $this->period)), $code);
+ for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
+ $ts = $timetamp + ($i * $this->period);
+ $timeslice += $this->codeEquals($this->getCode($secret, $ts), $code) ? $ts : 0;
+ }
- return (bool)$result;
+ return $timeslice > 0;
}
/**
@@ -134,7 +135,7 @@ class TwoFactorAuth
if ($timeproviders == null)
$timeproviders = array(
- new Providers\Time\ConvertUnixTimeDotComTimeProvider(),
+ new Providers\Time\NTPTimeProvider(),
new Providers\Time\HttpTimeProvider()
);
diff --git a/tests/TwoFactorAuthTest.php b/tests/TwoFactorAuthTest.php
index 587b19b..0eba4da 100644
--- a/tests/TwoFactorAuthTest.php
+++ b/tests/TwoFactorAuthTest.php
@@ -18,7 +18,7 @@ require_once 'lib/Providers/Rng/RNGException.php';
require_once 'lib/Providers/Time/ITimeProvider.php';
require_once 'lib/Providers/Time/LocalMachineTimeProvider.php';
require_once 'lib/Providers/Time/HttpTimeProvider.php';
-require_once 'lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php';
+require_once 'lib/Providers/Time/NTPTimeProvider.php';
require_once 'lib/Providers/Time/TimeException.php';
use RobThree\Auth\TwoFactorAuth;
@@ -124,7 +124,8 @@ class TwoFactorAuthTest extends PHPUnit_Framework_TestCase
public function testEnsureAllTimeProvidersReturnCorrectTime() {
$tfa = new TwoFactorAuth('Test', 6, 30, 'sha1');
$tfa->ensureCorrectTime(array(
- new RobThree\Auth\Providers\Time\ConvertUnixTimeDotComTimeProvider(),
+ new RobThree\Auth\Providers\Time\NTPTimeProvider(), // Uses pool.ntp.org by default
+ new RobThree\Auth\Providers\Time\NTPTimeProvider('time.windows.com'),
new RobThree\Auth\Providers\Time\HttpTimeProvider(), // Uses google.com by default
new RobThree\Auth\Providers\Time\HttpTimeProvider('https://github.com'),
new RobThree\Auth\Providers\Time\HttpTimeProvider('https://yahoo.com'),
@@ -150,6 +151,27 @@ class TwoFactorAuthTest extends PHPUnit_Framework_TestCase
$this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy
}
+ public function testVerifyCorrectTimeSliceIsReturned() {
+ $tfa = new TwoFactorAuth('Test', 6, 30);
+
+ // We test with discrepancy 3 (so total of 7 codes: c-3, c-2, c-1, c, c+1, c+2, c+3
+ // Ensure each corresponding timeslice is returned correctly
+ $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '534113', 3, 1426847190, $timeslice1));
+ $this->assertEquals(1426847100, $timeslice1);
+ $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '819652', 3, 1426847190, $timeslice2));
+ $this->assertEquals(1426847130, $timeslice2);
+ $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '915954', 3, 1426847190, $timeslice3));
+ $this->assertEquals(1426847160, $timeslice3);
+ $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 3, 1426847190, $timeslice4));
+ $this->assertEquals(1426847190, $timeslice4);
+ $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '348401', 3, 1426847190, $timeslice5));
+ $this->assertEquals(1426847220, $timeslice5);
+ $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '648525', 3, 1426847190, $timeslice6));
+ $this->assertEquals(1426847250, $timeslice6);
+ $this->assertEquals(true, $tfa->verifyCode('VMR466AB62ZBOKHE', '170645', 3, 1426847190, $timeslice7));
+ $this->assertEquals(1426847280, $timeslice7);
+ }
+
public function testTotpUriIsCorrect() {
$qr = new TestQrProvider();