// Invite codes were shown once and never again: only a SHA-256 hash and the // last four characters were kept. That is the right shape for a password and // the wrong one for an invitation, which has to be given to somebody — usually // not at the moment it is created, and often more than once. // // The code is now also stored encrypted with DATA_ENCRYPTION_KEY, the same // AES-256-GCM treatment as Nextcloud tokens and image prompts. The hash stays: // it is what a claim looks up, it is indexed, and a claim must not depend on // decrypting every row. // // What this costs, stated plainly: a code is recoverable by anyone who holds // both the database and the encryption key, where before it was recoverable by // nobody. An invitation is a low-value secret — it grants registration, not // access — and it expires. Existing rows keep working and stay unrecoverable; // they simply have no cipher to show. exports.up = async function (pgm) { pgm.sql('ALTER TABLE registration_invites ADD COLUMN IF NOT EXISTS code_cipher TEXT'); }; exports.down = async function (pgm) { pgm.sql('ALTER TABLE registration_invites DROP COLUMN IF EXISTS code_cipher'); };