scrypt-js vs scryptsy
フロントエンドにおける Scrypt 暗号化実装の比較
scrypt-jsscryptsy

フロントエンドにおける Scrypt 暗号化実装の比較

scrypt-jsscryptsy は、どちらもパスワードベースの鍵導出関数(KDF)である Scrypt を JavaScript で実装したパッケージです。Scrypt は、ブルートフォース攻撃に対して耐性を持つように設計されており、パスワードのハッシュ化や暗号鍵の生成に使用されます。scrypt-js はブラウザと Node.js の両方で動作する純粋な JavaScript 実装を目指しており、Web Worker を利用した非同期処理に重点を置いています。一方、scryptsy はより古くから存在する実装で、Node.js のネイティブ crypto モジュールとの互換性や、特定の環境での最適化を特徴としていますが、メンテナンス状況に注意が必要です。

npmのダウンロードトレンド

3 年

GitHub Starsランキング

統計詳細

パッケージ
ダウンロード数
Stars
サイズ
Issues
公開日時
ライセンス
scrypt-js0146-136年前MIT
scryptsy046-67年前MIT

scrypt-js vs scryptsy: 安全性、パフォーマンス、実装の深層比較

パスワードベースの鍵導出関数(KDF)である Scrypt を JavaScript で実装する際、scrypt-jsscryptsy は長年候補として挙がってきました。しかし、フロントエンドアーキテクチャの観点から見ると、両者の設計思想、メンテナンス状況、そしてセキュリティへのアプローチには明確な違いがあります。特に、メインスレッドをブロックしない処理と、進行状況の可視化は、ユーザー体験に直結する重要な要素です。

🛡️ セキュリティとメンテナンス状況

scrypt-js は、セキュリティとブラウザでの動作を第一に設計されています。

  • 依存関係が少なく、純粋な JavaScript で書かれているため、ビルドツールとの相性が良いです。
  • 定期的にセキュリティ関連の更新が行われており、現代のブラウザ環境に適合しています。
  • 進行状況(progress)を監視できるため、DoS 攻撃的な重たい処理中にユーザーにフィードバックを与えられます。
// scrypt-js: 進行状況の監視が可能
const scrypt = require('scrypt-js');

scrypt.scrypt(
  Buffer.from(password),
  Buffer.from(salt),
  N, r, p, dkLen,
  (error, progress, key) => {
    if (error) { throw error; }
    if (key) { return key; }
    // progress: 0.0 ~ 1.0
    updateProgressBar(progress); 
  }
);

scryptsy は歴史のあるライブラリですが、メンテナンス頻度は低いです。

  • 過去にセキュリティの懸念や、実装の正確性に関する議論がありました。
  • 最新のセキュリティ標準や、ブラウザの新しい機能(Web Worker の活用など)への対応が遅れている可能性があります。
  • 新しいプロジェクトで採用する際は、セキュリティ監査の結果を慎重に確認する必要があります。
// scryptsy: 基本的な同期/非同期呼び出し
const scryptsy = require('scryptsy');

// 同期版(メインスレッドをブロックするためブラウザでは非推奨)
const key = scryptsy(password, salt, N, r, p, dkLen);

// 非同期版
scryptsy.async(password, salt, N, r, p, dkLen, (err, hash) => {
  if (err) { throw err; }
  // 進行状況のコールバックは標準では提供されない
});

⚡ パフォーマンスとメインスレッドへの影響

Scrypt 計算は意図的に計算コストが高く、CPU を消費します。フロントエンドでは、これが UI のフリーズ(メインスレッドのブロック)を引き起こす主要な原因となります。

scrypt-js は、処理を細かく分割して実行する仕組みを持っています。

  • これにより、ブラウザが UI 描画やユーザー入力に応答する隙間を作れます。
  • setTimeout などを内部で使い、イベントループを占有しすぎないように制御しています。
// scrypt-js: 非同期処理で UI ブロックを回避
scrypt.scrypt(
  Buffer.from(password),
  Buffer.from(salt),
  1024, 1, 1, 32,
  (error, progress, key) => {
     // UI スレッドをブロックせずに進行状況を更新可能
     console.log(`Progress: ${progress * 100}%`);
  }
);

scryptsy は、単純な非同期ラッパーを提供していますが、内部処理の細分化までは保証されていません。

  • 環境によっては、一度の呼び出しで長時間 CPU を占有する可能性があります。
  • ブラウザで使用する場合、自前で Web Worker にオフロードする工夫が必要になることが多いです。
// scryptsy: 自前で Worker 管理が必要な場合が多い
// main.js
const worker = new Worker('scrypt-worker.js');
worker.postMessage({ password, salt });

// scrypt-worker.js
const scryptsy = require('scryptsy');
self.onmessage = (e) => {
  // Worker 内でも長時間処理になるリスクがある
  const hash = scryptsy(e.data.password, e.data.salt, 1024, 1, 1, 32);
  self.postMessage(hash);
};

🔌 API デザインと使いやすさ

API の設計は、開発者の生産性とミスの起こりにくさに影響します。

scrypt-js は、コールバックベースの API を採用しています。

  • 進行状況を含む 3 つの引数(error, progress, key)を扱う必要があります。
  • Promise ラッパーを自作するか、util.promisify を使うと現代的な async/await 構文で扱えます。
// scrypt-js: Promise ラッパー例
const scryptPromise = (password, salt) => {
  return new Promise((resolve, reject) => {
    scrypt.scrypt(password, salt, 1024, 1, 1, 32, (err, progress, key) => {
      if (err) reject(err);
      if (key) resolve(key);
    });
  });
};

const key = await scryptPromise(Buffer.from(pass), Buffer.from(salt));

scryptsy は、同期的な関数と非同期な関数が別れています。

  • 直感的ですが、ブラウザで同期版をうっかり使うとアプリがフリーズします。
  • 引数の順序や型(Buffer vs String)に一貫性がない場合があり、注意が必要です。
// scryptsy: 同期と非同期の使い分け
const hashSync = scryptsy(password, salt, 1024, 1, 1, 32); // ブロック注意

scryptsy.async(password, salt, 1024, 1, 1, 32, (err, hash) => {
  // 非同期処理
});

🌐 環境依存と互換性

パッケージがどの環境で動作するかは、アーキテクチャ決定の重要な要素です。

scrypt-js は、環境依存を最小限に抑えています。

  • Node.js の crypto モジュールや Buffer に依存しますが、ブラウザ向けに polyfill や変換処理を内包しています。
  • Webpack や Vite などのバンドラーとの相性が良く、設定なしで動作することが多いです。
// scrypt-js: バンドラーとの互換性
// webpack.config.js などで特別な設定が不要な場合が多い
import scrypt from 'scrypt-js';
// ブラウザでも Node でも同じ API で動作

scryptsy は、Node.js 寄りの設計です。

  • ブラウザで動作させるには、Buffer の polyfill や、プロセス関連のグローバル変数の設定が必要になることがあります。
  • 最近のフロントエンドビルドツールでは、この設定が手間になることがあります。
// scryptsy: ブラウザでの設定例
// Buffer の polyfill が必要な場合がある
import { Buffer } from 'buffer';
window.Buffer = Buffer;

import scryptsy from 'scryptsy';

🤝 共通点:Scrypt 算法の実装

両パッケージとも、同じ Scrypt 算法に基づいており、適切なパラメータを与えれば同じ出力を生成します。

1. 🔑 鍵導出機能(KDF)

  • どちらもパスワードとソルトから暗号鍵を生成します。
  • パラメータ(N, r, p)を調整してセキュリティレベルを変更できます。
// 両者共通のパラメータ概念
// N: CPU コスト (2 のべき乗)
// r: メモリコスト
// p: 並列化パラメータ
const N = 1024, r = 1, p = 1;

2. 📦 入出力形式

  • 入力:パスワード(文字列またはバイト配列)、ソルト(バイト配列)
  • 出力:導出された鍵(バイト配列)
// 両者共通の入力イメージ
const password = 'my-secret-password';
const salt = crypto.randomBytes(16); // または保存済みのソルト

3. 🔒 用途

  • パスワードの保存(ハッシュ化)
  • 暗号化鍵の生成(AES などの鍵として利用)
// 両者共通の用途:AES 鍵の生成
const key = await deriveKey(password, salt);
const cipher = aes.encrypt(data, key);

📊 比較サマリー

特徴scrypt-jsscryptsy
メンテナンス✅ 比較的活発⚠️ 停滞気味
UI ブロック対策✅ 内部で細分化処理❌ 自前で Worker 管理が必要
進行状況監視✅ コールバックで提供❌ 標準では提供されない
ブラウザ対応✅ 容易⚠️ Polyfill 設定が必要な場合あり
API 形態コールバック(Progress 付き)同期 / 非同期(別関数)
推奨度🟢 新しいプロジェクト🟡 レガシー維持の場合のみ

💡 結論と推奨

scrypt-js は、現代のフロントエンド開発において、セキュリティとユーザー体験のバランスが取れた選択肢です。特に、進行状況の通知機能と、メインスレッドをブロックしない設計は、パスワード入力などのインタラクティブな場面において不可欠な機能です。新しいプロジェクトでは、これを第一候補とするべきです。

scryptsy は、過去の資産や、特定の Node.js 環境との厳密な互換性が必要な場合にのみ検討します。しかし、メンテナンスのリスクを考慮すると、長期的には scrypt-js への移行、あるいは Web Crypto API を活用したより現代的なアプローチ(PBKDF2 など、ブラウザネイティブサポートがあるもの)への切り替えを検討することが賢明です。

最終的なアドバイス:暗号化実装では「動くこと」以上に「安全であること」と「維持できること」が重要です。パッケージの更新履歴とセキュリティ報告を定期的にチェックし、必要に応じて専門家の監査を受けることを忘れないでください。

選び方: scrypt-js vs scryptsy

  • scrypt-js:

    ブラウザ環境でのセキュリティと UX を最優先する場合に scrypt-js を選択します。このパッケージは Web Worker を活用してメインスレッドをブロックせず、進行状況(progress)のコールバックを提供するため、ユーザーに処理中の状態を表示しやすいです。純粋な JavaScript 実装であるため、Node.js の crypto モジュールに依存せず、環境を選ばず動作します。

  • scryptsy:

    既存のレガシーシステムとの互換性が必須である場合、または Node.js サーバー環境でネイティブモジュールとの挙動を厳密に合わせる必要がある場合に scryptsy を検討します。ただし、メンテナンスが停滞している可能性が高く、新しいプロジェクトではセキュリティと長期サポートの観点から scrypt-js や他の現代的な代替案(例:Web Crypto API を利用した実装)の評価を強く推奨します。

scrypt-js のREADME

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