UNPKG

@origami-minecraft/devbuilds

Version:

Origami is a terminal-first Minecraft launcher that supports authentication, installation, and launching of Minecraft versions — with built-in support for Microsoft accounts, mod loaders, profile management, and more. Designed for power users, modders, an

325 lines 13.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LauncherAccountManager = void 0; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const os = __importStar(require("os")); const crypto_1 = __importDefault(require("crypto")); const keytar_1 = __importDefault(require("keytar")); const common_1 = require("../../utils/common"); const handler_1 = require("../launch/handler"); const chalk_1 = __importDefault(require("chalk")); const inquirer_1 = __importDefault(require("inquirer")); const defaults_1 = require("../../../config/defaults"); const _1 = require("."); const SERVICE = 'OrigamiLauncher'; const ACCOUNT = os.userInfo().username; const IV_LENGTH = 16; const HMAC_ALGO = 'sha256'; const mcDir = (0, common_1.minecraft_dir)(true); const launcherProfilesPath = path.join(mcDir, 'accounts.dat'); const old_launcherProfilesPath = path.join(mcDir, 'launcher_profiles.json'); async function getOrGenerateKey() { const stored = await keytar_1.default.getPassword(SERVICE, ACCOUNT); if (stored) { return Buffer.from(stored, 'hex'); } const fingerprint = `${os.hostname()}-${os.arch()}-${os.platform()}-${defaults_1.ORIGAMI_CLIENT_TOKEN}`; const salt = crypto_1.default.randomBytes(16); const key = crypto_1.default.pbkdf2Sync(fingerprint, salt, 100_000, 32, 'sha256'); await keytar_1.default.setPassword(SERVICE, ACCOUNT, key.toString('hex')); return key; } function computeHMAC(data, key) { return crypto_1.default.createHmac(HMAC_ALGO, key).update(data).digest('base64'); } function encryptWithKey(text, key) { const iv = crypto_1.default.randomBytes(IV_LENGTH); const cipher = crypto_1.default.createCipheriv('aes-256-cbc', key, iv); let encrypted = cipher.update(text, 'utf8', 'base64'); encrypted += cipher.final('base64'); return iv.toString('base64') + ':' + encrypted; } function decryptWithKey(text, key) { const [ivBase64, encrypted] = text.split(':'); const iv = Buffer.from(ivBase64, 'base64'); const decipher = crypto_1.default.createDecipheriv('aes-256-cbc', key, iv); let decrypted = decipher.update(encrypted, 'base64', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } async function migrateLegacyFormat(filePath, currentKey) { try { const old = fs.existsSync(old_launcherProfilesPath) ? fs.readFileSync(old_launcherProfilesPath, 'utf-8') : '{}'; // Case 1: Plain JSON file (unencrypted) try { const parsed = JSON.parse(old); if (parsed.accounts) { handler_1.logger.warn('⚠️ Detected old launcher_profiles accounts. Migrating to encrypted format...'); const newData = parsed; const plaintext = JSON.stringify(newData); const encrypted = encryptWithKey(plaintext, currentKey); const hmac = computeHMAC(encrypted, currentKey); const wrapped = { encrypted, hmac }; fs.writeFileSync(filePath, JSON.stringify(wrapped, null, 2)); if (fs.existsSync(old_launcherProfilesPath)) { delete parsed.accounts; fs.writeFileSync(old_launcherProfilesPath, JSON.stringify(parsed, null, 2)); } ; return newData; } } catch (_) { // Not valid JSON, fall through to next check } const raw = fs.readFileSync(filePath, 'utf-8'); // Case 1: Plain JSON file (unencrypted) try { const parsed = JSON.parse(raw); if (parsed.accounts) { handler_1.logger.warn('⚠️ Detected unencrypted accounts.dat. Migrating to encrypted format...'); const newData = parsed; const plaintext = JSON.stringify(newData); const encrypted = encryptWithKey(plaintext, currentKey); const hmac = computeHMAC(encrypted, currentKey); const wrapped = { encrypted, hmac }; fs.writeFileSync(filePath, JSON.stringify(wrapped, null, 2)); return newData; } } catch (_) { // Not valid JSON, fall through to next check } // Case 2: Legacy encrypted format (AES-256-CBC with static ORIGAMI_CLIENT_TOKEN) try { const legacyKey = crypto_1.default.createHash('sha256').update(defaults_1.ORIGAMI_CLIENT_TOKEN).digest(); const decrypted = decryptWithKey(raw, legacyKey); const parsed = JSON.parse(decrypted); if (parsed.accounts) { handler_1.logger.warn('⚠️ Detected legacy-encrypted accounts.dat. Migrating to encrypted format...'); const newData = parsed; const plaintext = JSON.stringify(newData); const encrypted = encryptWithKey(plaintext, currentKey); const hmac = computeHMAC(encrypted, currentKey); const wrapped = { encrypted, hmac }; fs.writeFileSync(filePath, JSON.stringify(wrapped, null, 2)); return newData; } } catch (_) { // Not decryptable with legacy key } return null; } catch (err) { handler_1.logger.error('❌ Failed to read or migrate legacy accounts.dat:', err.message); return null; } } class LauncherAccountManager { filePath; data; key = null; constructor(filePath = launcherProfilesPath) { this.filePath = filePath; this.data = { accounts: {} }; this.load(); } async ensureKey() { if (!this.key) { this.key = await getOrGenerateKey(); } } async load() { await this.ensureKey(); if (fs.existsSync(this.filePath)) { try { const rawContent = fs.readFileSync(this.filePath, 'utf-8'); const parsed = JSON.parse(rawContent); if (!parsed.encrypted || !parsed.hmac) { throw new Error("Not new format"); } const { encrypted, hmac } = parsed; const computedHmac = computeHMAC(encrypted, this.key); if (computedHmac !== hmac) { throw new Error('HMAC validation failed.'); } const decrypted = decryptWithKey(encrypted, this.key); const json = JSON.parse(decrypted); if (json.selectedAccount) { this.data.selectedAccount = json.selectedAccount; } if (json.accounts) { this.data.accounts = json.accounts; } else { throw new Error("Decrypted JSON does not contain accounts."); } } catch (err) { handler_1.logger.warn('⚠️ Encrypted load failed. Attempting migration...'); const migrated = await migrateLegacyFormat(this.filePath, this.key); if (migrated) { this.data = migrated; } else { handler_1.logger.error('❌ Could not migrate legacy accounts.dat. Starting fresh.'); this.data = { accounts: {} }; } } } else { await this.save(); } } async save() { await this.ensureKey(); const plaintext = JSON.stringify({ accounts: this.data.accounts, selectedAccount: this.data.selectedAccount }); const encrypted = encryptWithKey(plaintext, this.key); const hmac = computeHMAC(encrypted, this.key); const final = { encrypted, hmac }; fs.writeFileSync(this.filePath, JSON.stringify(final, null, 2)); } reset() { if (fs.existsSync(this.filePath)) { fs.unlinkSync(this.filePath); } } async addAccount(account) { await this.load(); this.data.accounts[account.id] = account; await this.save(); } async deleteAccount(id) { await this.load(); if (this.data.accounts[id]) { delete this.data.accounts[id]; if (this.data.selectedAccount === id) this.data.selectedAccount = undefined; await this.save(); return true; } return false; } async hasAccount(cred, provider) { await this.load(); return Object.values(this.data.accounts).some(acc => acc.auth.name === provider.toLowerCase() && acc.credentials === cred); } async getAccount(id) { await this.load(); const acc = this.data.accounts[id]; if (!acc) { handler_1.logger.error(`Account "${id}" does not exist.`); return null; } return acc; } async selectAccount(id) { const acc = await this.getAccount(id); if (!acc) return null; this.data.selectedAccount = acc.id; await this.save(); return acc; } async listAccounts() { await this.load(); return Object.values(this.data.accounts); } async getSelectedAccount() { await this.load(); return this.getAccount(this.data.selectedAccount || 'no-id'); } async chooseAccount() { const accounts = await this.listAccounts(); if (accounts.length === 0) { console.log(chalk_1.default.red("❌ No accounts found.")); return null; } const allProviders = await (0, _1.getAuthProviders)(); const providerMeta = new Map(); for (const [key, ctor] of allProviders.entries()) { try { const meta = new ctor('', '').metadata; providerMeta.set(key, meta); } catch { providerMeta.set(key, { name: key, base: "Other" }); } } const groupedByBase = {}; for (const acc of accounts) { const authKey = acc.auth; const meta = providerMeta.get(authKey.name); const base = meta?.base ?? "Other"; if (!groupedByBase[base]) groupedByBase[base] = []; groupedByBase[base].push(acc); } const sortedBases = Object.keys(groupedByBase).sort(); const choices = []; for (const base of sortedBases) { choices.push(new inquirer_1.default.Separator(chalk_1.default.bold.cyan(`🔑 ${base.toUpperCase()}`))); const providerAccounts = groupedByBase[base] .sort((a, b) => (a.name || 'other').localeCompare(b.name || 'other')); for (const acc of providerAccounts) { const line = `${chalk_1.default.hex('#4ade80')(acc.name)} ${chalk_1.default.gray(`(${acc.uuid?.slice(0, 8)}...)`)} - ${chalk_1.default.hex('#facc15')(acc.auth.name || 'No info')}`; choices.push({ name: line, value: acc.id }); } } const { selectedId } = await inquirer_1.default.prompt([ { type: "list", name: "selectedId", message: chalk_1.default.hex("#60a5fa")("🎭 Choose an account to use:"), choices, loop: false } ]); const selectedAccount = await this.selectAccount(selectedId); if (selectedAccount) { console.log(chalk_1.default.green(`✅ Selected account: ${selectedAccount.name}`)); } return selectedAccount; } } exports.LauncherAccountManager = LauncherAccountManager; exports.default = LauncherAccountManager; //# sourceMappingURL=account.js.map