Skip to content

Add the rekey command; fix clone encryption of nested fields and custom collection names - #112

Merged
travist merged 4 commits into
improve-clone-performance-and-bug-fixesfrom
feat/rekey-command
Oct 28, 2026
Merged

travist merged 4 commits into
improve-clone-performance-and-bug-fixesfrom
feat/rekey-command

Conversation

@travist

@travist travist commented Oct 22, 2026

Copy link
Copy Markdown
Member

Stacked on #111. Retarget to master once #111 merges.

Why

A customer database was cloned with the wrong --dst-db-secret, leaving project settings (and possibly encrypted fields) under a key the destination server does not use. There was no safe way to re-encrypt a database in place. Looking into it turned up two clone bugs.

Changes

formio rekey (new command)

formio rekey <mongo_url> --new-db-secret NEW \
  --old-db-secret OLD1 [--old-db-secret OLD2] [--dry-run] [--project <ids>]
  • Re-encrypts projects.settings_encrypted with the new DB_SECRET. Encrypted field values are re-encrypted with the project's own settings.secret when it has one, otherwise with the new DB_SECRET, which is how the server picks the key.
  • Each value is tried with its target secret first and skipped if it already opens, so the command is safe to re-run. Otherwise each --old-db-secret is tried in turn, and the re-encrypted value is read back before writing.
  • Covers fields inside datagrid/editgrid rows and containers, custom submission collections, and submission revisions (unless --no-submission-revisions). Deleted documents are included.
  • Each write only applies if the stored ciphertext is unchanged since it was read, so a value edited mid-run is never overwritten.
  • Values no secret can open, and projects whose settings cannot be decrypted, are reported and left untouched, and the command exits non-zero.

The server's built-in DB_SECRET_OLD rotation (src/util/updateSecret.js in formio-server) isn't a substitute. It reads project.settings.secret from the raw document, where it is always undefined. It also misses nested values, skips falsy values and revisions, and fires its field updates without awaiting them.

Clone fixes

  • Encrypted fields inside datagrid/editgrid rows were not re-encrypted. Paths like data.grid.field were looked up against array data, found nothing, and those values silently kept the source secret. The form is now walked against the submission data itself.
  • Custom submission collections were misnamed. The server strips everything but letters and digits from the project and collection names (myproject_records). The clone used the raw names (my-project_records), so for projects with symbols in their name, submissions landed where the server never reads them. The clone now uses the server's naming and warns when the destination still holds submissions in a misnamed collection.
  • Revisions of custom collections were never cloned. They were read from and written to the default submissionrevisions instead of <collection>_revisions.

Refactoring

  • The crypto helpers moved to src/encryption.js and the connection helpers to src/db.js. decrypt() no longer logs every failed attempt, which rekey would trigger once per wrong key.
  • Removed src/eachComponentAsync.js, which is now unused.

Testing

  • test/cloner/rekey.js (19 tests): value walking, dry run, full re-key including nested, revision and custom-collection values, re-run idempotency, project-secret targeting, --project filtering, undecryptable settings and values, --no-submission-revisions, and the concurrent-change guard.
  • New clone tests: a datagrid with encrypted fields, server-style collection naming, custom-collection revisions, and the misnamed-collection warning. The datagrid and naming tests fail against the previous code.
  • CLI parsing tests for rekey.
  • mocha test/cloner/index.js: 269 passing. yarn test:cli: 16 passing. Each commit passes on its own.
  • Also run by hand against a 2,500-submission database: dry run, real run, then a re-run with nothing left to do.

🤖 Generated with Claude Code

travist and others added 2 commits October 24, 2026 14:29
- Re-encrypt encrypted fields within the rows of a datagrid or editgrid.
  The encrypted paths were looked up as flat paths (data.grid.field)
  against array data, so those values silently kept the source secret.
  The form is now walked against the submission data itself.
- Name custom submission collections the way the server does, with
  everything but letters and digits stripped from the project and
  collection names. Projects with a dash in their name had their
  submissions cloned where the server never reads them.
- Clone the revisions of a custom collection from and to its
  <collection>_revisions collection instead of submissionrevisions.
- Warn when the destination still holds submissions in a collection an
  earlier clone misnamed.
- Move the crypto helpers to src/encryption.js and the connection
  helpers to src/db.js. decrypt() no longer logs every failed attempt.
- Remove src/eachComponentAsync.js, which is no longer used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-encrypt the project settings and encrypted submission data of a
database with a new DB_SECRET, for example after a clone was run with
the wrong --dst-db-secret.

    formio rekey <mongo_url> --new-db-secret NEW \
      --old-db-secret OLD1 [--old-db-secret OLD2] [--dry-run]

- Settings are re-encrypted with the new DB_SECRET. Field values are
  re-encrypted with the project's own secret when it has one, otherwise
  with the new DB_SECRET, the same way the server picks the key.
- Each value is first tried with its target secret and left alone when
  it already opens, so the command can be re-run until nothing is left.
  Otherwise every old secret is tried, and the re-encrypted value is
  read back before it is written.
- Covers fields within datagrid and editgrid rows and containers,
  custom submission collections, and submission revisions (unless
  --no-submission-revisions).
- Writes only apply when the stored ciphertext is unchanged since it
  was read, so a value edited mid-run is never overwritten.
- Values no secret can open, and projects whose settings cannot be
  decrypted, are reported and left as they are, and the command exits
  with an error.

The server's own DB_SECRET_OLD rotation is not a substitute: it misses
nested values, skips falsy values and revisions, and runs its field
updates without awaiting them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
travist and others added 2 commits October 28, 2026 09:30
…y-command

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- rekey kept the projects and forms cursors open while it re-keyed the
  submissions of each form. On a large database the server closes an idle
  cursor, and the rekey stopped part way. It now lists the ids first and
  reads each project and form on its own.
- rekey never re-keyed the revisions of a custom collection that earlier
  clones wrote to the default submissionrevisions collection. It now
  re-keys them where they are and says so.
- Encrypted values an API returns as JSON serialized Buffers were skipped
  with no message when cloning from an API source. They are now read and
  re-encrypted like any other encrypted value.
- An absolute --ca or --cert path was prefixed with the working directory.
- idList (which now ignores repeated ids) and the legacy collection name
  live in src/db.js, shared by Cloner and Rekeyer.
- Removed the encrypt/decrypt/legacyKey wrappers and defaultSaltLength from
  Cloner, which call src/encryption.js directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@travist
travist merged commit 5c1d92e into improve-clone-performance-and-bug-fixes Oct 28, 2026
1 check failed
travist added a commit that referenced this pull request Oct 28, 2026
…-fixes

Bring the rekey command (#112) into master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant