Inital Commit:

This commit is contained in:
Li 2023-07-24 00:15:53 +12:00
commit 8707e18fa9
16 changed files with 2242 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
out/*
node_modules/*

1451
aes.js Normal file

File diff suppressed because it is too large Load Diff

131
ent.js Normal file
View File

@ -0,0 +1,131 @@
const fs = require("fs");
const path = require("path");
const aescfb = require('./aes');
const skinKey = "s5s5ejuDru4uchuF2drUFuthaspAbepE";
const localStatePath = path.join(process.env.LocalAppData, "/Packages/Microsoft.MinecraftUWP_8wekyb3d8bbwe/LocalState");
const mcpePath = path.join(localStatePath, "/games/com.mojang/minecraftpe");
const keyDb = {};
getEntFile();
function getTitleAccountId() {
const optionsTxt = path.join(mcpePath, 'options.txt');
if(fs.existsSync(optionsTxt)) {
const options = fs.readFileSync(optionsTxt).toString();
const lines = options.split('\n');
for (let i = 0; i < lines.length; i++) {
const [key, value] = lines[i].split(':');
if (key === "last_title_account_id") {
return value.replace("\n", "").replace("\r", "");
}
}
}
}
function getEntKey() {
const titleAccountId = getTitleAccountId();
const entXorKey = "X(nG*ejm&E8)m+8c;-SkLTjF)*QdN6_Y";
const entKey = Buffer.alloc(32);
for (let i = 0; i < 32; i++) {
entKey[i] = titleAccountId.charCodeAt(i % titleAccountId.length) ^ entXorKey.charCodeAt(i);
}
return entKey;
}
function lookupKey(uuid) {
return keyDb[uuid] || skinKey;
}
function getEntFile() {
if(fs.existsSync(localStatePath)) {
const files = fs.readdirSync(localStatePath);
const entFileNames = files.filter(file => file.endsWith(".ent"));
const entFiles = entFileNames.map(file => fs.readFileSync(path.join(localStatePath, file)).toString().substring("Version2".length));
for (let index = 0; index < entFiles.length; index++) {
const entFile = entFiles[index];
const cipherText = Buffer.from(entFile, 'base64')
const decrypted = decryptBuffer(cipherText, getEntKey());
try {
const json = JSON.parse(decrypted.toString());
parseEnt(json)
} catch {
continue;
}
}
}
}
module.exports = lookupKey;
function parseEnt(ent) {
const mainReceipt = ent.Receipt;
parseReceipt(mainReceipt)
for (let index = 0; index < ent.Items.length; index++) {
const item = ent.Items[index];
const receipt = item.Receipt;
parseReceipt(receipt);
}
}
function parseReceipt(receipt) {
const receiptContent = receipt.split(".")[1];
const content = JSON.parse(atob(receiptContent));
const entitlements = content.Receipt.Entitlements;
const deviceId = content.Receipt.ReceiptData?.DeviceId;
const entityId = content.Receipt.EntityId;
if (!deviceId || !entityId) return;
const userKey = deriveUserKey(deviceId, entityId);
for (let index = 0; index < entitlements.length; index++) {
const element = entitlements[index];
if (!element.ContentKey) continue;
keyDb[element.FriendlyId] = deobfuscateContentKey(element.ContentKey, userKey);
}
}
function deriveUserKey(deviceId, entityId) {
const deviceIdBuffer = Buffer.from(deviceId, 'utf16le');
const entityIdBuffer = Buffer.from(entityId, 'utf16le');
let length = deviceIdBuffer.length;
if (entityIdBuffer.length < length) length = entityIdBuffer.length;
const userKey = Buffer.alloc(length);
for (let index = 0; index < userKey.length; index++) {
userKey[index] = deviceIdBuffer[index] ^ entityIdBuffer[index];
}
return userKey;
}
function deobfuscateContentKey(contentKey, userKey) {
const b64DecodedKey = Buffer.from(contentKey, 'base64');
let length = b64DecodedKey.length;
if (userKey.length < length) length = userKey.length;
const deobfuscatedKey = Buffer.alloc(length);
for (let index = 0; index < deobfuscatedKey.length; index++) {
deobfuscatedKey[index] = b64DecodedKey[index] ^ userKey[index];
}
return deobfuscatedKey.toString("utf16le");
}
function decryptBuffer(buffer, key) {
const bufferKey = Buffer.from(key, 'binary');
return aescfb(buffer, bufferKey);
}

31
forge.config.js Normal file
View File

@ -0,0 +1,31 @@
module.exports = {
packagerConfig: {
asar: true,
icon: 'renderer/decrypt'
},
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {},
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
},
{
name: '@electron-forge/maker-deb',
config: {},
},
{
name: '@electron-forge/maker-rpm',
config: {},
},
],
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
],
};

158
main.js Normal file
View File

@ -0,0 +1,158 @@
const {app, BrowserWindow, ipcMain, dialog} = require("electron");
const path = require("path");
const fs = require("fs");
const PackDecryptor = require("./packDecrypter");
const minecraftFolderPath = path.join(process.env.LocalAppData, "/Packages/Microsoft.MinecraftUWP_8wekyb3d8bbwe");
const localStatePath = path.join(minecraftFolderPath, "LocalState")
const premiumCachePath = path.join(localStatePath, "premium_cache");
const worldsPath = path.join(localStatePath, "/games/com.mojang/minecraftWorlds");
function checkWorldEncrypted(worldPath){
const worldDbFolder = path.join(worldPath, "db");
const dbFiles = fs.readdirSync(worldDbFolder);
for(let index = 0; index < dbFiles.length; index++) {
if(path.extname(dbFiles[index]).toLowerCase() === ".ldb") {
return PackDecryptor.isContentFileEncrypted(path.join(worldDbFolder, dbFiles[index]))
}
}
return false;
}
function getWorlds() {
const worlds = [];
if(fs.existsSync(worldsPath)) {
const files = fs.readdirSync(worldsPath);
for (let index = 0; index < files.length; index++) {
const isEncrypted = checkWorldEncrypted(path.join(worldsPath, files[index]));
if (!isEncrypted) continue;
const name = fs.readFileSync(path.join(worldsPath, files[index], 'levelname.txt'), 'utf8');
const packIcon = getPackIcon(path.join(worldsPath, files[index]));
worlds.push({
name: replaceName(name),
packPath: path.join(worldsPath, files[index]),
packIcon,
})
}
}
return worlds;
}
function getPremiumCache() {
const packTypes = {};
if(fs.existsSync(premiumCachePath)) {
const files = fs.readdirSync(premiumCachePath);
for (let index = 0; index < files.length; index++) {
const dirname = files[index];
packTypes[dirname] = [];
const packs = getPacks(path.join(premiumCachePath, dirname))
if (packs.length === 0) {
delete packTypes[dirname];
continue;
}
packTypes[dirname] = packs;
}
}
return packTypes
}
function getPacks(dirPath) {
const packList = fs.readdirSync(dirPath);
return packList.map(packDir => {
const packPath = path.join(dirPath, packDir);
const packName = getPackName(packPath)
return {
name: replaceName(packName),
packPath: packPath,
packIcon: getPackIcon(packPath),
}
})
}
function replaceName(name) {
return name
.replaceAll("#", "")
.replaceAll("?", "")
.replaceAll("*", "")
.replaceAll("<", "")
.replaceAll(">", "")
.replaceAll("|", "")
.replaceAll(":", "")
.replaceAll("\\", "")
.replaceAll("/", "")
.trim()
}
function getPackIcon(packPath) {
const packIconNames = ["pack_icon.png", "pack_icon.jpeg" ,"world_icon.jpeg", "world_icon.png"]
for (let index = 0; index < packIconNames.length; index++) {
const packIconName = packIconNames[index];
const iconPath = path.join(packPath, packIconName);
if (fs.existsSync(iconPath)) {
return fs.readFileSync(iconPath, 'base64')
}
}
return null;
}
function getPackName(packPath) {
const langFile = fs.readFileSync(path.join(packPath, "texts", "en_US.lang"), 'utf8');
return langFile.split("\n")[0].split("=").at(-1).replace("\n", "").replace("\r", "");
}
app.whenReady().then(() => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegrationInWorker: true,
}
});
win.removeMenu()
win.loadFile("./renderer/index.html")
//win.webContents.openDevTools({mode: 'detach'})
ipcMain.handle("get-packs", (event) => {
packs = {worlds: getWorlds(), ...getPremiumCache()};
if(packs["worlds"].length == 0)
delete packs["worlds"];
return packs;
})
ipcMain.handle("pick-path", async (event, {path, type, name}) => {
const filter = {};
if (type === "world_templates") {
filter.name = "World Template";
filter.extensions = ["mctemplate"]
}
if (type === "resource_packs") {
filter.name = "Resource Pack";
filter.extensions = ["mcpack"]
}
if (type === "skin_packs") {
filter.name = "Skin Pack";
filter.extensions = ["mcpack"]
}
if (type === "persona") {
filter.name = "Persona Peice";
filter.extensions = ["mcpersona"]
}
if (type === "worlds") {
filter.name = "World";
filter.extensions = ["mcworld"]
}
const dialogReturnValue = await dialog.showSaveDialog({
defaultPath: name,
filters: [filter]
})
if (dialogReturnValue.canceled) return;
return dialogReturnValue.filePath;
})
})

197
packDecrypter.js Normal file
View File

@ -0,0 +1,197 @@
const fs = require("fs");
const path = require("path");
const aescfb = require('./aes');
const JSZip = require("jszip");
const lookupKey = require("./ent");
const Progress = require("./progress");
const { ipcMain } = require("electron");
module.exports = class PackDecryptor extends Progress {
inputPath = "";
outputFilePath = "";
zip = new JSZip();
zippedContent = [];
contentFiles = []
decryptDenylist = ["pack_icon.png", "pack_icon.jpeg", "world_icon.png", "world_icon.jpeg", "manifest.json"]
constructor(inputPath, outputFilePath) {
super();
this.inputPath = inputPath;
this.outputFilePath = outputFilePath;
}
async start() {
return new Promise(async res => {
console.log("start")
const dbPath = path.join(this.inputPath, "db");
this.contentFiles = recursiveReaddirrSync(this.inputPath);
this._started = true;
if (fs.existsSync(dbPath)) {
const dbDir = recursiveReaddirrSync(dbPath);
for (let index = 0; index < dbDir.length; index++) {
const dbFilePath = dbDir[index];
if (fs.lstatSync(dbFilePath).isDirectory()) {
continue;
}
const decrypted = await this.decryptContentFile(dbFilePath);
this.addFile(dbFilePath, decrypted)
}
}
for (let index = 0; index < this.contentFiles.length; index++) {
const name = path.basename(this.contentFiles[index]);
if (name.toLowerCase() === "contents.json") {
await this.decryptContent(this.contentFiles[index]);
}
}
for (let index = 0; index < this.contentFiles.length; index++) {
const filePath = this.contentFiles[index];
if (!this.zippedContent.includes(filePath)) {
this.addFile(filePath, fs.readFileSync(filePath));
}
}
await this.crackSkinPack()
this.crackWorld();
this.zip.generateAsync({type:"arraybuffer"}).then((content) => {
fs.writeFileSync(this.outputFilePath, Buffer.from(content));
console.log("done")
res(0);
});
})
}
crackWorld() {
const levelDatPath = path.join(this.inputPath, "level.dat");
if (!fs.existsSync(levelDatPath)) return;
const levelDat = fs.readFileSync(levelDatPath);
let offset = levelDat.indexOf("prid");
while (offset !== -1) {
levelDat.writeUInt8("a".charCodeAt(0), offset);
offset = levelDat.indexOf("prid");
}
this.addFile(levelDatPath, levelDat)
}
addFile(filePath, content) {
const relPath = path.relative(this.inputPath, filePath).replaceAll("\\", "/");
if (!this.zippedContent.includes(filePath)) {
this.zippedContent.push(filePath);
}
this.zip.file(relPath, content, {binary: true})
self.postMessage(this.getPercentage());
}
static isContentFileEncrypted(filePath){
const contents = fs.readFileSync(filePath);
if (contents.length < 0x100) return false;
const magic = contents.readUint32LE(0x4);
if (magic === 2614082044) {
return true;
}
return false;
}
async decryptContent(filePath) {
const dirname = path.dirname(filePath);
const isEncrypted = PackDecryptor.isContentFileEncrypted(filePath);
const content = await this.decryptContentFile(filePath);
const parsedContent = JSON.parse(content);
if(isEncrypted) {
for (let index = 0; index < parsedContent.content.length; index++) {
const key = parsedContent.content[index].key;
const filePath = parsedContent.content[index].path;
const fileName = path.basename(filePath);
if(this.decryptDenylist.indexOf(fileName.toLowerCase()) !== -1) continue;
if (!key) continue;
const joinedPath = path.join(dirname, filePath);
const file = await this.decryptFile(joinedPath, key);
this.addFile(joinedPath, file)
}
}
this.addFile(filePath, content)
}
async decryptContentFile(filePath) {
const contents = fs.readFileSync(filePath);
if (contents.length < 0x100) return contents;
const magic = contents.readUint32LE(0x4);
if (magic === 2614082044) {
const cipherText = contents.subarray(0x100);
const uuidSize = contents.readUInt8(0x10)
const uuid = contents.subarray(0x11, 0x11 + uuidSize)
const key = lookupKey(uuid);
const decrypted = decryptAes(key, cipherText)
return decrypted
} else {
return contents;
}
}
async decryptFile(filePath, key) {
const contents = fs.readFileSync(filePath);
const decrypted = decryptAes(key, contents)
return decrypted;
}
async crackSkinPack() {
const skinJsonFilePath = "skins.json";
if (!this.zip.files[skinJsonFilePath]) return;
const skinsFile = await this.zip.files[skinJsonFilePath].async("string");
try{
const skins = JSON.parse(skinsFile);
for (let index = 0; index < skins.skins.length; index++) {
const skin = skins.skins[index];
skin.type = "free";
}
this.addFile(path.join(this.inputPath, skinJsonFilePath), JSON.stringify(skins, null, 2));
}
catch(Exception) {};
}
}
function recursiveReaddirrSync(dir) {
let results = [];
let list = fs.readdirSync(dir);
list.forEach(function (file) {
file = path.join(dir, file);
let stat = fs.statSync(file);
if (stat && stat.isDirectory()) {
results = results.concat(recursiveReaddirrSync(file));
} else {
results.push(file);
}
});
return results;
}
function decryptAes(key, buffer) {
const bufferKey = Buffer.from(key, 'binary');
return aescfb(buffer, bufferKey);
}

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "MCPackDecrypt",
"version": "1.0.0",
"description": "Marketplace Pack Decrypter",
"main": "main.js",
"scripts": {
"start": "electron .",
"package": "electron-forge package",
"make": "electron-forge make"
},
"keywords": [],
"author": "MCPackDecrypt",
"license": "MIT",
"dependencies": {
"electron-squirrel-startup": "^1.0.0",
"jszip": "^3.10.1"
},
"devDependencies": {
"@electron-forge/cli": "^6.2.1",
"@electron-forge/maker-deb": "^6.2.1",
"@electron-forge/maker-rpm": "^6.2.1",
"@electron-forge/maker-squirrel": "^6.2.1",
"@electron-forge/maker-zip": "^6.2.1",
"@electron-forge/plugin-auto-unpack-natives": "^6.2.1",
"electron": "^22.3.18"
}
}

6
preload.js Normal file
View File

@ -0,0 +1,6 @@
const {contextBridge, ipcRenderer} = require('electron');
contextBridge.exposeInMainWorld("electron", {
getPacks: () => ipcRenderer.invoke('get-packs'),
pickPath: (inputDir) => ipcRenderer.invoke('pick-path', inputDir),
})

14
progress.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = class Progress {
_started = false;
constructor() {
}
getPercentage() {
if (!this._started) {
return 0;
}
return Math.round((this.zippedContent.length / this.contentFiles.length ) * 100);
}
isStarted(){
return this._started;
}
}

BIN
renderer/Thumbs.db Normal file

Binary file not shown.

BIN
renderer/decrypt.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

11
renderer/index.html Normal file
View File

@ -0,0 +1,11 @@
<html>
<head>
<link rel="stylesheet" href="./styles.css">
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
<title>MC Pack Decrypter</title>
</head>
<body>
<div id="categories"></div>
</body>
<script src="./script.js"></script>
</html>

BIN
renderer/pack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

117
renderer/script.js Normal file
View File

@ -0,0 +1,117 @@
electron.getPacks().then(ready)
const worker = new Worker('worker.js')
const queue = [];
function addToQueue(element, postFunc) {
queue.push({
element: element,
postFunc: postFunc
})
if (queue.length === 1) {
nextQueue();
}
}
function endQueue() {
queue.shift();
}
function nextQueue() {
const item = queue[0]
if (!item) return;
item.element.id = "pack-running";
item.postFunc();
}
worker.onmessage = (e) => {
if (e.data === "end") {
document.querySelector('#pack-running #pack-progress').style.width = "0%";
document.querySelector('#pack-running').id = "pack";
endQueue();
nextQueue();
if (queue.length === 0) {
alert("The pack(s) was decrypted.")
}
return;
}
document.querySelector('#pack-running #pack-progress').style.width = e.data + "%"
}
const categoriesEl = document.getElementById("categories");
function ready(categories) {
const order = ["worlds", "world_templates", "resource_packs", "skin_packs", "persona"];
const keys = Object.keys(categories).sort((a, b) => {
return order.indexOf(a) - order.indexOf(b)
});
if(keys.length > 0) {
for (let i = 0; i < keys.length; i++) {
const name = keys[i];
const categoryEl = createCategoryEl(name, categories[name])
categoriesEl.appendChild(categoryEl);
}
}
else {
displayError("No encrypted pack(s) were found.");
}
}
function displayError(msg) {
const errorEl = document.createElement("div");
errorEl.classList.add("error-msg")
const errorP = document.createElement("p");
errorP.textContent = msg;
errorEl.appendChild(errorP);
categoriesEl.appendChild(errorEl);
}
function createCategoryEl(name, packs) {
const categoryEl = document.createElement("div");
categoryEl.classList.add("category");
categoryEl.innerHTML = `<div class="category-title">${name.replace("_", " ")}</div>`
const packsEl = document.createElement("div");
packsEl.classList.add("packs");
for (let i = 0; i < packs.length; i++) {
const pack = packs[i];
const packEl = createPackEl(pack, name);
packsEl.appendChild(packEl);
}
categoryEl.appendChild(packsEl);
return categoryEl;
}
function createPackEl(pack, type) {
const packEl = document.createElement("div");
packEl.classList.add("pack");
const packClick = async() => {
const outPath = await electron.pickPath({path: pack.packPath, type, name: pack.name});
if (!outPath) return;
if(packEl.id === "pack-queued") return;
if(packEl.id === "pack-running") return;
packEl.id = "pack-queued"
addToQueue(packEl, () => worker.postMessage({outPath, path: pack.packPath, type, name: pack.name}))
}
packEl.addEventListener("click", packClick)
packEl.innerHTML = `
<div id="pack-progress"></div>
<img class="pack-icon ${!pack.packIcon ? 'pack-unknown-icon' : ''}" src="${pack.packIcon ? `data:image/png;base64,${pack.packIcon}` : './pack.png'}" class="pack-icon"></img>
<div class="pack-name">${pack.name}</div>
`
return packEl;
}

88
renderer/styles.css Normal file
View File

@ -0,0 +1,88 @@
body {
background-color: black;
color: white;
font-family: Arial, Helvetica, sans-serif;
}
.category {
background: rgba(128, 128, 128, 0.2);
margin: 10px;
padding: 10px;
border-radius: 6px;
}
.category-title {
font-weight: bold;
margin-bottom: 10px;
text-transform: capitalize;
}
.packs {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.pack {
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
width: 160px;
flex-shrink: 0;
background: rgba(255,255,255,0.1);
padding: 10px;
border-radius: 6px;
cursor: pointer;
}
.pack:hover {
background: rgba(255,255,255,0.2);
}
.pack-name {
text-align: center;
}
.pack-icon-container {
width: 64px;
height: 64px;
}
.error-msg {
text-align: center;
font-size: 200%;
color: #fbff00;
top: 50%;
background-color: #7e509b;
left: 50%;
border-radius: 50px;
padding: 20px;
transform: translate(-50%,-50%);
position: absolute;
}
.pack-icon {
width: 64px;
height: 64px;
}
.pack-unknown-icon {
filter: grayscale(100%);
}
#pack-progress {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 0%;
background-color: rgb(78, 78, 255);
z-index: -1;
}
#pack-queued {
background-color: rgb(25, 109, 78);
}

8
renderer/worker.js Normal file
View File

@ -0,0 +1,8 @@
const PackDecryptor = require("../packDecrypter");
self.addEventListener("message", async function(e) {
const decryptor = new PackDecryptor(e.data.path, e.data.outPath);
await decryptor.start();
self.postMessage("end");
});