crypto.scryptSync()

Derives a fixed-length key from a password using the deliberately slow, memory-hard scrypt function.

Signature
crypto.scryptSync(password, salt, keylen[, options])
  • Added in Node.js v10.5.0

Parameters

ParameterTypeDescription
$passwordstring | Buffer | TypedArray | DataViewThe password or passphrase to stretch.
$saltstring | Buffer | TypedArray | DataViewA value that should be as unique as possible; random and at least 16 bytes is recommended. It is not secret and must be stored with the derived key.
$keylennumberThe length of the derived key in bytes.
$optionsoptionalObjectTuning parameters for the cost of the derivation.
$options.costoptionalnumber= 16384CPU/memory cost parameter. Must be a power of two greater than one.
$options.blockSizeoptionalnumber= 8Block size parameter.
$options.parallelizationoptionalnumber= 1Parallelization parameter.
$options.Noptionalnumber= 16384Alias for cost. Only one of the two may be specified.
$options.roptionalnumber= 8Alias for blockSize. Only one of the two may be specified.
$options.poptionalnumber= 1Alias for parallelization. Only one of the two may be specified.
$options.maxmemoptionalnumber= 32 * 1024 * 1024Memory upper bound in bytes. It is an error when approximately 128 * N * r exceeds this value.

Return value

BufferA Buffer containing the derived key, exactly keylen bytes long.

Errors

Throws ERR_CRYPTO_INVALID_SCRYPT_PARAMS when 128 * N * r exceeds maxmem, or when N is not a power of two greater than one.

Changelog

  • PHP v12.8.0, v10.17.0The maxmem value can now be any safe integer.
  • PHP v10.9.0The cost, blockSize and parallelization option names were added as readable aliases for N, r and p.

crypto.scryptSync() turns a low-entropy password into a full-strength key. Its value lies in being slow on purpose: scrypt is memory-hard, so an attacker cannot simply throw a GPU or ASIC at it the way they can at SHA-256. With the default cost of 16384 each call takes tens of milliseconds and needs about 16 MB of RAM, which turns an offline cracking run from hours into years.

Two distinct jobs

It stores passwords — derive a key from the password and a per-user random salt, store both, and on login rederive and compare with crypto.timingSafeEqual(). It also stretches a passphrase into a real encryption key for createCipheriv(), which demands exactly 32 raw bytes for AES-256.

What trips people up

The Sync in the name is a warning. Every call blocks the entire event loop for its full duration, so a login endpoint using it can serve only one authentication at a time and a burst of logins stalls every other request on the server. Use the callback-based crypto.scrypt() in request handlers and keep scryptSync for startup, scripts and CLI tools.

The second trap is maxmem. Raising N to 1048576 for extra security throws ERR_CRYPTO_INVALID_SCRYPT_PARAMS, because 128 × N × r exceeds the 32 MB default. Raise maxmem alongside N, and remember the salt must be stored — without it the key can never be rederived.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Hashing a user's password for storage with a per-user random salt
  • Deriving a 32-byte AES key from a passphrase typed into a CLI tool
  • Rederiving a key at boot to decrypt an encrypted configuration file
  • Verifying a login by recomputing the key and comparing in constant time

Frequently asked questions

Is scrypt good enough for password hashing, or do I need bcrypt or argon2?

scrypt is a solid, widely accepted choice and has the advantage of being built into Node with no native dependency to compile. Argon2id is the current preference of most password-hashing guidance, but a correctly configured scrypt with a random per-user salt is far better than any plain hash.

Why does crypto.scryptSync throw ERR_CRYPTO_INVALID_SCRYPT_PARAMS?

Usually because 128 * N * r exceeds maxmem, which defaults to 32 MiB — raising N to 1048576 needs roughly 1 GiB, so maxmem must be raised too. It also throws if N is not a power of two greater than one.

Should I use scryptSync or the async crypto.scrypt?

Use the async crypto.scrypt in any request handler, because scryptSync blocks the entire event loop for the full derivation and serializes every concurrent login. scryptSync is fine in CLI tools, migration scripts and startup code where nothing else is waiting.

More examples

Storing and verifying a password

Encoding the parameters into the stored string is what makes the cost upgradable: when you later raise N, old records still verify with their original settings and can be rehashed on next successful login. The salt is stored in the clear because its only job is to stop one precomputed table from attacking every user at once.

Example · javascript
const crypto = require('node:crypto');

function hashPassword(password) {
  const salt = crypto.randomBytes(16); // unique per user, not secret
  const key = crypto.scryptSync(password, salt, 64);
  return `scrypt$16384$8$1$${salt.toString('hex')}$${key.toString('hex')}`;
}

function verifyPassword(password, stored) {
  const [scheme, N, r, p, saltHex, keyHex] = stored.split('$');
  if (scheme !== 'scrypt') return false;

  const expected = Buffer.from(keyHex, 'hex');
  const actual = crypto.scryptSync(password, Buffer.from(saltHex, 'hex'), expected.length, {
    N: Number(N), r: Number(r), p: Number(p),
  });
  return crypto.timingSafeEqual(expected, actual);
}

const record = hashPassword('hunter2');
console.log('field parts   :', record.split('$').length);
console.log('correct password:', verifyPassword('hunter2', record));
console.log('wrong password  :', verifyPassword('hunter3', record));

Don't block the event loop

The promisified crypto.scrypt hands the CPU work to libuv's threadpool, so concurrent logins overlap instead of queueing. The same failure message is returned for an unknown user and a bad password, which avoids revealing which email addresses have accounts.

Example · javascript
import crypto from 'node:crypto';
import { promisify } from 'node:util';
import express from 'express';

const scrypt = promisify(crypto.scrypt);
const app = express();
app.use(express.json());

app.post('/login', async (req, res) => {
  const user = await findUser(req.body.email);
  if (!user) return res.status(401).json({ error: 'invalid credentials' });

  // Async form: the work runs on the threadpool, so other requests
  // keep being served while this derivation is in flight.
  const derived = await scrypt(req.body.password, user.salt, 64);

  if (!crypto.timingSafeEqual(user.key, derived)) {
    return res.status(401).json({ error: 'invalid credentials' });
  }
  res.json({ ok: true });
});

// scryptSync here would serialize every login: with a ~100 ms derivation,
// 50 concurrent logins means the last one waits 5 seconds, and every
// unrelated request on the server waits behind them too.
console.log('login route registered');

Tuning cost, and the maxmem ceiling

Cost and memory both scale linearly with N, so each doubling roughly doubles the time and the RAM required — which is exactly the property that hurts an attacker running many guesses in parallel. Timings vary by machine; pick the largest N whose measured duration your login latency budget tolerates.

Example · javascript
const crypto = require('node:crypto');

const salt = crypto.randomBytes(16);

for (const N of [1024, 16384, 32768]) {
  const started = process.hrtime.bigint();
  crypto.scryptSync('password', salt, 32, { N, r: 8, p: 1 });
  const ms = Number(process.hrtime.bigint() - started) / 1e6;
  const memMiB = (128 * N * 8) / 1024 / 1024;
  console.log(`N=${String(N).padStart(5)}  ~${memMiB.toFixed(0)} MiB  ${ms.toFixed(0)} ms`);
}

// 128 * N * r must stay under maxmem (32 MiB by default).
try {
  crypto.scryptSync('password', salt, 32, { N: 1048576, r: 8, p: 1 });
} catch (err) {
  console.log('N=1048576 without maxmem ->', err.code);
}

// Raise maxmem together with N.
const big = crypto.scryptSync('password', salt, 32, {
  N: 1048576, r: 8, p: 1, maxmem: 2 * 1024 * 1024 * 1024,
});
console.log('with raised maxmem, key bytes:', big.length);

Discussion

  • Be the first to comment on this lesson.