初始提交:实习工作日志系统(设计方案 + Vue3 应用 + Electron 打包)
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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<boolean> {
|
||||
return (await getSetting('pwd_salt', '')) !== ''
|
||||
}
|
||||
|
||||
export async function setupVault(password: string): Promise<void> {
|
||||
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<void> {
|
||||
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<CryptoKey> {
|
||||
if (!masterKey) throw new Error('密码库未解锁')
|
||||
return masterKey
|
||||
}
|
||||
|
||||
export async function encryptSecret(plain: string): Promise<string> {
|
||||
const key = await requireKey()
|
||||
return ENCRYPT_PREFIX + await aesGcmEncrypt(key, plain)
|
||||
}
|
||||
|
||||
export async function decryptSecret(packed: string | null | undefined): Promise<string> {
|
||||
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<Credential[]> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await db.credential.delete(id)
|
||||
}
|
||||
|
||||
export async function revealCredentialPassword(id: number): Promise<string> {
|
||||
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<number> {
|
||||
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<string> {
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user