詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Google Meet のミュートをツールバーから切り替える拡張を作った

Google Meet Mute Toggler - Chrome ウェブストア

こんなやつです。

Google Meet で仕事をすることが多くなった昨今、咳払いや生活音といったよんどころない事情によりマイクをミュートにしたいこともあるわけですが、ミュートボタンは普段隠れていて急に出てこないし、切断ボタンの隣りにあってちょっと怖い。正確なエイムが求められるのは小さなストレスになってるわけですね。この切り替えボタンをツールバーの常に同じ位置に置くことで、それを軽減させようとするものです。

Chrome 拡張のストアって決まった大きさの画像をアップロードしないといけないので面倒だったんだけど、画面の録画を YouTube に上げたらいいってことがわかりました。

Google Meet Mute Toggler Demo - YouTube

ソースコードはこれ。

GitHub - motemen/chrome-meet-mute-toggler: Easily toggle your microphone mutes on Google Meet meetings, by clicking toolbar button.

最近のウェブアプリは外部からいじりやすいクラスがついてなくて困るので、aria-label 属性へのマッチで要素を探してるのだが、これがまた言語によってスペースが入ってたり入ってなかったり大文字小文字だったりで大変だ……。

  document.querySelector<HTMLElement>(
    [
      '[role=button][aria-label*="⌘+D" i]',
      '[role=button][aria-label*="⌘ + D" i]',
      '[role=button][aria-label*="Ctrl+D" i]',
      '[role=button][aria-label*="Ctrl + D" i]',
    ].join(",")
  );

Chromebookを買った

iPad が使えなくなってしまったので電子書籍を読むときにちょっと困っていて、8インチくらいの安い Android タブレットでもないかなーと社内で相談したところ、「Chromebook はどうか」と薦められた。えー10インチだし予算オーバーだし……と思っていたけど、魂が否応なく惹かれてしまう衝動があって Lenovo Chromebook Duet を買ってしまったのだった。運命だったのかもしれない。

結論から言うと大変満足している。ふだん Android 使ってる人にはオススメしたい!

ハードウェア

まず充電が USB-C で行えるのはすばらしいですね。

キーボードがついてるので普通のパソコンにもなるし、外せばタブレットにもなる。まあキーボード使うのはキーボードを使うってことを目的にした遊びだけで、基本的にタブレットとして使ってるけど。トラックパッドが付いてるけど、MacBook と比べるとこれの滑りがわるくて最初面食らった。

試しにこのエントリも Chromebook で書いてみていたけど、まあ書けないことはない。ふだんは AZIK を使ってるんだけど、Chrome OS の IME ではそのようにカスタマイズできないので、ちょっとつらい。Play ストアからアプリを入れられるので Gboard を試してみたけど、なんか安定しない感じがある。

あと指紋アンロックみたいな高度なことはできなくて、当初はオイ~と思ったけど後述するスマートアンロックで割と用を足せるのでそんなに不満はないかな~。

基本的にWebアプリを使う世界観

というか、Google Chrome を使う。2020年ともなると人間の活動の多くはウェブで済んでしまうので、ほぼ困らないのであった。

スマートフォン向けではなく、PC で使ってるいつもの Chrome がタブレット上で使えるのはちょっと不思議な感覚がある。拡張も普通に使えるので、タブレットでブラウジングしてるときに AutoPagerize が効いて大変快適。

ただ、PC とプロファイルを共有していると、インストールしている拡張やアドレスバーに置くボタンの設定まで同じになってしまうのでちょっと困る。タブレットでは使わない拡張(開発用ツールとか)もあるし、狭い画面であまり拡張ボタン増やしたくないんだよな……。デバイスごとに使い分けられるといいんだけど、致命的ではないのであきらめてボタンの数を減らして使ってる。

ウェブアプリは Chrome のメニューからショートカットをホーム画面に作るか、対応している場合はアプリとしてインストールできる。

タブレットモードで Scrapbox を使うときは、長押しが Chrome の右クリックメニューを呼び出してしまうのでやや使いづらい。さいわい、貼り付けなどは Chrome のメニューから行えるのでそれでまかなっている。こまかい範囲選択とかは難しそう。ただしアプリとしてインストールするとメニューが呼び出せないので、タブとして開くのがオススメ。

Androidアプリも使える

Google Play ストアから Android アプリをインストールできる。もともと Kindle と Google Play ブックが使いたかったのでこれは必須条件だ! 多くのことがウェブで可能だとはいえ、アプリで行いたいことも多い。たとえば YouTube はアプリで観たほうがスムーズな気がするな。けどそれくらい。音楽や絵をやる人はまた違うのだと思うが。

Chrome からインテントで URL を共有する、というようなことができないので、インテント目的で利用していたアプリは拡張に置き換わる感じ。

そしてブラウザではできない体験と言えば、カスタムキャストである。でかいデバイスを買ったらぜひインストールしたいところだ。 Android であれば .maid ファイルを Android/data/jp.customcast.cc2/files/Maid に Android File Transfer で送り込めばスマホで作成したキャラクターをインポートできたのだけど、Chromebook の場合はこれが使えないので、ひと手間必要だった。

……と思いきや、ファイラーの設定メニューから Android 領域を表示して(ファイルを開く、保存する、削除する - Chromebook ヘルプ)ファイルをコピペすればよさそう。かんたんじゃん。

一応メモっておくと自分は 開発環境 | Android デベロッパー | Android Developers にしたがって ADB デバッグを有効を使えるようにして、Linux コンソールから adb push でファイルを置いた。

f:id:motemen:20201022135514p:plain

やったね!(わざとらしいスクショ)

ただしパフォーマンスはよくない。でかい画面で見られるだけで僥倖ですが。あと、カメラがアクティブになってるランプが常時つきっぱなしになって、怖い。

Linuxでも遊べる

上に書いたように Linux コンソールが使える! のでとりあえず Homebrew を入れてみようとしたのだけど、Ruby のビルドから始めないといけなかった。Linuxbrew on IdeaPad Duet - pn11 に書いてあるような感じ。

ghq くらい入れておくかと思ったけど、アーキテクチャが arm64 な関係で Go も含めていろいろ自前でビルドしてやる必要があって、Homebrew をつかううまみはなかった……。

Androidとの連携が便利

指紋アンロックなどはない、と上に書いたが、Smart Lock というやつで、Android スマホをアンロックすれば Chrombook にログインできる、という連携が可能。スマホは指紋アンロックできるわけなので、これで割とまかなえる。ときどき連携がうまく働かないときもあるけど。 ただスマホの画面がずっとオンになってると常時アンロック可能になってしまうのでそこは気をつける必要がある。仕事には使ってません。

あと、スマホを連携しておくとインスタントテザリングというのも使えて、Chromebook の側からスマホのテザリングを有効にして接続していくことができる。電車で使うときに便利。

……という Chromebook がいまなら Amazon で10%オフクーポンつきですよ! 今年いっぱいなので Chromebook 仲間になっていいこと教えてください。

中間証明書のないサーバにアクセスする: 言語別の実装

前回の記事のつづきです。

中間証明書のないサーバにアクセスする - 詩と創作・思索のひろば

さて、この方法で信頼できる中間証明書を手元に集めることができたので、これをプログラムから利用するにはどうしたらよいのか、ということを見ていく。

実験したいのはこういうことである:

  • 信頼できる中間証明書を手元に置いておくことで、中間証明書がないが本来は信頼できるサイトにアクセスできる。
    • 欠けている中間証明書を含めたチェーンで、サイトの証明書を検証できる。
  • 信頼できる中間証明書を手元に置いても、普通のサイトが正しく取得できる。
    • システムがもともと持っている証明書を壊したりしていない。
  • 信頼できない中間証明書を手元に置いてしまっても、本来信頼できないサイトにはアクセスできない。
    • 雑に中間証明書を持っておいて、実行時にそれらの中間証明書まで含めて検証する、というアプローチが取れるか? という実験。
    • 自己署名ルートによって署名された中間証明書を手元に置いて、その中間証明書によって署名されたサイトにアクセスしても、そのルート証明書を信頼していないので失敗する。
    • ついでに、中間証明書だけで検証してしまう(ルートまでたどらない)方法も検討しておく。

言語は openssl コマンド(言語じゃないけど)、Perl、Go、Node。まあ、よく使ってるやつです。

リポジトリは https://github.com/motemen/sketch-interm-certs 。README に書いてあるとおりに進めれば同じ手順で確認できます。以下、各言語の様子を見ていく。

  • 自己署名ルート以下は cfssl で作っている。これがとにかく楽。
  • 信頼できる中間証明書と、信頼できない中間証明書をまとめて /opt/intermediate-certs/ca-bundle.crt というファイルにしている。
  • システムの信頼する証明書は /etc/ssl/certs ディレクトリに保管されている。

openssl コマンド

https://github.com/motemen/sketch-interm-certs/blob/b896e1f/tests/openssl.sh

openssl s_client でテストする。中間証明書バンドルを -CAfile で指定するが、そうするとシステムの証明書を使わなくなってしまうので、あわせて -CApath /etc/ssl/certs する必要がある。

また、-partial_chain を指定することで中間証明書だけでサイトを検証できるようになる(チェーンをルートまでたどらない)。

Perl

https://github.com/motemen/sketch-interm-certs/blob/b896e1f/tests/perl.t

LWP::UserAgent を利用する。ssl_opts オプションに SSL_ca_fileSSL_ca_path を指定する。openssl の場合と似てますね。内部で OpenSSL 使ってるので当然といえば当然か。

これらはそれぞれ PERL_LWP_SSL_CA_FILEPERL_LWP_SSL_CA_PATH 環境変数で指定できるらしい。けど普通はオープンインターネット向けのエージェント以外にも内部エンドポイント向けのエージェントも作りたいわけで、そうなるとデフォルト値をゆるめに運用はしたくないと思うので、検証はしてない(以下に書いている事情もある)。

my $ua_with_interm_partial_chain = LWP::UserAgent->new(
    ssl_opts => {
        SSL_ca_file => '/opt/intermediate-certs/ca-bundle.crt',
        SSL_ca_path => '/etc/ssl/certs',
    }
);

ただし、気をつけなければならないのは、デフォルトでは -partial_chain が指定されたかのようなふるまいをすること。手元に信頼できないルートによって署名された証明書を持っている場合、その証明書を信頼することになっている。これは openssl s_client と違う挙動。

どういう理由かはよくわからないんだけど、IO::Socket::SSL の中でそういうことをしている

これを回避するには、ssl_opts にコールバックを指定して、OpenSSL の API を呼んでやる。

my $ua_with_interm_full_chain = LWP::UserAgent->new(
    ssl_opts => {
        SSL_ca_file => '/opt/intermediate-certs/ca-bundle.crt',
        SSL_ca_path => '/etc/ssl/certs',
        SSL_create_ctx_callback => sub {
            my $ctx = shift;
            my $param = Net::SSLeay::CTX_get0_param($ctx);
            my $rv = Net::SSLeay::X509_VERIFY_PARAM_get_flags($param);
            Net::SSLeay::X509_VERIFY_PARAM_clear_flags($param, Net::SSLeay::X509_V_FLAG_PARTIAL_CHAIN());
            Net::SSLeay::CTX_set1_param($ctx, $param);
        },
    }
);

Go

https://github.com/motemen/sketch-interm-certs/blob/b896e1f/tests/go_test.go

Go は TLS の実装に OpenSSL を利用していない。ドキュメントを読む限りでは crypt/tls.Config の RootCAs をカスタマイズしてやればよさそう。

func buildTransportWithIntermCertsParialChain() http.RoundTripper {
    rootCAs, err := x509.SystemCertPool()
    if err != nil {
        log.Fatal(err)
    }

    intermPEM, err := ioutil.ReadFile("/opt/intermediate-certs/ca-bundle.crt")
    if err != nil {
        log.Fatal(err)
    }

    rootCAs.AppendCertsFromPEM(intermPEM)

    return &http.Transport{
        TLSClientConfig: &tls.Config{
            RootCAs: rootCAs,
        },
    }
}

ただ、名前のとおりこれはルート証明書を格納するものなので、信頼できない中間証明書をここに入れることはできない。

いちおう中間証明書を与えつつチェーン全部を検証する実装らしきものは書けたけど、専門家の書いたものではないのでこれを信用してはいけない。まあ、参考程度に。

func buildTransportWithIntermCertsFullChain() http.RoundTripper {
    rootCAs, err := x509.SystemCertPool()
    if err != nil {
        log.Fatal(err)
    }

    intermPEM, err := ioutil.ReadFile("/opt/intermediate-certs/ca-bundle.crt")
    if err != nil {
        log.Fatal(err)
    }

    return &http.Transport{
        DialTLS: func(network, addr string) (net.Conn, error) {
            host, _, err := net.SplitHostPort(addr)
            if err != nil {
                return nil, err
            }

            conf := &tls.Config{
                InsecureSkipVerify: true,
                VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {

                    // https://github.com/golang/go/blob/go1.14.4/src/crypto/tls/handshake_client.go#L793
                    certs := make([]*x509.Certificate, len(rawCerts))
                    for i, asn1Data := range rawCerts {
                        cert, err := x509.ParseCertificate(asn1Data)
                        if err != nil {
                            return errors.New("tls: failed to parse certificate from server: " + err.Error())
                        }
                        certs[i] = cert
                    }

                    opts := x509.VerifyOptions{
                        Roots:         rootCAs,
                        CurrentTime:   time.Now(),
                        DNSName:       host,
                        Intermediates: x509.NewCertPool(),
                    }
                    for _, cert := range certs[1:] {
                        opts.Intermediates.AddCert(cert)
                    }

                    opts.Intermediates.AppendCertsFromPEM(intermPEM)
                    _, err = certs[0].Verify(opts)

                    return err
                },
            }

            return tls.Dial(network, addr, conf)
        },
    }
}

Node

https://github.com/motemen/sketch-interm-certs/blob/b896e1f/tests/node.test.js

Node も OpenSSL を利用している。ぜんぜん使ったことはないのだけど、https.Agent に設定を詰めるのがいいみたいだ。こちらは非常にかんたん。

  const agent = new https.Agent({
    ca: [
      fs.readFileSync("/etc/ssl/certs/ca-certificates.crt"),
      fs.readFileSync("/opt/intermediate-certs/ca-bundle.crt")
    ]
  });

ca オプションで証明書のコンテンツを指定する。ドキュメントから明らかじゃないように見えるんだけど、tls.connect の一部のオプションも Agent に指定できるみたい。ca には内容を指定するので、openssl コマンドや Perl で見たようにディレクトリを指定することはできない。

そして Perl の場合と違い、中間証明書を与えてもチェーン全体が検証されるようだ。これが意図通りなのかどうかまではわからない。内部で X509_STORE_add_cert使われているけど、Untrusted objects should not be added in this way.って書いてあるんだよな……。

逆に、自分の見たかぎりでは -partial_chain 相当の挙動を実現することもできなそうだった。

また、NODE_EXTRA_CA_CERTS環境変数を指定することでも ca にそのファイルの中身を追加したかのような挙動を実現できる。

まとめ

いろいろな言語による HTTPS (TLS) 証明書の扱いを見てみました。

冒頭に書いていた「雑に中間証明書を持っておいて、実行時にそれらの中間証明書まで含めて検証する、というアプローチが取れるか?」という疑問に関しては、わりと厳しそう。何にしろ、中間証明書は信頼できるものだけを手元においておいたほうがよいですね。

はてなで一緒に働きませんか?