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;
}
}
@@ -0,0 +1,108 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
/**
* Send OTP Email Library
* Generates and sends a one-time password via email for two-factor authentication
*/
class SendOtpEmail
{
private $ci;
private $user;
private $otpCode;
private $sendResult;
public function __construct($user)
{
$this->ci =& get_instance();
$this->user = $user;
$this->ci->load->library('email');
$this->ci->load->model('beheer/UserOtp_model');
$this->handle();
}
/**
* Get the result of the email send operation
* @return bool True if email was sent successfully
*/
public function getSendResult()
{
return $this->sendResult;
}
/**
* Get the generated OTP code (for testing purposes)
* @return string The OTP code
*/
public function getOtpCode()
{
return $this->otpCode;
}
/**
* Main handler for OTP email process
*/
private function handle()
{
$this->generateOTP();
$this->sendOTPEmail();
}
/**
* Generate a new OTP code
*/
private function generateOTP()
{
// Generate OTP valid for 10 minutes
$this->otpCode = $this->ci->UserOtp_model->generateOTP($this->user->id, 10);
}
/**
* Send the OTP code via email
*/
private function sendOTPEmail()
{
// Build email configuration directly from environment variables
$config = [
'protocol' => $_ENV['PROTOCOL'] ?? getenv('PROTOCOL') ?: 'mail',
'smtp_host' => $_ENV['SMTP_HOST'] ?? getenv('SMTP_HOST') ?: '',
'smtp_user' => $_ENV['SMTP_USER'] ?? getenv('SMTP_USER') ?: '',
'smtp_pass' => $_ENV['SMTP_PASS'] ?? getenv('SMTP_PASS') ?: '',
'smtp_port' => $_ENV['SMTP_PORT'] ?? getenv('SMTP_PORT') ?: 25,
'smtp_crypto' => 'tls',
'smtp_timeout' => 30,
'charset' => 'iso-8859-1',
'mailtype' => 'html',
'newline' => "\r\n",
'crlf' => "\r\n",
'wordwrap' => TRUE,
'validate' => TRUE
];
// Re-initialize email library with config
$this->ci->email->initialize($config);
$data = [
'username' => $this->user->username,
'firstname' => $this->user->firstname,
'middlename' => $this->user->middlename,
'lastname' => $this->user->lastname,
'otp_code' => $this->otpCode
];
$html = $this->ci->load->view('emails/twofactor_otp_email', ['user' => $data], true);
$this->ci->email->from('noreply@dbois.nl', 'DBOIS');
$this->ci->email->to($this->user->email);
$this->ci->email->subject('DBOIS Tweefactorauthenticatie - Verificatiecode');
$this->ci->email->message($html);
$this->sendResult = $this->ci->email->send();
if (!$this->sendResult) {
log_message('error', 'Failed to send 2FA OTP email to ' . $this->user->email . ': ' . $this->ci->email->print_debugger());
}
}
}