diff --git a/src/extensions/SingletonCache.ts b/src/extensions/SingletonCache.ts
index 7f9f7895..b2253f73 100644
--- a/src/extensions/SingletonCache.ts
+++ b/src/extensions/SingletonCache.ts
@@ -44,7 +44,7 @@ export class SingletonCache<T> {
this.cachedValue = result;
return result;
} catch (e) {
- console.log(`[SingletonCache] Factory method failed, returning stale value:`, e);
+ console.error(`[SingletonCache] Factory method failed, returning stale value:`, e);
return this.cachedValue;
} finally {
this.isLocked = false;
diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts
index 3f654495..d0dd7a81 100644
--- a/src/util/util/Token.ts
+++ b/src/util/util/Token.ts
@@ -16,16 +16,18 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import crypto from "node:crypto";
+import crypto, { KeyObject } from "node:crypto";
import { existsSync } from "node:fs";
import path from "node:path";
import fs from "node:fs/promises";
import jwt from "jsonwebtoken";
import { HTTPError } from "lambert-server/HTTPError";
import { InstanceBan, Session, User } from "@spacebar/database";
-import { Random, TimeSpan } from "@spacebar/extensions";
+import { Random, sleep, Stopwatch, TimeSpan } from "@spacebar/extensions";
import { Config } from "./Config";
import { OrmUtils } from "@spacebar/util";
+import { clearInterval, setInterval } from "node:timers";
+import { ProcessLifecycle } from "@spacebar/util/util/ProcessLifecycle";
/// Change history:
/// 1 - Initial version with HS256
@@ -148,15 +150,13 @@ export const checkToken = (
legacyVersion = 1;
jwt.verify(token, Config.get().security.jwtSecret!, { algorithms: ["HS256"] }, validateUser);
} else if (dec.header.alg == "ES512") {
- loadOrGenerateKeypair().then((keyPair) => {
- jwt.verify(token, keyPair.publicKey, { algorithms: ["ES512"] }, validateUser);
- });
+ jwt.verify(token, JwtKeypairManager.keypair.publicKey, { algorithms: ["ES512"] }, validateUser);
} else return void rejectAndLog(reject, 400, "Unsupported token algorithm: " + dec.header.alg);
});
export async function generateToken(id: string, isAdminSession: boolean = false): Promise<string | undefined> {
const iat = Math.floor(Date.now() / 1000);
- const keyPair = await loadOrGenerateKeypair();
+ const keyPair = JwtKeypairManager.keypair;
let newSession;
do {
@@ -188,58 +188,92 @@ export async function generateToken(id: string, isAdminSession: boolean = false)
});
}
-let lastFsCheck: number;
-let cachedKeypair: {
- privateKey: crypto.KeyObject;
- publicKey: crypto.KeyObject;
- fingerprint: string;
-};
+export class JwtKeypairManager {
+ private static isLocked = false;
+ static #keypair: JwtKeypair;
+ static #filesystemCheckInterval: NodeJS.Timeout;
-// Get ECDSA keypair from file or generate it
-export async function loadOrGenerateKeypair() {
- if (cachedKeypair) {
- // check for file deletion every minute
- if (Date.now() - lastFsCheck > 60000) {
- if (!existsSync("jwt.key") || !existsSync("jwt.key.pub")) {
- console.log("[JWT] Keypair files disappeared... Saving them again.");
- await Promise.all([
- fs.writeFile("jwt.key", cachedKeypair.privateKey.export({ format: "pem", type: "sec1" })),
- fs.writeFile("jwt.key.pub", cachedKeypair.publicKey.export({ format: "pem", type: "spki" })),
- ]);
+ public static get keypair() {
+ return this.#keypair;
+ }
+
+ // Get ECDSA keypair from file or generate it
+ public static async init() {
+ if (this.isLocked) {
+ const lockSw = Stopwatch.startNew();
+ while (this.isLocked) {
+ await sleep(50);
+ if (lockSw.elapsed().totalSeconds > 10) throw new Error("[JwtKeypairManager] Initialization was locked for >10 seconds!");
}
- lastFsCheck = Date.now();
}
- return cachedKeypair;
- }
+ this.isLocked = true;
+ try {
+ let privateKey: crypto.KeyObject;
+ let publicKey: crypto.KeyObject;
- let privateKey: crypto.KeyObject;
- let publicKey: crypto.KeyObject;
+ if (existsSync("jwt.key") && existsSync("jwt.key.pub")) {
+ const [loadedPrivateKey, loadedPublicKey] = await Promise.all([fs.readFile("jwt.key"), fs.readFile("jwt.key.pub")]);
- if (existsSync("jwt.key") && existsSync("jwt.key.pub")) {
- const [loadedPrivateKey, loadedPublicKey] = await Promise.all([fs.readFile("jwt.key"), fs.readFile("jwt.key.pub")]);
+ privateKey = crypto.createPrivateKey(loadedPrivateKey);
+ publicKey = crypto.createPublicKey(loadedPublicKey);
+ } else {
+ console.log("[JWT] Generating new keypair:", path.resolve("jwt.key"), "- PWD:", process.cwd());
+ const res = crypto.generateKeyPairSync("ec", {
+ namedCurve: "secp521r1",
+ });
+ privateKey = res.privateKey;
+ publicKey = res.publicKey;
- privateKey = crypto.createPrivateKey(loadedPrivateKey);
- publicKey = crypto.createPublicKey(loadedPublicKey);
- } else {
- console.log("[JWT] Generating new keypair:", path.resolve("jwt.key"), "- PWD:", process.cwd());
- const res = crypto.generateKeyPairSync("ec", {
- namedCurve: "secp521r1",
- });
- privateKey = res.privateKey;
- publicKey = res.publicKey;
+ await Promise.all([
+ fs.writeFile("jwt.key", privateKey.export({ format: "pem", type: "sec1" })),
+ fs.writeFile("jwt.key.pub", publicKey.export({ format: "pem", type: "spki" })),
+ ]);
+ }
+
+ const fingerprint = crypto
+ .createHash("sha256")
+ .update(publicKey.export({ format: "pem", type: "spki" }))
+ .digest("hex");
- await Promise.all([
- fs.writeFile("jwt.key", privateKey.export({ format: "pem", type: "sec1" })),
- fs.writeFile("jwt.key.pub", publicKey.export({ format: "pem", type: "spki" })),
- ]);
+ this.#keypair = new JwtKeypair(privateKey, publicKey, fingerprint);
+
+ // set up interval to check if key was accidentally deleted
+ this.#filesystemCheckInterval = setInterval(async () => this.runDeletionCheck(), 60_000);
+ ProcessLifecycle.eventEmitter.on("stopped", async () => {
+ clearInterval(this.#filesystemCheckInterval);
+ await this.runDeletionCheck();
+ });
+ } catch (e) {
+ console.error(`[JwtKeypairManager] Initialization failed:`, e);
+ } finally {
+ this.isLocked = false;
+ }
}
- const fingerprint = crypto
- .createHash("sha256")
- .update(publicKey.export({ format: "pem", type: "spki" }))
- .digest("hex");
+ private static async runDeletionCheck() {
+ try {
+ if (!existsSync("jwt.key") || !existsSync("jwt.key.pub")) {
+ console.log("[JWT] Keypair files disappeared... Saving them again.");
+ await Promise.all([
+ fs.writeFile("jwt.key", this.#keypair.privateKey.export({ format: "pem", type: "sec1" })),
+ fs.writeFile("jwt.key.pub", this.#keypair.publicKey.export({ format: "pem", type: "spki" })),
+ ]);
+ }
+ } catch (e) {
+ console.error("[JwtKeypairManager] Failed to check if keypair was accidentally deleted:", e);
+ }
+ }
+}
+
+class JwtKeypair {
+ public readonly privateKey: KeyObject;
+ public readonly publicKey: KeyObject;
+ public readonly fingerprint: string;
- lastFsCheck = Date.now();
- return (cachedKeypair = { privateKey, publicKey, fingerprint });
+ constructor(privateKey: KeyObject, publicKey: KeyObject, fingerprint: string) {
+ this.privateKey = privateKey;
+ this.publicKey = publicKey;
+ this.fingerprint = fingerprint;
+ }
}
diff --git a/src/util/util/WebAuthn.ts b/src/util/util/WebAuthn.ts
index f6c5dbc4..84627951 100644
--- a/src/util/util/WebAuthn.ts
+++ b/src/util/util/WebAuthn.ts
@@ -18,12 +18,13 @@
import { Fido2Lib } from "fido2-lib";
import jwt from "jsonwebtoken";
-import { loadOrGenerateKeypair } from "./Token";
+import { JwtKeypairManager } from "./Token";
const jwtSignOptions: jwt.SignOptions = {
algorithm: "ES512",
expiresIn: "5m",
};
+
const jwtVerifyOptions: jwt.VerifyOptions = {
algorithms: ["ES512"],
};
@@ -42,22 +43,18 @@ export const WebAuthn: {
export async function generateWebAuthnTicket(challenge: string): Promise<string> {
return new Promise((res, rej) => {
- loadOrGenerateKeypair().then((kp) =>
- jwt.sign({ challenge }, kp.privateKey, jwtSignOptions, (err, token) => {
- if (err || !token) return rej(err || "no token");
- return res(token);
- }),
- );
+ jwt.sign({ challenge }, JwtKeypairManager.keypair.privateKey, jwtSignOptions, (err, token) => {
+ if (err || !token) return rej(err || "no token");
+ return res(token);
+ });
});
}
export async function verifyWebAuthnToken(token: string) {
return new Promise((res, rej) => {
- loadOrGenerateKeypair().then((kp) =>
- jwt.verify(token, kp.publicKey, jwtVerifyOptions, (err, decoded) => {
- if (err) return rej(err);
- return res(decoded);
- }),
- );
+ jwt.verify(token, JwtKeypairManager.keypair.publicKey, jwtVerifyOptions, (err, decoded) => {
+ if (err) return rej(err);
+ return res(decoded);
+ });
});
}
|