You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
65 lines
2.2 KiB
65 lines
2.2 KiB
|
4 days ago
|
import {
|
||
|
|
createHash,
|
||
|
|
createHmac,
|
||
|
|
randomBytes,
|
||
|
|
scrypt as scryptCallback,
|
||
|
|
timingSafeEqual,
|
||
|
|
} from 'node:crypto';
|
||
|
|
import { promisify } from 'node:util';
|
||
|
|
import { avatarReference } from './avatar.mjs';
|
||
|
|
|
||
|
|
const scrypt = promisify(scryptCallback);
|
||
|
|
export const hashToken = (token) =>
|
||
|
|
createHash('sha256').update(token).digest('hex');
|
||
|
|
export const publicUser = (user) =>
|
||
|
|
user
|
||
|
|
? {
|
||
|
|
id: user.id,
|
||
|
|
name: user.name,
|
||
|
|
nick: user.nick,
|
||
|
|
createdAt: user.createdAt,
|
||
|
|
guest: Boolean(user.guest),
|
||
|
|
avatar: avatarReference(user),
|
||
|
|
}
|
||
|
|
: null;
|
||
|
|
export async function hashPassword(password) {
|
||
|
|
const salt = randomBytes(16).toString('hex');
|
||
|
|
const hash = await scrypt(password, salt, 64);
|
||
|
|
return `${salt}:${hash.toString('hex')}`;
|
||
|
|
}
|
||
|
|
export async function verifyPassword(password, encoded) {
|
||
|
|
const [salt, stored] = encoded.split(':');
|
||
|
|
const expected = Buffer.from(stored, 'hex');
|
||
|
|
const actual = await scrypt(password, salt, 64);
|
||
|
|
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
||
|
|
}
|
||
|
|
export const hashPin = (pin, pepper) =>
|
||
|
|
hashPassword(createHmac('sha256', pepper).update(pin).digest('hex'));
|
||
|
|
export const verifyPin = (pin, encoded, pepper) =>
|
||
|
|
verifyPassword(
|
||
|
|
createHmac('sha256', pepper).update(pin).digest('hex'),
|
||
|
|
encoded,
|
||
|
|
);
|
||
|
|
export function cookieToken(header = '') {
|
||
|
|
const token = header
|
||
|
|
.split(';')
|
||
|
|
.map((v) => v.trim())
|
||
|
|
.find((v) => v.startsWith('mesa_session='))
|
||
|
|
?.slice(13);
|
||
|
|
return token && /^[a-f0-9]{64}$/.test(token) ? token : null;
|
||
|
|
}
|
||
|
|
export async function authenticate(store, token) {
|
||
|
|
if (!token) return null;
|
||
|
|
const session = await store.session(hashToken(token));
|
||
|
|
return session ? publicUser(await store.userById(session.userId)) : null;
|
||
|
|
}
|
||
|
|
export async function newSession(store, userId) {
|
||
|
|
const token = randomBytes(32).toString('hex');
|
||
|
|
const expiresAt = Date.now() + 30 * 24 * 60 * 60 * 1000;
|
||
|
|
await store.createSession({ hash: hashToken(token), userId, expiresAt });
|
||
|
|
return token;
|
||
|
|
}
|
||
|
|
export function sessionCookie(token, production) {
|
||
|
|
return `mesa_session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${token ? 2592000 : 0}${production ? '; Secure' : ''}`;
|
||
|
|
}
|