LoginSignup
0
1

Nuxt3でjavascript-obfuscationを使う方法

Last updated at Posted at 2023-10-24

めちゃくちゃ沼った上に、解決方法がかなり複雑なので備忘録も兼ねて書いておきます。

tl;dr

npm i git+https://github.com/ajiken4610/vite-plugin-javascript-obfuscator
replacer.js
export default (matched) => {
  return {
    name: "vite-plugin-replace-resolve-component",
    transform(src, id) {
      let ret = src;
      ret = ret.replaceAll(
        /(?<!function )(_?resolveComponent)\(["']([^\)]+?)["']\)/g,
        (_, resolve, name) => {
          const ret = `__${resolve}${name}`;
          matched.indexOf(ret) === -1 && matched.push(ret);
          return `"${ret}"`;
        },
      );
      return {
        code: ret,
        map: null,
      };
    },
  };
};
surrounder.js
export default () => {
  return {
    name: "vite-plugin-surround-resolve-component",
    transform(src) {
      let ret = src;
      ret = ret.replaceAll(
        /["']__(_?resolveComponent)([^"']+)["']/g,
        (_, resolve, name) => `(${resolve}("${name}"))`,
      );
      return {
        code: ret,
        map: null,
      };
    },
  };
};

nuxt.config.ts
const reservedStrings: string[] = []
...
vite: {
      plugins: [
        replacer(reservedStrings),
        obfuscator({
          options: {
            optionsPreset: "high-obfuscation",
            reservedStrings,
          },
          apply: "build",
        }),
        surrounder(),
      ],

注:build.minify === false && selfDefending === trueだと、ブラウザがフリーズします。

何をしているか

やっていることは意外と単純で、

  1. resolveComponent("ComponentName")"__resolveComponentComponentName"という文字列に置換する
  2. obfuscatorに、"__resolveComponentComponentName"以外をobfuscateしてもらう。
  3. "__resolveComponentComponentName"(resolveComponent("ComponentName"))に置換し戻す。

をしています。

経緯

ちょっと難読化してみたかっただけだけど、思ってた以上にここにたどり着くまでが長かった。

この処理をしないとどうなるか

レンダリングすると、以下のようになります。

<div id="__nuxt">
  <nuxtpage></nuxtpage>
</div>

私は、これを見ててっきりNuxtPageがTree-shakeされてしまったんだと思っていました。

実際、この状態のソースコードにはNuxtPageのものが含まれません。

そこで、私は、明示的にNuxt関係のコンポーネントをビルドに含めてみたりいろいろしてたのですが、23時間パソコンとぶっ通しでにらめっこしてようやく発見しました。

このソースを見てもらえばわかるのですが、Nuxt3は正規表現に一致したresolveComponent関数を見つけて、それに関するものをimportするという形式をとっているようです。

この正規表現がミソでした。

見るとわかりますが、(resolveComponent("SomeComponent"))というように、resolveComponent自体が()でくくられていないと機能しません。これはソース見ないとわからないよね。

ということで、原因はresolveComponent()が別の構文に変換されてしまうと、Nuxt3はそれを追跡できないから、でした。

では、tl;drに書いたことについてそれぞれ説明していきます。

git+https://github.com/ajiken4610/vite-plugin-javascript-obfuscator

このレポは https://github.com/elmeet/vite-plugin-javascript-obfuscator からforkしたものです。

  • javascript-obfuscatorのバージョンをlatestに
  • vite plugin の enforce:"post"の削除

以上の2点を変更しています。

重要なのは2つ目で、enforce:"post"が指定されていると、その前後にほかのプラグインを挟むのが難しくなります。なので、削除する必要がありました。

replacer.js

見やすいように、コメントを付けたものをもう一度貼っておきます。

replacer.js
export default (matched) => {
  return {
    name: "vite-plugin-replace-resolve-component",
    transform(src, id) {
      // 引数をそのまま変更するのは嫌だったので、変数に移す。
      let ret = src;
      ret = ret.replaceAll(
        // この正規表現については後述
        /(?<!function )(_?resolveComponent)\(["']([^\)]+?)["']\)/g,
        (_, resolve, name) => {
          // ___?resolveComponentSomeComponentNameに置換。
          const ret = `__${resolve}${name}`;
          // obfuscatorに無視してもらうために、配列に追加
          matched.indexOf(ret) === -1 && matched.push(ret);
          // 文字列として置換するために、二重引用符を付ける。
          return `"${ret}"`;
        },
      );
      return {
        code: ret,
        map: null,
      };
    },
  };
};

引数matched

どうにかしてobfuscatorに見つかったコンポーネントを置換した文字列を無視してもらう必要があります。
なので、matchedというstring[]を引数にとり、それをポインタとして使うことで、解決しました。

正規表現

前述のNuxt3のresolveComponentを見つける正規表現を見ると、_resolveComponentのように、前に_がついている場合があるようです。それはそのまま戻さなくてはならないので、

  • resolveComponent("SomeName")なら"__resolveComponentSomeName"に、
  • _resolveComponent("SomeName")なら"___resolveComponentSomeName"というように、

前につけるアンダーバーの数で区別するようにしています。

この説明の後にこれを見ると、(?<!function )がついているのがわかります。

ret.replaceAll(/(?<!function )(_?resolveComponent)\(["']([^\)]+?)["']\)/g,...)

なぜこんなものがついているかというと、ソースコード中のresolveComponentの定義部分は置換する必要がない、というより置換してしまってはエラーになるからです。

どういうことかは以下を見てもらえばわかります。

正しい例

_resolveComponent("NuxtLink")

が、以下に置換される

"___resolveComponentNuxtLink"

おかしい例

function _resolveComponent("NuxtLink"){ ... }

が、以下に置換される

function "___resolveComponentNuxtLink"{ ... }

...?

あれれ、おかしい例はもともと文法的にあり得なかった。

関数定義時って引数を二重引用符でくくらないですよね。

(?<!function )無しでも問題ないかもしれませんが、試してません。動いてればそれでいいのだ。

何はともあれ、これで文字列に置換することができました。

nuxt.config.ts

あとはこれをobfuscatorの引数に渡してやります。

nuxt.config.ts
const reservedStrings: string[] = []
...
vite: {
      plugins: [
        replacer(reservedStrings),
        obfuscator({
          options: {
            optionsPreset: "high-obfuscation",
            reservedStrings,
          },
          apply: "build",
        }),
        surrounder(),
      ],

これに関しては説明することは無いです。reservedStringsを定義して、それをreplacerobfuscatorに渡しているだけですので。

surrounder.js

コメントを付けたものを再掲。

surrounder.js
export default () => {
  return {
    name: "vite-plugin-surround-resolve-component",
    transform(src) {
      // 引数に代入するのは以下略
      let ret = src;
      ret = ret.replaceAll(
        // 後述
        /["']__(_?resolveComponent)([^"']+)["']/g,
        // ___?resolveComponentSomeComponentNameを
        // (_?resolveComponent("SomeComponentname"))にしているだけです。
        // 先に書いた、Nuxt3の実装では、これがかっこでくくられていないと置換してくれません。
        (_, resolve, name) => `(${resolve}("${name}"))`,
      );
      return {
        code: ret,
        map: null,
      };
    },
  };
};

正規表現

ret.replaceAll(/["']__(_?resolveComponent)([^"']+)["']/g,...)

ポイントは以下の通りです。

  • ["']: obfuscatorは二重引用符にするか一重にするか不明だったので、どちらにもマッチするようにしています。
  • _?: アンダーバーの数を見ています。2個ならresolveComponentになるし、3個なら_resolveComponentになります。

___resolveComponentComponentName(_resolveComponent("ComponentName"))に置換しているだけです。

まとめ

resolveComponentが別の構文に変換されないように、以下の手順が必要

  1. _?resolveComponent(...)を元に戻せる文字列に置換する
  2. obfuscatorにその文字列以外をobfuscateしてもらう
  3. 置換した文字列を、(_?resolveComponent("..."))に戻す

この手順でコンポーネントが無事resolveされました。

参考までに。

追記

javascript-obfuscatiorはimport.meta.XXXを置換できないようなので、hot-module-replacementを使うとimport.meta.hotにアクセスしようとしておかしくなります。dev時にはobfuscatorをオフにしてください。

0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1