import { db, type Credential } from './schema' import { getSetting, setSetting } from './repo' import { aesGcmDecrypt, aesGcmEncrypt, bufToB64, deriveKeyFromPassword, randomSaltB64 } from '../lib/crypto' import { nowLocalISO } from '../lib/date' const VERIFY_TEXT = 'intern-log-vault-verify' const ENCRYPT_PREFIX = 'enc:v1:' // 主密钥仅存于内存,页面刷新或手动锁定后需重新解锁 let masterKey: CryptoKey | null = null export function isVaultUnlocked(): boolean { return masterKey !== null } export function lockVault(): void { masterKey = null } export async function isVaultInitialized(): Promise { return (await getSetting('pwd_salt', '')) !== '' } export async function setupVault(password: string): Promise { if (password.length < 6) throw new Error('主密码至少 6 位') if (await isVaultInitialized()) throw new Error('主密码已设置') const salt = randomSaltB64() const key = await deriveKeyFromPassword(password, salt) const verifier = await aesGcmEncrypt(key, VERIFY_TEXT) await setSetting('pwd_salt', salt) await setSetting('pwd_verifier', verifier) masterKey = key } export async function unlockVault(password: string): Promise { const salt = await getSetting('pwd_salt', '') if (!salt) throw new Error('主密码尚未设置') const key = await deriveKeyFromPassword(password, salt) const verifier = await getSetting('pwd_verifier', '') try { await aesGcmDecrypt(key, verifier) } catch { throw new Error('主密码不正确') } masterKey = key } async function requireKey(): Promise { if (!masterKey) throw new Error('密码库未解锁') return masterKey } export async function encryptSecret(plain: string): Promise { const key = await requireKey() return ENCRYPT_PREFIX + await aesGcmEncrypt(key, plain) } export async function decryptSecret(packed: string | null | undefined): Promise { if (!packed) return '' if (!packed.startsWith(ENCRYPT_PREFIX)) return packed const key = await requireKey() return aesGcmDecrypt(key, packed.slice(ENCRYPT_PREFIX.length)) } // ---------- 账号密码 CRUD ---------- export async function listCredentials(): Promise { return db.credential.orderBy('system').toArray() } export interface CredentialInput { system: string account: string password: string remark: string } export async function saveCredential(input: CredentialInput, id?: number): Promise { const enc = await encryptSecret(input.password) const base = { system: input.system.trim(), account: input.account.trim(), password: enc, remark: input.remark.trim() } if (id) { await db.credential.update(id, { ...base, updated_at: nowLocalISO() }) } else { await db.credential.add({ ...base, created_at: nowLocalISO(), updated_at: nowLocalISO() }) } } export async function deleteCredential(id: number): Promise { await db.credential.delete(id) } export async function revealCredentialPassword(id: number): Promise { const row = await db.credential.get(id) if (!row) throw new Error('记录不存在') return decryptSecret(row.password) } // ---------- 修改主密码:旧密钥解密全部记录后用新密钥重新加密 ---------- export async function changeMasterPassword(oldPwd: string, newPwd: string): Promise { if (!oldPwd) throw new Error('请输入旧主密码') if (newPwd.length < 6) throw new Error('新主密码至少 6 位') if (newPwd === oldPwd) throw new Error('新主密码不能与旧主密码相同') const salt = await getSetting('pwd_salt', '') if (!salt) throw new Error('密码库尚未初始化') const oldKey = await deriveKeyFromPassword(oldPwd, salt) const verifier = await getSetting('pwd_verifier', '') try { await aesGcmDecrypt(oldKey, verifier) } catch { throw new Error('旧主密码不正确') } const newKey = await deriveKeyFromPassword(newPwd, salt) const rows = await db.credential.toArray() let count = 0 for (const row of rows) { if (row.id == null) continue const plain = await decryptSecretWithKey(row.password, oldKey) const enc = ENCRYPT_PREFIX + await aesGcmEncrypt(newKey, plain) await db.credential.update(row.id, { password: enc }) count++ } await setSetting('pwd_verifier', await aesGcmEncrypt(newKey, VERIFY_TEXT)) masterKey = newKey return count } async function decryptSecretWithKey(packed: string | null | undefined, key: CryptoKey): Promise { if (!packed) return '' if (!packed.startsWith(ENCRYPT_PREFIX)) return packed return aesGcmDecrypt(key, packed.slice(ENCRYPT_PREFIX.length)) } // 供备份模块使用:生成新的随机盐(占位,保持导出接口一致) export function newSaltB64(): string { return bufToB64(crypto.getRandomValues(new Uint8Array(16)).buffer) }