Signierte Datei-Links (15 min) fuer Anhaenge im Ticketsystem
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
acf6ff9ee1
commit
5f5df7ba1a
2 files changed
+58
No files matched your search
@@ -0,0 +1,27 @@
|
||||
import { createHmac, timingSafeEqual } from 'node:crypto'
|
||||
import { config } from '../config.js'
|
||||
|
||||
/**
|
||||
* Signierte Datei-Links fuer das Ticketsystem (Spec 2026-10-01, Abschnitt 4 "Dateien ohne Header"): <img>, <audio>
|
||||
* und Downloads koennen keinen Authorization-Header tragen. Der Link gilt 15 Minuten und nur fuer genau diese
|
||||
* Datei dieses Kunden; signiert mit dem Sitzungs-Geheimnis des Portals.
|
||||
*/
|
||||
export const FILE_LINK_TTL_MS = 15 * 60 * 1000
|
||||
|
||||
const digest = (kind, customerId, id, exp) =>
|
||||
createHmac('sha256', config.sessionSecret).update(`${kind}:${customerId}:${id}:${exp}`).digest('hex')
|
||||
|
||||
export function signFileLink({ kind, customerId, id, now = Date.now() }) {
|
||||
const exp = now + FILE_LINK_TTL_MS
|
||||
return { exp, sig: digest(kind, customerId, id, exp) }
|
||||
}
|
||||
|
||||
/** exp kommt aus der Adresse (Text oder Zahl); falsch bei Ablauf, fremder Datei oder kaputter Signatur. */
|
||||
export function verifyFileLink({ kind, customerId, id, exp, sig, now = Date.now() }) {
|
||||
const expiry = typeof exp === 'number' ? exp : /^\d+$/.test(String(exp ?? '')) ? Number(exp) : NaN
|
||||
if (!Number.isFinite(expiry) || now >= expiry) return false
|
||||
if (typeof sig !== 'string' || !/^[0-9a-f]{64}$/.test(sig)) return false
|
||||
const expected = Buffer.from(digest(kind, customerId, id, expiry), 'hex')
|
||||
const given = Buffer.from(sig, 'hex')
|
||||
return given.length === expected.length && timingSafeEqual(given, expected)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
|
||||
// Signierte Datei-Links (15 min) fuer Anhaenge im Ticketsystem: <img>/<audio>/Download ohne Authorization-Header
|
||||
useTestEnv({ appwriteUrl: 'http://127.0.0.1:9/v1', stripeUrl: 'http://127.0.0.1:9' })
|
||||
const { FILE_LINK_TTL_MS, signFileLink, verifyFileLink } = await import('../server/services/signedLinks.js')
|
||||
|
||||
test('Signatur passt nur fuer dieselbe Datei, denselben Kunden und dieselbe Art', () => {
|
||||
assert.equal(FILE_LINK_TTL_MS, 900000)
|
||||
const { exp, sig } = signFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', now: 1000 })
|
||||
assert.equal(exp, 1000 + 900000)
|
||||
assert.match(sig, /^[0-9a-f]{64}$/)
|
||||
assert.equal(verifyFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', exp, sig, now: 2000 }), true)
|
||||
for (const other of [{ customerId: 'c2' }, { id: 'a2' }, { kind: 'takeover' }]) {
|
||||
assert.equal(verifyFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', exp, sig, now: 2000, ...other }), false, JSON.stringify(other))
|
||||
}
|
||||
})
|
||||
|
||||
test('abgelaufen genau bei exp, manipuliertes exp, kaputte Signatur', () => {
|
||||
const { exp, sig } = signFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', now: 0 })
|
||||
assert.equal(verifyFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', exp, sig, now: exp }), false)
|
||||
assert.equal(verifyFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', exp: exp + 1, sig, now: 0 }), false)
|
||||
assert.equal(verifyFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', exp: String(exp), sig, now: 0 }), true, 'exp aus der Query ist ein String')
|
||||
for (const bad of ['', 'abc', 'x'.repeat(64), undefined]) {
|
||||
assert.equal(verifyFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', exp, sig: bad, now: 0 }), false, String(bad))
|
||||
}
|
||||
for (const badExp of ['', 'abc', undefined, NaN]) {
|
||||
assert.equal(verifyFileLink({ kind: 'chat', customerId: 'c1', id: 'a1', exp: badExp, sig, now: 0 }), false, String(badExp))
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user