Changeset 4590e66 in Klonkt
- Timestamp:
- 07/21/2026 03:39:35 AM (7 weeks ago)
- Branches:
- main
- Children:
- f574435
- Parents:
- b9beb16
- git-author:
- Robin <roboburr@…> (07/21/2026 03:39:33 AM)
- git-committer:
- Robin <roboburr@…> (07/21/2026 03:39:35 AM)
- Files:
-
- 1 added
- 4 edited
-
src/routes/admin-paid.js (modified) (1 diff)
-
src/services/CryptoBox.js (modified) (1 diff)
-
src/services/PaidPatreonService.js (modified) (1 diff)
-
src/views/pages/admin-paid.ejs (modified) (1 diff)
-
test/paid-secret.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/admin-paid.js
rb9beb16 r4590e66 48 48 router.post('/', requireGod, (req, res) => { 49 49 if (!gate(req, res)) return; 50 if (!cryptoBoxReady()) return res.redirect('/admin/paid?error=' + encodeURIComponent(' PAID_SECRET ontbreekt in de serverconfig; secrets kunnen niet versleuteldworden opgeslagen.'));50 if (!cryptoBoxReady()) return res.redirect('/admin/paid?error=' + encodeURIComponent('De encryptiesleutel kon niet worden aangemaakt of gelezen (schrijfrechten op de opslagmap?); secrets kunnen niet veilig worden opgeslagen.')); 51 51 const b = req.body || {}; 52 52 const eur = String(b.default_min_eur || '').replace(',', '.').trim(); -
src/services/CryptoBox.js
rb9beb16 r4590e66 1 1 // Symmetric encryption for secrets at rest (paid posts: the site owner's 2 2 // Patreon creator token, klonkt-demo-aki slice 1). AES-256-GCM with a key 3 // derived from PAID_SECRET (env), so a database dump alone leaks nothing usable.3 // derived from a secret, so a database dump alone leaks nothing usable. 4 4 // Format: base64(iv) : base64(tag) : base64(ciphertext). 5 5 // 6 // The codebase had no symmetric-crypto helper; this is that one, kept tiny and 7 // native (no dependency). If PAID_SECRET is unset, encryption is refused loudly 8 // rather than storing plaintext. 6 // The secret is resolved in this order so nobody has to edit the env: 7 // 1. PAID_SECRET (env) — authoritative; a self-hoster who set it by hand 8 // (or Bart, who already did) keeps working unchanged. 9 // 2. a persisted key file next to the database, auto-generated on first use 10 // (0600). This is what "first run" and "existing users after an update" 11 // get automatically. 12 // The key lives OUTSIDE the sqlite DB on purpose: encrypting the Patreon 13 // secrets is pointless if the key sits in the same file a DB dump would leak. 9 14 import crypto from 'crypto'; 15 import fs from 'fs'; 16 import path from 'path'; 17 import { fileURLToPath } from 'url'; 18 19 const __dirname = path.dirname(fileURLToPath(import.meta.url)); 20 21 // The key file sits in the same directory as the database. 22 function keyFilePath() { 23 const dbPath = process.env.DATABASE_PATH || path.join(__dirname, '../../storage/database.sqlite'); 24 const dir = dbPath === ':memory:' ? path.join(__dirname, '../../storage') : path.dirname(dbPath); 25 return path.join(dir, '.paid-secret'); 26 } 27 28 // Read the persisted key, generating + writing it (0600) the first time. 29 function fileSecret() { 30 const file = keyFilePath(); 31 try { 32 const existing = fs.readFileSync(file, 'utf8').trim(); 33 if (existing.length >= 16) return existing; 34 } catch { /* not created yet */ } 35 const generated = crypto.randomBytes(32).toString('base64'); 36 fs.mkdirSync(path.dirname(file), { recursive: true }); 37 fs.writeFileSync(file, generated, { mode: 0o600 }); 38 try { fs.chmodSync(file, 0o600); } catch { /* non-POSIX fs */ } 39 return generated; 40 } 41 42 // env wins; otherwise the auto-generated file. Never returns an ephemeral key: 43 // if the file can't be persisted, fileSecret throws and the feature stays gated 44 // (cryptoBoxReady false) rather than encrypting with a key lost on restart. 45 function resolveSecret() { 46 const env = process.env.PAID_SECRET; 47 if (env && String(env).length >= 16) return String(env); 48 return fileSecret(); 49 } 10 50 11 51 let _key = null; 12 52 function key() { 13 53 if (_key) return _key; 14 const secret = process.env.PAID_SECRET; 15 if (!secret || String(secret).length < 16) { 16 throw new Error('PAID_SECRET is missing or too short (need >= 16 chars) to encrypt paid-posts secrets'); 17 } 18 _key = crypto.scryptSync(String(secret), 'klonkt-paid', 32); 54 _key = crypto.scryptSync(resolveSecret(), 'klonkt-paid', 32); 19 55 return _key; 20 56 } -
src/services/PaidPatreonService.js
rb9beb16 r4590e66 50 50 // the admin form can be re-saved without re-pasting the secret. 51 51 export function saveOwnerConfig(siteId, patch) { 52 if (!cryptoBoxReady()) throw new Error(' PAID_SECRET is not set: cannot store Patreon secrets');52 if (!cryptoBoxReady()) throw new Error('encryption key unavailable: cannot store Patreon secrets'); 53 53 const cur = getOwnerConfig(siteId) || {}; 54 54 const merged = { -
src/views/pages/admin-paid.ejs
rb9beb16 r4590e66 10 10 <% if (error) { %><p style="color:#c0392b"><%= error %></p><% } %> 11 11 <% if (!secretReady) { %> 12 <p style="color:#c0392b">Let op: <code>PAID_SECRET</code> staat niet in de serverconfig. Zonder die sleutel kunnen secrets niet versleuteldworden opgeslagen.</p>12 <p style="color:#c0392b">Let op: de encryptiesleutel kon niet worden aangemaakt of gelezen (schrijfrechten op de opslagmap?). Zonder sleutel kunnen secrets niet veilig worden opgeslagen.</p> 13 13 <% } %> 14 14
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)