# Data Encryption

> Encrypt personal data at rest and in transit to comply with PDPA 2024. Covers field-level encryption for PII, AES-256-GCM patterns, key management, and database column encryption for Node.js, Python, Laravel, and Cloudflare Workers. Use when storing sensitive user data, implementing encryption, or responding to PDPA compliance requirements.

- Skill: `afu-it/data-encryption` (Agent Skill)
- Install (CLI): `npx skillmds@latest add afu-it/data-encryption`
- Raw SKILL.md: https://api.skillmd.com/api/skills/afu-it/data-encryption/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: afu-it (https://skillmd.com/u/afu-it)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/afu-it/data-encryption

---


# 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

```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)

```typescript
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

```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

```php
// 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:**
   ```bash
   # 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:
   ```typescript
   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

```sql
-- 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.

