Hashing: an overview
Let's see how hashing can be useful and where it can fail.
Hashing is the act of running data through a one-way function that produces a fixed-length fingerprint (the hash): the same input always gives the same output, but we cannot reverse it in order to recover the input.
Since changing even a single bit of the input data would result in a noticeably different output of the hashing function, we use it for storing passwords, checking the integrity of files and data structures.
One other important aspect of hashing functions is that good ones make it practically impossible for two inputs to generate the same output (that is called a “collision”).
Let’s consider this (very simple) hashing function below, which would easily generate collisions:
function simpleHash(text: string): number { let sum = 0; for (const character of text) { // charCodeAt returns the UTF-16 code of a character sum += character.charCodeAt(0); } return sum % 10;}
simpleHash('cat'); // (99 + 97 + 116) % 10 = 2simpleHash('act'); // (97 + 99 + 116) % 10 = 2 (collision!)- SHA-256: a major one, arguably the industry standard, used by Bitcoin for block mining and Git on commits;
- SHA-512: from the same family of SHA-256, but with a larger output, which provides a bigger security margin against collisions; it also works with 64-bit words, which can be faster on modern hardware;
- bcrypt: widely used for password hashing, especially because it works with Salt by default (see Salt: storing passwords safely);
- Argon2id: the industry standard and the one recommended by OWASP: slow and memory-hungry by design.
Fast hashing algorithms such as SHA-256 are not considered suitable for password storage, because the faster the algorithm the more attempts an attacker can make per second. Using slow, memory-hard algorithms makes brute-force attacks significantly more difficult, expensive, and time-consuming.