Encryption At Rest: AES-256, AWS KMS & Database Security for ChatGPT Apps
When your ChatGPT application stores sensitive user data—patient records for healthcare apps, financial information for banking integrations, or personal conversation histories—encryption at rest is not optional. It's the critical security layer that protects stored data from unauthorized access, whether through database breaches, insider threats, or infrastructure compromises.
The challenge: Modern compliance frameworks mandate specific encryption standards. HIPAA requires AES-256 encryption for Protected Health Information (PHI). GDPR demands encryption of personal data with proper key management. SOC 2 requires documented encryption policies and annual audits. PCI DSS Level 1 mandates field-level encryption for cardholder data.
The solution: This comprehensive guide provides production-ready encryption at rest architectures for ChatGPT applications. You'll learn AES-256-GCM implementation patterns, AWS KMS envelope encryption, database-level encryption for Firestore and PostgreSQL, automated key rotation strategies, and compliance validation workflows.
Whether you're building a HIPAA-compliant healthcare app, a financial services chatbot, or a SaaS integration handling customer data, this article delivers battle-tested code examples and security patterns used by enterprise-grade ChatGPT applications.
Table of Contents
- Encryption At Rest Fundamentals
- AES-256-GCM: Modern Symmetric Encryption
- AWS KMS Integration: Envelope Encryption Pattern
- Database-Level Encryption Strategies
- Field-Level Encryption for Sensitive Data
- Automated Key Rotation
- Compliance Requirements: HIPAA, GDPR, PCI DSS
- Production Architecture Examples
Encryption At Rest Fundamentals {#fundamentals}
What is Encryption At Rest?
Encryption at rest protects data stored in persistent storage—databases, file systems, object storage, and backup media. Unlike encryption in transit (which protects data moving across networks), encryption at rest ensures that even if an attacker gains direct access to your storage systems, the data remains unreadable without the decryption keys.
For ChatGPT applications, encryption at rest applies to:
- User profile data (names, email addresses, preferences) in Firestore or PostgreSQL
- Conversation histories stored for context or analytics
- Uploaded documents in Firebase Storage or AWS S3
- API keys and credentials in secrets management systems
- Database backups for disaster recovery
- Application logs containing user activity
Real-world breach example: In 2023, a healthcare chatbot provider exposed 500,000 patient records because their MongoDB database lacked encryption at rest. The database was publicly accessible for 72 hours before detection. With encryption at rest, the exposed data would have been cryptographically useless to attackers.
The Three-Layer Encryption Model
Professional encryption architectures use a three-layer defense-in-depth model:
Layer 1: Disk-Level Encryption (OS/Infrastructure)
- LUKS (Linux Unified Key Setup) for Linux servers
- BitLocker for Windows servers
- Cloud provider managed encryption (AWS EBS encryption, GCP PD encryption)
- Pro: Transparent, no code changes required
- Con: Keys often managed by cloud provider (limited control)
Layer 2: Database-Level Encryption (Database Engine)
- Firestore encryption at rest (automatic, AES-256)
- PostgreSQL Transparent Data Encryption (TDE)
- MySQL encrypted tablespaces
- Pro: Automatic, protects entire database
- Con: Doesn't protect against authorized database users
Layer 3: Application-Level Encryption (Field-Level)
- Encrypt specific fields (SSNs, credit cards, PHI) before writing to database
- Full control over keys and algorithms
- Pro: Maximum security, works with any database
- Con: Requires code changes, impacts queries
Best practice for ChatGPT apps: Combine all three layers for defense-in-depth. Use application-level encryption for sensitive fields (Layer 3), database-level encryption for baseline protection (Layer 2), and disk-level encryption as final fallback (Layer 1).
Symmetric vs. Asymmetric Encryption for At-Rest Data
Symmetric Encryption (AES-256)
- Same key encrypts and decrypts data
- Performance: 10-100x faster than asymmetric
- Use case: Bulk data encryption (database records, files)
- Key distribution challenge: How to securely share the key?
Asymmetric Encryption (RSA-4096)
- Public key encrypts, private key decrypts
- Performance: Much slower than symmetric
- Use case: Encrypting symmetric keys (envelope encryption pattern)
- Key distribution: Public key can be freely shared
Industry standard for at-rest data: Use symmetric encryption (AES-256-GCM) for actual data, protected by asymmetric encryption (RSA-4096 or KMS) for key management. This is called envelope encryption.
AES-256-GCM: Modern Symmetric Encryption {#aes-256-gcm}
Why AES-256-GCM is the Gold Standard
AES-256-GCM (Galois/Counter Mode) combines encryption with authentication in a single cryptographic primitive:
- AES-256: Advanced Encryption Standard with 256-bit keys (2^256 keyspace = virtually unbreakable)
- GCM mode: Galois/Counter Mode provides both confidentiality (encryption) and integrity (authentication tag)
- AEAD: Authenticated Encryption with Associated Data—detects tampering
Key advantages over older modes:
- AES-CBC: Vulnerable to padding oracle attacks, no authentication
- AES-CTR: No authentication, allows bit-flipping attacks
- AES-GCM: Parallelizable, AEAD, resistant to known attacks
Performance characteristics:
- Hardware-accelerated (AES-NI instruction set on modern CPUs)
- Encryption speed: ~1-3 GB/s on typical cloud instances
- Latency overhead: ~0.1-0.5ms per operation for small payloads
Production-Ready AES-256-GCM Encryptor
Here's a complete TypeScript implementation with proper IV generation, authentication tag handling, and error recovery:
// src/services/encryption/aes-encryptor.ts
import * as crypto from 'crypto';
/**
* AES-256-GCM encryption service for ChatGPT app data
* Implements NIST SP 800-38D recommendations for GCM mode
*/
export interface EncryptedPayload {
ciphertext: string; // Base64-encoded encrypted data
iv: string; // Base64-encoded initialization vector (96 bits)
authTag: string; // Base64-encoded authentication tag (128 bits)
algorithm: string; // Algorithm identifier for version control
version: number; // Encryption version (supports key rotation)
}
export interface EncryptionMetrics {
encryptionTimeMs: number;
plaintextSize: number;
ciphertextSize: number;
overhead: number;
}
export class AESEncryptor {
private static readonly ALGORITHM = 'aes-256-gcm';
private static readonly IV_LENGTH = 12; // 96 bits (NIST recommendation for GCM)
private static readonly AUTH_TAG_LENGTH = 16; // 128 bits
private static readonly VERSION = 1;
private encryptionKey: Buffer;
private keyVersion: number;
/**
* Initialize encryptor with a 256-bit encryption key
* @param keyHex - 64 hex characters (32 bytes = 256 bits)
* @param keyVersion - Version number for key rotation tracking
*/
constructor(keyHex: string, keyVersion: number = 1) {
if (!keyHex || keyHex.length !== 64) {
throw new Error('AES-256 requires 64 hex characters (256 bits)');
}
this.encryptionKey = Buffer.from(keyHex, 'hex');
this.keyVersion = keyVersion;
}
/**
* Encrypt plaintext with AES-256-GCM
* Uses cryptographically secure random IV for each encryption
*
* @param plaintext - Data to encrypt (will be converted to UTF-8)
* @param associatedData - Optional authenticated metadata (not encrypted, but authenticated)
* @returns EncryptedPayload with ciphertext, IV, and authentication tag
*/
encrypt(plaintext: string, associatedData?: string): EncryptedPayload {
const startTime = Date.now();
try {
// Generate cryptographically secure random IV (must be unique per encryption)
const iv = crypto.randomBytes(AESEncryptor.IV_LENGTH);
// Create cipher instance
const cipher = crypto.createCipheriv(
AESEncryptor.ALGORITHM,
this.encryptionKey,
iv
);
// Add associated data for authentication (optional, not encrypted)
if (associatedData) {
cipher.setAAD(Buffer.from(associatedData, 'utf-8'));
}
// Encrypt plaintext
const plaintextBuffer = Buffer.from(plaintext, 'utf-8');
const encrypted = Buffer.concat([
cipher.update(plaintextBuffer),
cipher.final()
]);
// Extract authentication tag (verifies integrity)
const authTag = cipher.getAuthTag();
const encryptionTimeMs = Date.now() - startTime;
// Log metrics for monitoring (in production, send to observability platform)
this.logMetrics({
encryptionTimeMs,
plaintextSize: plaintextBuffer.length,
ciphertextSize: encrypted.length,
overhead: ((encrypted.length - plaintextBuffer.length) / plaintextBuffer.length) * 100
});
return {
ciphertext: encrypted.toString('base64'),
iv: iv.toString('base64'),
authTag: authTag.toString('base64'),
algorithm: AESEncryptor.ALGORITHM,
version: this.keyVersion
};
} catch (error) {
throw new Error(`AES encryption failed: ${error.message}`);
}
}
/**
* Decrypt AES-256-GCM ciphertext
* Verifies authentication tag before returning plaintext
*
* @param payload - Encrypted payload from encrypt()
* @param associatedData - Same associated data used during encryption (if any)
* @returns Decrypted plaintext
* @throws Error if authentication tag is invalid (tampering detected)
*/
decrypt(payload: EncryptedPayload, associatedData?: string): string {
try {
// Convert Base64 to buffers
const ciphertext = Buffer.from(payload.ciphertext, 'base64');
const iv = Buffer.from(payload.iv, 'base64');
const authTag = Buffer.from(payload.authTag, 'base64');
// Create decipher instance
const decipher = crypto.createDecipheriv(
payload.algorithm,
this.encryptionKey,
iv
);
// Set authentication tag (GCM verifies this before decryption)
decipher.setAuthTag(authTag);
// Add associated data (must match encryption)
if (associatedData) {
decipher.setAAD(Buffer.from(associatedData, 'utf-8'));
}
// Decrypt ciphertext
const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final() // Throws error if auth tag verification fails
]);
return decrypted.toString('utf-8');
} catch (error) {
// Authentication failure indicates tampering
if (error.message.includes('Unsupported state or unable to authenticate data')) {
throw new Error('Authentication failed: Data may have been tampered with');
}
throw new Error(`AES decryption failed: ${error.message}`);
}
}
/**
* Generate a cryptographically secure 256-bit encryption key
* @returns Hex-encoded key (64 characters)
*/
static generateKey(): string {
return crypto.randomBytes(32).toString('hex');
}
private logMetrics(metrics: EncryptionMetrics): void {
// In production, send to CloudWatch, Datadog, or similar
console.debug(`[AES Encryption] Time: ${metrics.encryptionTimeMs}ms, ` +
`Plaintext: ${metrics.plaintextSize} bytes, ` +
`Ciphertext: ${metrics.ciphertextSize} bytes, ` +
`Overhead: ${metrics.overhead.toFixed(2)}%`);
}
}
Usage example:
// Initialize encryptor with master key (store in environment variable)
const encryptor = new AESEncryptor(process.env.ENCRYPTION_KEY_V1, 1);
// Encrypt sensitive patient data
const patientData = JSON.stringify({
ssn: '123-45-6789',
diagnosis: 'Type 2 Diabetes',
prescriptions: ['Metformin 500mg']
});
const encrypted = encryptor.encrypt(patientData, 'patient-record');
// Store encrypted payload in Firestore
await firestore.collection('patients').doc(patientId).set({
encryptedData: encrypted,
createdAt: FieldValue.serverTimestamp()
});
// Later: Decrypt when authorized user accesses
const doc = await firestore.collection('patients').doc(patientId).get();
const decrypted = encryptor.decrypt(doc.data().encryptedData, 'patient-record');
const patientData = JSON.parse(decrypted);
Key Security Considerations
1. IV Uniqueness is Critical
Never reuse an IV with the same encryption key. GCM mode security breaks down if IV is repeated:
// WRONG: Reusing IV
const iv = Buffer.from('000000000000000000000000', 'hex'); // Static IV
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
// CORRECT: Random IV per encryption
const iv = crypto.randomBytes(12); // New random IV every time
2. Authentication Tag Verification
Always verify the authentication tag before trusting decrypted data:
try {
decipher.setAuthTag(authTag);
const plaintext = decipher.final(); // Throws if tag verification fails
} catch (error) {
// CRITICAL: Never use plaintext if tag verification fails
throw new Error('Data tampering detected');
}
3. Key Storage
Never hardcode encryption keys in source code:
// WRONG: Hardcoded key
const encryptor = new AESEncryptor('a'.repeat(64), 1);
// CORRECT: Environment variable or secrets manager
const encryptor = new AESEncryptor(process.env.ENCRYPTION_KEY_V1, 1);
AWS KMS Integration: Envelope Encryption Pattern {#aws-kms}
What is Envelope Encryption?
Envelope encryption is a security pattern where you encrypt data with a Data Encryption Key (DEK), then encrypt the DEK with a Key Encryption Key (KEK) stored in a Hardware Security Module (HSM) or cloud Key Management Service (KMS).
Workflow:
- Generate random DEK (AES-256 key) for each record
- Encrypt data with DEK using AES-256-GCM
- Encrypt DEK with KEK stored in AWS KMS
- Store encrypted data + encrypted DEK together
- To decrypt: Decrypt DEK with KMS, then decrypt data with DEK
Benefits:
- Performance: Only small DEKs go to KMS (not bulk data)
- Auditability: All key decryptions logged in CloudTrail
- Key rotation: Rotate KEK without re-encrypting all data
- Compliance: KEK protected by FIPS 140-2 Level 3 HSMs
Production-Ready AWS KMS Client
// src/services/encryption/kms-client.ts
import { KMSClient, EncryptCommand, DecryptCommand, GenerateDataKeyCommand } from '@aws-sdk/client-kms';
export interface KMSEncryptedKey {
encryptedKey: string; // Base64-encoded ciphertext blob from KMS
keyId: string; // KMS key ARN or alias
algorithm: string; // Encryption algorithm used by KMS
}
export interface EnvelopeEncryptionResult {
encryptedData: string; // Data encrypted with DEK
encryptedDataKey: string; // DEK encrypted with KMS KEK
iv: string; // Initialization vector
authTag: string; // Authentication tag
kmsKeyId: string; // KMS key used
}
/**
* AWS KMS integration for envelope encryption pattern
* Implements best practices from AWS Security Hub and NIST
*/
export class KMSEncryptionService {
private kmsClient: KMSClient;
private kmsKeyId: string;
/**
* Initialize KMS client
* @param region - AWS region (e.g., 'us-east-1')
* @param kmsKeyId - KMS key ARN or alias (e.g., 'alias/chatgpt-app-master-key')
*/
constructor(region: string, kmsKeyId: string) {
this.kmsClient = new KMSClient({ region });
this.kmsKeyId = kmsKeyId;
}
/**
* Envelope encryption: Encrypt data with DEK, encrypt DEK with KMS
*
* @param plaintext - Data to encrypt
* @param encryptionContext - Key-value pairs for additional authentication
* @returns Encrypted data + encrypted DEK
*/
async envelopeEncrypt(
plaintext: string,
encryptionContext?: Record<string, string>
): Promise<EnvelopeEncryptionResult> {
try {
// Step 1: Generate Data Encryption Key (DEK) via KMS
const generateKeyCommand = new GenerateDataKeyCommand({
KeyId: this.kmsKeyId,
KeySpec: 'AES_256', // Generate 256-bit AES key
EncryptionContext: encryptionContext // Additional authentication context
});
const keyResponse = await this.kmsClient.send(generateKeyCommand);
// keyResponse contains:
// - Plaintext: Unencrypted DEK (use immediately, then discard)
// - CiphertextBlob: Encrypted DEK (store with data)
const plaintextDEK = Buffer.from(keyResponse.Plaintext!);
const encryptedDEK = Buffer.from(keyResponse.CiphertextBlob!);
// Step 2: Encrypt data with plaintext DEK using AES-256-GCM
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', plaintextDEK, iv);
const encrypted = Buffer.concat([
cipher.update(Buffer.from(plaintext, 'utf-8')),
cipher.final()
]);
const authTag = cipher.getAuthTag();
// Step 3: Securely erase plaintext DEK from memory
plaintextDEK.fill(0);
return {
encryptedData: encrypted.toString('base64'),
encryptedDataKey: encryptedDEK.toString('base64'),
iv: iv.toString('base64'),
authTag: authTag.toString('base64'),
kmsKeyId: this.kmsKeyId
};
} catch (error) {
throw new Error(`KMS envelope encryption failed: ${error.message}`);
}
}
/**
* Envelope decryption: Decrypt DEK with KMS, decrypt data with DEK
*
* @param payload - Encrypted payload from envelopeEncrypt()
* @param encryptionContext - Same context used during encryption
* @returns Decrypted plaintext
*/
async envelopeDecrypt(
payload: EnvelopeEncryptionResult,
encryptionContext?: Record<string, string>
): Promise<string> {
try {
// Step 1: Decrypt DEK using KMS
const decryptCommand = new DecryptCommand({
CiphertextBlob: Buffer.from(payload.encryptedDataKey, 'base64'),
KeyId: payload.kmsKeyId,
EncryptionContext: encryptionContext
});
const keyResponse = await this.kmsClient.send(decryptCommand);
const plaintextDEK = Buffer.from(keyResponse.Plaintext!);
// Step 2: Decrypt data with plaintext DEK
const iv = Buffer.from(payload.iv, 'base64');
const authTag = Buffer.from(payload.authTag, 'base64');
const ciphertext = Buffer.from(payload.encryptedData, 'base64');
const decipher = crypto.createDecipheriv('aes-256-gcm', plaintextDEK, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final()
]);
// Step 3: Securely erase plaintext DEK from memory
plaintextDEK.fill(0);
return decrypted.toString('utf-8');
} catch (error) {
throw new Error(`KMS envelope decryption failed: ${error.message}`);
}
}
/**
* Encrypt small data directly with KMS (no envelope pattern)
* Use for encrypting small keys, tokens, or passwords
*
* @param plaintext - Data to encrypt (max 4KB)
* @param encryptionContext - Additional authentication context
* @returns Encrypted ciphertext blob
*/
async encryptWithKMS(
plaintext: string,
encryptionContext?: Record<string, string>
): Promise<KMSEncryptedKey> {
try {
const encryptCommand = new EncryptCommand({
KeyId: this.kmsKeyId,
Plaintext: Buffer.from(plaintext, 'utf-8'),
EncryptionContext: encryptionContext
});
const response = await this.kmsClient.send(encryptCommand);
return {
encryptedKey: Buffer.from(response.CiphertextBlob!).toString('base64'),
keyId: response.KeyId!,
algorithm: response.EncryptionAlgorithm || 'SYMMETRIC_DEFAULT'
};
} catch (error) {
throw new Error(`KMS encryption failed: ${error.message}`);
}
}
/**
* Decrypt data encrypted with encryptWithKMS()
*
* @param payload - Encrypted payload
* @param encryptionContext - Same context used during encryption
* @returns Decrypted plaintext
*/
async decryptWithKMS(
payload: KMSEncryptedKey,
encryptionContext?: Record<string, string>
): Promise<string> {
try {
const decryptCommand = new DecryptCommand({
CiphertextBlob: Buffer.from(payload.encryptedKey, 'base64'),
KeyId: payload.keyId,
EncryptionContext: encryptionContext
});
const response = await this.kmsClient.send(decryptCommand);
return Buffer.from(response.Plaintext!).toString('utf-8');
} catch (error) {
throw new Error(`KMS decryption failed: ${error.message}`);
}
}
}
Usage example:
// Initialize KMS service
const kmsService = new KMSEncryptionService(
'us-east-1',
'alias/makeaihq-master-key'
);
// Encrypt patient record with envelope encryption
const patientRecord = JSON.stringify({
ssn: '123-45-6789',
medicalHistory: [...]
});
const encrypted = await kmsService.envelopeEncrypt(
patientRecord,
{ patientId: 'patient-12345', department: 'oncology' } // Encryption context for audit
);
// Store in Firestore
await firestore.collection('patients').doc('patient-12345').set({
encryptedData: encrypted.encryptedData,
encryptedDataKey: encrypted.encryptedDataKey,
iv: encrypted.iv,
authTag: encrypted.authTag,
kmsKeyId: encrypted.kmsKeyId
});
// Later: Decrypt
const doc = await firestore.collection('patients').doc('patient-12345').get();
const decrypted = await kmsService.envelopeDecrypt(
doc.data() as EnvelopeEncryptionResult,
{ patientId: 'patient-12345', department: 'oncology' }
);
Encryption Context for Auditability
AWS KMS supports encryption context—key-value pairs that are cryptographically bound to encrypted data but NOT encrypted themselves. This provides:
- Additional authentication: Decryption fails if context doesn't match
- Audit logging: CloudTrail logs show encryption context for every operation
- Access control: IAM policies can restrict decryption based on context
// Example: Restrict decryption to specific user
const encryptionContext = {
userId: 'user-123',
appId: 'chatgpt-app-456',
department: 'healthcare'
};
// Encrypt with context
const encrypted = await kmsService.envelopeEncrypt(data, encryptionContext);
// Decryption ONLY succeeds if context matches exactly
const decrypted = await kmsService.envelopeDecrypt(encrypted, encryptionContext);
IAM policy example (restrict decryption to matching context):
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:us-east-1:123456789012:key/*",
"Condition": {
"StringEquals": {
"kms:EncryptionContext:department": "healthcare"
}
}
}]
}
Database-Level Encryption Strategies {#database-encryption}
Firestore Encryption At Rest
Good news: Firestore automatically encrypts all data at rest using AES-256. You don't need to configure anything.
Limitations:
- Encryption keys managed by Google (you don't control key rotation)
- All users with database access can read plaintext
- No field-level encryption (entire documents encrypted as single unit)
When Firestore's built-in encryption is sufficient:
- General user data (names, emails, preferences)
- Non-sensitive conversation histories
- Public app data
When you need application-level encryption:
- HIPAA-regulated PHI (patient medical records)
- PCI DSS cardholder data (credit card numbers)
- Financial account information
- Social Security Numbers
Field-Level Encryption for Firestore
// src/services/encryption/firestore-encryption.service.ts
import { Firestore, FieldValue } from 'firebase-admin/firestore';
import { AESEncryptor } from './aes-encryptor';
export interface EncryptedField {
__encrypted: true;
ciphertext: string;
iv: string;
authTag: string;
algorithm: string;
version: number;
}
/**
* Firestore field-level encryption service
* Transparently encrypts/decrypts specific fields
*/
export class FirestoreEncryptionService {
private encryptor: AESEncryptor;
private sensitiveFields: Set<string>;
constructor(encryptionKey: string, sensitiveFields: string[]) {
this.encryptor = new AESEncryptor(encryptionKey, 1);
this.sensitiveFields = new Set(sensitiveFields);
}
/**
* Encrypt sensitive fields in a document before writing to Firestore
*
* @param data - Document data
* @returns Document with encrypted sensitive fields
*/
encryptDocument(data: Record<string, any>): Record<string, any> {
const encrypted = { ...data };
for (const field of this.sensitiveFields) {
if (field in encrypted && encrypted[field] !== null && encrypted[field] !== undefined) {
const plaintext = typeof encrypted[field] === 'string'
? encrypted[field]
: JSON.stringify(encrypted[field]);
const encryptedPayload = this.encryptor.encrypt(plaintext);
// Replace plaintext with encrypted payload
encrypted[field] = {
__encrypted: true,
...encryptedPayload
} as EncryptedField;
}
}
return encrypted;
}
/**
* Decrypt sensitive fields in a document after reading from Firestore
*
* @param data - Document data with encrypted fields
* @returns Document with decrypted sensitive fields
*/
decryptDocument(data: Record<string, any>): Record<string, any> {
const decrypted = { ...data };
for (const field of this.sensitiveFields) {
if (field in decrypted && this.isEncryptedField(decrypted[field])) {
const encryptedField = decrypted[field] as EncryptedField;
const plaintext = this.encryptor.decrypt({
ciphertext: encryptedField.ciphertext,
iv: encryptedField.iv,
authTag: encryptedField.authTag,
algorithm: encryptedField.algorithm,
version: encryptedField.version
});
// Restore original value (string or JSON object)
try {
decrypted[field] = JSON.parse(plaintext);
} catch {
decrypted[field] = plaintext;
}
}
}
return decrypted;
}
private isEncryptedField(value: any): value is EncryptedField {
return value && typeof value === 'object' && value.__encrypted === true;
}
}
Usage example:
// Initialize with sensitive fields
const encryptionService = new FirestoreEncryptionService(
process.env.FIRESTORE_ENCRYPTION_KEY,
['ssn', 'creditCard', 'medicalHistory', 'prescriptions']
);
// Write encrypted data
const patientData = {
name: 'John Doe',
email: 'john@example.com',
ssn: '123-45-6789', // Will be encrypted
medicalHistory: ['Diabetes'], // Will be encrypted
lastVisit: FieldValue.serverTimestamp()
};
const encrypted = encryptionService.encryptDocument(patientData);
await firestore.collection('patients').doc('patient-123').set(encrypted);
// Read and decrypt
const doc = await firestore.collection('patients').doc('patient-123').get();
const decrypted = encryptionService.decryptDocument(doc.data()!);
console.log(decrypted.ssn); // '123-45-6789' (decrypted)
PostgreSQL Field-Level Encryption
For PostgreSQL-based ChatGPT apps (e.g., Supabase):
// src/services/encryption/postgres-encryption.service.ts
import { Pool } from 'pg';
import { AESEncryptor } from './aes-encryptor';
export class PostgresEncryptionService {
private pool: Pool;
private encryptor: AESEncryptor;
constructor(postgresPool: Pool, encryptionKey: string) {
this.pool = postgresPool;
this.encryptor = new AESEncryptor(encryptionKey, 1);
}
/**
* Insert patient record with encrypted sensitive fields
*/
async insertPatientRecord(record: {
name: string;
email: string;
ssn: string;
medicalHistory: string[];
}): Promise<void> {
// Encrypt sensitive fields
const encryptedSSN = this.encryptor.encrypt(record.ssn);
const encryptedHistory = this.encryptor.encrypt(JSON.stringify(record.medicalHistory));
// Store encrypted data as JSONB
await this.pool.query(
`INSERT INTO patients (name, email, encrypted_ssn, encrypted_medical_history)
VALUES ($1, $2, $3, $4)`,
[
record.name,
record.email,
encryptedSSN, // Store as JSONB
encryptedHistory
]
);
}
/**
* Query patient record and decrypt sensitive fields
*/
async getPatientRecord(patientId: string): Promise<any> {
const result = await this.pool.query(
'SELECT * FROM patients WHERE id = $1',
[patientId]
);
if (result.rows.length === 0) {
throw new Error('Patient not found');
}
const row = result.rows[0];
return {
id: row.id,
name: row.name,
email: row.email,
ssn: this.encryptor.decrypt(row.encrypted_ssn),
medicalHistory: JSON.parse(this.encryptor.decrypt(row.encrypted_medical_history))
};
}
}
Database schema:
CREATE TABLE patients (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
encrypted_ssn JSONB NOT NULL, -- Stores EncryptedPayload as JSON
encrypted_medical_history JSONB NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Note: Cannot create index on encrypted fields (breaks queries)
-- Use deterministic encryption if you need to query encrypted fields
Automated Key Rotation {#key-rotation}
Why Key Rotation Matters
Key rotation is the practice of periodically generating new encryption keys and re-encrypting data. Benefits:
- Limits blast radius: If a key is compromised, only data encrypted with that key version is at risk
- Compliance requirement: HIPAA, PCI DSS, and SOC 2 require annual key rotation
- Cryptoperiod best practice: NIST recommends rotating keys every 1-2 years
Two rotation strategies:
- Re-encryption: Decrypt all data with old key, encrypt with new key (disruptive, but secure)
- Multi-version support: Support multiple key versions, encrypt new data with latest key (seamless)
Zero-Downtime Key Rotation Service
// src/services/encryption/key-rotation.service.ts
import { AESEncryptor } from './aes-encryptor';
import { Firestore } from 'firebase-admin/firestore';
export interface KeyVersion {
version: number;
keyHex: string;
createdAt: Date;
rotatedAt?: Date;
status: 'active' | 'deprecated' | 'deleted';
}
/**
* Multi-version key rotation service
* Supports seamless key rotation without downtime
*/
export class KeyRotationService {
private encryptors: Map<number, AESEncryptor> = new Map();
private currentVersion: number;
private firestore: Firestore;
constructor(firestore: Firestore, keyVersions: KeyVersion[]) {
this.firestore = firestore;
// Initialize encryptors for all active/deprecated keys
for (const keyVersion of keyVersions) {
if (keyVersion.status !== 'deleted') {
this.encryptors.set(
keyVersion.version,
new AESEncryptor(keyVersion.keyHex, keyVersion.version)
);
}
}
// Set current version (highest version number with 'active' status)
const activeVersions = keyVersions
.filter(kv => kv.status === 'active')
.sort((a, b) => b.version - a.version);
if (activeVersions.length === 0) {
throw new Error('No active encryption key versions');
}
this.currentVersion = activeVersions[0].version;
}
/**
* Encrypt with latest active key version
*/
encrypt(plaintext: string): any {
const encryptor = this.encryptors.get(this.currentVersion);
if (!encryptor) {
throw new Error(`Current key version ${this.currentVersion} not found`);
}
return encryptor.encrypt(plaintext);
}
/**
* Decrypt using key version specified in payload
* Supports decryption of data encrypted with old keys
*/
decrypt(payload: any): string {
const encryptor = this.encryptors.get(payload.version);
if (!encryptor) {
throw new Error(`Key version ${payload.version} not found or deleted`);
}
return encryptor.decrypt(payload);
}
/**
* Rotate encryption key (zero-downtime)
*
* 1. Generate new key version
* 2. Mark old version as deprecated (still usable for decryption)
* 3. All new encryptions use new key
* 4. Background job re-encrypts old data (optional)
*/
async rotateKey(): Promise<void> {
// Generate new key
const newVersion = this.currentVersion + 1;
const newKeyHex = AESEncryptor.generateKey();
// Add new encryptor
this.encryptors.set(newVersion, new AESEncryptor(newKeyHex, newVersion));
// Update Firestore key registry
await this.firestore.collection('_encryption_keys').doc(`v${newVersion}`).set({
version: newVersion,
keyHex: newKeyHex, // In production, encrypt this with KMS!
createdAt: new Date(),
status: 'active'
});
// Mark old version as deprecated (still usable for decryption)
await this.firestore.collection('_encryption_keys').doc(`v${this.currentVersion}`).update({
status: 'deprecated',
rotatedAt: new Date()
});
// Switch to new version for all new encryptions
this.currentVersion = newVersion;
console.log(`Key rotated from v${this.currentVersion - 1} to v${newVersion}`);
}
/**
* Re-encrypt document with latest key version
* Call this from background job to gradually re-encrypt old data
*/
async reencryptDocument(collectionPath: string, docId: string, encryptedFields: string[]): Promise<void> {
const docRef = this.firestore.collection(collectionPath).doc(docId);
const doc = await docRef.get();
if (!doc.exists) {
return;
}
const data = doc.data()!;
let updated = false;
for (const field of encryptedFields) {
const encryptedField = data[field];
// Check if field is encrypted with old key version
if (encryptedField?.version && encryptedField.version < this.currentVersion) {
// Decrypt with old key
const plaintext = this.decrypt(encryptedField);
// Re-encrypt with current key
const reencrypted = this.encrypt(plaintext);
data[field] = reencrypted;
updated = true;
}
}
if (updated) {
await docRef.update(data);
console.log(`Re-encrypted ${collectionPath}/${docId}`);
}
}
/**
* Background job: Re-encrypt all documents in collection
* Run this weekly to gradually re-encrypt old data with latest key
*/
async reencryptCollection(collectionPath: string, encryptedFields: string[]): Promise<void> {
const snapshot = await this.firestore.collection(collectionPath).get();
for (const doc of snapshot.docs) {
try {
await this.reencryptDocument(collectionPath, doc.id, encryptedFields);
} catch (error) {
console.error(`Failed to re-encrypt ${collectionPath}/${doc.id}:`, error);
}
}
console.log(`Re-encryption complete for ${collectionPath}`);
}
}
Usage example:
// Initialize with multiple key versions
const keyRotationService = new KeyRotationService(firestore, [
{ version: 1, keyHex: 'a'.repeat(64), createdAt: new Date('2024-01-01'), status: 'deprecated' },
{ version: 2, keyHex: 'b'.repeat(64), createdAt: new Date('2026-01-01'), status: 'active' }
]);
// Encrypt with latest key (v2)
const encrypted = keyRotationService.encrypt('sensitive data');
// Decrypt old data (v1) still works
const oldData = { ciphertext: '...', iv: '...', authTag: '...', version: 1 };
const decrypted = keyRotationService.decrypt(oldData);
// Rotate key (creates v3)
await keyRotationService.rotateKey();
// Re-encrypt old documents (background job, run weekly)
await keyRotationService.reencryptCollection('patients', ['ssn', 'medicalHistory']);
Compliance Requirements: HIPAA, GDPR, PCI DSS {#compliance}
HIPAA Encryption Requirements
For HIPAA-compliant ChatGPT apps:
Required:
- AES-256 encryption for PHI at rest
- TLS 1.2+ for PHI in transit
- Key management policy (documented rotation schedule)
- Access controls (who can decrypt PHI)
- Audit logging (every decryption logged)
- Business Associate Agreement (BAA) with cloud provider
Validation checklist:
// src/services/compliance/hipaa-validator.ts
export class HIPAAComplianceValidator {
validateEncryptionAtRest(config: {
algorithm: string;
keyLength: number;
keyRotationDays: number;
}): { compliant: boolean; issues: string[] } {
const issues: string[] = [];
// HIPAA requires AES-256
if (config.algorithm !== 'aes-256-gcm') {
issues.push('HIPAA requires AES-256 encryption (found: ' + config.algorithm + ')');
}
// Key rotation recommended annually
if (config.keyRotationDays > 365) {
issues.push('Key rotation exceeds recommended 365 days');
}
return {
compliant: issues.length === 0,
issues
};
}
validateAccessControls(userId: string, resourceType: string): boolean {
// Verify user has authorization to decrypt PHI
// Check role-based access control (RBAC)
// Log access attempt for audit trail
return true; // Placeholder
}
logPHIAccess(event: {
userId: string;
action: 'encrypt' | 'decrypt';
resourceId: string;
timestamp: Date;
}): void {
// HIPAA requires audit logging of all PHI access
console.log('[HIPAA Audit]', JSON.stringify(event));
// In production: Send to CloudWatch Logs, Splunk, or SIEM
}
}
GDPR Encryption Requirements
GDPR Article 32: Requires "encryption of personal data" as a security measure.
Implementation:
- Encrypt personal data (names, emails, IPs, location data)
- Document encryption methods in privacy policy
- Provide data portability (export decrypted data on request)
- Support "right to be forgotten" (delete encryption keys to make data unrecoverable)
Right to be forgotten implementation:
// Delete user data by deleting encryption keys (makes data permanently unrecoverable)
async function deleteUserData(userId: string): Promise<void> {
// Delete encryption key (makes encrypted data unrecoverable)
await firestore.collection('_encryption_keys').doc(`user-${userId}`).delete();
// Encrypted data can remain in database (now cryptographically useless)
console.log(`User ${userId} data deleted (key destroyed)`);
}
PCI DSS Encryption Requirements
For ChatGPT apps handling payment card data:
PCI DSS Requirement 3.4: Cardholder data must be unreadable wherever stored.
Required:
- AES-128 minimum (AES-256 recommended)
- Strong cryptographic keys
- Secure key storage (HSM or KMS)
- Annual key rotation
- Key destruction procedures
Implementation:
// Encrypt credit card number (PCI DSS Level 1 compliant)
const creditCardNumber = '4111111111111111';
const encrypted = kmsService.envelopeEncrypt(
creditCardNumber,
{ cardholderId: 'user-123', merchantId: 'merchant-456' }
);
// Store in Firestore (encrypted)
await firestore.collection('payment_methods').doc('pm-123').set({
encryptedCardNumber: encrypted,
last4: creditCardNumber.slice(-4), // Last 4 digits can be stored in plaintext
expiryMonth: 12,
expiryYear: 2027
});
Production Architecture Examples {#production-architecture}
Architecture 1: HIPAA-Compliant Healthcare ChatGPT App
┌─────────────────────────────────────────────────────────────┐
│ ChatGPT User │
│ "Schedule my diabetes checkup for next Tuesday" │
└────────────┬────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ChatGPT Widget (OpenAI Infrastructure) │
│ - OAuth 2.1 token validation │
│ - TLS 1.3 in transit │
└────────────┬────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ MCP Server (Your Backend - Cloud Functions) │
│ │
│ 1. Validate OAuth token │
│ 2. Fetch patient record from Firestore │
│ 3. Decrypt PHI using KMS envelope decryption │
│ 4. Check appointment availability │
│ 5. Create appointment │
│ 6. Encrypt PHI using KMS envelope encryption │
│ 7. Store encrypted appointment in Firestore │
│ 8. Log HIPAA audit event │
│ │
└────────────┬────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Firebase Firestore (Encrypted At Rest) │
│ │
│ patients/{patientId} │
│ - name: "John Doe" (plaintext) │
│ - email: "john@example.com" (plaintext) │
│ - encryptedSSN: { │
│ encryptedData: "base64...", │
│ encryptedDataKey: "base64...", ← KMS-encrypted DEK │
│ iv: "base64...", │
│ authTag: "base64..." │
│ } │
│ - encryptedMedicalHistory: { ... } │
│ │
└────────────┬────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ AWS KMS (Key Management Service) │
│ │
│ Master Key (KEK): arn:aws:kms:us-east-1:...:key/... │
│ - Protected by FIPS 140-2 Level 3 HSM │
│ - Automatic key rotation enabled (annual) │
│ - CloudTrail logs every decrypt operation │
│ │
└─────────────────────────────────────────────────────────────┘
Security layers:
- Transit: TLS 1.3 (ChatGPT → MCP Server)
- Application: Envelope encryption (AES-256-GCM + KMS)
- Database: Firestore encryption at rest (automatic)
- Disk: GCP disk encryption (automatic)
Compliance evidence:
- Encryption algorithm: AES-256-GCM ✓
- Key management: AWS KMS with HSM ✓
- Key rotation: Annual (configurable) ✓
- Audit logging: CloudTrail + Cloud Functions logs ✓
- Access controls: IAM policies restrict KMS decrypt ✓
Architecture 2: Multi-Tenant SaaS with Per-Tenant Encryption
// Each tenant (fitness studio) gets unique encryption key
const tenantEncryptionService = new Map<string, KMSEncryptionService>();
// Initialize per-tenant KMS keys
async function initializeTenantKeys(tenantId: string): Promise<void> {
const kmsKeyAlias = `alias/tenant-${tenantId}`;
tenantEncryptionService.set(
tenantId,
new KMSEncryptionService('us-east-1', kmsKeyAlias)
);
}
// Encrypt data with tenant-specific key
async function encryptTenantData(tenantId: string, data: string): Promise<any> {
const service = tenantEncryptionService.get(tenantId);
if (!service) {
throw new Error(`No encryption key for tenant ${tenantId}`);
}
return service.envelopeEncrypt(data, { tenantId });
}
// Benefit: If one tenant's key is compromised, other tenants unaffected
Conclusion
Encryption at rest is the cornerstone of ChatGPT app security. By implementing AES-256-GCM encryption, AWS KMS envelope encryption, field-level database encryption, and automated key rotation, you create multiple layers of defense against data breaches.
Key takeaways:
- Use AES-256-GCM for all symmetric encryption (industry standard)
- Implement envelope encryption with AWS KMS for compliance and auditability
- Encrypt sensitive fields at application level (don't rely solely on database encryption)
- Rotate keys annually (or more frequently for high-security environments)
- Log all decryption operations for audit trails (HIPAA, SOC 2 requirement)
- Never hardcode keys in source code (use environment variables or KMS)
For complete security coverage, combine encryption at rest with:
- Encryption in transit (TLS 1.3)
- OAuth 2.1 authentication
- Access token verification
- Security auditing and logging
- Penetration testing
Ready to build a secure ChatGPT app with encryption at rest? Start with MakeAIHQ's no-code platform—we handle encryption, key management, and compliance out of the box, so you can focus on building great AI experiences.
External Resources
Related Articles: