mirror of
https://github.com/spacebarchat/server.git
synced 2026-08-29 03:18:40 +00:00
Introduce JwtKeypairManager, JwtKeypair classes
This commit is contained in:
@@ -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;
|
||||
|
||||
+86
-52
@@ -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) {
|
||||
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!");
|
||||
}
|
||||
}
|
||||
|
||||
this.isLocked = true;
|
||||
try {
|
||||
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")]);
|
||||
|
||||
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");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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", cachedKeypair.privateKey.export({ format: "pem", type: "sec1" })),
|
||||
fs.writeFile("jwt.key.pub", cachedKeypair.publicKey.export({ format: "pem", type: "spki" })),
|
||||
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" })),
|
||||
]);
|
||||
}
|
||||
lastFsCheck = Date.now();
|
||||
} catch (e) {
|
||||
console.error("[JwtKeypairManager] Failed to check if keypair was accidentally deleted:", e);
|
||||
}
|
||||
|
||||
return cachedKeypair;
|
||||
}
|
||||
|
||||
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")]);
|
||||
|
||||
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");
|
||||
|
||||
lastFsCheck = Date.now();
|
||||
return (cachedKeypair = { privateKey, publicKey, fingerprint });
|
||||
}
|
||||
|
||||
class JwtKeypair {
|
||||
public readonly privateKey: KeyObject;
|
||||
public readonly publicKey: KeyObject;
|
||||
public readonly fingerprint: string;
|
||||
|
||||
constructor(privateKey: KeyObject, publicKey: KeyObject, fingerprint: string) {
|
||||
this.privateKey = privateKey;
|
||||
this.publicKey = publicKey;
|
||||
this.fingerprint = fingerprint;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-13
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user