diff --git a/src/services/api/routes/v1/register.ts b/src/services/api/routes/v1/register.ts index 87d05e2..308828f 100644 --- a/src/services/api/routes/v1/register.ts +++ b/src/services/api/routes/v1/register.ts @@ -6,7 +6,22 @@ import moment from 'moment'; import hcaptcha from 'hcaptcha'; import Mii from 'mii-js'; import { doesPNIDExist, connection as databaseConnection } from '@/database'; -import { isValidBirthday, getAgeFromDate, nintendoPasswordHash, sendConfirmationEmail } from '@/util'; +import { + isValidBirthday, + getAgeFromDate, + checkNNIDUsernameMinLength, + checkNNIDUsernameMaxLength, + checkNNIDUsernameValidCharacters, + checkNNIDUsernamePunctuationStart, + checkNNIDUsernamePunctuationEnd, + checkNNIDUsernameDuplicate, + checkNNIDPasswordMinLength, + checkNNIDPasswordMaxLength, + checkNNIDPasswordAtLeast2CharacterGroups, + checkNNIDPasswordRepeatCharacters, + nintendoPasswordHash, + sendConfirmationEmail +} from '@/util'; import IP2LocationManager from '@/ip2location'; import { SystemType } from '@/types/common/system-types'; import { TokenType } from '@/types/common/token-types'; @@ -21,17 +36,6 @@ import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; const router = express.Router(); -const PNID_VALID_CHARACTERS_REGEX = /^[\w\-.]*$/; -const PNID_PUNCTUATION_START_REGEX = /^[_\-.]/; -const PNID_PUNCTUATION_END_REGEX = /[_\-.]$/; -const PNID_PUNCTUATION_DUPLICATE_REGEX = /[_\-.]{2,}/; - -// * This sucks -const PASSWORD_WORD_OR_NUMBER_REGEX = /(?=.*[a-zA-Z])(?=.*\d).*/; -const PASSWORD_WORD_OR_PUNCTUATION_REGEX = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/; -const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX = /(?=.*\d)(?=.*[_\-.]).*/; -const PASSWORD_REPEATED_CHARACTER_REGEX = /(.)\1\1/; - const DEFAULT_MII_DATA = Buffer.from('AwAAQOlVognnx0GC2/uogAOzuI0n2QAAAEBEAGUAZgBhAHUAbAB0AAAAAAAAAEBAAAAhAQJoRBgmNEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGm9', 'base64'); /** @@ -166,7 +170,7 @@ router.post('/', webRegisterRatelimit, async (request: express.Request, response return; } - if (username.length < 6) { + if (!checkNNIDUsernameMinLength(username)) { response.status(400).json({ app: 'api', status: 400, @@ -176,7 +180,7 @@ router.post('/', webRegisterRatelimit, async (request: express.Request, response return; } - if (username.length > 16) { + if (!checkNNIDUsernameMaxLength(username)) { response.status(400).json({ app: 'api', status: 400, @@ -186,8 +190,7 @@ router.post('/', webRegisterRatelimit, async (request: express.Request, response return; } - if (!PNID_VALID_CHARACTERS_REGEX.test(username)) { - console.log(Buffer.from(username)); + if (!checkNNIDUsernameValidCharacters(username)) { response.status(400).json({ app: 'api', status: 400, @@ -197,7 +200,7 @@ router.post('/', webRegisterRatelimit, async (request: express.Request, response return; } - if (PNID_PUNCTUATION_START_REGEX.test(username)) { + if (!checkNNIDUsernamePunctuationStart(username)) { response.status(400).json({ app: 'api', status: 400, @@ -207,7 +210,7 @@ router.post('/', webRegisterRatelimit, async (request: express.Request, response return; } - if (PNID_PUNCTUATION_END_REGEX.test(username)) { + if (!checkNNIDUsernamePunctuationEnd(username)) { response.status(400).json({ app: 'api', status: 400, @@ -217,7 +220,7 @@ router.post('/', webRegisterRatelimit, async (request: express.Request, response return; } - if (PNID_PUNCTUATION_DUPLICATE_REGEX.test(username)) { + if (!checkNNIDUsernameDuplicate(username)) { response.status(400).json({ app: 'api', status: 400, @@ -259,7 +262,7 @@ router.post('/', webRegisterRatelimit, async (request: express.Request, response return; } - if (password.length < 6) { + if (!checkNNIDPasswordMinLength(password)) { response.status(400).json({ app: 'api', status: 400, @@ -269,7 +272,7 @@ router.post('/', webRegisterRatelimit, async (request: express.Request, response return; } - if (password.length > 16) { + if (!checkNNIDPasswordMaxLength(password)) { response.status(400).json({ app: 'api', status: 400, @@ -289,7 +292,7 @@ router.post('/', webRegisterRatelimit, async (request: express.Request, response return; } - if (!PASSWORD_WORD_OR_NUMBER_REGEX.test(password) && !PASSWORD_WORD_OR_PUNCTUATION_REGEX.test(password) && !PASSWORD_NUMBER_OR_PUNCTUATION_REGEX.test(password)) { + if (!checkNNIDPasswordAtLeast2CharacterGroups(password)) { response.status(400).json({ app: 'api', status: 400, @@ -299,7 +302,7 @@ router.post('/', webRegisterRatelimit, async (request: express.Request, response return; } - if (PASSWORD_REPEATED_CHARACTER_REGEX.test(password)) { + if (!checkNNIDPasswordRepeatCharacters(password)) { response.status(400).json({ app: 'api', status: 400, diff --git a/src/services/grpc/api/v1/register.ts b/src/services/grpc/api/v1/register.ts index cf8bbf2..2fe35c6 100644 --- a/src/services/grpc/api/v1/register.ts +++ b/src/services/grpc/api/v1/register.ts @@ -6,7 +6,20 @@ import moment from 'moment'; import hcaptcha from 'hcaptcha'; import Mii from 'mii-js'; import { doesPNIDExist, connection as databaseConnection } from '@/database'; -import { nintendoPasswordHash, sendConfirmationEmail } from '@/util'; +import { + checkNNIDUsernameMinLength, + checkNNIDUsernameMaxLength, + checkNNIDUsernameValidCharacters, + checkNNIDUsernamePunctuationStart, + checkNNIDUsernamePunctuationEnd, + checkNNIDUsernameDuplicate, + checkNNIDPasswordMinLength, + checkNNIDPasswordMaxLength, + checkNNIDPasswordAtLeast2CharacterGroups, + checkNNIDPasswordRepeatCharacters, + nintendoPasswordHash, + sendConfirmationEmail +} from '@/util'; import { LOG_ERROR } from '@/logger'; import { PNID } from '@/models/pnid'; import { NEXAccount } from '@/models/nex-account'; @@ -19,17 +32,6 @@ import type { RegisterRequest, DeepPartial } from '@pretendonetwork/grpc/api/reg import type { HydratedNEXAccountDocument } from '@/types/mongoose/nex-account'; import type { HydratedPNIDDocument } from '@/types/mongoose/pnid'; -const PNID_VALID_CHARACTERS_REGEX = /^[\w\-.]*$/; -const PNID_PUNCTUATION_START_REGEX = /^[_\-.]/; -const PNID_PUNCTUATION_END_REGEX = /[_\-.]$/; -const PNID_PUNCTUATION_DUPLICATE_REGEX = /[_\-.]{2,}/; - -// * This sucks -const PASSWORD_WORD_OR_NUMBER_REGEX = /(?=.*[a-zA-Z])(?=.*\d).*/; -const PASSWORD_WORD_OR_PUNCTUATION_REGEX = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/; -const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX = /(?=.*\d)(?=.*[_\-.]).*/; -const PASSWORD_REPEATED_CHARACTER_REGEX = /(.)\1\1/; - const DEFAULT_MII_DATA = Buffer.from('AwAAQOlVognnx0GC2/uogAOzuI0n2QAAAEBEAGUAZgBhAHUAbAB0AAAAAAAAAEBAAAAhAQJoRBgmNEYUgRIXaA0AACkAUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGm9', 'base64'); export async function register(request: RegisterRequest): Promise> { @@ -65,27 +67,27 @@ export async function register(request: RegisterRequest): Promise 16) { + if (!checkNNIDUsernameMaxLength(username)) { throw new ServerError(Status.INVALID_ARGUMENT, 'Username is too long'); } - if (!PNID_VALID_CHARACTERS_REGEX.test(username)) { + if (!checkNNIDUsernameValidCharacters(username)) { throw new ServerError(Status.INVALID_ARGUMENT, 'Username contains invalid characters'); } - if (PNID_PUNCTUATION_START_REGEX.test(username)) { + if (!checkNNIDUsernamePunctuationStart(username)) { throw new ServerError(Status.INVALID_ARGUMENT, 'Username cannot begin with punctuation characters'); } - if (PNID_PUNCTUATION_END_REGEX.test(username)) { + if (!checkNNIDUsernamePunctuationEnd(username)) { throw new ServerError(Status.INVALID_ARGUMENT, 'Username cannot end with punctuation characters'); } - if (PNID_PUNCTUATION_DUPLICATE_REGEX.test(username)) { + if (!checkNNIDUsernameDuplicate(username)) { throw new ServerError(Status.INVALID_ARGUMENT, 'Two or more punctuation characters cannot be used in a row'); } @@ -109,7 +111,7 @@ export async function register(request: RegisterRequest): Promise 16) { + if (!checkNNIDPasswordMinLength(password) || !checkNNIDPasswordMaxLength(password)) { throw new ServerError(Status.INVALID_ARGUMENT, 'Password must be between 6 and 16 characters long'); } @@ -117,11 +119,11 @@ export async function register(request: RegisterRequest): Promise 16) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { - cause: 'password', - code: '0002', - message: 'password format is invalid' - } - } - }).end()); - - return; - } - - if (!PASSWORD_WORD_OR_NUMBER_REGEX.test(person.password) && !PASSWORD_WORD_OR_PUNCTUATION_REGEX.test(person.password) && !PASSWORD_NUMBER_OR_PUNCTUATION_REGEX.test(person.password)) { - response.status(400).send(xmlbuilder.create({ - errors: { - error: { - cause: 'password', - code: '0002', - message: 'password format is invalid' - } - } - }).end()); - - return; - } - - if (PASSWORD_REPEATED_CHARACTER_REGEX.test(person.password)) { + if (!person.password || !checkNNIDPasswordValid(person.password)) { response.status(400).send(xmlbuilder.create({ errors: { error: { diff --git a/src/util.ts b/src/util.ts index 10e5e12..9ab7835 100644 --- a/src/util.ts +++ b/src/util.ts @@ -35,6 +35,88 @@ if (!disabledFeatures.s3) { }); } +const NNID_VALID_CHARACTERS_REGEX = /^[\w\-.]*$/; +const NNID_PUNCTUATION_START_REGEX = /^[_\-.]/; +const NNID_PUNCTUATION_END_REGEX = /[_\-.]$/; +const NNID_PUNCTUATION_DUPLICATE_REGEX = /[_\-.]{2,}/; + +// * This sucks +const PASSWORD_WORD_OR_NUMBER_REGEX = /(?=.*[a-zA-Z])(?=.*\d).*/; +const PASSWORD_WORD_OR_PUNCTUATION_REGEX = /(?=.*[a-zA-Z])(?=.*[_\-.]).*/; +const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX = /(?=.*\d)(?=.*[_\-.]).*/; +const PASSWORD_REPEATED_CHARACTER_REGEX = /(.)\1\1/; + +// * Checks if the input NNID username passes all validation rules +export function checkNNIDUsernameValid(username: string): boolean { + return checkNNIDUsernameLength(username) && checkNNIDUsernameValidCharacters(username) && checkNNIDUsernamePunctuationStart(username) && checkNNIDUsernamePunctuationEnd(username) && checkNNIDUsernameDuplicate(username); +} + +// * Ensures the NNID username meets the expected size requirements +export function checkNNIDUsernameLength(username: string): boolean { + return checkNNIDUsernameMinLength(username) && checkNNIDUsernameMaxLength(username); +} + +// * Ensures the NNID username is at least 6 characters long +export function checkNNIDUsernameMinLength(username: string): boolean { + return username.length >= 6; +} + +// * Ensures the NNID username is at most 16 characters long +export function checkNNIDUsernameMaxLength(username: string): boolean { + return username.length <= 16; +} + +// * Ensures the NNID username only contains valid characters +export function checkNNIDUsernameValidCharacters(username: string): boolean { + return NNID_VALID_CHARACTERS_REGEX.test(username); +} + +// * Ensures the NNID username does not start with a punctuation character +export function checkNNIDUsernamePunctuationStart(username: string): boolean { + return !NNID_PUNCTUATION_START_REGEX.test(username); +} + +// * Ensures the NNID username does not end with a punctuation character +export function checkNNIDUsernamePunctuationEnd(username: string): boolean { + return !NNID_PUNCTUATION_END_REGEX.test(username); +} + +// * Ensures the NNID username does not use a punctuation character more than twice in a row +export function checkNNIDUsernameDuplicate(username: string): boolean { + return !NNID_PUNCTUATION_DUPLICATE_REGEX.test(username); +} + +// * Checks if the input NNID password passes all validation rules +export function checkNNIDPasswordValid(password: string): boolean { + return checkNNIDPasswordLength(password) && checkNNIDPasswordAtLeast2CharacterGroups(password) && checkNNIDPasswordRepeatCharacters(password); +} + +// * Ensures the NNID password meets the expected size requirements +export function checkNNIDPasswordLength(password: string): boolean { + return checkNNIDPasswordMinLength(password) && checkNNIDPasswordMaxLength(password); +} + +// * Ensures the NNID password is at least 6 characters long +export function checkNNIDPasswordMinLength(password: string): boolean { + return password.length >= 6; +} + +// * Ensures the NNID password is at most 16 characters long +export function checkNNIDPasswordMaxLength(password: string): boolean { + return password.length <= 16; +} + +// * Ensures the NNID password has a mix of letters, numbers, and/or punctuation characters. +// * Passwords must contain letters from at least 2 of the 3 groups (letters, numbers, and punctuation) +export function checkNNIDPasswordAtLeast2CharacterGroups(password: string): boolean { + return PASSWORD_WORD_OR_NUMBER_REGEX.test(password) || PASSWORD_WORD_OR_PUNCTUATION_REGEX.test(password) || PASSWORD_NUMBER_OR_PUNCTUATION_REGEX.test(password); +} + +// * Ensures the NNID password does not have 3 or more repeating characters +export function checkNNIDPasswordRepeatCharacters(password: string): boolean { + return PASSWORD_REPEATED_CHARACTER_REGEX.test(password); +} + export function nintendoPasswordHash(password: string, pid: number): string { const pidBuffer = Buffer.alloc(4); pidBuffer.writeUInt32LE(pid);