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'; ?>
Выполнить команду
Для локальной разработки. Не используйте в интернете!