Wi-FiのQRコードはSSIDのセミコロンで壊れる
SSIDがcafe;freeのアクセスポイントをQRコード1にすると、読み取った端末にはcafeしか届かない。WIFI:のペイロードはフィールドの区切りが全部セミコロンなので、値の中のセミコロンを逃がさないと境目がずれる。
QRコード生成ツールを書くときにWi-Fi Allianceの仕様を読んだ。短いペイロードのわりに外しやすい細部が多く、ついでにnode-qrcodeで版の大きさを測ったら、書かなくていい項目を省くとQRコードが1版分小さくなることまで分かった。
ペイロードの形
Wi-Fiの接続情報のQRコードは、中身はただの短い文字列になっている。
WIFI:T:WPA;S:my-wifi;P:pass1234;;
T:が認証方式、S:がSSID、P:がパスワード。フィールドの区切りは;で、末尾が;;なのは、最後のフィールドを閉じる;とペイロード全体を閉じる;が並ぶから。仕様はWi-Fi AllianceのWPA3仕様書§7.1にABNFで書いてある2。
組み立てる側は、この程度の実装で足りる。
const parts = [
...(authType === 'nopass' ? [] : [`T:${authType}`]),
`S:${escapeWifiValue(ssid)}`,
...(hidden ? ['H:true'] : []),
...(authType === 'nopass' ? [] : [`P:${escapeWifiValue(password)}`]),
]
return `WIFI:${parts.join(';')};;`
短いのに、細部を外すと機種によって読めたり読めなかったりする。以下は外しやすいところ。
セミコロンを逃がさないと境目がずれる
値の中の;は\を前に置いて逃がす。逃がさないと、そこが新しいフィールドの始まりとして読まれる。
読む側を自分で書いて往復させてみた。\の次の1文字を素の文字として扱う、仕様どおりの読み手を用意して、SSIDがcafe;freeのときのペイロードを食わせる。
| 組み立て方 | ペイロード | 読み手が受け取るSSID |
|---|---|---|
| 逃がす | WIFI:T:WPA;S:cafe\;free;P:p;; | cafe;free |
| 逃がさない | WIFI:T:WPA;S:cafe;free;P:p;; | cafe |
逃がさないほうは、SSIDがcafeで切れて、残ったfreeはどのキーにも属さない断片になる。パスワードのP:は無事に読めてしまうので、接続に失敗する理由が画面からは分からない。
逆方向の事故もある。\を解かない素朴な読み手に逃がしたペイロードを渡すと、SSIDはcafe\になる。エスケープは書く側と読む側が両方守ってはじめて成立する取り決めで、片方だけ守っても直らない。
逃がすのはセミコロンだけじゃない
対象は\ ; , "の4つ。
function escapeWifiValue(value: string): string {
return value.replace(/[\\;,"]/g, (char) => `\\${char}`)
}
\は自分自身がエスケープ文字なので当然として、,と"は区切り文字ではないのに対象に入っている。ここが少し気持ち悪いところで、,を含むパスワードは次のようになる。
入力 P: a,b
ペイロード WIFI:T:WPA;S:s;P:a\,b;;
仕様どおりの読み手 a,b
\ を解かない読み手 a\,b
;と違ってフィールドは割れないが、パスワードが1文字余計になって接続に失敗する。逃がしても逃がさなくても事故になりうる文字なので、ここは仕様に従って逃がす側に倒すしかない。
書かなくていい項目は書かない
T:とH:には、値で表すのではなく項目ごと省く、という決まりがある。
- 認証なしのときは
T:を書かない(T:nopassとは書かない) H:は隠しネットワークのときだけ書く(H:falseとは書かない)
省略が単なる作法ではなくQRコードの絵に効くので、node-qrcodeで版と一辺のモジュール数を測った。誤り訂正レベルはツールの既定値であるMで揃えてある。
| 入力 | 省いた場合 | 書いた場合 |
|---|---|---|
SSID10字・認証なし(T:nopass;P:;) | v2・25角 | v3・29角 |
SSID25字・認証なし(T:nopass;P:;) | v3・29角 | v4・33角 |
SSID15字・隠しでない(H:true) | v3・29角 | v4・33角 |
T:nopass;P:;の12文字を省くだけで、SSIDが7〜18字なら版が3から2へ、23〜34字なら4から3へ落ちた。H:trueの7文字も同じで、SSIDが12〜18字なら足した瞬間に版が1つ上がる。版が上がるとモジュールが細かくなるぶん、印刷して貼るときの読み取り距離が短くなる。書かない、が仕様の言うとおり正しい。
セミコロンはQRコードにとって高い文字
もうひとつ実測して面白かったのが、区切り文字の選択そのもののコスト。
QRコードの英数モードが扱えるのは0-9とA-Z、それに空白と$ % * + - . / :だけで、;は入っていない。だからペイロードは;が出た時点でバイトモードに落ちる。
大文字だけのペイロードで、区切りの;を:に置き換えて比べた。
| ペイロード | セグメント | 版 |
|---|---|---|
WIFI:T:WPA;S:MYWIFI;P:PASS1234;; | 英数(10)+バイト(10)+英数(10)+バイト(2) | v3・29角 |
WIFI:T:WPA:S:MYWIFI:P:PASS1234:: | 英数(32) | v2・25角 |
同じ32文字なのに版が1つ違う。もちろん:は値の中にも出るので区切りには使えず、これは仕様への文句ではなくて、;区切りを選んだ時点で1版分の余裕を捨てているという話。だからこそ、前の節のT:nopassやH:falseを書かないことが効いてくる。
日本語のSSIDは3倍かかる
S:の値はSSIDそのままなので、日本語のSSIDも書ける。ただしバイトモードでUTF-8のまま入るので、1文字3バイトになる。
| SSID | ペイロード | UTF-8 | 版 |
|---|---|---|---|
ゲスト用ネットワーク(10字) | 36字 | 56バイト | v4・33角 |
guest-network-0123456789(24字) | 50字 | 50バイト | v4・33角 |
10字の日本語が24字のASCIIと同じ大きさになる。加えて、node-qrcodeはECIの指定を入れないので、バイトモードの中身がUTF-8であることはペイロードのどこにも書かれていない。読み手が別の符号化で解釈すればSSIDは化ける。ASCIIのSSIDにしておくのが無難だと思う。
短い文字列を組み立てるだけの仕様なのに、逃がす文字を1つ落とすと読み手の側で境目がずれ、書かなくていい項目を1つ足すと絵のほうが1版ぶん細かくなる。どちらも書いた本人の画面では何も起きないので、貼ったQRコードが読めない、という形でしか表に出てこない。
ツールのほうは/tools/qr-codeに置いてある。Wi-Fiモードでは組み立てたペイロードの文字列も一緒に出すようにしてあるので、逃がされた\がどこに入ったかはその場で見える。
Footnotes
-
QRコードは株式会社デンソーウェーブの登録商標。 ↩
-
Wi-Fi AllianceのWPA3仕様書。読むにあたってはWi-Fi QRコード仕様のメモ@okuokuを参照した。 ↩