From 198ec973b60a2eef4408c563e12355fdd3a6b9ae Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 8 Oct 2024 19:57:46 -0500 Subject: [PATCH] Load VAPID keys from configuration --- deno.json | 3 +- scripts/setup.ts | 11 ++++ scripts/vapid.ts | 7 +++ src/DittoPush.ts | 22 +++++--- src/config.ts | 57 ++++++++++++++------- src/controllers/api/instance.ts | 2 +- src/controllers/api/push.ts | 3 +- src/db/migrations/038_push_subscriptions.ts | 2 +- 8 files changed, 79 insertions(+), 28 deletions(-) create mode 100644 scripts/vapid.ts diff --git a/deno.json b/deno.json index 6c0fb961..e66d70cc 100644 --- a/deno.json +++ b/deno.json @@ -20,7 +20,8 @@ "soapbox": "curl -O https://dl.soapbox.pub/main/soapbox.zip && mkdir -p public && mv soapbox.zip public/ && cd public/ && unzip -o soapbox.zip && rm soapbox.zip", "trends": "deno run -A scripts/trends.ts", "clean:deps": "deno cache --reload src/app.ts", - "db:populate-search": "deno run -A scripts/db-populate-search.ts" + "db:populate-search": "deno run -A scripts/db-populate-search.ts", + "vapid": "deno run -A scripts/vapid.ts" }, "unstable": [ "cron", diff --git a/scripts/setup.ts b/scripts/setup.ts index 32376692..3f3fc955 100644 --- a/scripts/setup.ts +++ b/scripts/setup.ts @@ -1,3 +1,5 @@ +import { generateVapidKeys } from '@negrel/webpush'; +import { encodeBase64 } from '@std/encoding/base64'; import { exists } from '@std/fs/exists'; import { generateSecretKey, nip19 } from 'nostr-tools'; import question from 'question-deno'; @@ -95,6 +97,15 @@ if (vars.DITTO_UPLOADER === 'local') { vars.MEDIA_DOMAIN = `https://${mediaDomain}`; } +const VAPID_PRIVATE_KEY = Deno.env.get('VAPID_PRIVATE_KEY'); +if (VAPID_PRIVATE_KEY) { + vars.VAPID_PRIVATE_KEY = VAPID_PRIVATE_KEY; +} else { + const { privateKey } = await generateVapidKeys({ extractable: true }); + const bytes = await crypto.subtle.exportKey('pkcs8', privateKey); + vars.VAPID_PRIVATE_KEY = encodeBase64(bytes); +} + console.log('Writing to .env file...'); const result = Object.entries(vars).reduce((acc, [key, value]) => { diff --git a/scripts/vapid.ts b/scripts/vapid.ts new file mode 100644 index 00000000..2e467881 --- /dev/null +++ b/scripts/vapid.ts @@ -0,0 +1,7 @@ +import { generateVapidKeys } from '@negrel/webpush'; +import { encodeBase64 } from '@std/encoding/base64'; + +const { privateKey } = await generateVapidKeys({ extractable: true }); +const bytes = await crypto.subtle.exportKey('pkcs8', privateKey); + +console.log(encodeBase64(bytes)); diff --git a/src/DittoPush.ts b/src/DittoPush.ts index 52ce1009..364f08ae 100644 --- a/src/DittoPush.ts +++ b/src/DittoPush.ts @@ -5,18 +5,23 @@ import { Storages } from '@/storages.ts'; import { getInstanceMetadata } from '@/utils/instance.ts'; export class DittoPush { - static _server: Promise | undefined; + static _server: Promise | undefined; - static get server(): Promise { + static get server(): Promise { if (!this._server) { this._server = (async () => { const store = await Storages.db(); const meta = await getInstanceMetadata(store); + const keys = await Conf.vapidKeys; - return await ApplicationServer.new({ - contactInformation: `mailto:${meta.email}`, - vapidKeys: await Conf.vapidKeys, - }); + if (keys) { + return await ApplicationServer.new({ + contactInformation: `mailto:${meta.email}`, + vapidKeys: keys, + }); + } else { + console.warn('VAPID keys are not set. Push notifications will be disabled.'); + } })(); } @@ -29,6 +34,11 @@ export class DittoPush { opts: PushMessageOptions = {}, ): Promise { const server = await this.server; + + if (!server) { + return; + } + const subscriber = new PushSubscriber(server, subscription); const text = JSON.stringify(json); return subscriber.pushTextMessage(text, opts); diff --git a/src/config.ts b/src/config.ts index 2634c92c..bdeb8ec2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,9 +1,11 @@ import os from 'node:os'; import ISO6391, { LanguageCode } from 'iso-639-1'; -import { generateVapidKeys } from '@negrel/webpush'; import * as dotenv from '@std/dotenv'; import { getPublicKey, nip19 } from 'nostr-tools'; import { z } from 'zod'; +import { decodeBase64, encodeBase64 } from '@std/encoding/base64'; + +import { getEcdsaPublicKey } from '@/utils/crypto.ts'; /** Load environment config from `.env` */ await dotenv.load({ @@ -83,8 +85,42 @@ class Conf { static get pgliteDebug(): 0 | 1 | 2 | 3 | 4 | 5 { return Number(Deno.env.get('PGLITE_DEBUG') || 0) as 0 | 1 | 2 | 3 | 4 | 5; } - static get vapidKeys(): Promise { - return generateVapidKeys(); // FIXME: get the key from environment. + private static _vapidPublicKey: Promise | undefined; + static get vapidPublicKey(): Promise { + if (!this._vapidPublicKey) { + this._vapidPublicKey = (async () => { + const keys = await Conf.vapidKeys; + if (keys) { + const { publicKey } = keys; + const bytes = await crypto.subtle.exportKey('raw', publicKey); + return encodeBase64(bytes); + } + })(); + } + + return this._vapidPublicKey; + } + static get vapidKeys(): Promise { + return (async () => { + const encoded = Deno.env.get('VAPID_PRIVATE_KEY'); + + if (!encoded) { + return; + } + + const keyData = decodeBase64(encoded); + + const privateKey = await crypto.subtle.importKey( + 'pkcs8', + keyData, + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign'], + ); + const publicKey = await getEcdsaPublicKey(privateKey, true); + + return { privateKey, publicKey }; + })(); } static db = { /** Database query timeout configurations. */ @@ -107,21 +143,6 @@ class Conf { static get captchaTTL(): number { return Number(Deno.env.get('CAPTCHA_TTL') || 5 * 60 * 1000); } - /** - * BIP-32 derivation paths for different crypto use-cases. - * The `DITTO_NSEC` is used as the seed. - * Keys can be rotated by changing the derviation path. - */ - static wallet = { - /** Private key for AES-GCM encryption in the Postgres database. */ - get dbKeyPath(): string { - return Deno.env.get('WALLET_DB_KEY_PATH') || "m/0'/1'"; - }, - /** VAPID private key path. */ - get vapidKeyPath(): string { - return Deno.env.get('WALLET_VAPID_KEY_PATH') || "m/0'/3'"; - }, - }; /** Character limit to enforce for posts made through Mastodon API. */ static get postCharLimit(): number { return Number(Deno.env.get('POST_CHAR_LIMIT') || 5000); diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index 9ab9930d..fbf17a3f 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -104,7 +104,7 @@ const instanceV2Controller: AppController = async (c) => { streaming: `${wsProtocol}//${host}`, }, vapid: { - public_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', + public_key: await Conf.vapidPublicKey, }, accounts: { max_featured_tags: 10, diff --git a/src/controllers/api/push.ts b/src/controllers/api/push.ts index 2541966c..9eef700c 100644 --- a/src/controllers/api/push.ts +++ b/src/controllers/api/push.ts @@ -2,6 +2,7 @@ import { nip19 } from 'nostr-tools'; import { z } from 'zod'; import { AppController } from '@/app.ts'; +import { Conf } from '@/config.ts'; import { Storages } from '@/storages.ts'; import { parseBody } from '@/utils/api.ts'; import { getTokenHash } from '@/utils/auth.ts'; @@ -76,6 +77,6 @@ export const pushSubscribeController: AppController = async (c) => { endpoint: subscription.endpoint, alerts: data?.alerts ?? {}, policy: data?.policy ?? 'all', - // TODO: server_key + server_key: await Conf.vapidPublicKey, }); }; diff --git a/src/db/migrations/038_push_subscriptions.ts b/src/db/migrations/038_push_subscriptions.ts index be0b9fcb..c3fa95e9 100644 --- a/src/db/migrations/038_push_subscriptions.ts +++ b/src/db/migrations/038_push_subscriptions.ts @@ -3,7 +3,7 @@ import { Kysely, sql } from 'kysely'; export async function up(db: Kysely): Promise { await db.schema .createTable('push_subscriptions') - .addColumn('id', 'bigint', (c) => c.primaryKey().autoIncrement()) + .addColumn('id', 'bigserial', (c) => c.primaryKey()) .addColumn('pubkey', 'char(64)', (c) => c.notNull()) .addColumn('token_hash', 'bytea', (c) => c.references('auth_tokens.token_hash').onDelete('cascade').notNull()) .addColumn('endpoint', 'text', (c) => c.notNull())