Hashing Algorithms: Vì sao không dùng SHA-256 để hash mật khẩu?
Vì sao raw SHA-256 không phù hợp để hash mật khẩu? So sánh MD5, SHA-256, Bcrypt, Argon2id và hướng dẫn password storage, migration, salt/pepper an toàn.
ShanverseCập nhật 13 phút đọc— lượt xemĐang tải lượt xem
Password Hashing
Cryptography
Câu trả lời ngắn
Điểm mấu chốt không phải SHA-256 “không đủ dài”. Output 256-bit không cứu được password có entropy thấp như 123456, password hoặc một passphrase phổ biến.
Password threat model khác file hashing
Khi server đang hoạt động bình thường, login có thể được bảo vệ bởi:
Rate limiting.
Account lockout/risk controls.
CAPTCHA.
MFA.
Bình luận
Monitoring.
Nhưng nếu attacker lấy được bảng hash, họ có thể crack offline:
Không còn API rate limit. Defense chính là làm mỗi lần đoán tốn nhiều CPU, memory và thời gian.
Hashing không phải Encryption
Password storage nên là one-way verification:
Server không cần “giải mã” mật khẩu. Nếu người dùng quên, hệ thống reset password thay vì recover plaintext.
Encryption reversible và cần decryption key; nếu key và ciphertext cùng bị lấy, toàn bộ password có thể lộ.
MD5: vừa nhanh vừa đã hỏng về collision resistance
MD5 tạo output 128-bit và có collision attacks thực tế. Không dùng MD5 cho:
Password storage.
Digital signature mới.
Security-sensitive integrity.
Với password cracking, vấn đề còn đơn giản hơn: MD5 cực nhanh. Attacker không cần tạo collision; chỉ cần thử candidate rồi so sánh đúng hash.
Collision không phải mục tiêu chính khi crack password
Ba tính chất thường bị trộn:
Collision resistance: khó tìm hai input bất kỳ cùng hash.
Preimage resistance: từ hash tìm một input khớp.
Password guessing resistance: chi phí thử các candidate có xác suất cao.
Password attack thường khai thác dictionary, breach corpus và pattern con người, không cần phá collision resistance theo nghĩa mật mã.
SHA-256: vẫn an toàn cho nhiều mục đích, nhưng quá nhanh cho password
SHA-256 vẫn là cryptographic hash quan trọng cho:
File/content integrity trong context phù hợp.
HMAC-SHA-256.
Digital signature schemes.
Merkle trees/content addressing.
Là primitive bên trong PBKDF2 hoặc protocol khác.
Vấn đề là raw SHA-256(password):
SHA-256 được tối ưu để xử lý dữ liệu nhanh trên CPU, GPU và hardware chuyên dụng. Defender hash một lần; attacker hash hàng loạt candidate song song.
Hash nhiều vòng tự chế vẫn không phải thiết kế tốt
Thiết kế này dễ sai về encoding, salt, domain separation, parameter storage và upgrade. Hãy dùng password KDF chuẩn như Argon2id, scrypt hoặc PBKDF2 theo policy.
So sánh MD5, SHA-256, Bcrypt và Argon2id
Salt là gì?
Salt là giá trị random unique cho mỗi password record.
Salt giúp:
Ngăn attacker dùng một precomputed table chung cho mọi account.
Ngăn nhìn hash để biết hai user có cùng password.
Buộc crack từng salt/hash target.
Salt không cần bí mật và thường nằm ngay trong encoded hash.
Salt không làm một lần guess chậm hơn đáng kể. Work factor và memory cost mới tăng chi phí mỗi guess.
Không tự tạo salt bằng username, timestamp hoặc Math.random(). Dùng CSPRNG của library/OS; thư viện password hashing chuẩn thường tự sinh salt.
Pepper là gì?
Pepper là secret bổ sung dùng chung cho nhiều record, được lưu ngoài database, ví dụ trong secret manager hoặc HSM.
Nếu chỉ database bị lộ nhưng pepper không lộ, attacker thiếu một secret cần thiết. Tuy nhiên:
Pepper không thay thế salt.
Rotation khó vì không thể recompute verifier nếu không có plaintext password.
Nếu application và secret store cùng bị compromise, lợi ích giảm.
Cách kết hợp phải theo thiết kế được review, thường HMAC ở lớp phù hợp.
Không nối chuỗi tùy tiện:
Dùng thư viện/pattern đã được security team phê duyệt.
Work Factor là gì?
Work factor là tham số làm verification có chủ đích tốn tài nguyên.
Mục tiêu:
Chấp nhận được cho một login hợp lệ.
Đắt khi attacker thử hàng triệu candidate.
Có thể tăng theo hardware qua thời gian.
Không copy cost từ blog rồi coi là vĩnh viễn. Benchmark trên production-like hardware, dưới peak concurrency, và đặt latency/memory budget.
Bcrypt
Bcrypt được thiết kế cho password và có cost tăng theo lũy thừa:
Tăng cost thêm 1 thường xấp xỉ nhân đôi work, nhưng latency thực tế phụ thuộc implementation/hardware.
Ưu điểm
Được triển khai rộng.
Tự chứa algorithm, cost và salt trong encoded string.
Có lịch sử vận hành lâu dài.
Tốt hơn rất nhiều so với MD5/raw SHA-256.
Hạn chế
Input password có giới hạn 72 bytes trong bcrypt truyền thống/phổ biến.
Ký tự Unicode có thể chiếm nhiều byte.
Memory cost nhỏ so với Argon2id.
Cost chỉ chủ yếu điều chỉnh CPU.
Không được truncate password âm thầm. Validate byte length theo implementation và có migration plan. Đừng tự pre-hash bằng SHA-256 rồi đưa raw bytes vào bcrypt vì encoding/null-byte và compatibility có thể tạo lỗi; nếu bắt buộc giữ bcrypt, theo đúng hướng dẫn library/security policy.
OWASP hiện khuyến nghị bcrypt chủ yếu cho hệ thống legacy chưa thể dùng Argon2id và dùng work factor ít nhất 10, nhưng cần benchmark thay vì xem một con số là universal.
Argon2 và vì sao ưu tiên Argon2id
Argon2 có ba biến thể:
Argon2d: data-dependent access, chống GPU tốt nhưng nhạy hơn với side channel.
Argon2i: data-independent access, ưu tiên side-channel resistance.
Argon2id: hybrid; lựa chọn mặc định phổ biến cho password hashing.
Ba tham số chính:
Memory-hardness buộc attacker trả chi phí RAM/bandwidth cho mỗi guess, làm parallel cracking trên GPU/ASIC đắt hơn so với fast hash.
Baseline không phải chân lý vĩnh viễn
OWASP Password Storage Cheat Sheet đưa baseline Argon2id tối thiểu m=19 MiB, t=2, p=1. RFC 9106 đưa profile mạnh hơn, trong đó lựa chọn memory-constrained là m=64 MiB, t=3.
Chọn profile bằng benchmark và threat model:
Login server có bao nhiêu concurrent verification?
Container có memory limit nào?
P95/P99 latency budget?
Attacker có thể tạo login flood không?
Có worker pool/backpressure không?
Đặt memory quá cao mà không giới hạn concurrency có thể biến password endpoint thành DoS amplifier.
Argon2 encoded string
Dạng PHC string thường chứa mọi metadata không bí mật:
Lưu nguyên chuỗi encoded:
Algorithm/variant.
Version.
Parameters.
Salt.
Derived hash.
Điều này giúp verify và nâng cấp cost sau này. Không cần cột salt thủ công nếu library đã encode đúng.
TypeScript với Argon2id
Ví dụ dùng package argon2 phổ biến trong Node.js:
Không truyền salt cố định; library tự tạo random salt.
Validate input mà không phá passphrase
Giới hạn trên bảo vệ resource abuse, không phải khuyến khích password ngắn. Chọn limit theo product/policy. Không tự trim(), lowercase hoặc đổi password mà user nhập.
Nếu áp dụng Unicode normalization, phải áp dụng nhất quán ở register/change/login, document rõ và kiểm thử compatibility. Thay đổi normalization sau này có thể làm user không đăng nhập được.
TypeScript với Bcrypt
Cost 12 chỉ là ví dụ; benchmark trên hạ tầng thật. Behavior có thể khác giữa package/version, nên dùng dependency được duy trì và test vectors.
Tại sao không tự so sánh hash string?
Dùng API verify/compare của library. Nó parse parameters, derive đúng cách và thực hiện comparison phù hợp.
Không viết:
Ngoài format mismatch, comparison tự viết có thể tạo timing signal. Constant-time compare không sửa được việc dùng thuật toán sai, nhưng vẫn là chi tiết cần giao cho library.
Raw SHA-256 có bao giờ hợp lý với secret không?
Có một nuance quan trọng: password người dùng có entropy thấp; random API token 256-bit có entropy cao.
Với token được tạo từ CSPRNG đủ dài, lưu SHA-256(token) có thể phù hợp để lookup/revoke vì brute-force token space là không khả thi. Vẫn cần:
Token đủ entropy.
Domain separation/prefix.
Constant-time verification khi cần.
Expiration, rotation và scope.
Không áp dụng kết luận này cho password do con người chọn.
Password KDF và token hashing giải hai threat model khác nhau.
PBKDF2 dùng SHA-256 có mâu thuẫn không?
Không. Raw SHA-256(password) khác PBKDF2-HMAC-SHA-256.
PBKDF2 xây một password KDF có:
Salt.
Iteration count.
HMAC construction.
Standard encoding/parameters.
PBKDF2 không memory-hard như Argon2id nhưng có thể là lựa chọn khi compliance/FIPS hoặc platform yêu cầu. Dùng parameter theo policy hiện hành, không tự đặt vài nghìn iterations.
Password Verification Flow an toàn
Mermaid
Loading diagram…
Password hash bảo vệ database breach; online protections vẫn cần để chống credential stuffing và DoS.
Migration từ MD5/SHA-256/Bcrypt sang Argon2id
Không thể biến hash cũ thành Argon2id an toàn nếu không có plaintext password.
Chiến lược opportunistic rehash:
Lưu algorithm/version trong verifier hoặc cột metadata.
Khi login, detect hash cũ.
Verify bằng legacy algorithm.
Nếu đúng, hash candidate bằng Argon2id hiện tại.
Update atomically và xóa legacy verifier.
Với account không login lâu, yêu cầu reset theo risk policy.
Nếu raw hashes bị lộ hoặc algorithm quá yếu, cân nhắc force reset, revoke sessions và incident response thay vì chờ mọi user login.
Rehash khi tăng cost
Encoded hash chứa parameters. Sau verify thành công:
Một số library có needsRehash(). Nếu không, parse bằng API đáng tin cậy thay vì regex tự chế.
Không rehash trên failed login vì không có proof rằng candidate đúng.
Chống DoS cho Password Hashing
Argon2id cố ý tốn memory/CPU; attacker có thể gửi nhiều login request.
Biện pháp:
Rate limit theo account, IP và risk signal.
Bounded worker pool/concurrency semaphore.
Queue limit và backpressure.
Request timeout hợp lý.
Memory limit có headroom.
Không chạy hash đồng bộ trên Node.js event loop.
Metrics cho queue depth, latency, OOM và failure rate.
Generic response để giảm user enumeration.
Không giảm cost xuống mức yếu chỉ để che kiến trúc thiếu capacity control.
Những lỗi thường gặp
1. SHA256(password + salt)
Vẫn quá nhanh và construction tự chế.
2. Một salt dùng cho mọi user
Mất lợi ích per-record salt.
3. Salt bí mật nhưng pepper nằm trong DB
Salt không cần bí mật; pepper phải tách khỏi DB nếu sử dụng.
4. Log password hoặc candidate
Không log plaintext, kể cả debug/error/APM breadcrumbs.
5. Bcrypt truncate 72 bytes
Hai password khác nhau sau byte 72 có thể bị coi giống nhau tùy implementation. Validate và migrate.
6. Argon2 memory quá cao không có concurrency limit
Tạo OOM/DoS.
7. Global normalization không nhất quán
User đăng ký một representation nhưng login representation khác.
8. Hard-code cost vĩnh viễn
Hardware thay đổi; policy cần version và rehash.
9. Cache password verifier/candidate sai cách
Không cache plaintext password. Hạn chế lifetime của sensitive buffers/string theo khả năng runtime.
10. Nghĩ hash mạnh sửa được password yếu
KDF tăng chi phí đoán nhưng không tạo entropy. Cần password manager, breached-password screening và MFA/passkeys.
Parameters benchmark trên production-like hardware.
Cost/memory/version nằm trong encoded verifier.
Bounded concurrency cho endpoint tốn tài nguyên.
TLS bảo vệ password khi truyền.
Rate limit, MFA/passkeys và breached-password checks.
Secret manager/HSM nếu dùng pepper.
Rehash-on-login và kế hoạch force reset.
Audit dependency/CVE và update library.
Metrics cho latency, rehash rate và failures.
Incident response nếu credential table bị lộ.
Kết luận
Không dùng raw SHA-256 cho password không phải vì SHA-256 “bị hỏng”, mà vì nó quá hiệu quả:
MD5 không phù hợp và đã hỏng cho nhiều mục tiêu mật mã. Bcrypt được thiết kế cho password nhưng có giới hạn 72 bytes và memory-hardness yếu theo tiêu chuẩn hiện đại. Argon2id cho phép điều chỉnh memory, time và parallelism, nên thường là lựa chọn tốt cho hệ thống mới.
Thiết kế an toàn gồm Argon2id/bcrypt đúng cách, salt random từng record, cost được benchmark, optional pepper tách khỏi DB, rehash/versioning, bounded concurrency, TLS, rate limiting, MFA/passkeys và quy trình reset/incident response.
1MD5 / SHA-256: fast general-purpose hash → tốt cho integrity, tệ cho password2Bcrypt: adaptive password hash → phù hợp hệ thống legacy, có giới hạn 72 bytes3Argon2id: memory-hard password hash → lựa chọn ưu tiên cho hệ thống mới
Codeplain text
1candidate password2 ↓3run hash/KDF locally4 ↓5compare with stolen verifier6 ↓7repeat without contacting server
1functionvalidatePasswordInput(password:string):void {2constbytes=newTextEncoder().encode(password).byteLength;34if (bytes ===0) thrownewError("Password cannot be empty");5if (bytes >1024) thrownewError("Password is too large");6}