Twofactor authentication and strong password

This commit is contained in:
2025-10-17 14:52:45 +02:00
parent 71a688f97f
commit dbd8c8aed2
17 changed files with 3055 additions and 14 deletions
+193
View File
@@ -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;
}
}