Skip to content
HexaTransfer
Back to blog
Encryption & Security

Key Derivation Functions: PBKDF2, Argon2 & scrypt Compared

Compare key derivation functions for password-based encryption. PBKDF2, Argon2, and scrypt strengths and trade-offs for secure file transfer applications.

For password-derived encryption keys in 2026, Argon2id is the recommended choice (PHC winner, OWASP's top pick, actively defended against GPU and ASIC attackers), scrypt is a solid second (memory-hard, widely deployed in cryptocurrencies), and PBKDF2-SHA-256 at 600,000+ iterations remains acceptable for compatibility but offers minimal GPU resistance. For file transfer apps where users enter a password to protect a shared file, Argon2id with 3 iterations, 64 MiB memory, and 4 parallelism is the modern baseline. PBKDF2 survives because it's built into the Web Crypto API and requires no WASM dependency. Here's how the three differ and when each makes sense.

Comparison Table

| Property | PBKDF2 | scrypt | Argon2id | |---|---|---|---| | Year introduced | 2000 (RFC 2898) | 2009 (RFC 7914) | 2015 (PHC winner) | | Memory-hard | No | Yes | Yes | | Parameter flexibility | Iterations only | N, r, p | time, memory, parallelism | | GPU resistance | Weak | Moderate | Strong | | ASIC resistance | Very weak | Moderate | Strong | | Browser-native (Web Crypto) | Yes | No | No | | OWASP 2024 recommendation | Acceptable fallback | Acceptable | Preferred | | Typical browser cost (modern hardware) | 600,000 iter = ~500 ms | N=2^17 = ~800 ms | 3 iter, 64 MiB = ~1 s |

Why Memory-Hard Matters

The threat model for password-based encryption is offline brute force. An attacker grabs the ciphertext plus salt, runs through a password dictionary, and tries to derive a key that decrypts successfully. The defense is making each attempt expensive.

PBKDF2 makes each attempt expensive only in CPU time (SHA-256 iterations). Modern GPUs run billions of SHA-256 operations per second; a gaming GPU can test 10-100 million PBKDF2-SHA-256-600000 guesses per day. ASIC attackers do orders of magnitude better.

Memory-hard functions (scrypt, Argon2) require a fixed amount of memory per attempt. GPUs and ASICs have limited memory bandwidth, so per-device parallelism is bounded. A 64 MiB memory requirement means a GPU with 16 GB of VRAM can run at most 256 parallel guesses, not millions. The economic cost of brute force rises by 2-3 orders of magnitude.

PBKDF2: The Legacy Default

PBKDF2 (RFC 2898) iterates a pseudorandom function, typically HMAC-SHA-256 or HMAC-SHA-512, over the password and salt. The iteration count is the only tunable.

const passwordKey = await crypto.subtle.importKey(
  "raw", new TextEncoder().encode(password),
  "PBKDF2", false, ["deriveKey"]
);
const aesKey = await crypto.subtle.deriveKey(
  {
    name: "PBKDF2",
    salt,  // 16 random bytes
    iterations: 600000,
    hash: "SHA-256",
  },
  passwordKey,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"]
);

OWASP 2023 recommends at minimum 600,000 iterations of PBKDF2-SHA-256. NIST SP 800-132 is older and less specific. Some specs (e.g., LastPass's 2018 default of 100,100) are considered too low in 2026.

Advantages: built into Web Crypto, no WASM, FIPS-validated, supported in TLS 1.3 session resumption, works in Node and browsers identically.

Limitations: no memory hardness, vulnerable to GPU and ASIC acceleration. Doubling iterations doubles attacker cost but also doubles legitimate user cost. At some point legitimate users refuse to wait and you cap iterations.

scrypt: The First Memory-Hard Deployment

scrypt (RFC 7914) was invented by Colin Percival in 2009 for Tarsnap. It mixes password material through a large memory buffer, forcing the attacker to hold that buffer during each guess.

Three parameters:

  • N: cost factor (typically 2^14 to 2^20). Memory use is roughly 128 * N * r bytes.
  • r: block size (typically 8). Affects memory and GHASH iteration count.
  • p: parallelization (typically 1). Higher values speed up legitimate computation but also speed up attackers; usually leave at 1.

OWASP recommends N=2^17, r=8, p=1 as a baseline, which consumes ~128 MiB and runs in about 800 ms on modern hardware.

scrypt isn't in the Web Crypto API. In JavaScript, use scrypt-js, @noble/hashes, or libsodium.js. Litecoin and Dogecoin use scrypt as their proof-of-work, which has incentivized ASIC development specifically for scrypt, eroding its original asymmetric advantage against ASICs somewhat.

Argon2id: The 2026 Default

Argon2 won the Password Hashing Competition in 2015. Three variants: Argon2d (fastest, data-dependent, side-channel vulnerable), Argon2i (data-independent, slower), Argon2id (hybrid, recommended for most use). RFC 9106 standardized it in 2021.

Three parameters:

  • t (time): iterations through memory. Typical 2-3.
  • m (memory): memory in KiB. Typical 65536 (64 MiB) or higher.
  • p (parallelism): degree of parallelism. Typical 1-4.

OWASP 2024 baseline: t=2, m=19456 (19 MiB), p=1 for minimum, and t=3, m=65536 (64 MiB), p=4 for stronger protection.

import { argon2id } from '@noble/hashes/argon2';
import { utf8ToBytes } from '@noble/hashes/utils';

const derivedKey = argon2id(utf8ToBytes(password), salt, {
  t: 3, m: 65536, p: 4, dkLen: 32
});

Or via argon2-browser (WASM):

import argon2 from 'argon2-browser';
const hash = await argon2.hash({
  pass: password, salt,
  type: argon2.ArgonType.Argon2id,
  time: 3, mem: 65536, parallelism: 4, hashLen: 32
});

Argon2id defeats GPU attackers more effectively than scrypt because its memory access pattern is less amenable to bulk memory designs. ASICs for Argon2 exist in research but aren't economically deployed at attacker scale yet.

Picking Parameters for Your App

The calibration method: pick the longest wait your users tolerate (usually 500 ms to 2 seconds), measure on the slowest target device, and set parameters to hit that budget.

For HexaTransfer-style password-protected file shares, where the derivation happens once on upload and once on download, 1-2 seconds is acceptable. Parameters:

  • PBKDF2-SHA-256: 600,000-1,200,000 iterations
  • scrypt: N=2^17, r=8, p=1
  • Argon2id: t=3, m=65536, p=4

For login systems where the user waits after typing their password, 300-500 ms is the UX ceiling. Parameters halve roughly. For batch scenarios where the user doesn't wait (e.g., background re-encryption), crank up to 3-5 seconds.

Salt Management

All three KDFs need a salt. Rules:

  • 16 random bytes minimum
  • Generated via crypto.getRandomValues(), never Math.random()
  • Unique per password (if Alice and Bob use the same password, their salts should differ so derived keys differ)
  • Not secret — store it alongside the ciphertext

Pepper (a secret added to all derivations) is sometimes discussed. For file transfer where the "server" is a dumb blob store, pepper adds no value since there's no server-side secret. For account-based systems, a server-side pepper stored separately from the password database makes database dumps less useful to attackers.

Migrating Between KDFs

If you have an existing deployment on PBKDF2 and want to move to Argon2id:

  • Store the KDF identifier in the ciphertext metadata ("kdf": "pbkdf2-sha256-600000" or "kdf": "argon2id-3-65536-4")
  • On new uploads, use Argon2id
  • On decrypt, read the KDF identifier and use the matching function
  • Never blindly upgrade old ciphertexts; you'd need the password to re-derive

LastPass, 1Password, and Bitwarden have all gone through this migration. All three now default to PBKDF2 at 600,000+ iterations, with Argon2id available in newer versions. Yes, even password managers were slow to Argon2 — it's rolling out gradually because the ecosystem tooling took time to mature.

What About bcrypt?

bcrypt (1999) is a fine password-hashing function with modest memory hardness. It caps input at 72 bytes (a famous footgun — long passwords get truncated silently before rounds 2011). It's the historical default in Ruby on Rails and many PHP frameworks. For new code in 2026, prefer Argon2id; bcrypt is fine for maintaining existing systems.

The Realistic Recommendation

For a new file transfer service in 2026:

  • If you can ship a 15-200 KB dependency: Argon2id via @noble/hashes or libsodium.js
  • If you need zero dependencies and bundle-size-at-all-costs: PBKDF2-SHA-256 at 600,000 iterations via Web Crypto
  • If you're writing a cryptocurrency wallet or anything inheriting legacy scrypt: scrypt at N=2^17

For production apps handling sensitive files, Argon2id is worth the WASM dependency. For simple password-protected shares where users generate a random password anyway, PBKDF2 is adequate because the entropy is in the password, not the KDF.

Try it at hexatransfer.com — free, no account, 10 GB max.

Send large files securely with end-to-end encryption

Transfer files up to 10 GB for free with end-to-end encryption. No account required. Your files are encrypted in your browser before upload — no one else can read them.

Send a file