The Power of Encryption (and Decryption): Safeguarding Data Privacy with Payload Hooks

Published On
Show and hide date of birth text field with a relevant code snippet
Show and hide date of birth text field with a relevant code snippet
Imagine your web app allows users to log in and save personal and sensitive details, such as contact information or transaction records. Should your database ever get compromised, unencrypted data is an open book for malicious parties - this isn’t a risk worth taking. The solution? Encryption.

In a rapidly evolving digital age, even the most basic data can be dangerous in the wrong hands. Here are some types of data you should consider encrypting:

  • Any Personal Identifiable Information (PII)
  • Passwords
  • Online Transactions
  • Location Data
  • Financial Data
  • Healthcare Records
  • Corporate Proprietary Information
  • Legal Documents
  • Government and Military Data
  • And more

Effective encryption with Payload

Payload provides an effective and flexible approach to encryption, through the use of collection / field level hooks. In this example, we’ll utilize a Users collection and a userDOB (date of birth) field that we will be encrypting / decrypting.

1
import { CollectionConfig } from 'payload/types';
2
import { decryptField, encryptField } from '../../field-hooks/encryption';
3
4
export const Users: CollectionConfig = {
5
slug: 'users',
6
auth: true,
7
admin: {
8
useAsTitle: 'email',
9
},
10
fields: [
11
{
12
name: 'userDOB',
13
type: 'textarea',
14
hooks: {
15
beforeChange: [encryptField],
16
afterRead: [decryptField],
17
},
18
},
19
],
20
};

This encryption and decryption process utilizes two key Payload hooks: BeforeChange and AfterRead.

  1. BeforeChange: This is your database’s guardian. Every piece of data, before finding its resting place in the database, passes through this hook. We are using this hook to pass the userDOB data to an encryptField function (we’ll discuss this more later!).
  2. AfterRead: This hook comes into play when data needs to be retrieved. Just before Payload sends back the information via its API, we are using the AfterRead hook to decrypt the data coming from the database, which ensures that your application gets readable data.

Essentially, these hooks and encrypt / decrypt functions act as your data’s protective shield.

Diving into the Code

The encryption and decryption logic might seem complex, but it’s more systematic than you think. Let’s break down what’s happening in both hooks:

Encrypt Field Hook:
  • The encryptField hook ensures any text being saved is disguised (encrypted)
1
import type { FieldHook } from 'payload/types'
2
3
import { encrypt } from '../utilities/crypto'
4
5
export const encryptField: FieldHook = ({ value }) => {
6
if (typeof value === 'string') {
7
return encrypt(value as string)
8
}
9
10
return undefined
11
}

At its core, this utility relies on Node’s crypto module. The sequence involves:

  • PAYLOAD_SECRET: This environmental variable acts as your secret key. It's paramount that you ensure its value is both secure and strong, as it's integral to the encryption and decryption process.
  • A unique initialization Vector (IV) is generated for each encryption, enhancing security.
  • Using AES-256-CTR encryption algorithm, the data gets encrypted with the created key and IV.
1
import crypto from 'crypto'
2
import dotenv from 'dotenv'
3
import path from 'path'
4
5
dotenv.config({
6
path: path.resolve(__dirname, '../../../.env'),
7
})
8
9
const createKeyFromSecret = (secretKey: string): string =>
10
crypto.createHash('sha256').update(secretKey).digest('hex').slice(0, 32)
11
12
const algorithm = 'aes-256-ctr'
13
14
export const encrypt = (text: string): string => {
15
const iv = crypto.randomBytes(16)
16
const cipher = crypto.createCipheriv(
17
algorithm,
18
createKeyFromSecret(process.env.PAYLOAD_SECRET),
19
iv,
20
)
21
22
const encrypted = Buffer.concat([cipher.update(text), cipher.final()])
23
24
const ivString = iv.toString('hex')
25
const encryptedString = encrypted.toString('hex')
26
27
const result = `${ivString}${encryptedString}`
28
return result
29
}
Decrypt Field Hook:
  • The decryptField hook makes sure the disguised text is made understandable (decrypted) when it’s needed again.
  • On decryption, the previous process reverses, turning the encrypted hash back into its original form.
1
import type { FieldHook } from 'payload/types'
2
3
import { decrypt } from '../utilities/crypto'
4
5
export const decryptField: FieldHook = ({ value }) => {
6
try {
7
const decrypted = typeof value === 'string' ? decrypt(value as string) : value
8
return decrypted
9
} catch (e: unknown) {
10
return undefined
11
}
12
}
1
import crypto from 'crypto'
2
import dotenv from 'dotenv'
3
import path from 'path'
4
5
dotenv.config({
6
path: path.resolve(__dirname, '../../../.env'),
7
})
8
9
const createKeyFromSecret = (secretKey: string): string =>
10
crypto.createHash('sha256').update(secretKey).digest('hex').slice(0, 32)
11
12
const algorithm = 'aes-256-ctr'
13
14
export const decrypt = (hash: string): string => {
15
const iv = hash.slice(0, 32)
16
const content = hash.slice(32)
17
18
const decipher = crypto.createDecipheriv(
19
algorithm,
20
createKeyFromSecret(process.env.PAYLOAD_SECRET),
21
Buffer.from(iv, 'hex'),
22
)
23
24
const decrypted = Buffer.concat([decipher.update(Buffer.from(content, 'hex')), decipher.final()])
25
26
const result = decrypted.toString()
27
return result
28
}

Recap

Integrating encryption and decryption in Payload is straightforward, thanks to its robust hooks mechanism. By leveraging the power of modern cryptographic techniques and Payload’s collections, developers can rest easy, knowing their user data remains safe and secure. Remember, in today’s digital world, taking precautions isn’t an option - it’s a necessity.

Learn More

To learn more about Payload and the features used here, take a look at the following resources: