2026年10月9日時点の内容です。
Wi-Fi接続用のQRコードの中身は、WIFI:T:WPA;S:mynetwork;P:mypass;; という1行のテキストです。SSIDとパスワードが英数字だけなら、文字列を連結するだけで作れます。問題は、SSIDに ; が入っている、パスワードに : や \ が入っている、SSIDが日本語、といった場合です。
調べてみると、エスケープの決まりが書かれた文書は2つあり、内容が違います。この記事では両方の原文を確認し、Node.jsで実際に文字列を作って違いを見ます。私たちは無料のウェブツールをつくっている小さな制作チームで、公開しているQRコード作成ツールにもWi-Fi用の機能があります。
2つの文書
ZXingのwiki(事実上の標準)
この形式は、バーコード読み取りライブラリのZXingが提案したものです。wikiの「Barcode Contents」には次のように書かれています。
- 形式は MECARD に似た
WIFI:T:WPA;S:mynetwork;P:mypass;; -
Tは認証方式。WEP、WPA、WPA2-EAP、パスワードなしのnopass(または省略) -
SはSSIDで必須、Pはパスワード、HはtrueならSSIDが非公開 - フィールドの順序は問わない
- 特殊文字
\;,":はバックスラッシュでエスケープする - 16進数と解釈できるASCIIの名前(例:
ABCD)は二重引用符で囲む
WPA3仕様(Wi-Fi Allianceの文書)
Wi-Fi AllianceのWPA3 Specification v3.5は、第7章「WIFI URI」でこの形式をABNFで定義しています。要点は次のとおりです。
WIFI-qr = "WIFI:" [type ";"] [trdisable ";"] ssid ";" [hidden ";"] [id ";"] [password ";"] [public-key ";"] ";"-
ssid、password、idの値は*(printable / pct-encoded) -
printable = %x20-3a / %x3c-7e(セミコロンを除く) - printableに含まれないオクテットはパーセントエンコードする。セミコロンもパーセントエンコードする
-
typeがあるときの値はWPA。typeがなければ認証なしのネットワーク - WEP共有鍵での使用は定義しない
- 解釈できない要素は読み飛ばす
改訂履歴によると、WIFI URIの章はバージョン3.0(2020年12月)で追加されました。
並べてみる
| 項目 | ZXing wiki | WPA3仕様 v3.5 |
|---|---|---|
区切り文字 ; を値に含める |
\; |
%3B |
: , "
|
\: \, \"
|
そのまま(printable) |
\ |
\\ |
そのまま(printable) |
| ASCII以外(日本語SSIDなど) | 記載なし | オクテットごとに %XX
|
| 認証方式 |
WEP WPA WPA2-EAP nopass
|
WPA のみ。なしは T を省略 |
| 非公開SSID | H:true |
H:true |
| 追加フィールド |
E A I PH2(EAP用) |
R(Transition Disable)、I、K(SAE-PK公開鍵) |
I はZXingでは「EAPのID」、WPA3仕様では「SAEパスワード識別子」で、同じ1文字でも意味が違う点に注意してください。
実際に作って比べる
依存パッケージなしで動くスクリプトです。
// wifi.js — node wifi.js(依存パッケージなし)
// 1) ZXing 方式:\ ; , " : の前にバックスラッシュを置く
const escZxing = s => s.replace(/([\\;,:"])/g, '\\$1');
// 2) WPA3 仕様方式:printable(0x20–0x3A, 0x3C–0x7E)以外のオクテットを %XX にする
// % 自身も %25 にしておく(そうしないと読み取り側で元に戻せない)
function escWpa3(s) {
return Array.from(new TextEncoder().encode(s), b => {
const printable = (b >= 0x20 && b <= 0x3a) || (b >= 0x3c && b <= 0x7e);
return printable && b !== 0x25
? String.fromCharCode(b)
: '%' + b.toString(16).toUpperCase().padStart(2, '0');
}).join('');
}
const build = (esc, { ssid, pass }) => `WIFI:T:WPA;S:${esc(ssid)};P:${esc(pass)};;`;
// ZXing 方式の読み取り側:エスケープされていない ; だけで区切る
function parseZxing(payload) {
const out = {};
const body = payload.replace(/^WIFI:/, '');
for (const field of body.match(/(?:\\.|[^;\\])+/g) || []) {
const i = field.indexOf(':');
const key = i < 0 ? field : field.slice(0, i);
out[key] = i < 0 ? '' : field.slice(i + 1).replace(/\\(.)/g, '$1');
}
return out;
}
const cases = [
{ ssid: 'Cafe;Guest', pass: 'p:a,ss' },
{ ssid: 'C:\\net', pass: '"quoted"' },
{ ssid: 'カフェWi-Fi', pass: '50%OFF' },
];
for (const c of cases) {
const z = build(escZxing, c);
const w = build(escWpa3, c);
const back = parseZxing(z);
const cross = parseZxing(w);
console.log('ZXing :', z);
console.log('WPA3 :', w);
console.log('ZXing方式で書いてZXing方式で読む:', back.S === c.ssid && back.P === c.pass ? 'OK' : 'NG');
console.log('WPA3方式で書いてZXing方式で読む :', cross.S === c.ssid && cross.P === c.pass ? 'OK' : `NG (S=${cross.S} P=${cross.P})`);
console.log('');
}
// エスケープを忘れた場合
const raw = 'WIFI:T:WPA;S:Cafe;Guest;P:p:a,ss;;';
console.log('未処理:', JSON.stringify(parseZxing(raw)));
実行結果(Node.js v26.11.0)です。
ZXing : WIFI:T:WPA;S:Cafe\;Guest;P:p\:a\,ss;;
WPA3 : WIFI:T:WPA;S:Cafe%3BGuest;P:p:a,ss;;
ZXing方式で書いてZXing方式で読む: OK
WPA3方式で書いてZXing方式で読む : NG (S=Cafe%3BGuest P=p:a,ss)
ZXing : WIFI:T:WPA;S:C\:\\net;P:\"quoted\";;
WPA3 : WIFI:T:WPA;S:C:\net;P:"quoted";;
ZXing方式で書いてZXing方式で読む: OK
WPA3方式で書いてZXing方式で読む : NG (S=C:net P="quoted")
ZXing : WIFI:T:WPA;S:カフェWi-Fi;P:50%OFF;;
WPA3 : WIFI:T:WPA;S:%E3%82%AB%E3%83%95%E3%82%A7Wi-Fi;P:50%25OFF;;
ZXing方式で書いてZXing方式で読む: OK
WPA3方式で書いてZXing方式で読む : NG (S=%E3%82%AB%E3%83%95%E3%82%A7Wi-Fi P=50%25OFF)
未処理: {"T":"WPA","S":"Cafe","Guest":"","P":"p:a,ss"}
結果からわかること
1. エスケープしないと、黙って別のSSIDになる
最後の行が、エスケープを忘れた場合です。SSID Cafe;Guest が Cafe で切れ、Guest という意味のないフィールドが1つ増えています。エラーにはなりません。読み取った端末は Cafe というネットワークを探しに行くだけなので、利用者には「つながらないQRコード」に見えます。
2. 2つの方式は互換ではない
WPA3方式で書いた文字列を、ZXing方式の読み取り処理に渡すと、3つとも元の値に戻りません。%3B はそのまま文字として残り、C:\net の \n は「エスケープされた n」と解釈されて C:net になります。逆方向も同じで、ZXing方式の \; は、WPA3仕様の文法ではバックスラッシュのあとに区切りのセミコロンが来た形です。
バックスラッシュとパーセントという、どちらの方式でも「そのまま書ける」文字が、もう一方では特別な意味を持つのが原因です。
3. % の扱いは仕様に明記がない
WPA3仕様の printable には %(0x25)が含まれています。しかし % をそのまま書くと、読み取り側はパーセントエンコードの始まりと区別できません。上のコードでは %25 にしました。これは仕様の文言ではなく、往復できるようにするための実装上の判断です。
どちらで出力するか
生成する側としては、読み取る端末がどちらの方式で解釈するかがすべてです。これは端末やOSのバージョンによって変わりうるので、机上では決められません。現実的な進め方は次のとおりです。
-
記号を含まない値なら、どちらでも同じ文字列になる。 英数字とハイフン程度のSSID・パスワードは、2つの方式で1文字も変わりません。来客用ネットワークを新しく作るなら、SSIDとパスワードから
; : , " \ %と日本語を外すのがいちばん確実です。 -
記号が避けられないなら、対象の端末で実機テストをする。 テスト用のSSIDに
;と\を1つずつ入れ、iPhoneとAndroidの標準カメラで読み取って、表示されるネットワーク名を確認します。 -
認証方式は
WPAか、なし。WEPとWPA2-EAPはZXing wikiにはありますが、WPA3仕様は定義していません。WEPしか使えない機器のためでなければ、WPAを選びます。 -
末尾の
;;を落とさない。 最後のフィールドの終わりと、全体の終わりで、セミコロンが2つ続きます。
私たちのQRコード作成ツールは、ZXing方式(\ ; , : " をバックスラッシュでエスケープ)で出力しています。認証方式は WPA・WEP・nopass から選び、非公開SSIDのときだけ H:true を付けます。記号や日本語を含むSSIDで使う場合は、上の2番のとおり、印刷前に実機で確認してください。
実装するときのチェックリスト
- 値を連結する前に、必ずエスケープ関数を通す(SSIDとパスワードの両方)
- エスケープの対象文字をテストに入れる:
;:,"\%、日本語、絵文字 - 生成した文字列を自前のパーサーで読み戻し、元の値と一致するか確認する(上の「往復」テスト)
- パスワードはログ・解析イベント・エラー報告に出さない。QRコードの中身は平文なので、画像そのものも公開範囲に注意する
-
nopassのときはPを出力しない
まとめ
- Wi-Fi用QRコードのエスケープ規則は、ZXing wiki(バックスラッシュ)とWPA3仕様(パーセントエンコード)の2種類がある
- 記号を含まない値では結果が同じになる。記号や日本語を含むと別の文字列になり、互換性はない
- エスケープを忘れるとエラーにならず、別のSSIDとして読まれる
- 記号を使うなら、対象端末での実機確認が欠かせない
ブラウザだけで試せる無料のQRコード作成ツールでも、Wi-Fi用のコードを作れます。入力した内容はブラウザの中で処理され、サーバーには送られません。