Files
posCrypt/encrypt_jtl_pos.js
2026-07-05 06:06:17 +02:00

150 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Encrypt SQLite .db files and images/ into a JTL POS backup archive (ZIP).
* AES-256-ECB, PKCS7 padding, hardcoded key from app.
*
* Usage: node encrypt_jtl_pos.js <input-dir> <output-file>
*/
const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const KEY = Buffer.from([
0xCC, 0x50, 0x12, 0x30, 0x31, 0xDC, 0x03, 0xAA,
0x62, 0x0A, 0x1A, 0x97, 0x0B, 0xCA, 0x4B, 0x8F,
0xF6, 0x3D, 0xBA, 0xA8, 0xC4, 0x53, 0x7F, 0xDD,
0x9D, 0x0F, 0x3A, 0x73, 0x57, 0x71, 0x03, 0xB2,
]);
const ENCRYPTED_DBS = ['POS_DATABASE_', 'POS_LOG_DATABASE_', 'POS_R_DATABASE_'];
const ZIP_PREFIX = '/data/user/0/com.jtl.pos/files/';
function usage() {
console.error('Usage: node encrypt_jtl_pos.js <input-dir> <output-file>');
process.exit(1);
}
function rmDir(dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
function walkFiles(dir) {
const files = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...walkFiles(full));
} else {
files.push(full);
}
}
return files;
}
function encryptBuffer(data) {
const cipher = crypto.createCipheriv('aes-256-ecb', KEY, null);
cipher.setAutoPadding(true);
return Buffer.concat([cipher.update(data), cipher.final()]);
}
function md5Hex(buf) {
return crypto.createHash('md5').update(buf).digest('hex');
}
function zipStaging(stagingDir, outputPath) {
execFileSync('python3', ['-c', `
import os
import sys
import zipfile
staging = sys.argv[1]
output = sys.argv[2]
prefix = sys.argv[3]
with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as zf:
for root, _, names in os.walk(staging):
for name in names:
full = os.path.join(root, name)
rel = os.path.relpath(full, staging).replace(os.sep, '/')
arcname = prefix + rel
with open(full, 'rb') as fh:
zf.writestr(arcname, fh.read())
`, stagingDir, outputPath, ZIP_PREFIX], { stdio: 'pipe' });
}
function addImages(inputDir, stagingDir) {
const imagesDir = path.join(inputDir, 'images');
if (!fs.existsSync(imagesDir)) return 0;
const destRoot = path.join(stagingDir, 'images');
let count = 0;
for (const file of walkFiles(imagesDir)) {
const rel = path.relative(imagesDir, file);
const dest = path.join(destRoot, rel);
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(file, dest);
count++;
}
return count;
}
function encryptBackup(inputDir, outputPath) {
const inDir = path.resolve(inputDir);
if (!fs.existsSync(inDir)) {
throw new Error(`Input directory not found: ${inDir}`);
}
const outFile = path.resolve(outputPath);
fs.mkdirSync(path.dirname(outFile), { recursive: true });
const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jtl-pos-encrypt-'));
try {
const tempDir = path.join(stagingDir, 'temp');
fs.mkdirSync(tempDir, { recursive: true });
for (const encName of ENCRYPTED_DBS) {
const base = encName.replace(/_$/, '');
const dbPath = path.join(inDir, base + '.db');
if (!fs.existsSync(dbPath)) {
throw new Error(`Missing database: ${base}.db`);
}
const plain = fs.readFileSync(dbPath);
if (plain.subarray(0, 15).toString() !== 'SQLite format 3') {
throw new Error(`${base}.db: not a SQLite database`);
}
const encrypted = encryptBuffer(plain);
fs.writeFileSync(path.join(tempDir, encName), encrypted);
fs.writeFileSync(path.join(tempDir, base + '_md5'), md5Hex(plain));
fs.writeFileSync(path.join(tempDir, base + '-journal'), '');
console.log(`${dbPath} -> ${encName} (${encrypted.length} bytes)`);
}
const imageCount = addImages(inDir, stagingDir);
if (imageCount > 0) {
console.log(`${path.join(inDir, 'images')}/ (${imageCount} files)`);
}
zipStaging(stagingDir, outFile);
console.log(outFile);
} finally {
rmDir(stagingDir);
}
}
const inputArg = process.argv[2];
const outputArg = process.argv[3];
if (!inputArg || !outputArg) usage();
try {
encryptBackup(inputArg, outputArg);
} catch (err) {
console.error(err.message);
process.exit(1);
}