Skip to content

Encryption of sensitive data#194

Draft
junaidferoz wants to merge 1 commit into
devfrom
feat-156-pgcrypto_encryption
Draft

Encryption of sensitive data#194
junaidferoz wants to merge 1 commit into
devfrom
feat-156-pgcrypto_encryption

Conversation

@junaidferoz
Copy link
Copy Markdown
Collaborator

@junaidferoz junaidferoz commented May 5, 2026

Main Description

This PR introduces phase-1 database encryption for sensitive user fields using PostgreSQL pgcrypto, while keeping backend changes minimal and preserving existing Sequelize usage patterns.

Implemented in this scope:

  • Added DB encryption/decryption helper functions.
  • Added DB-only encryption flow for selected user fields:
    • firstName
    • lastName
    • email
    • initialPassword
  • Added runtime DB session key setup through DB_ENCRYPTION_KEY in Sequelize connection initialization.
  • Kept application-facing contract stable to avoid broad refactors.

New User Features

  • Sensitive user profile values are now encrypted at rest in the database for the fields listed above.

New Dev Features

  • Added migration-based pgcrypto setup.
  • Added reusable DB helper functions for encryption/decryption.
  • Added connection-level key injection for DB crypto operations.

Bug Fixes

  • N/A (security/feature enhancement).

Known Limitations

  • DB_ENCRYPTION_KEY must be configured in each environment.
  • Current implementation is intentionally limited to the basic user-table scope only.

Future Steps

  • Validate migration behavior on staging and production-like data.
  • Extend the same DB-only encryption approach to additional sensitive fields/tables in follow-up PRs.
  • Add key rotation and operational runbook in a dedicated follow-up.

Copilot AI review requested due to automatic review settings May 5, 2026 10:22
@junaidferoz junaidferoz linked an issue May 5, 2026 that may be closed by this pull request
@junaidferoz junaidferoz self-assigned this May 5, 2026
@junaidferoz junaidferoz added could have marks nice-to-have issues doing marked as someone is working on this labels May 5, 2026
@junaidferoz junaidferoz requested a review from dennis-zyska May 5, 2026 10:29
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a database-layer encryption approach for selected user fields in CARE by moving the physical table behind a compatibility view/trigger layer and setting a per-connection PostgreSQL encryption key. The goal is to keep the existing application-facing user contract unchanged while encrypting data at rest.

Changes:

  • Add a migration that renames user to user_secure, encrypts selected columns, creates helper SQL functions, and recreates user as a read/write view.
  • Route inserts/updates/deletes on the user view through an INSTEAD OF trigger.
  • Set app.encryption_key on Sequelize connections from DB_ENCRYPTION_KEY.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.

File Description
backend/db/migrations/20260505121500-transform-user-encryption-db-only.js Implements the encrypted backing table, compatibility view, SQL helpers, and trigger-based write path for user records.
backend/db/index.js Adds connection initialization logic to set the PostgreSQL session encryption key for application queries.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +173 to +178
NEW.deleted,
NEW."createdAt",
NEW."updatedAt",
NEW."deletedAt",
NEW."acceptedAt",
NEW."acceptDataSharing",
Comment on lines +144 to +149
IF EXISTS (
SELECT 1
FROM public.user_secure u
WHERE public.decrypt_text(u.email) = NEW.email
) THEN
RAISE unique_violation USING MESSAGE = 'duplicate key value violates unique constraint "user_email_key"';
Comment on lines +202 to +208
IF EXISTS (
SELECT 1
FROM public.user_secure u
WHERE u.id <> OLD.id
AND public.decrypt_text(u.email) = NEW.email
) THEN
RAISE unique_violation USING MESSAGE = 'duplicate key value violates unique constraint "user_email_key"';
Comment on lines +88 to +89
ALTER COLUMN email TYPE bytea USING public.encrypt_text(email),
ALTER COLUMN "initialPassword" TYPE bytea USING public.encrypt_text("initialPassword");
Comment on lines +117 to +120
"emailVerified",
"emailVerificationToken",
"resetToken",
"lastPasswordResetEmailSent",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you ask dennis do we need to encrypt them as well and also for 2fa otp and totpsectret.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay. @dennis-zyska -> Do we need to encrypt these columns as well, as well as the 2fa and totp ones also?

Comment on lines +122 to +125
"twoFactorOtp",
"twoFactorOtpExpiresAt",
"twoFactorMethods",
"totpSecret",
Comment thread backend/db/index.js
}
const encryptionKey = process.env.DB_ENCRYPTION_KEY;
if (!encryptionKey) {
return;
Comment on lines +79 to +129
`ALTER TABLE public."user" RENAME TO user_secure;`,
{ transaction }
);

await queryInterface.sequelize.query(
`
ALTER TABLE public.user_secure
ALTER COLUMN "firstName" TYPE bytea USING public.encrypt_text("firstName"),
ALTER COLUMN "lastName" TYPE bytea USING public.encrypt_text("lastName"),
ALTER COLUMN email TYPE bytea USING public.encrypt_text(email),
ALTER COLUMN "initialPassword" TYPE bytea USING public.encrypt_text("initialPassword");
`,
{ transaction }
);

await queryInterface.sequelize.query(
`
CREATE OR REPLACE VIEW public."user" AS
SELECT
id,
public.decrypt_text("firstName") AS "firstName",
public.decrypt_text("lastName") AS "lastName",
"userName",
public.decrypt_text(email) AS email,
"passwordHash",
"acceptTerms",
"acceptStats",
salt,
"lastLoginAt",
deleted,
"createdAt",
"updatedAt",
"deletedAt",
"acceptedAt",
"acceptDataSharing",
"rolesUpdatedAt",
"extId",
public.decrypt_text("initialPassword") AS "initialPassword",
"emailVerified",
"emailVerificationToken",
"resetToken",
"lastPasswordResetEmailSent",
"lastVerificationEmailSent",
"twoFactorOtp",
"twoFactorOtpExpiresAt",
"twoFactorMethods",
"totpSecret",
"orcidId",
"ldapUsername",
"samlNameId"
FROM public.user_secure;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it is a good recommendation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, thank you for the idea

Comment on lines +144 to +148
IF EXISTS (
SELECT 1
FROM public.user_secure u
WHERE public.decrypt_text(u.email) = NEW.email
) THEN
@junaidferoz junaidferoz marked this pull request as draft May 7, 2026 07:50
Comment thread backend/db/index.js
@@ -1,3 +1,3 @@
/**
* Declare all necessary dependencies to work with the database models
*
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add your name in the author

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

Comment thread backend/db/index.js
}
const encryptionKey = process.env.DB_ENCRYPTION_KEY;
if (!encryptionKey) {
return;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add error msg the copilot suggestion.

Comment on lines +79 to +129
`ALTER TABLE public."user" RENAME TO user_secure;`,
{ transaction }
);

await queryInterface.sequelize.query(
`
ALTER TABLE public.user_secure
ALTER COLUMN "firstName" TYPE bytea USING public.encrypt_text("firstName"),
ALTER COLUMN "lastName" TYPE bytea USING public.encrypt_text("lastName"),
ALTER COLUMN email TYPE bytea USING public.encrypt_text(email),
ALTER COLUMN "initialPassword" TYPE bytea USING public.encrypt_text("initialPassword");
`,
{ transaction }
);

await queryInterface.sequelize.query(
`
CREATE OR REPLACE VIEW public."user" AS
SELECT
id,
public.decrypt_text("firstName") AS "firstName",
public.decrypt_text("lastName") AS "lastName",
"userName",
public.decrypt_text(email) AS email,
"passwordHash",
"acceptTerms",
"acceptStats",
salt,
"lastLoginAt",
deleted,
"createdAt",
"updatedAt",
"deletedAt",
"acceptedAt",
"acceptDataSharing",
"rolesUpdatedAt",
"extId",
public.decrypt_text("initialPassword") AS "initialPassword",
"emailVerified",
"emailVerificationToken",
"resetToken",
"lastPasswordResetEmailSent",
"lastVerificationEmailSent",
"twoFactorOtp",
"twoFactorOtpExpiresAt",
"twoFactorMethods",
"totpSecret",
"orcidId",
"ldapUsername",
"samlNameId"
FROM public.user_secure;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it is a good recommendation.

Comment on lines +117 to +120
"emailVerified",
"emailVerificationToken",
"resetToken",
"lastPasswordResetEmailSent",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you ask dennis do we need to encrypt them as well and also for 2fa otp and totpsectret.

Add DB-only migration to encrypt user table fields with pgcrypto and wire encryption helpers in db/index.js.
@junaidferoz junaidferoz force-pushed the feat-156-pgcrypto_encryption branch from 44d84ec to c49c087 Compare May 18, 2026 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

could have marks nice-to-have issues doing marked as someone is working on this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Encrypting sensitive data in the database

3 participants