#!/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 */ 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 '); 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); }