From 93fc6355d41f5a00d0311797acf78cda266c5462 Mon Sep 17 00:00:00 2001 From: RobThree Date: Tue, 5 Jun 2018 22:17:49 +0200 Subject: [PATCH] * Dropped ConvertUnixTimeDotComTimeProvider * Added NTPTimeProvider * Added out argument on verifyCode() method that returns the matched timeslice (if any) when a valid code was entered --- README.md | 4 +- TwoFactorAuth.phpproj | 2 +- .../ConvertUnixTimeDotComTimeProvider.php | 15 ------ lib/Providers/Time/NTPTimeProvider.php | 52 +++++++++++++++++++ lib/TwoFactorAuth.php | 13 ++--- tests/TwoFactorAuthTest.php | 26 +++++++++- 6 files changed, 86 insertions(+), 26 deletions(-) delete mode 100644 lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php create mode 100644 lib/Providers/Time/NTPTimeProvider.php 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();