|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import crypto from "crypto"; |
|
|
import { Request, Response, NextFunction } from "express"; |
|
|
import { logger } from "../logger"; |
|
|
|
|
|
const log = logger.child({ module: "check-risu-token" }); |
|
|
|
|
|
const RISUAI_PUBLIC_KEY = ` |
|
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArEXBmHQfy/YdNIu9lfNC |
|
|
xHbVwb2aYx07pBEmqQJtvVEOISj80fASxg+cMJH+/0a/Z4gQgzUJl0HszRpMXAfu |
|
|
wmRoetedyC/6CLraHke0Qad/AEHAKwG9A+NwsHRv/cDfP8euAr20cnOyVa79bZsl |
|
|
1wlHYQQGo+ve+P/FXtjLGJ/KZYr479F5jkIRKZxPE8mRmkhAVS/u+18QM94BzfoI |
|
|
0LlbwvvCHe18QSX6viDK+HsqhhyYDh+0FgGNJw6xKYLdExbQt77FSukH7NaJmVAs |
|
|
kYuIJbnAGw5Oq0L6dXFW2DFwlcLz51kPVOmDc159FsQjyuPnta7NiZAANS8KM1CJ |
|
|
pwIDAQAB`; |
|
|
let IMPORTED_RISU_KEY: CryptoKey | null = null; |
|
|
|
|
|
type RisuToken = { id: string; expiresIn: number }; |
|
|
type SignedToken = { data: RisuToken; sig: string }; |
|
|
|
|
|
(async () => { |
|
|
try { |
|
|
log.debug("Importing Risu public key"); |
|
|
IMPORTED_RISU_KEY = await crypto.subtle.importKey( |
|
|
"spki", |
|
|
Buffer.from(RISUAI_PUBLIC_KEY.replace(/\s/g, ""), "base64"), |
|
|
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, |
|
|
true, |
|
|
["verify"] |
|
|
); |
|
|
log.debug("Imported Risu public key"); |
|
|
} catch (err) { |
|
|
log.warn({ error: err.message }, "Error importing Risu public key"); |
|
|
IMPORTED_RISU_KEY = null; |
|
|
} |
|
|
})(); |
|
|
|
|
|
export async function checkRisuToken( |
|
|
req: Request, |
|
|
_res: Response, |
|
|
next: NextFunction |
|
|
) { |
|
|
let header = req.header("x-risu-tk") || null; |
|
|
if (!header || !IMPORTED_RISU_KEY) { |
|
|
return next(); |
|
|
} |
|
|
|
|
|
try { |
|
|
const { valid, data } = await validCheck(header); |
|
|
|
|
|
if (!valid || !data) { |
|
|
req.log.warn( |
|
|
{ token: header, data }, |
|
|
"Invalid RisuAI token; using IP instead" |
|
|
); |
|
|
} else { |
|
|
req.log.info("RisuAI token validated"); |
|
|
req.risuToken = String(data.id); |
|
|
} |
|
|
} catch (err) { |
|
|
req.log.warn( |
|
|
{ error: err.message }, |
|
|
"Error validating RisuAI token; using IP instead" |
|
|
); |
|
|
} |
|
|
|
|
|
next(); |
|
|
} |
|
|
|
|
|
async function validCheck(header: string) { |
|
|
let tk: SignedToken; |
|
|
try { |
|
|
tk = JSON.parse( |
|
|
Buffer.from(decodeURIComponent(header), "base64").toString("utf-8") |
|
|
); |
|
|
} catch (err) { |
|
|
log.warn({ error: err.message }, "Provided unparseable RisuAI token"); |
|
|
return { valid: false }; |
|
|
} |
|
|
const data: RisuToken = tk.data; |
|
|
const sig = Buffer.from(tk.sig, "base64"); |
|
|
|
|
|
if (data.expiresIn < Math.floor(Date.now() / 1000)) { |
|
|
log.warn({ token: header }, "Provided expired RisuAI token"); |
|
|
return { valid: false }; |
|
|
} |
|
|
|
|
|
const valid = await crypto.subtle.verify( |
|
|
{ name: "RSASSA-PKCS1-v1_5" }, |
|
|
IMPORTED_RISU_KEY!, |
|
|
sig, |
|
|
Buffer.from(JSON.stringify(data)) |
|
|
); |
|
|
|
|
|
if (!valid) { |
|
|
log.warn({ token: header }, "RisuAI token failed signature check"); |
|
|
} |
|
|
|
|
|
return { valid, data }; |
|
|
} |
|
|
|