SuperAPI AI Directory

data-encryption

Security & auditing

View repository

Updated 10-15 08:42

About this skill

Data Encryption

Encrypt personal data at rest to comply with PDPA 2024. If your database is breached but PII fields are encrypted, you have a strong defense against negligence charges.

The Rule

ENCRYPT all Personally Identifiable Information (PII) at rest. Use AES-256-GCM with proper key management.

What Must Be Encrypted

Under PDPA 2024, these fields are data peribadi (personal data) and should be encrypted at rest:

Field Why
IC number (NRIC) Unique identifier, identity theft risk
Phone number Direct contact, harassment risk
Home address Physical safety risk
Bank account / card numbers Financial fraud risk
Medical records Discrimination risk
Biometric data Cannot be changed if leaked
Email (in some contexts) Phishing, account takeover

Implementation Patterns

Node.js / TypeScript

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

const ENCRYPTION_KEY = Buffer.from(env.ENCRYPTION_KEY, 'hex'); // 32 bytes for AES-256
const ALGORITHM = 'aes-256-gcm';

interface EncryptedField {
  ciphertext: string; // hex
  iv: string;         // hex
  tag: string;        // hex
}

function encrypt(plaintext: string): EncryptedField {
  const iv = randomBytes(12); // 96-bit IV for GCM
  const cipher = createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);

  let ciphertext = cipher.update(plaintext, 'utf8', 'hex');
  ciphertext += cipher.final('hex');
  const tag = cipher.getAuthTag();

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

function decrypt(encrypted: EncryptedField): string {
  const decipher = createDecipheriv(
    ALGORITHM,
    ENCRYPTION_KEY,
    Buffer.from(encrypted.iv, 'hex')
  );
  decipher.setAuthTag(Buffer.from(encrypted.tag, 'hex'));

  let plaintext = decipher.update(encrypted.ciphertext, 'hex', 'utf8');
  plaintext += decipher.final('utf8');
  return plaintext;
}

// Usage with database
async function createUser(data: { name: string; ic_number: string; phone: string }) {
  const encryptedIC = encrypt(data.ic_number);
  const encryptedPhone = encrypt(data.phone);

  await db.prepare(`
    INSERT INTO users (name, ic_number_enc, ic_number_iv, ic_number_tag, phone_enc, phone_iv, phone_tag)
    VALUES (?, ?, ?, ?, ?, ?, ?)
  `).bind(
    data.name,
    encryptedIC.ciphertext, encryptedIC.iv, encryptedIC.tag,
    encryptedPhone.ciphertext, encryptedPhone.iv, encryptedPhone.tag,
  ).run();
}

Cloudflare Workers (Web Crypto API)

const ALGORITHM = { name: 'AES-GCM', length: 256 };

async function getKey(env: Env): Promise<CryptoKey> {
  const keyData = Buffer.from(env.ENCRYPTION_KEY, 'hex');
  return crypto.subtle.importKey('raw', keyData, ALGORITHM, false, ['encrypt', 'decrypt']);
}

async function encrypt(plaintext: string, env: Env): Promise<string> {
  const key = await getKey(env);
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encoded = new TextEncoder().encode(plaintext);

  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    encoded
  );

  // Pack IV + ciphertext (GCM tag is appended automatically)
  const packed = new Uint8Array(iv.length + new Uint8Array(ciphertext).length);
  packed.set(iv);
  packed.set(new Uint8Array(ciphertext), iv.length);

  return Buffer.from(packed).toString('base64');
}

async function decrypt(packed: string, env: Env): Promise<string> {
  const key = await getKey(env);
  const data = Buffer.from(packed, 'base64');

  const iv = data.slice(0, 12);
  const ciphertext = data.slice(12);

  const plaintext = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv },
    key,
    ciphertext
  );

  return new TextDecoder().decode(plaintext);
}

Python

from cryptography.fernet import Fernet
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os, base64

# Option 1: Fernet (simpler, includes timestamp)
key = os.environ['ENCRYPTION_KEY'].encode()  # base64-encoded 32 bytes
f = Fernet(key)

encrypted = f.encrypt(plaintext.encode())
decrypted = f.decrypt(encrypted).decode()

# Option 2: AES-256-GCM (more control)
key = bytes.fromhex(os.environ['ENCRYPTION_KEY'])  # 32 bytes hex
aesgcm = AESGCM(key)

def encrypt_field(plaintext: str) -> str:
    nonce = os.urandom(12)
    ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)
    return base64.b64encode(nonce + ciphertext).decode()

def decrypt_field(packed: str) -> str:
    data = base64.b64decode(packed)
    nonce, ciphertext = data[:12], data[12:]
    return aesgcm.decrypt(nonce, ciphertext, None).decode()

Laravel

// Laravel has built-in encryption via APP_KEY
use Illuminate\Support\Facades\Crypt;

// In your model -- auto-encrypt/decrypt
class User extends Model
{
    protected $casts = [
        'ic_number' => 'encrypted',
        'phone' => 'encrypted',
        'address' => 'encrypted',
    ];
}

// Manual usage
$encrypted = Crypt::encryptString($icNumber);
$decrypted = Crypt::decryptString($encrypted);

Key Management Rules

  1. Generate keys properly:

    # Generate a 256-bit key
    openssl rand -hex 32
    
  2. Store keys separately from data -- if DB is breached, keys should not be in the same place

  3. Rotate keys periodically -- support multiple active keys:

    const KEYS = {
      current: env.ENCRYPTION_KEY_V2,
      previous: env.ENCRYPTION_KEY_V1, // For decrypting old data
    };
    
  4. Never hardcode keys -- always use environment variables or secret managers

  5. Use envelope encryption for large datasets -- encrypt data with a data key, encrypt the data key with a master key

Database Schema Pattern

-- Store encrypted fields with their IV/nonce
CREATE TABLE users (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT NOT NULL,

  -- Encrypted PII fields
  ic_number_enc TEXT,      -- Encrypted ciphertext
  phone_enc TEXT,          -- Encrypted ciphertext
  address_enc TEXT,        -- Encrypted ciphertext

  -- Key version for rotation support
  encryption_key_version INTEGER DEFAULT 1,

  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- You can still search by non-encrypted fields
-- For searchable encrypted fields, store a blind index (HMAC hash)

Encryption Checklist

  • All PII fields encrypted at rest (IC, phone, address, bank details)
  • Using AES-256-GCM (authenticated encryption)
  • Unique IV/nonce per encryption operation (never reuse)
  • Encryption keys stored in environment variables, not code
  • Key rotation strategy in place
  • Keys stored separately from encrypted data
  • HTTPS enforced for all data in transit
  • Database backups are also encrypted

PDPA 2024 Consequence

Seksyen 5(1) dan 5(2) PDPA 2010 (Pindaan 2024):

  • Denda sehingga RM1,000,000
  • Penjara sehingga 3 tahun

PDPA 2024 explicitly requires langkah keselamatan yang munasabah for protecting personal data. Encryption at rest is considered a baseline reasonable measure. If a breach occurs and data was stored in plaintext, the developer cannot argue they took reasonable precautions.