diff --git a/scenes/authentication/authenticationScene.js b/scenes/authentication/authenticationScene.js
index aafb79a..77b8d9f 100755
--- a/scenes/authentication/authenticationScene.js
+++ b/scenes/authentication/authenticationScene.js
@@ -1,8 +1,10 @@
-import { PasswordPrompt } from "./passwordPrompt.js";
-import { Scene } from "../scene.js";
-import { UsernamePrompt } from "./usernamePrompt.js";
-import { PlayerCreationScene } from "../playerCreation/playerCreationSene.js";
+import { Security } from "../../utils/security.js";
+import { Config } from "../../config.js";
import { GameScene } from "../gameLoop/gameScene.js";
+import { PlayerCreationScene } from "../playerCreation/playerCreationSene.js";
+import { Prompt } from "../prompt.js";
+import { Scene } from "../scene.js";
+import { gGame } from "../../models/globals.js";
/** @typedef {import("../../models/player.js").Player} Player */
@@ -16,7 +18,6 @@ export class AuthenticationScene extends Scene {
player;
onReady() {
- // current prompt
this.show(UsernamePrompt);
}
@@ -44,3 +45,136 @@ export class AuthenticationScene extends Scene {
this.session.setScene(new PlayerCreationScene(this.session));
}
}
+
+class UsernamePrompt extends Prompt {
+ //
+ promptText = [
+ "Please enter your username:", //
+ "(((type *:create* if you want to create a new user)))", //
+ ];
+
+ //
+ // When player types :help
+ helpText = [
+ "This is where you log in.",
+ "If you don't already have a player profile on this server, you can type *:create* to create one",
+ ];
+
+ //
+ // Let the client know that we're asking for a username
+ promptOptions = { username: true };
+
+ /** @returns {AuthenticationScene} */
+ get scene() {
+ return this._scene;
+ }
+
+ //
+ // User replied to our prompt
+ onReply(username) {
+ //
+ // do basic syntax checks on usernames
+ if (!Security.isUsernameSane(username)) {
+ console.info("Someone entered insane username: '%s'", username);
+ this.sendError("Incorrect username, try again");
+ this.execute();
+ return;
+ }
+
+ //
+ // try and fetch the player object from the game
+ const player = gGame.getPlayerByUsername(username);
+
+ //
+ // handle invalid username
+ if (!player) {
+ console.info("Someone entered incorrect username: '%s'", username);
+ this.sendError("Incorrect username, try again");
+ this.execute();
+ return;
+ }
+
+ //
+ // Tell daddy that we're done
+ this.scene.usernameAccepted(player);
+ }
+}
+
+class PasswordPrompt extends Prompt {
+ //
+ promptText = "Please enter your password";
+
+ //
+ // Let the client know that we're asking for a password
+ // so it can set
+ promptOptions = { password: true };
+
+ get player() {
+ return this.scene.player;
+ }
+
+ /** @returns {AuthenticationScene} */
+ get scene() {
+ return this._scene;
+ }
+
+ onReply(text) {
+ //
+ // Check of the password is sane. This is both bad from a security point
+ // of view, and technically not necessary as insane passwords couldn't
+ // reside in the player lists. However, let's save some CPU cycles on
+ // not hashing an insane password 1000+ times.
+ // This is technically bad practice, but since this is just a game,
+ // do it anyway.
+ if (!Security.isPasswordSane(text)) {
+ this.sendError("Insane password");
+ this.execute();
+ return;
+ }
+
+ //
+ // Block users who enter bad passwords too many times.
+ if (this.player.failedPasswordsSinceLastLogin > Config.maxFailedLogins) {
+ this.blockedUntil = Date.now() + Config.accountLockoutSeconds * 1000;
+ this.calamity("You have been locked out for too many failed password attempts, come back later");
+ return;
+ }
+
+ //
+ // Handle blocked users.
+ // They don't even get to have their password verified.
+ if (this.player.blockedUntil > Date.now()) {
+ //
+ // Try to re-login too soon, and your lockout lasts longer.
+ this.blockedUntil += Config.accountLockoutSeconds * 1000;
+ this.calamity("You have been locked out for too many failed password attempts, come back later");
+ return;
+ }
+
+ //
+ // Verify the password against the hash we've stored.
+ if (!Security.verifyPassword(text, this.player.passwordHash)) {
+ this.sendError("Incorrect password!");
+ this.player.failedPasswordsSinceLastLogin++;
+
+ this.session.sendDebug(`Failed login attempt #${this.player.failedPasswordsSinceLastLogin}`);
+ this.execute();
+
+ return;
+ }
+
+ this.player.lastSucessfulLoginAt = new Date();
+ this.player.failedPasswordsSinceLastLogin = 0;
+
+ //
+ // We do not allow a player to be logged in more than once!
+ if (this.player.loggedIn) {
+ this.calamity("This player is already logged in");
+ return;
+ }
+
+ // Password was correct, go to main game
+ // this.scene.passwordAccepted();
+ this.scene.passwordAccepted();
+ }
+}
diff --git a/scenes/authentication/passwordPrompt.js b/scenes/authentication/passwordPrompt.js
deleted file mode 100755
index 511bad8..0000000
--- a/scenes/authentication/passwordPrompt.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import { Prompt } from "../prompt.js";
-import * as security from "../../utils/security.js";
-import { Config } from "../../config.js";
-
-/** @typedef {import("./authentication.js").AuthenticationScene} AuthenticationScene */
-
-export class PasswordPrompt extends Prompt {
- //
- promptText = "Please enter your password";
-
- //
- // Let the client know that we're asking for a password
- // so it can set
- promptOptions = { password: true };
-
- get player() {
- return this.scene.player;
- }
-
- /** @returns {AuthenticationScene} */
- get scene() {
- return this._scene;
- }
-
- onReply(text) {
- //
- // Check of the password is sane. This is both bad from a security point
- // of view, and technically not necessary as insane passwords couldn't
- // reside in the player lists. However, let's save some CPU cycles on
- // not hashing an insane password 1000+ times.
- // This is technically bad practice, but since this is just a game,
- // do it anyway.
- if (!security.isPasswordSane(text)) {
- this.sendError("Insane password");
- this.execute();
- return;
- }
-
- //
- // Block users who enter bad passwords too many times.
- if (this.player.failedPasswordsSinceLastLogin > Config.maxFailedLogins) {
- this.blockedUntil = Date.now() + Config.accountLockoutSeconds * 1000;
- this.calamity("You have been locked out for too many failed password attempts, come back later");
- return;
- }
-
- //
- // Handle blocked users.
- // They don't even get to have their password verified.
- if (this.player.blockedUntil > Date.now()) {
- //
- // Try to re-login too soon, and your lockout lasts longer.
- this.blockedUntil += Config.accountLockoutSeconds * 1000;
- this.calamity("You have been locked out for too many failed password attempts, come back later");
- return;
- }
-
- //
- // Verify the password against the hash we've stored.
- if (!security.verifyPassword(text, this.player.passwordHash)) {
- this.sendError("Incorrect password!");
- this.player.failedPasswordsSinceLastLogin++;
-
- this.session.sendDebug(`Failed login attempt #${this.player.failedPasswordsSinceLastLogin}`);
- this.execute();
-
- return;
- }
-
- this.player.lastSucessfulLoginAt = new Date();
- this.player.failedPasswordsSinceLastLogin = 0;
-
- //
- // We do not allow a player to be logged in more than once!
- if (this.player.loggedIn) {
- this.calamity("This player is already logged in");
- return;
- }
-
- // Password was correct, go to main game
- // this.scene.passwordAccepted();
- this.scene.passwordAccepted();
- }
-}
diff --git a/scenes/authentication/usernamePrompt.js b/scenes/authentication/usernamePrompt.js
deleted file mode 100755
index 0b52c1e..0000000
--- a/scenes/authentication/usernamePrompt.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import { Prompt } from "../prompt.js";
-import { gGame } from "../../models/globals.js";
-import * as security from "../../utils/security.js";
-
-/** @typedef {import("./authenticationScene.js").AuthenticationScene} AuthenticationScene */
-/** @typedef {import("../../models/player.js").Player} Player */
-
-export class UsernamePrompt extends Prompt {
- //
- promptText = [
- "Please enter your username:", //
- "(((type *:create* if you want to create a new user)))", //
- ];
-
- //
- // When player types :help
- helpText = [
- "This is where you log in.",
- "If you don't already have a player profile on this server, you can type *:create* to create one",
- ];
-
- //
- // Let the client know that we're asking for a username
- promptOptions = { username: true };
-
- /** @returns {AuthenticationScene} */
- get scene() {
- return this._scene;
- }
-
- //
- // User replied to our prompt
- onReply(username) {
- //
- // do basic syntax checks on usernames
- if (!security.isUsernameSane(username)) {
- console.info("Someone entered insane username: '%s'", username);
- this.sendError("Incorrect username, try again");
- this.execute();
- return;
- }
-
- //
- // try and fetch the player object from the game
- const player = gGame.getPlayerByUsername(username);
-
- //
- // handle invalid username
- if (!player) {
- console.info("Someone entered incorrect username: '%s'", username);
- this.sendError("Incorrect username, try again");
- this.execute();
- return;
- }
-
- //
- // Tell daddy that we're done
- this.scene.usernameAccepted(player);
- }
-}
diff --git a/scenes/gameLoop/gameScene.js b/scenes/gameLoop/gameScene.js
index 972d617..43db875 100755
--- a/scenes/gameLoop/gameScene.js
+++ b/scenes/gameLoop/gameScene.js
@@ -34,7 +34,7 @@ export class GameScene extends Scene {
// If player does not have a previous session
// then we start in the Adventurers Guild in the Hovedstad
//
- this.doPrompt("new command prompt or whatever");
+ this.showBasicPrompt(this.castle);
}
get castle() {
diff --git a/scenes/playerCreation/createUasswprdPrompt.js b/scenes/playerCreation/createUasswprdPrompt.js
index 1fee7ce..e879219 100755
--- a/scenes/playerCreation/createUasswprdPrompt.js
+++ b/scenes/playerCreation/createUasswprdPrompt.js
@@ -1,5 +1,5 @@
import { Prompt } from "../prompt.js";
-import * as security from "../../utils/security.js";
+import { Security } from "../../utils/security.js";
import { Config } from "../../config.js";
export class CreatePasswordPrompt extends Prompt {
@@ -23,7 +23,7 @@ export class CreatePasswordPrompt extends Prompt {
// not hashing an insane password 1000+ times.
// This is technically bad practice, but since this is just a game,
// do it anyway.
- if (!security.isPasswordSane(text)) {
+ if (!Security.isPasswordSane(text)) {
this.sendError("Insane password");
this.execute();
return;
@@ -50,7 +50,7 @@ export class CreatePasswordPrompt extends Prompt {
//
// Verify the password against the hash we've stored.
- if (!security.verifyPassword(text, this.player.passwordHash)) {
+ if (!Security.verifyPassword(text, this.player.passwordHash)) {
this.sendError("Incorrect password!");
this.player.failedPasswordsSinceLastLogin++;
diff --git a/scenes/playerCreation/createUsernamePrompt.js b/scenes/playerCreation/createUsernamePrompt.js
index a75219a..71056d2 100755
--- a/scenes/playerCreation/createUsernamePrompt.js
+++ b/scenes/playerCreation/createUsernamePrompt.js
@@ -1,5 +1,5 @@
import { Prompt } from "../prompt.js";
-import * as security from "../../utils/security.js";
+import { Security } from "../../utils/security.js";
import { gGame } from "../../models/globals.js";
/** @typedef {import("./playerCreationScene.js").PlayerCreationScene} PlayerCreationScene */
@@ -35,7 +35,7 @@ export class CreateUsernamePrompt extends Prompt {
onReply(username) {
//
// do basic syntax checks on usernames
- if (!security.isUsernameSane(username)) {
+ if (!Security.isUsernameSane(username)) {
console.info("Someone entered insane username: '%s'", username);
this.sendError("Incorrect username, try again.");
this.execute();
diff --git a/scenes/playerCreation/playerCreationSene.js b/scenes/playerCreation/playerCreationSene.js
index 8f69fcd..3de20dd 100755
--- a/scenes/playerCreation/playerCreationSene.js
+++ b/scenes/playerCreation/playerCreationSene.js
@@ -1,6 +1,6 @@
import { Config } from "../../config.js";
import { gGame } from "../../models/globals.js";
-import { generateHash } from "../../utils/security.js";
+import { Security } from "../../utils/security.js";
import { Scene } from "../scene.js";
import { CreateUsernamePrompt } from "./createUsernamePrompt.js";
@@ -48,6 +48,6 @@ export class PlayerCreationScene extends Scene {
passwordAccepted(password) {
this.password = password;
this.session.sendText("*_Success_* ✅ You will now be asked to log in again, sorry for that ;)");
- this.player.setPasswordHash(generateHash(this.password));
+ this.player.setPasswordHash(Security.generateHash(this.password));
}
}
diff --git a/scenes/scene.js b/scenes/scene.js
index d8c040f..425f02c 100755
--- a/scenes/scene.js
+++ b/scenes/scene.js
@@ -21,10 +21,13 @@ export class Scene {
*/
introText = "";
+ /** @constant @readonly @type {Prompt?} */
+ introPrompt;
+
/** @readonly @constant @protected @type {Session} */
- _session;
+ #session;
get session() {
- return this._session;
+ return this.#session;
}
/**
@@ -34,20 +37,26 @@ export class Scene {
* @readonly
* @type {Prompt}
*/
- _currentPrompt;
+ #currentPrompt;
get currentPrompt() {
- return this._currentPrompt;
+ return this.#currentPrompt;
}
constructor() {}
/** @param {Session} session */
execute(session) {
- this._session = session;
+ this.#session = session;
+
if (this.introText) {
this.session.sendText(this.introText);
}
- this.onReady();
+
+ if (this.introPrompt) {
+ this.showPrompt(this.introPrompt);
+ } else {
+ this.onReady();
+ }
}
/** @abstract */
@@ -59,7 +68,7 @@ export class Scene {
* @param {Prompt} prompt
*/
showPrompt(prompt) {
- this._currentPrompt = prompt;
+ this.#currentPrompt = prompt;
prompt.execute();
}
diff --git a/utils/security.js b/utils/security.js
index cae5770..914235c 100755
--- a/utils/security.js
+++ b/utils/security.js
@@ -6,53 +6,55 @@ const ITERATIONS = 1000; // MAGIC CONSTANT - move to Config
const DIGEST = "sha256"; // MAGIC CONSTANT - move to Config
const KEYLEN = 32; // MAGIC CONSTANT - move to Config
-/**
- * Generate a hash from a string
- * @param {string} source @returns {string}
- */
-export function generateHash(source) {
- const salt = randomBytes(16).toString("hex"); // 128-bit salt
- const hash = pbkdf2Sync(source, salt, ITERATIONS, KEYLEN, DIGEST).toString("hex");
- return `${ITERATIONS}:${salt}:${hash}`;
-}
-
-/**
- * Verify that a password is correct against a given hash.
- *
- * @param {string} password_candidate
- * @param {string} stored_password_hash
- * @returns {boolean}
- */
-export function verifyPassword(password_candidate, stored_password_hash) {
- const [iterations, salt, hash] = stored_password_hash.split(":");
- const derived = pbkdf2Sync(password_candidate, salt, Number(iterations), KEYLEN, DIGEST).toString("hex");
- const success = hash === derived;
- if (Config.dev) {
- console.debug(
- "Verifying password:\n" +
- " Input : %s (the password as it was sent to us by the client)\n" +
- " Given : %s (the input password hashed by us (not necessary for validation))\n" +
- " Stored : %s (the password hash we have on file for the player)\n" +
- " Derived : %s (the hashed version of the input password)\n" +
- " Verified : %s (was the password valid)",
- password_candidate,
- generateHash(password_candidate),
- stored_password_hash,
- derived,
- success,
- );
+export class Security {
+ /**
+ * Generate a hash from a string
+ * @param {string} source @returns {string}
+ */
+ static generateHash(source) {
+ const salt = randomBytes(16).toString("hex"); // 128-bit salt
+ const hash = pbkdf2Sync(source, salt, ITERATIONS, KEYLEN, DIGEST).toString("hex");
+ return `${ITERATIONS}:${salt}:${hash}`;
}
- return success;
-}
-/** @param {string} candidate */
-export function isUsernameSane(candidate) {
- return Config.usernameSanityRegex.test(candidate);
-}
+ /**
+ * Verify that a password is correct against a given hash.
+ *
+ * @param {string} password_candidate
+ * @param {string} stored_password_hash
+ * @returns {boolean}
+ */
+ static verifyPassword(password_candidate, stored_password_hash) {
+ const [iterations, salt, hash] = stored_password_hash.split(":");
+ const derived = pbkdf2Sync(password_candidate, salt, Number(iterations), KEYLEN, DIGEST).toString("hex");
+ const success = hash === derived;
+ if (Config.dev) {
+ console.debug(
+ "Verifying password:\n" +
+ " Input : %s (the password as it was sent to us by the client)\n" +
+ " Given : %s (the input password hashed by us (not necessary for validation))\n" +
+ " Stored : %s (the password hash we have on file for the player)\n" +
+ " Derived : %s (the hashed version of the input password)\n" +
+ " Verified : %s (was the password valid)",
+ password_candidate,
+ this.generateHash(password_candidate),
+ stored_password_hash,
+ derived,
+ success,
+ );
+ }
+ return success;
+ }
-/** @param {string} candidate */
-export function isPasswordSane(candidate) {
- // We know the password must adhere to one of our client-side-hashed crypto schemes,
- // so we can be fairly strict with the allowed passwords
- return Config.passwordHashSanityRegex.test(candidate);
+ /** @param {string} candidate */
+ static isUsernameSane(candidate) {
+ return Config.usernameSanityRegex.test(candidate);
+ }
+
+ /** @param {string} candidate */
+ static isPasswordSane(candidate) {
+ // We know the password must adhere to one of our client-side-hashed crypto schemes,
+ // so we can be fairly strict with the allowed passwords
+ return Config.passwordHashSanityRegex.test(candidate);
+ }
}