You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/apps/hq/src/migrate-plaintext.ts

54 lines
2.5 KiB
TypeScript

import type { DB } from './db'
import { isLegacyCiphertext } from './crypto'
import { writeAudit } from './audit'
/**
* D32 keyless cleanup: HQ_SECRET_KEY was removed, so any credential still stored as a
* pre-D31 AES-256-GCM payload is now unreadable dead weight. This NULLs those values so
* the affected login shows as "not set" and is ready to be re-populated in plaintext
* (re-import from APEX, or re-entered). Idempotent — plaintext rows are left untouched.
* Writes ONE audit summary. No key required.
*/
export interface ClearResult {
modulePasswords: number; moduleSecrets: number; clientDbPasswords: number; gmailTokens: number
}
export async function clearLegacyCiphertext(db: DB): Promise<ClearResult> {
const res: ClearResult = { modulePasswords: 0, moduleSecrets: 0, clientDbPasswords: 0, gmailTokens: 0 }
for (const r of await db.all<{ id: string; password_enc: string }>(
`SELECT id, password_enc FROM client_module WHERE password_enc IS NOT NULL`)) {
if (!isLegacyCiphertext(r.password_enc)) continue
await db.run(`UPDATE client_module SET password_enc=NULL WHERE id=?`, r.id)
res.modulePasswords++
}
// secrets_enc is `keylist|ciphertext`; if the ciphertext is legacy, the whole blob is dead.
for (const r of await db.all<{ id: string; secrets_enc: string }>(
`SELECT id, secrets_enc FROM client_module WHERE secrets_enc IS NOT NULL`)) {
const sep = r.secrets_enc.indexOf('|')
const cipher = sep < 0 ? r.secrets_enc : r.secrets_enc.slice(sep + 1)
if (!isLegacyCiphertext(cipher)) continue
await db.run(`UPDATE client_module SET secrets_enc=NULL WHERE id=?`, r.id)
res.moduleSecrets++
}
for (const r of await db.all<{ id: string; db_password_enc: string }>(
`SELECT id, db_password_enc FROM client WHERE db_password_enc IS NOT NULL`)) {
if (!isLegacyCiphertext(r.db_password_enc)) continue
await db.run(`UPDATE client SET db_password_enc=NULL WHERE id=?`, r.id)
res.clientDbPasswords++
}
// A dead Gmail token means Gmail must be reconnected — remove the unreadable account row.
for (const r of await db.all<{ id: string; refresh_token_enc: string }>(
`SELECT id, refresh_token_enc FROM email_account WHERE refresh_token_enc IS NOT NULL`)) {
if (!isLegacyCiphertext(r.refresh_token_enc)) continue
await db.run(`DELETE FROM email_account WHERE id=?`, r.id)
res.gmailTokens++
}
await writeAudit(db, 'system', 'clear_legacy_credentials', 'system', 'crypto', undefined, { ...res })
return res
}