Large restructure

Restructured the codebase a bit. No big changes really. Need to handle registration sessions better
This commit is contained in:
Jonathan Barrow 2021-03-28 20:31:57 -04:00
parent f447d44921
commit 18b96b06c5
32 changed files with 2204 additions and 2100 deletions

128
.gitignore vendored
View file

@ -1,65 +1,65 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# custom
sign.js
t.js
config.json
servers.json
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# custom
sign.js
t.js
config.json
servers.json
certs

View file

@ -1,79 +1,79 @@
const prompt = require('prompt');
const crypto = require('crypto');
const util = require('./src/util');
const database = require('./src/database');
const { PNID } = require('./src/models/pnid');
prompt.message = '';
const properties = [
'username',
'email',
{
name: 'password',
hidden: true
}
];
prompt.get(properties, function (error, { username, email, password }) {
const date = new Date().toISOString();
const miiHash = crypto.createHash('md5').update(date).digest('hex');
const document = {
pid: 1,
creation_date: date.split('.')[0],
updated: date,
username: username,
password: password,
birthdate: '1990-01-01',
gender: 'M',
country: 'US',
language: 'en',
email: {
address: email,
primary: true,
parent: true,
reachable: true,
validated: true,
id: util.generateRandomInt(10)
},
region: 0x310B0000,
timezone: {
name: 'America/New_York',
offset: -14400
},
mii: {
name: 'UserMii',
primary: true,
data: 'AwAAQIhluwTgxEAA2NlGWQOzuI0n2QAAAEBsAG8AZwBpAG4AdABlAHMAdAAAAEBAAAAhAQJoRBgmNEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMw7', // hardcoded for now. currently testing
id: util.generateRandomInt(10),
hash: miiHash,
image_url: 'https://mii-secure.account.nintendo.net/2rtgf01lztoqo_standard.tga',
image_id: util.generateRandomInt(10)
},
flags: {
active: true,
marketing: false,
off_device: true
},
validation: {
// These values are temp and will be overwritten before the document saves
// These values are only being defined to get around the `E11000 duplicate key error collection` error
email_code: Date.now(),
email_token: Date.now().toString()
}
};
const newUser = new PNID(document);
newUser.save(async (error, newUser) => {
if (error) {
throw error;
}
console.log(newUser);
console.log('New user created');
});
});
const prompt = require('prompt');
const crypto = require('crypto');
const util = require('./src/util');
const database = require('./src/database');
const { PNID } = require('./src/models/pnid');
prompt.message = '';
const properties = [
'username',
'email',
{
name: 'password',
hidden: true
}
];
prompt.get(properties, function (error, { username, email, password }) {
const date = new Date().toISOString();
const miiHash = crypto.createHash('md5').update(date).digest('hex');
const document = {
pid: 1,
creation_date: date.split('.')[0],
updated: date,
username: username,
password: password,
birthdate: '1990-01-01',
gender: 'M',
country: 'US',
language: 'en',
email: {
address: email,
primary: true,
parent: true,
reachable: true,
validated: true,
id: util.generateRandomInt(10)
},
region: 0x310B0000,
timezone: {
name: 'America/New_York',
offset: -14400
},
mii: {
name: 'bella',
primary: true,
data: 'AwAAQOlVognnx0GC2X0LLQOzuI0n2QAAAUBiAGUAbABsAGEAAABFAAAAAAAAAEBAEgCBAQRoQxggNEYUgRIXaA0AACkDUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP6G', // hardcoded for now. currently testing
id: 1330180812,
hash: '1o54mkuuxfg8t',
image_url: 'https://mii-secure.account.nintendo.net/1o54mkuuxfg8t_standard.tga',
image_id: 1330180887
},
flags: {
active: true,
marketing: false,
off_device: true
},
validation: {
// These values are temp and will be overwritten before the document saves
// These values are only being defined to get around the `E11000 duplicate key error collection` error
email_code: Date.now(),
email_token: Date.now().toString()
}
};
const newUser = new PNID(document);
newUser.save(async (error, newUser) => {
if (error) {
throw error;
}
console.log(newUser);
console.log('New user created');
});
});
database.connect().then(prompt.start);

View file

@ -1,88 +1,88 @@
const NodeRSA = require('node-rsa');
const crypto = require('crypto');
const fs = require('fs-extra');
require('colors');
const args = process.argv.slice(2);
if (args.length < 1) {
usage();
return;
}
const [type, name] = args;
if (!['nex', 'service', 'access'].includes(type)) {
usage();
return;
}
if (type !== 'access' && (!name || name.trim() === '')) {
usage();
return;
}
let path;
if (type === 'access') {
path = `${__dirname}/certs/${type}`;
} else {
path = `${__dirname}/certs/${type}/${name}`;
}
// Ensure the output directories exist
console.log('Creating output directories'.brightGreen);
fs.ensureDirSync(path);
// Generate new AES key
console.log('Generating AES key'.brightGreen);
const aesKey = crypto.randomBytes(16);
// Saving AES key
fs.writeFileSync(`${path}/aes.key`, aesKey.toString('hex'));
console.log(`Saved AES key to file ${path}/aes.key`.brightBlue);
const key = new NodeRSA({ b: 1024}, null, {
environment: 'browser',
encryptionScheme: {
'hash': 'sha256',
}
});
// Generate new key pair
console.log('Generating RSA key pair'.brightGreen, '(this may take a while)'.yellow.bold);
key.generateKeyPair(1024);
// Export the keys
console.log('Exporting public key'.brightGreen);
const publickKey = key.exportKey('public');
// Saving public key
fs.writeFileSync(`${path}/public.pem`, publickKey);
console.log(`Saved public key to file ${path}/public.pem`.brightBlue);
console.log('Exporting private key'.brightGreen);
const privatekKey = key.exportKey('private');
// Saving private key
fs.writeFileSync(`${path}/private.pem`, privatekKey);
console.log(`Saved public key to file ${path}/private.pem`.brightBlue);
// Create HMAC secret key
console.log('Generating HMAC secret'.brightGreen);
const secret = crypto.randomBytes(16);
fs.writeFileSync(`${path}/secret.key`, secret.toString('hex'));
console.log(`Saved HMAC secret to file ${path}/secret.key`.brightBlue);
// Display usage information
function usage() {
console.log('Usage: node generate-keys.js type [name]');
console.log('Types:');
console.log(' - nex');
console.log(' - service');
console.log(' - access');
console.log('Name: service or nex server name. Not used in access type');
const NodeRSA = require('node-rsa');
const crypto = require('crypto');
const fs = require('fs-extra');
require('colors');
const args = process.argv.slice(2);
if (args.length < 1) {
usage();
return;
}
const [type, name] = args;
if (!['nex', 'service', 'access'].includes(type)) {
usage();
return;
}
if (type !== 'access' && (!name || name.trim() === '')) {
usage();
return;
}
let path;
if (type === 'access') {
path = `${__dirname}/certs/${type}`;
} else {
path = `${__dirname}/certs/${type}/${name}`;
}
// Ensure the output directories exist
console.log('Creating output directories'.brightGreen);
fs.ensureDirSync(path);
// Generate new AES key
console.log('Generating AES key'.brightGreen);
const aesKey = crypto.randomBytes(16);
// Saving AES key
fs.writeFileSync(`${path}/aes.key`, aesKey.toString('hex'));
console.log(`Saved AES key to file ${path}/aes.key`.brightBlue);
const key = new NodeRSA({ b: 1024}, null, {
environment: 'browser',
encryptionScheme: {
'hash': 'sha256',
}
});
// Generate new key pair
console.log('Generating RSA key pair'.brightGreen, '(this may take a while)'.yellow.bold);
key.generateKeyPair(1024);
// Export the keys
console.log('Exporting public key'.brightGreen);
const publickKey = key.exportKey('public');
// Saving public key
fs.writeFileSync(`${path}/public.pem`, publickKey);
console.log(`Saved public key to file ${path}/public.pem`.brightBlue);
console.log('Exporting private key'.brightGreen);
const privatekKey = key.exportKey('private');
// Saving private key
fs.writeFileSync(`${path}/private.pem`, privatekKey);
console.log(`Saved public key to file ${path}/private.pem`.brightBlue);
// Create HMAC secret key
console.log('Generating HMAC secret'.brightGreen);
const secret = crypto.randomBytes(16);
fs.writeFileSync(`${path}/secret.key`, secret.toString('hex'));
console.log(`Saved HMAC secret to file ${path}/secret.key`.brightBlue);
// Display usage information
function usage() {
console.log('Usage: node generate-keys.js type [name]');
console.log('Types:');
console.log(' - nex');
console.log(' - service');
console.log(' - access');
console.log('Name: service or nex server name. Not used in access type');
}

102
logger.js
View file

@ -1,52 +1,52 @@
const fs = require('fs-extra');
require('colors');
const root = __dirname;
fs.ensureDirSync(`${root}/logs`);
const streams = {
latest: fs.createWriteStream(`${root}/logs/latest.log`),
success: fs.createWriteStream(`${root}/logs/success.log`),
error: fs.createWriteStream(`${root}/logs/error.log`),
warn: fs.createWriteStream(`${root}/logs/warn.log`),
info: fs.createWriteStream(`${root}/logs/info.log`)
};
function success(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`;
streams.success.write(`${input}\n`);
console.log(`${input}`.green.bold);
}
function error(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`;
streams.error.write(`${input}\n`);
console.log(`${input}`.red.bold);
}
function warn(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`;
streams.warn.write(`${input}\n`);
console.log(`${input}`.yellow.bold);
}
function info(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`;
streams.info.write(`${input}\n`);
console.log(`${input}`.cyan.bold);
}
module.exports = {
success,
error,
warn,
info
const fs = require('fs-extra');
require('colors');
const root = __dirname;
fs.ensureDirSync(`${root}/logs`);
const streams = {
latest: fs.createWriteStream(`${root}/logs/latest.log`),
success: fs.createWriteStream(`${root}/logs/success.log`),
error: fs.createWriteStream(`${root}/logs/error.log`),
warn: fs.createWriteStream(`${root}/logs/warn.log`),
info: fs.createWriteStream(`${root}/logs/info.log`)
};
function success(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`;
streams.success.write(`${input}\n`);
console.log(`${input}`.green.bold);
}
function error(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`;
streams.error.write(`${input}\n`);
console.log(`${input}`.red.bold);
}
function warn(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`;
streams.warn.write(`${input}\n`);
console.log(`${input}`.yellow.bold);
}
function info(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`;
streams.info.write(`${input}\n`);
console.log(`${input}`.cyan.bold);
}
module.exports = {
success,
error,
warn,
info
};

150
package-lock.json generated
View file

@ -154,12 +154,12 @@
}
},
"bcrypt": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.7.tgz",
"integrity": "sha512-K5UglF9VQvBMHl/1elNyyFvAfOY9Bj+rpKrCSR9sFwcW8FywAYJSRwTURNej5TaAK2TEJkcJ6r6lh1YPmspx5Q==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz",
"integrity": "sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==",
"requires": {
"nan": "2.14.0",
"node-pre-gyp": "0.13.0"
"node-addon-api": "^3.0.0",
"node-pre-gyp": "0.15.0"
}
},
"bluebird": {
@ -204,9 +204,9 @@
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"chownr": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz",
"integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw=="
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"code-point-at": {
"version": "1.1.0",
@ -356,33 +356,6 @@
"vary": "~1.1.2"
}
},
"express-session": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.0.tgz",
"integrity": "sha512-t4oX2z7uoSqATbMfsxWMbNjAL0T5zpvcJCk3Z9wnPPN7ibddhnmDZXHfEcoBMG2ojKXZoCyPMc5FbtK+G7SoDg==",
"requires": {
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.0",
"uid-safe": "~2.1.5"
},
"dependencies": {
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"safe-buffer": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
}
}
},
"express-subdomain": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/express-subdomain/-/express-subdomain-1.0.5.tgz",
@ -526,9 +499,9 @@
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"ipaddr.js": {
"version": "1.9.0",
@ -624,9 +597,9 @@
}
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"minipass": {
"version": "2.9.0",
@ -646,11 +619,11 @@
}
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "0.0.8"
"minimist": "^1.2.5"
}
},
"moment": {
@ -765,20 +738,15 @@
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
},
"nan": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
},
"ncp": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz",
"integrity": "sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY="
},
"needle": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz",
"integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==",
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz",
"integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==",
"requires": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
@ -786,17 +754,17 @@
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
@ -805,21 +773,26 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"node-addon-api": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw=="
},
"node-pre-gyp": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz",
"integrity": "sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ==",
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz",
"integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==",
"requires": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.1",
"needle": "^2.2.1",
"mkdirp": "^0.5.3",
"needle": "^2.5.0",
"nopt": "^4.0.1",
"npm-packlist": "^1.1.6",
"npmlog": "^4.0.2",
"rc": "^1.2.7",
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4"
"tar": "^4.4.2"
}
},
"node-rsa": {
@ -836,9 +809,9 @@
"integrity": "sha512-g0n4nH1ONGvqYo1v72uSWvF/MRNnnq1LzmSzXb/6EPF3LFb51akOhgG3K2+aETAsJx90/Q5eFNTntu4vBCwyQQ=="
},
"nopt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
@ -858,12 +831,13 @@
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
},
"npm-packlist": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.7.tgz",
"integrity": "sha512-vAj7dIkp5NhieaGZxBJB8fF4R0078rqsmhJcAfXZ6O7JJhjhPK96n5Ry1oZcfLXgfun0GWTZPOxaEyqv8GBykQ==",
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
"requires": {
"ignore-walk": "^3.0.1",
"npm-bundled": "^1.0.1"
"npm-bundled": "^1.0.1",
"npm-normalize-package-bin": "^1.0.1"
}
},
"npmlog": {
@ -984,11 +958,6 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -1014,13 +983,6 @@
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
}
}
},
"read": {
@ -1032,9 +994,9 @@
}
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@ -1160,9 +1122,9 @@
"integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g=="
},
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
},
"sliced": {
"version": "1.0.1",
@ -1247,14 +1209,6 @@
"mime-types": "~2.1.24"
}
},
"uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"requires": {
"random-bytes": "~1.0.0"
}
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",

View file

@ -2,7 +2,7 @@
"name": "account",
"version": "1.0.0",
"description": "",
"main": "./src/server",
"main": "./src/server.js",
"scripts": {
"lint": "./node_modules/.bin/eslint .",
"start": "node .",
@ -20,10 +20,9 @@
},
"homepage": "https://github.com/PretendoNetwork/account#readme",
"dependencies": {
"bcrypt": "^3.0.7",
"bcrypt": "^5.0.0",
"colors": "^1.4.0",
"express": "^4.17.1",
"express-session": "^1.17.0",
"express-subdomain": "^1.0.5",
"fs-extra": "^8.1.0",
"moment": "^2.24.0",

View file

@ -1,170 +1,170 @@
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const util = require('./util');
const { PNID } = require('./models/pnid');
const { mongoose: mongooseConfig } = require('./config.json');
const { uri, database, options } = mongooseConfig;
let connection;
async function connect() {
await mongoose.connect(`${uri}/${database}`, options);
connection = mongoose.connection;
connection.on('error', console.error.bind(console, 'connection error:'));
}
function verifyConnected() {
if (!connection) {
throw new Error('Cannot make database requets without being connected');
}
}
async function getUserByUsername(username) {
verifyConnected();
if (typeof username !== 'string') {
return null;
}
const user = await PNID.findOne({
usernameLower: username.toLowerCase()
});
return user;
}
async function getUserByPID(pid) {
verifyConnected();
const user = await PNID.findOne({
pid
});
return user;
}
async function doesUserExist(username) {
verifyConnected();
return !!await this.getUserByUsername(username);
}
async function getUserBasic(token) {
verifyConnected();
const [username, password] = Buffer.from(token, 'base64').toString().split(' ');
const user = await this.getUserByUsername(username);
if (!user) {
return null;
}
const hashedPassword = util.nintendoPasswordHash(password, user.pid);
if (!bcrypt.compareSync(hashedPassword, user.password)) {
return null;
}
return user;
}
async function getUserBearer(token) {
verifyConnected();
const decryptedToken = util.decryptToken(Buffer.from(token, 'base64'));
const unpackedToken = util.unpackToken(decryptedToken);
const user = await getUserByPID(unpackedToken.pid);
if (user) {
const expireTime = Math.floor((Number(unpackedToken.date) / 1000) + 3600);
if (Math.floor(Date.now()/1000) > expireTime) {
return null;
}
}
return user;
}
async function getUserProfileJSONByPID(pid) {
verifyConnected();
const user = await this.getUserByPID(pid);
const device = user.get('devices')[0]; // Just grab the first device
let device_attributes;
if (device) {
device_attributes = device.get('device_attributes').map(({name, value, created_date}) => {
const deviceAttributeDocument = {
name,
value
};
if (created_date) {
deviceAttributeDocument.created_date = created_date;
}
return {
device_attribute: deviceAttributeDocument
};
});
}
return {
//accounts: {}, We need to figure this out, no idea what these values mean or what they do
active_flag: user.get('flags.active') ? 'Y' : 'N',
birth_date: user.get('birthdate'),
country: user.get('country'),
create_date: user.get('creation_date'),
device_attributes: device_attributes,
gender: user.get('gender'),
language: user.get('language'),
updated: user.get('updated'),
marketing_flag: user.get('flags.marketing') ? 'Y' : 'N',
off_device_flag: user.get('flags.off_device') ? 'Y' : 'N',
pid: user.get('pid'),
email: {
address: user.get('email.address'),
id: user.get('email.id'),
parent: user.get('email.parent') ? 'Y' : 'N',
primary: user.get('email.primary') ? 'Y' : 'N',
reachable: user.get('email.reachable') ? 'Y' : 'N',
type: 'DEFAULT',
updated_by: 'USER', // Can also be INTERNAL WS, don't know the difference
validated: user.get('email.validated') ? 'Y' : 'N',
//validated_date: user.get('email.validated_date') // not used atm
},
mii: {
status: 'COMPLETED',
data: user.get('mii.data'),
id: user.get('mii.id'),
mii_hash: user.get('mii.hash'),
mii_images: {
mii_image: {
cached_url: user.get('mii.image_url'),
id: user.get('mii.image_id'),
url: user.get('mii.image_url'),
type: 'standard'
}
},
name: user.get('mii.name'),
primary: user.get('mii.primary') ? 'Y' : 'N',
},
region: user.get('region'),
tz_name: user.get('timezone.name'),
user_id: user.get('username'),
utc_offset: user.get('timezone.offset')
};
}
module.exports = {
connect,
getUserByUsername,
getUserByPID,
doesUserExist,
getUserBasic,
getUserBearer,
getUserProfileJSONByPID,
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const util = require('./util');
const { PNID } = require('./models/pnid');
const { mongoose: mongooseConfig } = require('./config.json');
const { uri, database, options } = mongooseConfig;
let connection;
async function connect() {
await mongoose.connect(`${uri}/${database}`, options);
connection = mongoose.connection;
connection.on('error', console.error.bind(console, 'connection error:'));
}
function verifyConnected() {
if (!connection) {
throw new Error('Cannot make database requets without being connected');
}
}
async function getUserByUsername(username) {
verifyConnected();
if (typeof username !== 'string') {
return null;
}
const user = await PNID.findOne({
usernameLower: username.toLowerCase()
});
return user;
}
async function getUserByPID(pid) {
verifyConnected();
const user = await PNID.findOne({
pid
});
return user;
}
async function doesUserExist(username) {
verifyConnected();
return !!await this.getUserByUsername(username);
}
async function getUserBasic(token) {
verifyConnected();
const [username, password] = Buffer.from(token, 'base64').toString().split(' ');
const user = await this.getUserByUsername(username);
if (!user) {
return null;
}
const hashedPassword = util.nintendoPasswordHash(password, user.pid);
if (!bcrypt.compareSync(hashedPassword, user.password)) {
return null;
}
return user;
}
async function getUserBearer(token) {
verifyConnected();
const decryptedToken = util.decryptToken(Buffer.from(token, 'base64'));
const unpackedToken = util.unpackToken(decryptedToken);
const user = await getUserByPID(unpackedToken.pid);
if (user) {
const expireTime = Math.floor((Number(unpackedToken.date) / 1000) + 3600);
if (Math.floor(Date.now()/1000) > expireTime) {
return null;
}
}
return user;
}
async function getUserProfileJSONByPID(pid) {
verifyConnected();
const user = await this.getUserByPID(pid);
const device = user.get('devices')[0]; // Just grab the first device
let device_attributes;
if (device) {
device_attributes = device.get('device_attributes').map(({name, value, created_date}) => {
const deviceAttributeDocument = {
name,
value
};
if (created_date) {
deviceAttributeDocument.created_date = created_date;
}
return {
device_attribute: deviceAttributeDocument
};
});
}
return {
//accounts: {}, We need to figure this out, no idea what these values mean or what they do
active_flag: user.get('flags.active') ? 'Y' : 'N',
birth_date: user.get('birthdate'),
country: user.get('country'),
create_date: user.get('creation_date'),
device_attributes: device_attributes,
gender: user.get('gender'),
language: user.get('language'),
updated: user.get('updated'),
marketing_flag: user.get('flags.marketing') ? 'Y' : 'N',
off_device_flag: user.get('flags.off_device') ? 'Y' : 'N',
pid: user.get('pid'),
email: {
address: user.get('email.address'),
id: user.get('email.id'),
parent: user.get('email.parent') ? 'Y' : 'N',
primary: user.get('email.primary') ? 'Y' : 'N',
reachable: user.get('email.reachable') ? 'Y' : 'N',
type: 'DEFAULT',
updated_by: 'USER', // Can also be INTERNAL WS, don't know the difference
validated: user.get('email.validated') ? 'Y' : 'N',
//validated_date: user.get('email.validated_date') // not used atm
},
mii: {
status: 'COMPLETED',
data: user.get('mii.data'),
id: user.get('mii.id'),
mii_hash: user.get('mii.hash'),
mii_images: {
mii_image: {
cached_url: user.get('mii.image_url'),
id: user.get('mii.image_id'),
url: user.get('mii.image_url'),
type: 'standard'
}
},
name: user.get('mii.name'),
primary: user.get('mii.primary') ? 'Y' : 'N',
},
region: user.get('region'),
tz_name: user.get('timezone.name'),
user_id: user.get('username'),
utc_offset: user.get('timezone.offset')
};
}
module.exports = {
connect,
getUserByUsername,
getUserByPID,
doesUserExist,
getUserBasic,
getUserBearer,
getUserProfileJSONByPID,
};

View file

@ -1,19 +1,19 @@
{
"http": {
"port": 7070
},
"mongoose": {
"uri": "mongodb://localhost:27017",
"database": "database_name",
"options": {
"useNewUrlParser": true
}
},
"email": {
"service": "gmail",
"auth": {
"user": "email@address.com",
"pass": "password"
}
}
{
"http": {
"port": 7070
},
"mongoose": {
"uri": "mongodb://localhost:27017",
"database": "database_name",
"options": {
"useNewUrlParser": true
}
},
"email": {
"service": "gmail",
"auth": {
"user": "email@address.com",
"pass": "password"
}
}
}

View file

@ -1,24 +1,24 @@
[
{
"title_ids": [
"000500301001500A",
"000500301001510A",
"000500301001520A",
"0005001010001C00"
],
"server_id": "00003200",
"ip": "192.168.0.27",
"port": "60000",
"system": 1,
"crypto": {
"service": {
"cek": "",
"hmac_secret": ""
},
"nex": {
"cek": "",
"hmac_secret": ""
}
}
}
[
{
"title_ids": [
"000500301001500A",
"000500301001510A",
"000500301001520A",
"0005001010001C00"
],
"server_id": "00003200",
"ip": "192.168.0.27",
"port": "60000",
"system": 1,
"crypto": {
"service": {
"cek": "",
"hmac_secret": ""
},
"nex": {
"cek": "",
"hmac_secret": ""
}
}
}
]

View file

@ -1,33 +1,33 @@
const nodemailer = require('nodemailer');
const config = require('./config');
const transporter = nodemailer.createTransport(config.email);
/**
* Sends an email with the specified subject and message to an email address
* @param {String} email the destination email address
* @param {String} subject The Subject of the email
* @param {String} message The body of the email
*/
async function send(email, subject = 'No email subject provided', message = 'No email body provided') {
const options = {
from: config.email.auth.user,
to: email,
subject: subject,
html: message
};
return new Promise(resolve => {
transporter.sendMail(options, (error) => {
if (error) {
console.warn(error);
}
return resolve();
});
});
}
module.exports = {
send: send
const nodemailer = require('nodemailer');
const config = require('./config');
const transporter = nodemailer.createTransport(config.email);
/**
* Sends an email with the specified subject and message to an email address
* @param {String} email the destination email address
* @param {String} subject The Subject of the email
* @param {String} message The body of the email
*/
async function send(email, subject = 'No email subject provided', message = 'No email body provided') {
const options = {
from: config.email.auth.user,
to: email,
subject: subject,
html: message
};
return new Promise(resolve => {
transporter.sendMail(options, (error) => {
if (error) {
console.warn(error);
}
return resolve();
});
});
}
module.exports = {
send: send
};

View file

@ -1,38 +1,38 @@
const xmlbuilder = require('xmlbuilder');
const VALID_CLIENT_ID_SECRET_PAIRS = {
// 'Key' is the client ID, 'Value' is the client secret
'a2efa818a34fa16b8afbc8a74eba3eda': 'c91cdb5658bd4954ade78533a339cf9a', // Possibly WiiU exclusive?
'daf6227853bcbdce3d75baee8332b': '3eff548eac636e2bf45bb7b375e7b6b0', // Possibly 3DS exclusive?
'ea25c66c26b403376b4c5ed94ab9cdea': 'd137be62cb6a2b831cad8c013b92fb55', // Possibly 3DS exclusive?
};
function nintendoClientHeaderCheck(request, response, next) {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
const {headers} = request;
if (
!headers['x-nintendo-client-id'] ||
!headers['x-nintendo-client-secret'] ||
!VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] ||
headers['x-nintendo-client-secret'] !== VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']]
) {
return response.send(xmlbuilder.create({
errors: {
error: {
cause: 'client_id',
code: '0004',
message: 'API application invalid or incorrect application credentials'
}
}
}).end());
}
return next();
}
const xmlbuilder = require('xmlbuilder');
const VALID_CLIENT_ID_SECRET_PAIRS = {
// 'Key' is the client ID, 'Value' is the client secret
'a2efa818a34fa16b8afbc8a74eba3eda': 'c91cdb5658bd4954ade78533a339cf9a', // Possibly WiiU exclusive?
'daf6227853bcbdce3d75baee8332b': '3eff548eac636e2bf45bb7b375e7b6b0', // Possibly 3DS exclusive?
'ea25c66c26b403376b4c5ed94ab9cdea': 'd137be62cb6a2b831cad8c013b92fb55', // Possibly 3DS exclusive?
};
function nintendoClientHeaderCheck(request, response, next) {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
const {headers} = request;
if (
!headers['x-nintendo-client-id'] ||
!headers['x-nintendo-client-secret'] ||
!VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] ||
headers['x-nintendo-client-secret'] !== VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']]
) {
return response.send(xmlbuilder.create({
errors: {
error: {
cause: 'client_id',
code: '0004',
message: 'API application invalid or incorrect application credentials'
}
}
}).end());
}
return next();
}
module.exports = nintendoClientHeaderCheck;

View file

@ -1,50 +1,50 @@
const xmlbuilder = require('xmlbuilder');
const database = require('../database');
async function PNIDMiddleware(request, response, next) {
const { headers } = request;
if (!headers.authorization || !(headers.authorization.startsWith('Bearer') || headers.authorization.startsWith('Basic'))) {
return next();
}
const [type, token] = headers.authorization.split(' ');
let user;
if (type === 'Basic') {
user = await database.getUserBasic(token);
} else {
user = await database.getUserBearer(token);
}
if (!user) {
response.status(401);
if (type === 'Bearer') {
return response.send(xmlbuilder.create({
errors: {
error: {
cause: 'access_token',
code: '0005',
message: 'Invalid access token'
}
}
}).end());
}
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1105',
message: 'Email address, username, or password, is not valid'
}
}
}).end());
}
request.pnid = user;
return next();
}
const xmlbuilder = require('xmlbuilder');
const database = require('../database');
async function PNIDMiddleware(request, response, next) {
const { headers } = request;
if (!headers.authorization || !(headers.authorization.startsWith('Bearer') || headers.authorization.startsWith('Basic'))) {
return next();
}
const [type, token] = headers.authorization.split(' ');
let user;
if (type === 'Basic') {
user = await database.getUserBasic(token);
} else {
user = await database.getUserBearer(token);
}
if (!user) {
response.status(401);
if (type === 'Bearer') {
return response.send(xmlbuilder.create({
errors: {
error: {
cause: 'access_token',
code: '0005',
message: 'Invalid access token'
}
}
}).end());
}
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1105',
message: 'Email address, username, or password, is not valid'
}
}
}).end());
}
request.pnid = user;
return next();
}
module.exports = PNIDMiddleware;

View file

@ -1,23 +1,23 @@
// super basic and there's probably a much better way to do this
// this will only be used during the registration process, to track the progress of the user
// express-session uses cookies which the WiiU does not support during the registration process
// temp, in-memory session storage
const sessionStore = {};
function sessionMiddlware(request, response, next) {
const ip = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
if (!sessionStore[ip]) {
sessionStore[ip] = {};
}
const session = sessionStore[ip];
request.session = session;
return next();
}
// super basic and there's probably a much better way to do this
// this will only be used during the registration process, to track the progress of the user
// express-session uses cookies which the WiiU does not support during the registration process
// temp, in-memory session storage
const sessionStore = {};
function sessionMiddlware(request, response, next) {
const ip = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
if (!sessionStore[ip]) {
sessionStore[ip] = {};
}
const session = sessionStore[ip];
request.session = session;
return next();
}
module.exports = sessionMiddlware;

View file

@ -1,35 +1,35 @@
const { document: xmlParser } = require('xmlbuilder2');
function XMLMiddleware(request, response, next) {
if (request.method == 'POST' || request.method == 'PUT') {
const headers = request.headers;
let body = '';
if (
!headers['content-type'] ||
!headers['content-type'].toLowerCase().includes('xml')
) {
return next();
}
request.setEncoding('utf-8');
request.on('data', (chunk) => {
body += chunk;
});
request.on('end', () => {
try {
request.body = xmlParser(body);
request.body = request.body.toObject();
} catch (error) {
return next();
}
next();
});
} else {
next();
}
}
const { document: xmlParser } = require('xmlbuilder2');
function XMLMiddleware(request, response, next) {
if (request.method == 'POST' || request.method == 'PUT') {
const headers = request.headers;
let body = '';
if (
!headers['content-type'] ||
!headers['content-type'].toLowerCase().includes('xml')
) {
return next();
}
request.setEncoding('utf-8');
request.on('data', (chunk) => {
body += chunk;
});
request.on('end', () => {
try {
request.body = xmlParser(body);
request.body = request.body.toObject();
} catch (error) {
return next();
}
next();
});
} else {
next();
}
}
module.exports = XMLMiddleware;

View file

@ -1,37 +1,37 @@
const { Schema, model } = require('mongoose');
const DeviceAttributeSchema = new Schema({
created_date: String,
name: String,
value: String,
});
const DeviceAttribute = model('DeviceAttribute', DeviceAttributeSchema);
const DeviceSchema = new Schema({
is_emulator: {
type: Boolean,
default: false
},
console_type: {
type: String,
enum: ['wup', 'ctr', 'spr', 'ftr', 'ktr', 'red', 'jan'] // wup is WiiU, the rest are the 3DS family. Only wup is used atm
},
device_id: Number,
device_type: Number,
serial: String,
device_attributes: [DeviceAttributeSchema],
soap: {
token: String,
account_id: Number,
}
});
const Device = model('Device', DeviceSchema);
module.exports = {
DeviceSchema,
Device,
DeviceAttributeSchema,
DeviceAttribute
const { Schema, model } = require('mongoose');
const DeviceAttributeSchema = new Schema({
created_date: String,
name: String,
value: String,
});
const DeviceAttribute = model('DeviceAttribute', DeviceAttributeSchema);
const DeviceSchema = new Schema({
is_emulator: {
type: Boolean,
default: false
},
console_type: {
type: String,
enum: ['wup', 'ctr', 'spr', 'ftr', 'ktr', 'red', 'jan'] // wup is WiiU, the rest are the 3DS family. Only wup is used atm
},
device_id: Number,
device_type: Number,
serial: String,
device_attributes: [DeviceAttributeSchema],
soap: {
token: String,
account_id: Number,
}
});
const Device = model('Device', DeviceSchema);
module.exports = {
DeviceSchema,
Device,
DeviceAttributeSchema,
DeviceAttribute
};

View file

@ -1,220 +1,220 @@
const { Schema, model } = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const util = require('../util');
const { DeviceSchema } = require('./device');
const PNIDSchema = new Schema({
access_level: {
type: Number,
default: 0 // Standard user
},
pid: {
type: Number,
unique: true
},
creation_date: String,
updated: String,
username: {
type: String,
unique: true,
minlength: 6,
maxlength: 16
},
usernameLower: {
type: String,
unique: true
},
password: String,
birthdate: String,
gender: String,
country: String,
language: String,
email: {
address: String,
primary: Boolean,
parent: Boolean,
reachable: Boolean,
validated: Boolean,
validated_date: String,
id: {
type: Number,
unique: true
}
},
region: Number,
timezone: {
name: String,
offset: Number
},
mii: {
name: String,
primary: Boolean,
data: String,
id: {
type: Number,
unique: true
},
hash: {
type: String,
unique: true
},
image_url: String,
image_id: {
type: Number,
unique: true
},
},
flags: { // not entirely sure what these are used for
active: Boolean, // Is the account active? Like, not deleted maybe?
marketing: Boolean,
off_device: Boolean
},
devices: [DeviceSchema],
nex: {
password: String,
token: String
},
identification: { // user identification tokens
email_code: {
type: String,
unique: true
},
email_token: {
type: String,
unique: true
},
access_token: {
value: String,
ttl: Number
},
refresh_token: {
value: String,
ttl: Number
}
}
});
PNIDSchema.plugin(uniqueValidator, {message: '{PATH} already in use.'});
/*
According to http://pf2m.com/tools/rank.php Nintendo PID's start at 1,800,000,000 and count down with each account
This means the max PID is 1799999999 and hard-limits the number of potential accounts to 1,800,000,000
The author of that site does not give any information on how they found this out, but it does seem to hold true
https://account.nintendo.net/v1/api/admin/mapped_ids?input_type=pid&output_type=user_id&input=1800000000 returns nothing
https://account.nintendo.net/v1/api/admin/mapped_ids?input_type=pid&output_type=user_id&input=1799999999 returns `prodtest1`
and the next few accounts counting down seem to be admin, service and internal test accounts
*/
PNIDSchema.methods.generatePID = async function() {
const min = 1000000000; // The console (WiiU) seems to not accept PIDs smaller than this
const max = 1799999999;
let pid = Math.floor(Math.random() * (max - min + 1) + min);
const inuse = await PNID.findOne({
pid
});
pid = (inuse ? await PNID.generatePID() : pid);
this.set('pid', pid);
};
PNIDSchema.methods.generateNEXPassword = function() {
function character() {
const offset = Math.floor(Math.random() * 62);
if (offset < 10) return offset;
if (offset < 36) return String.fromCharCode(offset + 55);
return String.fromCharCode(offset + 61);
}
const output = [];
while (output.length < 16) {
output.push(character());
}
this.set('nex.password', output.join(''));
};
PNIDSchema.methods.generateEmailValidationCode = async function() {
let code = Math.random().toFixed(6).split('.')[1]; // Dirty one-liner to generate numbers of 6 length and padded 0
const inuse = await PNID.findOne({
'identification.email_code': code
});
code = (inuse ? await PNID.generateEmailValidationCode() : code);
this.set('identification.email_code', code);
};
PNIDSchema.methods.generateEmailValidationToken = async function() {
let token = crypto.randomBytes(32).toString('hex');
const inuse = await PNID.findOne({
'identification.email_token': token
});
token = (inuse ? await PNID.generateEmailValidationToken() : token);
this.set('identification.email_token', token);
};
PNIDSchema.methods.getDevice = async function(document) {
const devices = this.get('devices');
return devices.find(device => {
return (
(device.device_id === document.device_id) &&
(device.device_type === document.device_type) &&
(device.serial === document.serial)
);
});
};
PNIDSchema.methods.addDevice = async function(device) {
this.devices.push(device);
await this.save();
};
PNIDSchema.methods.removeDevice = async function(device) {
this.devices = this.devices.filter(({ _id }) => _id !== device._id);
await this.save();
};
PNIDSchema.methods.updateMii = async function({name, primary, data}) {
this.set('mii.name', name);
this.set('mii.primary', primary === 'Y');
this.set('mii.data', data);
await this.save();
};
PNIDSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
return next();
}
this.set('usernameLower', this.get('username').toLowerCase());
await this.generatePID();
await this.generateNEXPassword();
await this.generateEmailValidationCode();
await this.generateEmailValidationToken();
const primaryHash = util.nintendoPasswordHash(this.get('password'), this.get('pid'));
const hash = bcrypt.hashSync(primaryHash, 10);
this.set('password', hash);
next();
});
const PNID = model('PNID', PNIDSchema);
module.exports = {
PNIDSchema,
PNID
const { Schema, model } = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const util = require('../util');
const { DeviceSchema } = require('./device');
const PNIDSchema = new Schema({
access_level: {
type: Number,
default: 0 // Standard user
},
pid: {
type: Number,
unique: true
},
creation_date: String,
updated: String,
username: {
type: String,
unique: true,
minlength: 6,
maxlength: 16
},
usernameLower: {
type: String,
unique: true
},
password: String,
birthdate: String,
gender: String,
country: String,
language: String,
email: {
address: String,
primary: Boolean,
parent: Boolean,
reachable: Boolean,
validated: Boolean,
validated_date: String,
id: {
type: Number,
unique: true
}
},
region: Number,
timezone: {
name: String,
offset: Number
},
mii: {
name: String,
primary: Boolean,
data: String,
id: {
type: Number,
unique: true
},
hash: {
type: String,
unique: true
},
image_url: String,
image_id: {
type: Number,
unique: true
},
},
flags: { // not entirely sure what these are used for
active: Boolean, // Is the account active? Like, not deleted maybe?
marketing: Boolean,
off_device: Boolean
},
devices: [DeviceSchema],
nex: {
password: String,
token: String
},
identification: { // user identification tokens
email_code: {
type: String,
unique: true
},
email_token: {
type: String,
unique: true
},
access_token: {
value: String,
ttl: Number
},
refresh_token: {
value: String,
ttl: Number
}
}
});
PNIDSchema.plugin(uniqueValidator, {message: '{PATH} already in use.'});
/*
According to http://pf2m.com/tools/rank.php Nintendo PID's start at 1,800,000,000 and count down with each account
This means the max PID is 1799999999 and hard-limits the number of potential accounts to 1,800,000,000
The author of that site does not give any information on how they found this out, but it does seem to hold true
https://account.nintendo.net/v1/api/admin/mapped_ids?input_type=pid&output_type=user_id&input=1800000000 returns nothing
https://account.nintendo.net/v1/api/admin/mapped_ids?input_type=pid&output_type=user_id&input=1799999999 returns `prodtest1`
and the next few accounts counting down seem to be admin, service and internal test accounts
*/
PNIDSchema.methods.generatePID = async function() {
const min = 1000000000; // The console (WiiU) seems to not accept PIDs smaller than this
const max = 1799999999;
let pid = Math.floor(Math.random() * (max - min + 1) + min);
const inuse = await PNID.findOne({
pid
});
pid = (inuse ? await PNID.generatePID() : pid);
this.set('pid', pid);
};
PNIDSchema.methods.generateNEXPassword = function() {
function character() {
const offset = Math.floor(Math.random() * 62);
if (offset < 10) return offset;
if (offset < 36) return String.fromCharCode(offset + 55);
return String.fromCharCode(offset + 61);
}
const output = [];
while (output.length < 16) {
output.push(character());
}
this.set('nex.password', output.join(''));
};
PNIDSchema.methods.generateEmailValidationCode = async function() {
let code = Math.random().toFixed(6).split('.')[1]; // Dirty one-liner to generate numbers of 6 length and padded 0
const inuse = await PNID.findOne({
'identification.email_code': code
});
code = (inuse ? await PNID.generateEmailValidationCode() : code);
this.set('identification.email_code', code);
};
PNIDSchema.methods.generateEmailValidationToken = async function() {
let token = crypto.randomBytes(32).toString('hex');
const inuse = await PNID.findOne({
'identification.email_token': token
});
token = (inuse ? await PNID.generateEmailValidationToken() : token);
this.set('identification.email_token', token);
};
PNIDSchema.methods.getDevice = async function(document) {
const devices = this.get('devices');
return devices.find(device => {
return (
(device.device_id === document.device_id) &&
(device.device_type === document.device_type) &&
(device.serial === document.serial)
);
});
};
PNIDSchema.methods.addDevice = async function(device) {
this.devices.push(device);
await this.save();
};
PNIDSchema.methods.removeDevice = async function(device) {
this.devices = this.devices.filter(({ _id }) => _id !== device._id);
await this.save();
};
PNIDSchema.methods.updateMii = async function({name, primary, data}) {
this.set('mii.name', name);
this.set('mii.primary', primary === 'Y');
this.set('mii.data', data);
await this.save();
};
PNIDSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
return next();
}
this.set('usernameLower', this.get('username').toLowerCase());
await this.generatePID();
await this.generateNEXPassword();
await this.generateEmailValidationCode();
await this.generateEmailValidationToken();
const primaryHash = util.nintendoPasswordHash(this.get('password'), this.get('pid'));
const hash = bcrypt.hashSync(primaryHash, 10);
this.set('password', hash);
next();
});
const PNID = model('PNID', PNIDSchema);
module.exports = {
PNIDSchema,
PNID
};

View file

@ -1,59 +1,74 @@
process.title = 'Pretendo - Account';
const express = require('express');
const morgan = require('morgan');
const xmlparser = require('./middleware/xml-parser');
const database = require('./database');
const logger = require('../logger');
const config = require('./config.json');
const { http: { port } } = config;
const app = express();
const account = require('./services/account');
// START APPLICATION
app.set('etag', false);
app.disable('x-powered-by');
// Create router
logger.info('Setting up Middleware');
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({
extended: true
}));
app.use(xmlparser);
// import the servers into one
app.use(account);
// 404 handler
logger.info('Creating 404 status handler');
app.use((request, response) => {
response.status(404);
response.send();
});
// non-404 error handler
logger.info('Creating non-404 status handler');
app.use((error, request, response) => {
const status = error.status || 500;
response.status(status);
response.json({
app: 'api',
status,
error: error.message
});
});
// Starts the server
logger.info('Starting server');
database.connect().then(() => {
app.listen(port, () => {
logger.success(`Server started on port ${port}`);
});
process.title = 'Pretendo - Account';
const express = require('express');
const morgan = require('morgan');
const xmlparser = require('./middleware/xml-parser');
const database = require('./database');
const util = require('./util');
const logger = require('../logger');
const config = require('./config.json');
const { http: { port } } = config;
const app = express();
const accountWiiU = require('./services/wiiu');
//const account3DS = require('./services/3ds');
// START APPLICATION
app.set('etag', false);
app.disable('x-powered-by');
// Create router
logger.info('Setting up Middleware');
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({
extended: true
}));
app.use(xmlparser);
// import the servers into one
app.use(accountWiiU);
//app.use(account3DS);
// 404 handler
logger.info('Creating 404 status handler');
app.use((request, response) => {
const fullUrl = util.fullUrl(request);
const deviceId = request.headers['X-Nintendo-Device-ID'] || 'Unknown';
logger.warn(`HTTP 404 at ${fullUrl} from ${deviceId}`);
response.status(404);
response.json({
app: 'api',
status: 404,
error: 'Route not found'
});
});
// non-404 error handler
logger.info('Creating non-404 status handler');
app.use((error, request, response) => {
const status = error.status || 500;
const fullUrl = util.fullUrl(request);
const deviceId = request.headers['X-Nintendo-Device-ID'] || 'Unknown';
logger.warn(`HTTP ${status} at ${fullUrl} from ${deviceId}: ${error.message}`);
response.status(status);
response.json({
app: 'api',
status,
error: error.message
});
});
// Starts the server
logger.info('Starting server');
database.connect().then(() => {
app.listen(port, () => {
logger.success(`Server started on port ${port}`);
});
});

22
src/services/3ds/index.js Normal file
View file

@ -0,0 +1,22 @@
// handles NASC endpoints
const express = require('express');
const subdomain = require('express-subdomain');
const logger = require('../../../logger');
const routes = require('./routes');
// Main router for endpoints
const router = express.Router();
// Router to handle the subdomain restriction
const nasc = express.Router();
// Create subdomains
logger.info('[ACCOUNT - 3DS] Creating \'nasc\' subdomain');
router.use(subdomain('nasc', nasc));
// Setup routes
logger.info('[ACCOUNT - 3DS] Applying imported routes');
nasc.use('/ac', routes.ACCOUNT);
module.exports = router;

View file

View file

@ -0,0 +1,3 @@
module.exports = {
ACCOUNT: require('./account')
};

View file

@ -1,32 +1,35 @@
const express = require('express');
const subdomain = require('express-subdomain');
const sessionMiddleware = require('../../middleware/session');
const pnidMiddleware = require('../../middleware/pnid');
const logger = require('../../../logger');
const routes = require('./routes');
// Main router for endpoints
const router = express.Router();
// Router to handle the subdomain restriction
const account = express.Router();
// Create subdomains
logger.info('[ACCOUNT] Creating \'account\' subdomain');
router.use(subdomain('account', account));
logger.info('[ACCOUNT] Importing middleware');
account.use(sessionMiddleware);
account.use(pnidMiddleware);
// Setup routes
logger.info('[ACCOUNT] Applying imported routes');
account.use('/v1/api/admin', routes.ADMIN);
account.use('/v1/api/content', routes.CONTENT);
account.use('/v1/api/devices', routes.DEVICES);
account.use('/v1/api/oauth20', routes.OAUTH);
account.use('/v1/api/people', routes.PEOPLE);
account.use('/v1/api/provider', routes.PROVIDER);
account.use('/v1/api/support', routes.SUPPORT);
// handles "account.nintendo.net" endpoints
const express = require('express');
const subdomain = require('express-subdomain');
const sessionMiddleware = require('../../middleware/session');
const pnidMiddleware = require('../../middleware/pnid');
const logger = require('../../../logger');
const routes = require('./routes');
// Router to handle the subdomain restriction
const account = express.Router();
logger.info('[ACCOUNT - WiiU] Importing middleware');
account.use(sessionMiddleware);
account.use(pnidMiddleware);
// Setup routes
logger.info('[ACCOUNT - WiiU] Applying imported routes');
account.use('/v1/api/admin', routes.ADMIN);
account.use('/v1/api/content', routes.CONTENT);
account.use('/v1/api/devices', routes.DEVICES);
account.use('/v1/api/miis', routes.MIIS);
account.use('/v1/api/oauth20', routes.OAUTH);
account.use('/v1/api/people', routes.PEOPLE);
account.use('/v1/api/provider', routes.PROVIDER);
account.use('/v1/api/support', routes.SUPPORT);
// Main router for endpoints
const router = express.Router();
// Create subdomains
logger.info('[ACCOUNT - WiiU] Creating \'account\' subdomain');
router.use(subdomain('account', account));
module.exports = router;

View file

@ -1,39 +1,39 @@
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const { PNID } = require('../../../models/pnid');
const clientHeaderCheck = require('../../../middleware/client-header');
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/admin/mapped_ids
* Description: Maps given input to expected output
*/
router.get('/mapped_ids', clientHeaderCheck, async (request, response) => {
let { input: inputList, input_type: inputType, output_type: outputType } = request.query;
inputList = inputList.split(',');
if (inputType === 'user_id') {
inputType = 'usernameLower';
inputList = inputList.map(name => name.toLowerCase());
}
if (outputType === 'user_id') {
outputType = 'username';
}
let results = await PNID.where(inputType, inputList);
results = results.map(user => ({
in_id: user.get(inputType),
out_id: user.get(outputType) || ''
}));
response.send(xmlbuilder.create({
mapped_ids: {
mapped_id: results
}
}).end());
});
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const { PNID } = require('../../../models/pnid');
const clientHeaderCheck = require('../../../middleware/client-header');
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/admin/mapped_ids
* Description: Maps given input to expected output
*/
router.get('/mapped_ids', clientHeaderCheck, async (request, response) => {
let { input: inputList, input_type: inputType, output_type: outputType } = request.query;
inputList = inputList.split(',');
if (inputType === 'user_id') {
inputType = 'usernameLower';
inputList = inputList.map(name => name.toLowerCase());
}
if (outputType === 'user_id') {
outputType = 'username';
}
let results = await PNID.where(inputType, inputList);
results = results.map(user => ({
in_id: user.get(inputType),
out_id: user.get(outputType) || ''
}));
response.send(xmlbuilder.create({
mapped_ids: {
mapped_id: results
}
}).end());
});
module.exports = router;

View file

@ -1,161 +1,161 @@
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const timezones = require('../timezones.json');
const clientHeaderCheck = require('../../../middleware/client-header');
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/content/agreements/TYPE/REGION/VERSION
* Description: Sends the client requested agreement
*/
router.get('/agreements/:type/:region/:version', clientHeaderCheck, (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
// Registration process has started
request.session.registration_status = 0;
response.send(xmlbuilder.create({
agreements: {
agreement: [
{
country: 'US',
language: 'en',
language_name: 'English',
publish_date: '2014-09-29T20:07:35',
texts: {
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:type': 'chunkedStoredAgreementText',
main_title: {
'#cdata': 'Pretendo Network Services Agreement'
},
agree_text: {
'#cdata': 'I Accept'
},
non_agree_text: {
'#cdata': 'I Decline'
},
main_text: {
'@index': '1',
'#cdata': 'Dont be dumb'
}
},
type: 'NINTENDO-NETWORK-EULA',
version: '0300',
},
{
country: 'US',
language: 'en',
language_name: 'Español',
publish_date: '2014-09-29T20:07:35',
texts: {
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:type': 'chunkedStoredAgreementText',
main_title: {
'#cdata': 'Pretendo Network Services Agreement'
},
agree_text: {
'#cdata': 'I Accept'
},
non_agree_text: {
'#cdata': 'I Decline'
},
main_text: {
'@index': '1',
'#cdata': 'Dont be dumb'
}
},
type: 'NINTENDO-NETWORK-EULA',
version: '0300',
},
{
country: 'US',
language: 'en',
language_name: 'Français',
publish_date: '2014-09-29T20:07:35',
texts: {
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:type': 'chunkedStoredAgreementText',
main_title: {
'#cdata': 'Pretendo Network Services Agreement'
},
agree_text: {
'#cdata': 'I Accept'
},
non_agree_text: {
'#cdata': 'I Decline'
},
main_text: {
'@index': '1',
'#cdata': 'Dont be dumb'
}
},
type: 'NINTENDO-NETWORK-EULA',
version: '0300',
}
]
}
}).end());
});
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/content/time_zones/COUNTRY/LANGUAGE
* Description: Sends the client the requested timezones
*/
router.get('/time_zones/:countryCode/:language', clientHeaderCheck, (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
// Status should be 0 from previous request in registration process
if (request.session.registration_status !== 0) {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
}
/*
// Old method. Crashes WiiU when sending a list with over 32 entries, but otherwise works
// countryTimezones is "countries-and-timezones" module
const country = countryTimezones.getCountry(countryCode);
const timezones = country.timezones.map((timezone, index) => {
const data = countryTimezones.getTimezone(timezone);
return {
area: data.name,
language,
name: data.name,
utc_offset: data.utcOffset * 6 * 10,
order: index+1
};
});
*/
const { countryCode, language } = request.params;
const regionLanguages = timezones[countryCode];
const regionTimezones = regionLanguages[language] ? regionLanguages[language] : Object.values(regionLanguages)[0];
// Bump status to allow access to next endpoint
request.session.registration_status = 1;
response.send(xmlbuilder.create({
timezones: {
timezone: regionTimezones
}
}).end());
});
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const timezones = require('../timezones.json');
const clientHeaderCheck = require('../../../middleware/client-header');
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/content/agreements/TYPE/REGION/VERSION
* Description: Sends the client requested agreement
*/
router.get('/agreements/:type/:region/:version', clientHeaderCheck, (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
// Registration process has started
request.session.registration_status = 0;
response.send(xmlbuilder.create({
agreements: {
agreement: [
{
country: 'US',
language: 'en',
language_name: 'English',
publish_date: '2014-09-29T20:07:35',
texts: {
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:type': 'chunkedStoredAgreementText',
main_title: {
'#cdata': 'Pretendo Network Services Agreement'
},
agree_text: {
'#cdata': 'I Accept'
},
non_agree_text: {
'#cdata': 'I Decline'
},
main_text: {
'@index': '1',
'#cdata': 'Dont be dumb'
}
},
type: 'NINTENDO-NETWORK-EULA',
version: '0300',
},
{
country: 'US',
language: 'en',
language_name: 'Español',
publish_date: '2014-09-29T20:07:35',
texts: {
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:type': 'chunkedStoredAgreementText',
main_title: {
'#cdata': 'Pretendo Network Services Agreement'
},
agree_text: {
'#cdata': 'I Accept'
},
non_agree_text: {
'#cdata': 'I Decline'
},
main_text: {
'@index': '1',
'#cdata': 'Dont be dumb'
}
},
type: 'NINTENDO-NETWORK-EULA',
version: '0300',
},
{
country: 'US',
language: 'en',
language_name: 'Français',
publish_date: '2014-09-29T20:07:35',
texts: {
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xsi:type': 'chunkedStoredAgreementText',
main_title: {
'#cdata': 'Pretendo Network Services Agreement'
},
agree_text: {
'#cdata': 'I Accept'
},
non_agree_text: {
'#cdata': 'I Decline'
},
main_text: {
'@index': '1',
'#cdata': 'Dont be dumb'
}
},
type: 'NINTENDO-NETWORK-EULA',
version: '0300',
}
]
}
}).end());
});
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/content/time_zones/COUNTRY/LANGUAGE
* Description: Sends the client the requested timezones
*/
router.get('/time_zones/:countryCode/:language', clientHeaderCheck, (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
// Status should be 0 from previous request in registration process
if (request.session.registration_status !== 0) {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
}
/*
// Old method. Crashes WiiU when sending a list with over 32 entries, but otherwise works
// countryTimezones is "countries-and-timezones" module
const country = countryTimezones.getCountry(countryCode);
const timezones = country.timezones.map((timezone, index) => {
const data = countryTimezones.getTimezone(timezone);
return {
area: data.name,
language,
name: data.name,
utc_offset: data.utcOffset * 6 * 10,
order: index+1
};
});
*/
const { countryCode, language } = request.params;
const regionLanguages = timezones[countryCode];
const regionTimezones = regionLanguages[language] ? regionLanguages[language] : Object.values(regionLanguages)[0];
// Bump status to allow access to next endpoint
request.session.registration_status = 1;
response.send(xmlbuilder.create({
timezones: {
timezone: regionTimezones
}
}).end());
});
module.exports = router;

View file

@ -1,16 +1,16 @@
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const clientHeaderCheck = require('../../../middleware/client-header');
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/devices/@current/status
* Description: Unknown use
*/
router.get('/@current/status', clientHeaderCheck, async (request, response) => {
response.send(xmlbuilder.create({
device: ''
}).end());
});
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const clientHeaderCheck = require('../../../middleware/client-header');
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/devices/@current/status
* Description: Unknown use
*/
router.get('/@current/status', clientHeaderCheck, async (request, response) => {
response.send(xmlbuilder.create({
device: ''
}).end());
});
module.exports = router;

View file

@ -1,9 +1,10 @@
module.exports = {
ADMIN: require('./admin'),
CONTENT: require('./content'),
DEVICES: require('./devices'),
OAUTH: require('./oauth'),
PEOPLE: require('./people'),
PROVIDER: require('./provider'),
SUPPORT: require('./support'),
module.exports = {
ADMIN: require('./admin'),
CONTENT: require('./content'),
DEVICES: require('./devices'),
MIIS: require('./miis'),
OAUTH: require('./oauth'),
PEOPLE: require('./people'),
PROVIDER: require('./provider'),
SUPPORT: require('./support'),
};

View file

@ -0,0 +1,95 @@
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const { PNID } = require('../../../models/pnid');
const clientHeaderCheck = require('../../../middleware/client-header');
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/miis
* Description: Returns a list of NNID miis
*/
router.get('/', clientHeaderCheck, async (request, response) => {
const { pids } = request.query;
const results = await PNID.where('pid', pids);
const miis = [];
// We don't have a Mii renderer yet so hard code the images
const hardCodedImages = [
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_standard.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_standard.png',
type: 'standard'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_frustrated_face.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_frustrated_face.png',
type: 'frustrated_face'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_happy_face.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_happy_face.png',
type: 'happy_face'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_like_face.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_like_face.png',
type: 'like_face'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_normal_face.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_normal_face.png',
type: 'normal_face'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_puzzled_face.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_puzzled_face.png',
type: 'puzzled_face'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_surprised_face.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_surprised_face.png',
type: 'surprised_face'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_whole_body.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_whole_body.png',
type: 'whole_body'
}
];
for (const user of results) {
const { mii } = user;
miis.push({
data: mii.data.replace(/(\r\n|\n|\r)/gm, ''),
id: mii.id,
images: {
image: hardCodedImages
},
name: mii.name,
pid: user.pid,
primary: mii.primary ? 'Y' : 'N',
user_id: user.username
});
}
//console.log(results[0].mii.data.replace(/(\r\n|\n|\r)/gm, ''));
response.send(xmlbuilder.create({
miis: {
mii: miis
}
}).end());
});
module.exports = router;

View file

@ -1,116 +1,116 @@
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const bcrypt = require('bcrypt');
const fs = require('fs-extra');
const clientHeaderCheck = require('../../../middleware/client-header');
const database = require('../../../database');
const util = require('../../../util');
/**
* [POST]
* Replacement for: https://account.nintendo.net/v1/api/oauth20/access_token/generate
* Description: Generates an access token for a user
*/
router.post('/access_token/generate', clientHeaderCheck, async (request, response) => {
const titleId = request.headers['x-nintendo-title-id'];
const { body } = request;
const { grant_type, user_id, password } = body;
if (!['password', 'refresh_token'].includes(grant_type)) {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'grant_type',
code: '0004',
message: 'Invalid Grant Type'
}
}).end());
}
if (!user_id || user_id.trim() === '') {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'user_id',
code: '0002',
message: 'user_id format is invalid'
}
}).end());
}
if (!password || password.trim() === '') {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'password',
code: '0002',
message: 'password format is invalid'
}
}).end());
}
const pnid = await database.getUserByUsername(user_id);
if (!pnid || !bcrypt.compareSync(password, pnid.password)) {
response.status(400);
return response.send(xmlbuilder.create({
error: {
code: '0106',
message: 'Invalid account ID or password'
}
}).end());
}
const cryptoPath = `${__dirname}/../../../../certs/access`;
if (!fs.pathExistsSync(cryptoPath)) {
// Need to generate keys
return response.send(xmlbuilder.create({
errors: {
error: {
code: '0000',
message: 'Could not find access key crypto path'
}
}
}).end());
}
const publicKey = fs.readFileSync(`${cryptoPath}/public.pem`);
const hmacSecret = fs.readFileSync(`${cryptoPath}/secret.key`);
const cryptoOptions = {
public_key: publicKey,
hmac_secret: hmacSecret
};
const accessTokenOptions = {
system_type: 0x1, // WiiU
token_type: 0x1, // OAuth Access,
pid: pnid.get('pid'),
title_id: BigInt(parseInt(titleId, 16)),
date: BigInt(Date.now())
};
const refreshTokenOptions = {
system_type: 0x1, // WiiU
token_type: 0x2, // OAuth Refresh,
pid: pnid.get('pid'),
title_id: BigInt(parseInt(titleId, 16)),
date: BigInt(Date.now())
};
const accessToken = util.generateToken(cryptoOptions, accessTokenOptions);
const refreshToken = util.generateToken(cryptoOptions, refreshTokenOptions);
response.send(xmlbuilder.create({
OAuth20: {
access_token: {
token: accessToken,
refresh_token: refreshToken,
expires_in: 3600
}
}
}).end());
});
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const bcrypt = require('bcrypt');
const fs = require('fs-extra');
const clientHeaderCheck = require('../../../middleware/client-header');
const database = require('../../../database');
const util = require('../../../util');
/**
* [POST]
* Replacement for: https://account.nintendo.net/v1/api/oauth20/access_token/generate
* Description: Generates an access token for a user
*/
router.post('/access_token/generate', clientHeaderCheck, async (request, response) => {
const titleId = request.headers['x-nintendo-title-id'];
const { body } = request;
const { grant_type, user_id, password } = body;
if (!['password', 'refresh_token'].includes(grant_type)) {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'grant_type',
code: '0004',
message: 'Invalid Grant Type'
}
}).end());
}
if (!user_id || user_id.trim() === '') {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'user_id',
code: '0002',
message: 'user_id format is invalid'
}
}).end());
}
if (!password || password.trim() === '') {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'password',
code: '0002',
message: 'password format is invalid'
}
}).end());
}
const pnid = await database.getUserByUsername(user_id);
if (!pnid || !bcrypt.compareSync(password, pnid.password)) {
response.status(400);
return response.send(xmlbuilder.create({
error: {
code: '0106',
message: 'Invalid account ID or password'
}
}).end());
}
const cryptoPath = `${__dirname}/../../../../certs/access`;
if (!fs.pathExistsSync(cryptoPath)) {
// Need to generate keys
return response.send(xmlbuilder.create({
errors: {
error: {
code: '0000',
message: 'Could not find access key crypto path'
}
}
}).end());
}
const publicKey = fs.readFileSync(`${cryptoPath}/public.pem`);
const hmacSecret = fs.readFileSync(`${cryptoPath}/secret.key`);
const cryptoOptions = {
public_key: publicKey,
hmac_secret: hmacSecret
};
const accessTokenOptions = {
system_type: 0x1, // WiiU
token_type: 0x1, // OAuth Access,
pid: pnid.get('pid'),
title_id: BigInt(parseInt(titleId, 16)),
date: BigInt(Date.now())
};
const refreshTokenOptions = {
system_type: 0x1, // WiiU
token_type: 0x2, // OAuth Refresh,
pid: pnid.get('pid'),
title_id: BigInt(parseInt(titleId, 16)),
date: BigInt(Date.now())
};
const accessToken = util.generateToken(cryptoOptions, accessTokenOptions);
const refreshToken = util.generateToken(cryptoOptions, refreshTokenOptions);
response.send(xmlbuilder.create({
OAuth20: {
access_token: {
token: accessToken,
refresh_token: refreshToken,
expires_in: 3600
}
}
}).end());
});
module.exports = router;

View file

@ -1,283 +1,283 @@
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const moment = require('moment');
const crypto = require('crypto');
const { PNID } = require('../../../models/pnid');
const clientHeaderCheck = require('../../../middleware/client-header');
const database = require('../../../database');
const mailer = require('../../../mailer');
require('moment-timezone');
router.get('/:username', clientHeaderCheck, async (request, response) => {
// Status should be 1 from previous request in registration process
if (request.session.registration_status !== 1) {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
}
const { username } = request.params;
const userExists = await database.doesUserExist(username);
if (userExists) {
response.status(400);
return response.end(xmlbuilder.create({
errors: {
error: {
code: '0100',
message: 'Account ID already exists'
}
}
}).end());
}
// Bump status to allow access to next endpoint
request.session.registration_status = 2;
response.status(200);
response.end();
});
router.post('/', clientHeaderCheck, async (request, response) => {
// Status should be 3 from previous request in registration process
if (request.session.registration_status !== 3) {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
}
const person = request.body.get('person');
const userExists = await database.doesUserExist(person.get('user_id'));
if (userExists) {
response.status(400);
return response.end(xmlbuilder.create({
errors: {
error: {
code: '0100',
message: 'Account ID already exists'
}
}
}).end());
}
const creationDate = moment().format('YYYY-MM-DDTHH:MM:SS');
const document = {
pid: 1, // will be overwritten before saving
creation_date: creationDate,
updated: creationDate,
username: person.get('user_id'),
password: person.get('password'), // will be hashed before saving
birthdate: person.get('birth_date'),
gender: person.get('gender'),
country: person.get('country'),
language: person.get('language'),
email: {
address: person.get('email').get('address'),
primary: person.get('email').get('primary') === 'Y',
parent: person.get('email').get('parent') === 'Y',
reachable: false,
validated: person.get('email').get('validated') === 'Y',
id: Math.floor(Math.random(10000000000)*10000000000)
},
region: person.get('region'),
timezone: {
name: person.get('tz_name'),
offset: (moment.tz(person.get('tz_name')).utcOffset() * 60)
},
mii: {
name: person.get('mii').get('name'),
primary: person.get('mii').get('name') === 'Y',
data: person.get('mii').get('data'),
id: Math.floor(Math.random(10000000000)*10000000000),
hash: crypto.createHash('md5').update(person.get('mii').get('data')).digest('hex'),
image_url: 'https://mii-secure.account.nintendo.net/2rtgf01lztoqo_standard.tga',
image_id: Math.floor(Math.random(10000000000)*10000000000)
},
flags: {
active: true,
marketing: person.get('marketing_flag') === 'Y',
off_device: person.get('off_device_flag') === 'Y'
},
validation: {
email_code: 1, // will be overwritten before saving
email_token: '' // will be overwritten before saving
}
};
const newUser = new PNID(document);
newUser.save()
.catch(error => {
console.log(error);
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
})
.then(async newUser => {
await mailer.send(
newUser.get('email'),
'[Prentendo Network] Please confirm your e-mail address',
`Hello,
Your Prentendo Network ID activation is almost complete. Please click the link below to confirm your e-mail address and complete the activation process.
https://account.pretendo.cc/account/email-confirmation?token=` + newUser.get('identification.email_token') + `
If you are unable to connect to the above URL, please enter the following confirmation code on the device to which your Prentendo Network ID is linked.
&lt;&lt;Confirmation code: ` + newUser.get('identification.email_code') + '&gt;&gt;'
);
delete request.session.registration_status;
response.send(xmlbuilder.create({
person: {
pid: newUser.get('pid')
}
}).end());
});
});
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/people/@me/profile
* Description: Gets a users profile
*/
router.get('/@me/profile', clientHeaderCheck, async (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
const { pnid } = request;
const person = await database.getUserProfileJSONByPID(pnid.get('pid'));
response.send(xmlbuilder.create({
person
}).end());
//response.send(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><person><accounts><account><attributes><attribute><id>84955611</id><name>ctr_initial_device_account_id</name><updated_by>USER</updated_by><value>195833643</value></attribute><attribute><id>84955489</id><name>environment</name><updated_by>USER</updated_by><value>PROD</value></attribute></attributes><domain>ESHOP.NINTENDO.NET</domain><type>INTERNAL</type><username>327329101</username></account></accounts><active_flag>Y</active_flag><birth_date>1998-09-22</birth_date><country>US</country><create_date>2017-12-29T04:11:24</create_date><device_attributes><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>persistent_id</name><value>80000043</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>transferable_id_base</name><value>1200000444b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>transferable_id_base_common</name><value>1180000444b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>uuid_account</name><value>42ef6c46-0ea3-11ea-97fe-010144b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>uuid_common</name><value>3d9b06d8-0ea3-11ea-97fe-010144b6221d</value></device_attribute></device_attributes><gender>M</gender><language>en</language><updated>2019-06-02T04:17:56</updated><marketing_flag>Y</marketing_flag><off_device_flag>Y</off_device_flag><pid>1750087940</pid><email><address>halolink44@gmail.com</address><id>50463196</id><parent>N</parent><primary>Y</primary><reachable>Y</reachable><type>DEFAULT</type><updated_by>INTERNAL WS</updated_by><validated>Y</validated><validated_date>2017-12-29T04:12:32</validated_date></email><mii><status>COMPLETED</status><data>AwBzMOlVognnx0GCk6r2p0D0B2n+cgAA0lJSAGUAZABEAHUAYwBrAHMAAAAAAGQrAAAWAQJoRBgmNEYUgRIXaI0AiiWBSUhQUgBlAGQARAB1AGMAawBzAHMAAAAAAJZY</data><id>1151699634</id><mii_hash>u2jg043u028x</mii_hash><mii_images><mii_image><cached_url>https://mii-secure.account.nintendo.net/u2jg043u028x_standard.tga</cached_url><id>1319591505</id><url>https://mii-secure.account.nintendo.net/u2jg043u028x_standard.tga</url><type>standard</type></mii_image></mii_images><name>RedDucks</name><primary>Y</primary></mii><region>822870016</region><tz_name>America/New_York</tz_name><user_id>RedDuckss</user_id><utc_offset>-18000</utc_offset></person>`);
});
/**
* [POST]
* Replacement for: https://account.nintendo.net/v1/api/people/@me/devices
* Description: Gets user profile, seems to be the same as https://account.nintendo.net/v1/api/people/@me/profile
*/
router.post('/@me/devices', clientHeaderCheck, async (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
// We don't care about the device attributes
// The console ignores them and PNIDs are not tied to consoles anyway
// So the server also ignores them and does not save the ones posted here
const { pnid } = request;
const person = await database.getUserProfileJSONByPID(pnid.get('pid'));
response.send(xmlbuilder.create({
person
}).end());
});
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/people/@me/devices
* Description: Returns only user devices
*/
router.get('/@me/devices', clientHeaderCheck, async (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
const { pnid, headers } = request;
response.send(xmlbuilder.create({
devices: [
{
device: {
device_id: headers['x-nintendo-device-id'],
language: headers['accept-language'],
updated: moment().format('YYYY-MM-DDTHH:MM:SS'),
pid: pnid.get('pid'),
platform_id: headers['x-nintendo-platform-id'],
region: headers['x-nintendo-region'],
serial_number: headers['x-nintendo-serial-number'],
status: 'ACTIVE',
system_version: headers['x-nintendo-system-version'],
type: 'RETAIL',
updated_by: 'USER'
}
}
]
}).end());
});
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/people/@me/devices/owner
* Description: Gets user profile, seems to be the same as https://account.nintendo.net/v1/api/people/@me/profile
*/
router.get('/@me/devices/owner', clientHeaderCheck, async (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', moment().add(5, 'h'));
const { pnid } = request;
const person = await database.getUserProfileJSONByPID(pnid.get('pid'));
response.send(xmlbuilder.create({
person
}).end());
});
/**
* [PUT]
* Replacement for: https://account.nintendo.net/v1/api/people/@me/miis/@primary
* Description: Updates a users Mii
*/
router.put('/@me/miis/@primary', clientHeaderCheck, async (request, response) => {
const { pnid } = request;
const mii = request.body.get('mii');
const [name, primary, data] = [mii.get('name'), mii.get('primary'), mii.get('data')]
await pnid.updateMii({name, primary, data});
response.send('');
});
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const moment = require('moment');
const crypto = require('crypto');
const { PNID } = require('../../../models/pnid');
const clientHeaderCheck = require('../../../middleware/client-header');
const database = require('../../../database');
const mailer = require('../../../mailer');
require('moment-timezone');
router.get('/:username', clientHeaderCheck, async (request, response) => {
// Status should be 1 from previous request in registration process
if (request.session.registration_status !== 1) {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
}
const { username } = request.params;
const userExists = await database.doesUserExist(username);
if (userExists) {
response.status(400);
return response.end(xmlbuilder.create({
errors: {
error: {
code: '0100',
message: 'Account ID already exists'
}
}
}).end());
}
// Bump status to allow access to next endpoint
request.session.registration_status = 2;
response.status(200);
response.end();
});
router.post('/', clientHeaderCheck, async (request, response) => {
// Status should be 3 from previous request in registration process
if (request.session.registration_status !== 3) {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
}
const person = request.body.get('person');
const userExists = await database.doesUserExist(person.get('user_id'));
if (userExists) {
response.status(400);
return response.end(xmlbuilder.create({
errors: {
error: {
code: '0100',
message: 'Account ID already exists'
}
}
}).end());
}
const creationDate = moment().format('YYYY-MM-DDTHH:MM:SS');
const document = {
pid: 1, // will be overwritten before saving
creation_date: creationDate,
updated: creationDate,
username: person.get('user_id'),
password: person.get('password'), // will be hashed before saving
birthdate: person.get('birth_date'),
gender: person.get('gender'),
country: person.get('country'),
language: person.get('language'),
email: {
address: person.get('email').get('address'),
primary: person.get('email').get('primary') === 'Y',
parent: person.get('email').get('parent') === 'Y',
reachable: false,
validated: person.get('email').get('validated') === 'Y',
id: Math.floor(Math.random(10000000000)*10000000000)
},
region: person.get('region'),
timezone: {
name: person.get('tz_name'),
offset: (moment.tz(person.get('tz_name')).utcOffset() * 60)
},
mii: {
name: person.get('mii').get('name'),
primary: person.get('mii').get('name') === 'Y',
data: person.get('mii').get('data'),
id: Math.floor(Math.random(10000000000)*10000000000),
hash: crypto.createHash('md5').update(person.get('mii').get('data')).digest('hex'),
image_url: 'https://mii-secure.account.nintendo.net/2rtgf01lztoqo_standard.tga',
image_id: Math.floor(Math.random(10000000000)*10000000000)
},
flags: {
active: true,
marketing: person.get('marketing_flag') === 'Y',
off_device: person.get('off_device_flag') === 'Y'
},
validation: {
email_code: 1, // will be overwritten before saving
email_token: '' // will be overwritten before saving
}
};
const newUser = new PNID(document);
newUser.save()
.catch(error => {
console.log(error);
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
})
.then(async newUser => {
await mailer.send(
newUser.get('email'),
'[Prentendo Network] Please confirm your e-mail address',
`Hello,
Your Prentendo Network ID activation is almost complete. Please click the link below to confirm your e-mail address and complete the activation process.
https://account.pretendo.cc/account/email-confirmation?token=` + newUser.get('identification.email_token') + `
If you are unable to connect to the above URL, please enter the following confirmation code on the device to which your Prentendo Network ID is linked.
&lt;&lt;Confirmation code: ` + newUser.get('identification.email_code') + '&gt;&gt;'
);
delete request.session.registration_status;
response.send(xmlbuilder.create({
person: {
pid: newUser.get('pid')
}
}).end());
});
});
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/people/@me/profile
* Description: Gets a users profile
*/
router.get('/@me/profile', clientHeaderCheck, async (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
const { pnid } = request;
const person = await database.getUserProfileJSONByPID(pnid.get('pid'));
response.send(xmlbuilder.create({
person
}).end());
//response.send(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><person><accounts><account><attributes><attribute><id>84955611</id><name>ctr_initial_device_account_id</name><updated_by>USER</updated_by><value>195833643</value></attribute><attribute><id>84955489</id><name>environment</name><updated_by>USER</updated_by><value>PROD</value></attribute></attributes><domain>ESHOP.NINTENDO.NET</domain><type>INTERNAL</type><username>327329101</username></account></accounts><active_flag>Y</active_flag><birth_date>1998-09-22</birth_date><country>US</country><create_date>2017-12-29T04:11:24</create_date><device_attributes><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>persistent_id</name><value>80000043</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>transferable_id_base</name><value>1200000444b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>transferable_id_base_common</name><value>1180000444b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>uuid_account</name><value>42ef6c46-0ea3-11ea-97fe-010144b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>uuid_common</name><value>3d9b06d8-0ea3-11ea-97fe-010144b6221d</value></device_attribute></device_attributes><gender>M</gender><language>en</language><updated>2019-06-02T04:17:56</updated><marketing_flag>Y</marketing_flag><off_device_flag>Y</off_device_flag><pid>1750087940</pid><email><address>halolink44@gmail.com</address><id>50463196</id><parent>N</parent><primary>Y</primary><reachable>Y</reachable><type>DEFAULT</type><updated_by>INTERNAL WS</updated_by><validated>Y</validated><validated_date>2017-12-29T04:12:32</validated_date></email><mii><status>COMPLETED</status><data>AwBzMOlVognnx0GCk6r2p0D0B2n+cgAA0lJSAGUAZABEAHUAYwBrAHMAAAAAAGQrAAAWAQJoRBgmNEYUgRIXaI0AiiWBSUhQUgBlAGQARAB1AGMAawBzAHMAAAAAAJZY</data><id>1151699634</id><mii_hash>u2jg043u028x</mii_hash><mii_images><mii_image><cached_url>https://mii-secure.account.nintendo.net/u2jg043u028x_standard.tga</cached_url><id>1319591505</id><url>https://mii-secure.account.nintendo.net/u2jg043u028x_standard.tga</url><type>standard</type></mii_image></mii_images><name>RedDucks</name><primary>Y</primary></mii><region>822870016</region><tz_name>America/New_York</tz_name><user_id>RedDuckss</user_id><utc_offset>-18000</utc_offset></person>`);
});
/**
* [POST]
* Replacement for: https://account.nintendo.net/v1/api/people/@me/devices
* Description: Gets user profile, seems to be the same as https://account.nintendo.net/v1/api/people/@me/profile
*/
router.post('/@me/devices', clientHeaderCheck, async (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
// We don't care about the device attributes
// The console ignores them and PNIDs are not tied to consoles anyway
// So the server also ignores them and does not save the ones posted here
const { pnid } = request;
const person = await database.getUserProfileJSONByPID(pnid.get('pid'));
response.send(xmlbuilder.create({
person
}).end());
});
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/people/@me/devices
* Description: Returns only user devices
*/
router.get('/@me/devices', clientHeaderCheck, async (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', new Date().getTime());
const { pnid, headers } = request;
response.send(xmlbuilder.create({
devices: [
{
device: {
device_id: headers['x-nintendo-device-id'],
language: headers['accept-language'],
updated: moment().format('YYYY-MM-DDTHH:MM:SS'),
pid: pnid.get('pid'),
platform_id: headers['x-nintendo-platform-id'],
region: headers['x-nintendo-region'],
serial_number: headers['x-nintendo-serial-number'],
status: 'ACTIVE',
system_version: headers['x-nintendo-system-version'],
type: 'RETAIL',
updated_by: 'USER'
}
}
]
}).end());
});
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/people/@me/devices/owner
* Description: Gets user profile, seems to be the same as https://account.nintendo.net/v1/api/people/@me/profile
*/
router.get('/@me/devices/owner', clientHeaderCheck, async (request, response) => {
response.set('Content-Type', 'text/xml');
response.set('Server', 'Nintendo 3DS (http)');
response.set('X-Nintendo-Date', moment().add(5, 'h'));
const { pnid } = request;
const person = await database.getUserProfileJSONByPID(pnid.get('pid'));
response.send(xmlbuilder.create({
person
}).end());
});
/**
* [PUT]
* Replacement for: https://account.nintendo.net/v1/api/people/@me/miis/@primary
* Description: Updates a users Mii
*/
router.put('/@me/miis/@primary', clientHeaderCheck, async (request, response) => {
const { pnid } = request;
const mii = request.body.get('mii');
const [name, primary, data] = [mii.get('name'), mii.get('primary'), mii.get('data')]
await pnid.updateMii({name, primary, data});
response.send('');
});
module.exports = router;

View file

@ -1,149 +1,151 @@
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const fs = require('fs-extra');
const util = require('../../../util');
const servers = require('../../../servers.json');
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/provider/service_token/@me
* Description: Gets a service token
*/
router.get('/service_token/@me', async (request, response) => {
const { pnid } = request;
const titleId = request.headers['x-nintendo-title-id'];
const server = servers.find(({ title_ids }) => title_ids.includes(titleId));
if (!server) {
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}
const { name, system } = server;
const cryptoPath = `${__dirname}/../../../../certs/nex/${name}`;
if (!fs.pathExistsSync(cryptoPath)) {
// Need to generate keys
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}
const publicKey = fs.readFileSync(`${cryptoPath}/public.pem`);
const hmacSecret = fs.readFileSync(`${cryptoPath}/secret.key`);
const cryptoOptions = {
public_key: publicKey,
hmac_secret: hmacSecret
};
const tokenOptions = {
system_type: system,
token_type: 0x4, // service token,
pid: pnid.get('pid'),
title_id: BigInt(parseInt(titleId, 16)),
date: BigInt(Date.now())
};
const serviceToken = util.generateToken(cryptoOptions, tokenOptions);
response.send(xmlbuilder.create({
service_token: {
token: serviceToken
}
}).end());
});
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/provider/nex_token/@me
* Description: Gets a NEX server address and token
*/
router.get('/nex_token/@me', async (request, response) => {
const { game_server_id: gameServerID } = request.query;
const { pnid } = request;
if (!gameServerID) {
return response.send(xmlbuilder.create({
errors: {
error: {
code: '0118',
message: 'Unique ID and Game Server ID are not linked'
}
}
}).end());
}
const server = servers.find(({ server_id }) => server_id === gameServerID);
if (!server) {
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}
const { name, ip, port, system } = server;
const titleId = request.headers['x-nintendo-title-id'];
const cryptoPath = `${__dirname}/../../../../certs/nex/${name}`;
if (!fs.pathExistsSync(cryptoPath)) {
// Need to generate keys
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}
const publicKey = fs.readFileSync(`${cryptoPath}/public.pem`);
const hmacSecret = fs.readFileSync(`${cryptoPath}/secret.key`);
const cryptoOptions = {
public_key: publicKey,
hmac_secret: hmacSecret
};
const tokenOptions = {
system_type: system,
token_type: 0x3, // nex token,
pid: pnid.get('pid'),
title_id: BigInt(parseInt(titleId, 16)),
date: BigInt(Date.now())
};
const nexToken = util.generateToken(cryptoOptions, tokenOptions);
response.send(xmlbuilder.create({
nex_token: {
host: ip,
nex_password: pnid.get('nex.password'),
pid: pnid.get('pid'),
port: port,
token: nexToken
}
}).end());
});
const router = require('express').Router();
const xmlbuilder = require('xmlbuilder');
const fs = require('fs-extra');
const util = require('../../../util');
const servers = require('../../../servers.json');
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/provider/service_token/@me
* Description: Gets a service token
*/
router.get('/service_token/@me', async (request, response) => {
const { pnid } = request;
const titleId = request.headers['x-nintendo-title-id'];
const server = servers.find(({ title_ids }) => title_ids.includes(titleId));
console.log(titleId);
if (!server) {
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}
const { name, system } = server;
const cryptoPath = `${__dirname}/../../../../certs/service/${name}`;
if (!fs.pathExistsSync(cryptoPath)) {
// Need to generate keys
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}
const publicKey = fs.readFileSync(`${cryptoPath}/public.pem`);
const hmacSecret = fs.readFileSync(`${cryptoPath}/secret.key`);
const cryptoOptions = {
public_key: publicKey,
hmac_secret: hmacSecret
};
const tokenOptions = {
system_type: system,
token_type: 0x4, // service token,
pid: pnid.get('pid'),
title_id: BigInt(parseInt(titleId, 16)),
date: BigInt(Date.now())
};
const serviceToken = util.generateToken(cryptoOptions, tokenOptions);
response.send(xmlbuilder.create({
service_token: {
token: serviceToken
}
}).end());
});
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/provider/nex_token/@me
* Description: Gets a NEX server address and token
*/
router.get('/nex_token/@me', async (request, response) => {
const { game_server_id: gameServerID } = request.query;
const { pnid } = request;
if (!gameServerID) {
return response.send(xmlbuilder.create({
errors: {
error: {
code: '0118',
message: 'Unique ID and Game Server ID are not linked'
}
}
}).end());
}
const server = servers.find(({ server_id }) => server_id === gameServerID);
if (!server) {
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}
const { name, ip, port, system } = server;
const titleId = request.headers['x-nintendo-title-id'];
const cryptoPath = `${__dirname}/../../../../certs/nex/${name}`;
if (!fs.pathExistsSync(cryptoPath)) {
// Need to generate keys
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());
}
const publicKey = fs.readFileSync(`${cryptoPath}/public.pem`);
const hmacSecret = fs.readFileSync(`${cryptoPath}/secret.key`);
const cryptoOptions = {
public_key: publicKey,
hmac_secret: hmacSecret
};
const tokenOptions = {
system_type: system,
token_type: 0x3, // nex token,
pid: pnid.get('pid'),
title_id: BigInt(parseInt(titleId, 16)),
date: BigInt(Date.now())
};
const nexToken = util.generateToken(cryptoOptions, tokenOptions);
response.send(xmlbuilder.create({
nex_token: {
host: ip,
nex_password: pnid.get('nex.password'),
pid: pnid.get('pid'),
port: port,
token: nexToken
}
}).end());
});
module.exports = router;

View file

@ -1,55 +1,55 @@
const router = require('express').Router();
const dns = require('dns');
const xmlbuilder = require('xmlbuilder');
const clientHeaderCheck = require('../../../middleware/client-header');
router.post('/validate/email', clientHeaderCheck, async (request, response) => {
// Status should be 2 from previous request in registration process
if (request.session.registration_status !== 2) {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
}
const { email } = request.body;
if (!email) {
return response.send(xmlbuilder.create({
errors: {
error: {
cause: 'email',
code: '0103',
message: 'Email format is invalid'
}
}
}).end());
}
const domain = email.split('@')[1];
dns.resolveMx(domain, (error) => {
if (error) {
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1126',
message: 'The domain "' + domain + '" is not accessible.'
}
}
}).end());
}
request.session.registration_status = 3;
response.status(200);
response.end();
});
});
const router = require('express').Router();
const dns = require('dns');
const xmlbuilder = require('xmlbuilder');
const clientHeaderCheck = require('../../../middleware/client-header');
router.post('/validate/email', clientHeaderCheck, async (request, response) => {
// Status should be 2 from previous request in registration process
if (request.session.registration_status !== 2) {
response.status(400);
return response.send(xmlbuilder.create({
error: {
cause: 'Bad Request',
code: '1600',
message: 'Unable to process request'
}
}).end());
}
const { email } = request.body;
if (!email) {
return response.send(xmlbuilder.create({
errors: {
error: {
cause: 'email',
code: '0103',
message: 'Email format is invalid'
}
}
}).end());
}
const domain = email.split('@')[1];
dns.resolveMx(domain, (error) => {
if (error) {
return response.send(xmlbuilder.create({
errors: {
error: {
code: '1126',
message: 'The domain "' + domain + '" is not accessible.'
}
}
}).end());
}
request.session.registration_status = 3;
response.status(200);
response.end();
});
});
module.exports = router;

View file

@ -1,172 +1,182 @@
const crypto = require('crypto');
const NodeRSA = require('node-rsa');
const fs = require('fs-extra');
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;
}
function generateRandomInt(length = 4) {
return Math.floor(Math.pow(10, length-1) + Math.random() * 9 * Math.pow(10, length-1));
}
function generateToken(cryptoOptions, tokenOptions) {
// Access and refresh tokens use a different format since they must be much smaller
if ([0x1, 0x2].includes(tokenOptions.token_type)) {
const cryptoPath = `${__dirname}/../certs/access`;
const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex');
const dataBuffer = Buffer.alloc(4 + 8);
dataBuffer.writeUInt32LE(tokenOptions.pid, 0x0);
dataBuffer.writeBigUInt64LE(tokenOptions.date, 0x4);
const iv = Buffer.alloc(16);
const cipher = crypto.createCipheriv('aes-128-cbc', aesKey, iv);
let encryptedBody = cipher.update(dataBuffer);
encryptedBody = Buffer.concat([encryptedBody, cipher.final()]);
return encryptedBody.toString('base64');
}
const publicKey = new NodeRSA(cryptoOptions.public_key, 'pkcs8-public-pem', {
environment: 'browser',
encryptionScheme: {
'hash': 'sha256',
}
});
// Create the buffer containing the token data
const dataBuffer = Buffer.alloc(1 + 1 + 4 + 8 + 8);
dataBuffer.writeUInt8(tokenOptions.system_type, 0x0);
dataBuffer.writeUInt8(tokenOptions.token_type, 0x1);
dataBuffer.writeUInt32LE(tokenOptions.pid, 0x2);
dataBuffer.writeBigUInt64LE(tokenOptions.title_id, 0x6);
dataBuffer.writeBigUInt64LE(tokenOptions.date, 0xE);
// Calculate the signature of the token body
const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(dataBuffer);
const signature = hmac.digest();
// Generate random AES key and IV
const key = crypto.randomBytes(16);
const iv = crypto.randomBytes(16);
// Encrypt the token body with AES
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
let encryptedBody = cipher.update(dataBuffer);
encryptedBody = Buffer.concat([encryptedBody, cipher.final()]);
// Encrypt the AES key with RSA public key
const encryptedKey = publicKey.encrypt(key);
// Create crypto config token section
const cryptoConfig = Buffer.concat([
encryptedKey,
iv
]);
// Build the token
const token = Buffer.concat([
cryptoConfig,
signature,
encryptedBody
]);
return token.toString('base64'); // Encode to base64 for transport
}
function decryptToken(token) {
// Access and refresh tokens use a different format since they must be much smaller
// Assume a small length means access or refresh token
if (token.length <= 32) {
const cryptoPath = `${__dirname}/../certs/access`;
const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex');
const iv = Buffer.alloc(16);
const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv);
let decryptedBody = decipher.update(token);
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
return decryptedBody;
}
const cryptoPath = `${__dirname}/../certs/access`;
const cryptoOptions = {
private_key: fs.readFileSync(`${cryptoPath}/private.pem`),
hmac_secret: fs.readFileSync(`${cryptoPath}/secret.key`)
};
const privateKey = new NodeRSA(cryptoOptions.private_key, 'pkcs1-private-pem', {
environment: 'browser',
encryptionScheme: {
'hash': 'sha256',
}
});
const cryptoConfig = token.subarray(0, 0x90);
const signature = token.subarray(0x90, 0xA4);
const encryptedBody = token.subarray(0xA4);
const encryptedAESKey = cryptoConfig.subarray(0, 128);
const iv = cryptoConfig.subarray(128);
const decryptedAESKey = privateKey.decrypt(encryptedAESKey);
const decipher = crypto.createDecipheriv('aes-128-cbc', decryptedAESKey, iv);
let decryptedBody = decipher.update(encryptedBody);
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(decryptedBody);
const calculatedSignature = hmac.digest();
if (calculatedSignature !== signature) {
console.log('Token signature did not match');
return null;
}
return decryptedBody;
}
function unpackToken(token) {
if (token.length <= 32) {
return {
pid: token.readUInt32LE(0x0),
date: token.readBigUInt64LE(0x2)
};
}
return {
system_type: token.readUInt8(0x0),
token_type: token.readUInt8(0x1),
pid: token.readUInt32LE(0x2),
title_id: token.readBigUInt64LE(0x6),
date: token.readBigUInt64LE(0xE)
};
}
module.exports = {
nintendoPasswordHash,
generateRandomInt,
generateToken,
decryptToken,
unpackToken
const crypto = require('crypto');
const NodeRSA = require('node-rsa');
const fs = require('fs-extra');
const url = require('url');
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;
}
function generateRandomInt(length = 4) {
return Math.floor(Math.pow(10, length-1) + Math.random() * 9 * Math.pow(10, length-1));
}
function generateToken(cryptoOptions, tokenOptions) {
// Access and refresh tokens use a different format since they must be much smaller
if ([0x1, 0x2].includes(tokenOptions.token_type)) {
const cryptoPath = `${__dirname}/../certs/access`;
const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex');
const dataBuffer = Buffer.alloc(4 + 8);
dataBuffer.writeUInt32LE(tokenOptions.pid, 0x0);
dataBuffer.writeBigUInt64LE(tokenOptions.date, 0x4);
const iv = Buffer.alloc(16);
const cipher = crypto.createCipheriv('aes-128-cbc', aesKey, iv);
let encryptedBody = cipher.update(dataBuffer);
encryptedBody = Buffer.concat([encryptedBody, cipher.final()]);
return encryptedBody.toString('base64');
}
const publicKey = new NodeRSA(cryptoOptions.public_key, 'pkcs8-public-pem', {
environment: 'browser',
encryptionScheme: {
'hash': 'sha256',
}
});
// Create the buffer containing the token data
const dataBuffer = Buffer.alloc(1 + 1 + 4 + 8 + 8);
dataBuffer.writeUInt8(tokenOptions.system_type, 0x0);
dataBuffer.writeUInt8(tokenOptions.token_type, 0x1);
dataBuffer.writeUInt32LE(tokenOptions.pid, 0x2);
dataBuffer.writeBigUInt64LE(tokenOptions.title_id, 0x6);
dataBuffer.writeBigUInt64LE(tokenOptions.date, 0xE);
// Calculate the signature of the token body
const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(dataBuffer);
const signature = hmac.digest();
// Generate random AES key and IV
const key = crypto.randomBytes(16);
const iv = crypto.randomBytes(16);
// Encrypt the token body with AES
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
let encryptedBody = cipher.update(dataBuffer);
encryptedBody = Buffer.concat([encryptedBody, cipher.final()]);
// Encrypt the AES key with RSA public key
const encryptedKey = publicKey.encrypt(key);
// Create crypto config token section
const cryptoConfig = Buffer.concat([
encryptedKey,
iv
]);
// Build the token
const token = Buffer.concat([
cryptoConfig,
signature,
encryptedBody
]);
return token.toString('base64'); // Encode to base64 for transport
}
function decryptToken(token) {
// Access and refresh tokens use a different format since they must be much smaller
// Assume a small length means access or refresh token
if (token.length <= 32) {
const cryptoPath = `${__dirname}/../certs/access`;
const aesKey = Buffer.from(fs.readFileSync(`${cryptoPath}/aes.key`, { encoding: 'utf8' }), 'hex');
const iv = Buffer.alloc(16);
const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv);
let decryptedBody = decipher.update(token);
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
return decryptedBody;
}
const cryptoPath = `${__dirname}/../certs/access`;
const cryptoOptions = {
private_key: fs.readFileSync(`${cryptoPath}/private.pem`),
hmac_secret: fs.readFileSync(`${cryptoPath}/secret.key`)
};
const privateKey = new NodeRSA(cryptoOptions.private_key, 'pkcs1-private-pem', {
environment: 'browser',
encryptionScheme: {
'hash': 'sha256',
}
});
const cryptoConfig = token.subarray(0, 0x90);
const signature = token.subarray(0x90, 0xA4);
const encryptedBody = token.subarray(0xA4);
const encryptedAESKey = cryptoConfig.subarray(0, 128);
const iv = cryptoConfig.subarray(128);
const decryptedAESKey = privateKey.decrypt(encryptedAESKey);
const decipher = crypto.createDecipheriv('aes-128-cbc', decryptedAESKey, iv);
let decryptedBody = decipher.update(encryptedBody);
decryptedBody = Buffer.concat([decryptedBody, decipher.final()]);
const hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(decryptedBody);
const calculatedSignature = hmac.digest();
if (calculatedSignature !== signature) {
console.log('Token signature did not match');
return null;
}
return decryptedBody;
}
function unpackToken(token) {
if (token.length <= 32) {
return {
pid: token.readUInt32LE(0x0),
date: token.readBigUInt64LE(0x2)
};
}
return {
system_type: token.readUInt8(0x0),
token_type: token.readUInt8(0x1),
pid: token.readUInt32LE(0x2),
title_id: token.readBigUInt64LE(0x6),
date: token.readBigUInt64LE(0xE)
};
}
function fullUrl(request) {
return url.format({
protocol: request.protocol,
host: request.get('host'),
pathname: request.originalUrl
});
}
module.exports = {
nintendoPasswordHash,
generateRandomInt,
generateToken,
decryptToken,
unpackToken,
fullUrl
};