diff --git a/generate-keys.mjs b/generate-keys.mjs new file mode 100644 index 0000000..07bd706 --- /dev/null +++ b/generate-keys.mjs @@ -0,0 +1,17 @@ +import webPush from "web-push"; + +const vapidKeys = webPush.generateVAPIDKeys(); +const cookieSecretKey = Buffer.from( + await crypto.subtle.exportKey( + "raw", + await crypto.subtle.generateKey( + { name: "HMAC", hash: "SHA-256" }, true, ["sign", "verify"] + ) + ) +).toString("base64url"); + +console.log(`\ +NUXT_PUBLIC_VAPID_PUBLIC_KEY=${vapidKeys.publicKey} +NUXT_VAPID_PRIVATE_KEY=${vapidKeys.privateKey} +NUXT_COOKIE_SECRET_KEY=${cookieSecretKey} +`); diff --git a/generate-vapid-keys.mjs b/generate-vapid-keys.mjs deleted file mode 100644 index a4dc34b..0000000 --- a/generate-vapid-keys.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import webPush from "web-push"; -import fs from "node:fs/promises"; - -const vapidKeys = webPush.generateVAPIDKeys(); - -const envData = `\ -NUXT_PUBLIC_VAPID_PUBLIC_KEY=${vapidKeys.publicKey} -NUXT_VAPID_PRIVATE_KEY=${vapidKeys.privateKey} -`; -await fs.writeFile(".env", envData, "utf-8"); diff --git a/nuxt.config.ts b/nuxt.config.ts index 87883a1..f7de71c 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -3,6 +3,7 @@ export default defineNuxtConfig({ compatibilityDate: '2024-11-01', devtools: { enabled: true }, runtimeConfig: { + cookieSecretKey: "", vapidPrivateKey: "", public: { vapidPublicKey: "", diff --git a/server/utils/signed-cookie.ts b/server/utils/signed-cookie.ts new file mode 100644 index 0000000..678220b --- /dev/null +++ b/server/utils/signed-cookie.ts @@ -0,0 +1,42 @@ +import type { H3Event } from "h3"; + +let cachedCookieSecret: CryptoKey; +export async function useCookieSecret(event: H3Event) { + if (cachedCookieSecret) + return cachedCookieSecret; + + const runtimeConfig = useRuntimeConfig(event); + return cachedCookieSecret = await crypto.subtle.importKey( + "raw", + Buffer.from(runtimeConfig.cookieSecretKey, "base64url"), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"], + ); +} + +export async function setSignedCookie(event: H3Event, name: string, value: string) { + const secret = await useCookieSecret(event); + const signature = await crypto.subtle.sign("HMAC", secret, Buffer.from(value)); + const cookie = `${value}.${Buffer.from(signature).toString("base64url")}` + setCookie(event, name, cookie, { httpOnly: true, secure: true, sameSite: true }); +} + +export async function getSignedCookie(event: H3Event, name: string) { + const cookie = getCookie(event, name); + if (!cookie) + return; + + const rightDot = cookie.lastIndexOf("."); + if (rightDot === -1) + return; + + const value = cookie.slice(0, rightDot); + const secret = await useCookieSecret(event); + const signature = Buffer.from(cookie.slice(rightDot + 1), "base64url"); + const valid = await crypto.subtle.verify("HMAC", secret, signature, Buffer.from(value)); + if (!valid) + return + + return value; +}