Violet
Blog

What password stretching standards recommend, and what we ship

By The Violet build · published 2026-07-04 · updated 2026-07-04 · 7 minute read

OWASP recommends Argon2id first for password storage and reserves a 600,000 iteration figure for PBKDF2. Violet ships PBKDF2-SHA256 at 200,000 iterations in the browser, because the Web Cryptography API offers no memory-hard alternative and the client ships without dependencies. This post sets the current standards next to the exact shipped parameters, walks the threat math in plain words, and names the conditions under which the parameters change.

Why we publish our exact parameters

Violet encrypts personal data with a key derived from a passphrase, so the key derivation function and its cost settings are part of the product's security, not an implementation detail. Honest design means naming the actual numbers, comparing them to what the standards bodies recommend, and saying plainly where we fall short and why. Violet is pre-launch. This page describes code that exists in the repository today, not a marketing abstraction of it.

What the standards recommend

Two documents cover most of what people mean by the standard here. The OWASP Password Storage Cheat Sheet gives concrete parameter choices for storing password verifiers. NIST SP 800-132 covers password-based key derivation, which is closer to what Violet's client does: it derives an encryption key from a passphrase rather than storing a verifier. The primitives are the same; the guidance differs slightly in emphasis.

SourceWhat it says
OWASP Password Storage Cheat SheetUse Argon2id first, with a minimum of 19 MiB of memory, 2 iterations, and 1 degree of parallelism. Use scrypt where Argon2id is unavailable. Where PBKDF2 is required, for example for FIPS-140 compliance, use PBKDF2-HMAC-SHA256 with 600,000 iterations.
NIST SP 800-132For password-based key derivation: a random salt of at least 128 bits, and an iteration count as large as the environment can tolerate, with 1,000 as the bare minimum.
RFC 9106Defines Argon2 and recommends the Argon2id variant, which resists both GPU cracking and side-channel attacks.
RFC 8018Defines PBKDF2 itself. The iteration count is the cost knob, to be set as high as practical.
RFC 7914Defines scrypt, a memory-hard KDF that predates Argon2.

The ranking exists because Argon2id and scrypt are memory-hard: each guess costs the attacker real memory as well as compute, which blunts the advantage of GPUs and custom hardware. PBKDF2 is compute-only, so specialized hardware parallelizes it well. That is the qualitative gap the standards are pointing at, and no iteration count closes it.

What Violet actually ships

Three code paths in the repository stretch a secret. Here they are, with their exact parameters and where each one runs.

PathKDFParametersWhere it runs
Cloud sync content keyPBKDF2-SHA256200,000 iterations, random 128-bit per-account salt, derives a non-extractable AES-256-GCM keyIn the browser only; the key never leaves the client
Sign-in password hashPBKDF2-SHA256200,000 iterations, separate random 128-bit salt, constant-time comparisonOn the server, once per sign-in
Desktop journal vaultscryptN 32768, r 8, p 1, about 32 MiB of memory per derivationIn the desktop runtime via node:crypto

The sync path is the one this post is about. The derived key encrypts content before it leaves the device, and the server stores only an opaque nonce-plus-ciphertext envelope. This is the actual derivation code from the client:

const PBKDF2_ITERS = 200_000; // >= 200k per the spec

export async function deriveContentKey(passphrase, kdfSaltB64) {
  const baseKey = await crypto.subtle.importKey(
    "raw", enc.encode(passphrase), "PBKDF2", false, ["deriveKey"],
  );
  return crypto.subtle.deriveKey(
    { name: "PBKDF2", salt: b64ToBytes(kdfSaltB64),
      iterations: PBKDF2_ITERS, hash: "SHA-256" },
    baseKey,
    { name: "AES-GCM", length: 256 },
    false, // non-extractable: the key cannot be read back out
    ["encrypt", "decrypt"],
  );
}

The salt is 16 random bytes generated per account, which meets the 128-bit floor in NIST SP 800-132. The key is created non-extractable, so even the client's own code cannot export it and send it anywhere.

Why PBKDF2 in a browser

Violet's web client ships without dependencies: no packages, no bundler, and a strict content security policy that blocks external scripts. Under that rule, the only key stretching available is what the browser provides natively, and the Web Cryptography API's deriveKey supports PBKDF2 and HKDF but not Argon2 or scrypt. HKDF is not a password KDF, so PBKDF2 is the only stretching option on the table.

Shipping Argon2id in a browser today means shipping a WebAssembly build of it: a third-party dependency we would have to vet, pin, and keep patched, in a codebase whose rule is that there are none. So the honest statement is this: Argon2id is the better KDF, and we do not ship it here because we cannot do so without dependencies. Where the runtime does offer a memory-hard KDF we use one; the desktop journal vault derives its keys with scrypt from node:crypto. PBKDF2 in the browser is a constraint we are naming, not a choice we are dressing up as equivalent.

Why 200,000 and not 600,000

OWASP's 600,000 figure targets server-side password storage, where the server pays the derivation cost once per login on server hardware it controls. Violet's heaviest PBKDF2 use runs on the user's own device, every time a session derives the content key, including on modest phones. The spec set 200,000 as the floor for that budget, and that is what ships.

Even granting the different context, 200,000 is below OWASP's PBKDF2 number and we say so plainly rather than argue the guidance away. Two facts keep the gap in proportion. First, the difference between 200,000 and 600,000 is a factor of three, which in guessing terms is about 1.6 bits of extra work. Second, the jump the standards actually care about is memory-hardness, and tripling an iteration count does not buy any of it. Raising the count is cheap margin and we expect to take it; it is not the upgrade that matters most.

The KDF is also the second line of defense in this design, not the first. The server never holds the content key or the plaintext, so the derivation cost only becomes relevant to an attacker who has already obtained ciphertext. That does not excuse a weak parameter, but it does define what the parameter is protecting.

The threat math in plain words

When we would revisit

The summary is short. The standards say: memory-hard first, and if you must use PBKDF2, use a lot of iterations. We ship PBKDF2-SHA256 at 200,000 iterations in the browser because that is the strongest stretching the platform offers without dependencies, it sits below OWASP's PBKDF2 figure by a factor of three, the passphrase itself carries most of the real security either way, and the parameters are written down here so they can be held against us when the constraints change.

Questions

Is 200,000 iterations below the OWASP recommendation?

Yes. The OWASP Password Storage Cheat Sheet lists 600,000 iterations for PBKDF2-HMAC-SHA256 in password storage. Violet ships 200,000 in a different context, client-side key derivation on user hardware, but the number is still a factor of three lower, which is about 1.6 bits of guessing work. We state the gap instead of arguing it away, and raising the count is one of the named revisit paths.

Why not ship Argon2id compiled to WebAssembly?

The browser has no native Argon2, so shipping it means adding a third-party WebAssembly dependency to a client whose rule is zero dependencies and a strict content security policy. A cryptographic dependency would need vetting, pinning, and ongoing patching. If browsers add a memory-hard KDF natively, migration to it is the first revisit trigger.

Does a higher iteration count make a weak passphrase safe?

No. Iterations multiply the cost of each guess, and 200,000 iterations add about 17.6 bits of work. A passphrase from a common-password list falls in thousands of guesses regardless, because multiplying a tiny search space by any iteration count still leaves a feasible attack. Choose a long passphrase; the KDF only buys margin on top of it.

What would an attacker get by breaching the sync server?

Account emails, salted PBKDF2-SHA256 password hashes, per-account KDF salts, and opaque encrypted envelopes of nonce plus ciphertext. No content keys and no plaintext content are stored server-side; the content key is derived in the browser, is non-extractable, and never leaves the client. Reading content would require guessing passphrases offline against the KDF.

Related pages

Sources