genesis
This commit is contained in:
175
decrypt_jtl_pos.js
Normal file
175
decrypt_jtl_pos.js
Normal file
@@ -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 <backup-file> [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 <backup-file> [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);
|
||||
}
|
||||
149
encrypt_jtl_pos.js
Normal file
149
encrypt_jtl_pos.js
Normal file
@@ -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 <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);
|
||||
}
|
||||
Reference in New Issue
Block a user