Files
website/server/login-service.ts
T
2026-05-01 15:13:30 -04:00

117 lines
3.6 KiB
TypeScript

import {
assert,
assertFalse,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
type User = { username: string; passwordHashWithSalt: string };
const officialUsers: User[] = [
{
username: "marqueur",
passwordHashWithSalt:
"34373780cc542a1eddb6a08488ec149323b94d66025421acd98ffc20eb5055a0:6f2fa05777d45a2da9db79b7fd11e3bc69eb1807642ccb53d41a0b9eb71a3311",
},
{
username: "comite",
passwordHashWithSalt:
"8649feff48a50f53448e59ce480db97b5549eef1d92c886c49b5a14ee22648e5:a0017b879c8834d2f34938e7a4fa3f4c7be8f70545b9bb22a28cf56ab2eb26d9",
},
];
export class LoginService {
private readonly users: User[];
private readonly sessionKey: string;
constructor(users: User[], sessionKey: string) {
this.users = users;
this.sessionKey = sessionKey;
}
public static officialLoginService(): LoginService {
return new LoginService(
officialUsers,
Deno.env.get("SESSION_KEY") || "dev-only",
);
}
private async hashMessage(message: string): Promise<string> {
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8); // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join(""); // convert bytes to hex string
return hashHex;
}
private async shittyHmac(message: string, salt: string): Promise<string> {
const h1 = await this.hashMessage(message + salt);
const h2 = await this.hashMessage(salt + message);
return await this.hashMessage(h1 + h2);
}
async hashPassword(password: string): Promise<string> {
const salt = await this.hashMessage(`${Math.random()}`);
const hash = await this.shittyHmac(password, salt);
return `${salt}:${hash}`;
}
async verifyPassword(
password: string,
passwordHashWithSalt: string,
): Promise<boolean> {
const [passwordSalt, passwordHash] = passwordHashWithSalt.split(":");
return passwordHash === (await this.shittyHmac(password, passwordSalt));
}
async validateCredentials(
username: string,
password: string,
): Promise<boolean> {
for (const user of this.users) {
if (user.username === username) {
return this.verifyPassword(password, user.passwordHashWithSalt);
}
}
return false;
}
async login(ctx: any): Promise<void> {
const nDays = 90;
const maxAgesInSecond = nDays * 24 * 3600;
const maxUnixEpoch = Math.floor(Date.now() / 1000) + maxAgesInSecond;
await ctx.cookies.set("phenix-session", `${maxUnixEpoch}`, {
maxAge: maxAgesInSecond,
});
await ctx.cookies.set(
"phenix-session-sign",
await this.shittyHmac(`${maxUnixEpoch}`, this.sessionKey),
{
maxAge: maxAgesInSecond,
},
);
}
async isLoggedIn(ctx: any): Promise<boolean> {
const expiration = await ctx.cookies.get("phenix-session");
const signature = await ctx.cookies.get("phenix-session-sign");
if (signature !== (await this.shittyHmac(expiration, this.sessionKey))) {
return false;
}
if (!expiration || !/^\d+$/.test(expiration)) {
return false;
}
return Date.now() < parseInt(expiration) * 1000;
}
}
Deno.test("login service", async (t) => {
await t.step("hashing password", async () => {
const service = new LoginService([], "ss");
const passwordHashWithSalt = await service.hashPassword("qwerty");
assert(await service.verifyPassword("qwerty", passwordHashWithSalt));
assertFalse(await service.verifyPassword("ytrewq", passwordHashWithSalt));
});
});