diff --git a/config/config-example.js b/config/config-example.js index 3e281cc..1737062 100644 --- a/config/config-example.js +++ b/config/config-example.js @@ -151,6 +151,12 @@ exports.watchconfig = true; */ exports.restartip = null; +/** + * An IP to allow Smogon acc-linking requests from. + * @type {null | string} + */ +exports.smogonip = null; + /** * Custom actions for your loginserver. * @type {{[k: string]: import('../src/server').QueryHandler} | null} diff --git a/src/actions.ts b/src/actions.ts index 5ab52e3..25d2492 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -13,12 +13,15 @@ import {Ladder} from './ladder'; import {Replays} from './replays'; import {ActionError, QueryHandler, Server} from './server'; import {Session} from './user'; -import {toID, updateserver, bash, time, escapeHTML} from './utils'; +import { + toID, updateserver, bash, time, escapeHTML, encrypt, decrypt, makeEncryptKey, +} from './utils'; import * as tables from './tables'; import {SQL} from './database'; import IPTools from './ip-tools'; const OAUTH_TOKEN_TIME = 2 * 7 * 24 * 60 * 60 * 1000; +const SMOGON_KEY = makeEncryptKey(); async function getOAuthClient(clientId?: string, origin?: string) { if (!clientId) throw new ActionError("No client_id provided."); @@ -902,6 +905,29 @@ export const actions: {[k: string]: QueryHandler} = { } return {password: pw}; }, + + // sent by ps server + 'smogon/encrypt'(params) { + if (this.getIp() !== Config.restartip) { + throw new ActionError("Access denied."); + } + params.username = toID(params.username); + if (!params.username) { + throw new ActionError("Invalid PS username provided."); + } + return {encrypted_username: encrypt(SMOGON_KEY, params.username)}; + }, + + // sent by smogon to validate given encrypted name + 'smogon/validate'(params) { + if (this.getIp() !== Config.smogonip) { + throw new ActionError("Access denied."); + } + if (!params.encrypted_name || !toID(params.encrypted_name)) { + throw new ActionError("No encrypted name provided."); + } + return {decrypted_name: decrypt(SMOGON_KEY, params.encrypted_name)}; + }, }; if (Config.actions) { diff --git a/src/utils.ts b/src/utils.ts index 66538bb..d70520c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -104,3 +104,46 @@ export function escapeHTML(str: string | number) { .replace(/"/g, '"') .replace(/'/g, '''); } + +const IV_LENGTH = 16; + +const NONCE_LENGTH = 20; + +export function encrypt(key: Buffer, text: string) { + const nonce = crypto.randomBytes(NONCE_LENGTH); + const iv = Buffer.alloc(IV_LENGTH); + nonce.copy(iv); + + const cipher = crypto.createCipheriv('aes-256-ctr', key, iv); + const encrypted = cipher.update(text.toString()); + return Buffer.concat([nonce, encrypted, cipher.final()]).toString('base64'); +} + +export function decrypt(key: Buffer, text: string) { + const message = Buffer.from(text, 'base64'); + const iv = Buffer.alloc(IV_LENGTH); + message.copy(iv, 0, 0, NONCE_LENGTH); + const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv); + let decrypted = decipher.update(message.slice(NONCE_LENGTH)); + try { + decrypted = Buffer.concat([decrypted, decipher.final()]); + return decrypted.toString(); + } catch (err) { + return null; + } +} + +// 32 chars - 256 bytes +export function makeEncryptKey(len = 32) { + let chars = 'abcdefghijklmnopqrstuvwxyz'; + chars += chars.toUpperCase(); + chars += "1234567890"; + chars += "()-={}|!@#$%^&*?><:"; + + let key = ""; + for (let i = 0; i < len; i++) { + key += chars[Math.round(Math.random() * chars.length)]; + } + + return crypto.pbkdf2Sync(key, Math.random() + "", 10000, len, 'sha512'); +}