From beb64e10a2fac8aa4fe93b909dcebdc81d3c0083 Mon Sep 17 00:00:00 2001 From: RedDucks Date: Tue, 10 Apr 2018 14:03:13 -0400 Subject: [PATCH] Finally added proper JWT support --- .gitignore | 6 +- README.md | 6 ++ certs/jwt/nex/git-noremove | 1 + certs/jwt/service/git-noremove | 1 + db.js | 2 +- example.config.js | 49 ++++++++++ example.config.json | 12 ++- helpers.js | 17 ++++ mailer.js | 2 +- package-lock.json | 113 ++++++++++++++++++----- package.json | 3 +- routes/people/index.js | 12 +-- routes/provider/index.js | 158 +++++++++++++++++++++++++++++---- server.js | 2 +- 14 files changed, 334 insertions(+), 50 deletions(-) create mode 100644 certs/jwt/nex/git-noremove create mode 100644 certs/jwt/service/git-noremove create mode 100644 example.config.js diff --git a/.gitignore b/.gitignore index 01f9187..77b2dc8 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,8 @@ typings/ .env # custom -config.json \ No newline at end of file +config.js +certs/jwt/service/private.pem +certs/jwt/service/public.pem +certs/jwt/nex/private.pem +certs/jwt/nex/public.pem \ No newline at end of file diff --git a/README.md b/README.md index 486c8c4..aff5c3f 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,10 @@ This is the PN account server, which replaces the official NN account server acc - [POST] https://account.nintendo.net/v1/api/support/validate/email - [GET] https://id.nintendo.net/account/email-confirmation +# Currently implemented nex servers +- supermariomaker +- friends + ### Footnotes @@ -58,3 +62,5 @@ This is the PN account server, which replaces the official NN account server acc 2 There are MANY values here that Nintendo seems to generate on their servers. I have no idea what some of these values mean and where/how they are used. Because of this I am unsure how to properly generate these values, and I am using placeholder values instead! ([see here for an example of what the return for an account is ](https://github.com/RedDuckss/csms/blob/master/OFFICIAL_SCHEMA.md#grab-profile)) The entire `accounts` section at the beginning is new, and not sent by the registration request. It seems to have something to do with eShop accounts, though I don't know what exactly. I went to the eShop and it never even makes a request to that endpoint so the eShop isn't using that data, yet it's the only "account" mentioned. I am also unsure as to what `active_flag` is used for. There are also several `id` fields that seem completely pointless, like the `id` field in the `email` section and how the `mii` has it's own `id`, as do each of the different `mii_image` fields. [↩](#a3) + +The EULAs need to be changed, as they are currently stock Nintendo's. diff --git a/certs/jwt/nex/git-noremove b/certs/jwt/nex/git-noremove new file mode 100644 index 0000000..e40558b --- /dev/null +++ b/certs/jwt/nex/git-noremove @@ -0,0 +1 @@ +This is where you place the certs \ No newline at end of file diff --git a/certs/jwt/service/git-noremove b/certs/jwt/service/git-noremove new file mode 100644 index 0000000..e40558b --- /dev/null +++ b/certs/jwt/service/git-noremove @@ -0,0 +1 @@ +This is where you place the certs \ No newline at end of file diff --git a/db.js b/db.js index d453315..e2b35df 100644 --- a/db.js +++ b/db.js @@ -1,5 +1,5 @@ const mongoist = require('mongoist'); -const config = require('./config.json'); +const config = require('./config'); const user_database_collection_name = 'users'; const database = config.mongo.database; const hostname = config.mongo.hostname; diff --git a/example.config.js b/example.config.js new file mode 100644 index 0000000..7c50637 --- /dev/null +++ b/example.config.js @@ -0,0 +1,49 @@ +const fs = require('fs'); + +module.exports = { + JWT : { + SERVICE: { + PASSPHRASE: 'service_token_rsa_password', + PRIVATE: fs.readFileSync('./certs/jwt/service/private.pem'), + PUBLIC: fs.readFileSync('./certs/jwt/service/public.pem'), + }, + NEX: { + PASSPHRASE: 'nex_rsa_password', + PRIVATE: fs.readFileSync('./certs/jwt/nex/private.pem'), + PUBLIC: fs.readFileSync('./certs/jwt/nex/public.pem'), + } + }, + email: { + address: 'email@provider.com', + password: 'password' + }, + mongo: { + database: 'database_name', + hostname: 'localhost', + port: 27017, + use_authentication: true, + authentication: { + username: 'username', + password: 'password', + authentication_database: 'admin' + } + + }, + http: { + port: 80 + }, + nex_servers: { + secure_auth: { + ip: 'ip', + port: 'port' + }, + friends: { + ip: 'ip', + port: 'port' + }, + supermariomaker: { + ip: 'ip', + port: 'port' + } + } +}; \ No newline at end of file diff --git a/example.config.json b/example.config.json index 1acce34..d6e6bef 100644 --- a/example.config.json +++ b/example.config.json @@ -16,5 +16,15 @@ }, "http": { "port": 8080 - } + }, + "nex_servers": { + "friends": { + "ip": "ip", + "port": "port" + }, + "supermariomaker": { + "ip": "ip", + "port": "port" + } + } } \ No newline at end of file diff --git a/helpers.js b/helpers.js index 9769ec1..45ba2a7 100644 --- a/helpers.js +++ b/helpers.js @@ -5,6 +5,22 @@ const crypto = require('crypto'); const constants = require('./constants'); const database = require('./db'); +function genNEXPassoword() { + const output = []; + const 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); + } + + while (output.length < 16) { + output.push(character()); + } + + return output.join(''); +} + async function generatePID() { // Quick, dirty fix for PIDs const pid = Math.floor(Math.random() * (4294967295 - 1000000000) + 1000000000); @@ -236,6 +252,7 @@ function mapUser(user) { module.exports = { + genNEXPassoword: genNEXPassoword, generatePID: generatePID, generateRandID: generateRandID, generateNintendoHashedPWrd: generateNintendoHashedPWrd, diff --git a/mailer.js b/mailer.js index 0dcf10c..090ee37 100644 --- a/mailer.js +++ b/mailer.js @@ -1,5 +1,5 @@ const nodemailer = require('nodemailer'); -const config = require('./config.json'); +const config = require('./config'); const transporter = nodemailer.createTransport({ service: 'gmail', diff --git a/package-lock.json b/package-lock.json index 02fa6b0..55f91bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -132,6 +132,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", + "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" + }, "basic-auth": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", @@ -193,6 +198,11 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-shims": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", @@ -425,6 +435,15 @@ "resolved": "https://registry.npmjs.org/each-series/-/each-series-1.0.0.tgz", "integrity": "sha1-+Ibmxm39sl7x/nNWQUbuXLR4r8s=" }, + "ecdsa-sig-formatter": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", + "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", + "requires": { + "base64url": "2.0.0", + "safe-buffer": "5.1.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -494,22 +513,6 @@ "text-table": "0.2.0" } }, - "eslint-plugin-eslint-snake-case": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-snake-case/-/eslint-plugin-eslint-snake-case-0.0.6.tgz", - "integrity": "sha1-K4pMb9Terzy8LgOizXm1DsiULRM=", - "requires": { - "requireindex": "1.1.0" - } - }, - "eslint-plugin-more-naming-conventions": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-more-naming-conventions/-/eslint-plugin-more-naming-conventions-1.0.1.tgz", - "integrity": "sha512-K1Lw0dMUOIYeY0qe7SYSjHpHW8aZfxaVpXoFli672ywpRq05R1STmpxfhXyVxrphbsNt/SgBCaJPXl7pqSpUfg==", - "requires": { - "eslint": "4.18.2" - } - }, "eslint-scope": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", @@ -960,6 +963,44 @@ "graceful-fs": "4.1.11" } }, + "jsonwebtoken": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.2.0.tgz", + "integrity": "sha512-1Wxh8ADP3cNyPl8tZ95WtraHXCAyXupgc0AhMHjU9er98BV+UcKsO7OJUjfhIu0Uba9A40n1oSx8dbJYrm+EoQ==", + "requires": { + "jws": "3.1.4", + "lodash.includes": "4.3.0", + "lodash.isboolean": "3.0.3", + "lodash.isinteger": "4.0.4", + "lodash.isnumber": "3.0.3", + "lodash.isplainobject": "4.0.6", + "lodash.isstring": "4.0.1", + "lodash.once": "4.1.1", + "ms": "2.1.1", + "xtend": "4.0.1" + } + }, + "jwa": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", + "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", + "requires": { + "base64url": "2.0.0", + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.9", + "safe-buffer": "5.1.1" + } + }, + "jws": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", + "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", + "requires": { + "base64url": "2.0.0", + "jwa": "1.1.5", + "safe-buffer": "5.1.1" + } + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -974,6 +1015,41 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "long": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", @@ -1368,11 +1444,6 @@ } } }, - "requireindex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", - "integrity": "sha1-5UBLgVV+91225JxacgBIk/4D4WI=" - }, "resolve-from": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", diff --git a/package.json b/package.json index e69d31d..71b7f79 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "server.js", "scripts": { - "lint": "./node_modules/.bin/eslint .", + "lint": "./node_modules/.bin/eslint .", "test": "echo \"Error: no test specified\" && exit 1", "start": "NODE_ENV=production node server.js", "start:dev": "NODE_ENV=development node server.js" @@ -21,6 +21,7 @@ "express-subdomain": "^1.0.5", "fs-extra": "^5.0.0", "json2xml": "^0.1.3", + "jsonwebtoken": "^8.2.0", "moment": "^2.20.1", "moment-timezone": "^0.5.14", "mongoist": "^1.5.1", diff --git a/routes/people/index.js b/routes/people/index.js index 4dc53a4..9e6be23 100644 --- a/routes/people/index.js +++ b/routes/people/index.js @@ -4,6 +4,7 @@ const json2xml = require('json2xml'); const bcrypt = require('bcryptjs'); const moment = require('moment'); const puid = require('puid'); +const mongo = require('mongodb'); const helpers = require('../../helpers'); const constants = require('../../constants'); const database = require('../../db'); @@ -99,7 +100,7 @@ routes.post('/', new RateLimit({ attributes: [ { attribute: { - id: helpers.generateRandID(8), // THIS IS A PLACE HOLDER + id: new mongo.ObjectID(), // THIS IS A PLACE HOLDER name: 'environment', updated_by: 'USER', value: 'PROD' @@ -108,7 +109,7 @@ routes.post('/', new RateLimit({ ], domain: 'ESHOP.NINTENDO.NET', type: 'INTERNAL', - username: helpers.generateRandID(9) // THIS IS A PLACE HOLDER + username: new mongo.ObjectID() // THIS IS A PLACE HOLDER } } ], @@ -125,7 +126,7 @@ routes.post('/', new RateLimit({ pid: pid, email: { address: user_data.email, - id: helpers.generateRandID(8), // THIS IS A PLACE HOLDER + id: new mongo.ObjectID(), // THIS IS A PLACE HOLDER parent: user_data.parent, primary: user_data.primary, reachable: 'N', @@ -136,13 +137,13 @@ routes.post('/', new RateLimit({ mii: { status: 'COMPLETED', // idk man, idk data: user_data.mii.data, - id: helpers.generateRandID(10), // THIS IS A PLACE HOLDER + id: new mongo.ObjectID(), // THIS IS A PLACE HOLDER mii_hash: mii_hash, mii_images: [ { mii_image: { cached_url: constants.URL_ENDPOINTS.mii + mii_hash + '_standard.tga', - id: helpers.generateRandID(10), // THIS IS A PLACE HOLDER + id: new mongo.ObjectID(), // THIS IS A PLACE HOLDER url: constants.URL_ENDPOINTS.mii + mii_hash + '_standard.tga', type: 'standard' } @@ -169,6 +170,7 @@ routes.post('/', new RateLimit({ code: email_code, }, password: password, + nex_password: helpers.genNEXPassoword(), linked_devices: { wiiu: { serial: headers['x-nintendo-serial-number'], diff --git a/routes/provider/index.js b/routes/provider/index.js index d08aad9..037b535 100644 --- a/routes/provider/index.js +++ b/routes/provider/index.js @@ -1,22 +1,15 @@ const routes = require('express').Router(); const json2xml = require('json2xml'); +const jwt = require('jsonwebtoken'); const debug = require('../../debugger'); +const config = require('../../config'); +const constants = require('../../constants'); +const helpers = require('../../helpers'); const route_debugger = new debug('Provider Route'.green); +const gamePort = require('../../config.json').nex_servers; route_debugger.log('Loading \'provider\' API routes'); -//The game ips and ports are stored here. When the game tries to access its specific server, it will be given the respecive ip and port. -const gamePort = { - friends: { - ip: '10.0.0.225', - port: '1300' - }, - supermariomaker: { - ip: '10.0.0.225', - port: '1301' - } -}; - /** * [GET] * Replacement for: https://account.nintendo.net/v1/api/provider/service_token/@me @@ -26,10 +19,73 @@ routes.get('/service_token/@me', async (request, response) => { response.set('Content-Type', 'text/xml'); response.set('Server', 'Nintendo 3DS (http)'); response.set('X-Nintendo-Date', new Date().getTime()); + + const headers = request.headers; + + if ( + !headers['x-nintendo-client-id'] || + !headers['x-nintendo-client-secret'] || + !constants.VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] || + headers['x-nintendo-client-secret'] !== constants.VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] + ) { + const error = { + errors: { + error: { + cause: 'client_id', + code: '0004', + message: 'API application invalid or incorrect application credentials' + } + } + }; + + return response.send(json2xml(error)); + } + + if ( + !headers['authorization'] + ) { + const error = { + errors: { + error: { + cause: 'access_token', + code: '0002', + message: 'Invalid access token' + } + } + }; + + return response.send(json2xml(error)); + } + const user = await helpers.getUser(headers['authorization'].replace('Bearer ','')); + + if (!user) { + const error = { + errors: { + error: { + cause: 'access_token', + code: '0002', + message: 'Invalid access token' + } + } + }; + + return response.send(json2xml(error)); + } + + delete user.sensitive; + const token = { service_token: { - token: 'pretendo_test' + token: jwt.sign({ + data: { + type: 'service_token', + payload: user + } + }, { + key: config.JWT.SERVICE.PRIVATE, + passphrase: config.JWT.SERVICE.PASSPHRASE + }, { algorithm: 'RS256'}) } }; @@ -45,12 +101,66 @@ routes.get('/nex_token/@me', async (request, response) => { response.set('Content-Type', 'text/xml'); response.set('Server', 'Nintendo 3DS (http)'); response.set('X-Nintendo-Date', new Date().getTime()); + + const headers = request.headers; + + if ( + !headers['x-nintendo-client-id'] || + !headers['x-nintendo-client-secret'] || + !constants.VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] || + headers['x-nintendo-client-secret'] !== constants.VALID_CLIENT_ID_SECRET_PAIRS[headers['x-nintendo-client-id']] + ) { + const error = { + errors: { + error: { + cause: 'client_id', + code: '0004', + message: 'API application invalid or incorrect application credentials' + } + } + }; + + return response.send(json2xml(error)); + } + + if ( + !headers['authorization'] + ) { + const error = { + errors: { + error: { + cause: 'access_token', + code: '0002', + message: 'Invalid access token' + } + } + }; + + return response.send(json2xml(error)); + } + + const user = await helpers.getUser(headers['authorization'].replace('Bearer ','')); + + if (!user) { + const error = { + errors: { + error: { + cause: 'access_token', + code: '0002', + message: 'Invalid access token' + } + } + }; + + return response.send(json2xml(error)); + } + + const nex_password = user.sensitive.nex_password; + delete user.sensitive; let ip = null; let port = null; - console.log(request.query.game_server_id); - switch(request.query.game_server_id){ case '00003200': ip = gamePort.friends.ip; @@ -83,10 +193,22 @@ routes.get('/nex_token/@me', async (request, response) => { const token = { nex_token: { host: ip, - nex_password: 'pretendo', - pid: request.headers['authorization'].replace('Bearer ',''), + nex_password: nex_password, + pid: user.pid, port: port, - token: 'pretendo_test' + token: { + service_token: { + token: jwt.sign({ + data: { + type: 'service_token', + payload: user + } + }, { + key: config.JWT.SERVICE.PRIVATE, + passphrase: config.JWT.SERVICE.PASSPHRASE + }, { algorithm: 'RS256'}) + } + } } }; diff --git a/server.js b/server.js index 89f3454..c5bb58a 100644 --- a/server.js +++ b/server.js @@ -4,7 +4,7 @@ const table = require('cli-table'); const morgan = require('morgan'); const XMLMiddleware = require('./xml-middleware'); const debug = require('./debugger'); -const config = require('./config.json'); +const config = require('./config'); const app = express(); const router = express.Router(); const testing_env = process.env.NODE_ENV !== 'production';