This commit is contained in:
Jonathan Barrow 2026-04-25 22:38:34 -04:00 committed by GitHub
commit 74e1cc5726
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 112 additions and 6 deletions

View File

@ -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');
}

View File

@ -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 });

View File

@ -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<INEXAccount, NEXAccountModel, INEXAccountMethods>({
@ -25,7 +27,8 @@ const NEXAccountSchema = new Schema<INEXAccount, NEXAccountModel, INEXAccountMet
type: String,
default: 'prod' // * everyone is in production by default
},
friend_code: String
friend_code: String,
uidhmac: String
});
NEXAccountSchema.plugin(uniqueValidator, { message: '{PATH} already in use.' });
@ -74,4 +77,12 @@ NEXAccountSchema.method('generatePassword', function generatePassword(): void {
this.password = output.join('');
});
NEXAccountSchema.method('generateUIDHMAC', function generateUIDHMAC(): void {
const CHAR_SET = '!"#$%&\'()*+,-./0123456789:;<=>?@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<INEXAccount, NEXAccountModel>('NEXAccount', NEXAccountSchema);

View File

@ -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();

View File

@ -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
};

View File

@ -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<void> => {
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;

View File

@ -75,4 +75,5 @@ export interface Config {
api_key: string;
api_username: string;
};
uidhmac_key: string;
}

View File

@ -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<void>;
generatePassword(): void;
generateUIDHMAC(): void;
}
interface INEXAccountQueryHelpers {}