112 lines
3.4 KiB
TypeScript
112 lines
3.4 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:
|
|
"5603007580f73044864e7a0b66744f405406eaaf28fa514d724ccf11d010bd89:070b1a477e6a877d44f31961dc0402a9f3258465eed73ab6a9110c461ac68714",
|
|
},
|
|
];
|
|
|
|
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([]);
|
|
const passwordHashWithSalt = await service.hashPassword("qwerty");
|
|
assert(await service.verifyPassword("qwerty", passwordHashWithSalt));
|
|
assertFalse(await service.verifyPassword("ytrewq", passwordHashWithSalt));
|
|
});
|
|
});
|