// Generated, do not edit. sha256:9577dee0ed20ca611f2d96db3a931311a0d672a76d95d80dda4d00726259c4be import { randomBytes, scrypt as scryptCb, createCipheriv, createDecipheriv, timingSafeEqual } from 'node:crypto'; import { promisify } from 'node:util'; import { readFile, writeFile, rm, mkdtemp, rename, access } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { secretsFile } from './keys.mjs'; import { run } from './db/conn.mjs'; import { initDatabase, dbFile } from './db.js'; import { keurBestand, wisselDatabaseBestand } from './db-restore.mjs'; import { fail } from '../shared/app-error.js'; const scrypt = promisify(scryptCb); const MAGIC = Buffer.from('ASSETAPP-BACKUP', 'latin1'); const VERSIE = 1; const SALT_LEN = 16; const IV_LEN = 12; const TAG_LEN = 16; const KOP_LEN = MAGIC.length + 1 + SALT_LEN + IV_LEN + TAG_LEN; const SCRYPT = Object.freeze({ N: 2 ** 15, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }); const sleutelUit = (wachtwoord, salt) => scrypt(wachtwoord, salt, 32, SCRYPT); function eisWachtwoord(wachtwoord) { const w = String(wachtwoord ?? ''); if (w.length < 8) fail('dbfile.passTooShort', null, 'Choose a password of at least 8 characters for the backup.'); return w; } const bestaat = async pad => { try { await access(pad); return true; } catch { return false; } }; async function databaseKopie() { const dir = await mkdtemp(join(tmpdir(), 'assetapp-backup-')); const uit = join(dir, 'db.sqlite'); try { run(`VACUUM INTO '${uit.replace(/'/g, "''")}'`, []); return { bytes: await readFile(uit), opruimen: () => rm(dir, { recursive: true, force: true }) }; } catch (e) { await rm(dir, { recursive: true, force: true }); throw e; } } export async function createBackup(wachtwoord) { const w = eisWachtwoord(wachtwoord); const kopie = await databaseKopie(); try { const geheimenPad = secretsFile(); const geheimen = await bestaat(geheimenPad) ? await readFile(geheimenPad) : Buffer.from('{}', 'utf-8'); const manifest = Buffer.from(JSON.stringify({ v: 1, gemaakt: new Date().toISOString(), db: kopie.bytes.length, secrets: geheimen.length, }) + '\n', 'utf-8'); const plat = Buffer.concat([manifest, kopie.bytes, geheimen]); const salt = randomBytes(SALT_LEN); const iv = randomBytes(IV_LEN); const c = createCipheriv('aes-256-gcm', await sleutelUit(w, salt), iv); const cijfer = Buffer.concat([c.update(plat), c.final()]); const bytes = Buffer.concat([MAGIC, Buffer.of(VERSIE), salt, iv, c.getAuthTag(), cijfer]); return { bytes, db: kopie.bytes.length, secrets: geheimen.length }; } finally { await kopie.opruimen(); } } async function openBackup(bytes, wachtwoord) { if (!bytes || bytes.length < KOP_LEN) fail('dbfile.notABackup', null, 'This file is not an AssetApp backup (too short).'); if (!bytes.subarray(0, MAGIC.length).equals(MAGIC)) { fail('dbfile.notABackupUseRestore', null, 'This file is not an AssetApp backup; restore a loose .sqlite with "Terugzetten".'); } const versie = bytes[MAGIC.length]; if (versie !== VERSIE) fail('dbfile.unknownBackupVersion', { versie, kent: VERSIE }, `Unknown backup version (${versie}); this app knows version ${VERSIE}.`); let p = MAGIC.length + 1; const salt = bytes.subarray(p, p += SALT_LEN); const iv = bytes.subarray(p, p += IV_LEN); const tag = bytes.subarray(p, p += TAG_LEN); const cijfer = bytes.subarray(p); const d = createDecipheriv('aes-256-gcm', await sleutelUit(String(wachtwoord ?? ''), salt), iv); d.setAuthTag(tag); let plat; try { plat = Buffer.concat([d.update(cijfer), d.final()]); } catch { fail('dbfile.decryptFailed', null, 'Decryption failed: wrong password, or the file is damaged.'); } const nl = plat.indexOf(0x0a); if (nl < 0) fail('dbfile.noManifest', null, 'Backup damaged: no manifest found.'); let manifest; try { manifest = JSON.parse(plat.subarray(0, nl).toString('utf-8')); } catch { fail('dbfile.manifestUnreadable', null, 'Backup damaged: the manifest is unreadable.'); } const dbLen = Number(manifest.db), secretsLen = Number(manifest.secrets); if (!Number.isInteger(dbLen) || !Number.isInteger(secretsLen) || nl + 1 + dbLen + secretsLen !== plat.length) { fail('dbfile.manifestLengths', null, 'Backup damaged: the lengths in the manifest do not match the content.'); } return { manifest, db: plat.subarray(nl + 1, nl + 1 + dbLen), secrets: plat.subarray(nl + 1 + dbLen), }; } export async function restoreBackup(bytes, wachtwoord) { const { manifest, db, secrets } = await openBackup(bytes, wachtwoord); const doel = dbFile(); const geheimenPad = secretsFile(); const incoming = `${doel}.new`; await writeFile(incoming, db, { mode: 0o600 }); let keuring; try { keuring = keurBestand(incoming); } catch (e) { await rm(incoming, { force: true }); throw e; } let aantalGeheimen; try { aantalGeheimen = Object.keys(JSON.parse(secrets.toString('utf-8'))).length; } catch { await rm(incoming, { force: true }); fail('dbfile.secretsUnreadable', null, 'Backup damaged: the secrets file is not readable JSON.'); } const previousDb = `${doel}.previous`; const previousSecrets = `${geheimenPad}.previous`; await wisselDatabaseBestand(incoming, previousDb, async () => { if (await bestaat(geheimenPad)) { await rm(previousSecrets, { force: true }); await rename(geheimenPad, previousSecrets); } await writeFile(geheimenPad, secrets, { mode: 0o600 }); }); const regel = initDatabase(); return { gemaakt: manifest.gemaakt ?? null, versie: keuring.versie, wezen: keuring.wezen, geheimen: aantalGeheimen, previousDb, previousSecrets, regel, }; } export function isBackupBestand(bytes) { return !!bytes && bytes.length >= MAGIC.length && bytes.subarray(0, MAGIC.length).equals(MAGIC); } export const FORMAAT = Object.freeze({ MAGIC, VERSIE, SALT_LEN, IV_LEN, TAG_LEN, KOP_LEN, SCRYPT });