From dbac688cfa57b80ada3eed97d9840c8ede189779 Mon Sep 17 00:00:00 2001 From: Jonathan Barrow Date: Sun, 4 Jan 2026 13:14:48 -0500 Subject: [PATCH] feat: give service tokens state --- src/models/independent_service_token.ts | 4 ++-- src/services/nasc/routes/ac.ts | 23 +++++++++++++++-------- src/services/nnas/routes/provider.ts | 21 ++++++++++++++------- src/types/common/service-token-options.ts | 6 ++++++ src/types/common/token-options.ts | 11 ----------- src/util.ts | 20 ++++++++++++++++++++ 6 files changed, 57 insertions(+), 28 deletions(-) create mode 100644 src/types/common/service-token-options.ts delete mode 100644 src/types/common/token-options.ts diff --git a/src/models/independent_service_token.ts b/src/models/independent_service_token.ts index efa7f72..fe843ce 100644 --- a/src/models/independent_service_token.ts +++ b/src/models/independent_service_token.ts @@ -5,7 +5,7 @@ import type { Model, HydratedDocument } from 'mongoose'; export interface IIndependentServiceToken { token: string; client_id: string; - title_ids: string[]; + title_id: string; pid: number; info: { system_type: number; @@ -27,7 +27,7 @@ export type HydratedIndependentServiceTokenDocument = HydratedDocument({ token: String, client_id: String, - title_ids: [String], + title_id: String, pid: Number, info: { system_type: Number, diff --git a/src/services/nasc/routes/ac.ts b/src/services/nasc/routes/ac.ts index dfec142..8f5cd17 100644 --- a/src/services/nasc/routes/ac.ts +++ b/src/services/nasc/routes/ac.ts @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import express from 'express'; import { SystemType } from '@/types/common/system-types'; import { TokenType } from '@/types/common/token-types'; -import { nintendoBase64Encode, nintendoBase64Decode, nascDateTime, nascError } from '@/util'; +import { nintendoBase64Encode, nintendoBase64Decode, nascDateTime, nascError, createServiceToken } from '@/util'; import { getServerByTitleID } from '@/database'; import { IndependentServiceToken } from '@/models/independent_service_token'; import { NEXToken } from '@/models/nex_token'; @@ -96,18 +96,25 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number, }); } -async function processServiceTokenRequest(_server: HydratedServerDocument, pid: number, titleID: string): Promise { - const serviceToken = await IndependentServiceToken.create({ - token: crypto.randomBytes(36).toString('base64'), - client_id: '', // * Unused in NASC - title_ids: [titleID], +async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise { + const serviceTokenOptions = { pid: pid, + title_id: titleID, + issued: new Date(), + expires: new Date(Date.now() + 24 * 3600 * 1000) + }; + + const serviceToken = await IndependentServiceToken.create({ + token: nintendoBase64Encode(createServiceToken(server, serviceTokenOptions)), + client_id: server.game_server_id, + title_id: serviceTokenOptions.title_id, + pid: serviceTokenOptions.pid, info: { system_type: SystemType.CTR, token_type: TokenType.IndependentService, title_id: BigInt(parseInt(titleID, 16)), - issued: new Date(), - expires: new Date(Date.now() + 12 * 3600 * 1000) // * These don't technicaly expire, thats up to the independent server, but adding this as a suggested expiration anyway + issued: serviceTokenOptions.issued, + expires: serviceTokenOptions.expires } }); diff --git a/src/services/nnas/routes/provider.ts b/src/services/nnas/routes/provider.ts index a154c57..9372895 100644 --- a/src/services/nnas/routes/provider.ts +++ b/src/services/nnas/routes/provider.ts @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import express from 'express'; import xmlbuilder from 'xmlbuilder'; import { getServerByClientID, getServerByGameServerID } from '@/database'; -import { getValueFromHeaders, getValueFromQueryString } from '@/util'; +import { createServiceToken, getValueFromHeaders, getValueFromQueryString } from '@/util'; import { TokenType } from '@/types/common/token-types'; import { IndependentServiceToken } from '@/models/independent_service_token'; import { NEXToken } from '@/models/nex_token'; @@ -93,17 +93,24 @@ router.get('/service_token/@me', async (request: express.Request, response: expr return; } - const serviceToken = await IndependentServiceToken.create({ - token: crypto.randomBytes(36).toString('base64'), - client_id: clientID, - title_ids: [titleID], + const serviceTokenOptions = { pid: pnid.pid, + title_id: titleID, + issued: new Date(), + expires: new Date(Date.now() + 24 * 3600 * 1000) + }; + + const serviceToken = await IndependentServiceToken.create({ + token: createServiceToken(server, serviceTokenOptions).toString('base64'), + client_id: clientID, + title_id: serviceTokenOptions.title_id, + pid: serviceTokenOptions.pid, info: { system_type: server.device, token_type: TokenType.IndependentService, title_id: BigInt(parseInt(titleID, 16)), - issued: new Date(), - expires: new Date(Date.now() + 12 * 3600 * 1000) // * These don't technicaly expire, thats up to the independent server, but adding this as a suggested expiration anyway + issued: serviceTokenOptions.issued, + expires: serviceTokenOptions.expires } }); diff --git a/src/types/common/service-token-options.ts b/src/types/common/service-token-options.ts new file mode 100644 index 0000000..4fabbab --- /dev/null +++ b/src/types/common/service-token-options.ts @@ -0,0 +1,6 @@ +export interface ServiceTokenOptions { + pid: number; + title_id: string; + issued: Date; + expires: Date; +} diff --git a/src/types/common/token-options.ts b/src/types/common/token-options.ts deleted file mode 100644 index ca1bdb5..0000000 --- a/src/types/common/token-options.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { SystemType } from '@/types/common/system-types'; -import type { TokenType } from '@/types/common/token-types'; - -export interface TokenOptions { - system_type: SystemType; - token_type: TokenType; - pid: number; - access_level?: number; - title_id?: bigint; - expire_time: bigint; -} diff --git a/src/util.ts b/src/util.ts index 3d7791b..38f5286 100644 --- a/src/util.ts +++ b/src/util.ts @@ -14,6 +14,8 @@ import type { ObjectCannedACL } from '@aws-sdk/client-s3'; import type { IncomingHttpHeaders } from 'node:http'; import type { IPNID, IPNIDMethods } from '@/types/mongoose/pnid'; import type { SafeQs } from '@/types/common/safe-qs'; +import type { HydratedServerDocument } from '@/types/mongoose/server'; +import type { ServiceTokenOptions } from '@/types/common/service-token-options'; let s3: S3; @@ -53,6 +55,24 @@ export function nintendoBase64Encode(decoded: string | Buffer): string { return encoded.replaceAll('+', '.').replaceAll('/', '-').replaceAll('=', '*'); } +export function createServiceToken(server: HydratedServerDocument, options: ServiceTokenOptions): Buffer { + const dataBuffer = Buffer.alloc(28); + + dataBuffer.writeUInt32BE(options.pid, 0); + dataBuffer.writeBigUInt64BE(BigInt(parseInt(options.title_id, 16)), 4); + dataBuffer.writeBigUInt64BE(BigInt(options.issued.getTime()), 12); + dataBuffer.writeBigUInt64BE(BigInt(options.expires.getTime()), 20); + + // * Not using AES anymore but fuck it, it's here already + // TODO - rename the AES field + const hmac = crypto.createHmac('sha256', server.aes_key).update(dataBuffer).digest(); + + return Buffer.concat([ + dataBuffer, + hmac + ]); +} + export function fullUrl(request: express.Request): string { const protocol = request.protocol; const host = request.host;