From 9ede5b5c1eac10672f1dad43a7ed281b4006d305 Mon Sep 17 00:00:00 2001 From: Jonathan Barrow Date: Mon, 15 Dec 2025 13:04:19 -0500 Subject: [PATCH 1/4] feat(nasc): add uihmac repair adds the ability to remake the users uidhmac when needed --- src/config-manager.ts | 8 +- src/middleware/nasc.ts | 12 ++- src/models/nex-account.ts | 15 +++- src/services/api/index.ts | 1 + src/services/api/routes/index.ts | 4 +- src/services/api/routes/v1/repair-uidhmac.ts | 77 ++++++++++++++++++++ src/types/common/config.ts | 1 + src/types/mongoose/nex-account.ts | 2 + 8 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 src/services/api/routes/v1/repair-uidhmac.ts diff --git a/src/config-manager.ts b/src/config-manager.ts index e651a69..94698e5 100644 --- a/src/config-manager.ts +++ b/src/config-manager.ts @@ -102,7 +102,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) { @@ -268,6 +269,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..7ac97c6 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('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..7661efb --- /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 || !/^[0-9A-Za-z]{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/register: ' + 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 66a77c7..9dce89f 100644 --- a/src/types/common/config.ts +++ b/src/types/common/config.ts @@ -72,4 +72,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 {} From e8c5008336e7f529e1ae32166053ab5c840d1611 Mon Sep 17 00:00:00 2001 From: Jonathan Barrow Date: Sat, 27 Dec 2025 22:00:59 -0500 Subject: [PATCH 2/4] fix(api): correct error log message route in /v1/repair-uidhmac --- src/services/api/routes/v1/repair-uidhmac.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/api/routes/v1/repair-uidhmac.ts b/src/services/api/routes/v1/repair-uidhmac.ts index 7661efb..8f09ee1 100644 --- a/src/services/api/routes/v1/repair-uidhmac.ts +++ b/src/services/api/routes/v1/repair-uidhmac.ts @@ -59,7 +59,7 @@ router.post('/', async (request: express.Request, response: express.Response): P } }); } catch (error: any) { - LOG_ERROR('[POST] /v1/register: ' + error); + LOG_ERROR('[POST] /v1/repair-uidhmac: ' + error); if (error.stack) { console.error(error.stack); } From 2b3cd45de17f816c69f3b87766205b5e67eb9946 Mon Sep 17 00:00:00 2001 From: Jonathan Barrow Date: Sat, 27 Dec 2025 22:01:59 -0500 Subject: [PATCH 3/4] fix(api): correct NEX password regex pattern in /v1/repair-uidhmac --- src/services/api/routes/v1/repair-uidhmac.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/api/routes/v1/repair-uidhmac.ts b/src/services/api/routes/v1/repair-uidhmac.ts index 8f09ee1..03776f2 100644 --- a/src/services/api/routes/v1/repair-uidhmac.ts +++ b/src/services/api/routes/v1/repair-uidhmac.ts @@ -23,7 +23,7 @@ router.post('/', async (request: express.Request, response: express.Response): P return; } - if (!nexPassword || !/^[0-9A-Za-z]{16}$/.test(nexPassword)) { + if (!nexPassword || !/^[\x21-\x5B\x5D-\x7D]{16}$/.test(nexPassword)) { response.status(400).json({ app: 'api', status: 400, From f44c276834bb438203f986a74a512e803579e60f Mon Sep 17 00:00:00 2001 From: Jonathan Barrow Date: Sat, 27 Dec 2025 22:05:13 -0500 Subject: [PATCH 4/4] feat(nasc): update uidhmac generation algorithm to allow for more characters --- src/models/nex-account.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/models/nex-account.ts b/src/models/nex-account.ts index 7ac97c6..5394225 100644 --- a/src/models/nex-account.ts +++ b/src/models/nex-account.ts @@ -78,13 +78,11 @@ NEXAccountSchema.method('generatePassword', function generatePassword(): void { }); NEXAccountSchema.method('generateUIDHMAC', function generateUIDHMAC(): void { - const pidByteArray = Buffer.alloc(4); - pidByteArray.writeUInt32LE(this.pid); + const CHAR_SET = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}'; + const hmac = crypto.createHmac('sha256', config.uidhmac_key).update(this.pid.toString()); + const hash = hmac.digest(); - const mac = crypto.createHmac('md5', config.uidhmac_key); - mac.update(pidByteArray); - - this.uidhmac = mac.digest('hex'); + this.uidhmac = Array.from(hash.subarray(0, 8), byte => CHAR_SET[byte % CHAR_SET.length]).join(''); }); export const NEXAccount = model('NEXAccount', NEXAccountSchema);