Load VAPID keys from configuration

This commit is contained in:
Alex Gleason 2024-10-08 19:57:46 -05:00
parent 8f437839d0
commit 198ec973b6
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
8 changed files with 79 additions and 28 deletions

View file

@ -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", "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", "trends": "deno run -A scripts/trends.ts",
"clean:deps": "deno cache --reload src/app.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": [ "unstable": [
"cron", "cron",

View file

@ -1,3 +1,5 @@
import { generateVapidKeys } from '@negrel/webpush';
import { encodeBase64 } from '@std/encoding/base64';
import { exists } from '@std/fs/exists'; import { exists } from '@std/fs/exists';
import { generateSecretKey, nip19 } from 'nostr-tools'; import { generateSecretKey, nip19 } from 'nostr-tools';
import question from 'question-deno'; import question from 'question-deno';
@ -95,6 +97,15 @@ if (vars.DITTO_UPLOADER === 'local') {
vars.MEDIA_DOMAIN = `https://${mediaDomain}`; 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...'); console.log('Writing to .env file...');
const result = Object.entries(vars).reduce((acc, [key, value]) => { const result = Object.entries(vars).reduce((acc, [key, value]) => {

7
scripts/vapid.ts Normal file
View file

@ -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));

View file

@ -5,18 +5,23 @@ import { Storages } from '@/storages.ts';
import { getInstanceMetadata } from '@/utils/instance.ts'; import { getInstanceMetadata } from '@/utils/instance.ts';
export class DittoPush { export class DittoPush {
static _server: Promise<ApplicationServer> | undefined; static _server: Promise<ApplicationServer | undefined> | undefined;
static get server(): Promise<ApplicationServer> { static get server(): Promise<ApplicationServer | undefined> {
if (!this._server) { if (!this._server) {
this._server = (async () => { this._server = (async () => {
const store = await Storages.db(); const store = await Storages.db();
const meta = await getInstanceMetadata(store); const meta = await getInstanceMetadata(store);
const keys = await Conf.vapidKeys;
if (keys) {
return await ApplicationServer.new({ return await ApplicationServer.new({
contactInformation: `mailto:${meta.email}`, contactInformation: `mailto:${meta.email}`,
vapidKeys: await Conf.vapidKeys, vapidKeys: keys,
}); });
} else {
console.warn('VAPID keys are not set. Push notifications will be disabled.');
}
})(); })();
} }
@ -29,6 +34,11 @@ export class DittoPush {
opts: PushMessageOptions = {}, opts: PushMessageOptions = {},
): Promise<void> { ): Promise<void> {
const server = await this.server; const server = await this.server;
if (!server) {
return;
}
const subscriber = new PushSubscriber(server, subscription); const subscriber = new PushSubscriber(server, subscription);
const text = JSON.stringify(json); const text = JSON.stringify(json);
return subscriber.pushTextMessage(text, opts); return subscriber.pushTextMessage(text, opts);

View file

@ -1,9 +1,11 @@
import os from 'node:os'; import os from 'node:os';
import ISO6391, { LanguageCode } from 'iso-639-1'; import ISO6391, { LanguageCode } from 'iso-639-1';
import { generateVapidKeys } from '@negrel/webpush';
import * as dotenv from '@std/dotenv'; import * as dotenv from '@std/dotenv';
import { getPublicKey, nip19 } from 'nostr-tools'; import { getPublicKey, nip19 } from 'nostr-tools';
import { z } from 'zod'; import { z } from 'zod';
import { decodeBase64, encodeBase64 } from '@std/encoding/base64';
import { getEcdsaPublicKey } from '@/utils/crypto.ts';
/** Load environment config from `.env` */ /** Load environment config from `.env` */
await dotenv.load({ await dotenv.load({
@ -83,8 +85,42 @@ class Conf {
static get pgliteDebug(): 0 | 1 | 2 | 3 | 4 | 5 { static get pgliteDebug(): 0 | 1 | 2 | 3 | 4 | 5 {
return Number(Deno.env.get('PGLITE_DEBUG') || 0) as 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<CryptoKeyPair> { private static _vapidPublicKey: Promise<string | undefined> | undefined;
return generateVapidKeys(); // FIXME: get the key from environment. static get vapidPublicKey(): Promise<string | undefined> {
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<CryptoKeyPair | undefined> {
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 = { static db = {
/** Database query timeout configurations. */ /** Database query timeout configurations. */
@ -107,21 +143,6 @@ class Conf {
static get captchaTTL(): number { static get captchaTTL(): number {
return Number(Deno.env.get('CAPTCHA_TTL') || 5 * 60 * 1000); 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. */ /** Character limit to enforce for posts made through Mastodon API. */
static get postCharLimit(): number { static get postCharLimit(): number {
return Number(Deno.env.get('POST_CHAR_LIMIT') || 5000); return Number(Deno.env.get('POST_CHAR_LIMIT') || 5000);

View file

@ -104,7 +104,7 @@ const instanceV2Controller: AppController = async (c) => {
streaming: `${wsProtocol}//${host}`, streaming: `${wsProtocol}//${host}`,
}, },
vapid: { vapid: {
public_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', public_key: await Conf.vapidPublicKey,
}, },
accounts: { accounts: {
max_featured_tags: 10, max_featured_tags: 10,

View file

@ -2,6 +2,7 @@ import { nip19 } from 'nostr-tools';
import { z } from 'zod'; import { z } from 'zod';
import { AppController } from '@/app.ts'; import { AppController } from '@/app.ts';
import { Conf } from '@/config.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { parseBody } from '@/utils/api.ts'; import { parseBody } from '@/utils/api.ts';
import { getTokenHash } from '@/utils/auth.ts'; import { getTokenHash } from '@/utils/auth.ts';
@ -76,6 +77,6 @@ export const pushSubscribeController: AppController = async (c) => {
endpoint: subscription.endpoint, endpoint: subscription.endpoint,
alerts: data?.alerts ?? {}, alerts: data?.alerts ?? {},
policy: data?.policy ?? 'all', policy: data?.policy ?? 'all',
// TODO: server_key server_key: await Conf.vapidPublicKey,
}); });
}; };

View file

@ -3,7 +3,7 @@ import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> { export async function up(db: Kysely<any>): Promise<void> {
await db.schema await db.schema
.createTable('push_subscriptions') .createTable('push_subscriptions')
.addColumn('id', 'bigint', (c) => c.primaryKey().autoIncrement()) .addColumn('id', 'bigserial', (c) => c.primaryKey())
.addColumn('pubkey', 'char(64)', (c) => c.notNull()) .addColumn('pubkey', 'char(64)', (c) => c.notNull())
.addColumn('token_hash', 'bytea', (c) => c.references('auth_tokens.token_hash').onDelete('cascade').notNull()) .addColumn('token_hash', 'bytea', (c) => c.references('auth_tokens.token_hash').onDelete('cascade').notNull())
.addColumn('endpoint', 'text', (c) => c.notNull()) .addColumn('endpoint', 'text', (c) => c.notNull())