Share a Wi-Fi network by QR code

GNOME's Wi-Fi panel has this and it is the most-used thing in it: the
alternative is reading a passphrase out loud. Network & Devices now shows
a scannable code for any saved network whose passphrase this user can
read.

The image contains the network password in machine-readable form, so
most of the care here is about that rather than about QR codes. It is
written under XDG_RUNTIME_DIR -- 0700, on tmpfs, gone at logout --
rather than /tmp, which is shared; the file is 0600; the passphrase is
piped to qrencode on stdin rather than passed as an argument, because
argv is world-readable through /proc for as long as the process runs;
and it is never printed or included in an error message. Generated on
demand, because producing a code for every saved network up front means
writing images of passwords nobody asked to see.

Enterprise networks are listed as not shareable rather than offered and
broken: there is no passphrase to encode, so the code could not work.

Two bugs the contract caught while being written. Semicolons in an SSID
were not escaped -- the sed replacement had one backslash where its four
neighbours have two, so sed dropped it, and an SSID containing a
semicolon would have produced a QR code describing a different network.
And nmcli's trailing newline landed inside the payload; it decoded here,
but a newline in the middle of a WIFI: URI is not something every phone
tolerates, and that failure would present as "the QR code just doesn't
work on my phone".

The contract stubs nmcli and qrencode, because the real ones would write
this machine's actual Wi-Fi password into a fixture directory. It
asserts the escaping, the absence of a newline, the file and directory
modes, that no temporary payload survives, and that the passphrase never
reaches argv.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Gabriel Brown
2026-08-18 10:56:01 -04:00
parent 54b8a82803
commit 4279da999a
4 changed files with 415 additions and 1 deletions
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# A QR code for a saved Wi-Fi network, so a guest can join by pointing a camera.
#
# GNOME's Wi-Fi panel has this and it is the single most-used thing in it.
# The payload is the de-facto WIFI: URI that Android and iOS both scan:
#
# WIFI:T:WPA;S:<ssid>;P:<passphrase>;H:<hidden>;;
#
# HANDLING THE PASSPHRASE
#
# This image contains the network password in machine-readable form. Anyone who
# can read the file can read the password, so:
#
# * it is written under XDG_RUNTIME_DIR, which is 0700 and on tmpfs, so it
# never reaches disk and disappears at logout -- not /tmp, which is shared;
# * it is created with umask 077;
# * the passphrase is never printed, never passed as an argument (argv is
# world-readable via /proc), and never appears in an error message.
#
# It is piped to qrencode on stdin for that last reason.
#
# Usage:
# panama-wifi-qr list -> {"networks":[{"name","ssid","shareable"}]}
# panama-wifi-qr qr <name> -> {"path":"/run/user/…/….png"}
set -uo pipefail
emit_error() {
printf '{"networks":[],"path":"","error":%s}\n' "$(jq -Rn --arg e "$1" '$e')"
exit 0
}
command -v nmcli >/dev/null 2>&1 || emit_error 'NetworkManager is not available'
command -v qrencode >/dev/null 2>&1 || emit_error 'qrencode is not installed, so a Wi-Fi QR code cannot be drawn'
cmd_list() {
local rows=() name ssid psk
while IFS= read -r name; do
[[ -n "$name" ]] || continue
ssid="$(nmcli -g 802-11-wireless.ssid connection show "$name" 2>/dev/null)"
[[ -n "$ssid" ]] || ssid="$name"
# Only networks whose passphrase this user can actually read are
# shareable. An enterprise network has no passphrase to share at all,
# and a QR code for one would simply not work.
psk="$(nmcli -s -g 802-11-wireless-security.psk connection show "$name" 2>/dev/null)"
rows+=("$(jq -cn --arg name "$name" --arg ssid "$ssid" \
--argjson shareable "$([[ -n "$psk" ]] && echo true || echo false)" \
'{name: $name, ssid: $ssid, shareable: $shareable}')")
done < <(nmcli -t -f NAME,TYPE connection show 2>/dev/null \
| awk -F: '$2 == "802-11-wireless" { print $1 }')
if [[ ${#rows[@]} -eq 0 ]]; then
printf '{"networks":[],"path":"","error":"No saved Wi-Fi networks."}\n'
return 0
fi
printf '{"networks":[%s],"path":"","error":""}\n' "$(IFS=,; printf '%s' "${rows[*]}")"
}
# The WIFI: URI reserves \ ; , : and ", each escaped with a backslash. An SSID
# containing a semicolon would otherwise terminate the field early and produce a
# QR code for a different network entirely.
#
# Trailing newlines are stripped as well. nmcli terminates every value with one,
# and left in place it lands INSIDE the payload -- the code still decodes here,
# but a newline in the middle of a WIFI: URI is not something every phone's
# scanner tolerates, and the failure would look like "the QR code just does not
# work on my phone".
escape_field() {
sed -e 's/\\/\\\\/g' -e 's/;/\\;/g' -e 's/,/\\,/g' -e 's/:/\\:/g' -e 's/"/\\"/g' \
| tr -d '\n'
}
cmd_qr() {
local name="${1:-}"
[[ -n "$name" ]] || emit_error 'no network named'
local ssid hidden psk_file payload_file out_dir out_file
ssid="$(nmcli -g 802-11-wireless.ssid connection show "$name" 2>/dev/null)"
[[ -n "$ssid" ]] || emit_error "There is no saved network called \"$name\"."
hidden="$(nmcli -g 802-11-wireless.hidden connection show "$name" 2>/dev/null)"
[[ "$hidden" == "yes" ]] && hidden=true || hidden=false
out_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/panama"
umask 077
mkdir -p "$out_dir" 2>/dev/null || emit_error 'could not create the runtime directory'
chmod 700 "$out_dir" 2>/dev/null || true
# Named after the connection, hashed, so repeated shares reuse one file
# instead of accumulating images of the password.
out_file="$out_dir/wifi-$(printf '%s' "$name" | sha256sum | cut -c1-16).png"
# Built in a file rather than a variable that could be echoed, and piped to
# qrencode on stdin so the passphrase never appears in argv.
payload_file="$(mktemp "$out_dir/payload.XXXXXX")" || emit_error 'could not create a temporary file'
trap 'rm -f "$payload_file"' RETURN
{
printf 'WIFI:T:WPA;S:'
printf '%s' "$ssid" | escape_field
printf ';P:'
nmcli -s -g 802-11-wireless-security.psk connection show "$name" 2>/dev/null | escape_field
printf ';H:%s;;' "$hidden"
} >"$payload_file"
if ! qrencode -o "$out_file" -s 8 -m 2 -l M <"$payload_file" 2>/dev/null; then
emit_error "Could not generate a QR code for \"$name\"."
fi
chmod 600 "$out_file" 2>/dev/null || true
jq -cn --arg path "$out_file" '{networks: [], path: $path, error: ""}'
}
case "${1:-list}" in
list) cmd_list ;;
qr) shift; cmd_qr "${1:-}" ;;
*) printf 'usage: panama-wifi-qr [list|qr <name>]\n' >&2; exit 2 ;;
esac