A simple explanation of hashing, integrity checks, and why one-way functions matter in modern applications.
This article focuses on the practical side of the workflow, including where developers usually get stuck, what to verify before trusting the result, and how the topic shows up in day-to-day implementation work.
What a Hash Function Actually Does
A hash function takes input of any size, a single character, a 4 GB video file, or an entire database dump, and produces a fixed-length string of characters called a digest. Feed SHA-256 the word hello and you get a 64-character hexadecimal value. Feed it the complete works of Shakespeare and you still get exactly 64 characters. That fixed-length output is the first defining property, and it is what makes hashes so convenient as identifiers and fingerprints.
The second defining property is that the function is deterministic and one-way. The same input always produces the same digest, but you cannot run the process backwards to recover the original input from the digest alone. There is no decode button. A good cryptographic hash also has the avalanche effect: change one bit of the input and roughly half the output bits flip. Changing hello to Hello produces a digest that looks completely unrelated to the first, with no visible pattern connecting them.
Because of these properties, a hash acts like a compact fingerprint of data. If two fingerprints match, you can be extremely confident the underlying data is identical. If they differ, you know with certainty that something changed, even if only a single byte moved. That simple guarantee underpins an enormous amount of everyday web infrastructure.
Hashing Is Not Encoding, and It Is Not Encryption
These three terms get mixed up constantly, and confusing them leads to real security mistakes. Encoding, like Base64 or URL encoding, simply transforms data into a different representation so it can travel safely through a system. It is fully reversible by design and requires no key. Base64 is not security at all; anyone can decode it in a browser console in seconds. If you see a token that is just Base64, treat it as plain text.
Encryption is reversible too, but only if you hold the correct key. AES-encrypted data is meant to be turned back into its original form by an authorised party. The entire point is that the protection is two-way and controlled by a secret. Encryption answers the question of how to keep something confidential but still get it back later.
Hashing answers a different question entirely: how to verify something without ever needing to reverse it. There is no key and no way back. You hash a password to check it later by hashing the attempt and comparing digests, never by decrypting the stored value. A useful rule of thumb: if your design ever requires getting the original data back, you want encryption, not hashing.
Verifying Downloads with Checksums
One of the most common real-world uses of hashing is file integrity verification. When you download a Linux ISO or a release binary, the project usually publishes a SHA-256 checksum next to the download link. After your download finishes, you compute the hash of the file locally and compare it to the published value. On macOS or Linux you would run something like shasum -a 256 against the file, and the output should match the project value character for character.
If the two strings match, you have strong evidence the file arrived intact and was not corrupted in transit or tampered with on a mirror server. If even one byte was altered, the avalanche effect guarantees a wildly different digest, so a mismatch is immediately obvious. This is why a flaky download that produces a subtly broken file will fail the checksum rather than silently installing something corrupt.
The same idea powers deduplication and caching. Content-addressable storage systems and Git itself name objects by their hash, so identical content is stored only once and a changed file is detected instantly. When you commit code, Git computes a hash of the contents to produce the commit identifier, which is why the same content always lands at the same address.
Why MD5 and SHA-1 Are Considered Broken
MD5 and SHA-1 were workhorses for years, but both are now cryptographically broken because researchers found practical ways to create collisions. A collision is when two different inputs produce the same digest. For MD5, collisions can be generated in seconds on an ordinary laptop. For SHA-1, Google demonstrated a real collision in 2017 with two distinct PDF files sharing one digest, an attack nicknamed SHAttered.
This matters most for anything involving trust or signatures. If an attacker can craft two files with the same hash, they can get a harmless version approved or signed, then swap in a malicious version that still matches the digest. That defeats the whole purpose of using a hash to prove a file is genuine. For this reason, certificate authorities, package signing, and version control have all moved away from these algorithms for security-sensitive checks.
That said, MD5 is not entirely useless. For non-adversarial tasks like a quick checksum to detect accidental corruption or as a cache key where no attacker is trying to fool you, it is fast and perfectly acceptable. The rule is simple: never use MD5 or SHA-1 anywhere security depends on the hash being hard to forge.
SHA-256 and the Modern Family
For general-purpose cryptographic hashing today, the SHA-2 family is the workhorse, and SHA-256 is its most widely deployed member. It produces a 256-bit digest with no known practical collision attacks, and it is fast enough to hash large files without noticeable delay. It secures TLS certificates, signs software, anchors blockchains, and verifies countless downloads every day.
When SHA-256 is not the right fit, there are well-regarded alternatives. SHA-512 produces a longer digest and can actually be faster on 64-bit hardware. SHA-3, based on a completely different internal design called Keccak, exists as a structural backup so that the world is not relying on a single mathematical approach. BLAKE2 and BLAKE3 are modern options prized for raw speed while remaining secure, and BLAKE3 in particular is exceptionally fast for hashing large amounts of data.
The practical takeaway for most web developers: reach for SHA-256 by default for integrity and signature work. It is supported everywhere, from the SubtleCrypto API in browsers to the hashlib module in Python and the crypto module in Node.js, so you rarely need to pull in a third-party dependency just to compute one.
Why You Must Never Store Passwords with Plain SHA-256
It is tempting to think that since SHA-256 is a strong, modern hash, it is the right tool for storing user passwords. It is not, and this is one of the most important distinctions in this entire topic. The problem is that SHA-256 is designed to be fast, and speed is exactly what you do not want when defending passwords against a stolen database.
If your password table leaks, an attacker does not try to reverse the hashes. Instead they guess. With a single modern GPU they can compute billions of SHA-256 hashes per second, running through enormous wordlists and common passwords until the digests match. Because the hash is so fast, weak and even moderate passwords fall almost instantly. Precomputed lookup tables, known as rainbow tables, make this even cheaper for unsalted hashes.
The correct tools are deliberately slow password hashing functions: bcrypt, scrypt, and Argon2. These are designed so that computing a single hash takes a measurable fraction of a second and consumes memory, which is trivial for one legitimate login but devastatingly expensive when an attacker needs to try billions of guesses. Argon2 is the current recommended default for new systems; bcrypt remains a solid, battle-tested choice that is available in almost every language ecosystem.
Salting, and Why Two Identical Passwords Should Not Match
A salt is a unique random value generated for each user and combined with their password before hashing. Without salting, two users who both choose password123 would produce identical stored hashes, which immediately tells an attacker that cracking one reveals both. Salting ensures that even identical passwords result in completely different stored values, so each one must be attacked individually.
Salts also neutralise rainbow tables entirely. A precomputed table is only useful if it maps to the exact input being hashed, but a random per-user salt means the attacker would need a separate gigantic table for every single user, which is computationally pointless. The salt does not need to be secret; it is typically stored right alongside the hash, because its security value comes from being unique and unpredictable, not hidden.
The good news is that modern password hashing libraries handle all of this for you. When you call a bcrypt or Argon2 function, it generates a salt automatically, embeds it in the output string along with the algorithm parameters, and reads it back when verifying. You should never roll your own salting scheme by concatenating strings yourself; use the library built-in functions and let it manage the salt, cost factor, and comparison.
Living with Collisions and Choosing the Right Hash
Collisions are not a flaw of broken algorithms alone; they are a mathematical certainty for every hash function. Since the input space is infinite and the output is a fixed number of bits, some different inputs must eventually map to the same digest. The goal of a secure hash is not to make collisions impossible, which is unachievable, but to make finding one computationally infeasible within any realistic timeframe.
This is also why you should be aware of the birthday problem when sizing hashes. The effort to find any collision is far lower than you might intuitively expect, roughly the square root of the output space, which is why short hashes like 32-bit checksums collide easily and why cryptographic hashes use 256 bits or more. For a non-security identifier where an accidental clash is merely inconvenient, a shorter or faster hash is fine; for anything an adversary might attack, you need the full strength.
Putting it all together gives a clean mental checklist. Use SHA-256 or BLAKE3 for file integrity, deduplication, and signatures. Use Argon2 or bcrypt, always with a salt, for passwords. Reach for encryption, not hashing, when you need the data back. And retire MD5 and SHA-1 from anything where forgery would cause harm. Get those four decisions right and you will avoid the vast majority of hashing mistakes that show up in real production systems.