tion
Architecture Decisions
The provisioning pipeline must be entirely client-side. Transmitting network credentials to a remote API for QR generation introduces unacceptable risks, including credential logging, man-in-the-middle exposure, and dependency on external availability. The solution involves a local TypeScript module that accepts a network configuration object, sanitizes the fields, constructs the compliant payload, and returns the string for local QR matrix rendering.
Implementation Strategy
- Strict Escaping: Implement a sanitization function that processes characters in a specific order to prevent double-escaping. The backslash must be escaped first.
- Schema Compliance: Construct the payload using the
WIFI: prefix, key-value pairs separated by semicolons, and a mandatory double-semicolon terminator.
- Conditional Fields: Omit the password field for open networks (
nopass) to prevent parser confusion. Use the H:true flag for hidden SSIDs.
TypeScript Implementation
The following implementation uses a builder pattern to ensure type safety and clear separation of concerns. It differs from naive approaches by using a character map for escaping and enforcing structural rules.
/**
* Supported authentication protocols for WiFi QR payloads.
*/
export type AuthProtocol = 'WPA' | 'WEP' | 'nopass';
/**
* Configuration interface for WiFi network provisioning.
*/
export interface NetworkProfile {
ssid: string;
passphrase?: string;
protocol?: AuthProtocol;
isHidden?: boolean;
}
/**
* Sanitizes a raw string segment according to MECARD escaping rules.
*
* Critical: Backslashes must be escaped first to prevent double-escaping
* artifacts when other characters are processed.
*
* @param raw - The unescaped string segment.
* @returns The sanitized string safe for QR payload inclusion.
*/
function sanitizeSegment(raw: string): string {
if (!raw) return '';
// Map of characters that require escaping in the WiFi schema
const escapeMap: Record<string, string> = {
'\\': '\\\\',
';': '\\;',
':': '\\:',
',': '\\,'
};
// Split into characters, map to escaped version if present, rejoin
return raw.split('').map(char => escapeMap[char] || char).join('');
}
/**
* Constructs a compliant WiFi QR code payload string.
*
* @param profile - The network configuration object.
* @returns A string ready for QR code matrix generation.
* @throws Error if required fields are missing or invalid.
*/
export function constructWifiPayload(profile: NetworkProfile): string {
if (!profile.ssid) {
throw new Error('SSID is required for WiFi payload construction.');
}
const protocol = profile.protocol || 'nopass';
// Validate password requirement for secure protocols
if (protocol !== 'nopass' && !profile.passphrase) {
throw new Error(`Passphrase is required for protocol: ${protocol}`);
}
// Build key-value pairs
const segments: string[] = [];
// SSID is mandatory and must be escaped
segments.push(`S:${sanitizeSegment(profile.ssid)}`);
// Protocol type
segments.push(`T:${protocol}`);
// Password is only included for secure networks
if (protocol !== 'nopass' && profile.passphrase) {
segments.push(`P:${sanitizeSegment(profile.passphrase)}`);
}
// Hidden network flag
if (profile.isHidden) {
segments.push('H:true');
}
// Join segments with semicolons and append mandatory terminator
return `WIFI:${segments.join(';')};`;
}
Usage Example
const network: NetworkProfile = {
ssid: 'Office:Network\\Main',
passphrase: 'p@ss;word123',
protocol: 'WPA',
isHidden: false
};
try {
const payload = constructWifiPayload(network);
console.log(payload);
// Output: WIFI:S:Office\:Network\\Main;T:WPA;P:p@ss\;word123;;
// Pass 'payload' to your local QR library (e.g., qrcode.react, qr-image)
} catch (error) {
console.error('Payload construction failed:', error);
}
Rationale for Choices
- Character Map vs. Regex: Using a map and split/join avoids the cognitive load of chaining multiple regex replacements and reduces the risk of ordering errors.
- Explicit
nopass Handling: The code explicitly omits the P: field for open networks. Some parsers interpret an empty P:: field as a password consisting of a single colon, causing authentication failures.
- Terminator Enforcement: The function always appends the trailing semicolon. The schema requires
;; at the end of the payload to signal the parser that the structure is complete.
Pitfall Guide
1. Reverse Escaping Order
- Explanation: If you escape the semicolon before the backslash, a password like
pass\;word becomes pass\\;;word. The parser sees \\ (escaped backslash) followed by ; (unescaped delimiter), truncating the password.
- Fix: Always escape backslashes first. The implementation above handles this via the map logic, but if using regex, ensure
.replace(/\\/g, '\\\\') runs before other replacements.
2. The "Wifi:" Case Trap
- Explanation: Using
Wifi: or wifi: as the prefix. While modern iOS versions may be lenient, many Android devices and older firmware strictly require uppercase WIFI:.
- Fix: Hardcode the prefix as
WIFI: in your generator.
3. Implicit Quote Injection
- Explanation: Wrapping SSIDs or passwords in double quotes (e.g.,
S:"My Network"). Some parsers strip quotes, but others treat them as literal characters, causing the device to search for an SSID named "My Network" (including quotes).
- Fix: Never add quotes. Rely on escaping for special characters. The parser handles raw strings correctly when escaped.
4. Missing Terminator
- Explanation: Omitting the final
;;. The parser state machine may not recognize the end of the payload, leading to incomplete parsing or ignoring the code entirely.
- Fix: Ensure every payload ends with a double semicolon.
5. Over-Engineering Error Correction
- Explanation: Using High (H) error correction levels. This increases the density of the QR matrix, making it harder to scan in low light or at a distance.
- Fix: Use Medium (M) or Quartile (Q) error correction. This provides sufficient damage resistance for most environments while maintaining scan speed.
6. Hidden Flag Syntax Errors
- Explanation: Using
H:1 or H:yes for hidden networks. The schema expects a boolean string.
- Fix: Use
H:true for hidden networks. Omit the field entirely for visible networks.
7. Empty Password Field for Open Networks
- Explanation: Including
P: with an empty value for nopass networks.
- Fix: Omit the
P: segment entirely when the protocol is nopass.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Guest Network with Simple Password | Naive Encoder | Low risk of special characters; speed of implementation prioritized. | Low |
| Enterprise Network with Complex Creds | Sanitized Builder | High probability of special chars; silent failures are unacceptable. | Medium |
| High-Security Environment | Client-Side Only + ECC M | Prevents credential leakage; ECC M balances security and scan reliability. | Medium |
| Legacy Device Support | Uppercase Prefix + Strict Escaping | Older firmware is less forgiving of schema deviations. | Low |
| Low-Light Scanning Environment | ECC Q + High Contrast | Quartile correction offers better damage resistance than M for physical wear. | Low |
Configuration Template
Copy this template into your project to establish a secure, compliant WiFi QR generation module.
// wifi-qr-encoder.ts
export type AuthProtocol = 'WPA' | 'WEP' | 'nopass';
export interface NetworkProfile {
ssid: string;
passphrase?: string;
protocol?: AuthProtocol;
isHidden?: boolean;
}
const ESCAPE_MAP: Record<string, string> = {
'\\': '\\\\',
';': '\\;',
':': '\\:',
',': '\\,'
};
function sanitize(raw: string): string {
return raw.split('').map(c => ESCAPE_MAP[c] || c).join('');
}
export function createWifiPayload(profile: NetworkProfile): string {
if (!profile.ssid) throw new Error('SSID is mandatory.');
const proto = profile.protocol || 'nopass';
if (proto !== 'nopass' && !profile.passphrase) {
throw new Error(`Passphrase required for ${proto}.`);
}
const parts = [
`S:${sanitize(profile.ssid)}`,
`T:${proto}`
];
if (proto !== 'nopass' && profile.passphrase) {
parts.push(`P:${sanitize(profile.passphrase)}`);
}
if (profile.isHidden) {
parts.push('H:true');
}
return `WIFI:${parts.join(';')};`;
}
Quick Start Guide
- Install Dependencies: Add a client-side QR library to your project (e.g.,
npm install qrcode.react for React or qr-image for Node).
- Import Module: Import
createWifiPayload and NetworkProfile from your encoder module.
- Define Profile: Create a
NetworkProfile object with your network details.
const profile: NetworkProfile = {
ssid: 'MySecureNetwork',
passphrase: 'Complex!Pass;123',
protocol: 'WPA'
};
- Generate Payload: Call
createWifiPayload(profile) to get the string.
- Render QR: Pass the payload string to your QR library with ECC level 'M'.
// Example with qrcode.react
<QRCode value={payload} level="M" />
- Test: Scan the generated code with an iOS and Android device to verify automatic connection.