Add a hash-based password bypass link
All checks were successful
Docker / build-and-push (push) Successful in 3m8s
All checks were successful
Docker / build-and-push (push) Successful in 3m8s
The upload password gate can now be skipped with a bookmarkable #psst=<sha256-hash> fragment instead of typing the password each time. The server accepts either the plaintext password (X-Upload-Password) or its SHA-256 hash (X-Upload-Password-Hash), compared timing-safely. The client computes the hash locally after a normal password entry, so the bypass link never contains the real password — only a one-way digest of it, kept out of the URL query string (and thus server logs) by living in the fragment, same as the decryption key.
This commit is contained in:
24
server/src/auth.ts
Normal file
24
server/src/auth.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createHash, timingSafeEqual } from 'node:crypto'
|
||||
import type { Request } from 'express'
|
||||
import { config } from './config.js'
|
||||
|
||||
const expectedHash = createHash('sha256').update(config.uploadPassword).digest('hex')
|
||||
|
||||
function timingSafeStringEqual(a: string, b: string): boolean {
|
||||
const bufA = Buffer.from(a)
|
||||
const bufB = Buffer.from(b)
|
||||
return bufA.length === bufB.length && timingSafeEqual(bufA, bufB)
|
||||
}
|
||||
|
||||
// Accepts either the plaintext password (typed-in flow) or its SHA-256 hash
|
||||
// (the #psst= bypass-link flow) — the hash lets a bookmarked link work
|
||||
// without the plaintext password ever appearing in the URL.
|
||||
export function checkUploadCredential(req: Request): boolean {
|
||||
const plain = req.get('X-Upload-Password')
|
||||
if (plain !== undefined) return timingSafeStringEqual(plain, config.uploadPassword)
|
||||
|
||||
const hash = req.get('X-Upload-Password-Hash')
|
||||
if (hash !== undefined) return timingSafeStringEqual(hash.toLowerCase(), expectedHash)
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -3,21 +3,18 @@ import fs from 'node:fs'
|
||||
import { config } from './config.js'
|
||||
import { db, blobPath } from './db.js'
|
||||
import { newBlobId, newConfirmToken } from './ids.js'
|
||||
import { checkUploadCredential } from './auth.js'
|
||||
|
||||
export const router = Router()
|
||||
|
||||
function checkUploadPassword(req: import('express').Request): boolean {
|
||||
return req.get('X-Upload-Password') === config.uploadPassword
|
||||
}
|
||||
|
||||
// Lets the client gate the upload UI behind a password without uploading
|
||||
// anything yet.
|
||||
router.post('/auth/check', (req, res) => {
|
||||
res.json({ ok: checkUploadPassword(req) })
|
||||
res.json({ ok: checkUploadCredential(req) })
|
||||
})
|
||||
|
||||
router.post('/drops', (req, res) => {
|
||||
if (!checkUploadPassword(req)) {
|
||||
if (!checkUploadCredential(req)) {
|
||||
res.status(401).json({ error: 'bad password' })
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user