<?php
/**
 * (c) ININOUT & PYXIS. All Rights Reserved.
 * Wszelkie modyfikacje tego pliku wymagaja zgloszenia oraz zgody autorow.
 * Any modifications to this file require notification and consent of the authors.
 */
/**
 * i-BOK 5 - TOTP (Time-based One-Time Password)
 * Pure PHP, no external dependencies, RFC 6238 compliant
 */

define('TOTP_BASE32_CHARS', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567');

/**
 * Generate a random TOTP secret (20 bytes = 32 base32 chars)
 */
function totp_generate_secret($length = 20) {
    return totp_base32_encode(random_bytes($length));
}

/**
 * Base32 encode
 */
function totp_base32_encode($data) {
    $binary = '';
    for ($i = 0; $i < strlen($data); $i++) {
        $binary .= str_pad(decbin(ord($data[$i])), 8, '0', STR_PAD_LEFT);
    }
    $result = '';
    $chunks = str_split($binary, 5);
    foreach ($chunks as $chunk) {
        $chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT);
        $result .= TOTP_BASE32_CHARS[bindec($chunk)];
    }
    return $result;
}

/**
 * Base32 decode
 */
function totp_base32_decode($encoded) {
    $encoded = strtoupper(trim($encoded));
    $encoded = rtrim($encoded, '=');
    $binary = '';
    for ($i = 0; $i < strlen($encoded); $i++) {
        $pos = strpos(TOTP_BASE32_CHARS, $encoded[$i]);
        if ($pos === false) continue;
        $binary .= str_pad(decbin($pos), 5, '0', STR_PAD_LEFT);
    }
    $result = '';
    $bytes = str_split($binary, 8);
    foreach ($bytes as $byte) {
        if (strlen($byte) < 8) break;
        $result .= chr(bindec($byte));
    }
    return $result;
}

/**
 * Generate TOTP code for given secret and time
 */
function totp_generate_code($secret, $timeSlice = null) {
    if ($timeSlice === null) {
        $timeSlice = (int)floor(time() / 30);
    }

    $secretKey = totp_base32_decode($secret);

    // Pack time as 8-byte big-endian
    $time = pack('N*', 0, $timeSlice);

    // HMAC-SHA1
    $hash = hash_hmac('sha1', $time, $secretKey, true);

    // Dynamic truncation
    $offset = ord($hash[19]) & 0x0F;
    $code = (
        ((ord($hash[$offset]) & 0x7F) << 24) |
        ((ord($hash[$offset + 1]) & 0xFF) << 16) |
        ((ord($hash[$offset + 2]) & 0xFF) << 8) |
        (ord($hash[$offset + 3]) & 0xFF)
    );

    return str_pad($code % 1000000, 6, '0', STR_PAD_LEFT);
}

/**
 * Verify TOTP code with time window tolerance
 */
function totp_verify($secret, $code, $window = 1) {
    $code = trim($code);
    if (strlen($code) !== 6 || !ctype_digit($code)) {
        return false;
    }

    $timeSlice = (int)floor(time() / 30);

    for ($i = -$window; $i <= $window; $i++) {
        if (hash_equals(totp_generate_code($secret, $timeSlice + $i), $code)) {
            return true;
        }
    }

    return false;
}

/**
 * Generate otpauth:// URI for QR code
 */
function totp_get_uri($secret, $account, $issuer = 'i-BOK 5') {
    return 'otpauth://totp/' . rawurlencode($issuer) . ':' . rawurlencode($account)
        . '?secret=' . $secret
        . '&issuer=' . rawurlencode($issuer)
        . '&algorithm=SHA1&digits=6&period=30';
}

/**
 * Generate random backup codes
 */
function twofa_generate_backup_codes($count = 8) {
    $codes = [];
    for ($i = 0; $i < $count; $i++) {
        $codes[] = bin2hex(random_bytes(4)); // 8-char hex codes
    }
    return $codes;
}

// ===== File storage (db/2fa/) =====

/**
 * Get path to 2FA data file
 */
function twofa_filepath($type, $id) {
    $dir = dirname(__DIR__) . '/db/2fa';
    if (!is_dir($dir)) {
        @mkdir($dir, 0750, true);
    }
    $safe_id = preg_replace('/[^a-zA-Z0-9_-]/', '', $id);
    $safe_type = ($type === 'admin') ? 'admin' : 'client';
    return $dir . '/' . $safe_type . '_' . $safe_id . '.json';
}

/**
 * Load 2FA data for user
 */
function twofa_load($type, $id) {
    $file = twofa_filepath($type, $id);
    if (!file_exists($file)) return null;
    $data = @json_decode(file_get_contents($file), true);
    return is_array($data) ? $data : null;
}

/**
 * Save 2FA data for user
 */
function twofa_save($type, $id, $data) {
    $file = twofa_filepath($type, $id);
    $dir = dirname($file);
    if (!is_dir($dir)) {
        @mkdir($dir, 0750, true);
    }
    return file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX) !== false;
}

/**
 * Check if 2FA is enabled for user
 */
function twofa_is_enabled($type, $id) {
    $data = twofa_load($type, $id);
    return $data !== null && !empty($data['enabled']) && !empty($data['secret']);
}

/**
 * Delete 2FA data for user
 */
function twofa_delete($type, $id) {
    $file = twofa_filepath($type, $id);
    if (file_exists($file)) {
        return @unlink($file);
    }
    return true;
}

/**
 * Verify and consume a backup code
 */
function twofa_use_backup_code($type, $id, $code) {
    $data = twofa_load($type, $id);
    if (!$data || empty($data['backup_codes'])) return false;

    $code = strtolower(trim($code));
    $found = false;

    foreach ($data['backup_codes'] as $idx => $stored) {
        if (hash_equals(strtolower($stored), $code)) {
            unset($data['backup_codes'][$idx]);
            $data['backup_codes'] = array_values($data['backup_codes']);
            $found = true;
            break;
        }
    }

    if ($found) {
        twofa_save($type, $id, $data);
    }

    return $found;
}
