You've built a sleek new web application with user accounts, and now you need to store passwords. Like any security-conscious developer, you know never to store passwords in plain text. 😅
After a quick search, you discover SHA-256 — a modern cryptographic hash function from the respected SHA-2 family. It produces a 256-bit hash, seems widely used, and is built right into your framework. Problem solved, right?
Not so fast.
While SHA-256 is an excellent cryptographic hash function for many purposes, using it alone for password storage can be a mistake that could put your users at risk.
This article will explain why SHA-256 by itself fails to provide adequate password security, what attackers can do to exploit these weaknesses, and how to implement proper password security in modern applications.
Understanding SHA-256
SHA-256 is part of the SHA-2 (Secure Hash Algorithm 2) family developed by the NSA and published by NIST back in 2001. It can take input of any size and it produces a fixed 256-bit (or 32 byte) output hash.
This is how it works:
- Padding the input message to a multiple of 512 bits
- Breaking the padded message into 512-bit blocks
- Processing each block sequentially through a compression function
- Producing a final 256-bit hash value
The process creates a deterministic, seemingly random output that is computationally difficult to reverse.
Why SHA-256 Alone Isn't Enough
Back in 2008 when I was getting started professionally in the software development field, it wasn't uncommon to see SHA-256 used for password hashing at large organizations.
However, alot has changed during the past few decades, such as the advent of super powerful GPU's that can compute billions of hashes per second.
Despite SHA-256's main strengths, it's got a few characteristics that make it unsuitable as a standalone hashing option, particularly for passwords.
SHA-256 Is Designed to be Fast
SHA-256 was mainly designed for efficiency and speed. This makes it perfect for validating file integrity or digital signatures, but becomes a weakness for password hashing.
Modern GPU's can calculate billions of SHA-256 hashes per second. An NVIDIA RTX 3090 GPU for example can compute over 2 billion SHA-256 hashes per second, making brute force attacks possible.
Just for comparison sake, here's an estimate showing where it lies in terms of speed:
Algorithm |
Hashes/second (RTX 3090) |
SHA-256 |
2,000,000,000 |
SHA-256 (salted) |
1,800,000,000 |
PBKDF2-SHA256 (310,000 iterations) |
5,800 |
Bcrypt (cost=12) |
23,000 |
Argon2id (t=3, m=4096, p=1) |
3,500 |
It's a rough estimate, but it paints the picture as to why solely relying on SHA-256 for password hashing is not ideal.
In comparison, something like the more commonly Bcrypt makes way more sense in terms of security.
No Build-in Salt Mechanism
SHA-256 doesn't include any built in method for incorporating a unique salt per password. Without a salt present identical passwords will hash to identical values. This makes certain attacks possible, such as:
- Rainbow table attacks
- Cross-user attacks
- Batch cracking
No Work Factor Adjustment
Unlike modern password hashing functions, SHA-256 lacks a configurable work factor that can be increased over time as hardware becomes more powerful. This means that as computing power increases year after year, SHA-256 becomes increasingly more vulnerable with no real way to compensate.
Parallel Processing Vulnerability
SHA-256's algorithm is highly parallelizable, meaning attackers can distribute cracking attempts across multiple processors, GPU's or even specialized ASICs to speed up attacks.
And if you haven't heard of the term ASIC's before:
Application-Specific Integrated Circuits (ASICs) are custom-designed chips created for a particular use rather than for general-purpose functionality. Unlike general processors like CPUs or even GPUs, ASICs are hardwired to perform specific tasks extremely efficiently.
Proper Password Hashing
Let's take a look at a few key components of secure password storage and hashing that you should be leveraging:
1. Salting
A salt is a random value generated for each user and combined with their password before hashing. A proper salt requires:
- Uniqueness: A new random salt string for every user and password
- Sufficient length: At least 16 bytes of randomness
- Proper generation: Cryptographically secure random number generator
- Storage strategy: Cryptographically secure random number generator the salt alongside the hash, typically concatenated or in a separate field.
This helps to ensure that no user ever has the same password hash, even when they share the same passwords:
** Example
User 1: helloWorldGTASA2342
User 2: helloWorldLJLJJEIO42
In this example, two users could be sharing the helloWorld password, but because of the added salt, their final hashes will be different.
2. Key Stretching
This technique applies the hash function multiple times in a loop (iteration), making the hashing process slower (on purpose).
- Iteration count: Modern recommendations suggest 300,000+ iterations for PBKDF2-SHA256.
- Adaptive costs: Increase iterations over time as hardware improves.
- Diminishing returns: Each doubling of iterations doubles the attack cost.
3. Memory Hardness
The newest password hashing functions also require significant memory to compute, making attacks using specialized hardware much more expensive in terms of brute forcing:
- Memory costs: Forces each hashing operation to use a configurable amount of RAM.
- Time-memory tradeoff: Prevents shortcuts that reduce memory usage.
It's important to note that the more secure techniques purposely rely on being slow and taking up more resources, which again, SHA-256 is not great at.
Better alternatives
So what's a developer to do? Well lucky for us, there are better alternatives that are relatively simple to implement.
PBKDF2
Password-Based Key Derivation Function 2 applies a pseudorandom function many times to derive a key.
The following is a C# example:
// C# implementation using PBKDF2
public static string HashPassword(string password, byte[] salt, int iterations = 310000)
{
using (var pbkdf2 = new Rfc2898DeriveBytes(
password,
salt,
iterations,
HashAlgorithmName.SHA256))
{
byte[] hash = pbkdf2.GetBytes(32);
// Combine salt, iterations, and hash for storage
// Format: iterations:salt:hash
return $"{iterations}:{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}";
}
}
** Pros:
- It's built into most programming languages and frameworks
- It's FIPS-140 compliant
- Can use different hash functions (SHA-256, SHA-512, etc)
** Cons
- No memory-hardness (makes it vulnerable to specialized hardware)
- Requires very high iteration counts for security
- Parallelizable (making GPU attacks more efficient)
Bcrypt
Bcrypt was developed in 1999 specifically for password-hashing. It derives from the Blowfish cipher and it includes built-in salting. And it's pretty easy to implement:
// C# implementation using BCrypt.Net-Next package
public static string HashPassword(string password)
{
// WorkFactor 12 is the default - increase for more security
return BCrypt.HashPassword(password, workFactor: 12);
}
** Pros
- Purposefully built for password hashing
- Integrated salt generation and storage
- Configurable work force
- It's inherently resistant to GPU acceleration
** Cons
- Limited to 72 bytes of input
- Fixed memory usage (not memory-hard)
- Implementation inconsistencies across language
Argon2id
Argon2id won the 2015 Password Hashing Competition. It combines time, memory and parallelism costs.
// C# implementation using Konscious.Security.Cryptography package
public static string HashPassword(string password)
{
var salt = new byte[16];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
using (var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password)))
{
argon2.Salt = salt;
argon2.DegreeOfParallelism = 4; // Number of threads
argon2.Iterations = 3; // Time cost
argon2.MemorySize = 65536; // Memory cost in KiB (64 MB)
byte[] hash = argon2.GetBytes(32);
return $"$argon2id$v=19$m=65536,t=3,p=4${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}";
}
}
** Pros
- State of the art security design
- Configurable memory usage, time cost and parallelism
- Resistant to all known attack vectors
- Excellent time-memory tradeoff protections
** Cons
- Less widespread adoption
- Fewer mature library implementations
- Less battle-tested than Bcrypt or PBKDF2
Conclusion and Key takeaways
SHA-256 alone is insufficient for password storage in this day and age. Instead:
- Use a dedicated password function, like Bcrypt or Argon2id.
- Apply proper salting for each password generated
- Add additional security layers, such as:
- Strong password policies
- Multiple factor authentication
- Account lockout policies
- Break detection and notification
It's important for developers to learn how to implement multiple layers of security as statistically, breaking through any one layer is going to happen eventually. But breaking through 3 or 4 is much less likely.