Twofactor authentication and strong password
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
/**
|
||||
* Two-Factor Authentication Library
|
||||
* Implements TOTP (Time-based One-Time Password) for Google Authenticator
|
||||
* Based on RFC 6238
|
||||
*/
|
||||
class TwoFactorAuth
|
||||
{
|
||||
protected $ci;
|
||||
protected $codeLength = 6;
|
||||
protected $period = 30; // 30 seconds validity window
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ci =& get_instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new secret key for TOTP
|
||||
* @return string Base32 encoded secret
|
||||
*/
|
||||
public function generateSecret($length = 16)
|
||||
{
|
||||
$secret = '';
|
||||
$validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // Base32 alphabet
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$secret .= $validChars[random_int(0, 31)];
|
||||
}
|
||||
|
||||
return $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate QR code URL for Google Authenticator
|
||||
* @param string $secret The user's TOTP secret
|
||||
* @param string $username The username
|
||||
* @param string $issuer The application name
|
||||
* @return string QR code URL
|
||||
*/
|
||||
public function getQRCodeUrl($secret, $username, $issuer = 'DBOIS')
|
||||
{
|
||||
$encodedIssuer = rawurlencode($issuer);
|
||||
$encodedUsername = rawurlencode($username);
|
||||
|
||||
$otpauthUrl = "otpauth://totp/{$encodedIssuer}:{$encodedUsername}?secret={$secret}&issuer={$encodedIssuer}";
|
||||
|
||||
// Using Google Charts API for QR code generation
|
||||
return "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=" . urlencode($otpauthUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a TOTP code
|
||||
* @param string $secret The user's secret
|
||||
* @param string $code The code to verify
|
||||
* @param int $discrepancy Time window (default 1 = +/- 30 seconds)
|
||||
* @return bool True if code is valid
|
||||
*/
|
||||
public function verifyCode($secret, $code, $discrepancy = 1)
|
||||
{
|
||||
$currentTimeSlice = floor(time() / $this->period);
|
||||
|
||||
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
|
||||
$calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
|
||||
if ($this->timingSafeEquals($calculatedCode, $code)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TOTP code for a given time slice
|
||||
* @param string $secret The secret key
|
||||
* @param int $timeSlice The time slice (defaults to current)
|
||||
* @return string The generated code
|
||||
*/
|
||||
public function getCode($secret, $timeSlice = null)
|
||||
{
|
||||
if ($timeSlice === null) {
|
||||
$timeSlice = floor(time() / $this->period);
|
||||
}
|
||||
|
||||
$secretKey = $this->base32Decode($secret);
|
||||
|
||||
// Pack time into binary string
|
||||
$time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
|
||||
|
||||
// Hash with secret
|
||||
$hash = hash_hmac('sha1', $time, $secretKey, true);
|
||||
|
||||
// Use last 4 bits as offset
|
||||
$offset = ord(substr($hash, -1)) & 0x0F;
|
||||
|
||||
// Get 4 bytes from offset
|
||||
$hashPart = substr($hash, $offset, 4);
|
||||
|
||||
// Unpack binary value
|
||||
$value = unpack('N', $hashPart);
|
||||
$value = $value[1];
|
||||
|
||||
// Only 32 bits
|
||||
$value = $value & 0x7FFFFFFF;
|
||||
|
||||
// Get the required digits
|
||||
$modulo = pow(10, $this->codeLength);
|
||||
|
||||
return str_pad($value % $modulo, $this->codeLength, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode Base32 string
|
||||
* @param string $secret Base32 encoded string
|
||||
* @return string Binary string
|
||||
*/
|
||||
protected function base32Decode($secret)
|
||||
{
|
||||
if (empty($secret)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
$base32charsFlipped = array_flip(str_split($base32chars));
|
||||
|
||||
$paddingCharCount = substr_count($secret, '=');
|
||||
$allowedValues = array(6, 4, 3, 1, 0);
|
||||
|
||||
if (!in_array($paddingCharCount, $allowedValues)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
if ($paddingCharCount == $allowedValues[$i] &&
|
||||
substr($secret, -($allowedValues[$i])) != str_repeat('=', $allowedValues[$i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$secret = str_replace('=', '', $secret);
|
||||
$secret = str_split($secret);
|
||||
$binaryString = '';
|
||||
|
||||
for ($i = 0; $i < count($secret); $i = $i + 8) {
|
||||
$x = '';
|
||||
if (!in_array($secret[$i], $base32charsFlipped)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$eightBits = str_split($x, 8);
|
||||
|
||||
for ($z = 0; $z < count($eightBits); $z++) {
|
||||
$binaryString .= (($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : '';
|
||||
}
|
||||
}
|
||||
|
||||
return $binaryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timing-safe string comparison
|
||||
* @param string $safeString Known string
|
||||
* @param string $userString User provided string
|
||||
* @return bool True if strings match
|
||||
*/
|
||||
protected function timingSafeEquals($safeString, $userString)
|
||||
{
|
||||
if (function_exists('hash_equals')) {
|
||||
return hash_equals($safeString, $userString);
|
||||
}
|
||||
|
||||
$safeLen = strlen($safeString);
|
||||
$userLen = strlen($userString);
|
||||
|
||||
if ($userLen != $safeLen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = 0;
|
||||
|
||||
for ($i = 0; $i < $userLen; $i++) {
|
||||
$result |= (ord($safeString[$i]) ^ ord($userString[$i]));
|
||||
}
|
||||
|
||||
return $result === 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user