This commit is contained in:
Ash Monty
2021-12-13 16:24:41 +01:00
13 changed files with 2841 additions and 45 deletions

View File

@@ -1,9 +1,12 @@
const { Router } = require('express');
const crypto = require('crypto');
const DiscordOauth2 = require('discord-oauth2');
const { v4: uuidv4 } = require('uuid');
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 +18,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');
}
@@ -29,8 +32,8 @@ router.get('/', async (request, response) => {
};
// Reset message cookies
response.clearCookie('linked');
response.clearCookie('error');
response.clearCookie('linked', { domain: '.pretendo.network' });
response.clearCookie('error', { domain: '.pretendo.network' });
// Attempt to get user data
let apiResponse = await util.apiGetRequest('/v1/user', {
@@ -183,7 +186,7 @@ router.get('/login', async (request, response) => {
error: request.cookies.error
};
response.clearCookie('error');
response.clearCookie('error', { domain: '.pretendo.network' });
response.render('account_login', renderData);
});
@@ -191,7 +194,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 +211,71 @@ 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');
});
router.get('/register', async (request, response) => {
const renderData = {
layout: 'main',
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
error: request.cookies.error,
email: request.cookies.email,
username: request.cookies.username,
mii_name: request.cookies.mii_name,
};
response.clearCookie('error', { domain: '.pretendo.network' });
response.clearCookie('email', { domain: '.pretendo.network' });
response.clearCookie('username', { domain: '.pretendo.network' });
response.clearCookie('mii_name', { domain: '.pretendo.network' });
response.render('account_register', renderData);
});
router.post('/register', async (request, response) => {
const { email, username, mii_name, password, password_confirm } = request.body;
response.cookie('email', email, { domain: '.pretendo.network' });
response.cookie('username', username, { domain: '.pretendo.network' });
response.cookie('mii_name', mii_name, { domain: '.pretendo.network' });
const apiResponse = await util.apiPostGetRequest('/v1/register', {}, {
email, username, mii_name, password, password_confirm
});
if (apiResponse.statusCode !== 200) {
response.cookie('error', apiResponse.body.error, { domain: '.pretendo.network' });
return response.redirect('/account/register');
}
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.clearCookie('error', { domain: '.pretendo.network' });
response.clearCookie('email', { domain: '.pretendo.network' });
response.clearCookie('username', { domain: '.pretendo.network' });
response.clearCookie('mii_name', { domain: '.pretendo.network' });
response.redirect('/account');
});
@@ -255,9 +323,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}`
@@ -281,4 +349,91 @@ 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()]);
const miiNameBuffer = Buffer.alloc(0x16);
const miiName = Buffer.from(account.mii.name, 'utf16le').swap16();
miiName.copy(miiNameBuffer);
let accountDat = 'AccountInstance_00000000\n';
accountDat += 'PersistentId=80000001\n';
accountDat += 'TransferableIdBase=0\n';
accountDat += `Uuid=${uuidv4().replace(/-/g, '')}\n`;
accountDat += `MiiData=${Buffer.from(account.mii.data, 'base64').toString('hex')}\n`;
accountDat += `MiiName=${miiNameBuffer.toString('hex')}\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=${decryptedPasswordHash.toString('hex')}`;
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.status(200);
response.set('Content-Disposition', 'attachment; filename="Online Files.zip');
response.set('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
};