您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Generate account files for Cemu in order to access Pretendo
// ==UserScript== // @name pretendo-cemu-files-gen // @namespace https://github.com/CorySanin // @version 1.1 // @description Generate account files for Cemu in order to access Pretendo // @author Cory Sanin // @match *://pretendo.network/account // @require https://raw.githubusercontent.com/pwasystem/zip/d0bbf615/zip.js#sha256=365e2abbc948a2ce0d876e227db05efc294f7e25e37cf43786c5689d49c0cdd8 // @license Unlicense // @icon https://pretendo.network/assets/images/icons/favicon-32x32.png // ==/UserScript== (function () { 'use strict'; // #region DOM elements function createButton(onClick) { let inner = document.createTextNode('Download account files'); let outer = document.createElement('p'); outer.classList.add('caption'); outer.appendChild(inner); inner = outer; outer = document.createElement('a'); outer.href = '#'; outer.id = 'download-cemu-files'; outer.classList.add('button', 'secondary'); outer.appendChild(inner); document.querySelector('.account-sidebar .buttons').appendChild(outer); outer.addEventListener('click', onClick); return outer; } function createModal(onClick) { const parent = document.querySelector('div.main-body'); let inner = document.createTextNode('Download account files'); let outer = document.createElement('h1'); let container = document.createElement('div'); container.classList.add('modal'); outer.appendChild(inner); outer.classList.add('title'); container.appendChild(outer); inner = document.createTextNode('Enter your Pretendo password and click download to save your account files for Cemu. Note that this password is only sent to Pretendo. If unsure, please close this prompt.'); outer = document.createElement('p'); outer.appendChild(inner); outer.classList.add('modal-caption'); container.appendChild(outer); inner = document.createElement('input'); inner.name = 'password'; inner.id = 'password'; inner.type = 'password'; inner.placeholder = 'password' container.appendChild(inner); outer = document.createElement('div'); outer.classList.add('modal-button-wrapper'); container.appendChild(outer); inner = document.createElement('button'); inner.appendChild(document.createTextNode('Confirm')); inner.classList.add('button', 'primary', 'confirm'); inner.id = 'onlineFilesConfirmButton'; inner.addEventListener('click', onClick); outer.appendChild(inner); inner = document.createElement('button'); inner.appendChild(document.createTextNode('Cancel')); inner.classList.add('button', 'cancel'); inner.id = 'onlineFilesCloseButton'; outer.appendChild(inner); outer = document.createElement('div'); outer.appendChild(container); outer.id = 'onlinefiles'; outer.classList.add('modal-wrapper', 'hidden'); inner.addEventListener('click', (ev) => { ev.preventDefault(); outer.classList.add('hidden'); }); parent.appendChild(outer); return outer; } // #endregion // #region helper functions // Not gonna lie, I used ChatGPT to translate the NodeJS hash algorithm to browser JS async function nintendoPasswordHash(password, pid) { // Create a buffer of 4 bytes for the pid in little-endian format const pidBuffer = new Uint8Array(4); new DataView(pidBuffer.buffer).setUint32(0, pid, true); // true for little-endian // Convert the password to a Uint8Array const passwordBuffer = new TextEncoder().encode(password); // Create the constant buffer const constantBuffer = new Uint8Array([0x02, 0x65, 0x43, 0x46]); // Concatenate the buffers const unpacked = new Uint8Array(pidBuffer.length + constantBuffer.length + passwordBuffer.length); unpacked.set(pidBuffer, 0); unpacked.set(constantBuffer, pidBuffer.length); unpacked.set(passwordBuffer, pidBuffer.length + constantBuffer.length); // Hash the unpacked data using SHA-256 const hashBuffer = await crypto.subtle.digest('SHA-256', unpacked); // Convert the hash buffer to a hexadecimal string const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashed = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); return hashed; } function base64Hex(str) { const binaryString = atob(str); let hexString = ''; for (let i = 0; i < binaryString.length; i++) { const hex = binaryString.charCodeAt(i).toString(16); hexString += (hex.length === 1 ? '0' + hex : hex); } return hexString; } function nameToHex(nameStr) { // Create a buffer with a fixed size of 0x16 const buffer = new Uint8Array(0x16); let arr = [] for (let i = 0; i < nameStr.length; i++){ let ch = nameStr.charCodeAt(i); arr.push((ch & 0xFF00) >>> 8); arr.push(ch & 0xFF); } // Copy the swapped data into the buffer buffer.set(arr.slice(0, 0x16)); // Convert the buffer to a hexadecimal string const hexString = Array.from(buffer).map(byte => byte.toString(16).padStart(2, '0')).join(''); return hexString; } // https://stackoverflow.com/a/8809472/11210376 function generateUUID() { let s; var d = new Date().getTime(); //Timestamp var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now()*1000)) || 0;//Time in microseconds since page-load or 0 if unsupported s = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16;//random number between 0 and 16 if (d > 0) {//Use timestamp until depleted r = (d + r) % 16 | 0; d = Math.floor(d / 16); } else {//Use microseconds since page-load if supported r = (d2 + r) % 16 | 0; d2 = Math.floor(d2 / 16); } return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); return s; } // #endregion // #region plugin logic // This is the section to survey 👀 const modal = createModal(async (ev) => { ev.preventDefault(); modal.classList.add('hidden'); const passwordTxt = modal.querySelector('#password'); const password = passwordTxt.value; passwordTxt.value = ''; const tokenType = document.cookie.split('; ').find(row => row.startsWith('token_type=')).split('=')[1]; const accessToken = document.cookie.split('; ').find(row => row.startsWith('access_token=')).split('=')[1]; try { const resp = await fetch('https://api.pretendo.cc/v1/user', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': `${tokenType} ${decodeURIComponent(accessToken)}` }, body: JSON.stringify({ environment: 'prod' // might not work for other environments }) }); const json = await resp.json(); if (!json.error) { const account = json; const hashedPassword = await nintendoPasswordHash(password, account.pid); let accountDat = 'AccountInstance_00000000\n'; accountDat += 'PersistentId=80000001\n'; accountDat += 'TransferableIdBase=0\n'; accountDat += `Uuid=${generateUUID().replace(/-/g, '')}\n`; accountDat += `MiiData=${base64Hex(account.mii.data)}\n`; accountDat += `MiiName=${nameToHex(account.mii.name)}\n`; accountDat += `AccountId=${account.username}\n`; accountDat += 'BirthYear=0\n'; accountDat += 'BirthMonth=0\n'; accountDat += 'BirthDay=0\n'; accountDat += 'Gender=0\n'; accountDat += `EmailAddress=${account.email.address}\n`; accountDat += 'Country=0\n'; accountDat += 'SimpleAddressId=0\n'; accountDat += `PrincipalId=${account.pid.toString(16)}\n`; accountDat += 'IsPasswordCacheEnabled=1\n'; accountDat += `AccountPasswordCache=${hashedPassword}`; const z = new Zip('mlc01'); z.str2zip('account.dat', accountDat, 'usr/save/system/act/80000001/'); z.makeZip(); } else { console.log(json.error); alert('Failed to get account data'); } } catch (error) { console.log(error); alert('Failed to get account data'); } }); createButton((ev) => { ev.preventDefault(); modal.classList.remove('hidden'); modal.querySelector('#password').focus(); }); // #endregion })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址