はじめに

JavaScript のソースコードの圧縮・難読化ツールについて調べたり試したりしたので記事にまとめた。

Google chrome の拡張機能の開発で javascript-obfuscator/javascript-obfuscator を使って minify をしていたが、Google さんから「Policy 違反」と言われてしまったため、何がまずかったのかと代替できそうなツールを探した。

※2019/09/28 現在では、新しいツール(terser/terser を選択)を使って minify したソースをまだ提出していないので、もし Chrome 拡張機能の JS 圧縮について調べていてこのページにたどり着いた場合は鵜呑みにしないように注意。

TL;DR

目次

  1. はじめに
  2. TL;DR
  3. 環境・条件
  4. 詳細
    1. javascript-obfuscator による圧縮で Chrome 拡張機能のポリシー違反
    2. 他ツールの調査と検証結果
      1. terser/terser
      2. javascript-obfuscator/javascript-obfuscator
      3. babel/minify
      4. google/closure-compiler-npm
      5. mishoo/UglifyJS2
  5. まとめ
  6. 参考文献

環境・条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.14.6
BuildVersion: 18G95

$ node -v
v12.7.0

$ npm -v
6.10.3

$ for package in terser javascript-obfuscator babel-minify google-closure-compiler uglify-js; do npm v $package | grep versions; done
terser@4.3.3 | BSD-2-Clause | deps: 3 | versions: 55
javascript-obfuscator@0.18.1 | BSD-2-Clause | deps: 18 | versions: 102
babel-minify@0.5.1 | MIT | deps: 7 | versions: 96
google-closure-compiler@20190909.0.0 | Apache-2.0 | deps: 9 | versions: 347
uglify-js@3.6.0 | BSD-2-Clause | deps: 2 | versions: 206

詳細

javascript-obfuscator による圧縮で Chrome 拡張機能のポリシー違反

Google chrome の拡張機能を javascript-obfuscator/javascript-obfuscator で圧縮していたら、ポリシー違反 のメールを受信した。

Chrome 拡張機能で使用しても良い圧縮は以下の通り。

Code Readability Requirements:

Developers must not obfuscate code or conceal functionality of their extension. This also applies to any external code or resource fetched by the extension package. Minification is allowed, including the following forms:

  • Removal of whitespace, newlines, code comments, and block delimiters
  • Shortening of variable and function names
  • Collapsing files together

https://developer.chrome.com/webstore/program_policies#content_policies より

以下、Google 翻訳結果。

コードの読みやすさの要件:

開発者は、コードを難読化したり、拡張機能を隠したりしないでください。 これは、拡張パッケージによってフェッチされる外部コードまたはリソースにも適用されます。 次のフォームを含む縮小が許可されます。

  • 空白、改行、コードコメント、ブロック区切り文字の削除
  • 変数名と関数名の短縮
  • ファイルをまとめて折りたたむ

実際に使用していたコマンドやオプションは以下の通り。

1
$ javascript-obfuscator XXX.js --compact true --string-array false --identifier-names-generator mangled --output XXX.js

以下のコードを↑のコマンドで変換してみる。

1
2
3
4
5
6
7
// 変換前
function hello() {
let hoge = "hello ";
let fuga = " ワールド";
console.log(hoge + fuga);
}
hello();
1
2
// 変換後
function hello(){let c='hello\x20';let d='\x20ワールド';console['log'](c+d);}hello();

変換結果を見てみると、半角空白が \x20 という特殊文字(16進数での ASCII コード指定)に置き換わっている。(Google から明確な回答が得られたわけではないので推測にはなるが) おそらくこの部分がまずかったものと思われる。

なお、--string-array false--identifier-names-generator mangled のオプションを指定しなかった場合はこうなる。(読みやすいように --compact false とした)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var _0x3c77 = ['hello\x20'];
(function (_0x4116ab, _0x5c3f18) {
var _0x16a7f6 = function (_0x4b16fd) {
while (--_0x4b16fd) {
_0x4116ab['push'](_0x4116ab['shift']());
}
};
_0x16a7f6(++_0x5c3f18);
}(_0x3c77, 0x15c));
var _0x2ec0 = function (_0xb479be, _0x4bb6ab) {
_0xb479be = _0xb479be - 0x0;
var _0x44c2ed = _0x3c77[_0xb479be];
return _0x44c2ed;
};
function hello() {
let _0x454ccc = _0x2ec0('0x0');
let _0x3cc0fe = '\x20ワールド';
console['log'](_0x454ccc + _0x3cc0fe);
}
hello();

確かにこのコードだと悪意のあるコードが紛れていないかの検証は厳しい。

他ツールの調査と検証結果

javascript-obfuscator/javascript-obfuscator に代わるツールを探した。

なお、各ツールで日本語(などのマルチバイト文字)の変換を制御できないかオプションを探したが、それらしいオプションは見つけられなかった。

terser/terser

最終的に terser/terser を使うことにした。

インストール(開発環境のみ)

1
$ npm i -D terser

圧縮コマンド。

1
$ terser --compress --mangle --ascii_only true --output XXX.js -- XXX.js

変換結果。

1
function hello(){console.log("hello  ワールド")}hello();

javascript-obfuscator/javascript-obfuscator

再掲。実は --reserved-strings というオプションがあり、それを使うと半角スペースの変換を抑制はできるので紹介だけしておく。

インストール(開発環境のみ)

1
$ npm i -D javascript-obfuscator

圧縮コマンド。

1
$ javascript-obfuscator XXX.js --compact true --string-array false --identifier-names-generator mangled --reserved-strings " " --output XXX.js

変換結果。

1
function hello(){let c='hello ';let d=' ワールド';console['log'](c+d);}hello();

これだけ見ると問題無さそうに見えるが、さらに次のコードを変換してみる。文字列の中でさらに引用符を付けて出力するだけのコード。

1
2
console.log("' hello '");
console.log('" wolrd "');

変換結果がこちら。

1
console['log']('' hello '');console['log']('" wolrd "');

見て分かる通り、このコードは SyntaxError: missing ) after argument list でエラーになる。

コード中の全ての箇所で、シングルクォーテーションを使っているなら良いが場合によってはダブルクォーテーションを使うこともあるだろうし、正直そんなことを気にしながら開発したくないし、既存コードを変換するのもダルいので却下。

ちなみに --reserved-strings " '\"" とするとこうなる。そうじゃないんだよなぁ…

1
console['log']('\x27\x20hello\x20\x27');console['log']('\x22\x20wolrd\x20\x22');

babel/minify

日本語文字列が unicode に変換されるので今回の目的には合わず。

インストール(開発環境のみ)

1
$ npm i -D babel-minify

圧縮コマンド。

1
$ minify XXX.js --mangle --out-file XXX.js

変換結果。

1
function hello(){console.log("hello "+" \u30EF\u30FC\u30EB\u30C9")}hello();

google/closure-compiler-npm

日本語文字列が unicode に変換されるので今回の目的には合わず。

インストール(開発環境のみ)

1
$ npm i -D google-closure-compiler

圧縮コマンド。

1
$ google-closure-compiler --js XXX.js --js_output_file XXX.js

変換結果。

1
function hello(){console.log("hello  \u30ef\u30fc\u30eb\u30c9")}hello();

mishoo/UglifyJS2

ECMAScript2015 以降未対応なので、圧縮実行時にエラーとなる。

インストール(開発環境のみ)

1
$ npm i -D uglify-js

圧縮コマンド、let に対応していないのでエラー。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ uglifyjs --compress --mangle --output XXX.js XXX.js
Parse error at XXX.js:2,6
let hoge = "hello ";
^
ERROR: Unexpected token: name «hoge», expected: punc «;»
at JS_Parse_Error.get (eval at <anonymous> (/Users/xxxx/node_modules/uglify-js/tools/node.js:20:1), <anonymous>:71:23)
at fatal (/Users/xxxx/node_modules/uglify-js/bin/uglifyjs:298:27)
at run (/Users/xxxx/node_modules/uglify-js/bin/uglifyjs:241:9)
at Object.<anonymous> (/Users/xxxx/node_modules/uglify-js/bin/uglifyjs:167:5)
at Module._compile (internal/modules/cjs/loader.js:777:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:788:10)
at Module.load (internal/modules/cjs/loader.js:643:32)
at Function.Module._load (internal/modules/cjs/loader.js:556:12)
at Function.Module.runMain (internal/modules/cjs/loader.js:840:10)
at internal/main/run_main_module.js:17:11

まとめ

参考文献

関連記事