first commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
// 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');
|
||||
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, numericCode,
|
||||
newMfaSecret, totp, verifyTotp, otpauthUrl,
|
||||
};
|
||||
Reference in New Issue
Block a user