mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-09-13 22:05:20 -05:00
Changed token format to be much smaller (thanks 3DS)
This commit is contained in:
130
src/cache.ts
130
src/cache.ts
@@ -6,8 +6,6 @@ let client: redis.RedisClientType;
|
||||
|
||||
const memoryCache: { [key: string]: Buffer } = {};
|
||||
|
||||
const SERVICE_CERTS_BASE: string = `${__dirname}/../certs/service`;
|
||||
const NEX_CERTS_BASE: string = `${__dirname}/../certs/nex`;
|
||||
const LOCAL_CDN_BASE: string = `${__dirname}/../cdn`;
|
||||
|
||||
export async function connect(): Promise<void> {
|
||||
@@ -42,134 +40,6 @@ export async function getCachedFile(fileName: string, encoding?: BufferEncoding)
|
||||
return cachedFile;
|
||||
}
|
||||
|
||||
// * NEX server cache functions
|
||||
|
||||
export async function getNEXPublicKey(name: string, encoding?: BufferEncoding): Promise<Buffer> {
|
||||
let publicKey: Buffer = await getCachedFile(`nex:${name}:public_key`, encoding);
|
||||
|
||||
if (publicKey === null) {
|
||||
publicKey = await fs.readFile(`${NEX_CERTS_BASE}/${name}/public.pem`);
|
||||
await setNEXPublicKey(name, publicKey);
|
||||
}
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
export async function getNEXPrivateKey(name: string, encoding?: BufferEncoding): Promise<Buffer> {
|
||||
let privateKey: Buffer = await getCachedFile(`nex:${name}:private_key`, encoding);
|
||||
|
||||
if (privateKey === null) {
|
||||
privateKey = await fs.readFile(`${NEX_CERTS_BASE}/${name}/private.pem`);
|
||||
await setNEXPrivateKey(name, privateKey);
|
||||
}
|
||||
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
export async function getNEXSecretKey(name: string, encoding?: BufferEncoding): Promise<Buffer> {
|
||||
let secretKey: Buffer = await getCachedFile(`nex:${name}:secret_key`, encoding);
|
||||
|
||||
if (secretKey === null) {
|
||||
const fileBuffer: string = await fs.readFile(`${NEX_CERTS_BASE}/${name}/secret.key`, { encoding: 'utf8' });
|
||||
secretKey = Buffer.from(fileBuffer, encoding);
|
||||
await setNEXSecretKey(name, secretKey);
|
||||
}
|
||||
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
export async function getNEXAESKey(name: string, encoding?: BufferEncoding): Promise<Buffer> {
|
||||
let aesKey: Buffer = await getCachedFile(`nex:${name}:aes_key`, encoding);
|
||||
|
||||
if (aesKey === null) {
|
||||
const fileBuffer: string = await fs.readFile(`${NEX_CERTS_BASE}/${name}/aes.key`, { encoding: 'utf8' });
|
||||
aesKey = Buffer.from(fileBuffer, encoding);
|
||||
await setNEXAESKey(name, aesKey);
|
||||
}
|
||||
|
||||
return aesKey;
|
||||
}
|
||||
|
||||
export async function setNEXPublicKey(name: string, value: Buffer): Promise<void> {
|
||||
await setCachedFile(`nex:${name}:public_key`, value);
|
||||
}
|
||||
|
||||
export async function setNEXPrivateKey(name: string, value: Buffer): Promise<void> {
|
||||
await setCachedFile(`nex:${name}:private_key`, value);
|
||||
}
|
||||
|
||||
export async function setNEXSecretKey(name: string, value: Buffer): Promise<void> {
|
||||
await setCachedFile(`nex:${name}:secret_key`, value);
|
||||
}
|
||||
|
||||
export async function setNEXAESKey(name: string, value: Buffer): Promise<void> {
|
||||
await setCachedFile(`nex:${name}:aes_key`, value);
|
||||
}
|
||||
|
||||
// * 3rd party service cache functions
|
||||
|
||||
export async function getServicePublicKey(name: string, encoding?: BufferEncoding): Promise<Buffer> {
|
||||
let publicKey: Buffer = await getCachedFile(`service:${name}:public_key`, encoding);
|
||||
|
||||
if (publicKey === null) {
|
||||
publicKey = await fs.readFile(`${SERVICE_CERTS_BASE}/${name}/public.pem`);
|
||||
await setServicePublicKey(name, publicKey);
|
||||
}
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
export async function getServicePrivateKey(name: string, encoding?: BufferEncoding): Promise<Buffer> {
|
||||
let privateKey: Buffer = await getCachedFile(`service:${name}:private_key`, encoding);
|
||||
|
||||
if (privateKey === null) {
|
||||
privateKey = await fs.readFile(`${SERVICE_CERTS_BASE}/${name}/private.pem`);
|
||||
await setServicePrivateKey(name, privateKey);
|
||||
}
|
||||
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
export async function getServiceSecretKey(name: string, encoding?: BufferEncoding): Promise<Buffer> {
|
||||
let secretKey: Buffer = await getCachedFile(`service:${name}:secret_key`, encoding);
|
||||
|
||||
if (secretKey === null) {
|
||||
const fileBuffer: string = await fs.readFile(`${SERVICE_CERTS_BASE}/${name}/secret.key`, { encoding: 'utf8' });
|
||||
secretKey = Buffer.from(fileBuffer, encoding);
|
||||
await setServiceSecretKey(name, secretKey);
|
||||
}
|
||||
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
export async function getServiceAESKey(name: string, encoding?: BufferEncoding): Promise<Buffer> {
|
||||
let aesKey: Buffer = await getCachedFile(`service:${name}:aes_key`, encoding);
|
||||
|
||||
if (aesKey === null) {
|
||||
const fileBuffer: string = await fs.readFile(`${SERVICE_CERTS_BASE}/${name}/aes.key`, { encoding: 'utf8' });
|
||||
aesKey = Buffer.from(fileBuffer, encoding);
|
||||
await setServiceAESKey(name, aesKey);
|
||||
}
|
||||
|
||||
return aesKey;
|
||||
}
|
||||
|
||||
export async function setServicePublicKey(name: string, value: Buffer): Promise<void> {
|
||||
await setCachedFile(`service:${name}:public_key`, value);
|
||||
}
|
||||
|
||||
export async function setServicePrivateKey(name: string, value: Buffer): Promise<void> {
|
||||
await setCachedFile(`service:${name}:private_key`, value);
|
||||
}
|
||||
|
||||
export async function setServiceSecretKey(name: string, value: Buffer): Promise<void> {
|
||||
await setCachedFile(`service:${name}:secret_key`, value);
|
||||
}
|
||||
|
||||
export async function setServiceAESKey(name: string, value: Buffer): Promise<void> {
|
||||
await setCachedFile(`service:${name}:aes_key`, value);
|
||||
}
|
||||
|
||||
// * Local CDN cache functions
|
||||
|
||||
export async function getLocalCDNFile(name: string, encoding?: BufferEncoding): Promise<Buffer> {
|
||||
|
||||
@@ -64,7 +64,8 @@ export const config: Config = {
|
||||
disk_path: process.env.PN_ACT_CONFIG_CDN_DISK_PATH || '',
|
||||
base_url: process.env.PN_ACT_CONFIG_CDN_BASE_URL || ''
|
||||
},
|
||||
website_base: process.env.PN_ACT_CONFIG_WEBSITE_BASE || ''
|
||||
website_base: process.env.PN_ACT_CONFIG_WEBSITE_BASE || '',
|
||||
aes_key: process.env.PN_ACT_CONFIG_AES_KEY || ''
|
||||
};
|
||||
|
||||
LOG_INFO('Config loaded, checking integrity');
|
||||
@@ -162,4 +163,9 @@ if (disabledFeatures.s3) {
|
||||
if (disabledFeatures.redis) {
|
||||
LOG_WARN('Both s3 and Redis are disabled. Large CDN files will use the in-memory cache, which may result in high memory use. Please enable s3 if you\'re running a production server.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.aes_key) {
|
||||
LOG_ERROR('Token AES key is not set. Set the PN_ACT_CONFIG_AES_KEY environment variable to your AES-256-CBC key');
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -104,7 +104,7 @@ export async function getPNIDByBearerAuth(token: string): Promise<HydratedPNIDDo
|
||||
verifyConnected();
|
||||
|
||||
try {
|
||||
const decryptedToken: Buffer = await decryptToken(Buffer.from(token, 'base64'));
|
||||
const decryptedToken: Buffer = await decryptToken(config.aes_key, Buffer.from(token, 'hex'));
|
||||
const unpackedToken: Token = unpackToken(decryptedToken);
|
||||
|
||||
const pnid: HydratedPNIDDocument | null = await getPNIDByPID(unpackedToken.pid);
|
||||
|
||||
@@ -3,15 +3,16 @@ import uniqueValidator from 'mongoose-unique-validator';
|
||||
import { IServer, IServerMethods, ServerModel } from '@/types/mongoose/server';
|
||||
|
||||
const ServerSchema = new Schema<IServer, ServerModel, IServerMethods>({
|
||||
ip: String, // Example: 1.1.1.1
|
||||
port: Number, // Example: 60000
|
||||
service_name: String, // Example: friends
|
||||
service_type: String, // Example: nex
|
||||
game_server_id: String, // Example: 00003200
|
||||
title_ids: [String], // Example: ["000500001018DB00", "000500001018DC00", "000500001018DD00"]
|
||||
access_mode: String, // Example: prod
|
||||
maintenance_mode: Boolean, // Example: false
|
||||
device: Number, // Example: 1 (WiiU)
|
||||
ip: String,
|
||||
port: Number,
|
||||
service_name: String,
|
||||
service_type: String,
|
||||
game_server_id: String,
|
||||
title_ids: [String],
|
||||
access_mode: String,
|
||||
maintenance_mode: Boolean,
|
||||
device: Number,
|
||||
aes_key: String
|
||||
});
|
||||
|
||||
ServerSchema.plugin(uniqueValidator, { message: '{PATH} already in use.' });
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import express from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import fs from 'fs-extra';
|
||||
import { getPNIDByUsername, getPNIDByBearerAuth } from '@/database';
|
||||
import { getServicePublicKey, getServiceSecretKey } from '@/cache';
|
||||
import { nintendoPasswordHash, generateToken} from '@/util';
|
||||
import { CryptoOptions } from '@/types/common/crypto-options';
|
||||
import { config } from '@/config-manager';
|
||||
import { TokenOptions } from '@/types/common/token-options';
|
||||
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
|
||||
@@ -88,45 +86,25 @@ router.post('/', async (request: express.Request, response: express.Response) =>
|
||||
}
|
||||
}
|
||||
|
||||
const cryptoPath: string = `${__dirname}/../../../../../certs/service/account`;
|
||||
|
||||
if (!await fs.pathExists(cryptoPath)) {
|
||||
// Need to generate keys
|
||||
return response.status(500).json({
|
||||
app: 'api',
|
||||
status: 500,
|
||||
error: 'Failed to locate crypto keys. Please contact an administrator'
|
||||
});
|
||||
}
|
||||
|
||||
const publicKey: Buffer = await getServicePublicKey('account');
|
||||
const secretKey: Buffer = await getServiceSecretKey('account');
|
||||
|
||||
const cryptoOptions: CryptoOptions = {
|
||||
public_key: publicKey,
|
||||
hmac_secret: secretKey
|
||||
};
|
||||
|
||||
const accessTokenOptions: TokenOptions = {
|
||||
system_type: 0xF, // API
|
||||
token_type: 0x1, // OAuth Access,
|
||||
system_type: 0x1, // * WiiU
|
||||
token_type: 0x1, // * OAuth Access
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
const refreshTokenOptions: TokenOptions = {
|
||||
system_type: 0xF, // API
|
||||
token_type: 0x2, // OAuth Refresh,
|
||||
system_type: 0x1, // * WiiU
|
||||
token_type: 0x2, // * OAuth Refresh
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
const accessToken: string | null = await generateToken(cryptoOptions, accessTokenOptions);
|
||||
const newRefreshToken: string | null = await generateToken(cryptoOptions, refreshTokenOptions);
|
||||
const accessTokenBuffer: Buffer | null = await generateToken(config.aes_key, accessTokenOptions);
|
||||
const refreshTokenBuffer: Buffer | null = await generateToken(config.aes_key, refreshTokenOptions);
|
||||
|
||||
const accessToken: string = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
|
||||
const newRefreshToken: string = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
|
||||
|
||||
// TODO - Handle null tokens
|
||||
|
||||
|
||||
@@ -3,19 +3,16 @@ import crypto from 'node:crypto';
|
||||
import express from 'express';
|
||||
import emailvalidator from 'email-validator';
|
||||
import bcrypt from 'bcrypt';
|
||||
import fs from 'fs-extra';
|
||||
import moment from 'moment';
|
||||
import hcaptcha from 'hcaptcha';
|
||||
import Mii from 'mii-js';
|
||||
import mongoose from 'mongoose';
|
||||
import { doesPNIDExist, connection as databaseConnection } from '@/database';
|
||||
import { getServicePublicKey, getServiceSecretKey } from '@/cache';
|
||||
import { nintendoPasswordHash, sendConfirmationEmail, generateToken } from '@/util';
|
||||
import { LOG_ERROR } from '@/logger';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
import { config, disabledFeatures } from '@/config-manager';
|
||||
import { CryptoOptions } from '@/types/common/crypto-options';
|
||||
import { TokenOptions } from '@/types/common/token-options';
|
||||
import { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
|
||||
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
@@ -326,45 +323,25 @@ router.post('/', async (request: express.Request, response: express.Response) =>
|
||||
|
||||
await sendConfirmationEmail(pnid);
|
||||
|
||||
const cryptoPath: string = `${__dirname}/../../../../../certs/service/account`;
|
||||
|
||||
if (!await fs.pathExists(cryptoPath)) {
|
||||
// Need to generate keys
|
||||
return response.status(500).json({
|
||||
app: 'api',
|
||||
status: 500,
|
||||
error: 'Failed to locate crypto keys. Please contact an administrator'
|
||||
});
|
||||
}
|
||||
|
||||
const publicKey: Buffer = await getServicePublicKey('account');
|
||||
const secretKey: Buffer = await getServiceSecretKey('account');
|
||||
|
||||
const cryptoOptions: CryptoOptions = {
|
||||
public_key: publicKey,
|
||||
hmac_secret: secretKey
|
||||
};
|
||||
|
||||
const accessTokenOptions: TokenOptions = {
|
||||
system_type: 0xF, // API
|
||||
token_type: 0x1, // OAuth Access,
|
||||
system_type: 0x1, // * WiiU
|
||||
token_type: 0x1, // * OAuth Access
|
||||
pid: pnid.pid,
|
||||
access_level: 0,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
const refreshTokenOptions: TokenOptions = {
|
||||
system_type: 0xF, // API
|
||||
token_type: 0x2, // OAuth Refresh,
|
||||
system_type: 0x1, // * WiiU
|
||||
token_type: 0x2, // * OAuth Refresh
|
||||
pid: pnid.pid,
|
||||
access_level: 0,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
const accessToken: string | null = await generateToken(cryptoOptions, accessTokenOptions);
|
||||
const refreshToken: string | null = await generateToken(cryptoOptions, refreshTokenOptions);
|
||||
const accessTokenBuffer: Buffer | null = await generateToken(config.aes_key, accessTokenOptions);
|
||||
const refreshTokenBuffer: Buffer | null = await generateToken(config.aes_key, refreshTokenOptions);
|
||||
|
||||
const accessToken: string = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
|
||||
const refreshToken: string = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
|
||||
|
||||
// TODO - Handle null tokens
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import express from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { decryptToken, unpackToken, nintendoPasswordHash } from '@/util';
|
||||
import { config } from '@/config-manager';
|
||||
import { Token } from '@/types/common/token';
|
||||
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
|
||||
@@ -28,7 +29,7 @@ router.post('/', async (request: express.Request, response: express.Response) =>
|
||||
|
||||
let unpackedToken: Token;
|
||||
try {
|
||||
const decryptedToken: Buffer = await decryptToken(Buffer.from(token, 'base64'));
|
||||
const decryptedToken: Buffer = await decryptToken(config.aes_key, Buffer.from(token, 'base64'));
|
||||
unpackedToken = unpackToken(decryptedToken);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import express from 'express';
|
||||
import { nintendoBase64Encode, nintendoBase64Decode, nascError, generateToken } from '@/util';
|
||||
import { getServerByTitleId } from '@/database';
|
||||
import { getNEXPublicKey, getNEXSecretKey } from '@/cache';
|
||||
import { CryptoOptions } from '@/types/common/crypto-options';
|
||||
import { TokenOptions } from '@/types/common/token-options';
|
||||
import { NASCRequestParams } from '@/types/services/nasc/request-params';
|
||||
import { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account';
|
||||
@@ -18,33 +16,17 @@ const router: express.Router = express.Router();
|
||||
router.post('/', async (request: express.Request, response: express.Response) => {
|
||||
const requestParams: NASCRequestParams = request.body;
|
||||
const action: string = nintendoBase64Decode(requestParams.action).toString();
|
||||
let responseData: URLSearchParams = nascError('null');
|
||||
|
||||
switch (action) {
|
||||
case 'LOGIN':
|
||||
responseData = await processLoginRequest(request);
|
||||
break;
|
||||
case 'SVCLOC':
|
||||
responseData = await processServiceTokenRequest(request);
|
||||
break;
|
||||
}
|
||||
|
||||
response.status(200).send(responseData.toString());
|
||||
});
|
||||
|
||||
async function processLoginRequest(request: express.Request): Promise<URLSearchParams> {
|
||||
const requestParams: NASCRequestParams = request.body;
|
||||
const titleID: string = nintendoBase64Decode(requestParams.titleid).toString();
|
||||
const nexAccount: HydratedNEXAccountDocument | null = request.nexAccount;
|
||||
let responseData: URLSearchParams = nascError('null');
|
||||
|
||||
if (!nexAccount) {
|
||||
// TODO - Research this error more
|
||||
return nascError('null');
|
||||
return response.status(200).send(responseData.toString());
|
||||
}
|
||||
|
||||
// TODO: REMOVE AFTER PUBLIC LAUNCH
|
||||
// LET EVERYONE IN THE `test` FRIENDS SERVER
|
||||
// THAT WAY EVERYONE CAN GET AN ASSIGNED PID
|
||||
// * LET EVERYONE IN THE `test` FRIENDS SERVER
|
||||
// * THAT WAY EVERYONE CAN GET AN ASSIGNED PID
|
||||
let serverAccessLevel: string = 'test';
|
||||
if (titleID !== '0004013000003202') {
|
||||
serverAccessLevel = nexAccount.server_access_level;
|
||||
@@ -52,32 +34,33 @@ async function processLoginRequest(request: express.Request): Promise<URLSearchP
|
||||
|
||||
const server: HydratedServerDocument | null = await getServerByTitleId(titleID, serverAccessLevel);
|
||||
|
||||
if (!server || !server.service_name || !server.ip) {
|
||||
return nascError('110');
|
||||
if (!server || !server.aes_key) {
|
||||
return response.status(200).send( nascError('110').toString());
|
||||
}
|
||||
|
||||
if (server.port <= 0 && server.ip !== '0.0.0.0') {
|
||||
if (action === 'LOGIN' && server.port <= 0 && server.ip !== '0.0.0.0') {
|
||||
// * Addresses of 0.0.0.0:0 are allowed
|
||||
// * They are expected for titles with no NEX server
|
||||
return nascError('110');
|
||||
return response.status(200).send( nascError('110').toString());
|
||||
}
|
||||
|
||||
const serverName: string = server.service_name;
|
||||
const ip: string = server.ip;
|
||||
const port: number = server.port;
|
||||
switch (action) {
|
||||
case 'LOGIN':
|
||||
responseData = await processLoginRequest(server, nexAccount.pid, titleID);
|
||||
break;
|
||||
case 'SVCLOC':
|
||||
responseData = await processServiceTokenRequest(server, nexAccount.pid, titleID);
|
||||
break;
|
||||
}
|
||||
|
||||
const publicKey: Buffer = await getNEXPublicKey(serverName);
|
||||
const secretKey: Buffer = await getNEXSecretKey(serverName);
|
||||
|
||||
const cryptoOptions: CryptoOptions = {
|
||||
public_key: publicKey,
|
||||
hmac_secret: secretKey
|
||||
};
|
||||
response.status(200).send(responseData.toString());
|
||||
});
|
||||
|
||||
async function processLoginRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
|
||||
const tokenOptions: TokenOptions = {
|
||||
system_type: 0x2, // 3DS
|
||||
token_type: 0x3, // nex token,
|
||||
pid: nexAccount.pid,
|
||||
system_type: 0x2, // * 3DS
|
||||
token_type: 0x3, // * NEX token
|
||||
pid: pid,
|
||||
access_level: 0,
|
||||
title_id: BigInt(parseInt(titleID, 16)),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
@@ -85,11 +68,11 @@ async function processLoginRequest(request: express.Request): Promise<URLSearchP
|
||||
|
||||
// TODO - Handle null tokens
|
||||
|
||||
let nexToken: string | null = await generateToken(cryptoOptions, tokenOptions);
|
||||
nexToken = nintendoBase64Encode(Buffer.from(nexToken || '', 'base64'));
|
||||
const nexTokenBuffer: Buffer | null = await generateToken(server.aes_key, tokenOptions);
|
||||
const nexToken: string = nintendoBase64Encode(nexTokenBuffer || '');
|
||||
|
||||
return new URLSearchParams({
|
||||
locator: nintendoBase64Encode(`${ip}:${port}`),
|
||||
locator: nintendoBase64Encode(`${server.ip}:${server.port}`),
|
||||
retry: nintendoBase64Encode('0'),
|
||||
returncd: nintendoBase64Encode('001'),
|
||||
token: nexToken,
|
||||
@@ -97,11 +80,25 @@ async function processLoginRequest(request: express.Request): Promise<URLSearchP
|
||||
});
|
||||
}
|
||||
|
||||
async function processServiceTokenRequest(_request: express.Request): Promise<URLSearchParams> {
|
||||
async function processServiceTokenRequest(server: HydratedServerDocument, pid: number, titleID: string): Promise<URLSearchParams> {
|
||||
const tokenOptions: TokenOptions = {
|
||||
system_type: 0x2, // * 3DS
|
||||
token_type: 0x4, // * Service token
|
||||
pid: pid,
|
||||
access_level: 0,
|
||||
title_id: BigInt(parseInt(titleID, 16)),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
// TODO - Handle null tokens
|
||||
|
||||
const serviceTokenBuffer: Buffer | null = await generateToken(server.aes_key, tokenOptions);
|
||||
const serviceToken: string = nintendoBase64Encode(serviceTokenBuffer || '');
|
||||
|
||||
return new URLSearchParams({
|
||||
retry: nintendoBase64Encode('0'),
|
||||
returncd: nintendoBase64Encode('007'),
|
||||
servicetoken: nintendoBase64Encode(Buffer.alloc(64).toString()), // hard coded for now
|
||||
servicetoken: serviceToken,
|
||||
statusdata: nintendoBase64Encode('Y'),
|
||||
svchost: nintendoBase64Encode('n/a'),
|
||||
datetime: nintendoBase64Encode(Date.now().toString()),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import express from 'express';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import bcrypt from 'bcrypt';
|
||||
import fs from 'fs-extra';
|
||||
import { getPNIDByUsername } from '@/database';
|
||||
import { generateToken } from '@/util';
|
||||
import { config } from '@/config-manager';
|
||||
import { TokenOptions } from '@/types/common/token-options';
|
||||
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
|
||||
@@ -75,44 +75,28 @@ router.post('/access_token/generate', async (request: express.Request, response:
|
||||
}).end());
|
||||
}
|
||||
|
||||
const cryptoPath: string = `${__dirname}/../../../../certs/service/account`;
|
||||
|
||||
if (!await fs.pathExists(cryptoPath)) {
|
||||
// Need to generate keys
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
code: '0000',
|
||||
message: 'Could not find account access key crypto path'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
}
|
||||
|
||||
const accessTokenOptions: TokenOptions = {
|
||||
system_type: 0x1, // WiiU
|
||||
token_type: 0x1, // OAuth Access,
|
||||
system_type: 0x1, // * WiiU
|
||||
token_type: 0x1, // * OAuth Access
|
||||
pid: pnid.pid,
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
const refreshTokenOptions: TokenOptions = {
|
||||
system_type: 0x1, // WiiU
|
||||
token_type: 0x2, // OAuth Refresh,
|
||||
system_type: 0x1, // * WiiU
|
||||
token_type: 0x2, // * OAuth Refresh
|
||||
pid: pnid.pid,
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
let accessToken: string | null = await generateToken(null, accessTokenOptions);
|
||||
let refreshToken: string | null = await generateToken(null, refreshTokenOptions);
|
||||
const accessTokenBuffer: Buffer | null = await generateToken(config.aes_key, accessTokenOptions);
|
||||
const refreshTokenBuffer: Buffer | null = await generateToken(config.aes_key, refreshTokenOptions);
|
||||
|
||||
const accessToken: string = accessTokenBuffer ? accessTokenBuffer.toString('hex') : '';
|
||||
const refreshToken: string = refreshTokenBuffer ? refreshTokenBuffer.toString('hex') : '';
|
||||
|
||||
// TODO - Handle null tokens
|
||||
|
||||
if (request.isCemu) {
|
||||
accessToken = Buffer.from(accessToken || '', 'base64').toString('hex');
|
||||
refreshToken = Buffer.from(refreshToken || '', 'base64').toString('hex');
|
||||
}
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
OAuth20: {
|
||||
access_token: {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import express from 'express';
|
||||
import xmlbuilder from 'xmlbuilder';
|
||||
import fs from 'fs-extra';
|
||||
import { getServerByTitleId, getServerByGameServerId } from '@/database';
|
||||
import { generateToken, getValueFromHeaders, getValueFromQueryString } from '@/util';
|
||||
import { getServicePublicKey, getServiceSecretKey, getNEXPublicKey, getNEXSecretKey } from '@/cache';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
import { CryptoOptions } from '@/types/common/crypto-options';
|
||||
import { TokenOptions } from '@/types/common/token-options';
|
||||
import { HydratedPNIDDocument } from '@/types/mongoose/pnid';
|
||||
import { HydratedServerDocument } from '@/types/mongoose/server';
|
||||
@@ -35,9 +32,9 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
|
||||
}).end());
|
||||
}
|
||||
|
||||
const titleId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-title-id');
|
||||
const titleID: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-title-id');
|
||||
|
||||
if (!titleId) {
|
||||
if (!titleID) {
|
||||
// TODO - Research this error more
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
@@ -50,9 +47,9 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
|
||||
}
|
||||
|
||||
const serverAccessLevel: string = pnid.server_access_level;
|
||||
const server: HydratedServerDocument | null = await getServerByTitleId(titleId, serverAccessLevel);
|
||||
const server: HydratedServerDocument | null = await getServerByTitleId(titleID, serverAccessLevel);
|
||||
|
||||
if (!server) {
|
||||
if (!server || !server.aes_key) {
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
@@ -63,46 +60,20 @@ router.get('/service_token/@me', async (request: express.Request, response: expr
|
||||
}).end());
|
||||
}
|
||||
|
||||
const serverName: string = server.service_name;
|
||||
const device: number = server.device;
|
||||
|
||||
const cryptoPath: string = `${__dirname}/../../../../certs/service/${serverName}`;
|
||||
|
||||
if (!await fs.pathExists(cryptoPath)) {
|
||||
// Need to generate keys
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
code: '1021',
|
||||
message: 'The requested game server was not found'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
}
|
||||
|
||||
const publicKey: Buffer = await getServicePublicKey(serverName);
|
||||
const secretKey: Buffer = await getServiceSecretKey(serverName);
|
||||
|
||||
const cryptoOptions: CryptoOptions = {
|
||||
public_key: publicKey,
|
||||
hmac_secret: secretKey
|
||||
};
|
||||
|
||||
const tokenOptions: TokenOptions = {
|
||||
system_type: device,
|
||||
token_type: 0x4, // service token,
|
||||
system_type: server.device,
|
||||
token_type: 0x4, // * Service token
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(parseInt(titleId, 16)),
|
||||
title_id: BigInt(parseInt(titleID, 16)),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
let serviceToken: string | null = await generateToken(cryptoOptions, tokenOptions);
|
||||
|
||||
// TODO - Handle null tokens
|
||||
const serviceTokenBuffer: Buffer | null = await generateToken(server.aes_key, tokenOptions);
|
||||
let serviceToken: string = serviceTokenBuffer ? serviceTokenBuffer.toString('base64') : '';
|
||||
|
||||
if (request.isCemu) {
|
||||
serviceToken = Buffer.from(serviceToken || '', 'base64').toString('hex');
|
||||
serviceToken = Buffer.from(serviceToken, 'base64').toString('hex');
|
||||
}
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
@@ -134,82 +105,6 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
|
||||
}).end());
|
||||
}
|
||||
|
||||
const gameServerID: string | undefined = getValueFromQueryString(request.query, 'game_server_id');
|
||||
|
||||
if (!gameServerID) {
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
code: '0118',
|
||||
message: 'Unique ID and Game Server ID are not linked'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
}
|
||||
|
||||
const serverAccessLevel: string = pnid.server_access_level;
|
||||
const server: HydratedServerDocument | null = await getServerByGameServerId(gameServerID, serverAccessLevel);
|
||||
|
||||
if (!server) {
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
code: '1021',
|
||||
message: 'The requested game server was not found'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
}
|
||||
|
||||
const serverName: string = server.service_name;
|
||||
const ip: string = server.ip;
|
||||
const port: number = server.port;
|
||||
const device: number = server.device;
|
||||
const titleId: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-title-id');
|
||||
|
||||
if (!titleId) {
|
||||
// TODO - Research this error more
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
code: '1021',
|
||||
message: 'The requested game server was not found'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
}
|
||||
|
||||
const cryptoPath: string = `${__dirname}/../../../../certs/nex/${serverName}`;
|
||||
|
||||
if (!await fs.pathExists(cryptoPath)) {
|
||||
// Need to generate keys
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
code: '1021',
|
||||
message: 'The requested game server was not found'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
}
|
||||
|
||||
const publicKey: Buffer = await getNEXPublicKey(serverName);
|
||||
const secretKey: Buffer = await getNEXSecretKey(serverName);
|
||||
|
||||
const cryptoOptions: CryptoOptions = {
|
||||
public_key: publicKey,
|
||||
hmac_secret: secretKey
|
||||
};
|
||||
|
||||
const tokenOptions: TokenOptions = {
|
||||
system_type: device,
|
||||
token_type: 0x3, // nex token,
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(parseInt(titleId, 16)),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
const nexAccount: HydratedNEXAccountDocument | null = await NEXAccount.findOne({
|
||||
owning_pid: pnid.pid
|
||||
});
|
||||
@@ -226,9 +121,58 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
|
||||
}).end());
|
||||
}
|
||||
|
||||
let nexToken: string | null = await generateToken(cryptoOptions, tokenOptions);
|
||||
const gameServerID: string | undefined = getValueFromQueryString(request.query, 'game_server_id');
|
||||
|
||||
// TODO = Handle null tokens
|
||||
if (!gameServerID) {
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
code: '0118',
|
||||
message: 'Unique ID and Game Server ID are not linked'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
}
|
||||
|
||||
const serverAccessLevel: string = pnid.server_access_level;
|
||||
const server: HydratedServerDocument | null = await getServerByGameServerId(gameServerID, serverAccessLevel);
|
||||
|
||||
if (!server || !server.aes_key) {
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
code: '1021',
|
||||
message: 'The requested game server was not found'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
}
|
||||
|
||||
const titleID: string | undefined = getValueFromHeaders(request.headers, 'x-nintendo-title-id');
|
||||
|
||||
if (!titleID) {
|
||||
// TODO - Research this error more
|
||||
return response.send(xmlbuilder.create({
|
||||
errors: {
|
||||
error: {
|
||||
code: '1021',
|
||||
message: 'The requested game server was not found'
|
||||
}
|
||||
}
|
||||
}).end());
|
||||
}
|
||||
|
||||
const tokenOptions: TokenOptions = {
|
||||
system_type: server.device,
|
||||
token_type: 0x3, // nex token,
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(parseInt(titleID, 16)),
|
||||
expire_time: BigInt(Date.now() + (3600 * 1000))
|
||||
};
|
||||
|
||||
const nexTokenBuffer: Buffer | null = await generateToken(server.aes_key, tokenOptions);
|
||||
let nexToken: string = nexTokenBuffer ? nexTokenBuffer.toString('base64') : '';
|
||||
|
||||
if (request.isCemu) {
|
||||
nexToken = Buffer.from(nexToken || '', 'base64').toString('hex');
|
||||
@@ -236,10 +180,10 @@ router.get('/nex_token/@me', async (request: express.Request, response: express.
|
||||
|
||||
response.send(xmlbuilder.create({
|
||||
nex_token: {
|
||||
host: ip,
|
||||
host: server.ip,
|
||||
nex_password: nexAccount.password,
|
||||
pid: nexAccount.pid,
|
||||
port: port,
|
||||
port: server.port,
|
||||
token: nexToken
|
||||
}
|
||||
}).end());
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface Config {
|
||||
base_url: string;
|
||||
};
|
||||
website_base: string;
|
||||
aes_key: string;
|
||||
}
|
||||
|
||||
export interface DisabledFeatures {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface CryptoOptions {
|
||||
public_key: Buffer;
|
||||
hmac_secret: Buffer;
|
||||
};
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Model, HydratedDocument } from 'mongoose';
|
||||
|
||||
export interface IServer {
|
||||
ip: string; // Example: 1.1.1.1
|
||||
port: number; // Example: 60000
|
||||
service_name: string; // Example: friends
|
||||
service_type: string; // Example: nex
|
||||
game_server_id: string; // Example: 00003200
|
||||
title_ids: string[]; // Example: ["000500001018DB00", "000500001018DC00", "000500001018DD00"]
|
||||
access_mode: string; // Example: prod
|
||||
maintenance_mode: boolean; // Example: false
|
||||
device: number; // Example: 1 (WiiU)
|
||||
ip: string;
|
||||
port: number;
|
||||
service_name: string;
|
||||
service_type: string;
|
||||
game_server_id: string;
|
||||
title_ids: string[];
|
||||
access_mode: string;
|
||||
maintenance_mode: boolean;
|
||||
device: number;
|
||||
aes_key: string;
|
||||
}
|
||||
|
||||
export interface IServerMethods {}
|
||||
|
||||
205
src/util.ts
205
src/util.ts
@@ -1,15 +1,12 @@
|
||||
import crypto from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import NodeRSA from 'node-rsa';
|
||||
import aws from 'aws-sdk';
|
||||
import fs from 'fs-extra';
|
||||
import express from 'express';
|
||||
import mongoose from 'mongoose';
|
||||
import { ParsedQs } from 'qs';
|
||||
import { sendMail } from '@/mailer';
|
||||
import { getServiceAESKey, getServicePrivateKey, getServiceSecretKey, getServicePublicKey } from '@/cache';
|
||||
import { config, disabledFeatures } from '@/config-manager';
|
||||
import { CryptoOptions } from '@/types/common/crypto-options';
|
||||
import { TokenOptions } from '@/types/common/token-options';
|
||||
import { Token } from '@/types/common/token';
|
||||
import { IPNID, IPNIDMethods } from '@/types/mongoose/pnid';
|
||||
@@ -50,178 +47,61 @@ export function nintendoBase64Encode(decoded: string | Buffer): string {
|
||||
return encoded.replaceAll('+', '.').replaceAll('/', '-').replaceAll('=', '*');
|
||||
}
|
||||
|
||||
export async function generateToken(cryptoOptions: CryptoOptions | null, tokenOptions: TokenOptions): Promise<string | null> {
|
||||
// Access and refresh tokens use a different format since they must be much smaller
|
||||
// They take no extra crypto options
|
||||
if (!cryptoOptions) {
|
||||
const aesKey: Buffer = await getServiceAESKey('account', 'hex');
|
||||
export function generateToken(key: string, options: TokenOptions): Buffer | null {
|
||||
let dataBuffer: Buffer = Buffer.alloc(1 + 1 + 4 + 8);
|
||||
|
||||
const dataBuffer: Buffer = Buffer.alloc(1 + 1 + 4 + 8);
|
||||
dataBuffer.writeUInt8(options.system_type, 0x0);
|
||||
dataBuffer.writeUInt8(options.token_type, 0x1);
|
||||
dataBuffer.writeUInt32LE(options.pid, 0x2);
|
||||
dataBuffer.writeBigUInt64LE(options.expire_time, 0x6);
|
||||
|
||||
dataBuffer.writeUInt8(tokenOptions.system_type, 0x0);
|
||||
dataBuffer.writeUInt8(tokenOptions.token_type, 0x1);
|
||||
dataBuffer.writeUInt32LE(tokenOptions.pid, 0x2);
|
||||
dataBuffer.writeBigUInt64LE(tokenOptions.expire_time, 0x6);
|
||||
if (options.token_type !== 0x1 && options.token_type !== 0x2) {
|
||||
if (options.access_level === undefined || options.title_id === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iv: Buffer = Buffer.alloc(16);
|
||||
const cipher: crypto.Cipher = crypto.createCipheriv('aes-128-cbc', aesKey, iv);
|
||||
dataBuffer = Buffer.concat([
|
||||
dataBuffer,
|
||||
Buffer.alloc(8 + 1)
|
||||
]);
|
||||
|
||||
let encryptedBody: Buffer = cipher.update(dataBuffer);
|
||||
encryptedBody = Buffer.concat([encryptedBody, cipher.final()]);
|
||||
|
||||
return encryptedBody.toString('base64');
|
||||
} else if (tokenOptions.access_level === undefined || tokenOptions.title_id === undefined) {
|
||||
return null;
|
||||
dataBuffer.writeBigUInt64LE(options.title_id, 0xE);
|
||||
dataBuffer.writeUInt8(options.access_level, 0x16);
|
||||
}
|
||||
|
||||
const publicKey: NodeRSA = new NodeRSA(cryptoOptions.public_key, 'pkcs8-public-pem', {
|
||||
environment: 'browser',
|
||||
encryptionScheme: {
|
||||
scheme: 'pkcs1_oaep',
|
||||
hash: 'sha256'
|
||||
}
|
||||
});
|
||||
const iv: Buffer = Buffer.alloc(16);
|
||||
const cipher: crypto.Cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
|
||||
|
||||
// Create the buffer containing the token data
|
||||
const dataBuffer: Buffer = Buffer.alloc(1 + 1 + 4 + 1 + 8 + 8);
|
||||
|
||||
dataBuffer.writeUInt8(tokenOptions.system_type, 0x0);
|
||||
dataBuffer.writeUInt8(tokenOptions.token_type, 0x1);
|
||||
dataBuffer.writeUInt32LE(tokenOptions.pid, 0x2);
|
||||
dataBuffer.writeUInt8(tokenOptions.access_level, 0x6);
|
||||
dataBuffer.writeBigUInt64LE(tokenOptions.title_id, 0x7);
|
||||
dataBuffer.writeBigUInt64LE(tokenOptions.expire_time, 0xF);
|
||||
|
||||
// Calculate the signature of the token body
|
||||
const hmac: crypto.Hmac = crypto.createHmac('sha1', cryptoOptions.hmac_secret).update(dataBuffer);
|
||||
const signature: Buffer = hmac.digest();
|
||||
|
||||
// You can thank the 3DS for the shit thats about to happen with the AES IV
|
||||
// The 3DS only allows for strings up to 255 characters in NEX
|
||||
// So this is done to reduce the token size as much as possible
|
||||
// I am sorry, and have already asked every God I could think of for forgiveness
|
||||
|
||||
// Generate random AES key
|
||||
const key: Buffer = crypto.randomBytes(16);
|
||||
|
||||
// Encrypt the AES key with RSA public key
|
||||
const encryptedKey: Buffer = publicKey.encrypt(key);
|
||||
|
||||
// Take two random points in the RSA encrypted key
|
||||
const point1: number = ~~((encryptedKey.length - 8) * Math.random());
|
||||
const point2: number = ~~((encryptedKey.length - 8) * Math.random());
|
||||
|
||||
// Build an IV from each of the two points
|
||||
const iv: Buffer = Buffer.concat([
|
||||
Buffer.from(encryptedKey.subarray(point1, point1 + 8)),
|
||||
Buffer.from(encryptedKey.subarray(point2, point2 + 8))
|
||||
]);
|
||||
|
||||
const cipher: crypto.Cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
|
||||
|
||||
// Encrypt the token body with AES
|
||||
const encryptedBody: Buffer = Buffer.concat([
|
||||
return Buffer.concat([
|
||||
cipher.update(dataBuffer),
|
||||
cipher.final()
|
||||
]);
|
||||
|
||||
// Create crypto config token section
|
||||
const cryptoConfig: Buffer = Buffer.concat([
|
||||
encryptedKey,
|
||||
Buffer.from([point1, point2])
|
||||
]);
|
||||
|
||||
// Build the token
|
||||
const token: Buffer = Buffer.concat([
|
||||
cryptoConfig,
|
||||
signature,
|
||||
encryptedBody
|
||||
]);
|
||||
|
||||
return token.toString('base64'); // Encode to base64 for transport
|
||||
}
|
||||
|
||||
export async function decryptToken(token: Buffer): Promise<Buffer> {
|
||||
// Access and refresh tokens use a different format since they must be much smaller
|
||||
// Assume a small length means access or refresh token
|
||||
if (token.length <= 32) {
|
||||
const aesKey: Buffer = await getServiceAESKey('account', 'hex');
|
||||
export function decryptToken(key: string, token: Buffer): Buffer {
|
||||
const iv: Buffer = Buffer.alloc(16);
|
||||
const decipher: crypto.Decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
|
||||
|
||||
const iv: Buffer = Buffer.alloc(16);
|
||||
|
||||
const decipher: crypto.Decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv);
|
||||
|
||||
const decryptedBody: Buffer = Buffer.concat([
|
||||
decipher.update(token),
|
||||
decipher.final()
|
||||
]);
|
||||
|
||||
return decryptedBody;
|
||||
}
|
||||
|
||||
const privateKeyBytes: Buffer = await getServicePrivateKey('account');
|
||||
const secretKey: Buffer = await getServiceSecretKey('account');
|
||||
|
||||
const privateKey: NodeRSA = new NodeRSA(privateKeyBytes, 'pkcs1-private-pem', {
|
||||
environment: 'browser',
|
||||
encryptionScheme: {
|
||||
scheme: 'pkcs1_oaep',
|
||||
hash: 'sha256'
|
||||
}
|
||||
});
|
||||
|
||||
const cryptoConfig: Buffer = token.subarray(0, 0x82);
|
||||
const signature: Buffer = token.subarray(0x82, 0x96);
|
||||
const encryptedBody: Buffer = token.subarray(0x96);
|
||||
|
||||
const encryptedAESKey: Buffer = cryptoConfig.subarray(0, 128);
|
||||
const point1: number = cryptoConfig.readInt8(0x80);
|
||||
const point2: number = cryptoConfig.readInt8(0x81);
|
||||
|
||||
const iv: Buffer = Buffer.concat([
|
||||
Buffer.from(encryptedAESKey.subarray(point1, point1 + 8)),
|
||||
Buffer.from(encryptedAESKey.subarray(point2, point2 + 8))
|
||||
]);
|
||||
|
||||
const decryptedAESKey: Buffer = privateKey.decrypt(encryptedAESKey);
|
||||
|
||||
const decipher: crypto.Decipher = crypto.createDecipheriv('aes-128-cbc', decryptedAESKey, iv);
|
||||
|
||||
const decryptedBody: Buffer = Buffer.concat([
|
||||
decipher.update(encryptedBody),
|
||||
return Buffer.concat([
|
||||
decipher.update(token),
|
||||
decipher.final()
|
||||
]);
|
||||
|
||||
const hmac: crypto.Hmac = crypto.createHmac('sha1', secretKey).update(decryptedBody);
|
||||
const calculatedSignature: Buffer = hmac.digest();
|
||||
|
||||
if (!signature.equals(calculatedSignature)) {
|
||||
// TODO - FIX THIS. ONLY DONE SO STRICT MODE DOESN'T YELL
|
||||
console.log('Token signature did not match');
|
||||
return Buffer.alloc(0);
|
||||
}
|
||||
|
||||
return decryptedBody;
|
||||
}
|
||||
|
||||
export function unpackToken(token: Buffer): Token {
|
||||
if (token.length <= 14) {
|
||||
return {
|
||||
system_type: token.readUInt8(0x0),
|
||||
token_type: token.readUInt8(0x1),
|
||||
pid: token.readUInt32LE(0x2),
|
||||
expire_time: token.readBigUInt64LE(0x6)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
const unpacked: Token = {
|
||||
system_type: token.readUInt8(0x0),
|
||||
token_type: token.readUInt8(0x1),
|
||||
pid: token.readUInt32LE(0x2),
|
||||
access_level: token.readUInt8(0x6),
|
||||
title_id: token.readBigUInt64LE(0x7),
|
||||
expire_time: token.readBigUInt64LE(0xF)
|
||||
expire_time: token.readBigUInt64LE(0x6)
|
||||
};
|
||||
|
||||
if (unpacked.token_type !== 0x1 && unpacked.token_type !== 0x2) {
|
||||
unpacked.title_id = token.readBigUInt64LE(0xE);
|
||||
unpacked.access_level = token.readUInt8(0x16);
|
||||
}
|
||||
|
||||
return unpacked;
|
||||
}
|
||||
|
||||
export function fullUrl(request: express.Request): string {
|
||||
@@ -289,24 +169,17 @@ export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument<IP
|
||||
}
|
||||
|
||||
export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
|
||||
const publicKey: Buffer = await getServicePublicKey('account');
|
||||
const secretKey: Buffer = await getServiceSecretKey('account');
|
||||
|
||||
const cryptoOptions: CryptoOptions = {
|
||||
public_key: publicKey,
|
||||
hmac_secret: secretKey
|
||||
};
|
||||
|
||||
const tokenOptions: TokenOptions = {
|
||||
system_type: 0xF, // API
|
||||
token_type: 0x5, // Password reset
|
||||
system_type: 0xF, // * API
|
||||
token_type: 0x5, // * Password reset
|
||||
pid: pnid.pid,
|
||||
access_level: pnid.access_level,
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (24 * 60 * 60 * 1000)) // Only valid for 24 hours
|
||||
};
|
||||
|
||||
const passwordResetToken: string | null = await generateToken(cryptoOptions, tokenOptions);
|
||||
const tokenBuffer: Buffer | null = await generateToken(config.aes_key, tokenOptions);
|
||||
const passwordResetToken: string = tokenBuffer ? tokenBuffer.toString('hex') : '';
|
||||
|
||||
// TODO - Handle null token
|
||||
|
||||
@@ -317,9 +190,9 @@ export async function sendForgotPasswordEmail(pnid: mongoose.HydratedDocument<IP
|
||||
paragraph: 'a password reset has been requested from this account. If you did not request the password reset, please ignore this email. If you did request this password reset, please click the link below to reset your password.',
|
||||
link: {
|
||||
text: 'Reset password',
|
||||
href: `${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken || '')}`
|
||||
href: `${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken)}`
|
||||
},
|
||||
text: `Dear ${pnid.username}, a password reset has been requested from this account. \r\n\r\nIf you did not request the password reset, please ignore this email. \r\nIf you did request this password reset, please click the link to reset your password: ${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken || '')}`
|
||||
text: `Dear ${pnid.username}, a password reset has been requested from this account. \r\n\r\nIf you did not request the password reset, please ignore this email. \r\nIf you did request this password reset, please click the link to reset your password: ${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken)}`
|
||||
};
|
||||
|
||||
await sendMail(mailerOptions);
|
||||
|
||||
Reference in New Issue
Block a user