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; } }