feat(nasc): use NASC keyhash for service tokens

This commit is contained in:
Jonathan Barrow 2026-01-05 14:19:00 -05:00
parent dbac688cfa
commit d174cf4dbb
No known key found for this signature in database
GPG Key ID: 2A7DAA6DED5A77E5
4 changed files with 230 additions and 24 deletions

View File

@ -6,10 +6,10 @@ import { connection as databaseConnection } from '@/database';
import NintendoCertificate from '@/nintendo-certificate';
import { LOG_ERROR } from '@/logger';
import type express from 'express';
import type { NASCRequestParams } from '@/types/services/nasc/request-params';
import type { NASCACRequestParams } from '@/types/services/nasc/ac-request-params';
async function NASCMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
const requestParams: NASCRequestParams = request.body;
const requestParams: NASCACRequestParams = request.body;
if (!requestParams.action ||
!requestParams.fcdcert ||
@ -37,15 +37,15 @@ async function NASCMiddleware(request: express.Request, response: express.Respon
let pidHmac = '';
let password = '';
if (requestParams.userid) {
if ('userid' in requestParams) {
pid = Number(nintendoBase64Decode(requestParams.userid).toString());
}
if (requestParams.uidhmac) {
if ('uidhmac' in requestParams) {
pidHmac = nintendoBase64Decode(requestParams.uidhmac).toString();
}
if (requestParams.passwd) {
if ('passwd' in requestParams) {
password = nintendoBase64Decode(requestParams.passwd).toString();
}

View File

@ -6,7 +6,7 @@ import { nintendoBase64Encode, nintendoBase64Decode, nascDateTime, nascError, cr
import { getServerByTitleID } from '@/database';
import { IndependentServiceToken } from '@/models/independent_service_token';
import { NEXToken } from '@/models/nex_token';
import type { NASCRequestParams } from '@/types/services/nasc/request-params';
import type { NASCACRequestParams, NASCLoginACRequestParams, NASCServiceTokenACRequestParams } from '@/types/services/nasc/ac-request-params';
import type { HydratedServerDocument } from '@/types/mongoose/server';
const router = express.Router();
@ -17,7 +17,7 @@ const router = express.Router();
* Description: Gets a NEX server address and token
*/
router.post('/', async (request: express.Request, response: express.Response): Promise<void> => {
const requestParams: NASCRequestParams = request.body;
const requestParams: NASCACRequestParams = request.body;
const action = nintendoBase64Decode(requestParams.action).toString();
const titleID = nintendoBase64Decode(requestParams.titleid).toString();
const gameServerID = nintendoBase64Decode(requestParams.gameid).toString();
@ -63,17 +63,18 @@ router.post('/', async (request: express.Request, response: express.Response): P
switch (action) {
case 'LOGIN':
responseData = await processLoginRequest(server, nexAccount.pid, titleID);
responseData = await processLoginRequest(server, nexAccount.pid, requestParams as NASCLoginACRequestParams); // TODO - Remove this "as" with field checking
break;
case 'SVCLOC':
responseData = await processServiceTokenRequest(server, nexAccount.pid, titleID);
responseData = await processServiceTokenRequest(server, nexAccount.pid, requestParams as NASCServiceTokenACRequestParams); // TODO - Remove this "as" with field checking
break;
}
response.status(200).send(responseData.toString());
});
async function processLoginRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
async function processLoginRequest(server: HydratedServerDocument, pid: number, requestParams: NASCLoginACRequestParams): Promise<URLSearchParams> {
const titleID = nintendoBase64Decode(requestParams.titleid).toString();
const nexToken = await NEXToken.create({
token: nintendoBase64Encode(crypto.randomBytes(112)),
game_server_id: server.game_server_id,
@ -96,7 +97,8 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number,
});
}
async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, requestParams: NASCServiceTokenACRequestParams): Promise<URLSearchParams> {
const titleID = nintendoBase64Decode(requestParams.titleid).toString();
const serviceTokenOptions = {
pid: pid,
title_id: titleID,
@ -106,7 +108,7 @@ async function processServiceTokenRequest(server: HydratedServerDocument, pid: n
const serviceToken = await IndependentServiceToken.create({
token: nintendoBase64Encode(createServiceToken(server, serviceTokenOptions)),
client_id: server.game_server_id,
client_id: nintendoBase64Decode(requestParams.keyhash).toString(),
title_id: serviceTokenOptions.title_id,
pid: serviceTokenOptions.pid,
info: {

View File

@ -0,0 +1,216 @@
/**
* Çommon request parameters found on all NASC `ac` requests.
* All fields are base64 encoded using Nintendo's custom alphabet:
* '+' -> '.', '/' -> '-', '=' -> '*'
*/
export interface NASCCommonACRequestParams {
/**
* Game server ID (`%08X`). This is the same as the `X-GameId` header.
* Derived from the games default title ID (usually the Japanese title ID)
*/
gameid: string;
/**
* Major and minor SDK version (`%03d%03d`). Always `000000`
*/
sdkver: string;
/**
* Title ID (`%016X`)
*/
titleid: string;
/**
* Product code. See https://3dsdb.com/
*/
gamecd: string;
/**
* Title version (`%04X`)
*/
gamever: string;
/**
* Game type
*
* - 0 = System
* - 1 = Digital
* - 2 = Cartridge
*/
mediatype: string;
/**
* Unique ROM (game) ID.
* Only present if the media type is 2 (cartridge)
*/
romid?: string;
/**
* Product maker (company code)
*/
makercd: string;
/**
* Unit code
*
* - 0 = NDS
* - 1 = Wii
* - 2 = 3DS
*/
unitcd: string;
/**
* Device MAC address
*/
macadr: string;
/**
* BSSID of active wifi network
*/
bssid: string;
/**
* Information about the used Wi-Fi access point in the format `AA:BBBBBBBBBB`.
* Example `01:0000000000`. `AA` is the AP slot. `BBBBBBBBBB` comes from either `ACU_GetNZoneApNumService` or `ACU_GetConnectingHotspotSubset` based on the result from `ACU_GetWifiStatus`
*/
apinfo: string;
/**
* LocalFriendCodeSeed_B
*/
fcdcert: string;
/**
* Device name (UTF-16-LE)
*/
devname: string;
/**
* Environment (`L1` for production)
*/
servertype: string;
/**
* FPD version (`%04X`). This is also included in the user agent.
*/
fpdver: string;
/**
* Current device time (`%y%m%d%H%M%S`)
*/
devtime: string;
/**
* Language code (`%02X`)
*/
lang: string;
/**
* Region code (`%02X`)
*/
region: string;
/**
* Serial number
*/
csnum: string;
/**
* The type of action the console wishes to perform:
*
* - LOGIN = Register new game server account or login to existing on
* - SVCLOC = Request service token
* - nzchk = Unknown, but seems related to Nintendo Zone
* - parse = Unknown
* - message = Unknown
*/
action: string;
}
/**
* Request parameters for when a console wants to register a new game server account.
* See also NASCCommonRequestParams.
* All fields are base64 encoded using Nintendo's custom alphabet:
* '+' -> '.', '/' -> '-', '=' -> '*'
*/
export interface NASCRegistrationACRequestParams extends NASCCommonACRequestParams {
/**
* Game server account password the console wishes to use for the new account.
* Can be any character between `\x21-\x5B` and `\x5D-\x7D`. Always 16 characters long
*/
passwd: string;
/**
* Nickname provided by game.
* Usually unused, leftover from the Wii
*/
ingamesn: string;
}
/**
* Request parameters for when a console wants to login to an existing game server account.
* See also NASCCommonRequestParams.
* All fields are base64 encoded using Nintendo's custom alphabet:
* '+' -> '.', '/' -> '-', '=' -> '*'
*/
export interface NASCLoginACRequestParams extends NASCCommonACRequestParams {
/**
* Hash of the user ID
*/
uidhmac: string;
/**
* Game server account user ID/username.
* Always the NEX account PID on 3DS
*/
userid: string;
/**
* Nickname provided by game.
* Usually unused, leftover from the Wii
*/
ingamesn: string;
}
/**
* Request parameters for when a console wants to request a service token.
* See also NASCCommonRequestParams.
* All fields are base64 encoded using Nintendo's custom alphabet:
* '+' -> '.', '/' -> '-', '=' -> '*'
*/
export interface NASCServiceTokenACRequestParams extends NASCCommonACRequestParams {
/**
* Hash of the user ID
*/
uidhmac: string;
/**
* Game server account user ID/username.
* Always the NEX account PID on 3DS
*/
userid: string;
/**
* Unique hash assigned to each game, regardless of title ID.
* Analogous to the NNAS client ID
*/
keyhash: string;
/**
* Service request type. Changes the `svchost` response field.
* This is likely a remnant from the original NAS/NASWII servers.
*
* - 0000 = "n/a"
* - 9001 = "dls1.nintendowifi.net"
*/
svc: string;
}
/**
* Union type representing all possible NASC request parameter types.
* The specific type used depends on the `action` field value:
*
* - `action: 'LOGIN'` = `NASCRegistrationRequestParams` or `NASCLoginRequestParams`
* - `action: 'SVCLOC'` = `NASCServiceTokenRequestParams`
*/
export type NASCACRequestParams = NASCRegistrationACRequestParams | NASCLoginACRequestParams | NASCServiceTokenACRequestParams;

View File

@ -1,12 +0,0 @@
export interface NASCRequestParams {
action: string;
fcdcert: string;
csnum: string;
macadr: string;
titleid: string;
servertype: string;
gameid: string;
userid?: string;
uidhmac?: string;
passwd?: string;
}