data-encryption

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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.
对静态个人数据进行加密,以符合PDPA 2024要求。如果你的数据库遭到泄露但PII字段已加密,你就拥有了强有力的抗辩理由,可避免被判存在过失。

The Rule

规则

ENCRYPT all Personally Identifiable Information (PII) at rest. Use AES-256-GCM with proper key management.
加密所有静态的个人可识别信息(PII)。使用AES-256-GCM并配合完善的密钥管理。

What Must Be Encrypted

必须加密的内容

Under PDPA 2024, these fields are data peribadi (personal data) and should be encrypted at rest:
FieldWhy
IC number (NRIC)Unique identifier, identity theft risk
Phone numberDirect contact, harassment risk
Home addressPhysical safety risk
Bank account / card numbersFinancial fraud risk
Medical recordsDiscrimination risk
Biometric dataCannot be changed if leaked
Email (in some contexts)Phishing, account takeover
根据PDPA 2024,以下字段属于data peribadi(个人数据),静态存储时应当加密:
字段原因
IC number (NRIC)唯一标识符,存在身份盗用风险
电话号码直接联系方式,存在骚扰风险
家庭住址存在人身安全风险
银行账户/卡号存在金融诈骗风险
医疗记录存在歧视风险
生物识别数据泄露后无法更改
邮箱(部分场景下)存在钓鱼、账户被盗风险

Implementation Patterns

实现模式

Node.js / TypeScript

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();
}
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)

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);
}
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

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

Option 1: Fernet (simpler, includes timestamp)

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

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()
undefined
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()
undefined

Laravel

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);
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
  1. 正确生成密钥:
    bash
    # Generate a 256-bit key
    openssl rand -hex 32
  2. 密钥与数据分开存储 -- 如果数据库被泄露,密钥不应存放在同一位置
  3. 定期轮换密钥 -- 支持多个活跃密钥:
    typescript
    const KEYS = {
      current: env.ENCRYPTION_KEY_V2,
      previous: env.ENCRYPTION_KEY_V1, // For decrypting old data
    };
  4. 严禁硬编码密钥 -- 始终使用环境变量或密钥管理服务
  5. 大型数据集采用信封加密 -- 用数据密钥加密数据,再用主密钥加密数据密钥

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)
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
  • 所有PII字段均已静态加密(IC、电话、地址、银行信息)
  • 使用AES-256-GCM(认证加密)
  • 每次加密操作使用唯一IV/nonce(绝不重复使用)
  • 加密密钥存储在环境变量中,而非代码内
  • 已制定密钥轮换策略
  • 密钥与加密数据分开存储
  • 所有传输中数据强制使用HTTPS
  • 数据库备份也已加密

PDPA 2024 Consequence

PDPA 2024 违规后果

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.
Seksyen 5(1) dan 5(2) PDPA 2010(Pindaan 2024):
  • 最高罚款 RM1,000,000
  • 最高监禁 3 年
PDPA 2024明确要求采取langkah keselamatan yang munasabah(合理安全措施)保护个人数据。静态加密被认为是一项基础的合理措施。如果发生数据泄露且数据以明文存储,开发人员无法主张自己已采取合理的预防措施。