Back to Blog & Guides/GDPR Data Encryption at Rest & in Transit: A Developer's Implementation Guide
#GDPR#Encryption#Security#Node.js#Cryptography#PostgreSQL

GDPR Data Encryption at Rest & in Transit: A Developer's Implementation Guide

Learn how to implement AES-256-GCM field-level database encryption, authenticated key rotation, PostgreSQL pgcrypto, and TLS 1.3 cipher suite enforcement for GDPR Article 32 and Article 25 compliance.

SovereignShield Engineering

1. Regulatory Context: GDPR Article 32 & Article 25

The General Data Protection Regulation (GDPR) establishes data security as a mandatory legal obligation rather than an optional operational preference. Within the SovereignShield Compliance Matrix, cryptographic controls form the bedrock of technical safeguard compliance. Two primary articles govern cryptographic implementations for web applications and cloud architectures:

Article 32: Security of Processing

Article 32(1)(a) mandates that data controllers and processors implement appropriate technical and organizational measures to ensure a level of security appropriate to the risk, explicitly highlighting:

“The pseudonymisation and encryption of personal data…”

Furthermore, Article 32(1)(d) requires:

“A process for regularly testing, assessing, and evaluating the effectiveness of technical and organizational measures for ensuring the security of the processing.”

Article 25: Data Protection by Design and by Default

Article 25 requires engineers to integrate privacy and cryptographic safeguards directly into application logic and database schemas during the initial architectural design phase, rather than attempting to apply perimeter security after deployment. When combined with Article 17 erasure requirements (explored in our GDPR Right to Be Forgotten Database Patterns Guide), field encryption ensures that even archived records remain unreadable.


2. Encryption at Rest: Field-Level Encryption (FLE) vs. Infrastructure Encryption

Why Infrastructure Encryption Is Insufficient

Many development teams rely exclusively on storage-layer encryption (such as AWS EBS Volume Encryption, Azure Storage Encryption, or Cloud SQL Disk Encryption). While disk-level encryption (LUKS, BitLocker) protects physical server hardware against theft from a data center, it provides zero protection against application-layer vulnerabilities:

  • An attacker executing a SQL Injection (SQLi) attack receives decrypted records because the database engine transparently decrypts storage upon disk read.
  • A compromised database connection string or exposed administrative console (pgAdmin, DBeaver) exposes all raw PII in plaintext.
  • Unencrypted backups exported from the database contain exposed user metadata.

Field-Level Encryption Architecture

Field-Level Encryption (FLE) encrypts specific sensitive columns (e.g. email addresses, national tax IDs, financial metrics, IP addresses) in application memory before the payload reaches the database driver. The database stores only ciphertext, initialization vectors (IVs), authentication tags, and key version metadata.

+-------------------+      +-----------------------+      +-----------------------+
|  Application Code | ---> |  AES-256-GCM Encrypt  | ---> |   Database Storage    |
| (Raw Plaintext PII) |      |  (Application Memory) |      | (Ciphertext + IV + Tag)|
+-------------------+      +-----------------------+      +-----------------------+

3. Node.js & TypeScript AES-256-GCM Implementation

AES-256-GCM (Galois/Counter Mode) is an Authenticated Encryption with Associated Data (AEAD) algorithm. Unlike legacy modes such as CBC (Cipher Block Chaining), GCM generates an authentication tag that detects any unauthorized modification or tampering of the encrypted payload prior to decryption.

Cryptographic Parameters

  • Algorithm: aes-256-gcm
  • Key Length: 256 bits (32 bytes)
  • Initialization Vector (IV): Cryptographically secure random 96-bit (12-byte) buffer generated per record. Never reuse an IV with the same encryption key.
  • Auth Tag: 128-bit (16-byte) authentication tag.

Production Key Rotation Engine

import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

export interface EncryptedPayload {
  ciphertext: string;  // Hex-encoded encrypted data
  iv: string;          // Hex-encoded 96-bit IV
  tag: string;         // Hex-encoded 128-bit authentication tag
  keyVersion: number;  // Version identifier for zero-downtime key rotation
}

export class FieldEncryptionService {
  private masterKeys: Map<number, Buffer>;
  private activeVersion: number;

  /**
   * Initializes the encryption service with versioned master keys.
   * @param keys Map of version numbers to 64-character hex strings (32 bytes / 256 bits)
   * @param activeVersion The key version to use for new encryptions
   */
  constructor(keys: Record<number, string>, activeVersion: number) {
    this.masterKeys = new Map();
    
    for (const [versionStr, hexKey] of Object.entries(keys)) {
      const version = Number(versionStr);
      const keyBuffer = Buffer.from(hexKey, 'hex');
      
      if (keyBuffer.length !== 32) {
        throw new Error(`[EncryptionService] Master key version ${version} must be exactly 256 bits (32 bytes).`);
      }
      this.masterKeys.set(version, keyBuffer);
    }

    if (!this.masterKeys.has(activeVersion)) {
      throw new Error(`[EncryptionService] Active key version ${activeVersion} is not configured.`);
    }

    this.activeVersion = activeVersion;
  }

  /**
   * Encrypts plaintext string using AES-256-GCM
   */
  public encrypt(plaintext: string): EncryptedPayload {
    if (typeof plaintext !== 'string' || plaintext.length === 0) {
      throw new Error('[EncryptionService] Cannot encrypt empty payload.');
    }

    // Generate 96-bit IV (12 bytes) using CSPRNG
    const iv = randomBytes(12);
    const key = this.masterKeys.get(this.activeVersion)!;

    const cipher = createCipheriv('aes-256-gcm', key, iv);
    let ciphertext = cipher.update(plaintext, 'utf8', 'hex');
    ciphertext += cipher.final('hex');
    const tag = cipher.getAuthTag().toString('hex');

    return {
      ciphertext,
      iv: iv.toString('hex'),
      tag,
      keyVersion: this.activeVersion,
    };
  }

  /**
   * Decrypts ciphertext and verifies authenticity tag.
   * Supports legacy key versions to enable zero-downtime key rotation.
   */
  public decrypt(payload: EncryptedPayload): string {
    const key = this.masterKeys.get(payload.keyVersion);
    if (!key) {
      throw new Error(`[EncryptionService] Key version ${payload.keyVersion} not found for decryption.`);
    }

    const decipher = createDecipheriv(
      'aes-256-gcm',
      key,
      Buffer.from(payload.iv, 'hex')
    );

    // Set the expected authentication tag
    decipher.setAuthTag(Buffer.from(payload.tag, 'hex'));

    try {
      let plaintext = decipher.update(payload.ciphertext, 'hex', 'utf8');
      plaintext += decipher.final('utf8');
      return plaintext;
    } catch (err) {
      throw new Error('[EncryptionService] Decryption failed. Payload may be tampered or corrupted.');
    }
  }

  /**
   * Re-encrypts payload to update it to the active master key version
   */
  public reencrypt(payload: EncryptedPayload): EncryptedPayload {
    if (payload.keyVersion === this.activeVersion) {
      return payload; // Already using latest key
    }
    const decryptedText = this.decrypt(payload);
    return this.encrypt(decryptedText);
  }
}

4. PostgreSQL Database Integration (pgcrypto)

For systems performing field encryption inside database triggers or stored procedures, PostgreSQL provides the pgcrypto extension.

View the complete pgcrypto Pseudonymization Reference Implementation on our GitHub

-- Enable pgcrypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- Table definition storing PII with AES-256 symmetric encryption
CREATE TABLE sensitive_user_profiles (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    organization_id UUID NOT NULL,
    -- Store encrypted PII as bytea
    encrypted_email BYTEA NOT NULL,
    encrypted_ssn BYTEA NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Insert record encrypted with AES-256-CBC / GCM key
INSERT INTO sensitive_user_profiles (organization_id, encrypted_email, encrypted_ssn)
VALUES (
    'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
    pgp_sym_encrypt('user@example.com', 'SUPER_SECRET_DB_MASTER_KEY', 'cipher-algo=aes256'),
    pgp_sym_encrypt('999-00-1234', 'SUPER_SECRET_DB_MASTER_KEY', 'cipher-algo=aes256')
);

-- Query and decrypt record
SELECT 
    id,
    organization_id,
    pgp_sym_decrypt(encrypted_email, 'SUPER_SECRET_DB_MASTER_KEY') AS email,
    pgp_sym_decrypt(encrypted_ssn, 'SUPER_SECRET_DB_MASTER_KEY') AS ssn
FROM sensitive_user_profiles
WHERE id = 'SOME_PROFILE_UUID';

5. Encryption in Transit: Hardened TLS 1.3 Web Server Setup

All data transmitted across public or internal networks must be secured using Transport Layer Security (TLS). GDPR enforcement authorities consider TLS 1.0 and TLS 1.1 obsolete and non-compliant due to vulnerabilities such as POODLE, BEAST, and CRIME.

NGINX TLS 1.3 Production Configuration

# /etc/nginx/sites-available/sovereignshield.conf

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name api.sovereignshieldapp.com;

    # SSL Certificate Paths (Let's Encrypt / Custom CA)
    ssl_certificate /etc/letsencrypt/live/api.sovereignshieldapp.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.sovereignshieldapp.com/privkey.pem;

    # Strictly enforce TLS 1.2 and TLS 1.3 exclusively
    ssl_protocols TLSv1.2 TLSv1.3;

    # High-security AEAD Cipher Suites with Perfect Forward Secrecy (PFS)
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # HSTS - HTTP Strict Transport Security (2 Years + Subdomains + Preload)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    
    # Defensive HTTP Security Headers
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Session Ticket & Caching Optimization
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    location / {
        proxy_pass http://127.0.0.1:4321;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }
}

6. Verification Checklist & Compliance Matrix

Requirement Target Technical Standard GDPR Article Verification Method
Field Encryption AES-256-GCM AEAD Article 32(1)(a) Unit test suite verifies payload encryption
Tamper Prevention 128-bit Auth Tag verification Article 32(1)(b) Test invalid auth tag throws error
Key Versioning Envelope key tags (e.g. keyVersion: 2) Article 32(1)(d) Zero-downtime key rotation pipeline test
Network Security TLS 1.3 / TLS 1.2 with PFS Article 32(1)(a) npx testssl.sh api.sovereignshieldapp.com
HSTS Enforcement max-age=63072000; includeSubDomains Article 25 curl -I https://sovereignshieldapp.com

7. Automated Cryptographic Unit Tests (Jest/Vitest)

// tests/unit/encryption.test.ts
import { describe, it, expect } from 'vitest';
import { FieldEncryptionService } from '../../src/utils/FieldEncryptionService';

const TEST_KEYS = {
  1: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
  2: 'fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210'
};

describe('FieldEncryptionService (AES-256-GCM)', () => {
  const service = new FieldEncryptionService(TEST_KEYS, 1);

  it('should encrypt and decrypt plaintext successfully', () => {
    const rawPII = 'patient-ssn-999-00-1234';
    const encrypted = service.encrypt(rawPII);

    expect(encrypted.ciphertext).not.toBe(rawPII);
    expect(encrypted.keyVersion).toBe(1);

    const decrypted = service.decrypt(encrypted);
    expect(decrypted).toBe(rawPII);
  });

  it('should throw error when auth tag is tampered with', () => {
    const encrypted = service.encrypt('sensitive-data');
    encrypted.tag = '00000000000000000000000000000000'; // Corrupt tag

    expect(() => service.decrypt(encrypted)).toThrow(/Decryption failed/);
  });

  it('should seamlessly decrypt older key versions during key rotation', () => {
    const v1Service = new FieldEncryptionService(TEST_KEYS, 1);
    const v1Payload = v1Service.encrypt('legacy-secret');

    const v2Service = new FieldEncryptionService(TEST_KEYS, 2);
    const decrypted = v2Service.decrypt(v1Payload);
    expect(decrypted).toBe('legacy-secret');

    // Re-encrypt to update key version
    const updatedPayload = v2Service.reencrypt(v1Payload);
    expect(updatedPayload.keyVersion).toBe(2);
  });
});

Conclusion & Architecture Recommendations

Implementing robust data protection for GDPR compliance requires combining field-level authenticated encryption (AES-256-GCM) at rest with hardened transport-layer security (TLS 1.3) in transit. Relying solely on disk volume encryption leaves applications vulnerable to application-level attacks. Developers should adopt application-layer envelope key rotation, enforce HSTS preload headers, and maintain automated cryptographic test suites across all production services.

To expand your technical compliance infrastructure:

CONTINUE READING

Related Compliance Engineering Guides

Explore SovereignShield Compliance Automation Tools

Audit GDPR & HIPAA controls, generate public trust centers, and export certified PDF proofs with zero data egress.