#!/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:;P:;H:;; # # 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 -> {"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 ]\n' >&2; exit 2 ;; esac