Repository files, collaboration, releases and project activity.
ARCHFORGE HISTORY
Blame · src/security.js
Branch main · the Commit author is not necessarily a verified ArchForge account.
e59b7f7cc6archadmin2026-10-11
1 'use strict';e59b7f7cc6archadmin2026-10-11
2 const crypto=require('node:crypto');e59b7f7cc6archadmin2026-10-11
3 const {URL}=require('node:url');e59b7f7cc6archadmin2026-10-11
4 const base64url=b=>Buffer.from(b).toString('base64url');e59b7f7cc6archadmin2026-10-11
5 const randomToken=(bytes=32)=>base64url(crypto.randomBytes(bytes));e59b7f7cc6archadmin2026-10-11
6 const sha=s=>crypto.createHash('sha256').update(s).digest('hex');e59b7f7cc6archadmin2026-10-11
7 function safeEqual(a,b){const x=Buffer.from(String(a)),y=Buffer.from(String(b));return x.length===y.length&&crypto.timingSafeEqual(x,y);}e59b7f7cc6archadmin2026-10-11
8 function passwordHash(pass,salt=crypto.randomBytes(16).toString('hex')){if(pass.length<12||Buffer.byteLength(pass)>1024)throw Error('Use a password between 12 and 1024 bytes');return `scrypt$${salt}$${crypto.scryptSync(pass,Buffer.from(salt,'hex'),64,{N:16384,r:8,p:1}).toString('hex')}`;}e59b7f7cc6archadmin2026-10-11
9 function verifyPassword(pass,stored){try{const [v,s,h]=stored.split('$');if(v!=='scrypt'||!s||!h)return false;return safeEqual(crypto.scryptSync(pass,Buffer.from(s,'hex'),64,{N:16384,r:8,p:1}).toString('hex'),h);}catch{return false;}}e59b7f7cc6archadmin2026-10-11
10 const alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';e59b7f7cc6archadmin2026-10-11
11 function base32Encode(bytes){let bits=0,v=0,out='';for(const b of bytes){v=(v<<8)|b;bits+=8;while(bits>=5){out+=alphabet[(v>>>(bits-=5))&31];}}if(bits)out+=alphabet[(v<<(5-bits))&31];return out;}e59b7f7cc6archadmin2026-10-11
12 function base32Decode(input){let bits=0,v=0,out=[];for(const c of input.replace(/=|\s/g,'').toUpperCase()){let n=alphabet.indexOf(c);if(n<0)throw Error('Invalid base32');v=(v<<5)|n;bits+=5;if(bits>=8){out.push((v>>>(bits-=8))&255);}}return Buffer.from(out);}e59b7f7cc6archadmin2026-10-11
13 function totp(secret,when=Date.now()){const step=Math.floor(when/30000);const buf=Buffer.alloc(8);buf.writeBigUInt64BE(BigInt(step));const digest=crypto.createHmac('sha1',base32Decode(secret)).update(buf).digest();const off=digest[19]&15;return String((digest.readUInt32BE(off)&0x7fffffff)%1000000).padStart(6,'0');}e59b7f7cc6archadmin2026-10-11
14 function verifyTotp(secret,input,when=Date.now()){if(!/^\d{6}$/.test(String(input)))return false;for(let offset=-1;offset<=1;offset++)if(safeEqual(totp(secret,when+offset*30000),input))return true;return false;}e59b7f7cc6archadmin2026-10-11
15 function encrypt(key,text){const nonce=crypto.randomBytes(12),cipher=crypto.createCipheriv('aes-256-gcm',Buffer.from(key,'hex'),nonce);const ciphertext=Buffer.concat([cipher.update(text,'utf8'),cipher.final()]);return [base64url(nonce),base64url(ciphertext),base64url(cipher.getAuthTag())].join('.');}e59b7f7cc6archadmin2026-10-11
16 function decrypt(key,encoded){const [n,c,t]=encoded.split('.');const dec=crypto.createDecipheriv('aes-256-gcm',Buffer.from(key,'hex'),Buffer.from(n,'base64url'));dec.setAuthTag(Buffer.from(t,'base64url'));return Buffer.concat([dec.update(Buffer.from(c,'base64url')),dec.final()]).toString('utf8');}e59b7f7cc6archadmin2026-10-11
17 const esc=value=>String(value??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));e59b7f7cc6archadmin2026-10-11
18 function cookies(req){return Object.fromEntries((req.headers.cookie||'').split(';').map(p=>p.trim().split('=')).filter(x=>x.length===2));}e59b7f7cc6archadmin2026-10-11
19 function rateLimiter(windowMs,maximum){const map=new Map();return (key)=>{const now=Date.now();if(map.size>10000){for(const [k,v] of map)if(v.until<now)map.delete(k);}let x=map.get(key);if(!x||x.until<now){x={count:0,until:now+windowMs};map.set(key,x);}x.count++;return x.count<=maximum;};}e59b7f7cc6archadmin2026-10-11
20 function validEmail(v){return typeof v==='string'&&v.length<=254&&/^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(v);}e59b7f7cc6archadmin2026-10-11
21 function isAllowedRedirect(candidate,origin){try{const u=new URL(candidate,origin);return u.protocol==='https:'&&u.origin===new URL(origin).origin;}catch{return false;}}e59b7f7cc6archadmin2026-10-11
22 module.exports={randomToken,sha,safeEqual,passwordHash,verifyPassword,base32Encode,totp,verifyTotp,encrypt,decrypt,esc,cookies,rateLimiter,validEmail,isAllowedRedirect};