diff --git a/src/config-manager.ts b/src/config-manager.ts index 3499f05..9cc14e2 100644 --- a/src/config-manager.ts +++ b/src/config-manager.ts @@ -106,7 +106,8 @@ export const config: Config = { forum_url: process.env.PN_ACT_CONFIG_DISCOURSE_FORUM_URL || '', api_key: process.env.PN_ACT_CONFIG_DISCOURSE_API_KEY || '', api_username: process.env.PN_ACT_CONFIG_DISCOURSE_API_USERNAME || '' - } + }, + uidhmac_key: process.env.PN_ACT_CONFIG_UIDHMAC_KEY || '' }; if (process.env.PN_ACT_CONFIG_STRIPE_SECRET_KEY) { @@ -272,6 +273,11 @@ if (!config.grpc.port) { configValid = false; } +if (!config.uidhmac_key) { + LOG_ERROR('Failed to find NASC uidhmac key. Set the PN_ACT_CONFIG_UIDHMAC_KEY environment variable'); + configValid = false; +} + if (!config.stripe?.secret_key) { LOG_WARN('Failed to find Stripe api key! If a PNID is deleted with an active subscription, the subscription will *NOT* be canceled! Set the PN_ACT_CONFIG_STRIPE_SECRET_KEY environment variable to enable'); } diff --git a/src/middleware/nasc.ts b/src/middleware/nasc.ts index 944e9e8..db8ff63 100644 --- a/src/middleware/nasc.ts +++ b/src/middleware/nasc.ts @@ -34,7 +34,7 @@ async function NASCMiddleware(request: express.Request, response: express.Respon const fcdcertHash = crypto.createHash('sha256').update(fcdcert).digest('base64'); let pid = 0; // * Real PIDs are always positive and non-zero - let pidHmac = ''; + let uidhmac = ''; let password = ''; if (requestParams.userid) { @@ -42,7 +42,7 @@ async function NASCMiddleware(request: express.Request, response: express.Respon } if (requestParams.uidhmac) { - pidHmac = nintendoBase64Decode(requestParams.uidhmac).toString(); + uidhmac = nintendoBase64Decode(requestParams.uidhmac).toString(); } if (requestParams.passwd) { @@ -102,6 +102,11 @@ async function NASCMiddleware(request: express.Request, response: express.Respon response.status(200).send(nascError('102').toString()); return; } + + if (!uidhmac || nexAccount.uidhmac !== uidhmac) { + response.status(200).send(nascError('122').toString()); + return; + } } let device = await Device.findOne({ @@ -160,7 +165,7 @@ async function NASCMiddleware(request: express.Request, response: express.Respon } if (titleID === '0004013000003202') { - if (password && !pid && !pidHmac) { + if (password && !pid && !uidhmac) { // * Register new user const session = await databaseConnection().startSession(); @@ -174,6 +179,7 @@ async function NASCMiddleware(request: express.Request, response: express.Respon }); await nexAccount.generatePID(); + nexAccount.generateUIDHMAC(); await nexAccount.save({ session }); diff --git a/src/models/nex-account.ts b/src/models/nex-account.ts index 6f08d83..5394225 100644 --- a/src/models/nex-account.ts +++ b/src/models/nex-account.ts @@ -1,5 +1,7 @@ +import crypto from 'node:crypto'; import { Schema, model } from 'mongoose'; import uniqueValidator from 'mongoose-unique-validator'; +import { config } from '@/config-manager'; import type { INEXAccount, INEXAccountMethods, NEXAccountModel } from '@/types/mongoose/nex-account'; const NEXAccountSchema = new Schema({ @@ -25,7 +27,8 @@ const NEXAccountSchema = new Schema?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}'; + const hmac = crypto.createHmac('sha256', config.uidhmac_key).update(this.pid.toString()); + const hash = hmac.digest(); + + this.uidhmac = Array.from(hash.subarray(0, 8), byte => CHAR_SET[byte % CHAR_SET.length]).join(''); +}); + export const NEXAccount = model('NEXAccount', NEXAccountSchema); diff --git a/src/services/api/index.ts b/src/services/api/index.ts index 9420158..e1ef48c 100644 --- a/src/services/api/index.ts +++ b/src/services/api/index.ts @@ -23,6 +23,7 @@ api.use('/v1/login', V1.LOGIN); api.use('/v1/register', V1.REGISTER); api.use('/v1/reset-password', V1.RESET_PASSWORD); api.use('/v1/user', V1.USER); +api.use('/v1/repair-uidhmac', V1.REPAIR_UIDHMAC); // * Main router for endpoints const router = express.Router(); diff --git a/src/services/api/routes/index.ts b/src/services/api/routes/index.ts index de8208b..9f4654f 100644 --- a/src/services/api/routes/index.ts +++ b/src/services/api/routes/index.ts @@ -5,6 +5,7 @@ import login_v1 from '@/services/api/routes/v1/login'; import register_v1 from '@/services/api/routes/v1/register'; import resetPassword_v1 from '@/services/api/routes/v1/resetPassword'; import user_v1 from '@/services/api/routes/v1/user'; +import repair_uidhmac_v1 from '@/services/api/routes/v1/repair-uidhmac'; export const V1 = { CONNECTIONS: connections_v1, @@ -13,5 +14,6 @@ export const V1 = { LOGIN: login_v1, REGISTER: register_v1, RESET_PASSWORD: resetPassword_v1, - USER: user_v1 + USER: user_v1, + REPAIR_UIDHMAC: repair_uidhmac_v1 }; diff --git a/src/services/api/routes/v1/repair-uidhmac.ts b/src/services/api/routes/v1/repair-uidhmac.ts new file mode 100644 index 0000000..03776f2 --- /dev/null +++ b/src/services/api/routes/v1/repair-uidhmac.ts @@ -0,0 +1,77 @@ +import express from 'express'; +import { LOG_ERROR } from '@/logger'; +import { NEXAccount } from '@/models/nex-account'; + +const router = express.Router(); + +/** + * [POST] + * Implementation of: https://api.pretendo.cc/v1/repair-uidhmac + * Description: Creates a new user PNID + */ +router.post('/', async (request: express.Request, response: express.Response): Promise => { + const pid = request.body.pid?.trim(); // * This has to be forwarded since this request comes from the websites server + const nexPassword = request.body.password?.trim(); + + if (!pid || pid === '' || !/^\d+$/.test(pid)) { + response.status(400).json({ + app: 'api', + status: 400, + error: 'Invalid PID format' + }); + + return; + } + + if (!nexPassword || !/^[\x21-\x5B\x5D-\x7D]{16}$/.test(nexPassword)) { + response.status(400).json({ + app: 'api', + status: 400, + error: 'Invalid NEX password format' + }); + + return; + } + + try { + const nexAccount = await NEXAccount.findOne({ + pid: parseInt(pid), + password: nexPassword + }); + + if (!nexAccount) { + response.json({ + app: 'api', + status: 400, + error: 'Invalid NEX account' + }); + + return; + } + + nexAccount.generateUIDHMAC(); + + response.json({ + app: 'api', + status: 200, + data: { + uidhmac: nexAccount.uidhmac + } + }); + } catch (error: any) { + LOG_ERROR('[POST] /v1/repair-uidhmac: ' + error); + if (error.stack) { + console.error(error.stack); + } + + response.status(500).json({ + app: 'api', + status: 500, + error: 'Internal server error' + }); + + return; + } +}); + +export default router; diff --git a/src/types/common/config.ts b/src/types/common/config.ts index 0eab22f..f412c55 100644 --- a/src/types/common/config.ts +++ b/src/types/common/config.ts @@ -75,4 +75,5 @@ export interface Config { api_key: string; api_username: string; }; + uidhmac_key: string; } diff --git a/src/types/mongoose/nex-account.ts b/src/types/mongoose/nex-account.ts index f6596be..07aa393 100644 --- a/src/types/mongoose/nex-account.ts +++ b/src/types/mongoose/nex-account.ts @@ -16,11 +16,13 @@ export interface INEXAccount { access_level: ACCESS_LEVEL; server_access_level: string; friend_code: string; + uidhmac: string; } export interface INEXAccountMethods { generatePID(): Promise; generatePassword(): void; + generateUIDHMAC(): void; } interface INEXAccountQueryHelpers {}