Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,154 @@
|
||||
// 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 });
|
||||
Reference in new issue
Block a user