scrypt-js vs scryptsy
Secure Key Derivation for Client-Side Wallets
scrypt-jsscryptsy

Secure Key Derivation for Client-Side Wallets

scrypt-js and scryptsy are pure JavaScript implementations of the Scrypt key derivation function (KDF). They allow developers to derive cryptographic keys from passwords directly in the browser or Node.js environment without native dependencies. This is essential for encrypting and decrypting cryptocurrency wallets (like Ethereum keystores) client-side, ensuring private keys never leave the user's device.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
scrypt-js0146-136 years agoMIT
scryptsy046-67 years agoMIT

scrypt-js vs scryptsy: Pure JavaScript Scrypt Implementations

Both scrypt-js and scryptsy solve the same critical problem: running the Scrypt key derivation function in environments without native C bindings. This is vital for frontend security, allowing password-based encryption entirely within the browser. However, they differ significantly in API design, data types, and maintenance status.

๐Ÿ”„ API Style: Promises vs Callbacks and Sync

scrypt-js is built for modern asynchronous JavaScript.

  • It returns a Promise, making it easy to use with async/await.
  • This prevents callback hell and integrates cleanly with modern frontend frameworks.
// scrypt-js: Promise-based API
import { scryptJs } from 'scrypt-js';

const key = await scryptJs(
  password, 
  salt, 
  N, r, p, 
  dkLen
);

scryptsy offers both synchronous and callback-based asynchronous options.

  • The synchronous version blocks the main thread, which can freeze the UI.
  • The callback version follows older Node.js patterns.
// scryptsy: Callback or Sync API
const scrypt = require('scryptsy');

// Async with callback
scrypt(password, salt, N, r, p, dkLen, (err, derivedKey) => {
  if (err) throw err;
  console.log(derivedKey);
});

// Sync (blocks thread)
const key = scrypt(password, salt, N, r, p, dkLen);

๐Ÿ“ฆ Data Types: Uint8Array vs Buffer

scrypt-js returns a Uint8Array.

  • This is a native JavaScript typed array.
  • Works out-of-the-box in browsers without polyfills.
// scrypt-js: Native Uint8Array
const key = await scryptJs(password, salt, N, r, p, dkLen);
console.log(key instanceof Uint8Array); // true

scryptsy returns a Buffer.

  • Buffers are common in Node.js but require polyfills in browsers.
  • Adds extra configuration to bundlers like Webpack or Vite.
// scryptsy: Node.js Buffer
const key = scrypt(password, salt, N, r, p, dkLen);
console.log(Buffer.isBuffer(key)); // true
// In browser, may need: import buffer from 'buffer/';

๐Ÿ“Š Progress Tracking: Built-in vs Manual

scrypt-js includes a progress callback.

  • Scrypt is CPU-intensive and can take seconds.
  • You can update a progress bar to show the user something is happening.
// scrypt-js: Progress callback
const key = await scryptJs(
  password, salt, N, r, p, dkLen,
  (progress) => {
    console.log(`Progress: ${progress * 100}%`);
  }
);

scryptsy does not have a built-in progress callback.

  • You must implement custom logic to estimate progress.
  • Harder to provide user feedback during long computations.
// scryptsy: No built-in progress
// You must wrap the call or estimate time manually
scrypt(password, salt, N, r, p, dkLen, (err, key) => {
  // Done, but no intermediate steps reported
});

๐Ÿ›ก๏ธ Maintenance and Trust

scrypt-js is maintained by the author of ethers.js.

  • Widely audited and used in production wallet software.
  • Regularly updated to match modern security standards.
// scrypt-js: Active ecosystem
// Used internally by ethers.js wallet encryption
import { wallet } from 'ethers';

scryptsy has less active maintenance.

  • It is a stable library but sees fewer updates.
  • Often found in older projects or legacy cryptocurrency tools.
// scryptsy: Legacy usage
// Common in older ethereumjs-wallet versions
const wallet = require('ethereumjs-wallet');

๐ŸŒ Real-World Scenarios

Scenario 1: Modern React Wallet App

You are building a new cryptocurrency wallet interface in React.

  • โœ… Best choice: scrypt-js
  • Why? Promises fit React hooks better, and Uint8Array avoids build errors.
// React component with scrypt-js
async function handleEncrypt() {
  try {
    const key = await scryptJs(password, salt, N, r, p, 32);
    setDerivedKey(key);
  } catch (e) {
    setError(e.message);
  }
}

Scenario 2: Legacy Node.js Script

You are maintaining an old backend script that decrypts keystores.

  • โœ… Best choice: scryptsy
  • Why? If the rest of the pipeline uses Buffers and callbacks, switching adds work.
// Legacy script with scryptsy
const key = scrypt(password, salt, N, r, p, 32);
const decrypted = decrypt(keystore, key);

Scenario 3: User Experience Focus

You need to show a progress bar during key generation.

  • โœ… Best choice: scrypt-js
  • Why? Built-in progress callback saves development time.
// UX focused with scrypt-js
await scryptJs(password, salt, N, r, p, 32, (p) => {
  updateProgressBar(p);
});

๐Ÿ“Œ Summary Table

Featurescrypt-jsscryptsy
API Style๐Ÿค Promises (async/await)๐Ÿ“ž Callbacks or Sync
Return Type๐Ÿงถ Uint8Array (Native)๐Ÿ“ฆ Buffer (Node.js)
Progressโœ… Built-in callbackโŒ Manual implementation
Maintenance๐ŸŸข Active (ethers.js author)๐ŸŸก Less Active
Browser Readyโœ… Yes (no polyfills)โš ๏ธ Needs Buffer polyfill

๐Ÿ’ก Final Recommendation

scrypt-js is the clear choice for new frontend projects.
It aligns with modern JavaScript standards, avoids Buffer polyfill issues, and provides better user feedback through progress callbacks. Its connection to the ethers.js ecosystem means it is battle-tested in real-world wallet applications.

scryptsy should only be used for maintaining legacy systems.
While functional, its reliance on Buffers and callback patterns makes it harder to integrate into modern frontend build chains. Unless you are fixing an old codebase, there is little reason to choose it over scrypt-js.

Final Thought: Security libraries need to be easy to use correctly. scrypt-js reduces friction for frontend developers while maintaining high security standards.

How to Choose: scrypt-js vs scryptsy

  • scrypt-js:

    Choose scrypt-js for modern applications requiring Promise-based workflows and active maintenance. It is the standard choice for Ethereum tooling (ethers.js) and returns native Uint8Array types, avoiding Buffer polyfills in the browser.

  • scryptsy:

    Choose scryptsy only if you are maintaining legacy codebases that depend on its specific synchronous API or callback structure. It is less actively maintained and returns Buffer objects, which may require additional setup in modern frontend build pipelines.

README for scrypt-js

scrypt

The scrypt password-base key derivation function (pbkdf) is an algorithm designed to be brute-force resistant that converts human readable passwords into fixed length arrays of bytes, which can then be used as a key for symmetric block ciphers, private keys, et cetera.

Features:

  • Non-blocking - Gives other events in the event loop opportunities to run (asynchronous)
  • Cancellable - If the key is no longer required, the computation can be cancelled
  • Progress Callback - Provides the current progress of key derivation as a percentage complete

Tuning

The scrypt algorithm is, by design, expensive to execute, which increases the amount of time an attacker requires in order to brute force guess a password, adjustable by several parameters which can be tuned:

  • N - The CPU/memory cost; increasing this increases the overall difficulty
  • r - The block size; increasing this increases the dependency on memory latency and bandwidth
  • p - The parallelization cost; increasing this increases the dependency on multi-processing

Installing

node.js

If you do not require the progress callback or cancellable features, and your application is specific to node.js, you should likely use the built-in crypto package.

Otherwise, to install in node.js, use:

npm install scrypt-js

browser

<script src="https://raw.githubusercontent.com/ricmoo/scrypt-js/master/scrypt.js" type="text/javascript"></script>

API

scrypt . scrypt ( password , salt , N , r , p , dkLen [ , progressCallback ] ) => Promise

Compute the scrypt PBKDF asynchronously using a Promise. If progressCallback is provided, it is periodically called with a single parameter, a number between 0 and 1 (inclusive) indicating the completion progress; it will always emit 0 at the beginning and 1 at the end, and numbers between may repeat.

scrypt . syncScrypt ( password , salt , N , r , p , dkLen ) => Uint8Array

Compute the scrypt PBKDF synchronously. Keep in mind this may stall UI and other tasks and the asynchronous version is highly preferred.

Example

<html>
  <body>
    <div><span id="progress"></span>% complete...</div>
    <!-- These two libraries are highly recommended for encoding password/salt -->
    <script src="libs/buffer.js" type="text/javascript"></script>

    <!-- This shim library greatly improves performance of the scrypt algorithm -->
    <script src="libs/setImmediate.js" type="text/javascript"></script>

    <script src="index.js" type="text/javascript"></script>
    <script type="text/javascript">

      // See the section below: "Encoding Notes"
      const password = new buffer.SlowBuffer("anyPassword".normalize('NFKC'));
      const salt = new buffer.SlowBuffer("someSalt".normalize('NFKC'));

      const N = 1024, r = 8, p = 1;
      const dkLen = 32;

      function updateInterface(progress) {
          document.getElementById("progress").textContent = Math.trunc(100 * progress);
      }

      // Async
      const keyPromise = scrypt.scrypt(password, salt, N, r, p, dkLen, updateInterface);

      keyPromise.then(function(key) {
          console.log("Derived Key (async): ", key);
      });

      // Sync
      const key = scrypt.syncScrypt(password, salt, N, r, p, dkLen);
      console.log("Derived Key (sync): ", key);
    </script>
  </body>
</html>

Encoding Notes

TL;DR - either only allow ASCII characters in passwords, or use
        String.prototype.normalize('NFKC') on any password

It is HIGHLY recommended that you do NOT pass strings into this (or any password-base key derivation function) library without careful consideration; you should convert your strings to a canonical format that you will use consistently across all platforms.

When encoding passwords with UTF-8, it is important to realize that there may be multiple UTF-8 representations of a given string. Since the key generated by a password-base key derivation function is dependent on the specific bytes, this matters a great deal.

Composed vs. Decomposed

Certain UTF-8 code points can be combined with other characters to create composed characters. For example, the letter a with the umlaut diacritic mark (two dots over it) can be expressed two ways; as its composed form, U+00FC; or its decomposed form, which is the letter "u" followed by U+0308 (which basically means modify the previous character by adding an umlaut to it).

// In the following two cases, a "u" with an umlaut would be seen
> '\u00fc'
> 'u\u0308'


// In its composed form, it is 2 bytes long
> new Buffer('u\u0308'.normalize('NFKC'))
<Buffer c3 bc>
> new Buffer('\u00fc')
<Buffer c3 bc>

// Whereas the decomposed form is 3 bytes, the letter u followed by U+0308
> new Buffer('\u00fc'.normalize('NFKD'))
<Buffer 75 cc 88>
> new Buffer('u\u0308')
<Buffer 75 cc 88>

Compatibility equivalence mode

Certain strings are often displayed the same, even though they may have different semantic means. For example, UTF-8 provides a code point for the roman number for one, which appears as the letter I, in most fonts identically. Compatibility equivalence will fold these two cases into simply the capital letter I.

> '\u2160'
'I'
> 'I'
'I'
> '\u2160' === 'I'
false
> '\u2160'.normalize('NFKC') === 'I'
true

Normalizing

The normalize() method of a string can be used to convert a string to a specific form. Without going into too much detail, I generally recommend NFKC, however if you wish to dive deeper into this, a nice short summary can be found in Pythons unicodedata module's documentation.

For browsers without normalize() support, the npm unorm module can be used to polyfill strings.

Another example of encoding woes

One quick story I will share is a project which used the SHA256(encodeURI(password)) as a key, which (ignoring rainbow table attacks) had an unfortunate consequence of old web browsers replacing spaces with + while on new web browsers, replacing it with a %20, causing issues for anyone who used spaces in their password.

Suggestions

  • While it may be inconvenient to many international users, one option is to restrict passwords to a safe subset of ASCII, for example: /^[A-Za-z0-9!@#$%^&*()]+$/.
  • My personal recommendation is to normalize to the NFKC form, however, one could imagine setting their password to a Chinese phrase on one computer, and then one day using a computer that does not have Chinese input capabilities and therefore be unable to log in.

See: Unicode Equivalence

Tests

The test cases from the scrypt whitepaper are included in test/test-vectors.json and can be run using:

npm test

Special Thanks

I would like to thank @dchest for his scrypt-async library and for his assistance providing feedback and optimization suggestions.

License

MIT license.

References

Donations

Obviously, it's all licensed under the MIT license, so use it as you wish; but if you'd like to buy me a coffee, I won't complain. =)

  • Ethereum - ricmoo.eth