-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth.js
More file actions
36 lines (32 loc) · 1.53 KB
/
Copy pathauth.js
File metadata and controls
36 lines (32 loc) · 1.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// auth.js — password hashing + device tokens, using only Node's crypto (no
// dependencies, nothing to compile).
//
// Passwords: salted scrypt (a memory-hard KDF). Stored as "scrypt$<salt>$<hash>".
// Device tokens: 256 bits of randomness. The raw token goes to the client once;
// the server stores and looks it up only by its sha256 — the token *is* the
// secret, so a fast deterministic hash is correct here (and is what lets us look
// a user up by their token), while a DB leak still can't reveal live tokens.
import crypto from 'crypto';
import { promisify } from 'util';
const scrypt = promisify(crypto.scrypt);
const KEYLEN = 32;
export async function hashPassword(password) {
const salt = crypto.randomBytes(16);
const hash = await scrypt(password, salt, KEYLEN);
return `scrypt$${salt.toString('hex')}$${hash.toString('hex')}`;
}
export async function verifyPassword(password, stored) {
if (typeof stored !== 'string') return false;
const [scheme, saltHex, hashHex] = stored.split('$');
if (scheme !== 'scrypt' || !saltHex || !hashHex) return false;
const expected = Buffer.from(hashHex, 'hex');
const actual = await scrypt(password, Buffer.from(saltHex, 'hex'), expected.length);
// timing-safe compare (equal lengths guaranteed by keylen above)
return expected.length === actual.length && crypto.timingSafeEqual(expected, actual);
}
export function makeToken() {
return crypto.randomBytes(32).toString('base64url');
}
export function hashToken(raw) {
return crypto.createHash('sha256').update(String(raw)).digest('hex');
}