Loading...
Loading...
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.
npx skill4agent add afu-it/security-for-vibecoders data-encryption| 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 |
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();
}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);
}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 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);# Generate a 256-bit key
openssl rand -hex 32const KEYS = {
current: env.ENCRYPTION_KEY_V2,
previous: env.ENCRYPTION_KEY_V1, // For decrypting old data
};-- 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)