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: "5603007580f73044864e7a0b66744f405406eaaf28fa514d724ccf11d010bd89:070b1a477e6a877d44f31961dc0402a9f3258465eed73ab6a9110c461ac68714", }, { 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 { 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 { 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 { const salt = await this.hashMessage(`${Math.random()}`); const hash = await this.shittyHmac(password, salt); return `${salt}:${hash}`; } async verifyPassword( password: string, passwordHashWithSalt: string, ): Promise { const [passwordSalt, passwordHash] = passwordHashWithSalt.split(":"); return passwordHash === (await this.shittyHmac(password, passwordSalt)); } async validateCredentials( username: string, password: string, ): Promise { for (const user of this.users) { if (user.username === username) { return this.verifyPassword(password, user.passwordHashWithSalt); } } return false; } async login(ctx: any): Promise { 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 { 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)); }); });