Mã hóa Database: Đừng để dữ liệu “lộ thiên” với pgcrypto và AES_ENCRYPT

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Tại sao lưu plain-text là “tự sát” trong kỷ nguyên bảo mật?

Ngày đầu vào nghề, mình từng nghĩ chỉ cần chặn cổng database từ bên ngoài là dữ liệu đã an toàn. Nhưng đời không như mơ. Theo báo cáo của IBM năm 2023, chi phí trung bình cho một vụ rò rỉ dữ liệu đã lên tới 4.45 triệu USD. Nếu server bị chiếm quyền (RCE) hoặc file backup lọt vào tay kẻ xấu, toàn bộ số CCCD hay địa chỉ khách hàng sẽ bị phơi bày sạch sẽ dưới dạng văn bản thuần.

Theo tiêu chuẩn GDPR, bảo vệ dữ liệu cá nhân là yêu cầu bắt buộc chứ không còn là tùy chọn. Cách tốt nhất để giảm rủi ro là mã hóa dữ liệu ngay tại tầng database (Encryption at Rest). Khi đó, nếu hacker lấy được file dữ liệu mà không có khóa giải mã (Secret Key), thứ chúng nhận được chỉ là những chuỗi ký tự vô nghĩa.

Mình đã kinh qua nhiều dự án với MySQL và PostgreSQL. Với những hệ thống đòi hỏi tính tuân thủ cao, pgcrypto (Postgres) và AES_ENCRYPT (MySQL) là hai công cụ đắc lực nhất nhờ sự cân bằng giữa hiệu năng và bảo mật.

Mã hóa dữ liệu trong PostgreSQL với extension pgcrypto

PostgreSQL không tích hợp mã hóa đối xứng trực tiếp vào nhân (core). Thay vào đó, họ cung cấp pgcrypto — một extension cực kỳ mạnh mẽ hỗ trợ từ AES, Blowfish đến cả PGP.

Cài đặt pgcrypto

Để bắt đầu, bạn cần kích hoạt extension này trong database mục tiêu. Chỉ cần một dòng lệnh duy nhất:

CREATE EXTENSION IF NOT EXISTS pgcrypto;

Sau khi chạy, hãy thử liệt kê các hàm có tiền tố pgp_ để đảm bảo mọi thứ đã sẵn sàng.

Thao tác Insert và Select dữ liệu

Giả sử bạn cần bảo mật cột phone_number trong bảng customers. Một lưu ý quan trọng: cột chứa dữ liệu mã hóa bắt buộc phải để kiểu bytea (binary data).

CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    full_name TEXT,
    phone_number BYTEA
);

Khi insert, chúng ta dùng hàm pgp_sym_encrypt. Để tạo secret key đủ mạnh (thường là 32 ký tự), mình hay dùng Password Generator. Công cụ này chạy hoàn toàn dưới client nên không lo bị lộ key về server.

INSERT INTO customers (full_name, phone_number) 
VALUES ('Nguyen Van A', pgp_sym_encrypt('0901234567', 'my_super_secret_key'));

Để đọc lại dữ liệu, bạn chỉ cần dùng hàm pgp_sym_decrypt để đưa nó về dạng text ban đầu:

SELECT full_name, pgp_sym_decrypt(phone_number, 'my_super_secret_key') as phone 
FROM customers;

Bảo vệ dữ liệu trong MySQL với AES_ENCRYPT

Khác với Postgres, MySQL tích hợp sẵn các hàm mã hóa. Tuy nhiên, một sai lầm phổ biến của các bạn Junior là dùng cấu hình mặc định, vốn rất lỏng lẻo.

Thiết lập chế độ mã hóa (Encryption Mode)

Mặc định MySQL dùng aes-128-ecb. Chế độ ECB (Electronic Codebook) rất yếu vì các khối dữ liệu giống nhau sẽ cho ra kết quả mã hóa y hệt nhau. Để đạt chuẩn bảo mật hiện đại, hãy chuyển sang aes-256-cbc.

-- Cấu hình trong session hoặc file my.cnf
SET block_encryption_mode = 'aes-256-cbc';

Cách dùng AES_ENCRYPT và AES_DECRYPT

Với chế độ CBC, bạn cần thêm một tham số là Initialization Vector (IV). IV giúp đảm bảo cùng một nội dung nhưng mỗi lần mã hóa sẽ cho ra một kết quả khác nhau, khiến hacker khó lòng đoán biết quy luật.

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255),
    ssn VARBINARY(255) -- Dùng VARBINARY thay vì VARCHAR
);

Khi insert, hãy tạo một chuỗi IV ngẫu nhiên dài 16 bytes bằng hàm RANDOM_BYTES(16):

SET @key = 'secret_key_32_chars_long_for_aes256';
SET @iv = RANDOM_BYTES(16);

INSERT INTO users (email, ssn) 
VALUES ('[email protected]', AES_ENCRYPT('123-456-789', @key, @iv));

Nhớ lưu lại @iv này vào một cột riêng. Nếu mất IV, coi như bạn đã “vứt” luôn dữ liệu đó vì không thể giải mã được.

Kinh nghiệm thực chiến và những lưu ý “xương máu”

Mã hóa dữ liệu không chỉ đơn thuần là chạy vài câu lệnh SQL. Trong quá trình vận hành, mình đã rút ra 3 bài học lớn:

1. Đừng mã hóa vô tội vạ

Mã hóa và giải mã cực kỳ ngốn CPU. Nếu bạn mã hóa mọi cột trong bảng có hàng triệu dòng, latency có thể tăng gấp 2-3 lần. Chỉ nên ưu tiên những thông tin thực sự nhạy cảm.

Ngoài ra, bạn không thể dùng WHERE phone_number = '090...' trực tiếp vì dữ liệu đã bị biến đổi. Muốn tìm kiếm nhanh, hãy dùng kỹ thuật Blind Index (lưu thêm một bản hash của dữ liệu). Bạn có thể dùng Hash Generator để chọn thuật toán SHA-256 cho mục đích này.

2. Quản lý Secret Key

Tuyệt đối không hard-code key trong code hay SQL script. Nếu lỡ tay push key lên GitHub, toàn bộ hệ thống coi như sụp đổ. Hãy sử dụng Environment Variables hoặc các dịch vụ chuyên dụng như AWS Secrets Manager.

3. Kiểm tra độ dài cột

Dữ liệu sau khi mã hóa thường dài hơn text gốc và ở dạng binary. Nếu độ dài cột không đủ, dữ liệu sẽ bị cắt cụt và không thể giải mã. Mình thường dùng Base64 Encoder để kiểm tra chuỗi sau mã hóa, đảm bảo nó không bị lỗi khi truyền tải qua API.

Chốt lại, mã hóa database là lá chắn cuối cùng bảo vệ uy tín của bạn. Dù chọn Postgres hay MySQL, hãy luôn nhớ: chọn thuật toán mạnh, dùng mode CBC và giữ chìa khóa thật kỹ!

Share: