Skip to content

Hash and verify a password snippet

Password hashing must be SLOW on purpose: SHA-256 finishes in nanoseconds, and a GPU farm tries billions of SHA-256 guesses per second — a password hash needs a tuned cost (CPU, memory, or both) so each guess costs milliseconds.

Password hashing must be SLOW on purpose: SHA-256 finishes in nanoseconds, and a GPU farm tries billions of SHA-256 guesses per second — a password hash needs a tuned cost (CPU, memory, or both) so each guess costs milliseconds. bcrypt, scrypt, and argon2 are the sanctioned answers; MD5, SHA-1, and plain SHA-256 for passwords are break-ins waiting to be noticed. Every correct API embeds the salt and cost IN the stored string and returns a boolean verify — never re-hash and string-compare, and never invent a salt format.

Runnable recipe · 12 languagesOpen the bcrypt tool →
Security Hardeningpasswordbcryptargon2scryptkdfhashingsalt

Every language

12 implementations, copy-ready. One at a time with syntax highlighting, or all inline.

JSJavaScript
const bcrypt = require('bcryptjs'); // bcryptjs = pure JS; bcrypt = faster native

async function hashPassword(plain) {
  return bcrypt.hash(plain, 12); // $2b$12$<salt+digest> — one self-describing string
}

async function verifyPassword(plain, stored) {
  return bcrypt.compare(plain, stored); // boolean; salt+cost parsed from `stored`
}

The 12 is the log2 round count — +1 DOUBLES the work (12 ≈ 250 ms of CPU per hash). Never re-hash the input and compare with ===: bcrypt.compare pulls the salt out of the stored string, which is the whole point of the format. argon2id via the argon2 npm package is the modern pick where native builds are acceptable.

TSTypeScript
import { hash, compare } from 'bcryptjs'; // or: import * as argon2 from 'argon2'

export async function hashPassword(plain: string): Promise<string> {
  return hash(plain, 12);
}

export async function verifyPassword(plain: string, stored: string): Promise<boolean> {
  return compare(plain, stored);
}

The stored value is just `string` — no separate salt column — because both bcrypt and argon2 embed salt+params in the output ($2b$12$… vs the PHC string $argon2id$v=19$m=65536,t=3,p=4$…). That self-describing format is the contract; inventing your own salt storage breaks every future migration.

GoGo
import "golang.org/x/crypto/bcrypt"

const passwordCost = 12 // log2 rounds — the ONE place to tune

func HashPassword(pw string) (string, error) {
	b, err := bcrypt.GenerateFromPassword([]byte(pw), passwordCost)
	return string(b), err
}

func VerifyPassword(pw, stored string) bool {
	return bcrypt.CompareHashAndPassword([]byte(stored), []byte(pw)) == nil // nil = match
}

bcrypt.DefaultCost is 10 — name your own cost constant in exactly one var so tuning is a one-line change. CompareHashAndPassword returns an error, not a bool: it parses salt+cost from the stored hash; there is no string-compare variant to misuse. argon2id lives in the same module, golang.org/x/crypto/argon2, if you need memory hardness.

RsRust
use argon2::{
    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
    Argon2,
};

fn hash_password(plain: &str) -> Result<String, argon2::password_hash::Error> {
    let salt = SaltString::generate(&mut OsRng);
    Ok(Argon2::default() // Argon2id v19: m=19456 KiB, t=2, p=1 (OWASP baseline)
        .hash_password(plain.as_bytes(), &salt)?
        .to_string())
}

fn verify_password(plain: &str, stored: &str) -> Result<bool, argon2::password_hash::Error> {
    let parsed = PasswordHash::new(stored)?;
    Ok(Argon2::default().verify_password(plain.as_bytes(), &parsed).is_ok())
}

to_string() emits the PHC format — $argon2id$v=19$m=19456,t=2,p=1$<b64 salt>$<b64 hash> — and PasswordHash::new parses those params back out on verify, so you never store them separately. Argon2::default() tracks the RustCrypto/OWASP-recommended defaults; override with Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::new(m_kib, t, p, None)?) when you tune.

PHPPHP
$stored = password_hash($pw, PASSWORD_DEFAULT); // argon2id if compiled in, else bcrypt

if (password_verify($pw, $stored)) {              // boolean, constant-time inside
    if (password_needs_rehash($stored, PASSWORD_DEFAULT)) {
        $stored = password_hash($pw, PASSWORD_DEFAULT); // transparent upgrade on login
    }
    // credentials OK — proceed with the session
}

THE canonical password API in any stdlib: PASSWORD_DEFAULT silently moves to stronger algorithms across PHP releases, and password_needs_rehash + re-hash at login is the built-in migration path — no legacy-hash column, no reset campaign. md5($pw), sha1($pw), or crypt() with a hand-rolled salt here are break-ins waiting to be noticed.

PyPython
import hashlib, hmac, os

def hash_password(pw: str) -> str:
    salt = os.urandom(16)
    dk = hashlib.scrypt(pw.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32)
    return f"scrypt$16384$8$1${salt.hex()}${dk.hex()}"  # you own this envelope

def verify_password(pw: str, stored: str) -> bool:
    algo, n, r, p, salt_hex, dk_hex = stored.split('$')
    dk = hashlib.scrypt(pw.encode(), salt=bytes.fromhex(salt_hex),
                        n=int(n), r=int(r), p=int(p), dklen=len(dk_hex) // 2)
    return hmac.compare_digest(dk.hex(), dk_hex)  # constant-time, never ==

hashlib.scrypt is in the STDLIB (no pip install) but raw: you must persist salt+params yourself, hence the hand-built envelope string — get that format wrong once and old hashes stop verifying. The turnkey routes: argon2-cffi (argon2.hash → PHC string, argon2.verify → bool), the bcrypt package, or passlib wrapping them all behind one API.

C#C#
using Microsoft.AspNetCore.Identity;

var hasher = new PasswordHasher<AppUser>();

string stored = hasher.HashPassword(user, pw); // PBKDF2-HMAC-SHA256, 100k iterations

var result = hasher.VerifyHashedPassword(user, stored, pw);
if (result == PasswordVerificationResult.SuccessRehashNeeded)
    stored = hasher.HashPassword(user, pw);     // upgrade the old hash at login

PasswordHasher<T> is PBKDF2, not bcrypt — still a tuned-cost KDF — and its output embeds a version byte (V3 = PBKDF2-SHA256/100k; V2 = the legacy weaker format). VerifyHashedPassword returns an enum, and SuccessRehashNeeded IS the rehash-on-login pattern: re-hash when it fires. Non-Identity stacks use BCrypt.Net-Next (BCrypt.Net.BCrypt.HashPassword/Verify).

JvJava
import org.mindrot.jbcrypt.BCrypt;

static String hashPassword(String plain) {
    return BCrypt.hashpw(plain, BCrypt.gensalt(12)); // $2a$12$...
}

static boolean verifyPassword(String plain, String stored) {
    return BCrypt.checkpw(plain, stored);
}

// Spring Security alternative — the {bcrypt} id prefix rides in the string:
// PasswordEncoder e = PasswordEncoderFactories.createDelegatingPasswordEncoder();
// e.encode(pw);            // "{bcrypt}$2a$12$..."
// e.matches(pw, stored);   // e.upgradeEncoding(stored) → re-hash on login

On a Spring stack the DelegatingPasswordEncoder route wins: it stores an {id} prefix ({bcrypt}, {argon2}, {pbkdf2}) so you can adopt a stronger algorithm later without invalidating existing users — upgradeEncoding() flags the rehash-on-login. Raw jBCrypt has no such versioning; note "{bcrypt}$2a$..." and "$2a$..." are different strings, so pick one convention at the start.

SwSwift
import CommonCrypto
import Foundation

func deriveKey(password: String, salt: Data, rounds: UInt32 = 100_000) -> Data {
    var derived = Data(repeating: 0, count: 32)
    let pw = password.data(using: .utf8)!
    let status = derived.withUnsafeMutableBytes { dk in
        salt.withUnsafeBytes { s in
            pw.withUnsafeBytes { p in
                CCKeyDerivationPBKDF(
                    CCPBKDFAlgorithm(kCCPBKDF2),
                    p.bindMemory(to: Int8.self).baseAddress, pw.count,
                    s.bindMemory(to: UInt8.self).baseAddress, salt.count,
                    CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256), rounds,
                    dk.bindMemory(to: UInt8.self).baseAddress, 32)
            }
        }
    }
    precondition(status == kCCSuccess)
    return derived
}

Honest gap: CryptoKit has HKDF but NO password KDF — no bcrypt/argon2/scrypt anywhere in the framework, so there is no pure-Swift sanctioned answer. CommonCrypto's CCKeyDerivationPBKDF (PBKDF2, 100k+ rounds) is the closest-to-stdlib route, but it returns raw derived bytes: YOU persist salt+rounds beside the hash and compare constant-time. For real password storage, bind the argon2 or bcrypt C libraries via SPM.

KtKotlin
import org.mindrot.jbcrypt.BCrypt

object PasswordHasher {
    private const val COST = 12 // log2 rounds; 10..12 is the sane band

    fun hash(plain: String): String = BCrypt.hashpw(plain, BCrypt.gensalt(COST))

    fun verify(plain: String, stored: String): Boolean = BCrypt.checkpw(plain, stored)
}

Same JVM jBCrypt as the Java recipe, wrapped in an object so the cost lives in exactly one const. If Spring is on the classpath, prefer PasswordEncoderFactories.createDelegatingPasswordEncoder() — the {id} prefix convention is what future-proofs storage against an algorithm switch (no mass password reset).

RbRuby
require 'bcrypt'

stored = BCrypt::Password.create(pw, cost: 12)  # $2a$12$<salt+digest>

def valid_password?(stored, pw)
  BCrypt::Password.new(stored) == pw  # == on the Password object, not on strings
end

The == is safe ONLY because the left side is a BCrypt::Password — its == runs bcrypt's constant-time comparison internally, re-deriving with the embedded salt. BCrypt::Password.new(stored) == pw, never stored == computed_hash (timing-leaky) and never BCrypt::Engine.hash(pw) + ===. cost: is the log2 round count; default is 10.

ZigZig
const std = @import("std");
const c = @cImport(@cInclude("argon2.h")); // build.zig: linkSystemLibrary("argon2")

fn hashPassword(allocator: std.mem.Allocator, password: []const u8) ![]u8 {
    var salt: [16]u8 = undefined;
    std.crypto.random.bytes(&salt);

    const enc_len = c.argon2_encodedlen(2, 19456, 1, salt.len, 32, c.Argon2_id);
    const out = try allocator.alloc(u8, @intCast(enc_len));
    const rc = c.argon2id_hash_encoded(
        2,      // t: iterations
        19456,  // m: KiB of memory (the memory-hard knob)
        1,      // p: parallelism
        password.ptr, password.len,
        &salt, salt.len,
        32,     // hash length
        out.ptr, enc_len);
    if (rc != c.ARGON2_OK) return error.Argon2Failed;
    return out; // $argon2id$v=19$m=19456,t=2,p=1$... — params ride in the string
}

fn verifyPassword(password: []const u8, stored: []const u8) bool {
    return c.argon2id_verify(stored.ptr, password.ptr, password.len) == c.ARGON2_OK;
}

The honest answer for Zig: std.crypto ships hashes, AEADs, even Ed25519 — but NO password KDF (no bcrypt/argon2/scrypt; std.pdb is a debugger format, not a KDF), and there is no stdlib fallback worth showing. The route is this C binding to the reference argon2 implementation; argon2id_verify reads the params back out of the stored PHC string and compares constant-time inside.

Keep going

Try the interactive bcrypt tool →