Javascript is required
Best practicesPublished 2026-07-28Updated 2026-08-088 min read

PBKDF2 Password Hashing vs Plain SHA-256

Storing passwords using a single SHA-256 hash leaves them vulnerable to rainbow table lookups and high-speed GPU brute-force attacks. Password protection relies on unique per-user random salts to eliminate precomputed tables and PBKDF2 slow-hashing iterations. This article covers salted PBKDF2 mechanics and Web Crypto code.

By: Tiny
SaltPBKDF2SHA-256Password SecurityCryptography

1. Problem: security vulnerabilities of single-iteration SHA-256 password storage

Developers wrongly assume 'SHA-256 is an irreversible hash, making passwords safe.' However, SHA-256 is optimized for high-throughput message digestion; modern GPUs calculate tens of billions of SHA-256 hashes per second. Unsalted SHA-256 hashes in stolen databases are rapidly reversed via rainbow tables or brute force.

2. Minimal reproduction: unsalted plain passwords matched by rainbow tables

This comparison demonstrates the security gap between single SHA-256 and salted PBKDF2 against precomputation attacks:

/* Insecure: single-iteration SHA-256 (no salt) */
// SHA256("123456") = "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92"
// Attackers look up this hex in a public rainbow table to recover "123456" in seconds!

/* Secure: Random Salt + PBKDF2 (600,000 iterations) */
// Generate a 16-byte random salt per user, increasing per-hash latency from nanoseconds to milliseconds.

3. Root cause: rainbow table precomputation and GPU hardware dominance

1. Rainbow tables: precomputed lookup tables mapping billions of common passwords to SHA-256 hashes. Unsalted identical passwords produce identical hashes, enabling batch database cracking. 2. GPU processing: SHA-256's lightweight logic allows massive GPU parallelization. 3. Key Derivation Functions (KDF): PBKDF2, Argon2, and bcrypt run thousands of iterations (e.g., OWASP recommends >= 600,000 iterations for PBKDF2-HMAC-SHA256) to intentionally slow down computation.

4. Recommendation: OWASP password guidance and tunable work factors

1. Unique per-user salt: generate at least 16 cryptographically random bytes. 2. Algorithm choice: prefer Argon2id, use scrypt when it is unavailable, and use PBKDF2-HMAC-SHA256 when a FIPS-140-compatible implementation is required; bcrypt is mainly for legacy systems. 3. Work factor: OWASP currently lists at least 600,000 iterations for PBKDF2-HMAC-SHA256, but calibrate on target servers and upgrade over time. 4. Store versioned parameters: persist the algorithm, salt, work factor, and hash so records can be migrated.

5. Complete code: PBKDF2 password derivation and verification with Web Crypto

This TypeScript snippet implements both record creation and verification. The same API is available in Web Crypto-capable server runtimes; password storage and verification belong on the server, with iteration counts calibrated on production-class hardware.

interface PasswordHashResult {
  saltHex: string;
  hashHex: string;
  iterations: number;
}

const DEFAULT_PBKDF2_ITERATIONS = 600_000;

function bytesToHex(bytes: Uint8Array): string {
  return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
}

function hexToBytes(hex: string): Uint8Array {
  if (!/^(?:[0-9a-f]{2})+$/i.test(hex)) throw new Error("Invalid hexadecimal data");
  return Uint8Array.from(hex.match(/.{2}/g)!, (pair) => Number.parseInt(pair, 16));
}

async function derivePassword(
  password: string,
  salt: Uint8Array,
  iterations: number,
): Promise<Uint8Array> {
  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(password),
    "PBKDF2",
    false,
    ["deriveBits"]
  );
  const bits = await crypto.subtle.deriveBits(
    {
      name: "PBKDF2",
      salt,
      iterations,
      hash: "SHA-256",
    },
    keyMaterial,
    256
  );
  return new Uint8Array(bits);
}

async function hashPasswordPBKDF2(
  password: string,
  iterations = DEFAULT_PBKDF2_ITERATIONS,
): Promise<PasswordHashResult> {
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const hash = await derivePassword(password, salt, iterations);
  return {
    saltHex: bytesToHex(salt),
    hashHex: bytesToHex(hash),
    iterations,
  };
}

async function verifyPasswordPBKDF2(
  password: string,
  stored: PasswordHashResult,
): Promise<boolean> {
  const expected = hexToBytes(stored.hashHex);
  const actual = await derivePassword(password, hexToBytes(stored.saltHex), stored.iterations);
  if (actual.length !== expected.length) return false;

  let difference = 0;
  for (let i = 0; i < actual.length; i++) difference |= actual[i] ^ expected[i];
  return difference === 0;
}

const stored = await hashPasswordPBKDF2("MySecurePass123!");
console.log(await verifyPasswordPBKDF2("MySecurePass123!", stored)); // true

6. Incorrect approaches

Hashing passwords with PBKDF2 on the client before sending (acting as a fixed plaintext hash credential). Using a single static global salt for all users. Setting PBKDF2 iteration count to 1.

7. Boundaries: client-side hashing vs. server-side verification

Password salting and PBKDF2 computation must occur on the server. Hashing client-side and sending the hash allows attackers to perform Pass-the-Hash authentication using the stolen hash directly. The client needs only HTTPS to transmit plaintext.

8. Verification

Benchmark registration and login derivation on target server hardware and calibrate iterations within the security budget. Test correct passwords, wrong passwords, malformed records, and distinct salts. The JavaScript loop merely avoids an obvious early exit; on the server, prefer a mature password library with a constant-time comparison primitive.

9. FAQ

How should I choose among PBKDF2, bcrypt, and Argon2id? Prefer Argon2id in general, use PBKDF2-HMAC-SHA256 when a FIPS-140-compatible implementation is required, and reserve bcrypt mainly for existing legacy systems.

Can I loop SHA-256 a thousand times instead? Do not invent a password hashing construction; use a mature, configurable, widely reviewed password KDF.

Does salting still help after a database leak? Yes. Unique salts prevent reuse of precomputed results across users, although weak passwords can still be guessed record by record.

10. Summary

Protecting passwords relies on slowing down attackers. High-entropy random salts prevent rainbow tables, and multi-iteration PBKDF2/Argon2 neutralizes GPU brute-force capabilities.

Sources and further reading

Specifications and references for checking the article’s technical details.

Related articles

Keep exploring

Use the related browser tool to test the workflow with your own sample data.

Open related tool