summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRobThree <rob@devcorner.nl>2014-09-19 01:00:59 +0200
committerRobThree <rob@devcorner.nl>2014-09-19 01:00:59 +0200
commitc2dae86d9a6c827c4ee8c820cf511b63a2e965bf (patch)
tree525b4364611571effe5946f42a4cbf4415464d5e
parentc9128739f267f434e50c2f6aed038134e3b228f4 (diff)
downloadTwoFactorAuth-c2dae86d9a6c827c4ee8c820cf511b63a2e965bf.zip
TwoFactorAuth-c2dae86d9a6c827c4ee8c820cf511b63a2e965bf.tar.gz
TwoFactorAuth-c2dae86d9a6c827c4ee8c820cf511b63a2e965bf.tar.bz2
* Initial commit
-rw-r--r--README.md17
-rw-r--r--TwoFactorAuth.phpproj36
-rw-r--r--TwoFactorAuth.sln22
-rw-r--r--index.php20
-rw-r--r--src/TwoFactorAuth.php178
5 files changed, 273 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6648bd7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# TwoFactorAuth class for PHP
+
+PHP class for two-factor authorization using [TOTP](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) and [QR-codes](http://en.wikipedia.org/wiki/QR_code). Inspired, based and mostly an improvement on '[GoogleAuthenticator](https://github.com/PHPGangsta/GoogleAuthenticator)'.
+
+## Requirements
+
+* Tested on PHP5.4
+* [cURL](http://php.net/manual/en/book.curl.php) when using the default `GoogleQRCodeProvider` but you can provide your own QR-code provider.
+
+
+## Usage
+
+*TODO* For now: see index.php
+
+## License
+
+Licensed under MIT license. See LICENSE file for details. \ No newline at end of file
diff --git a/TwoFactorAuth.phpproj b/TwoFactorAuth.phpproj
new file mode 100644
index 0000000..acd7597
--- /dev/null
+++ b/TwoFactorAuth.phpproj
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Name>TwoFactorAuth</Name>
+ <ProjectGuid>{e569f53a-a604-4579-91ce-4e35b27da47b}</ProjectGuid>
+ <RootNamespace>TwoFactorAuth</RootNamespace>
+ <OutputType>Library</OutputType>
+ <ProjectTypeGuids>{A0786B88-2ADB-4C21-ABE8-AA2D79766269}</ProjectTypeGuids>
+ <SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
+ <Server>PHPDev</Server>
+ <PublishEvent>None</PublishEvent>
+ <PHPDevAutoPort>True</PHPDevAutoPort>
+ <PHPDevPort>41315</PHPDevPort>
+ <PHPDevHostName>localhost</PHPDevHostName>
+ <IISProjectUrl>http://localhost:41315/</IISProjectUrl>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <IncludeDebugInformation>true</IncludeDebugInformation>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+ <IncludeDebugInformation>false</IncludeDebugInformation>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="index.php" />
+ <Compile Include="src\TwoFactorAuth.php" />
+ <Compile Include=".gitignore" />
+ <Compile Include="README.md" />
+ </ItemGroup>
+ <ItemGroup>
+ <Folder Include="src\" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Include="LICENSE" />
+ </ItemGroup>
+</Project> \ No newline at end of file
diff --git a/TwoFactorAuth.sln b/TwoFactorAuth.sln
new file mode 100644
index 0000000..df901f6
--- /dev/null
+++ b/TwoFactorAuth.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2013
+VisualStudioVersion = 12.0.30723.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{A0786B88-2ADB-4C21-ABE8-AA2D79766269}") = "TwoFactorAuth", "TwoFactorAuth.phpproj", "{E569F53A-A604-4579-91CE-4E35B27DA47B}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {E569F53A-A604-4579-91CE-4E35B27DA47B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E569F53A-A604-4579-91CE-4E35B27DA47B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E569F53A-A604-4579-91CE-4E35B27DA47B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E569F53A-A604-4579-91CE-4E35B27DA47B}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..70ddb33
--- /dev/null
+++ b/index.php
@@ -0,0 +1,20 @@
+<?php
+error_reporting(-1);
+require_once 'src/TwoFactorAuth.php';
+
+$tfa = new TwoFactorAuth('MyApp');
+
+$secret = $tfa->createSecret();
+echo 'First create a secret and associate it with a user: ' . $secret . ' (keep this code private; do not share it with the user or anyone, just store it in your database or something)<br>';
+
+echo 'Next create a QR code and let the user scan it:<br>';
+
+$label = 'My label';
+echo 'Image:<br><img src="' . $tfa->getQRCodeImageAsDataUri($label, $secret) . '"><br>';
+
+$code = $tfa->getCode($secret);
+echo 'Next, have the user verify the code; at this time the code displayed by a 2FA-app would be: <span style="color:#00c">' . $code . '</span> (but that changes periodically)<br>';
+
+echo 'When the code checks out, 2FA can be / is enabled; store secret with user (encrypted?) and have the user verify a code each time a new session is started.<br>';
+echo 'When aforementioned code was entered, the result would be: ' . (($tfa->verifyCode($secret, $code) === true) ? '<span style="color:#0c0">OK</span>' : '<span style="color:#c00">FAIL</span>') . '<br>';
+echo 'Make sure server-time is NTP-synced!<br>'; \ No newline at end of file
diff --git a/src/TwoFactorAuth.php b/src/TwoFactorAuth.php
new file mode 100644
index 0000000..ab86eba
--- /dev/null
+++ b/src/TwoFactorAuth.php
@@ -0,0 +1,178 @@
+<?php
+// Based on / inspired by: https://github.com/PHPGangsta/GoogleAuthenticator
+// Algorithms, digits, period etc. explained: https://code.google.com/p/google-authenticator/wiki/KeyUriFormat
+class TwoFactorAuth {
+
+ private $algorithm;
+ private $period;
+ private $digits;
+ private $issuer;
+ private $qrcodeprovider;
+ private static $_base32;
+ private static $_base32lookup = array();
+ private static $_supportedalgos = array('sha1', 'sha256', 'sha512', 'md5');
+
+ function __construct($issuer = null, $digits = 6, $period = 30, $algorithm = 'sha1', $qrcodeprovider = null) {
+ $this->issuer = $issuer;
+
+ if (!is_int($digits) || $digits <= 0)
+ throw new Exception('Digits must be int > 0');
+ $this->digits = $digits;
+
+ if (!is_int($period) || $period <= 0)
+ throw new Exception('Period must be int > 0');
+ $this->period = $period;
+
+ $algorithm = strtolower(trim($algorithm));
+ if (!in_array($algorithm, self::$_supportedalgos))
+ throw new Exception('Unsupported algorithm: ' . $algorithm);
+ $this->algorithm = $algorithm;
+
+ if ($qrcodeprovider==null)
+ $qrcodeprovider = new GoogleQRCodeProvider();
+
+ if (!($qrcodeprovider instanceof IQRCodeProvider))
+ throw new Exception('QRCodeProvider must implement IQRCodeProvider');
+
+ $this->qrcodeprovider = new GoogleQRCodeProvider();
+
+ self::$_base32 = str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=');
+ for ($i = 0; $i < sizeof(self::$_base32); $i++)
+ self::$_base32lookup[self::$_base32[$i]] = $i;
+ }
+
+ /**
+ * Create a new secret
+ */
+ public function createSecret($length = 16) {
+ $secret = '';
+ $rnd = openssl_random_pseudo_bytes($length);
+ for ($i = 0; $i < $length; $i++)
+ $secret .= self::$_base32[ord($rnd[$i]) & 31]; //Mask out left 3 bits for 0-31 values
+ return $secret;
+ }
+
+ /**
+ * Calculate the code with given secret and point in time
+ */
+ public function getCode($secret, $time = null)
+ {
+ $secretkey = $this->base32Decode($secret);
+
+ $ts = "\0\0\0\0".pack('N*', $this->getTimeSlice($this->getTime($time))); // Pack time into binary string
+ $hm = hash_hmac($this->algorithm, $ts, $secretkey, true); // Hash it with users secret key
+ $offset = ord(substr($hm, -1)) & 0x0F; // Use last nibble of result as index/offset
+ $hashpart = substr($hm, $offset, 4); // Grab 4 bytes of the result
+ $value = unpack('N', $hashpart); // Unpack binary value
+ $value = $value[1] & 0x7FFFFFFF; // Drop MSB, keep only 31 bits
+
+ return str_pad($value % pow(10, $this->digits), $this->digits, '0', STR_PAD_LEFT);
+ }
+
+ /**
+ * 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)
+ {
+ $t = $this->getTime($time);
+ for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
+ if (strcmp($this->getCode($secret, $t + ($i * $this->period)), $code) === 0)
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get data-uri of QRCode
+ */
+ public function getQRCodeImageAsDataUri($label, $secret, $size = 200) {
+ if (!is_int($size) || $size < 0)
+ throw new Exception('Size must be int > 0');
+
+ return 'data:image/png;base64,'.base64_encode($this->qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size));
+ }
+
+ private function getTime($time) {
+ return ($time === null) ? time() : $time;
+ }
+
+ private function getTimeSlice($time = null, $offset = 0) {
+ return (int)floor($time / $this->period) + ($offset * $this->period);
+ }
+
+ /**
+ * Builds a string to be encoded in a QR code
+ */
+ private function getQRText($label, $secret) {
+ return 'otpauth://totp/' . rawurlencode($label)
+ . '?secret=' . rawurlencode($secret)
+ . '&issuer=' . rawurlencode($this->issuer)
+ . '&period=' . intval($this->period)
+ . '&algorithm=' . rawurlencode(strtoupper($this->algorithm))
+ . '&digits=' . intval($this->digits);
+ }
+
+ private function base32Decode($value)
+ {
+ if (strlen($value)==0) return '';
+
+ $s = '';
+ foreach (str_split($value) as $c) {
+ if ($c !== '=')
+ $s .= str_pad(decbin(self::$_base32lookup[$c]), 5, 0, STR_PAD_LEFT);
+ }
+ $l = strlen($s);
+ $r = trim(chunk_split(substr($s, 0, $l - ($l % 8)), 8, ' '));
+
+ $o = '';
+ foreach (explode(' ', $r) as $b)
+ $o .= chr(bindec(str_pad($b, 8, 0, STR_PAD_RIGHT)));
+
+ return $o;
+ }
+}
+
+interface IQRCodeProvider
+{
+ public function getQRCodeImage($qrtext, $size);
+}
+
+class GoogleQRCodeProvider implements IQRCodeProvider {
+ private $verifyssl;
+
+ function __construct($verifyssl = false) {
+ if (!is_bool($verifyssl))
+ throw new Exception('VerifySSL must be bool');
+
+ $this->verifyssl = $verifyssl;
+ }
+
+ public function getQRCodeImage($qrtext, $size) {
+ return $this->get_content($this->getUrl($qrtext, $size));
+ }
+
+ public function getUrl($qrtext, $size) {
+ return 'https://chart.googleapis.com/chart?chs='.$size.'x'.$size.'&chld=M|0&cht=qr&chl='.rawurlencode($qrtext);
+ }
+
+ private function get_content($url){
+ $ch = curl_init();
+
+ curl_setopt_array($ch, array(
+ CURLOPT_URL => $url,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_MAXREDIRS => 3,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_CONNECTTIMEOUT => 10,
+ CURLOPT_DNS_CACHE_TIMEOUT => 10,
+ CURLOPT_TIMEOUT => 10,
+ CURLOPT_SSL_VERIFYPEER => $this->verifyssl,
+ CURLOPT_USERAGENT => 'TwoFactorAuth'
+ ));
+ $data = curl_exec($ch);
+
+ curl_close($ch);
+ return $data;
+ }
+} \ No newline at end of file