← All articles

Storing password hashes: Argon2id, bcrypt and what the output means

Hashing is the part of passwords you do not have to remember — which is why it is also the part people get wrong most often.

Hashing is not encryption

Encryption is reversible: with the right key, you get the original back. Hashing is a one-way function — the output cannot be turned into the input, and there is no key to steal. That is exactly what a service needs: when you log in, it hashes what you typed and compares the result with what it stored. It never needs to know your password, and a database leak does not hand your password to the attacker.

What actually gets stored

Modern hash strings carry everything needed to verify later. For Argon2id the standard format is the PHC string, for example:

$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHRzYWx0c2E$3Jk…

  • argon2id — the algorithm, in this case the memory-hard variant recommended by the Password Hashing Competition.
  • m=65536 — memory cost, in KiB: 64 MiB per hash attempt.
  • t=3 — time cost: three passes over that memory.
  • p=4 — parallelism, i.e. how many lanes are used at once.
  • The two following fields are the salt and the hash, both base64. The salt is random and unique per stored password.

You can produce and inspect exactly this string in the Hash tab, and check a password against one in the Verify tab.

Why the salt matters more than the algorithm choice

A salt is a random value mixed into the input before hashing. Because it is unique per user, two people with the same password end up with two completely different hashes, and an attacker cannot compute one table of pre-hashed passwords and reuse it across your entire database. The salt is not secret, and it does not need to be: its job is to make each guess cost work, individually.

Choosing between Argon2id and bcrypt

  • Argon2id — the default choice for new work. It is memory-hard, so attacking it with a GPU farm or custom silicon is far more expensive than with a fast hash. A reasonable starting point is 64 MiB, three iterations and one to four lanes; tune upwards while a single hash still takes a fraction of a second on your hardware.
  • bcrypt — still perfectly respectable, and still what many existing systems expect. A cost factor of 12 is a normal baseline. Its well-known limitation is that it only reads the first 72 bytes of the input: anything beyond that is ignored, so a password and a 200-byte passphrase with the same first 72 bytes verify identically. If you generate 128-character passwords, that matters.

Verify, never "recover"

There is no legitimate operation that turns a hash back into a password. If a tool claims to "decrypt" hashes, it is either running a guess-and-check search or lying. Verification is the only operation that exists: hash the candidate with the same parameters and salt and compare. Implementations should compare in constant time so that a partial match cannot be detected by timing — the server in this project does exactly that, and the Verify tab demonstrates the behaviour end to end.

Mistakes worth avoiding

  • Plain SHA-256 or MD5 for passwords. They are designed to be fast, which is the opposite of what you want; a modern GPU tries billions per second.
  • No salt, or one shared salt. It makes every identical password identical in your database and enables precomputed attacks.
  • Storing the password "encrypted" for convenience. Anything your application can decrypt, an attacker with your server can decrypt too.
  • Silently truncating long passwords. If you use bcrypt, be explicit about the 72-byte limit rather than letting it surprise users with long passphrases.
  • Rolling your own crypto. Use a reviewed library — Go's x/crypto/argon2 and x/crypto/bcrypt, libsodium, or your language's equivalent. This site's hashing is powered by those vetted Go packages.

Where this site's hasher fits

The Hash and Verify tabs are built for one-off, hands-on work: producing a fixture for a test suite, checking that a row in your database matches what you think it does, or confirming that a migration from bcrypt to Argon2id behaves as intended. It is deliberately not a login system. Hashing here is performed by this project's own Go server over HTTPS, the password is used once and discarded, and request bodies are never logged.

Open the hashing tools — Argon2id with PHC output, bcrypt with a cost factor, and a Verify tab that answers match or no match.