0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Wi-Fi の QR で「:」までエスケープするのは、SSID の中の P: をパスワードと読ませないためだった

0
Posted at

背景

自分のツールサイトに、Wi-Fi に接続するための QR コードを作るページを置いています。
https://hashitosystem.com/tools/wifiqr/
スマホのカメラで読むと、パスワードを打たずにそのまま接続できる、あの QR です。
コードを読み返していたら、SSID とパスワードを \ ; , : " の5文字だけエスケープしていました。
; は区切りなので分かるのですが、: まで逃がす理由が自分でも説明できなかったので、読み取り側のコードまで追いかけてみました(QR の仕様は毎回うろ覚えです…)。

Wi-Fi の QR の中身

前提です。
Wi-Fi 接続用の QR は、画像の中に下記のような1行の文字列を入れているだけだ。

WIFI:T:WPA;S:mynetwork;P:mypass;;

T: が認証方式(WPA / WEP / nopass)、S: が SSID(Wi-Fi のネットワーク名)、P: がパスワード、H: がネットワークを隠しているかどうか。
各項目は ; で終わり、最後に ; をもう1つ付けて閉じます。

この形式は、オープンソースの QR 読み取りライブラリ ZXing(ゼブラクロッシング)が wiki にまとめたものが事実上の基準になっています。
https://github.com/zxing/zxing/wiki/Barcode-Contents
そこには、\ ; , " : の5文字はバックスラッシュでエスケープする、と書かれている。
項目の順番は問わない、とも書かれています。

ツールでは、SSID とパスワードを下記の関数に通してから組み立てています。
ssid と pass は入力欄の値です。

function esc(s){ return String(s).replace(/([\\;,:"])/g, '\\$1'); }
'WIFI:T:WPA;S:' + esc(ssid) + ';P:' + esc(pass) + ';H:false;;';

読み取り側はどう項目を探しているか

ZXing の読み取り処理(ResultParser と WifiResultParser)を読むと、項目はおおむね下記の手順で取り出していました。

  1. WIFI: を外した残りの文字列から、P: のような接頭辞を indexOf で探す
  2. そこから先の ; を探し、直前のバックスラッシュが奇数個なら「エスケープされた ;」として飛ばす
  3. 見つかった ; までを値とし、最後にバックスラッシュを外す

ポイントは1です。
接頭辞を探すとき、項目の区切りを意識せず、文字列の先頭から素直に探している。
なので SSID の中に P: という並びがあると、そこをパスワードの始まりだと思ってしまいます。

手元で再現する

同じ考え方で書いた最小の読み取り関数で、エスケープの有無を比べました。
build がツール側の組み立て、read が読み取り側です。
Node.js 24 で動かしました。

function esc(s) { return String(s).replace(/([\\;,:"])/g, '\\$1'); }
function build(ssid, pass, escape) {
  const e = escape ? esc : (x => x);
  return 'WIFI:T:WPA;S:' + e(ssid) + ';P:' + e(pass) + ';H:false;;';
}
function backslashesBefore(s, pos) {
  let n = 0; for (let i = pos - 1; i >= 0 && s[i] === '\\'; i--) n++; return n;
}
function field(raw, prefix) {
  const at = raw.indexOf(prefix); // 接頭辞を文字列全体から探す
  if (at < 0) return null;
  const start = at + prefix.length;
  let i = start;
  while ((i = raw.indexOf(';', i)) >= 0) {
    if (backslashesBefore(raw, i) % 2 === 1) { i++; continue; } // \; は値の一部
    return raw.substring(start, i).replace(/\\(.)/g, '$1');
  }
  return null;
}
function read(qr) {
  const raw = qr.slice('WIFI:'.length);
  return { S: field(raw, 'S:'), P: field(raw, 'P:') };
}
for (const [ssid, pass] of [['AP:2F', 's3cret'], ['Home', 'cafe;latte'], ['Home', 'pa\\ss']]) {
  for (const escape of [false, true]) {
    const qr = build(ssid, pass, escape);
    console.log(escape ? 'esc ' : 'raw ', qr, '=>', JSON.stringify(read(qr)));
  }
}

結果は下記です。

raw  WIFI:T:WPA;S:AP:2F;P:s3cret;H:false;; => {"S":"AP:2F","P":"2F"}
esc  WIFI:T:WPA;S:AP\:2F;P:s3cret;H:false;; => {"S":"AP:2F","P":"s3cret"}
raw  WIFI:T:WPA;S:Home;P:cafe;latte;H:false;; => {"S":"Home","P":"cafe"}
esc  WIFI:T:WPA;S:Home;P:cafe\;latte;H:false;; => {"S":"Home","P":"cafe;latte"}
raw  WIFI:T:WPA;S:Home;P:pa\ss;H:false;; => {"S":"Home","P":"pass"}
esc  WIFI:T:WPA;S:Home;P:pa\\ss;H:false;; => {"S":"Home","P":"pa\\ss"}

1行目が今回の本題です。
SSID が AP:2F(2階のアクセスポイント、くらいの気持ちで付けそうな名前)だと、エスケープしない場合はパスワードが 2F と読まれました。
S:AP:2F の中の P:2F を、P: 項目の始まりだと見たわけです。
エスケープすると AP\:2F になり、P: という並び自体が文字列から消えるので、正しく s3cret が取れる。

2行目は ; を含むパスワードで、エスケープしないと cafe で切れました。
3行目はバックスラッシュを含むパスワードで、エスケープしないと読み取り側が \s を「s をエスケープしたもの」と解釈して pass になってしまう。
どれも QR は問題なく読めて、接続だけが失敗するタイプの壊れ方なので、気づきにくいです。

なぜ5文字全部を逃がすのか

: を逃がすのは、上のとおり接頭辞の誤検出を防ぐためでした。
, は、同じ書式の元になった MECARD(連絡先 QR の書式)で区切りに使われている文字なので、同じ規則に合わせて逃がしているようです。
「この文字は今の読み取り器では問題にならないから省く」と個別に判断すると、読み取りアプリごとの実装差に振り回されるので、wiki に書かれた5文字をそのまま全部逃がすことにしています。
なお、16進数に見える SSID は "ABCD" のように二重引用符で囲む、という注記も wiki にありますが、ZXing の WifiResultParser 自体は引用符を特別扱いしていませんでした。
ここは読み取りアプリ次第っぽいので、ツールでは対応していません。

Wi-Fi の QR を自分で組み立てるときは、SSID とパスワードの両方に \ ; , : " の5文字のエスケープを必ず通してください。
特に : は、SSID に P: や S: を含む名前があると、エスケープしないだけで別のパスワードとして読まれます。


本記事はAI補助で執筆した、個人開発の紹介記事です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?