Changeset 4590e66 in Klonkt for src/services/CryptoBox.js
- 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)
- File:
-
- 1 edited
-
src/services/CryptoBox.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
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 }
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)