Repository navigation
feat(campaigns,tax): generated data keys in the keychain; tax documents encrypted at rest (campaigns 1.0.3, tax 2.1.0) - #513
Merged
kh0pper merged 3 commits intoOct 17, 2026
Conversation
…ts encrypted at rest (campaigns 1.0.3, tax 2.1.0) Recovery keys, option 1 (queue item 30, decided 2026-10-16). Stacked on the keychain restore + Export reminder change. - CROW_CAMPAIGNS_ENCRYPTION_KEY and CROW_TAX_ENCRYPTION_KEY are generated at install and saved to the keychain (a reinstall takes them back from it). They reach the MCP child through its mcp-addons entry; both dashboard panels read them in the child's own order (the entry first, then the gateway env), so panel and server always use one key. - An install still on the tax example key once published in the repo gets a new key and the old one moves to CROW_TAX_ENCRYPTION_KEY_PREVIOUS (generic manifest field replace_values_sha256, validated by the bundle contract), so its returns still open and move to the new key. - Tax uploads are stored only encrypted (<CROW_HOME>/tax-documents, AES-256-GCM with a key id in the header); ingestion reads the upload from memory. Documents stored before are encrypted at dashboard start, one row at a time and verified before the plain file is removed: unique per-run names, compare-and-swap on the row, a lock file against two migrators, crash leftovers cleaned once verified, and only regular files inside the documents folder are ever touched. scripts/documents.mjs reverses it (in place, or as plain copies to a folder). The MCP ingest tool opens a stored .enc. - A version bump now ships a non-docker bundle's engine/ and pdf/ (tax's panel imports the installed engine). - The tax manifest says exactly what is encrypted (returns, documents) and what is not (the figures read from documents). Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
…figure never overwrites the generated tax key Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
…ecks the path before reading; restored rows find their .enc; safer lock - The document key id is now an HMAC under the salted PBKDF2 output (per file), not under the passphrase: no cheap offline guess check, nothing linkable across files. - The MCP ingest tool checks the extension (.pdf/.enc), that the path is a regular file, and a 25 MB cap BEFORE reading (the AI chooses the path). - A crow.db restored from a backup taken before the migration (rows still naming the plain file) finds the single <name>.<8hex>.enc successor and repoints the row. - The documents lock: stale takeover by atomic rename (one winner), touched per row, folder tightened to 0700; the panel logs a held lock. --out folders are 0700. - Upload names get a random part (two same-millisecond uploads never share a file). - An install on the published example key: the upload refusal and the install log say to reinstall, and say why when the previous-key slot is already in use. - The CLI uses the database of the same CROW_HOME as its key and folders. Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
kh0pper
force-pushed
the
feat/campaigns-tax-generated-keys
branch
from
October 17, 2026 00:11
f432fb2 to
66bc1fc
Compare
Owner
Author
|
I ran a fresh code and security review and pushed the fixes in 66bc1fc (rebased on #512's d11b6b2).
Results:
|
kh0pper
merged commit Oct 17, 2026
d6966f0
into
feat/keychain-restore-and-export-reminder
4 checks passed
kh0pper
added a commit
that referenced
this pull request
Oct 17, 2026
…ts encrypted at rest (campaigns 1.0.3, tax 2.1.0) (#513) * feat(campaigns,tax): generated data keys in the keychain; tax documents encrypted at rest (campaigns 1.0.3, tax 2.1.0) Recovery keys, option 1 (queue item 30, decided 2026-10-16). Stacked on the keychain restore + Export reminder change. - CROW_CAMPAIGNS_ENCRYPTION_KEY and CROW_TAX_ENCRYPTION_KEY are generated at install and saved to the keychain (a reinstall takes them back from it). They reach the MCP child through its mcp-addons entry; both dashboard panels read them in the child's own order (the entry first, then the gateway env), so panel and server always use one key. - An install still on the tax example key once published in the repo gets a new key and the old one moves to CROW_TAX_ENCRYPTION_KEY_PREVIOUS (generic manifest field replace_values_sha256, validated by the bundle contract), so its returns still open and move to the new key. - Tax uploads are stored only encrypted (<CROW_HOME>/tax-documents, AES-256-GCM with a key id in the header); ingestion reads the upload from memory. Documents stored before are encrypted at dashboard start, one row at a time and verified before the plain file is removed: unique per-run names, compare-and-swap on the row, a lock file against two migrators, crash leftovers cleaned once verified, and only regular files inside the documents folder are ever touched. scripts/documents.mjs reverses it (in place, or as plain copies to a folder). The MCP ingest tool opens a stored .enc. - A version bump now ships a non-docker bundle's engine/ and pdf/ (tax's panel imports the installed engine). - The tax manifest says exactly what is encrypted (returns, documents) and what is not (the figures read from documents). Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp * test(tax): the previous_key Configure test runs on a typed field; Configure never overwrites the generated tax key Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp * fix(tax): code review — key id from the salted derived key; ingest checks the path before reading; restored rows find their .enc; safer lock - The document key id is now an HMAC under the salted PBKDF2 output (per file), not under the passphrase: no cheap offline guess check, nothing linkable across files. - The MCP ingest tool checks the extension (.pdf/.enc), that the path is a regular file, and a 25 MB cap BEFORE reading (the AI chooses the path). - A crow.db restored from a backup taken before the migration (rows still naming the plain file) finds the single <name>.<8hex>.enc successor and repoints the row. - The documents lock: stale takeover by atomic rename (one winner), touched per row, folder tightened to 0700; the panel logs a held lock. --out folders are 0700. - Upload names get a random part (two same-millisecond uploads never share a file). - An install on the published example key: the upload refusal and the install log say to reinstall, and say why when the previous-key slot is already in use. - The CLI uses the database of the same CROW_HOME as its key and folders. Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
kh0pper
added a commit
that referenced
this pull request
Oct 17, 2026
…, and generated campaigns/tax keys with encrypted tax PDFs (#512, includes #513) * feat(keychain): a reinstall restores generated keys from the keychain; a reminder to export; federated data keys saved too Recovery keys, option 1 (queue item 30, decided 2026-10-16): the keychain plus its passphrase-encrypted Export is how a generated key survives a lost machine. - A reinstall that finds no .env and no retained copy now takes a keychain:true generated key from a readable keychain entry (restored crow.db + imported Export) instead of minting a new one, which also overwrote the imported entry and stranded data saved with the old key. A store_as key gets a fresh hash of the restored plaintext. Best-effort: no key file or an unreadable entry mints, as before, and says so by key name. Values are never logged; crow_keychain stays local-only. - Export reminder: a Nest info issue while a key Crow generated is in no keychain Export (created after the last export, a local setting); the install log says so too. - Mastodon (secret_key_base, OTP secret, three Active Record encryption keys), Pixelfed (APP_KEY) and PeerTube (server secret) save their generated keys to the keychain. mastodon/pixelfed/peertube 1.0.4. Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp * fix(keychain): review — never mint over an unreadable entry; boot backfill of kept keys; reminder counts what no Export holds; mcp-addons.json at 600 Plan review findings: - An install that would mint a keychain:true key whose entry exists but is sealed with a keychain key this machine lacks (crow.db restored, Export not imported) is refused: the new value would replace that entry and a later Import would skip it, losing the old key for good. Deleting the old entry is the explicit way to start fresh. - Boot backfill: an extension installed before its manifest saved a generated key to the keychain gets that key (from .env, retained copy or MCP entry) saved once, so it is visible in Passwords and goes into the Export (the boot refresh never runs the install job). Never over an existing entry; never a store_as hash. - The Export reminder fingerprints the sealed values the last Export held (and those an Import brought in), so a re-minted or kept key counts and an imported one does not; the health cache is dropped on export and import. - mcp-addons.json (MCP children's generated keys) is written atomically at mode 600. - The delete warning for a generated entry also covers data keys. Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp * fix(keychain): code review — fail closed on a keychain read error; a deleted entry stays deleted; the reminder snapshot covers removed extensions - keychainRestoreValues fails CLOSED: a read error refuses any keychain:true mint (it could replace an entry the read missed), and one damaged entry counts as unreadable instead of clearing the result. - The refusal keeps a prior install's folder (its .env holds other secrets) and says that deleting needs a dashboard password or two-factor sign-in. - Boot backfill runs once per bundle+key (local marker): a key the user deletes in Passwords is never brought back by the next boot. - Export fingerprints are taken before the Export is built and cover every status, so a reactivated removed extension is not nagged about and a key saved mid-export is. - The install-log reminder follows the same rule as the Nest (anything unexported). - Mastodon/Pixelfed/PeerTube database passwords go to the keychain too: a database restored from a backup still opens after a reinstall. - Tests: fail-closed, damaged entry, deleted-stays-deleted, removed extension, and the real export/import HTTP routes. Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp * feat(campaigns,tax): generated data keys in the keychain; tax documents encrypted at rest (campaigns 1.0.3, tax 2.1.0) (#513) * feat(campaigns,tax): generated data keys in the keychain; tax documents encrypted at rest (campaigns 1.0.3, tax 2.1.0) Recovery keys, option 1 (queue item 30, decided 2026-10-16). Stacked on the keychain restore + Export reminder change. - CROW_CAMPAIGNS_ENCRYPTION_KEY and CROW_TAX_ENCRYPTION_KEY are generated at install and saved to the keychain (a reinstall takes them back from it). They reach the MCP child through its mcp-addons entry; both dashboard panels read them in the child's own order (the entry first, then the gateway env), so panel and server always use one key. - An install still on the tax example key once published in the repo gets a new key and the old one moves to CROW_TAX_ENCRYPTION_KEY_PREVIOUS (generic manifest field replace_values_sha256, validated by the bundle contract), so its returns still open and move to the new key. - Tax uploads are stored only encrypted (<CROW_HOME>/tax-documents, AES-256-GCM with a key id in the header); ingestion reads the upload from memory. Documents stored before are encrypted at dashboard start, one row at a time and verified before the plain file is removed: unique per-run names, compare-and-swap on the row, a lock file against two migrators, crash leftovers cleaned once verified, and only regular files inside the documents folder are ever touched. scripts/documents.mjs reverses it (in place, or as plain copies to a folder). The MCP ingest tool opens a stored .enc. - A version bump now ships a non-docker bundle's engine/ and pdf/ (tax's panel imports the installed engine). - The tax manifest says exactly what is encrypted (returns, documents) and what is not (the figures read from documents). Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp * test(tax): the previous_key Configure test runs on a typed field; Configure never overwrites the generated tax key Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp * fix(tax): code review — key id from the salted derived key; ingest checks the path before reading; restored rows find their .enc; safer lock - The document key id is now an HMAC under the salted PBKDF2 output (per file), not under the passphrase: no cheap offline guess check, nothing linkable across files. - The MCP ingest tool checks the extension (.pdf/.enc), that the path is a regular file, and a 25 MB cap BEFORE reading (the AI chooses the path). - A crow.db restored from a backup taken before the migration (rows still naming the plain file) finds the single <name>.<8hex>.enc successor and repoints the row. - The documents lock: stale takeover by atomic rename (one winner), touched per row, folder tightened to 0700; the panel logs a held lock. --out folders are 0700. - Upload names get a random part (two same-millisecond uploads never share a file). - An install on the published example key: the upload refusal and the install log say to reinstall, and say why when the previous-key slot is already in use. - The CLI uses the database of the same CROW_HOME as its key and folders. Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #512. Its base is
feat/keychain-restore-and-export-reminderwhile #512 is open; retarget it tomainonce #512 merges. This is PR 2 of 2 for the recovery-key decision (queue item 30, Option 1 plus all four extras; no escrow to paired instances).Changes
CROW_CAMPAIGNS_ENCRYPTION_KEYandCROW_TAX_ENCRYPTION_KEYare nowgenerate: "secret"andkeychain: true. A reinstall takes them back from the keychain (feat(keychain): a reinstall restores generated keys from the keychain; Export reminder; federated data keys saved too #512). They reach the MCP child only through itsmcp-addons.jsonentry, never the shared gateway.env.proxy.js. The panel and the server can never end up on different keys. New helper:bundles/campaigns/server/key.js.CROW_TAX_ENCRYPTION_KEY_PREVIOUS, so returns saved with it still open and move to the new key.env_vars[].replace_values_sha256, which the bundle contract validates: lowercase hex digests, and it needs bothgenerateandprevious_key.previous_keyslot that is already in use is never overwritten.<CROW_HOME>/tax-documentsas<name>.enconly.documents-crypto.js):file_pathis updated with compare-and-swap. The plain file is removed only after that.bundles/tax/scripts/documents.mjs:decrypt --in-placeis the exact inverse of the migration.decrypt --out <dir>writes plain copies and leaves the DB untouched.CROW_TAX_KEEP_PLAIN_DOCUMENTS=1stops the start-up pass from encrypting again after a downgrade..encfile.engine/andpdf/. Tax's panel imports the installed engine, and until nowengine/was never refreshed.bundle-form-friction: the campaigns/tax exemptions are gone. An MCP-server bundle's generated key must be inserver.envKeys.previous_keyConfigure test now runs on a typed field, plus a new test that Configure never overwrites the generated key.Tests
tests/tax-documents-crypto.test.js(12 tests):tests/campaigns-tax-generated-keys.test.js(10 tests):.encfile;bundles/tax/testsgive the same result before and after (52 pass, 1 failing integration flow, which predates this branch).Live proof (scratch home only)
Setup:
HOMEincluded, was in a session scratch directory, checked before running.npmensured nothing was downloaded.systemd-run --user -p RuntimeMaxSec=1800, with loopback-only HTTP.Home A:
origin/main) was installed on the example key, with a plain W-2 in the legacy folder.engine/._PREVIOUS. No key value appeared in the job log..enc, with no plaintext in either documents folder.VACUUM INTOcopy of crow.db plus the.encfiles. Home A was then deleted.Home B:
Licensing
No new third-party component.
https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp