バックエンドからバイトコード配列キーを取得する
VM バイトコードの復号キーをクライアントに置かないようにします。非同期キーゲッターを使い、読み込み時にバックエンドから取得します。認証の背後で保護されるため、盗まれたバンドルは無力になります。
問題
vmBytecodeArrayEncoding は VM バイトコードをキーで暗号化します。そのキーが
バンドル内に同梱されていると、ファイルを持っている人は誰でも復号に必要なものをすべて手にしていることになります。
vmBytecodeArrayEncodingKeyGetter を使うと、キーを別の場所に置き、実行時に生成できるようになります。しかし同期ゲッターは、
すでにクライアント上に存在するもの(グローバル、Cookie、localStorage)しか読み取れません。認証の背後で保護し、失効させられるように
サーバーからキーを取得するには、ゲッターが非同期でなければなりません。
解決策
vmAsyncExecutor(v7.3.0 以降)を有効にします。非同期エグゼキュータを使うと、
キーゲッターは Promise を返せるため、VM が実行される前にバックエンドからキーを fetch できます。3 つのオプションが
連携して機能します:
vmBytecodeArrayEncoding: true— バイトコード配列を暗号化します。vmBytecodeArrayEncodingKey— コンパイル時に使用するキー(バンドルではなくサーバーに保持します)。vmBytecodeArrayEncodingKeyGetter— 実行時にその同じキーを返す JS 式です。vmAsyncExecutorが 有効な場合はPromiseを返せます。無効な場合はゲッターがキーを同期的に返さなければなりません。
クライアント側の難読化設定
JavaScriptObfuscator.obfuscate(sourceCode, {
vmObfuscation: true,
vmAsyncExecutor: true,
vmBytecodeArrayEncoding: true,
vmBytecodeArrayEncodingKey: process.env.VM_KEY, // e.g. 'mySecretKey123'
vmBytecodeArrayEncodingKeyGetter:
'fetch("/api/vm-key", { credentials: "include" }).then((res) => res.text())'
});
ゲッターの式はそのまま埋め込まれ、ブラウザで評価されます。コンパイル時の
vmBytecodeArrayEncodingKey はクライアントのリポジトリに含めないでください。ビルド時に環境変数またはシークレットから
注入し、以下のエンドポイントからまったく同じ文字列を返してください。
2 つのキーの仕組み
あなたのキーが単独で使われることはありません。両側で、難読化ツールが制御する内部キーと混合されます:
- コンパイル時。
vmBytecodeArrayEncodingKeyは難読化ツールが導出する内部キーと結合され、 バイトコード配列はその混合されたキーでエンコードされます。 - 実行時。 あなたの
vmBytecodeArrayEncodingKeyGetterが解決する値(サーバーが返す値)は、 さまざまな実行時の要因からクライアント上で再構築された同じ内部キーと結合され、バイトコードをデコードします。
両側があなたのキーを内部キーと混合するため、ゲッターは vmBytecodeArrayEncodingKey として渡した文字列と
まったく同じ文字列に解決しなければなりません。どちらの部分も単独では不十分です。内部キーのないあなたのキーはバイトコードを
デコードできず、あなたのキーのない内部キーも役に立ちません。だからこそ、認証済みの呼び出し元にのみキーを提供することで、
盗まれたバンドルが無力なままになるのです。
サーバー側
エンドポイントは、アプリケーションが信頼する対象(有効なセッション、期待される Origin や Referer、
ライセンスチェックなど)に基づいて、どのキーを返すかを決定します。ひとひねり: 信頼できない呼び出し元を拒否するのではなく、
誤ったキーを返します。すると、バイトコードはゴミにデコードされ、保護されたコードは自ら失敗します。これは、攻撃者に
何を回避すればよいかを正確に教えてしまう明白な 401 よりもステルス性が高い方法です。
// Express example — the exact checks depend on your app
app.get('/api/vm-key', (req, res) => {
const origin = req.get('origin');
const trusted =
req.session?.user && // a valid session, and
origin === 'https://app.example.com'; // the expected production origin
res.type('text/plain').send(
// Real key for valid users; a decoy for everyone else
// (no session, or a localhost / unexpected origin).
trusted ? process.env.VM_KEY : process.env.VM_DECOY_KEY
);
});
