source: Klonkt/test/paid-secret.test.js@ f574435

main
Last change on this file since f574435 was 4590e66, checked in by Robin <roboburr@…>, 7 weeks ago

Feature: auto-generate the paid-posts encryption key (no env needed)

Nobody should have to hand-edit the env to use paid posts. CryptoBox now
resolves its key as: PAID_SECRET (env) if set, else a persisted key file
next to the database, auto-generated (0600) on first use. New installs and
existing users after an update get a key with zero config; a self-hoster
who set PAID_SECRET (Bart already did) keeps working unchanged, env wins.

The key lives OUTSIDE the sqlite DB on purpose: encrypting the Patreon
secrets is pointless if the key sits in the same file a DB dump would leak.
If the file can't be persisted (read-only fs) the box stays "not ready"
rather than using an ephemeral key that a restart would lose, so ciphertext
never becomes undecryptable.

Admin copy that referenced PAID_SECRET is updated: the warning now describes
the real remaining failure (key can't be created/read), not a missing env.

Changed files:
src/services/CryptoBox.js

  • resolve secret: env, else auto-generated 0600 key file by the database

src/services/PaidPatreonService.js

  • save error no longer names PAID_SECRET

src/routes/admin-paid.js, src/views/pages/admin-paid.ejs

  • not-ready copy describes the key file, not a missing env var

New file:
test/paid-secret.test.js

  • without PAID_SECRET: key file generated (0600), encrypt/decrypt roundtrips, key persists

remarks: the generated storage/.paid-secret must be backed up alongside the
DB, or the stored Patreon secrets can't be decrypted after a restore.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 1.6 KB
Line 
1// Paid posts: the encryption key auto-generates (no env needed). This file runs
2// in its own process (node --test), so deleting PAID_SECRET here doesn't affect
3// the other paid tests, which set it.
4import { test } from 'node:test';
5import assert from 'node:assert/strict';
6import fs from 'fs';
7import os from 'os';
8import path from 'path';
9
10delete process.env.PAID_SECRET; // force the file path
11const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'paidkey-'));
12process.env.DATABASE_PATH = path.join(dir, 'database.sqlite');
13
14const { encrypt, decrypt, cryptoBoxReady } = await import('../src/services/CryptoBox.js');
15const keyFile = path.join(dir, '.paid-secret');
16
17test('without PAID_SECRET, a key file is generated and the box is ready', () => {
18 assert.equal(cryptoBoxReady(), true);
19 assert.ok(fs.existsSync(keyFile), 'key file was written next to the database');
20 if (process.platform !== 'win32') {
21 assert.equal(fs.statSync(keyFile).mode & 0o777, 0o600, 'key file is 0600');
22 }
23});
24
25test('encrypt/decrypt roundtrips with the generated key', () => {
26 const enc = encrypt('patreon-creator-token');
27 assert.notEqual(enc, 'patreon-creator-token');
28 assert.equal(decrypt(enc), 'patreon-creator-token');
29});
30
31test('the generated key persists (a second read reuses the same file)', () => {
32 const first = fs.readFileSync(keyFile, 'utf8');
33 assert.ok(first.length >= 16);
34 // Encrypt now, and decrypting still works: the same persisted key is used.
35 const enc = encrypt('x');
36 assert.equal(fs.readFileSync(keyFile, 'utf8'), first, 'key file unchanged');
37 assert.equal(decrypt(enc), 'x');
38});
Note: See TracBrowser for help on using the repository browser.