const express = require('express'); const crypto = require('crypto'); const bcrypt = require('bcryptjs'); const db = require('../db'); const { authMiddleware } = require('../middleware/auth'); const router = express.Router(); // PBKDF2 迭代数:新 PIN 使用 OWASP 2023 建议的 600000;老用户兼容旧值 100000 const PBKDF2_ITERATIONS = 600000; const LEGACY_ITERATIONS = 100000; // In-memory session store for unlocked encryption keys(滑动过期) const unlockedSessions = new Map(); const SESSION_TTL = 3600000; // 1 小时滑动过期 // 解锁失败限流:userId -> { count, firstTs, lockedUntil } const unlockFails = new Map(); const FAIL_LIMIT = 3; // 5 分钟内最多失败次数 const FAIL_WINDOW = 5 * 60 * 1000; // 统计窗口 const LOCK_DURATION = 5 * 60 * 1000; // 锁定时长 function getEncryptionKey(userId, pin) { const setting = db.get('SELECT kdf_salt, pin_iter FROM user_settings WHERE user_id = ?', [userId]); if (!setting || !setting.kdf_salt) return null; const salt = Buffer.from(setting.kdf_salt, 'hex'); // 按用户存储的迭代数派生:老用户 100000 继续可用,新 set-pin 后为 600000 const iterations = setting.pin_iter || LEGACY_ITERATIONS; return crypto.pbkdf2Sync(pin, salt, iterations, 32, 'sha256'); } // 会话辅助:惰性过期检查 + 滑动续期 function getSession(userId) { const session = unlockedSessions.get(userId); if (!session) return null; if (Date.now() > session.expires) { // 已过期,惰性清除 unlockedSessions.delete(userId); return null; } return session; } function refreshSession(userId) { const session = getSession(userId); if (!session) return false; session.expires = Date.now() + SESSION_TTL; // 每次操作刷新过期时间 return true; } // 限流辅助 function isLocked(userId) { const rec = unlockFails.get(userId); if (!rec || !rec.lockedUntil) return false; if (Date.now() >= rec.lockedUntil) { // 锁定期结束,清除 unlockFails.delete(userId); return false; } return true; } function recordFail(userId) { const now = Date.now(); let rec = unlockFails.get(userId); if (!rec || (now - rec.firstTs) > FAIL_WINDOW) rec = { count: 0, firstTs: now, lockedUntil: 0 }; rec.count += 1; if (rec.count >= FAIL_LIMIT) { rec.lockedUntil = now + LOCK_DURATION; rec.count = 0; } unlockFails.set(userId, rec); } function encrypt(text, key) { const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const tag = cipher.getAuthTag().toString('hex'); return iv.toString('hex') + ':' + tag + ':' + encrypted; } function decrypt(encoded, key) { try { const parts = encoded.split(':'); const iv = Buffer.from(parts[0], 'hex'); const tag = Buffer.from(parts[1], 'hex'); const encrypted = parts[2]; const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); decipher.setAuthTag(tag); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch { return null; } } // Set or change PIN router.post('/set-pin', authMiddleware, (req, res) => { const { pin } = req.body; // PIN 强度:仅长度 < 6 拒绝(纯数字不强制拒绝,但提示建议包含字母) if (!pin || typeof pin !== 'string' || pin.length < 6) { return res.status(400).json({ error: 'PIN 至少 6 位,建议包含字母' }); } const existing = db.get('SELECT id, kdf_salt FROM user_settings WHERE user_id = ?', [req.user.id]); const entries = db.all('SELECT id, encrypted_password FROM password_entries WHERE user_id = ?', [req.user.id]); // L8:改 PIN 旧密文重新加密——旧条目用「解锁会话中的旧密钥」解密, // 换新 salt 新密钥后重新加密写回。若已有条目但无解锁会话,则无法解密旧数据, // 拒绝修改并要求先解锁(否则改完 PIN 旧条目将永久不可解)。 let oldKey = null; if (existing && existing.kdf_salt && entries.length > 0) { const session = getSession(req.user.id); if (!session) { return res.status(400).json({ error: '请先解锁密码管理器后再修改 PIN(需用旧 PIN 重新加密已有条目)' }); } oldKey = session.key; } const pinHash = bcrypt.hashSync(pin, 10); const kdfSalt = crypto.randomBytes(16).toString('hex'); // 新密钥直接用新 salt 派生(避免依赖已更新到 DB 的 kdf_salt) const newKey = crypto.pbkdf2Sync(pin, Buffer.from(kdfSalt, 'hex'), PBKDF2_ITERATIONS, 32, 'sha256'); try { // 事务:换 salt 与重加密原子完成(失败自动 ROLLBACK,绝不出现"盐已换但密文没重加密"的中间态) db.transaction(() => { if (existing) { db.run('UPDATE user_settings SET pin_hash=?, kdf_salt=?, pin_iter=? WHERE user_id=?', [pinHash, kdfSalt, PBKDF2_ITERATIONS, req.user.id]); } else { db.run('INSERT INTO user_settings (user_id, pin_hash, kdf_salt, pin_iter) VALUES (?, ?, ?, ?)', [req.user.id, pinHash, kdfSalt, PBKDF2_ITERATIONS]); } if (oldKey) { for (const e of entries) { const plain = decrypt(e.encrypted_password, oldKey); if (plain === null) continue; // 单条损坏则跳过保留原样,避免整批失败 db.run('UPDATE password_entries SET encrypted_password = ? WHERE id = ?', [encrypt(plain, newKey), e.id]); } } }); } catch (e) { console.error('Set-PIN error:', e.message); return res.status(500).json({ error: '服务器内部错误' }); } // Auto-unlock after setting PIN(滑动过期)——直接用新密钥,避免再查库 unlockedSessions.set(req.user.id, { key: newKey, expires: Date.now() + SESSION_TTL }); res.json({ message: 'PIN 设置成功' }); }); // Check if PIN is set router.get('/pin-status', authMiddleware, (req, res) => { const setting = db.get('SELECT pin_hash FROM user_settings WHERE user_id = ?', [req.user.id]); res.json({ hasPin: !!setting && !!setting.pin_hash, unlocked: !!getSession(req.user.id) }); }); // Unlock with PIN router.post('/unlock', authMiddleware, (req, res) => { const { pin } = req.body; if (!pin) return res.status(400).json({ error: '请输入 PIN' }); // 失败限流:锁定期内直接拒绝 if (isLocked(req.user.id)) { return res.status(429).json({ error: '尝试次数过多,请稍后再试' }); } const setting = db.get('SELECT pin_hash FROM user_settings WHERE user_id = ?', [req.user.id]); if (!setting || !setting.pin_hash) return res.status(400).json({ error: '请先设置 PIN' }); if (!bcrypt.compareSync(pin, setting.pin_hash)) { recordFail(req.user.id); // 达到失败上限后本次直接返回 429 if (isLocked(req.user.id)) return res.status(429).json({ error: '尝试次数过多,请稍后再试' }); return res.status(401).json({ error: 'PIN 错误' }); } const key = getEncryptionKey(req.user.id, pin); if (!key) return res.status(500).json({ error: '解密失败' }); unlockFails.delete(req.user.id); // 成功解锁清零失败计数 unlockedSessions.set(req.user.id, { key, expires: Date.now() + SESSION_TTL }); // 滑动过期,不再用一次性 setTimeout res.json({ message: '已解锁' }); }); // Lock router.post('/lock', authMiddleware, (req, res) => { unlockedSessions.delete(req.user.id); res.json({ message: '已锁定' }); }); // Ensure unlocked middleware(滑动过期:每次操作刷新过期时间) function requireUnlock(req, res, next) { if (!refreshSession(req.user.id)) { return res.status(401).json({ error: '请先解锁密码管理器' }); } next(); } // List all password entries (decrypted) router.get('/', authMiddleware, requireUnlock, (req, res) => { const entries = db.all('SELECT * FROM password_entries WHERE user_id = ? ORDER BY created_at DESC', [req.user.id]); const session = getSession(req.user.id); if (!session) return res.status(401).json({ error: '请先解锁密码管理器' }); const decrypted = entries.map(e => { const pwd = decrypt(e.encrypted_password, session.key); return { id: e.id, title: e.title, username: e.username, password: pwd || '', url: e.url, notes: e.notes, created_at: e.created_at }; }); res.json(decrypted); }); // Create router.post('/', authMiddleware, requireUnlock, (req, res) => { const { title, username, password, url, notes } = req.body; if (!title || !password) return res.status(400).json({ error: '标题和密码不能为空' }); const session = getSession(req.user.id); const encrypted = encrypt(password, session.key); const id = db.run( 'INSERT INTO password_entries (user_id, title, username, encrypted_password, url, notes) VALUES (?, ?, ?, ?, ?, ?)', [req.user.id, title, username || '', encrypted, url || '', notes || '']); res.json({ id, title, username, password, url, notes, message: '保存成功' }); }); // Update router.put('/:id', authMiddleware, requireUnlock, (req, res) => { const { title, username, password, url, notes } = req.body; const existing = db.get('SELECT id FROM password_entries WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]); if (!existing) return res.status(404).json({ error: '记录不存在' }); const session = getSession(req.user.id); const encrypted = encrypt(password || '', session.key); db.run( "UPDATE password_entries SET title=?, username=?, encrypted_password=?, url=?, notes=?, updated_at=datetime('now') WHERE id=?", [title || '', username || '', encrypted, url || '', notes || '', req.params.id]); res.json({ message: '更新成功' }); }); // Delete router.delete('/:id', authMiddleware, requireUnlock, (req, res) => { const existing = db.get('SELECT id FROM password_entries WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]); if (!existing) return res.status(404).json({ error: '记录不存在' }); db.run('DELETE FROM password_entries WHERE id = ?', [req.params.id]); res.json({ message: '删除成功' }); }); module.exports = router;