hnwの日記

Karabiner-Elements で日本語キーボードを英語キーボードとして使う設定

直前の記事「Mac mini 2018を買っての感想」の通りですが、私は約3年ぶりにmacOSのマシンを買い換えまして、macOSのバージョンがEl CapitanからMojaveまで一気に3バージョン上がりました。

そこで悩んだのがキーボード配置変更ソフトです。それまでは Karabiner という有名ソフトを愛用していましたが、最近のmacOSでは別実装である Karabiner-Elements しか動きません。設定などは一からやり直しになってしまいます。

実際に Karabiner-Elements を試してみると、大半の機能についてはスムーズに移行できました。しかし、私が使っていた機能が一つだけデフォルトで用意されていません。具体的には「Use Japanese Keyboard as US Keyboard」という日本語キーボードを英語配列で使う設定です。

そこで、Karabiner-Elements版の「Use Japanese Keyboard as US Keyboard」を作ってみました。

これは Karabiner-Elements の issue #167 「change Japanese keyboard to US(JIS to ANSI) 」でsteveonjavaさんが提示したJSONをさらに修正したものです1

使い方

上記のJSONファイルを $HOME/.config/karabiner/assets/complex_modifications 以下に配置し、「Preferences…」「Complex Modifications」から「Add rule」「Use Japanese Keyboard as US Keyboard」を有効にしてください。

これを有効にすると、キーボード配置は下図のようになります。

f:id:hnw:20190104163128p:plain
適用後のキー配列(CC BY-SA 3.0 the original by Denelson83, modified by hnw)

「`~」キーが多すぎだろ、という意見もあると思いますが、Karabiner時代の設定がそうなっていたのでまあいいかという気持ちです。

Ctrl+[ をescキーとして使いたい場合は「Ctrl+@ to Escape」も有効にした上で「Use Japanese Keyboard as US Keyboard」より優先度を上げてください。

f:id:hnw:20190104172856p:plain
私の「Complex Modifications」の設定

他の解決方法

この設定を使わなくても、「システム環境設定」「キーボード」からレイアウト変更ができます。これで困らないならこちらの方が素直な解だと思います。


  1. 主な修正点は「JISキーボードのときだけレイアウト変更するようにした」「Cmd+記号など修飾キーと組み合わせた場合にも対応」の2点です。

Mac mini 2018を買っての感想

新年明けましておめでとうございます。いきなり去年の話をします。

2018年12月に自宅にMac mini 2018を買いました。シルバーグレイのちょっとオシャレな奴です。

Apple Mac mini (3.6GHzクアッドコアIntel Core i3プロセッサ, 128GB)
Apple Mac mini MRTR2J/A

同じ機種を買おうと思っている人の参考になれば、ということで感想文を書いてみます。

何をいくらで買ったか

下記のものを12月初旬にビックカメラで買いました。

  • Mac mini 2018 (第8世代 Core i3 3.6GHz、8GBメモリ、128GB SSD)96973円
  • 16GBメモリ(DDR4 SO-DIMM 2666MHz) 20898円 x2

計138769円。安くない買い物ですし、それでいてスペックは微妙に見えるかもしれません。

これはPayPayの「100億円あげちゃうキャンペーン」で衝動買いした結果です。今すぐビックカメラで買える一番おトクなものは何か?と考えた結果こうなりました。

自由に選べる状況であれば、店舗で買える高い方のモデル(Core i5 3.0GHz、8GBメモリ、256GB SSD)の方が良かった気がします。

自分でメモリ差し替えをして死にかけた

この新しいMac miniですが、公式にはメモリのアップグレードは個人でできないことになっています。とはいえネット上を探すと自分でメモリ換装している人も多いので、自力でやってみることにしました。Appleでカスタマイズすると6.6万円追加になるところ、4万円で済むならおトクですよね。

ところが、Mac miniを開けてみて気づいたんですがメモリ交換にはセキュリティートルクスドライバーが必要です。ネット上の記事をみるとみんなスイスイ開けてるんですが、私は普通のトルクスドライバーしか持っていなかったので難儀しました。買う前に調べておくべきでした。

数日後にAmazonで安いセキュリティートルクスドライバーを買って分解してメモリを交換してみました。すると画面が真っ暗で起動しません。新たなトラブル発生で焦りましたが、再度分解して改めてメモリを差し直したところ無事動作しました。メモリがきっちり差さっていなかっただけでした。

私の利用したメモリはCFD販売の「D4N2666PS-16G」というものです。DDR4 260pin 2666MHzのメモリなら大体動くんじゃないでしょうか(保証はできませんが)。

ちなみにメモリを認識しない場合、Mac miniの電源LEDが長い点滅(5秒に1回)をします。同じミスをする人はいないと思いますが、ご参考まで。

第8世代Core i3は十分な性能

メモリの交換も終わってMac miniが無事使えるようになったわけですが、今のところスペックについては不満点はありません。特にCPU性能が不安だったのですが、現時点ではそれほどCPUについてストレスを感じていません。

これまでCore i3というと遅いイメージがあったCPUだった気がしますが、今回の第8世代からCore i3のコア数が2から4になり、性能面では大きく改善されたようです。

もちろん作業内容によってはCPUのアップグレードは必須でしょう。写真や動画の編集をするような人だと上のスペックにした方がいいのかもしれません。

4Kディスプレイを常用するようになった

Mac mini 2018のHDMIポートはHDMI 2.0に対応しており、HDMIケーブル経由で4K 60Hz出力が可能です。

私はLGの24インチ 4Kディスプレイを購入しましたが、問題なく4K 60Hz表示できています。

ちなみに24インチで4K表示すると文字が小さくなってしまって厳しいので3K表示(3008 x 1692)で使っています。表示用の解像度はRetina MBPなどと同様「システム環境設定」-「ディスプレイ」から選択できます。

f:id:hnw:20190101141831p:plain
表示用解像度が5段階で選択できます

私は今までFull HDのディスプレイしか使ったことがありませんでしたが、情報量が増えるだけで世界が変わりますね。とはいえ家の広さ的には24インチや27インチで3K表示くらいが自分に合っている気がします。

スリープ復帰後にディスプレイが全部緑になる問題

しばらく使っていると、スリープから復帰したときに低確率で画面が緑になる問題に気づきました。正確にはマウスカーソルのエッジだけ黒く見え、それ以外が全て緑一色になります。この状態になるとHDMIケーブルを抜き差ししても直りません。画面表示以外は正常に動作しているようです。

ネット上に同様の挙動の報告は無いようですが、OSもしくはハードウェアの問題だという印象です。もしくは、私がメモリ換装の際に何かを破壊したのかもしれません。

ひとまずスリープしない設定にすることで問題は再現しなくなりました。

corespeechdがCPUを食う問題

しばらく使っていると、corespeechdというSiri関連のデーモンが時々1コアを使いきっていることに気づきました。

私はSiriを無効にしていたのですが、検索してみるとSiriを一度有効にしてから無効にすると直るという報告を見つけました。試してみると、実際に再現しなくなりました。

メモリは32GBあっても簡単に使い切る

このMac miniはメモリ換装の結果32GB搭載になりました。組んだ時点では絶対に使いきれないと思っていましたが、意外と頻繁にスワップアウトします。だいたい犯人はWebブラウザです。

先ほども重く感じたのでChromeを殺してみたところ、メモリが20GBほど空きました。以前のメモリ16GBのマシンではタブを100個も開くと重くて耐えられなくなっていたのが、今は200個ほどまで耐えられるようになったという印象です*1

ストレージは将来確実に不足する

私はストレージ128GBのモデルを買ってしまったわけですが、これは今どき厳しいサイズだと思います。特に音楽や電子書籍を大量に持っているような人だと絶望的でしょう。

私の場合はプログラムを書く以外に特に趣味がないので絶対無理というわけではありませんが、 それでも git clone したり docker pull したりするだけでストレージ容量はモリモリ減っていきます。長期的にストレージ不足は避けられないでしょう。

とはいえ目先は128GBでやりくりして、不足してくるころにはThunerbolt 3接続の速くて安いSSDが出ているはずなのでそれを買おう、と楽観的に考えています。

まとめ

Mac Mini 2018の安いモデルを買いました。買ってみての感想は次のようなものです。

  • コスパ良し
  • CPUはCore i5が良かった気もする、とはいえ第8世代 Core i3で十分戦える
  • メモリ32GBは案外快適なのでオススメ、最低でも16GBは欲しい
  • ストレージは最低でも256GBは欲しい、安い方のモデルはそこが厳しい
  • ディスプレイは4Kにすべき(Full HD使い回しは逆にもったいない)

余談:PayPay当選しました

最後に自慢になりますが、この買い物でPayPay満額当選しました。ラッキーですね。

PayPay10万円分当選の図
PayPay10万円分当選の図

*1:あくまで感覚値であり、本当にChromeのせいなのかは不明です

GAE/SE PHP 7.2環境は実用性が高そうだという話

筆者の周囲だけかもしれませんが、さいきんGoogle App Engine Standard Environment(以下GAE/SE)が再注目されつつあるように思います。今回筆者もgVisorベースのGAE/SE PHP 7.2環境に触ってみたので、その内容を紹介します。

GAE/SEとは

GAE/SEは元祖PaaSとも言えるような、Googleが提供するフルマネージド環境です。以前からJavaPython、Go、PHPの4言語の環境が提供されていましたが、Go以外の言語のバージョンアップは長いこと提供されておらず、Googleの本気度に疑問を持っていた人も多かったように思います(私もその一人でした)。

ところが最近になってNode.js 8、Java 8、Python 3.7、PHP 7.2と立て続けに新バージョンを提供してきており、Googleが水面下でGAE/SEに開発リソースを投入していたことが明らかになってきました。

この急ピッチの言語提供に一役買っているのが新サンドボックス実装であるgVisorです。私のざっくりの理解ではLinuxシステムコール類をユーザーランドで提供するようなもので、既に多くのOSSが修正なしで動くような対応状況になっているようです。このgVIsorはGoでフルスクラッチ実装されており、ソースコードも公開されています。

サンドボックスのGAE/SE PHP 5.5環境とPHP 7.2環境の違い

以前のGAE/SE PHP 5.5環境を使ったことがある人は、GAEは制約が強い環境だという印象を持っているかもしれません。実際、旧サンドボックス上で動作しているPHP 5.5環境には次のような制約がありました。

  • PHP本体にかなり大きなパッチが当たっている
    • SAPI名からして「Google AppEngine PHP Runtime SAPI」と普通ではない雰囲気
  • PHPから読めるディレクトリが非常に少ない
    • 自分のデプロイしたファイルくらいしか見えない
  • PHPから書き込めるディレクトリはおそらく無い
    • ファイルを作りたい場合GCSに書く必要がある
  • コマンド実行が許可されていない
    • そもそもプロセスの概念が存在する環境なのか疑問

特にファイル操作とコマンド実行の制約が大きく、既存のPHPアプリケーションがそのまま動くとは言いがたい状況でした。

一方、PHP 7.2環境は次のような状況です。

  • 普通のPHPが動いている
    • SAPIはphp-fpm
  • PHPから全ファイルが見える
  • /tmpがtmpfsになっており、PHPから書き込める
  • コマンド実行ができる
    • /usr/bin などに置いてあるバイナリが実行可能
    • Ubuntux86_64バイナリをデプロイすると実行可能
  • わりと普通のLinux環境っぽい
    • /proc/devがある(まだ少し足りていない雰囲気)

サンドボックス環境に比べれば圧倒的に普通の環境と言えそうです。PHP 5.5時代にGAEは厳しいなーと思った人がいたとしても、今回は当時とは全然違う印象になるのではないでしょうか。

PHP 7.2環境の各種コマンドの実行結果

いくつかのコマンドを試した中から面白かったものを紹介します。

ps aux

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.3  0.2  13488  5620 ?        Ssl  17:11   0:00 serve index.php
root        14  1.0  4.2 265716 88352 ?        Ss   17:11   0:00 php-fpm: master process (/tmp/serve-361831195/php-fpm.conf)
root        15  0.8  3.0 266060 63724 ?        S    17:11   0:00 php-fpm: pool app
root        16  2.4  1.1  30132 23492 ?        Sl   17:11   0:00 caddy -quiet -conf /tmp/serve-361831195/Caddyfile
root        38  0.4  0.1  12680  3240 ?        S    17:11   0:00 sh -c ps aux 2>&1
root        39  0.4  0.2  42440  5928 ?        R    17:11   0:00 ps aux

サンドボックス環境を知っていると「psコマンドがマトモに動く!」という当たり前のことに感動してしまいますね。

PID 16番のCaddyはGo言語で書かれたWebサーバ実装で、php-fpmの前段で動いているようです。

lsof -p 16

lsofコマンドでCaddyがオープンしているファイルを確認してみました。

COMMAND PID USER   FD      TYPE DEVICE     SIZE NODE NAME
caddy    16 root  cwd   unknown                      /proc/16/cwd (readlink: No such file or directory)
caddy    16 root  rtd   unknown                      /proc/16/root (readlink: No such file or directory)
caddy    16 root  txt       REG   0,14 20099720  200 /usr/local/sbin/caddy
caddy    16 root    0u      CHR    0,0             5 /dev/null
caddy    16 root    1u     FIFO   0,12             2 host
caddy    16 root    2u     FIFO   0,12             3 host
caddy    16 root    3u     sock    0,5             5 can't identify protocol
caddy    16 root    4u     0000    0,2        0   14 anon_inode
caddy    16 root    5u     sock    0,5            24 can't identify protocol
caddy    16 root    6u     sock    0,6            23 can't identify protocol

ファイルディスクリプタ3番5番6番TCPだと思うんですが、「can't identify protocol」となっています。/proc以下の未実装部分の影響でしょう。

ちなみにlsofコマンドは標準イメージ内に無かったのでUbuntu環境から持ってきたバイナリファイルをデプロイしましたが、普通に動きました。

ifconfig

eth0      Link encap:AMPR NET/ROM  HWaddr
          inet addr:192.168.1.1  Mask:255.255.255.255
          UP RUNNING  MTU:0  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

eth2      Link encap:AMPR NET/ROM  HWaddr
          inet addr:169.254.8.1  Mask:255.255.255.255
          inet6 addr: fe80::c001/128 Scope:Global
          UP RUNNING  MTU:0  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:AMPR NET/ROM  HWaddr
          inet addr:127.0.0.1  Mask:255.255.255.255
          inet6 addr: ::1/128 Scope:Global
          UP RUNNING  MTU:0  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

ifconfigコマンドもUbuntuから持ってきたものです。送受信パケット数がゼロなのは、現時点では未実装ということだと思います。

一部コマンドは動かない

体感値としては9割以上のコマンドがそれっぽい結果を返してくれるのですが、コマンドによっては異常終了するものもあります。

たとえばdfコマンドは未実装システムコールを呼んでいるのか「Function not implemented」というエラーが出て終了してしまいます。また、netstatを試すとサービス全体が刺さってしまい、インスタンスを再起動する羽目になりました。gVisor自体がまだ不安定なのかもしれません。

まとめ

そんなわけでGAE/SE PHP 7.2環境で色々試してみて、PHP 5.5環境よりは断然制約が少ない環境のように感じました。

これだけ普通のLinuxっぽければ言語本体の移植が早いのも不思議は無いしアプリケーションの動作もほぼ問題なしだよね、という気持ちになります*1。逆に、以前のサンドボックスがマゾすぎたとも言えそうですね…。

gVisorの開発は続いていくはずですので、その意味でも今後ますます期待できる環境だと言えそうです。

*1:たとえば既にWordPressは動いているようです

Travis CIのcron jobsを使ってGitHubに定期的にcommitする方法

みなさん、Travis CI使ってますか?Trais CIはクラウドCIサービスの1つで、GitHub上で公開しているOSSを自動テストする目的であれば定番中の定番といっていいサービスです。

ところで、さいきん私の公開しているプロジェクト「hnw/wsoui」で以下のことを実現したいと考えました。

  • ネット上のデータを加工してGoの連想配列の形で提供したい
  • 自動更新したい
    • 情報ソースは不定期に更新される
    • そこそこ最新のデータを反映していてほしい

これを実現する方法は何種類か思いつきますが、今回Travis CI のcron jobsを使って1週間に1回、更新があったときだけgit commitするような仕組みを作りましたので、これを紹介します。

Travis CI のcron jobs

Travis CIでは、gitリポジトリに新たなcommitがpushされるたびにテストスクリプトが実行されます。これにより、万一バグがあった場合でも早期に気付くことができたり、どのcommitでエンバグしたか調べる材料になったりするわけです。

また、commitごとのテスト実行とは別に、定期的にテストを行うような機能「Cron Jobs」も提供されています。これはTravis CIのコンソール画面「Settings」「Cron Jobs」から設定できます。ビルド間隔は日次、週次、月次のいずれか一つを選択できます。

本来cron jobsは依存ライブラリのバージョンアップなどで以前通っていたテストが失敗するようになっていないかを定期的にチェックする機能です。今回のようにcron jobsのときだけ別の処理を行うというのは変則的な使い方ではありますが、色々と便利な応用が考えられるのではないでしょうか。

通常のテストと自動更新との共存

さて、Travis CI上でジョブを定期実行できることはわかりました。テストスクリプト内で環境変数を見ればcron jobsかどうか判定できますから、「定期実行されたときだけ自動更新を実行する」という今回の目標は十分実現できそうです。

とはいえ、テストスクリプトにテスト以外の処理を追記してしまうと保守性が下がってしまうかもしれません。もう少し綺麗に実現する方法はないのでしょうか。

2018/6時点ではβ機能ですが、Travis CIにはBuild Stageという仕組みがあり、「cron jobsのときだけ実行」をシンプルに実現することができます。具体的には、.travis.ymlを次のように記述できます。

jobs:
  include:
    - stage: 'Test'
      script:
        - go test -v .
    - stage: 'Update Check'
      if: type = cron
      script:
        - [cron jobsのときに実行したいコマンド1] ...
        - [cron jobsのときに実行したいコマンド2] ...

このように記述すると普段はテストだけを実行し、cron jobsのときだけテスト成功後に「Update Check」処理が走ります。小さなことですが、別の処理を別の場所に記述できた方が安心ですよね。


git commitするために

さて、ここまでの知識でTravis CI上でcurlでデータを取ってきて変換スクリプトを動かすところまではできそうです。あとは生成されたファイルをgit commitするだけですが、これが意外と面倒です。

まずは対象リポジトリのデプロイキーを用意します。これはSSH鍵ペアを新規作成した上で、公開鍵をGitHubリポジトリページから「Settings」「Deploy keys」で設定します。

$ ssh-keygen -t ed25519 -f /tmp/id_ed25519
$ cat /tmp/id_ed25519.pub
ssh-ed25519 ******************************************************************** hnw@example.com

今回のようにデプロイキーで外部からpushするような場合は「Allow write access」は必須になりますので注意してください。

次に、先ほど生成した秘密鍵を暗号化してリポジトリにアップロードします。まずtravisコマンドをインストールしてそれを利用します。

$ gem install travis
(snip)
$ travis encrypt-file /tmp/id_ed25519
Detected repository as hnw/wsoui, is this correct? |yes|
encrypting /tmp/id_ed25519 for hnw/wsoui
storing result as id_ed25519.enc
storing secure env variables for decryption

Please add the following to your build script (before_install stage in your .travis.yml, for instance):

    openssl aes-256-cbc -K $encrypted_****_key -iv $encrypted_****_iv -in id_ed25519.enc -out /tmp/id_ed25519 -d

Pro Tip: You can add it automatically by running with --add.

Make sure to add id_ed25519.enc to the git repository.
Make sure not to add /tmp/id_ed25519 to the git repository.
Commit all changes to your .travis.yml.
$ git add id_ed25519.enc

暗号化した秘密鍵Travis CI上で復号して$HOME/.ssh/以下にコピーして使います。設定ファイル.travis.ymlは最終的に次のようになりました。

language: go

go:
  - "1.10"

jobs:
  include:
    - stage: 'Test'
      script:
        - go test -v .
    - stage: 'Update Check'
      if: type = cron
      script:
        - git checkout master
        - curl "https://code.wireshark.org/****" > /tmp/manuf
        - scripts/oui-convert.pl /tmp/manuf > ouidata.go
        - go test -v .
        - openssl aes-256-cbc -K $encrypted_****_key -iv $encrypted_****_iv -in id_ed25519.enc -out $HOME/.ssh/id_ed25519 -d
        - chmod 600 $HOME/.ssh/id_ed25519
        - scripts/push-if-updated.sh ouidata.go

ここで注意すべきことですが、Travis CIのテスト対象は特定commitでありGitでいうところの「detached HEAD」と呼ばれる状態になっているため、そのままでは元のリポジトリにpushできません。そのため、まずmasterブランチに切り替えるという処理を行っています。ちょっとしたTIPSですが、私は少々ハマりました。

Travis CI上で自動更新を行うメリット

上記設定により、Travis CI上で自プロジェクトの自動更新ができるようになりました。しかし、cronで実行するだけであれば、VPSを利用することもできます。わざわざ手間をかけてTravis CIで実行するメリットは何でしょうか。

個人的には、GitHub+Travis CI環境では実行に必要な全ファイルが公開されていること、また実行ログが残ることがメリットだと考えています。

今回は自動生成したソースコードを自動commitしているわけですから、おかしな内容がcommitされてしまう可能性もゼロではありません。そんな場合でも、必要ファイルとログが公開されていれば、赤の他人であっても原因をつきとめて修正してPull Requestを送ることが可能です。

こんな過疎プロジェクトで誰もPull Request送らないでしょ、というツッコミもあるとは思いますが、可能性って大事だと思うんですよね。

まとめ

  • Travis CIのcron jobsを使うと定期実行ジョブをソースコードつき・ログつきで公開できる
    • インターネット上で更新され続けている何かを変換してgit commitするような用途には非常に良いのでは?たとえば日本の祝日カレンダーライブラリなど。

日本語のパスワードジェネレータを作ってみた

Webサービスを使っていると、たまに「秘密の質問」の設定を求められることがあります。

こういう場合、個人的にはランダム文字列を登録したいと思うのですが、次のようにマルチバイト文字しか登録できないことが多い気がします。

普通のパスワードジェネレータではマルチバイト文字のパスワード生成ができないので、このような用途には使えません。そこで、ランダムなマルチバイト文字列を生成するサービス「 秘密の答えジェネレータ」を作ってみました。

自分でも実用しており、既に5サービスに設定しましたが、非常に便利だと感じます。

本稿ではこのサービスの技術面の詳細について紹介します。

「秘密の答えジェネレータ」の構成要素

「秘密の答えジェネレータ」はHTML+JavaScriptだけで実現されており、GitHub Pagesでホストしています。また、独自ドメインDNSおよびSSL化はCloudFlareで行っています。

このような構成にすると月額コスト0円で運用できる点、また万一GitHub Pagesが使えなくなったようなときに移転先がいくらでも見つかる*1点が良いですね。

ブラウザ上でパスワード生成することの是非

パスワード類は自分だけが知っているのが大前提ですから、リモートサーバ上で生成された文字列はパスワードとしては不適切です。その意味で、パスワードジェネレータをサーバサイド実装するのは良いアイデアとは言えません。

前述したとおり、本サービスはHTML+JavaScriptだけで実現されています。言い換えると、ランダム文字列の生成処理は全てローカルマシンのWebブラウザ上で実行されます。つまり、技術的には安全と言えるはずです。

ただ、利用者が一見してサーバサイド実装かクライアントサイド実装かわからない、というのは問題かもしれません。

乱数生成について

JavaScript上で乱数生成する場合Math.random()を使う事例が多いかと思いますが、この関数は多くのブラウザ上で暗号論的擬似乱数生成器(CSPRNG)として実装されておらず、特にパスワード生成には向かないようです*2

一方、多くのブラウザ上で動くCSPRNGとしてwindow.crypto.getRandomValues()という関数があります。私の手元で試したところ、ChromeSafari、Edge、Firefoxの全てで動作しました。PCブラウザだけでなく、スマートフォン上のブラウザでも実装されているようです。

Operaだけはwindow.crypto.getRandomValues()が実装されていないようですが、OperaではMath.random()がCSPRNGになっているとのことです*3。つまり、window.crypto.getRandomValues()が存在しなかったときだけMath.random()にフォールバックすればブラウザ上でもセキュアに乱数生成ができると言えそうです*4

ちなみに、本実装ではwindow.crypto.getRandomValues()から取り出したUInt32の値の剰余を取ることでN文字から1文字を取り出す処理を実装しているのですが、偏りが出てしまうので厳密には良くありません。具体的には、ひらがなのランダム文字列を生成する際に「ぁ」が「ん」より0.0000000233%ほど出現しやすくなっています。将来的に修正するかもしれませんが、現時点ではそのような問題があると認識した上でご利用ください。

ES6実装について

私は普段JavaScriptをほとんど書かないのですが、そろそろ生のES6で書いても許される空気を感じたため、今回のサービスでは実験的に大半の処理をES6で書いてみました。

そのためIE11で動かなくなってしまいましたが、それ以外であればPC・スマートフォンとも大抵のブラウザで動作します。

私が使った機能はarrow functionとclassくらいではあるのですが、トランスパイラなしでES6を書ける時代はすぐそこかな、という印象を持ちました。

秘密の質問は常に悪なのか?

さて、今回秘密の質問に対して嘘の回答をすることをオススメしているわけですが、そもそも秘密の質問というやり方自体にセキュリティ上の問題があるのでしょうか?

個人的には秘密の質問が常に悪いとは思いません。重要な操作の際のみ要求するような、第二パスワード的な位置づけであれば十分意味があるように思います。一方で、秘密の質問に正解するだけでパスワード変更ができるようなサービス設計は非常に問題があると感じます。

また、秘密の質問が複数のサイトで重複しやすい点(母親の旧姓や好きな食べ物など)、また平文で記録されることが多い点、この2つはかなり致命的な問題であると感じます。つまり、あるサービスでユーザ情報の漏洩が起きた場合に、他サービスの秘密の質問の答えまでバレてしまうリスクが高いわけです。

上記のような理由から、秘密の質問の答えは毎回異なるランダム文字列にすべきだと私は考えています。

生成した文字列をどう保管するか

このサービスで生成した文字列をサービス登録後に破棄してしまうことはオススメしません。破棄するのでなく、パスワードリマインダーのメモ欄などに記録しておいた方が後々面倒が無いように思います。

秘密の質問の答えを平文で保存しておくのが不安な場合は、正しい回答と自動生成文字列を組み合わせて使うこともできます。たとえば「母親の旧姓は?」に対して「高橋ゃたぞゅもぽねん」などと入力し、後ろの8文字だけを平文で記録しておけば多少は安心度が増すかもしれません。

参考URL

まとめ

  • 「秘密の質問」向けのパスワードジェネレータを作りました
    • 既存のパスワード生成ツールではマルチバイト文字に対応していないため
  • 半分シャレのつもりでしたが、案外実用的です
    • 入力内容を他人に見られた場合のショルダーハック対策にもなるように思います(↓入力例)

*1:パッと思いついた中ではGAEかNetlifyが良さそうです

*2:Secure random values (in Node.js)

*3:cryptanalysis - Are there any Javascript CSPRNGs? - Cryptography Stack Exchange

*4:IE10以前ではセキュアじゃないと思いますが、今回はそもそも動作しないので無視しています

PHPのis_numeric関数は使うべきでないという話

本稿は私が前職の技術ブログで執筆した記事「そのis_numeric()は適切ですか?」を改題・再編集して掲載するものです。前職には許可を取ってあります*1

本稿ではPHPの関数is_numeric()の使いどころについて問題提起をしてみます。

is_numeric関数とは

さて、まずはis_numeric()のリファレンスマニュアルを見てみましょう。

bool is_numeric ( mixed $var )


指定した変数が数値であるかどうかを調べます。数値形式の文字列は以下の要素から なります。(オプションの)符号、任意の数の数字、(オプションの)小数部、 そして(オプションの)指数部。つまり、+0123.45e6 は数値として有効な値です。十六進表記(0xf4c3b00c など) や二進表記 (0b10100111001 など) は認められません。


http://php.net/manual/ja/function.is-numeric.php

なるほど、与えられた引数がnumeric(数字的)かを調べるような、名前の通りの関数なんですね、というのが初見での印象かと思います。

何が問題なのか?

マニュアルの記述を見ただけでこの関数の問題点に気付く人はあまりいないかもしれません。私がこの関数について問題だと思うのは、「numeric」と言われたときに想像するものが人によって違う点です。

具体的には、ユーザーの入力値が10進整数として正しいかチェックする意図でis_numeric()を利用しているPHPプログラムを見かけることがあります。しかし、マニュアルを読めばわかるように、この関数は以下の文字列を受け取ってもtrueを返してしまいます。

  • 1.23
  • .123
  • 1e2

これらの文字列を与えてもtrueになることを忘れていないですか、というのが今回の問題提起です。もちろん、is_numeric()によるチェックをすり抜けて先の処理に進んだところで、セキュリティホールになるようなコードは考えにくいと思います。しかし、これが原因のバグというのは十分考えられるのではないでしょうか。

下記はネット上で見つけたコードを少しアレンジしたものです。

<?php
function validate_date($date) {
    $dateArr = explode("-", $date);
    if (count($dateArr) == 3 &&
        is_numeric($dateArr[0]) && strlen($dateArr[0]) == 4 &&
        is_numeric($dateArr[1]) && strlen($dateArr[1]) == 2 &&
        is_numeric($dateArr[2]) && strlen($dateArr[2]) == 2) {
          return checkdate($dateArr[1], $dateArr[2], $dateArr[0]);
    }
    return false;
}

例えば、上記のコードでvalidate_date("12e3-12-31")はtrueになりますが、この値をそのままSQL文の日付型の値として使うとSQLエラーになります。似たような状況は十分あり得るのではないでしょうか。

is_numeric()の詳細な挙動

細かい挙動が気になる人のために、以下にPHP 7.2.4でis_numeric()がtrueを返す条件を書き出してみました。(バージョンごとに挙動が異なる部分もあるのですが、下記のように把握をしておけば大抵の人にとっては十分だろうと思います)

  • 整数
  • 浮動小数点数
  • 全体が下記のいずれかの正規表現にマッチする文字列
    • [\x20\t\x0a-\x0d]*[\+\-]?[0-9]+([\.][0-9]*)?([Ee][\+\-]?[0-9]+)?
    • [\x20\t\x0a-\x0d]*[\+\-]?[\.][0-9]+([Ee][\+\-]?[0-9]+)?

ほぼマニュアルの通りの挙動です。マニュアルに書いていないことは、先頭の空白文字列を読み飛ばしてくれることくらいでしょうか。「3.」も「.3」もtrueになるのに驚かれた方がいるかもしれませんが、PHPプログラム中に浮動小数点数を書く場合も同じように書けますので、それほど意外なことではありません。

まとめ

is_numeric()は引数が数値っぽいかを返す関数ですが、浮動小数点数形式の文字列であってもtrueを返します。知識としては知っている人が多いと思いますが、うっかり10進整数のチェックに使っていたりしないでしょうか。10進整数のチェックが目的であれば、正規表現^[0-9]+$などと記述するのが一番誤解が少ない書き方のような気がします。

is_numeric()のチェックの後で浮動小数点数にキャストするのが適した状況も考えられなくはありませんが、少なくとも私はそんなコードを書いたことがありませんね…。

じゃあctype_digit()を使えばいいんじゃないか?と考える人がいるかもしれませんが、これはこれで罠があるので使わない方がいいというのが私の考えです(参考:「ctype_digit関数の罠」)。

*1:「いっすかー」「いいよー」くらいのノリです

PHP 7.2.0からDateTimeでミリ秒表示するときの丸め処理が変わった話

エイプリルフールなので(?)、PHPの日付処理の細かい挙動がひっそり変わった話の解説をします。

ちなみに本稿はSlackグループ「PHPユーザーズ」の#randomチャンネルでの議論をまとめ直したものです。議論のきっかけを下さったmsngさん、tadsanさん、do_akiさんはじめとする皆様ありがとうございました。

PHP 7.0から日付のフォーマット文字列にミリ秒を意味する「v」が追加された

PHP 7.0.0から、DateTime::format()でミリ秒指定ができるようになっています。

v ミリ秒 (PHP 7.0.0 で追加) Same note applies as for u. 例: 654


http://php.net/manual/ja/function.date.php

date関数と違ってDateTimeオブジェクトはマイクロ秒の処理を行うので、これをミリ秒単位に丸めるような場合にvが有用というわけです。たとえば次のように利用できます。

<?php
var_dump((new \DateTime('2018-04-01 00:11:22.123456'))
         ->format('v')); // string(3) "123"

PHP 7.2から「v」の挙動が変わった

ところで、このvの挙動がPHP 7.1と7.2とで変わりました。7.1までは1ミリ秒以下を四捨五入していたのが、7.2からは切り捨てになっています。

<?php
var_dump((new \DateTime('2018-04-01 00:11:22.345678'))
         ->format('v')); // PHP 7.1.x: "346" / PHP 7.2.x: "345"

仕様変更が必要だった理由

仕様変更の理由を想像すると、単純に四捨五入すると困る状況があるから、ということだと思います。たとえば次のように現在時刻をミリ秒まで表示するプログラムを考えてみます。

<?php
var_dump((new \DateTime())->format('Y-m-d H:i:s.v'));

DateTimeのコンストラクタは無引数の場合現在時刻を返します。たとえば次のような値を返しているのであれば、四捨五入でも切り捨てでも全く問題はありません。

string(23) "2018-04-01 00:11:22.346"

しかし、現在時刻の秒未満が仮に999.9ミリ秒だった場合、四捨五入すると次のような結果になってしまいます。

string(24) "2018-04-01 00:11:22.1000"

この文字列全体を日付関数でparseするような場合、これでは23秒ではなく22.1秒として解釈されてしまうでしょう。つまり、vで四捨五入するのは仕様として良くないということになります。切り捨てであればこのような問題は起こらず、丸め後の値は必ず3桁になります。

一般に、何かの数を丸める場合に良かれと思って四捨五入にしてしまうことがある気がします。ところが、今回は四捨五入では問題になるという面白い事例でした。

参考URL