forked from dsh/panel-auth
panel-auth: DSH web panel password gate plugin (Basic auth + signed cookie, fail-open)
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
// panel-auth — credential hashing helpers (pure functions, no harness deps).
|
||||
import { createHmac, randomBytes, scryptSync, timingSafeEqual } from 'node:crypto'
|
||||
|
||||
/** Self-describing scrypt encoding: $scrypt$N=16384,r=8,p=1$<saltB64>$<hashB64> */
|
||||
export function hashPassword(password, { N = 16384, r = 8, p = 1, keylen = 32 } = {}) {
|
||||
const salt = randomBytes(16)
|
||||
const hash = scryptSync(String(password), salt, keylen, { N, r, p })
|
||||
return `$scrypt$N=${N},r=${r},p=${p}$${salt.toString('base64')}$${hash.toString('base64')}`
|
||||
}
|
||||
|
||||
/** Constant-time verification against a hashPassword() encoding. */
|
||||
export function verifyPassword(password, encoded) {
|
||||
if (typeof encoded !== 'string' || typeof password !== 'string') return false
|
||||
const parts = encoded.split('$')
|
||||
if (parts.length !== 5 || parts[1] !== 'scrypt') return false
|
||||
const params = {}
|
||||
for (const kv of parts[2].split(',')) {
|
||||
const [k, v] = kv.split('=')
|
||||
params[k] = Number(v)
|
||||
}
|
||||
if (!params.N || !params.r || !params.p) return false
|
||||
try {
|
||||
const expected = Buffer.from(parts[4], 'base64')
|
||||
const actual = scryptSync(password, Buffer.from(parts[3], 'base64'), expected.length, {
|
||||
N: params.N,
|
||||
r: params.r,
|
||||
p: params.p,
|
||||
})
|
||||
return expected.length === actual.length && timingSafeEqual(actual, expected)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** HMAC-signed cookie payload: base64url(username).expirySeconds.signature */
|
||||
export function signSession(secret, username, expirySeconds) {
|
||||
const payload = `${Buffer.from(username, 'utf8').toString('base64url')}.${expirySeconds}`
|
||||
const sig = createHmac('sha256', secret).update(payload).digest('base64url')
|
||||
return `${payload}.${sig}`
|
||||
}
|
||||
|
||||
/** Returns the username when the cookie is intact and unexpired, else null. */
|
||||
export function verifySession(secret, cookieValue, nowSeconds) {
|
||||
if (typeof cookieValue !== 'string') return null
|
||||
const dot = cookieValue.lastIndexOf('.')
|
||||
if (dot <= 0) return null
|
||||
const payload = cookieValue.slice(0, dot)
|
||||
const sig = cookieValue.slice(dot + 1)
|
||||
const expected = createHmac('sha256', secret).update(payload).digest('base64url')
|
||||
if (sig.length !== expected.length || !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return null
|
||||
const sep = payload.lastIndexOf('.')
|
||||
if (sep <= 0) return null
|
||||
const username = Buffer.from(payload.slice(0, sep), 'base64url').toString('utf8')
|
||||
const expiry = Number(payload.slice(sep + 1))
|
||||
if (!Number.isFinite(expiry) || expiry <= nowSeconds) return null
|
||||
return username
|
||||
}
|
||||
Reference in New Issue
Block a user