🔧

nw.jsネイティブコード化ツールnwjcを使ってコードを隠匿する

2023/04/22に公開

はじめに

nw.jsでビルドする前提のゲームを開発していたんですが、nw.jsはビルド後コードがあっさり見れてしまうという特徴(というか弱点)があります。

その対策としてnwjcというnwのオプション的ツールを使ってネイティブコード化(バイナリ化)する方法があります。

しかし公式説明が英語かつ説明不足の感があり、また日本語(英語も?)のしっかりした記事を見かけなかったことなどから、こちらにまとめてみようと思います。

ただ、次のようなデメリットもあるので、そこを先に承知したうえで検討することをお勧めます。(特に最後)

  • ネイティブコード化する工程、およびそれを読み込む処理を追加しないといけない
  • ビルド時にnw.jsのバージョンに気を遣う必要がある
  • マルチプラットフォーム対応がかなり面倒になる

―――

本編

環境や前提など

  • OS:windows 10 64bit
  • node.js: v18.x系
  • ターミナル: Git Bash

またnwjcはCLIで利用するため、その基本的な知識(ターミナルの使い方など)があることも前提とします。

アプリ化はnw-builder(v3.x系)を使う前提です。

nwjcの導入

nwjcは単体でDL等するものではなく、nwパッケージのSDK版に付属しています。
Windowsではnwjc.exeという名前で存在します。

nwjc付きのnwパッケージはnpmjsから導入することもできますが、以下のようにsdk指定オプションを付けることを忘れなきよう。

npm install nw --nwjs_build_type=sdk

npm経由の場合node_modules/nw/nwjs/nwjc にあるはずです。

今回はnpm経由でnwパッケージを追加した前提で話を進めていきます。
具体的には以下のプロジェクト構造を想定します。

(プロジェクトroot)
|- node_modules
  |- nw
|- src
  |- main.js (コンパイル対象コード)
|- nwapp (最終的にアプリ化するフォルダ)
  |- index.html

※スクリプトはカレントディレクトリがrootにある状態で実行します。

nwjcによるコンパイル

[path/to/nwjc.exe] [ソースファイルの場所] [バイナリファイルの出力先] という形でコマンドを実行します。

上記階層プロジェクトの場合、

node_modules/nw/nwjs/nwjc src/main.js nwapp/binary.bin

として、binary.binnwapp内に生成します。
(名前は何でもよい)

バイナリを読み込む

生成したbinary.binは専用APIを使って読み込みます。
nwapp/index.htmlにて以下のように処理をします。

nwapp/index.html
<script>
  nw.Window.get().evalNWBin(null, "./binary.bin");
</script>

注意点

⚠nw-builderのversionオプションに注意

nwjcによってバイナリ化したファイルは、同じnwバージョンでなければ読み込むことができます。

開発中はnode_modules内のnwモジュールが利用されるはずなので、このことが問題になることはまずありませんが、nw-builderによるアプリ化の際、デフォルトでは最新のnwパッケージをDLして組み込むため、バージョンがマッチしない事態になり得ます。

以下のようにpackage.jsonのdevDependencies(あるいはdependencies)から対応バージョン文字列を取得するようにすると後々のnwパッケージアップデートにも対応できて便利です。
^記号などは消す必要があります)

const { nwbuild } = require("nw-builder");
const pkg = require("../package.json");

const nwVersion = pkg.devDependencies.nw.replace("^", "");

// nw-builderは執筆地点最新のv4とv3で設定できるオプションが結構異なりますが`version`はそのままなので差異はない…はず。
nwbuild({
  version: nwVersion,
  // ...そのほかのオプション
});

なおバージョンがマッチしてない場合、一瞬だけ立ち上がるものの即落ちする謎アプリと化してしまいます。(バイナリ化ファイルが読み込めずエラーになる?)

⚠マルチプラットフォーム対応する場合の注意点

nwjcバイナリコードにはもう一つ制限があり、それはコンパイルを実行したOS・環境(筆者環境ではwin10 64bit)でしか起動できないという点です。

これはプラットフォームごとに実機や仮想環境を用意するなどして、それぞれのマシンからnwjcを実行して対応しないといけないことを意味します。

https://stackoverflow.com/questions/68494972/how-do-i-use-the-nwjc-versions-for-mac-and-linux-on-windows

nw.jsの利点であるマルチプラットフォーム対応がかなりやりづらくなることに留意しないといけません。

おまけ: バンドラーを経由したコンパイルプロセスの一例

実際にはバニラJSではなくtypescriptで書いてトランスパイルしたり、webpackなどのバンドラーを経由して画像などとバンドルする工程を事前に行うかと思われます。
そこで一応手順を例示しておきます。(esbuildを使った例)

import { buildSync } from "esbuild";
import { execFile } from "node:child_process";
import { unlink } from "node:fs";

const entryPoints = ["./src/index.ts"];
const outfile = "./nwapp/bundle.js";
const binOutfile = "./nwapp/binary.bin";

// esbuildでtypescriptコンパイル&バンドル(bundle.js生成)
buildSync({
  bundle: true,
  entryPoints,
  outfile,
  loader: { ".png": "dataurl" }, // pngファイルはbase64変換
  platform: "node",
  watch: false,
});

// nwjcでネイティブコード化
execFile(
  "node_modules\\nw\\nwjs\\nwjc",
  [outfile, binOutfile],
  (err, _stdout, _stderr) => {
    if (err) throw new Error(err);

    // 変換完了後、元のbundle.jsは不要なので消す
    unlink(outfile, (err) => {
      if (err) throw err;
      console.log("Build done!");
    });
  }
);

その他

  • 似たような仕組みのelectronにはこの機能はない(はず、知ってたらご一報ください)のでこのツールのためだけにnw.jsを選ぶのもアリ
  • バンドラーで画像などのアセットリソースをbase64化した状態でバイナリ化を行うとそれらも同時に秘匿できそう(どれくらいの秘匿強度かは不明)
  • 先の記事でソースコードが大規模になるとバイナリファイルが読み込めなくなるとの報告がありました、が自分の環境では問題なく実行できました
  • まだ本人は実際アプリとしてリリースするに至っていない(目処も立ってない)状態です…

Discussion