summary refs log tree commit diff
path: root/src/util/Signing.ts
diff options
context:
space:
mode:
authorPuyodead1 <puyodead@proton.me>2025-04-16 23:28:43 -0400
committerRory& <root@rory.gay>2025-05-03 12:30:22 +0200
commit080b2c7d383b5e09fee97f267d35f4e7cd22f0a9 (patch)
tree066bfcba19fdc7060d28d46db8b1218c0e365011 /src/util/Signing.ts
parentMerge pull request #1274 from dank074/patch/fix-requestGuildMembers-libraires (diff)
downloadserver-ts-080b2c7d383b5e09fee97f267d35f4e7cd22f0a9.tar.xz
Implement signed cdn urls
Diffstat (limited to 'src/util/Signing.ts')
-rw-r--r--src/util/Signing.ts136
1 files changed, 136 insertions, 0 deletions
diff --git a/src/util/Signing.ts b/src/util/Signing.ts
new file mode 100644

index 000000000..5763ed165 --- /dev/null +++ b/src/util/Signing.ts
@@ -0,0 +1,136 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { Config } from "@spacebar/util"; +import { createHmac, timingSafeEqual } from "crypto"; +import ms, { StringValue } from "ms"; +import { ParsedQs } from "qs"; + +export const getUrlSignature = (path: string) => { + const { cdnSignatureKey, cdnSignatureDuration } = Config.get().security; + + // calculate the expiration time + const now = Date.now(); + const issuedAt = now.toString(16); + const expiresAt = (now + ms(cdnSignatureDuration as StringValue)).toString( + 16, + ); + + // hash the url with the cdnSignatureKey + const hash = createHmac("sha256", cdnSignatureKey as string) + .update(path) + .update(issuedAt) + .update(expiresAt) + .digest("hex"); + + return { + hash, + issuedAt, + expiresAt, + }; +}; + +export const calculateHash = ( + url: string, + issuedAt: string, + expiresAt: string, +) => { + const { cdnSignatureKey } = Config.get().security; + const hash = createHmac("sha256", cdnSignatureKey as string) + .update(url) + .update(issuedAt) + .update(expiresAt) + .digest("hex"); + return hash; +}; + +export const isExpired = (ex: string, is: string) => { + // convert issued at + const issuedAt = parseInt(is, 16); + const expiresAt = parseInt(ex, 16); + + if (Number.isNaN(issuedAt) || Number.isNaN(expiresAt)) { + // console.debug("Invalid timestamps in query"); + return true; + } + + const currentTime = Date.now(); + const isExpired = expiresAt < currentTime; + const isValidIssuedAt = issuedAt < currentTime; + if (isExpired || !isValidIssuedAt) { + // console.debug("Signature expired"); + return true; + } + + return false; +}; + +export const hasValidSignature = (path: string, query: ParsedQs) => { + // get url path + const { ex, is, hm } = query; + + // if the required query parameters are not present, return false + if (!ex || !is || !hm) return false; + + // check if the signature is expired + if (isExpired(ex as string, is as string)) { + return false; + } + + const calcd = calculateHash(path, is as string, ex as string); + const calculated = Buffer.from(calcd); + const received = Buffer.from(hm as string); + + const isHashValid = + calculated.length === received.length && + timingSafeEqual(calculated, received); + // if (!isHashValid) { + // console.debug("Invalid signature"); + // console.debug(calcd, hm); + // } + return isHashValid; +}; + +export const resignUrl = (attachmentUrl: string) => { + const url = new URL(attachmentUrl); + + // if theres an existing signature, check if its expired or not. no reason to resign if its not expired + if (url.searchParams.has("ex") && url.searchParams.has("is")) { + // extract the ex and is + const ex = url.searchParams.get("ex"); + const is = url.searchParams.get("is"); + + if (!isExpired(ex as string, is as string)) { + // if the signature is not expired, return the url as is + return attachmentUrl; + } + } + + let path = url.pathname; + // strip / from the start + if (path.startsWith("/")) { + path = path.slice(1); + } + + const { hash, issuedAt, expiresAt } = getUrlSignature(path); + url.searchParams.set("ex", expiresAt); + url.searchParams.set("is", issuedAt); + url.searchParams.set("hm", hash); + + return url.toString(); +};