Encoding, hash, encryption, signing: bốn thứ hay bị nhầm
Bắt đầu Cryptography nhé. Trong shop-app, đồng nghiệp lưu số điện thoại khách hàng dạng Base64 vào DB và bảo "đã mã hóa rồi". Vậy có ổn không?
Không ổn. Base64 không phải mã hóa. Chương này dùng project shop-app xuyên suốt: lưu password, mã hóa card token và PII (dữ liệu cá nhân), ký webhook và JWT. Bài đầu tiên làm rõ bốn khái niệm hay bị trộn lẫn.
1. Bài toán
shop-app có bảng customer:
id | email | phone_b64
1 | an@shop.example | MDkwMTIzNDU2Nw==
Ai có quyền đọc DB (DBA, bản backup bị lộ, log SQL) đều thấy cột phone_b64.
2. Vấn đề
echo "MDkwMTIzNDU2Nw==" | base64 -d
# 0901234567
- Base64 không có key. Ai cũng decode được trong 1 giây.
- Nó chỉ đổi cách biểu diễn bytes thành ký tự ASCII để truyền qua JSON, HTTP header, email.
- Gọi nó là "mã hóa" tạo cảm giác an toàn giả.
3. Giải pháp: phân biệt bốn thao tác
| Thao tác | Mục đích | Có key? | Đảo ngược? | Ví dụ |
|---|---|---|---|---|
| Encoding | Đổi định dạng biểu diễn | Không | Có, ai cũng làm được | Base64, Hex, URL encoding, UTF-8 |
| Hashing | Tạo "dấu vân tay" cố định độ dài, kiểm tra toàn vẹn | Không | Không (one-way) | SHA-256, SHA-3 |
| Encryption | Giữ bí mật (confidentiality) | Có | Có, chỉ người có key | AES-GCM, RSA-OAEP |
| Signing / MAC | Chứng minh ai gửi và không bị sửa (integrity, authenticity) | Có | Không cần đảo ngược, chỉ verify | HMAC-SHA256, RS256, ES256 |
Câu hỏi để chọn đúng:
- Cần đọc lại giá trị gốc, và chỉ người có quyền được đọc? → Encryption (bài 13.4).
- Chỉ cần so sánh, không bao giờ cần giá trị gốc (password)? → Password hashing (bài 13.3).
- Cần chắc dữ liệu không bị sửa và đúng người gửi (webhook, JWT)? → MAC / chữ ký (bài 13.2, 13.5).
- Chỉ cần nhét bytes vào JSON/URL? → Encoding.
4. Code từng bước
Bước 1: Dependency. Không cần gì thêm, JCA (Java Cryptography Architecture) có sẵn trong JDK: java.security, javax.crypto.
Bước 2: Config. Không có. Key thật sẽ đến từ biến môi trường / KMS (bài 13.6), ví dụ ${PII_KEY}.
Bước 3: Code Java, chạy cả bốn thao tác trên cùng một chuỗi:
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.HexFormat;
public class FourOps {
public static void main(String[] args) throws Exception {
byte[] data = "0901234567".getBytes(StandardCharsets.UTF_8);
// 1. Encoding: đảo ngược được, không bí mật
String b64 = Base64.getEncoder().encodeToString(data);
String back = new String(Base64.getDecoder().decode(b64), StandardCharsets.UTF_8);
// 2. Hashing: một chiều, cùng input -> cùng output
byte[] digest = MessageDigest.getInstance("SHA-256").digest(data);
System.out.println(HexFormat.of().formatHex(digest));
// 3. Encryption (AES-GCM): xem bài 13.4
// 4. MAC / Signature: xem bài 13.2 và 13.5
}
}
Randomness: mọi thứ trong crypto (key, IV, salt, token reset password) cần số ngẫu nhiên không đoán được.
import java.security.SecureRandom;
public final class Tokens {
private static final SecureRandom RNG = new SecureRandom(); // thread-safe, tái sử dụng
// Token reset password: 32 bytes = 256 bit
public static String resetToken() {
byte[] buf = new byte[32];
RNG.nextBytes(buf);
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
}
}
Bước 4: Test bằng curl
curl -X POST https://api.shop.example/auth/forgot-password \
-H "Content-Type: application/json" -d '{"email":"an@shop.example"}'
# Email nhận link: /reset?token=q3Zr...43 ký tự, không đoán được
5. Cách hoạt động
"0901234567"
|-- Base64 ------------> "MDkwMTIzNDU2Nw==" (decode lại được, không cần key)
|-- SHA-256 -----------> 32 bytes cố định (không quay lại được)
|-- AES-GCM + key -----> ciphertext (chỉ ai có key mới giải được)
|-- HMAC + key --------> tag 32 bytes (gửi kèm data, bên nhận verify)
Random (LCG, seed 48 bit):
seed -> x1 -> x2 -> x3 ... biết vài output -> tính ngược ra seed -> đoán hết token
SecureRandom:
lấy entropy từ OS (/dev/urandom, CryptGenRandom) -> không đoán được
6. So sánh: nguồn ngẫu nhiên trong Java
| java.util.Random | ThreadLocalRandom | SecureRandom | UUID.randomUUID() | |
|---|---|---|---|---|
| Đoán được? | Có (LCG) | Có | Không (CSPRNG) | Không, dùng SecureRandom bên trong |
| Tốc độ | Nhanh | Rất nhanh | Chậm hơn, đủ dùng | Nhanh |
| Entropy | 48 bit seed | 64 bit | Tùy độ dài bạn lấy | 122 bit (v4) |
| Dùng cho | Game, simulation, test | Shuffle, load balancing | Key, IV, salt, token | ID; token tạm được nhưng 32 bytes SecureRandom tốt hơn |
Lưu ý: UUID v1 chứa MAC address và timestamp, đoán được. UUID v7 (theo thời gian, hợp làm primary key) có phần đầu là timestamp, nên không dùng làm secret.
7. Khi nào nên dùng / không nên dùng
- Base64: nên dùng để truyền bytes (ciphertext, signature) qua JSON/header. Không bao giờ coi là bảo mật.
- SecureRandom: luôn dùng cho mọi giá trị bảo mật. Không gọi
setSeedvới giá trị cố định. - Random: không dùng cho OTP, token, mật khẩu tạm.
- Không tự chế thuật toán ("XOR với key rồi Base64"). Dùng thuật toán chuẩn qua JCA.
phone nên hash hay encrypt? Hash thì có tìm kiếm được không?Vậy số điện thoại mình hash SHA-256 cho chắc, khỏi lo key luôn, được không? Nhưng chăm sóc khách hàng vẫn cần xem số để gọi lại.
Bạn tự trả lời rồi đó: nhân viên cần đọc lại số điện thoại, nên hash là sai công cụ. Hash một chiều, không lấy lại số gốc được. Thêm nữa, số điện thoại Việt Nam chỉ có khoảng 109 giá trị, SHA-256 tính cực nhanh, nên kẻ tấn công thử hết trong vài phút:
for phone in 0900000000..0999999999:
if sha256(phone) == leaked_hash: found!
Cách đúng cho shop-app:
- Cột
phone_enc: mã hóa AES-GCM (bài 13.4) để đọc lại khi cần. - Nếu cần tìm theo số điện thoại: thêm cột
phone_idx = HMAC-SHA256(indexKey, phone)(blind index, bài 13.2). Có key bí mật nên không brute-force được nếu chỉ lộ DB.
Mình đang dùng UUID.randomUUID() làm token reset password và lưu nguyên token vào bảng. Như vậy có sao không?
Bạn hiểu gần đúng, chỉ cần chỉnh hai điểm.
- Độ ngẫu nhiên:
randomUUID()dùngSecureRandom, 122 bit ngẫu nhiên, đủ để không đoán được. Dùng được. 32 bytes SecureRandom thì gọn và rõ ý đồ hơn. - Lưu nguyên token là vấn đề: ai đọc được DB (backup lộ, SQL injection) sẽ dùng token để đổi password của người khác. Hãy lưu
SHA-256(token). Token đã có entropy cao nên SHA-256 thường là đủ, không cần bcrypt như password.
String token = Tokens.resetToken(); // gửi qua email
String stored = sha256Hex(token); // lưu DB, kèm expires_at = now + 15 phút
// Khi user bấm link: tìm theo sha256Hex(tokenTrongUrl), kiểm tra hết hạn, dùng 1 lần rồi xóa
Phỏng vấn hỏi encoding, hashing, encryption khác nhau thế nào thì trả lời sao?
Q1. Encoding, hashing, encryption, signing khác nhau thế nào?
🇻🇳 Tiếng Việt:
Encoding như Base64 chỉ đổi cách biểu diễn, không có key, ai cũng decode được. Hashing như SHA-256 là hàm một chiều, dùng để kiểm tra toàn vẹn. Encryption như AES dùng key để giữ bí mật và giải mã lại được. Signing hoặc MAC dùng key để chứng minh dữ liệu không bị sửa và đến từ đúng người gửi.
🇬🇧 English:
Encoding such as Base64 only changes representation; there is no key and anyone can decode it. Hashing such as SHA-256 is a one-way function used for integrity checks. Encryption such as AES uses a key to keep data confidential and can be reversed by the key holder. Signing or a MAC uses a key to prove the data was not modified and came from the expected sender.
Q2. Vì sao không dùng java.util.Random để sinh token?
🇻🇳 Tiếng Việt:
Random là LCG với seed 48 bit, quan sát vài output là tính ra được seed và đoán các token tiếp theo. Với token, key, salt, IV em dùng SecureRandom, là CSPRNG lấy entropy từ hệ điều hành.
🇬🇧 English:
Random is a linear congruential generator with a 48-bit seed, so after observing a few outputs an attacker can recover the seed and predict future tokens. For tokens, keys, salts and IVs I use SecureRandom, a CSPRNG seeded from the operating system.
Q3. Lưu PII như số điện thoại thì hash hay encrypt?
🇻🇳 Tiếng Việt:
Nếu cần đọc lại thì phải encrypt, ví dụ AES-GCM với key trong KMS. Hash không dùng được vì không đảo ngược và không gian số điện thoại nhỏ nên dễ brute-force. Nếu cần tìm kiếm, em thêm một blind index bằng HMAC với key riêng.
🇬🇧 English:
If we need to read it back, we must encrypt it, for example with AES-GCM and a key held in a KMS. Plain hashing does not work because it is irreversible and the phone number space is small enough to brute-force. If we need lookups, I add a blind index computed with HMAC and a separate key.
Cách nhớ ngắn gọn
- Encoding = định dạng. Hash = vân tay. Encryption = bí mật. Signature/MAC = ai gửi, có bị sửa không.
- Base64 không phải mã hóa.
- Giá trị bảo mật → SecureRandom, không bao giờ Random.
- Cần đọc lại → encrypt; chỉ cần so sánh → hash.
Tự kiểm tra
Base64 có phải encryption không? Vì sao?
Không. Không có key, ai cũng decode được. Nó chỉ đổi bytes thành ký tự ASCII.
Muốn chứng minh webhook không bị sửa thì dùng hash, encryption hay MAC?
MAC (HMAC) hoặc chữ ký số. Hash không có key nên kẻ sửa dữ liệu cũng tính lại được hash.
Vì sao SHA-256 số điện thoại không bảo vệ được dữ liệu?
Không gian chỉ khoảng 10^9 giá trị, SHA-256 rất nhanh, brute-force hết trong vài phút.
UUID v7 có dùng làm token bí mật được không?
Không nên. Phần đầu là timestamp, giảm độ ngẫu nhiên và lộ thời điểm tạo. Dùng SecureRandom 32 bytes.
Token reset password nên lưu thế nào trong DB?
Lưu SHA-256 của token, kèm thời hạn ngắn, dùng một lần rồi xóa.