27355cec76
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 rindas
2.9 KiB
JavaScript
78 rindas
2.9 KiB
JavaScript
// Auth utilities — password hashing (scrypt), tokens, and TOTP MFA.
|
|
// Uses only Node's built-in crypto, no external auth deps.
|
|
const crypto = require('crypto');
|
|
|
|
// ---- Passwords (scrypt) ----
|
|
function hashPassword(password, salt = crypto.randomBytes(16).toString('hex')) {
|
|
const hash = crypto.scryptSync(password, salt, 64).toString('hex');
|
|
return { hash, salt };
|
|
}
|
|
function verifyPassword(password, salt, expectedHash) {
|
|
const hash = crypto.scryptSync(password, salt, 64).toString('hex');
|
|
return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expectedHash));
|
|
}
|
|
|
|
// ---- Random tokens ----
|
|
const token = (bytes = 24) => crypto.randomBytes(bytes).toString('hex');
|
|
const id = () => crypto.randomBytes(8).toString('hex');
|
|
// Deterministic hash for storing high-value tokens (e.g. refresh tokens) at rest.
|
|
const hashToken = (t) => crypto.createHash('sha256').update(String(t)).digest('hex');
|
|
const numericCode = (digits = 6) =>
|
|
String(crypto.randomInt(0, 10 ** digits)).padStart(digits, '0');
|
|
|
|
// ---- TOTP (RFC 6238), SHA-1, 30s, 6 digits ----
|
|
const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
function base32Encode(buf) {
|
|
let bits = 0, value = 0, out = '';
|
|
for (const byte of buf) {
|
|
value = (value << 8) | byte; bits += 8;
|
|
while (bits >= 5) { out += B32[(value >>> (bits - 5)) & 31]; bits -= 5; }
|
|
}
|
|
if (bits > 0) out += B32[(value << (5 - bits)) & 31];
|
|
return out;
|
|
}
|
|
function base32Decode(str) {
|
|
const clean = str.replace(/=+$/, '').toUpperCase();
|
|
let bits = 0, value = 0; const out = [];
|
|
for (const ch of clean) {
|
|
const idx = B32.indexOf(ch);
|
|
if (idx === -1) continue;
|
|
value = (value << 5) | idx; bits += 5;
|
|
if (bits >= 8) { out.push((value >>> (bits - 8)) & 0xff); bits -= 8; }
|
|
}
|
|
return Buffer.from(out);
|
|
}
|
|
function newMfaSecret() {
|
|
return base32Encode(crypto.randomBytes(20));
|
|
}
|
|
function totp(secret, timeStep = Math.floor(Date.now() / 30000)) {
|
|
const key = base32Decode(secret);
|
|
const buf = Buffer.alloc(8);
|
|
buf.writeBigUInt64BE(BigInt(timeStep));
|
|
const hmac = crypto.createHmac('sha1', key).update(buf).digest();
|
|
const offset = hmac[hmac.length - 1] & 0xf;
|
|
const code =
|
|
((hmac[offset] & 0x7f) << 24) |
|
|
((hmac[offset + 1] & 0xff) << 16) |
|
|
((hmac[offset + 2] & 0xff) << 8) |
|
|
(hmac[offset + 3] & 0xff);
|
|
return String(code % 1_000_000).padStart(6, '0');
|
|
}
|
|
function verifyTotp(secret, code, window = 1) {
|
|
if (!/^\d{6}$/.test(String(code || ''))) return false;
|
|
const step = Math.floor(Date.now() / 30000);
|
|
for (let i = -window; i <= window; i++) {
|
|
if (totp(secret, step + i) === String(code)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
function otpauthUrl(secret, email, issuer = 'RemoteAccess') {
|
|
return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(email)}` +
|
|
`?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`;
|
|
}
|
|
|
|
module.exports = {
|
|
hashPassword, verifyPassword, token, id, hashToken, numericCode,
|
|
newMfaSecret, totp, verifyTotp, otpauthUrl,
|
|
};
|