Add a hash-based password bypass link
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:
explewd
2026-07-09 19:48:56 +02:00
parent 22619ebd11
commit 121dbe81f1
5 changed files with 91 additions and 16 deletions

24
server/src/auth.ts Normal file
View 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
}

View File

@@ -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
}