本文へ移動
myowchick

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

  1. QRコードは株式会社デンソーウェーブの登録商標。 ↩

  2. Wi-Fi AllianceのWPA3仕様書。読むにあたってはWi-Fi QRコード仕様のメモ@okuokuを参照した。 ↩

← 一覧へ戻る