From 2060811d888e9277f2483a2b8d77ad8654515ec6 Mon Sep 17 00:00:00 2001 From: Igor Santos Date: Sat, 23 Jan 2021 19:10:34 -0300 Subject: [PATCH 1/5] Add logo option to Endroid's provider closes #56 --- lib/Providers/Qr/EndroidQrCodeProvider.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/Providers/Qr/EndroidQrCodeProvider.php b/lib/Providers/Qr/EndroidQrCodeProvider.php index 735f8a0..0df614d 100755 --- a/lib/Providers/Qr/EndroidQrCodeProvider.php +++ b/lib/Providers/Qr/EndroidQrCodeProvider.php @@ -10,6 +10,8 @@ class EndroidQrCodeProvider implements IQRCodeProvider public $color; public $margin; public $errorcorrectionlevel; + protected $logoPath; + protected $logoSize; public function __construct($bgcolor = 'ffffff', $color = '000000', $margin = 0, $errorcorrectionlevel = 'H') { @@ -19,6 +21,17 @@ class EndroidQrCodeProvider implements IQRCodeProvider $this->errorcorrectionlevel = $this->handleErrorCorrectionLevel($errorcorrectionlevel); } + /** + * Adds an image to the middle of the QR Code. + * @param string $path Path to an image file + * @param array|int $size Just the width, or [width, height] + */ + public function setLogo($path, $size = null) + { + $this->logoPath = $path; + $this->logoSize = (array)$size; + } + public function getMimeType() { return 'image/png'; @@ -34,6 +47,13 @@ class EndroidQrCodeProvider implements IQRCodeProvider $qrCode->setBackgroundColor($this->bgcolor); $qrCode->setForegroundColor($this->color); + if ($this->logoPath) { + $qrCode->setLogoPath($this->logoPath); + if ($this->logoSize) { + $qrCode->setLogoSize($this->logoSize[0], $this->logoSize[1]); + } + } + return $qrCode->writeString(); } From c3f3c0a849ef2e741fe40c8040cac7c45626cf7b Mon Sep 17 00:00:00 2001 From: Igor Santos Date: Sun, 24 Jan 2021 00:34:25 -0300 Subject: [PATCH 2/5] Move logo implementation to its own class --- lib/Providers/Qr/EndroidQrCodeProvider.php | 27 ++++---------- .../Qr/EndroidQrCodeWithLogoProvider.php | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 21 deletions(-) create mode 100755 lib/Providers/Qr/EndroidQrCodeWithLogoProvider.php diff --git a/lib/Providers/Qr/EndroidQrCodeProvider.php b/lib/Providers/Qr/EndroidQrCodeProvider.php index 0df614d..810aa9b 100755 --- a/lib/Providers/Qr/EndroidQrCodeProvider.php +++ b/lib/Providers/Qr/EndroidQrCodeProvider.php @@ -10,8 +10,6 @@ class EndroidQrCodeProvider implements IQRCodeProvider public $color; public $margin; public $errorcorrectionlevel; - protected $logoPath; - protected $logoSize; public function __construct($bgcolor = 'ffffff', $color = '000000', $margin = 0, $errorcorrectionlevel = 'H') { @@ -21,23 +19,17 @@ class EndroidQrCodeProvider implements IQRCodeProvider $this->errorcorrectionlevel = $this->handleErrorCorrectionLevel($errorcorrectionlevel); } - /** - * Adds an image to the middle of the QR Code. - * @param string $path Path to an image file - * @param array|int $size Just the width, or [width, height] - */ - public function setLogo($path, $size = null) - { - $this->logoPath = $path; - $this->logoSize = (array)$size; - } - public function getMimeType() { return 'image/png'; } public function getQRCodeImage($qrtext, $size) + { + return $this->qrCodeInstance($qrtext, $size)->writeString(); + } + + protected function qrCodeInstance($qrtext, $size) { $qrCode = new QrCode($qrtext); $qrCode->setSize($size); @@ -47,14 +39,7 @@ class EndroidQrCodeProvider implements IQRCodeProvider $qrCode->setBackgroundColor($this->bgcolor); $qrCode->setForegroundColor($this->color); - if ($this->logoPath) { - $qrCode->setLogoPath($this->logoPath); - if ($this->logoSize) { - $qrCode->setLogoSize($this->logoSize[0], $this->logoSize[1]); - } - } - - return $qrCode->writeString(); + return $qrCode; } private function handleColor($color) diff --git a/lib/Providers/Qr/EndroidQrCodeWithLogoProvider.php b/lib/Providers/Qr/EndroidQrCodeWithLogoProvider.php new file mode 100755 index 0000000..ed8cc98 --- /dev/null +++ b/lib/Providers/Qr/EndroidQrCodeWithLogoProvider.php @@ -0,0 +1,35 @@ +logoPath = $path; + $this->logoSize = (array)$size; + } + + protected function qrCodeInstance($qrtext, $size) { + $qrCode = parent::qrCodeInstance($qrtext, $size); + + if ($this->logoPath) { + $qrCode->setLogoPath($this->logoPath); + if ($this->logoSize) { + $qrCode->setLogoSize($this->logoSize[0], $this->logoSize[1]); + } + } + + return $qrCode; + } +} From ee9bf04ab74852f0a3b3f14ed8cad40e8de22352 Mon Sep 17 00:00:00 2001 From: Igor Santos Date: Sun, 24 Jan 2021 00:34:42 -0300 Subject: [PATCH 3/5] Mention embedded image support in docs --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 10a18c5..46a90ab 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ PHP library for [two-factor (or multi-factor) authentication](http://en.wikipedi Optionally, you may need: -* [endroid/qr-code](https://github.com/endroid/qr-code) if using `EndroidQrCodeProvider`. +* [endroid/qr-code](https://github.com/endroid/qr-code) if using `EndroidQrCodeProvider` or `EndroidQrCodeWithLogoProvider`. * [bacon/bacon-qr-code](https://github.com/Bacon/BaconQrCode) if using `BaconQrCodeProvider`. ## Installation @@ -73,7 +73,8 @@ Another, more user-friendly, way to get the shared secret into the app is to gen 2. `ImageChartsQRCodeProvider` 3. `QRicketProvider` 4. `EndroidQrCodeProvider` (requires `endroid/qr-code` to be installed) -5. `BaconQrCodeProvider` (requires `bacon/bacon-qr-code` to be installed) +5. `EndroidQrCodeWithLogoProvider` (same, but supporting embedded images) +6. `BaconQrCodeProvider` (requires `bacon/bacon-qr-code` to be installed) ...or implement your own provider. To implement your own provider all you need to do is implement the `IQRCodeProvider` interface. You can use the built-in providers mentioned before to serve as an example or read the next chapter in this file. The built-in classes all use a 3rd (e.g. external) party (Image-charts, QRServer and QRicket) for the hard work of generating QR-codes (note: each of these services might at some point not be available or impose limitations to the number of codes generated per day, hour etc.). You could, however, easily use a project like [PHP QR Code](http://phpqrcode.sourceforge.net/) (or one of the [many others](https://packagist.org/search/?q=qr)) to generate your QR-codes without depending on external sources. Later on we'll [demonstrate](#qr-code-providers) how to do this. @@ -128,7 +129,7 @@ public function verifyCode($secret, $code, $discrepancy = 1, $time = null): bool As mentioned before, this library comes with five 'built-in' QR-code providers. This chapter will touch the subject a bit but most of it should be self-explanatory. The `TwoFactorAuth`-class accepts a `$qrcodeprovider` argument which lets you specify a built-in or custom QR-code provider. All five built-in providers do a simple HTTP request to retrieve an image using cURL and implement the [`IQRCodeProvider`](lib/Providers/Qr/IQRCodeProvider.php) interface which is all you need to implement to write your own QR-code provider. -The default provider is the [`QRServerProvider`](lib/Providers/Qr/QRServerProvider.php) which uses the [goqr.me API](http://goqr.me/api/doc/create-qr-code/) to render QR-codes. Then we have the [`ImageChartsQRCodeProvider`](lib/Providers/Qr/ImageChartsQRCodeProvider.php) which uses the [image-charts.com replacement for Google Image Charts](https://image-charts.com) to render QR-codes and the [`QRicketProvider`](lib/Providers/Qr/QRicketProvider.php) which uses the [QRickit API](http://qrickit.com/qrickit_apps/qrickit_api.php). These three providers all inherit from a common (abstract) baseclass named [`BaseHTTPQRCodeProvider`](lib/Providers/Qr/BaseHTTPQRCodeProvider.php) because all three share the same functionality: retrieve an image from a 3rd party over HTTP. Finally, we have [`EndroidQrCodeProvider`](lib/Providers/Qr/EndroidQrCodeProvider.php) and [`BaconQrCodeProvider`](lib/Providers/Qr/BaconQrCodeProvider.php) which require an optional dependency to be installed to use (see Requirements section above), but will generate the QR codes locally. All five classes have constructors that allow you to tweak some settings and most, if not all, arguments should speak for themselves. If you're not sure which values are supported, click the links in this paragraph for documentation on the API's that are utilized by these classes. +The default provider is the [`QRServerProvider`](lib/Providers/Qr/QRServerProvider.php) which uses the [goqr.me API](http://goqr.me/api/doc/create-qr-code/) to render QR-codes. Then we have the [`ImageChartsQRCodeProvider`](lib/Providers/Qr/ImageChartsQRCodeProvider.php) which uses the [image-charts.com replacement for Google Image Charts](https://image-charts.com) to render QR-codes and the [`QRicketProvider`](lib/Providers/Qr/QRicketProvider.php) which uses the [QRickit API](http://qrickit.com/qrickit_apps/qrickit_api.php). These three providers all inherit from a common (abstract) baseclass named [`BaseHTTPQRCodeProvider`](lib/Providers/Qr/BaseHTTPQRCodeProvider.php) because all three share the same functionality: retrieve an image from a 3rd party over HTTP. Finally, we have [`EndroidQrCodeProvider`](lib/Providers/Qr/EndroidQrCodeProvider.php), [`EndroidQrCodeWithLogoProvider`](lib/Providers/Qr/EndroidQrCodeWithLogoProvider.php) and [`BaconQrCodeProvider`](lib/Providers/Qr/BaconQrCodeProvider.php) which require an optional dependency to be installed to use (see Requirements section above), but will generate the QR codes locally. All five classes have constructors that allow you to tweak some settings and most, if not all, arguments should speak for themselves. If you're not sure which values are supported, click the links in this paragraph for documentation on the API's that are utilized by these classes. If you don't like any of the built-in classes because you don't want to rely on external resources for example or because you're paranoid about sending the TOTP secret to these 3rd parties (which is useless to them since they miss *at least one* other factor in the [MFA process](http://en.wikipedia.org/wiki/Multi-factor_authentication)), feel tree to implement your own. The `IQRCodeProvider` interface couldn't be any simpler. All you need to do is implement 2 methods: From 94571a257a6c67a032746c567f2503bf53e0cc1f Mon Sep 17 00:00:00 2001 From: Igor Santos Date: Sun, 24 Jan 2021 00:34:58 -0300 Subject: [PATCH 4/5] Remove temp file(?) --- phpunit.xml.tmppica | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 phpunit.xml.tmppica diff --git a/phpunit.xml.tmppica b/phpunit.xml.tmppica deleted file mode 100644 index e69de29..0000000 From cc2ae19bcd95404caf6a3115fea0aec04a27c192 Mon Sep 17 00:00:00 2001 From: Igor Santos Date: Sun, 24 Jan 2021 02:55:03 -0300 Subject: [PATCH 5/5] Document the need for the code to be a string --- README.md | 4 +++- lib/TwoFactorAuth.php | 13 ++++++++++--- tests/TwoFactorAuthTest.php | 3 +-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 46a90ab..ab2c9b6 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,9 @@ When the shared secret is added to the app, the app will be ready to start gener $result = $tfa->verifyCode($_SESSION['secret'], $_POST['verification']); ```` -`verifyCode()` will return either `true` (the code was valid) or `false` (the code was invalid; no points for you!). You may need to store `$secret` in a `$_SESSION` or other persistent storage between requests. The `verifyCode()` accepts, aside from `$secret` and `$code`, three more arguments. The first being `$discrepancy`. Since TOTP codes are based on time("slices") it is very important that the server (but also client) have a correct date/time. But because the two *may* differ a bit we usually allow a certain amount of leeway. Because generated codes are valid for a specific period (remember the `$period` argument in the `TwoFactorAuth`'s constructor?) we usually check the period directly before and the period directly after the current time when validating codes. So when the current time is `14:34:21`, which results in a 'current timeslice' of `14:34:00` to `14:34:30` we also calculate/verify the codes for `14:33:30` to `14:34:00` and for `14:34:30` to `14:35:00`. This gives us a 'window' of `14:33:30` to `14:35:00`. The `$discrepancy` argument specifies how many periods (or: timeslices) we check in either direction of the current time. The default `$discrepancy` of `1` results in (max.) 3 period checks: -1, current and +1 period. A `$discrepancy` of `4` would result in a larger window (or: bigger time difference between client and server) of -4, -3, -2, -1, current, +1, +2, +3 and +4 periods. +If you do extra validations with your `$_POST` values, just make sure the code is still submitted as string - even if that's a numeric code, casting it to integer is unreliable. Also, you may need to store `$secret` in a `$_SESSION` or other persistent storage between requests. `verifyCode()` will return either `true` (the code was valid) or `false` (the code was invalid; no points for you!). + + The `verifyCode()` accepts, aside from `$secret` and `$code`, three more arguments, with the first being `$discrepancy`. Since TOTP codes are based on time("slices") it is very important that the server (but also client) have a correct date/time. But because the two *may* differ a bit we usually allow a certain amount of leeway. Because generated codes are valid for a specific period (remember the `$period` argument in the `TwoFactorAuth`'s constructor?) we usually check the period directly before and the period directly after the current time when validating codes. So when the current time is `14:34:21`, which results in a 'current timeslice' of `14:34:00` to `14:34:30` we also calculate/verify the codes for `14:33:30` to `14:34:00` and for `14:34:30` to `14:35:00`. This gives us a 'window' of `14:33:30` to `14:35:00`. The `$discrepancy` argument specifies how many periods (or: timeslices) we check in either direction of the current time. The default `$discrepancy` of `1` results in (max.) 3 period checks: -1, current and +1 period. A `$discrepancy` of `4` would result in a larger window (or: bigger time difference between client and server) of -4, -3, -2, -1, current, +1, +2, +3 and +4 periods. The second, `$time`, allows you to check a code for a specific point in time. This argument has no real practical use but can be handy for unittesting etc. The default value, `null`, means: use the current time. diff --git a/lib/TwoFactorAuth.php b/lib/TwoFactorAuth.php index 7bc067d..e74ad97 100644 --- a/lib/TwoFactorAuth.php +++ b/lib/TwoFactorAuth.php @@ -62,6 +62,7 @@ class TwoFactorAuth /** * Calculate the code with given secret and point in time + * @return string */ public function getCode($secret, $time = null) { @@ -78,10 +79,16 @@ class TwoFactorAuth /** * Check if the code is correct. This will accept codes starting from ($discrepancy * $period) sec ago to ($discrepancy * period) sec from now + * @param string $secret + * @param string $code This shouldn't be casted to integer - you may lose zeroes to the left + * @param int $discrepancy + * @param int|null $time + * @param int $timeslice + * @return bool */ public function verifyCode($secret, $code, $discrepancy = 1, $time = null, &$timeslice = 0) { - $timetamp = $this->getTime($time); + $timestamp = $this->getTime($time); $timeslice = 0; @@ -90,7 +97,7 @@ class TwoFactorAuth // of the match. Each iteration we either set the timeslice variable to the timeslice of the match // or set the value to itself. This is an effort to maintain constant execution time for the code. for ($i = -$discrepancy; $i <= $discrepancy; $i++) { - $ts = $timetamp + ($i * $this->period); + $ts = $timestamp + ($i * $this->period); $slice = $this->getTimeSlice($ts); $timeslice = $this->codeEquals($this->getCode($secret, $ts), $code) ? $slice : $timeslice; } @@ -253,4 +260,4 @@ class TwoFactorAuth } return $this->timeprovider; } -} \ No newline at end of file +} diff --git a/tests/TwoFactorAuthTest.php b/tests/TwoFactorAuthTest.php index e011fc4..c9a070d 100644 --- a/tests/TwoFactorAuthTest.php +++ b/tests/TwoFactorAuthTest.php @@ -136,7 +136,6 @@ class TwoFactorAuthTest extends PHPUnit\Framework\TestCase } public function testVerifyCodeWorksCorrectly() { - $tfa = new TwoFactorAuth('Test', 6, 30); $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190)); $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy @@ -410,4 +409,4 @@ class TestTimeProvider implements ITimeProvider { public function getTime() { return $this->time; } -} \ No newline at end of file +}