ホンキで考えるNW.js暗号化!/コード難読化モジュールにバグ?


※この記事はブログからの転載・まとめ記事(この記事この記事この記事)です。

はじめに

以前からブログの方でNW.jsのソースコードを保護するにはどうすべきかを検討していました。
その結果、
結局現状のNW.jsってソースコードを保護できてないんじゃね?
という結論に至ったのでご紹介いたします。

nw-builderだけだと暗号化とは到底言えない!

さて、以前nw-builderを利用してソースの暗号化を行う...といった内容をQiitaの記事にしましたが、
これが思ったより暗号化できていないです。
まずはこのファイルを見ていただきたいと思います。

これは僕が作ってた「mailToKindle」の64bit版exeファイルです。
そして次は「nw-builder」の「cache」上に存在する「nw.exe」

バイナリエディタを用いて、
この「nw.exe」の容量分、「mailToKindle.exe」の先頭からバイナリ列を削除します。
(ちなみに今回使用するのは「Stirling」というソフトです。)

すると以下のように「PK」から始まるバイナリ列になると思います。

(なんかMotherのPSIみたい...)
この状態で適当にな名前で拡張子を「.zip」として保存します。

もうお気づきですかね...

そうなんですよ...普通に中身が見えてしまうんです!
一応、(index以外の)HTMLファイルやcssファイル、JavaScriptのソースファイルはちょっとマシな暗号化する手立てがありますが、画像などのリソースファイルは手動で暗号化の手立てを打たなければなりません。
あとの「手立て」も本当に気休め程度です。

* リソースファイルは犠牲となったのだ!▼(タラリラー

まぁ、ある意味これは当然の結果で...なぜかと申しますとここに「zipで圧縮してexeと結合してね!」と書いているためです。

一応別の解決策でEnigma VirtualBoxという仮想化システムを使って仮想環境に封じ込める方法もあることにはあるのですが、
こちらも「Enigma VirtualBox 復号化」とかでググるとまぁ復号方法が出るわ出るわ...(しかも仮想PCが乗っかるので重たくなり、ファイルも肥大化します。)

DevToolを使わせたくなければ、normal版のnw.jsを使うべき

また別問題で、SDK版に同梱されたnw.exeではF12キーなどからDevToolが呼び出されてしまいます。
normal版に同梱されたnw.exeではDevToolが呼び出されなくなります。

しかし、ソースファイルとnw.exeが分離した状態では、
SDK版nw.exeを同じディレクトリに設置し、そちらを実行することでDevToolを利用できてしまうため、ソースコードを圧縮し、exeファイルと結合する必要があります。

つまり、前述のexeとの結合は必須事項ではあるのですが、バイナリエディタで容易に分離できるなどがあるため、全然解決策ではありません。

...package.jsonをイジるなど、愚策中の愚策なわけです。

NW.jsにはソースコードをコンパイルして読めなくする機能がある!

上で、「ちょっとマシな暗号化する手立て」と紹介っした方法で、
NW.jsのSDKにはいろいろと付属のexeが存在しています。(あまり語られていませんが...)
その中にnwjc.exeというJavaScriptを機械語(かな?)に変換して読みにくくしてやろう!というツールがあります。

nwjcの使用方法は、NW.jsのSDK版をダウンロードし、zipファイルに同梱されているnwjcを以下のようなコマンドで叩くだけです。お手軽ですね。

nwjc input.js output.js.bin

なお、この結果出力されたファイルは
NW.jsのバージョンを合わせなければ実行することができませんので、そこだけお気をつけください。
(normalでもdevでも問題ないです。バージョンの番号の話です。)

難読化されたファイルをコードとして利用する方法も、

nw.Window.get().evalNWBin(null, 'output.js.bin'); // 同期的にロードしてeval

だけと、お手軽で、非常にいい感じです。

コンパイル後ファイルは読み込み側に問題あり

...このまますんなり使えればどれだけ良かったか...

[0829/042332.674:ERROR:http_transport_win.cc(175)] WinHttpCrackUrl: URL は認識されているプロトコルを使用していません (0x2ee6)

他のコードと何ら変わりなく、nwjcで難読化し、evalNWBinする過程で上記のパッと見よくわからんエラーに出くわしました。
表示されている内容が内容だけに、
- fs.readFileしてはいけなかったか?
- 逆にGETに問題があったか?
- Ajaxか...?

などと、いろいろ考えられるパターンを探したのですが、
原因が全然見つからず、気がつけばAM 6:00という割と洒落にならない事態に...
そして巡り巡って見つけた結果がこちら

わいやんけえええ!!
しかも何が悲しいかって、

  • どうもファイルサイズの影響を受けているらしく、回避するにはコードを分割しなければならない
  • そもそもこのIssueはまだOpen

悲しい...悲しすぎる...

まとめ

あまりにも悲しい結果ですが、要約すると以下のとおりです。

DevToolを使わせないようにするには...

  • NW.jsはnormal版を使おう!
  • nw.exeとソースコードは結合しよう!

完全にソースコードを秘匿化するには...

  • nwjcを使ってJavaScriptのコードをコンパイルしよう!
  • でもnwjcは大きなファイルが受け取れないので、ソースコードは小さく分けよう!

リソースファイルは?

あきらめるか、独自で暗号化しよう!

一応おまけ程度にRPGツクールMVのコアスクリプトはMITライセンスのオープンソースで、音声や画像の暗号化・復号化の処理があったはず!ここが参考になるかも。

ただ、この処理、暗号化されていないjsonにパスをべた書きなのであまり意味がない...

以下バグってるかもしれませんが、

(function(){
    const _keys = ['${_RAND_ARRAY.join('\',\'')}'];
    const _key = _keys[${_USE}];
    const crypto = require('crypto');

    const _decrypt = function(text, key){
        let decipher = crypto.createDecipher('aes-256-ctr', key);
        let dec = decipher.update(text, 'hex', 'utf8')
        dec += decipher.final('utf8');
        return dec;
    };

    DataManager.loadDataFile = function(name, src) {
        var xhr = new XMLHttpRequest();
        var url = 'data/' + src;
        xhr.open('GET', url);
        xhr.overrideMimeType('application/json');
        xhr.onload = function() {
            if (xhr.status < 400) {
                window[name] = JSON.parse(_decrypt(xhr.responseText, _key));
                DataManager.onLoad(window[name]);
            }
        };
        xhr.onerror = this._mapLoader || function() {
            DataManager._errorUrl = DataManager._errorUrl || url;
        };
        window[name] = null;
        xhr.send();
    };

    PluginManager.loadScript = function(name) {
        var url = this._path + name;
        //nw.Window.get().evalNWBin(null, url);
        var request = new XMLHttpRequest();
        request.open('GET', url, false);
        request.send();
        var s = document.createElement('script');
        s.innerHTML = _decrypt(request.responseText, _key);
        console.log(s.innerHTML);
        document.body.appendChild(s);
    };

    PluginManager.setup($plugins);
    encrypterLoader = function(){
        SceneManager.run(Scene_Boot);
        if(process.versions['nw-flavor'] != 'normal'){
            Graphics.printError('Error', '【データ保護違反】ゲームエンジンが改造されている疑いがあります。');
            setTimeout(function(){
                process.exit();
            }, 1000);
        }
    };
    encrypterLoader();
    window.addEventListener('devtoolschange', function () {
        while(true) {
            debugger;
        }
    });
})()

_RAND_ARRAY_USEは各々で決めてください。また、node.jsに依存するのでRPGアツマールなどのWeb上・モバイル端末での実行はできなくなります。(探せばnodeモジュールじゃない版もありそうですが)

わざわざ配列にパスフレーズを入れているのはコンパイル後のファイルをバイナリエディタで開かれるとバレちゃうから読みにくくしてやろうという魂胆です。

こんな感じにmain.jsを書き換えてnwjcmain.jsを難読化、cryptoにて上記コードで利用するパスフレーズと同じパスフレーズで各JSONファイルを暗号化すれば解読されにくくなるのではないかと思います。

これでも時間をかければ解読できちゃいますけどね...

まーすげーめんどくさいですし、NW.js単体でのソースコード保護はちょっと無理そうというお話でした。