Files
Password-Manager/test_pbkdf2.php
T
Zaki 40b3154a34 feat: MFA tools, single-instance, tray polish, prefs persistence
Session highlights:

- feat(prefs): DPAPI-backed key/value store (PM.UserPrefs) — fixes
  rememberedUsername being lost across reboots due to the random
  ephemeral HTTP port changing the localStorage origin every launch.
  Bridge cmd://prefs/{get,set} round-trips through Delphi.

- feat(tray): icon visible from startup (NIM_ADD at constructor, not
  at first minimize). Tray context menu themed via uxtheme!135
  SetPreferredAppMode so it follows the app's dark/light setting.

- feat(single-instance): named mutex + RegisterWindowMessage broadcast.
  Second launch posts WM_PMSHOW to HWND_BROADCAST and exits; the
  running bridge restores the window from tray. Mutex lives in Local\
  namespace so distinct Windows users can still each run one.

- feat(mfa): Authenticator sidebar view (live TOTP codes for every
  entry with a secret) + standalone TOTP generator modal (paste
  base32 / otpauth:// URI, or generate a random 20-byte secret).

- feat(sidebar): Folders / Tags / Tools sections collapsible with
  chevron toggle. Badge counts stay visible when collapsed. State
  persisted in settings_json (synced across devices).

- feat(autofill): hotkey when vault is locked now restores the app
  and focuses the master password input instead of no-op'ing
  silently. Cleaner UX for the common "I hit Ctrl+Shift+L but the
  vault was locked" path.

- feat(quick-unlock): when enabled, skip lockVault on Windows lock /
  sleep. Rationale: the DPAPI blob already gates access via the
  Windows account, so re-locking on top of the OS lock is redundant.
  Idle auto-lock still fires (separate opt-in).

- fix(quick-unlock): re-sync state.quickUnlockEnabled from DPAPI
  source-of-truth at boot, instead of trusting (now-volatile)
  localStorage.

- docs: CLAUDE.md updated with all new modules, bridge commands,
  and the port-ephemeral pitfall.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 21:31:39 +01:00

52 lines
1.9 KiB
PHP

<?php
// Diagnostic tool: compare what PHP's hash_pbkdf2 produces vs what Delphi stored.
// Usage: http://localhost/password-manager/test_pbkdf2.php?u=YOURUSER&p=YOURPASSWORD
//
// DELETE THIS FILE after diagnosis — it exposes hashes/salts in plaintext.
header('Content-Type: text/plain; charset=utf-8');
$u = $_GET['u'] ?? '';
$p = $_GET['p'] ?? '';
if (!$u || !$p) { echo "Provide ?u=USER&p=PASSWORD\n"; exit; }
$db = new SQLite3(__DIR__ . '/vault.db');
$db->busyTimeout(5000);
$st = $db->prepare('SELECT id, username, password_hash, salt, hash_algo FROM users WHERE username=:u');
$st->bindValue(':u', $u, SQLITE3_TEXT);
$row = $st->execute()->fetchArray(SQLITE3_ASSOC);
if (!$row) { echo "User '$u' not found.\n"; exit; }
echo "=== Stored in vault.db ===\n";
echo "id : " . $row['id'] . "\n";
echo "username : " . $row['username'] . "\n";
echo "hash_algo : " . ($row['hash_algo'] ?? '(null)') . "\n";
echo "salt : " . $row['salt'] . "\n";
echo "salt len : " . strlen($row['salt']) . "\n";
echo "stored hash : " . $row['password_hash'] . "\n";
echo "stored len : " . strlen($row['password_hash']) . "\n\n";
$algo = $row['hash_algo'] ?? 'pbkdf2';
if ($algo === 'pbkdf2') {
$computed = hash_pbkdf2('sha256', $p, $row['salt'], 100000);
echo "=== PHP hash_pbkdf2('sha256', '$p', salt, 100000) ===\n";
echo "computed : $computed\n";
echo "computed len : " . strlen($computed) . "\n\n";
$match = hash_equals($row['password_hash'], $computed);
echo "Match : " . ($match ? 'YES ✓ (PHP would let this user in)' : 'NO ✗ (Delphi & PHP disagree on PBKDF2)') . "\n";
if (!$match) {
echo "\nFirst chars side by side:\n";
echo "Stored : " . substr($row['password_hash'], 0, 32) . "\n";
echo "Computed : " . substr($computed, 0, 32) . "\n";
}
} else {
echo "Account uses BCRYPT, not PBKDF2. Cannot diagnose PBKDF2 mismatch here.\n";
}
$db->close();