Added Cemu files download

This commit is contained in:
Jonathan Barrow 2021-12-12 20:48:20 -05:00
parent 3da33fa3d2
commit f8f651d641
8 changed files with 145 additions and 9 deletions

View File

@ -16,5 +16,6 @@
"tester_roles": [
"role id"
]
}
},
"aes_key": "hex key here"
}

14
package-lock.json generated
View File

@ -8,6 +8,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"adm-zip": "^0.5.9",
"colors": "^1.4.0",
"cookie-parser": "^1.4.5",
"discord-oauth2": "github:ryanblenis/discord-oauth2",
@ -306,6 +307,14 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
"engines": {
"node": ">=6.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -2668,6 +2677,11 @@
"dev": true,
"requires": {}
},
"adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg=="
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",

View File

@ -17,6 +17,7 @@
},
"homepage": "https://github.com/PretendoNetwork/website#readme",
"dependencies": {
"adm-zip": "^0.5.9",
"colors": "^1.4.0",
"cookie-parser": "^1.4.5",
"discord-oauth2": "github:ryanblenis/discord-oauth2",

View File

@ -25,6 +25,17 @@
border-radius: 100%;
background: var(--btn-secondary);
}
.account-sidebar .user #download-cemu-files {
width: 100%;
padding: 4px 16px;
cursor: pointer;
background: #383f6b;
font-size: 0.9rem;
margin: 8px 0 0;
}
.account-sidebar .user .cemu-warning {
font-size: 0.7rem;
}
/* Settings */
.settings-wrapper {

View File

@ -1,5 +1,5 @@
document.getElementById('remove-discord-connection').addEventListener('click', () => {
document.getElementById('remove-discord-connection')?.addEventListener('click', () => {
// TODO: Refresh access token if expired (move this to the backend maybe?)
const tokenType = document.cookie.split('; ').find(row => row.startsWith('token_type=')).split('=')[1];

View File

@ -1,9 +1,11 @@
const { Router } = require('express');
const crypto = require('crypto');
const DiscordOauth2 = require('discord-oauth2');
const AdmZip = require('adm-zip');
const util = require('../util');
const config = require('../../config.json');
const router = new Router();
const aesKey = Buffer.from(config.aes_key, 'hex');
// Create OAuth client
const discordOAuth = new DiscordOauth2({
@ -15,7 +17,7 @@ const discordOAuth = new DiscordOauth2({
router.get('/', async (request, response) => {
// Verify the user is logged in
if (!request.cookies.access_token || !request.cookies.refresh_token) {
if (!request.cookies.access_token || !request.cookies.refresh_token || !request.cookies.ph) {
return response.redirect('/account/login');
}
@ -191,7 +193,7 @@ router.get('/login', async (request, response) => {
router.post('/login', async (request, response) => {
const { username, password } = request.body;
const apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
let apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
username,
password,
grant_type: 'password'
@ -208,6 +210,22 @@ router.post('/login', async (request, response) => {
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
apiResponse = await util.apiGetRequest('/v1/user', {
'Authorization': `${tokens.token_type} ${tokens.access_token}`
});
const account = apiResponse.body;
const hashedPassword = util.nintendoPasswordHash(password, account.pid);
const hashedPasswordBuffer = Buffer.from(hashedPassword, 'hex');
const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, Buffer.alloc(16));
let encryptedBody = cipher.update(hashedPasswordBuffer);
encryptedBody = Buffer.concat([encryptedBody, cipher.final()]);
response.cookie('ph', encryptedBody.toString('hex'), { domain: '.pretendo.network' });
response.redirect('/account');
});
@ -304,9 +322,9 @@ router.get('/connect/discord', async (request, response) => {
const tokens = apiResponse.body;
response.cookie('refresh_token', tokens.refresh_token, { domain : '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', {
'Authorization': `${tokens.token_type} ${tokens.access_token}`
@ -330,4 +348,76 @@ router.get('/connect/discord', async (request, response) => {
response.cookie('linked', 'Discord', { domain: '.pretendo.network' }).redirect('/account');
});
router.get('/online-files', async (request, response) => {
// Verify the user is logged in
if (!request.cookies.access_token || !request.cookies.refresh_token|| !request.cookies.ph) {
return response.redirect('/account/login');
}
// Attempt to get user data
let apiResponse = await util.apiGetRequest('/v1/user', {
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
});
if (apiResponse.statusCode !== 200) {
// Assume expired, refresh and retry request
apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
refresh_token: request.cookies.refresh_token,
grant_type: 'refresh_token'
});
if (apiResponse.statusCode !== 200) {
// TODO: Error message
return response.status(apiResponse.statusCode).json({
error: 'Bad'
});
}
const tokens = apiResponse.body;
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
apiResponse = await util.apiGetRequest('/v1/user', {
'Authorization': `${tokens.token_type} ${tokens.access_token}`
});
}
// If still failed, something went horribly wrong
if (apiResponse.statusCode !== 200) {
// TODO: Error message
return response.status(apiResponse.statusCode).json({
error: 'Bad'
});
}
const account = apiResponse.body;
const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, Buffer.alloc(16));
let decryptedPasswordHash = decipher.update(Buffer.from(request.cookies.ph, 'hex'));
decryptedPasswordHash = Buffer.concat([decryptedPasswordHash, decipher.final()]);
let accountDat = 'AccountInstance_00000000\n';
accountDat += `AccountPasswordCache=${decryptedPasswordHash.toString('hex')}\n`;
accountDat += 'IsPasswordCacheEnabled=1\n';
accountDat += `AccountId=${account.username}\n`;
accountDat += 'PersistentId=80000001';
const onlineFiles = new AdmZip();
onlineFiles.addFile('mlc01/usr/save/system/act/80000001/account.dat', Buffer.from(accountDat)); // Minimal account.dat
onlineFiles.addFile('otp.bin', Buffer.alloc(0x400)); // nulled OTP
onlineFiles.addFile('seeprom.bin', Buffer.alloc(0x200)); // nulled SEEPROM
response.writeHead(200, {
'Content-Disposition': 'attachment; filename="Online Files.zip"',
'Content-Type': 'application/zip',
});
response.end(onlineFiles.toBuffer());
});
module.exports = router;

View File

@ -1,5 +1,6 @@
const fs = require('fs-extra');
const got = require('got');
const crypto = require('crypto');
const logger = require('./logger');
function fullUrl(request) {
@ -52,10 +53,26 @@ function apiDeleteGetRequest(path, headers, json) {
});
}
function nintendoPasswordHash(password, pid) {
const pidBuffer = Buffer.alloc(4);
pidBuffer.writeUInt32LE(pid);
const unpacked = Buffer.concat([
pidBuffer,
Buffer.from('\x02\x65\x43\x46'),
Buffer.from(password)
]);
const hashed = crypto.createHash('sha256').update(unpacked).digest().toString('hex');
return hashed;
}
module.exports = {
fullUrl,
getLocale,
apiGetRequest,
apiPostGetRequest,
apiDeleteGetRequest
apiDeleteGetRequest,
nintendoPasswordHash
};

View File

@ -9,7 +9,9 @@
<div class="user">
<img src="{{account.mii.image_url}}" class="mii" />
<p class="miiname">{{account.mii.name}}</p>
<p class="username">PNID: {{account.username}}</p>
<p class="username" value="{{account.username}}">PNID: {{account.username}}</p>
<a class="button secondary" id="download-cemu-files" href="/account/online-files" download>Download account files</a>
<p class="cemu-warning">(will not work with Nintendo Network)</p>
</div>
</div>