Mười bài trước xây từng viên gạch: hash, salt, AES, chế độ, IV, ngẫu nhiên an toàn. Bài này ghép chúng lại thành một việc thực tế ai cũng cần: mã hóa một file bằng mật khẩu. Nghe đơn giản nhưng có một khoảng cách quan trọng: người dùng nhớ một mật khẩu (chuỗi text), còn AES-256 cần một khóa nhị phân đúng 32 byte. Cầu nối là hàm dẫn khóa (KDF). Bài này làm thật bằng openssl 3.0 với AES-256-CBC + PBKDF2, và chỉ ra một cờ bắt buộc nhiều người quên.
Từ mật khẩu tới khóa: PBKDF2
AES không nhận "mật khẩu" — nó nhận khóa 32 byte ngẫu nhiên. Để biến mật khẩu người dùng thành khóa, ta dùng KDF (như bài băm mật khẩu):
khóa = PBKDF2(mật_khẩu, salt, số_vòng)
- salt ngẫu nhiên: cùng một mật khẩu dẫn ra khóa khác nhau mỗi lần mã hóa → hai file mã cùng mật khẩu không lộ là cùng khóa, và rainbow table vô dụng.
- số vòng lớn (600.000): mỗi lần thử mật khẩu tốn thời gian → chống brute-force (đúng lý do ở bài băm mật khẩu).
openssl enc gói sẵn cả quy trình: dẫn khóa từ mật khẩu, sinh salt/IV, mã hóa, và ghi salt vào đầu file để lúc giải mã dẫn lại đúng khóa.
openssl enc -aes-256-cbc -pbkdf2 -iter 600000 -salt -in f -out f.enc -pass pass:MatKhau
openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -in f.enc -pass pass:MatKhau

Hình 1: Mật khẩu → khóa AES qua PBKDF2 (salt + nhiều vòng). openssl enc -pbkdf2 -iter gói cả quy trình; file mã hóa tự chứa salt (header Salted__). Thiếu -pbkdf2 thì openssl dùng KDF cũ rất yếu.
Đo thật: mã hóa, giải mã, và vai trò salt

Hình 2: Mã hóa secret.txt (58 byte) → secret.enc (80 byte). File mở đầu bằng Salted__ + salt. Giải mã đúng mật khẩu ra nội dung; sai mật khẩu → bad decrypt. Mã hai lần: 8 byte Salted__ giống nhau, 8 byte salt sau khác (4113b807... vs 7e727047...) → bản mã khác.
Kết quả:
- Mã hóa: file 58 byte thành 80 byte (thêm 16 byte header
Salted__+ salt, cộng padding của CBC). - Header tự mô tả: file bắt đầu bằng 8 byte chữ
Salted__rồi 8 byte salt ngẫu nhiên. Nhờ salt nằm ngay trong file, lúc giải mã openssl đọc salt để dẫn lại đúng khóa từ mật khẩu — bạn không phải lưu salt riêng. - Giải mã: đúng mật khẩu ra lại nội dung gốc; sai mật khẩu →
bad decrypt. Mật khẩu sai dẫn ra khóa khác, giải ra rác và CBC phát hiện padding hỏng nên báo lỗi. - Salt làm bản mã khác nhau: mã cùng file + cùng mật khẩu hai lần cho hai bản mã khác — chỉ 8 byte
Salted__(5361...5f5f) giống, còn salt khác hẳn. Đây là điều salt bảo đảm: không lộ hai lần mã là cùng nội dung/mật khẩu.
Đánh đổi và lưu ý
BẮT BUỘC -pbkdf2 -iter. Đây là cái bẫy quan trọng nhất: openssl enc không có -pbkdf2 sẽ dùng hàm dẫn khóa cũ (EVP_BytesToKey) — một vòng MD5, cực yếu, brute-force nhanh. Luôn thêm -pbkdf2 -iter <số lớn>. openssl mới cảnh báo nếu thiếu, nhưng đừng dựa vào cảnh báo.
AES-256-CBC không tự xác thực. Như bài AES đã nói, CBC giấu nội dung nhưng không phát hiện được nếu file mã bị kẻ tấn công sửa có chủ đích (chỉ phát hiện hỏng padding ngẫu nhiên). Muốn chống giả mạo thật sự, dùng AES-GCM (authenticated encryption) hoặc thêm HMAC. openssl enc không hỗ trợ GCM tốt qua CLI — cho file quan trọng, dùng công cụ chuyên (age, gpg) hoặc thư viện (python cryptography Fernet/AESGCM).
Đừng truyền mật khẩu qua pass: trên dòng lệnh thật. -pass pass:MatKhau để lộ mật khẩu trong lịch sử shell và danh sách tiến trình (ps). Dùng -pass env:VAR, -pass file:path, hoặc nhập tương tác (-pass stdin). Ở đây pass: chỉ để minh họa.
Cân nhắc công cụ hiện đại. Cho nhu cầu mã hóa file thực tế, age (đơn giản, hiện đại, có xác thực) hay gpg (chuẩn OpenPGP) thường tốt hơn openssl enc — chúng lo GCM/xác thực, quản khóa, định dạng chuẩn. openssl enc tuyệt để hiểu cơ chế, nhưng không phải lựa chọn số một cho production.
Ba ý mang về
- KDF (PBKDF2) là cầu nối mật khẩu → khóa AES: dẫn khóa 32 byte từ mật khẩu + salt qua nhiều vòng — đo thật, mã hóa
secret.txtbằngopenssl enc -aes-256-cbc -pbkdf2 -iter 600000, giải mã đúng ra nội dung, sai mật khẩubad decrypt. - Salt làm bản mã khác nhau mỗi lần: file tự chứa
Salted__+ salt ngẫu nhiên; mã cùng file+mật khẩu hai lần ra bản mã khác (đo thật salt4113b807vs7e727047) — không lộ trùng lặp, rainbow table vô dụng. - Dùng đúng: BẮT BUỘC
-pbkdf2 -iter(thiếu là KDF yếu); CBC không tự xác thực nên cân nhắc AES-GCM/HMAC hoặc công cụ hiện đại (age/gpg); và đừng để mật khẩu lộ qua dòng lệnh (pass:).
Nguồn
- OpenSSL docs — openssl-enc (mục -pbkdf2, -iter, -pass): https://docs.openssl.org/3.0/man1/openssl-enc/
- OWASP — Password Storage / Key Derivation (PBKDF2 iterations): https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
Phần sau — bài cuối sê-ri — ta ghép hash + HMAC + thời gian thành thứ bạn dùng mỗi ngày: mã OTP 6 số của xác thực hai lớp (TOTP), tự tính bằng openssl và một script ngắn.