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",
"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",

View file

@ -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]) => {

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';
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) {
this._server = (async () => {
const store = await Storages.db();
const meta = await getInstanceMetadata(store);
const keys = await Conf.vapidKeys;
if (keys) {
return await ApplicationServer.new({
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 = {},
): Promise<void> {
const server = await this.server;
if (!server) {
return;
}
const subscriber = new PushSubscriber(server, subscription);
const text = JSON.stringify(json);
return subscriber.pushTextMessage(text, opts);

View file

@ -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<CryptoKeyPair> {
return generateVapidKeys(); // FIXME: get the key from environment.
private static _vapidPublicKey: Promise<string | undefined> | undefined;
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 = {
/** 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);

View file

@ -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,

View file

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

View file

@ -3,7 +3,7 @@ import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
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())