Стигло је ново издање. Освјежи страну.
Право на увид у код. Отворен протокол омогућава независне оператере и интероперабилност. Манифест
Извор
4d7883a894b3
- server.js
- lib/auth.js
- lib/build.js
- lib/cards.js
- lib/db.js
- lib/gradovi.js
- lib/icons.js
- lib/imena.js
- lib/jezik.js
- lib/kartapodaci.js
- lib/lekcije.js
- lib/md.js
- lib/mehanika.js
- lib/mreza.js
- lib/pages.js
- lib/registar.js
- lib/routes.js
- lib/schema2.js
- lib/views.js
- seed/content.mjs
- seed/seed.mjs
- public/app.js
- public/style.css
1 // Passwords (argon2id), sessions, CSRF and rate limits.
2 import crypto from 'node:crypto';
3
4 const A2 = { parallelism: 1, tagLength: 32, memory: 19456, passes: 2 };
5
6 export function hashPassword(pw) {
7 const nonce = crypto.randomBytes(16);
8 const tag = crypto.argon2Sync('argon2id', { ...A2, message: Buffer.from(pw, 'utf8'), nonce });
9 return `$argon2id$v=19$m=${A2.memory},t=${A2.passes},p=${A2.parallelism}$${nonce.toString('base64url')}$${tag.toString('base64url')}`;
10 }
11
12 export async function verifyPassword(pw, stored) {
13 const m = /^\$argon2id\$v=19\$m=(\d+),t=(\d+),p=(\d+)\$([\w-]+)\$([\w-]+)$/.exec(stored || '');
14 if (!m) {
15 // Constant-ish work for unusable or unknown hashes, so timing does not reveal accounts.
16 await argon(pw, crypto.randomBytes(16), A2);
17 return false;
18 }
19 const opts = { parallelism: +m[3], tagLength: 32, memory: +m[1], passes: +m[2] };
20 const want = Buffer.from(m[5], 'base64url');
21 const got = await argon(pw, Buffer.from(m[4], 'base64url'), opts);
22 return got.length === want.length && crypto.timingSafeEqual(got, want);
23 }
24
25 function argon(pw, nonce, opts) {
26 return new Promise((resolve, reject) =>
27 crypto.argon2('argon2id', { ...opts, message: Buffer.from(String(pw), 'utf8'), nonce }, (e, k) => (e ? reject(e) : resolve(k))));
28 }
29
30 export const sha = (s) => crypto.createHash('sha256').update(s).digest('hex');
31 export const token = (n = 32) => crypto.randomBytes(n).toString('base64url');
32
33 export const SESSION_COOKIE = 'yu_sid';
34 export const ANON_COOKIE = 'yu_anon';
35 const SESSION_MS = 30 * 24 * 3600 * 1000;
36
37 export function createSession(db, userId) {
38 const raw = token(32);
39 const now = Date.now();
40 db.run('INSERT INTO sessions(id_hash, user_id, csrf, created_at, expires_at) VALUES(?, ?, ?, ?, ?)',
41 sha(raw), userId, token(24), now, now + SESSION_MS);
42 db.run('DELETE FROM sessions WHERE expires_at < ?', now);
43 return { raw, maxAge: SESSION_MS / 1000 };
44 }
45
46 export function loadSession(db, raw) {
47 if (!raw || raw.length > 100) return null;
48 const s = db.get(`SELECT s.id_hash, s.csrf, s.expires_at, u.* FROM sessions s JOIN users u ON u.id = s.user_id
49 WHERE s.id_hash = ? AND s.expires_at > ? AND u.banned = 0 AND u.deleted = 0`, sha(raw), Date.now());
50 return s || null;
51 }
52
53 export function destroySession(db, raw) {
54 if (raw) db.run('DELETE FROM sessions WHERE id_hash = ?', sha(raw));
55 }
56
57 // Anonymous CSRF: token = HMAC(secret, anon cookie). Logged-in CSRF: per-session random token.
58 export function anonCsrf(secret, anonId) {
59 return crypto.createHmac('sha256', secret).update('csrf:' + anonId).digest('base64url').slice(0, 32);
60 }
61
62 export function safeEqual(a, b) {
63 const x = Buffer.from(String(a || '')), y = Buffer.from(String(b || ''));
64 return x.length === y.length && x.length > 0 && crypto.timingSafeEqual(x, y);
65 }
66
67 // Fixed-window limiter in memory: key -> {n, reset}.
68 export class Limiter {
69 constructor() { this.m = new Map(); }
70 hit(key, max, windowMs, now = Date.now()) {
71 let e = this.m.get(key);
72 if (!e || e.reset <= now) { e = { n: 0, reset: now + windowMs }; this.m.set(key, e); }
73 e.n++;
74 if (this.m.size > 20000) for (const [k, v] of this.m) if (v.reset <= now) this.m.delete(k);
75 return e.n <= max ? 0 : Math.ceil((e.reset - now) / 1000);
76 }
77 }
78
79 export const HANDLE_RE = /^[a-z0-9_]{3,20}$/;
80 export const RESERVED = new Set(['admin', 'signal', 'root', 'steward', 'stewards', 'moderator', 'mod', 'system', 'registry',
81 'support', 'help', 'yu', 'yunetwork', 'official', 'gov', 'bank', 'id', 'hello', 'market', 'dinar', 'null', 'undefined',
82 'departed', 'anonymous', 'tejl']);
83