сриједа, 11. новембар 2026. · Година И · Број 15 · Цијена: 0 динара
ЈУГОСЛОВЕНСКИ ФОРУМ

Гласило YU мреже · НЕМА ГРАНИЦА ЗА ЗНАЊЕ

Пријава Упис
Право на увид у код. Отворен протокол омогућава независне оператере и интероперабилност. Манифест

Извор

4d7883a894b3

   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