From fa3055228144133a90f0ace3e0a40678d32724d8 Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 5 Jul 2026 06:06:17 +0200 Subject: [PATCH] genesis --- decrypt_jtl_pos.js | 175 +++++++++++++++++++++++++++++++++++++++++++++ encrypt_jtl_pos.js | 149 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 decrypt_jtl_pos.js create mode 100644 encrypt_jtl_pos.js diff --git a/decrypt_jtl_pos.js b/decrypt_jtl_pos.js new file mode 100644 index 0000000..675b921 --- /dev/null +++ b/decrypt_jtl_pos.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node +/** + * Decrypt a JTL POS backup archive (ZIP) to SQLite .db files and images/. + * AES-256-ECB, PKCS7 padding, hardcoded key from app. + * + * Usage: node decrypt_jtl_pos.js [output-dir] + */ +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 IMAGES_MARKER = `${path.sep}files${path.sep}images${path.sep}`; + +function usage() { + console.error('Usage: node decrypt_jtl_pos.js [output-dir]'); + process.exit(1); +} + +function rmDir(dir) { + fs.rmSync(dir, { recursive: true, force: true }); +} + +function findFile(dir, basename) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + const found = findFile(full, basename); + if (found) return found; + } else if (entry.name === basename) { + return full; + } + } + return null; +} + +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 decryptBuffer(data) { + const decipher = crypto.createDecipheriv('aes-256-ecb', KEY, null); + decipher.setAutoPadding(true); + return Buffer.concat([decipher.update(data), decipher.final()]); +} + +function md5Hex(buf) { + return crypto.createHash('md5').update(buf).digest('hex'); +} + +function imageExtension(buf) { + if (buf.length >= 4 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) { + return '.png'; + } + if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) { + return '.jpg'; + } + if (buf.length >= 4 && buf.subarray(0, 4).toString() === 'GIF8') { + return '.gif'; + } + if (buf.length >= 12 && buf.subarray(0, 4).toString() === 'RIFF' && buf.subarray(8, 12).toString() === 'WEBP') { + return '.webp'; + } + return ''; +} + +function imageDestPath(rel, data) { + if (path.extname(rel)) return rel; + const ext = imageExtension(data); + return ext ? rel + ext : rel; +} + +function extractImages(tempDir, outDir) { + const imagesDir = path.join(outDir, 'images'); + let count = 0; + + for (const file of walkFiles(tempDir)) { + const idx = file.indexOf(IMAGES_MARKER); + if (idx === -1) continue; + + const rel = imageDestPath(file.slice(idx + IMAGES_MARKER.length), fs.readFileSync(file)); + const dest = path.join(imagesDir, rel); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.copyFileSync(file, dest); + count++; + } + + return count; +} + +function unzipBackup(backup, tempDir) { + try { + execFileSync('unzip', ['-o', backup, '-d', tempDir], { stdio: 'pipe' }); + } catch { + if (!findFile(tempDir, ENCRYPTED_DBS[0])) { + throw new Error('Failed to extract backup (is it a valid ZIP?)'); + } + } +} + +function decryptBackup(backupPath, outputDir) { + const backup = path.resolve(backupPath); + if (!fs.existsSync(backup)) { + throw new Error(`Backup not found: ${backup}`); + } + + const outDir = path.resolve(outputDir || path.dirname(backup)); + fs.mkdirSync(outDir, { recursive: true }); + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jtl-pos-decrypt-')); + try { + unzipBackup(backup, tempDir); + + for (const encName of ENCRYPTED_DBS) { + const encPath = findFile(tempDir, encName); + if (!encPath) { + throw new Error(`Missing encrypted database in backup: ${encName}`); + } + + const plain = decryptBuffer(fs.readFileSync(encPath)); + if (plain.subarray(0, 15).toString() !== 'SQLite format 3') { + throw new Error(`${encName}: decrypted data is not SQLite`); + } + + const md5Path = findFile(tempDir, encName.replace(/_$/, '') + '_md5'); + if (md5Path) { + const expected = fs.readFileSync(md5Path, 'utf8').trim(); + const actual = md5Hex(plain); + if (actual.toLowerCase() !== expected.toLowerCase()) { + throw new Error(`${encName}: MD5 mismatch (expected ${expected}, got ${actual})`); + } + } + + const outName = encName.replace(/_$/, '') + '.db'; + const outPath = path.join(outDir, outName); + fs.writeFileSync(outPath, plain); + console.log(`${outPath} (${plain.length} bytes)`); + } + + const imageCount = extractImages(tempDir, outDir); + if (imageCount > 0) { + console.log(`${path.join(outDir, 'images')}/ (${imageCount} files)`); + } + } finally { + rmDir(tempDir); + } +} + +const backupArg = process.argv[2]; +if (!backupArg) usage(); + +try { + decryptBackup(backupArg, process.argv[3]); +} catch (err) { + console.error(err.message); + process.exit(1); +} diff --git a/encrypt_jtl_pos.js b/encrypt_jtl_pos.js new file mode 100644 index 0000000..f35d4cb --- /dev/null +++ b/encrypt_jtl_pos.js @@ -0,0 +1,149 @@ +#!/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); +}