Skip to content

Commit

Permalink
chore: move back to number types for token, with const enum
Browse files Browse the repository at this point in the history
  • Loading branch information
binaryoverload committed Feb 2, 2025
1 parent 1197f71 commit 0578bfd
Show file tree
Hide file tree
Showing 10 changed files with 55 additions and 85 deletions.
9 changes: 5 additions & 4 deletions src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PNIDProfile } from '@/types/services/nnas/pnid-profile';
import { ConnectionData } from '@/types/services/api/connection-data';
import { ConnectionResponse } from '@/types/services/api/connection-response';
import { DiscordConnectionData } from '@/types/services/api/discord-connection-data';
import { SystemType, TokenType } from '@/types/common/token';

const connection_string = config.mongoose.connection_string;
const options = config.mongoose.options;
Expand Down Expand Up @@ -112,7 +113,7 @@ export async function getPNIDByNNASAccessToken(token: string): Promise<HydratedP
const unpackedToken = unpackToken(decryptedToken);

// * Return if the system type isn't Wii U (NNAS) and the token type isn't "OAuth Access"
if (unpackedToken.system_type !== 'WIIU' || unpackedToken.token_type !== 'OAUTH_ACCESS') {
if (unpackedToken.system_type !== SystemType.WIIU || unpackedToken.token_type !== TokenType.OAUTH_ACCESS) {
return null;
}

Expand Down Expand Up @@ -142,7 +143,7 @@ export async function getPNIDByNNASRefreshToken(token: string): Promise<Hydrated
const unpackedToken = unpackToken(decryptedToken);

// * Return if the system type isn't Wii U (NNAS) and the token type isn't "OAuth Refresh"
if (unpackedToken.system_type !== 'WIIU' || unpackedToken.token_type !== 'OAUTH_REFRESH') {
if (unpackedToken.system_type !== SystemType.WIIU || unpackedToken.token_type !== TokenType.OAUTH_ACCESS) {
return null;
}

Expand Down Expand Up @@ -172,7 +173,7 @@ export async function getPNIDByAPIAccessToken(token: string): Promise<HydratedPN
const unpackedToken = unpackToken(decryptedToken);

// * Return if the system type isn't API (REST and gRPC) and the token type isn't "OAuth Access"
if (unpackedToken.system_type !== 'API' || unpackedToken.token_type !== 'OAUTH_ACCESS') {
if (unpackedToken.system_type !== SystemType.API || unpackedToken.token_type !== TokenType.OAUTH_ACCESS) {
return null;
}

Expand Down Expand Up @@ -202,7 +203,7 @@ export async function getPNIDByAPIRefreshToken(token: string): Promise<HydratedP
const unpackedToken = unpackToken(decryptedToken);

// * Return if the system type isn't API (REST and gRPC) and the token type isn't "OAuth Refresh"
if (unpackedToken.system_type !== 'API' || unpackedToken.token_type !== 'OAUTH_REFRESH') {
if (unpackedToken.system_type !== SystemType.API || unpackedToken.token_type !== TokenType.OAUTH_REFRESH) {
return null;
}

Expand Down
3 changes: 2 additions & 1 deletion src/services/api/routes/v1/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import bcrypt from 'bcrypt';
import { getPNIDByUsername, getPNIDByAPIRefreshToken } from '@/database';
import { nintendoPasswordHash, generateOAuthTokens} from '@/util';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { SystemType } from '@/types/common/token';

const router = express.Router();

Expand Down Expand Up @@ -109,7 +110,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
}

try {
const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
const tokenGeneration = generateOAuthTokens(SystemType.API, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days

response.json({
access_token: tokenGeneration.accessToken,
Expand Down
3 changes: 2 additions & 1 deletion src/services/api/routes/v1/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { NEXAccount } from '@/models/nex-account';
import { config, disabledFeatures } from '@/config-manager';
import { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { SystemType } from '@/types/common/token';

const router = express.Router();

Expand Down Expand Up @@ -367,7 +368,7 @@ router.post('/', async (request: express.Request, response: express.Response): P
await sendConfirmationEmail(pnid);

try {
const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
const tokenGeneration = generateOAuthTokens(SystemType.API, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days

response.json({
access_token: tokenGeneration.accessToken,
Expand Down
3 changes: 2 additions & 1 deletion src/services/grpc/api/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import bcrypt from 'bcrypt';
import { getPNIDByUsername, getPNIDByAPIRefreshToken } from '@/database';
import { nintendoPasswordHash, generateOAuthTokens} from '@/util';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { SystemType } from '@/types/common/token';

export async function login(request: LoginRequest): Promise<DeepPartial<LoginResponse>> {
const grantType = request.grantType?.trim();
Expand Down Expand Up @@ -43,7 +44,7 @@ export async function login(request: LoginRequest): Promise<DeepPartial<LoginRes
}

try {
const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
const tokenGeneration = generateOAuthTokens(SystemType.API, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days

return {
accessToken: tokenGeneration.accessToken,
Expand Down
3 changes: 2 additions & 1 deletion src/services/grpc/api/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { NEXAccount } from '@/models/nex-account';
import { config, disabledFeatures } from '@/config-manager';
import type { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
import type { HydratedPNIDDocument } from '@/types/mongoose/pnid';
import { SystemType } from '@/types/common/token';

const PNID_VALID_CHARACTERS_REGEX = /^[\w\-.]*$/;
const PNID_PUNCTUATION_START_REGEX = /^[_\-.]/;
Expand Down Expand Up @@ -230,7 +231,7 @@ export async function register(request: RegisterRequest): Promise<DeepPartial<Lo
await sendConfirmationEmail(pnid);

try {
const tokenGeneration = generateOAuthTokens('API', pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days
const tokenGeneration = generateOAuthTokens(SystemType.API, pnid, { refreshExpiresIn: 14 * 24 * 60 * 60 }); // * 14 days

return {
accessToken: tokenGeneration.accessToken,
Expand Down
10 changes: 5 additions & 5 deletions src/services/nasc/routes/ac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { nintendoBase64Encode, nintendoBase64Decode, nascDateTime, nascError, ge
import { getServerByTitleID } from '@/database';
import { NASCRequestParams } from '@/types/services/nasc/request-params';
import { HydratedServerDocument } from '@/types/mongoose/server';
import { TokenOptions } from '@/types/common/token';
import { SystemType, TokenOptions, TokenType } from '@/types/common/token';

const router = express.Router();

Expand Down Expand Up @@ -66,8 +66,8 @@ router.post('/', async (request: express.Request, response: express.Response): P

async function processLoginRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
const tokenOptions: TokenOptions = {
system_type: '3DS',
token_type: 'NEX',
system_type: SystemType['3DS'],
token_type: TokenType.NEX,
pid: pid,
access_level: 0,
title_id: BigInt(parseInt(titleID, 16)),
Expand All @@ -90,8 +90,8 @@ async function processLoginRequest(server: HydratedServerDocument, pid: number,

async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
const tokenOptions: TokenOptions = {
system_type: '3DS',
token_type: 'SERVICE',
system_type: SystemType['3DS'],
token_type: TokenType.SERVICE,
pid: pid,
access_level: 0,
title_id: BigInt(parseInt(titleID, 16)),
Expand Down
3 changes: 2 additions & 1 deletion src/services/nnas/routes/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import consoleStatusVerificationMiddleware from '@/middleware/console-status-ver
import { getPNIDByNNASRefreshToken, getPNIDByUsername } from '@/database';
import { generateOAuthTokens } from '@/util';
import { Device } from '@/models/device';
import { SystemType } from '@/types/common/token';

const router = express.Router();

Expand Down Expand Up @@ -153,7 +154,7 @@ router.post('/access_token/generate', deviceCertificateMiddleware, consoleStatus
}

try {
const tokenGeneration = generateOAuthTokens('WIIU', pnid);
const tokenGeneration = generateOAuthTokens(SystemType.WIIU, pnid);

response.send(xmlbuilder.create({
OAuth20: {
Expand Down
45 changes: 15 additions & 30 deletions src/services/nnas/routes/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import xmlbuilder from 'xmlbuilder';
import { getServerByClientID, getServerByGameServerID } from '@/database';
import { generateToken, getValueFromHeaders, getValueFromQueryString } from '@/util';
import { NEXAccount } from '@/models/nex-account';
import { TokenOptions } from '@/types/common/token';
import { serverDeviceToSystemType } from '@/types/common/token';
import { SystemType, TokenOptions, TokenType } from '@/types/common/token';

const router = express.Router();

Expand Down Expand Up @@ -91,23 +90,16 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
return;
}

const systemType = serverDeviceToSystemType[server.device];
if (!systemType) {
response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());

return;
if (!(server.device in Object.values(SystemType))) {
throw new Error('Invalid system type');
}

// * Asserted safely because of the check above
const systemType = server.device as SystemType;

const tokenOptions: TokenOptions = {
system_type: systemType,
token_type: 'SERVICE',
system_type: systemType as SystemType,
token_type: TokenType.SERVICE,
pid: pnid.pid,
access_level: pnid.access_level,
title_id: BigInt(parseInt(titleID, 16)),
Expand Down Expand Up @@ -228,23 +220,16 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
return;
}

const systemType = serverDeviceToSystemType[server.device];
if (!systemType) {
response.send(xmlbuilder.create({
errors: {
error: {
code: '1021',
message: 'The requested game server was not found'
}
}
}).end());

return;
if (!(server.device in Object.values(SystemType))) {
throw new Error('Invalid system type');
}

// * Asserted safely because of the check above
const systemType = server.device as SystemType;

const tokenOptions: TokenOptions = {
system_type: systemType,
token_type: 'NEX',
system_type: systemType,
token_type: TokenType.NEX,
pid: pnid.pid,
access_level: pnid.access_level,
title_id: BigInt(parseInt(titleID, 16)),
Expand Down
23 changes: 4 additions & 19 deletions src/types/common/token.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,18 @@
export const TokenTypes = {
export const TokenType = {
OAUTH_ACCESS: 1,
OAUTH_REFRESH: 2,
NEX: 3,
SERVICE: 4,
PASSWORD_RESET: 5
} as const;
export type TokenType = keyof typeof TokenTypes;
export type TokenType = typeof TokenType[keyof typeof TokenType];

export function getTokenTypeFromValue(type: number): TokenType | undefined {
const keys = Object.keys(TokenTypes) as TokenType[];
return keys.find((key) => TokenTypes[key] === type);
}

export const SystemTypes = {
export const SystemType = {
'WIIU': 1,
'3DS': 2,
'API': 3
} as const;
export type SystemType = keyof typeof SystemTypes;

export const serverDeviceToSystemType: Record<number, SystemType> = {
1: 'WIIU',
2: '3DS'
};

export function getSystemTypeFromValue(type: number): SystemType | undefined {
const keys = Object.keys(SystemTypes) as SystemType[];
return keys.find((key) => SystemTypes[key] === type);
}
export type SystemType = typeof SystemType[keyof typeof SystemType];

export interface Token {
system_type: SystemType;
Expand Down
38 changes: 16 additions & 22 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import crc32 from 'buffer-crc32';
import crc from 'crc';
import { sendMail } from '@/mailer';
import { config, disabledFeatures } from '@/config-manager';
import { getSystemTypeFromValue, getTokenTypeFromValue, OAuthTokenGenerationResponse, OAuthTokenOptions, SystemType, SystemTypes, Token, TokenOptions, TokenTypes } from '@/types/common/token';
import { OAuthTokenGenerationResponse, OAuthTokenOptions, SystemType, Token, TokenOptions, TokenType } from '@/types/common/token';
import { HydratedPNIDDocument, IPNID, IPNIDMethods } from '@/types/mongoose/pnid';
import { SafeQs } from '@/types/common/safe-qs';

Expand Down Expand Up @@ -58,15 +58,15 @@ export function generateOAuthTokens(systemType: SystemType, pnid: HydratedPNIDDo

const accessTokenOptions: TokenOptions = {
system_type: systemType,
token_type: 'OAUTH_ACCESS',
token_type: TokenType.OAUTH_ACCESS,
pid: pnid.pid,
access_level: pnid.access_level,
expire_time: BigInt(Date.now() + (accessTokenExpiresInSecs * 1000))
};

const refreshTokenOptions: TokenOptions = {
system_type: systemType,
token_type: 'OAUTH_REFRESH',
token_type: TokenType.OAUTH_REFRESH,
pid: pnid.pid,
access_level: pnid.access_level,
expire_time: BigInt(Date.now() + (refreshTokenExpiresInSecs * 1000))
Expand All @@ -92,15 +92,12 @@ export function generateOAuthTokens(systemType: SystemType, pnid: HydratedPNIDDo
export function generateToken(key: string, options: TokenOptions): Buffer | null {
let dataBuffer = Buffer.alloc(1 + 1 + 4 + 8);

const systemType = SystemTypes[options.system_type];
const tokenType = TokenTypes[options.token_type];

dataBuffer.writeUInt8(systemType, 0x0);
dataBuffer.writeUInt8(tokenType, 0x1);
dataBuffer.writeUInt8(options.system_type, 0x0);
dataBuffer.writeUInt8(options.token_type, 0x1);
dataBuffer.writeUInt32LE(options.pid, 0x2);
dataBuffer.writeBigUInt64LE(options.expire_time, 0x6);

if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 'API') {
if ((options.token_type !== TokenType.OAUTH_ACCESS && options.token_type !== TokenType.OAUTH_REFRESH) || options.system_type === SystemType.API) {
// * Access and refresh tokens have smaller bodies due to size constraints
// * The API does not have this restraint, however
if (options.title_id === undefined || options.access_level === undefined) {
Expand All @@ -126,7 +123,7 @@ export function generateToken(key: string, options: TokenOptions): Buffer | null

let final = encrypted;

if ((options.token_type !== 'OAUTH_ACCESS' && options.token_type !== 'OAUTH_REFRESH') || options.system_type === 'API') {
if ((options.token_type !== TokenType.OAUTH_ACCESS && options.token_type !== TokenType.OAUTH_REFRESH) || options.system_type === SystemType.API) {
// * Access and refresh tokens don't get a checksum due to size constraints
const checksum = crc32(dataBuffer);

Expand Down Expand Up @@ -171,23 +168,20 @@ export function decryptToken(token: Buffer, key?: string): Buffer {
}

export function unpackToken(token: Buffer): Token {
const systemTypeNum = token.readUInt8(0x0);
const tokenTypeNum = token.readUInt8(0x1);

const systemType = getSystemTypeFromValue(systemTypeNum);
const tokenType = getTokenTypeFromValue(tokenTypeNum);
const systemType = token.readUInt8(0x0);
const tokenType = token.readUInt8(0x1);

if (!systemType) throw new Error('Invalid system type');
if (!tokenType) throw new Error('Invalid token type');
if (!(systemType in Object.values(SystemType))) throw new Error('Invalid system type');
if (!(tokenType in Object.values(TokenType))) throw new Error('Invalid token type');

const unpacked: Token = {
system_type: systemType,
token_type: tokenType,
system_type: systemType as SystemType,
token_type: tokenType as TokenType,
pid: token.readUInt32LE(0x2),
expire_time: token.readBigUInt64LE(0x6)
};

if (unpacked.token_type !== 'OAUTH_ACCESS' && unpacked.token_type !== 'OAUTH_REFRESH') {
if (unpacked.token_type !== TokenType.OAUTH_ACCESS && unpacked.token_type !== TokenType.OAUTH_REFRESH) {
unpacked.title_id = token.readBigUInt64LE(0xE);
unpacked.access_level = token.readInt8(0x16);
}
Expand Down Expand Up @@ -274,8 +268,8 @@ export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument<IP

export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
const tokenOptions: TokenOptions = {
system_type: 'API',
token_type: 'PASSWORD_RESET',
system_type: SystemType.API,
token_type: TokenType.PASSWORD_RESET,
pid: pnid.pid,
access_level: pnid.access_level,
title_id: BigInt(0),
Expand Down

0 comments on commit 0578bfd

Please sign in to comment.