PHP WebShell

Текущая директория: /var/www/bitcardoApp/security/totp

Просмотр файла: setup.php

<?php
// security/totp/setup.php — Enroll user into TOTP & generate backup codes once (shown once)
require_once __DIR__ . '/../../config/bootstrap.php';
require_once __DIR__ . '/../../lib/totp.php';

if (empty($_SESSION['user_id'])) { header('Location: /auth/login.php'); exit; }
$userId = (int)$_SESSION['user_id'];
$email  = $_SESSION['email'] ?? 'user@bitcardo.com';

// --- helpers (local) ---
function bc_generate_backup_codes(int $n = 10): array {
  $codes = [];
  for ($i=0; $i<$n; $i++) {
    $raw = strtoupper(bin2hex(random_bytes(5)));      // 10 hex chars
    $codes[] = rtrim(chunk_split($raw, 4, '-'), '-'); // AAAA-BBBB-CC style
  }
  return $codes;
}
function bc_backup_count(mysqli $conn, int $userId): int {
  $c = 0;
  $q = $conn->prepare("SELECT COUNT(*) FROM user_backup_codes WHERE user_id=? AND used_at IS NULL");
  $q->bind_param('i', $userId); $q->execute(); $q->bind_result($c); $q->fetch(); $q->close();
  return (int)$c;
}
function bc_insert_codes(mysqli $conn, int $userId, array $codes): void {
  $ins = $conn->prepare("INSERT INTO user_backup_codes (user_id, code_hash) VALUES (?, ?)");
  foreach ($codes as $c) {
    $hash = hash('sha256', strtoupper(str_replace([' ', '-'], '', $c)));
    $ins->bind_param('is', $userId, $hash);
    $ins->execute();
  }
  $ins->close();
}

// Check if already enabled
$secret = null; $enabled = 0;
$stmt = $conn->prepare("SELECT secret_base32, enabled FROM user_totp WHERE user_id=? LIMIT 1");
$stmt->bind_param('i', $userId);
$stmt->execute();
$stmt->bind_result($secret, $enabled);
$stmt->fetch();
$stmt->close();

$justEnabled = false;
$error = '';
$backupRemain = bc_backup_count($conn, $userId);

// If not present, create a pending secret (enabled = 0)
if (!$secret) {
  $secret = totp_secret_generate();
  $stmt = $conn->prepare("INSERT INTO user_totp (user_id, secret_base32, enabled) VALUES (?, ?, 0)
                          ON DUPLICATE KEY UPDATE secret_base32=VALUES(secret_base32)");
  $stmt->bind_param('is', $userId, $secret);
  $stmt->execute();
  $stmt->close();
}

// Handle form verify to enable
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['verify_code'])) {
  $code = $_POST['verify_code'] ?? '';
  if (totp_verify($secret, $code, 1)) {
    // enable TOTP
    $u = $conn->prepare("UPDATE user_totp SET enabled=1 WHERE user_id=?");
    $u->bind_param('i', $userId);
    $u->execute();
    $u->close();
    $enabled = 1;
    $justEnabled = true;

    // generate backup codes ONCE if none exist; show them once via session
    $backupRemain = bc_backup_count($conn, $userId);
    if ($backupRemain === 0) {
      $codes = bc_generate_backup_codes(10);
      bc_insert_codes($conn, $userId, $codes);
      $_SESSION['backup_shown_once'] = $codes; // will render once below
      $backupRemain = 10;
    }
  } else {
    $error = 'Invalid code. Open your authenticator app and try again.';
  }
}

// SAFETY NET: If TOTP is enabled already but backup codes are missing, generate once now.
if ($enabled && $backupRemain === 0 && empty($_SESSION['backup_shown_once'])) {
  $codes = bc_generate_backup_codes(10);
  bc_insert_codes($conn, $userId, $codes);
  $_SESSION['backup_shown_once'] = $codes;
  $backupRemain = 10;
}

// otpauth URI
$issuer = rawurlencode('Bitcardo');
$label  = rawurlencode($email);
$otpauth = "otpauth://totp/{$issuer}:{$label}?secret={$secret}&issuer={$issuer}&algorithm=SHA1&digits=6&period=30";

// External QR (client downloads the QR from a public service)
$qrUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' . urlencode($otpauth);

// one-time reveal of codes after enabling (or safety net)
$justCodes = $_SESSION['backup_shown_once'] ?? null;
if ($justCodes) unset($_SESSION['backup_shown_once']);

// View
include __DIR__ . '/../../user/common/header.php';
?>
<style>
  .secure-card { border:1px solid rgba(7,98,137,.12); border-radius:16px; box-shadow:0 10px 30px rgba(7,98,137,.08); background:#fff; }
  .secure-input { padding:10px 12px; border:1px solid #e5eaee; border-radius:10px; }
  .secure-input:focus { border-color:#0a7bab; box-shadow: 0 0 0 3px rgba(10,123,171,.12); }
  .btn-secure-primary{ background:#076289; border-color:#076289; color:#fff !important; }
  .btn-secure-primary:hover, .btn-secure-primary:focus{
    background:#fff; border-color:#076289; color:#076289 !important; box-shadow:0 0 0 3px rgba(7,98,137,.12);
  }
  .btn-rounded { border-radius:999px; }
  .muted { color:#6b7280; }
  .code-row code { letter-spacing:.5px; }
</style>

<div class="container mt-5">
  <div class="offset-md-3 col-md-6 pt-4 mt-5">
    <div class="secure-card p-4">
      <h3 class="mb-1">Set up Authenticator (TOTP)</h3>
      <p class="muted mb-3">Add an extra lock to your account with a time-based code from an authenticator app.</p>

      <?php if (!empty($error)): ?>
        <div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
      <?php endif; ?>

      <?php if (!$enabled): ?>
        <div class="row g-3 align-items-center">
          <div class="col-12 col-md-5 text-center">
            <img src="<?= htmlspecialchars($qrUrl) ?>" width="180" height="180" alt="Scan QR in your authenticator" />
            <div class="small muted mt-2">Scan this QR in Google Authenticator, 1Password, or Authy.</div>
          </div>
          <div class="col-12 col-md-7">
            <label class="form-label mt-2">Or enter this key manually</label>

            <div class="input-group">
              <input
                type="text"
                id="totpSecret"
                class="form-control form-control-lg font-monospace"
                value="<?= htmlspecialchars($secret) ?>"
                readonly
                aria-label="TOTP secret (read only)"
              >
              <button
                type="button"
                id="copyTotpSecret"
                class="btn btn-outline-secondary"
                aria-label="Copy secret"
              >Copy</button>
            </div>

            <small class="text-muted d-block mt-1">
              Keep this secret safe. Do not share it.
            </small>

            <form method="post" class="mt-3">
              <label for="verify_code" class="form-label">Enter the 6-digit code</label>
              <input id="verify_code" name="verify_code" inputmode="numeric" autocomplete="one-time-code" class="form-control secure-input" required>
              <button class="btn btn-secure-primary btn-rounded mt-3">Enable TOTP</button>
            </form>
          </div>
        </div>

      <?php else: ?>
        <div class="alert alert-success">Authenticator is enabled on your account.</div>

        <?php if ($justCodes && is_array($justCodes) && count($justCodes)): ?>
          <div class="alert alert-warning">
            <strong>Save these backup codes now.</strong> This is the only time they will be shown.
          </div>
          <ul class="list-group">
            <?php foreach ($justCodes as $c): ?>
              <li class="list-group-item d-flex justify-content-between align-items-center code-row">
                <code><?= htmlspecialchars($c) ?></code>
                <button class="btn btn-sm btn-outline-secondary"
                        onclick="navigator.clipboard.writeText('<?= htmlspecialchars($c, ENT_QUOTES) ?>'); this.textContent='Copied!'; setTimeout(()=>this.textContent='Copy', 1200);">
                  Copy
                </button>
              </li>
            <?php endforeach; ?>
          </ul>
          <div class="d-flex gap-2 mt-3">
            <button class="btn btn-outline-secondary" onclick="window.print()">Print</button>
            <button class="btn btn-outline-secondary" onclick="downloadCodes()">Download TXT</button>
          </div>
          <script>
            function downloadCodes(){
              const items = Array.from(document.querySelectorAll('.code-row code')).map(n => n.textContent.trim());
              if (!items.length) return;
              const blob = new Blob([items.join('\n')+'\n'], {type:'text/plain'});
              const a = document.createElement('a');
              a.href = URL.createObjectURL(blob);
              a.download = 'bitcardo-backup-codes.txt';
              document.body.appendChild(a); a.click(); a.remove();
              setTimeout(()=> URL.revokeObjectURL(a.href), 0);
            }
          </script>
        <?php else: ?>
          <div class="muted">Backup codes are set up. Remaining active codes: <strong><?= (int)$backupRemain ?></strong>.</div>
          <div class="d-flex gap-2 mt-2">
            <!-- No manage/regenerate button by design -->
            <a class="btn btn-outline-danger btn-rounded" href="/security/totp/disable.php">Disable TOTP</a>
          </div>
        <?php endif; ?>

      <?php endif; ?>
    </div>
  </div>

  <div class="mt-5 text-center">
    <a href="/user/dashboard/index.php" class="py-1 px-3 btn btn-sm btn-rounded btn-light mt-3 border">Return to Dashboard</a>
  </div>
</div>

<script>
  (function(){
    const btn = document.getElementById('copyTotpSecret');
    const inp = document.getElementById('totpSecret');
    if (!btn || !inp) return;

    btn.addEventListener('click', async function () {
      inp.focus(); inp.select(); inp.setSelectionRange(0, 99999);
      try { await navigator.clipboard.writeText(inp.value); }
      catch(e){ document.execCommand('copy'); }

      const old = btn.textContent;
      btn.textContent = 'Copied!';
      btn.classList.remove('btn-outline-secondary');
      btn.classList.add('btn-success');
      setTimeout(() => {
        btn.textContent = old;
        btn.classList.remove('btn-success');
        btn.classList.add('btn-outline-secondary');
        inp.setSelectionRange(0, 0);
        inp.blur();
      }, 1200);
    }, false);
  })();
</script>
<?php include __DIR__ . '/../../user/common/footer.php'; ?>

Выполнить команду


Для локальной разработки. Не используйте в интернете!