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:
@@ -19,13 +19,18 @@ import qs.services
|
|||||||
SettingsPage {
|
SettingsPage {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
|
||||||
title: "Network & Devices"
|
title: "Network & Devices"
|
||||||
lede: Connectivity.activeNetwork
|
lede: Connectivity.activeNetwork
|
||||||
? "Connected to " + Connectivity.activeNetwork.name
|
? "Connected to " + Connectivity.activeNetwork.name
|
||||||
: "Wi-Fi, Bluetooth, and the things Fedora owns."
|
: "Wi-Fi, Bluetooth, and the things Fedora owns."
|
||||||
|
|
||||||
// Drive the scanners only while this page is the one being shown.
|
// Drive the scanners only while this page is the one being shown.
|
||||||
Component.onCompleted: Connectivity.active = true
|
Component.onCompleted: {
|
||||||
|
Connectivity.active = true;
|
||||||
|
if (!WifiShare.scanned)
|
||||||
|
WifiShare.refresh();
|
||||||
|
}
|
||||||
Component.onDestruction: Connectivity.active = false
|
Component.onDestruction: Connectivity.active = false
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
@@ -68,6 +73,69 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sharing a network by QR, the way GNOME's Wi-Fi panel does. The
|
||||||
|
// alternative is reading a passphrase out loud.
|
||||||
|
//
|
||||||
|
// The image holds the password in machine-readable form, so it is generated
|
||||||
|
// on demand rather than up front, and the helper writes it to tmpfs under
|
||||||
|
// XDG_RUNTIME_DIR instead of anywhere persistent.
|
||||||
|
SettingsCard {
|
||||||
|
visible: Connectivity.wifiDevice !== null && WifiShare.shareable.length > 0
|
||||||
|
title: "Share a network"
|
||||||
|
subtitle: WifiShare.sharing !== ""
|
||||||
|
? "Point a phone's camera at the code to join " + WifiShare.sharing + "."
|
||||||
|
: "Shows a QR code a phone can scan to join, without reading the password out."
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: WifiShare.shareable
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
id: shareRow
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
label: shareRow.modelData.ssid
|
||||||
|
detail: WifiShare.sharing === shareRow.modelData.name
|
||||||
|
? "Showing a code below — anyone who can see the screen can join"
|
||||||
|
: "Saved network"
|
||||||
|
action: WifiShare.sharing === shareRow.modelData.name ? "Hide" : "Show code"
|
||||||
|
divider: shareRow.index < WifiShare.shareable.length - 1 || WifiShare.sharing !== ""
|
||||||
|
onTriggered: WifiShare.sharing === shareRow.modelData.name
|
||||||
|
? WifiShare.stopSharing()
|
||||||
|
: WifiShare.share(shareRow.modelData.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drawn at its natural size on a white plate: a QR code inverted or
|
||||||
|
// tinted to match a dark theme is unreliable to scan, and this one has
|
||||||
|
// exactly one job.
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
visible: WifiShare.sharing !== "" && WifiShare.imagePath !== ""
|
||||||
|
implicitHeight: visible ? plate.height + 20 : 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: plate
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
y: 10
|
||||||
|
width: 208
|
||||||
|
height: 208
|
||||||
|
radius: 10
|
||||||
|
color: "white"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 184
|
||||||
|
height: 184
|
||||||
|
smooth: false
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
cache: false
|
||||||
|
source: WifiShare.imagePath !== "" ? "file://" + WifiShare.imagePath : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Bluetooth"
|
title: "Bluetooth"
|
||||||
visible: Connectivity.adapter !== null
|
visible: Connectivity.adapter !== null
|
||||||
|
|||||||
Executable
+121
@@ -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
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
// A QR code for a saved Wi-Fi network, so a guest can join by pointing a phone
|
||||||
|
// at the screen. GNOME's Wi-Fi panel has this and it is the most-used thing in
|
||||||
|
// it; reading a passphrase aloud is the alternative.
|
||||||
|
//
|
||||||
|
// The generated image contains the network password in machine-readable form,
|
||||||
|
// so the helper writes it under XDG_RUNTIME_DIR -- 0700, on tmpfs, gone at
|
||||||
|
// logout -- rather than anywhere persistent. Nothing here ever holds the
|
||||||
|
// passphrase itself; this service only ever sees a file path.
|
||||||
|
//
|
||||||
|
// Generated on demand. Producing a QR for every saved network up front would
|
||||||
|
// mean writing images of passwords nobody asked to see.
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-wifi-qr"
|
||||||
|
|
||||||
|
// [{ name, ssid, shareable }]
|
||||||
|
property var networks: []
|
||||||
|
property bool scanned: false
|
||||||
|
property string lastError: ""
|
||||||
|
|
||||||
|
// The network whose code is on screen, and where its image is. Empty when
|
||||||
|
// nothing is being shared.
|
||||||
|
property string sharing: ""
|
||||||
|
property string imagePath: ""
|
||||||
|
|
||||||
|
readonly property var shareable: root.networks.filter(n => n.shareable)
|
||||||
|
|
||||||
|
function refresh(): void {
|
||||||
|
if (!list.running)
|
||||||
|
list.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function share(name: string): void {
|
||||||
|
if (generate.running)
|
||||||
|
return;
|
||||||
|
// Cache-bust: the helper reuses one file per network, so a QML Image
|
||||||
|
// pointed at the same path would keep showing the previous render.
|
||||||
|
root.imagePath = "";
|
||||||
|
root.sharing = name;
|
||||||
|
generate.command = [root.helperPath, "qr", name];
|
||||||
|
generate.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSharing(): void {
|
||||||
|
root.sharing = "";
|
||||||
|
root.imagePath = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: list
|
||||||
|
command: [root.helperPath, "list"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(this.text);
|
||||||
|
root.networks = Array.isArray(parsed.networks) ? parsed.networks : [];
|
||||||
|
root.lastError = String(parsed.error ?? "");
|
||||||
|
} catch (error) {
|
||||||
|
root.networks = [];
|
||||||
|
root.lastError = "Could not read the Wi-Fi helper's output.";
|
||||||
|
console.warn("WifiShare: could not parse helper output:", error);
|
||||||
|
}
|
||||||
|
root.scanned = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: generate
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(this.text);
|
||||||
|
const path = String(parsed.path ?? "");
|
||||||
|
const error = String(parsed.error ?? "");
|
||||||
|
if (error !== "" || path === "") {
|
||||||
|
root.lastError = error !== "" ? error : "No QR code was produced.";
|
||||||
|
root.sharing = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.lastError = "";
|
||||||
|
root.imagePath = path;
|
||||||
|
} catch (error) {
|
||||||
|
root.lastError = "Could not read the generated QR code's path.";
|
||||||
|
root.sharing = "";
|
||||||
|
console.warn("WifiShare: could not parse helper output:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+126
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# panama-wifi-qr renders a saved network as a QR code a phone can scan.
|
||||||
|
#
|
||||||
|
# The QR contains the network PASSWORD in machine-readable form, so most of what
|
||||||
|
# is worth testing here is about handling that safely rather than about QR
|
||||||
|
# codes. Both nmcli and qrencode are stubbed: the real ones would read this
|
||||||
|
# machine's actual passphrases, and a test that writes the daily driver's Wi-Fi
|
||||||
|
# password into a fixture directory is not one worth having.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
helper="$repo_dir/config/dot/quickshell/scripts/panama-wifi-qr"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'wifi qr contract: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
work="$(mktemp -d /tmp/panama-wifiqr.XXXXXX)"
|
||||||
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
mkdir -p "$work/bin" "$work/run"
|
||||||
|
|
||||||
|
readonly SECRET='hunter2-secret'
|
||||||
|
|
||||||
|
cat >"$work/bin/nmcli" <<STUB
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# -t -f NAME,TYPE connection show
|
||||||
|
if [[ "\$*" == *"-f NAME,TYPE"* ]]; then
|
||||||
|
printf 'home net:802-11-wireless\n'
|
||||||
|
printf 'work-eap:802-11-wireless\n'
|
||||||
|
printf 'Wired connection 1:802-3-ethernet\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
name="\${@: -1}"
|
||||||
|
case "\$*" in
|
||||||
|
*802-11-wireless.ssid*)
|
||||||
|
# An SSID containing reserved characters, to prove they are escaped.
|
||||||
|
case "\$name" in
|
||||||
|
"home net") printf 'home;net\n' ;;
|
||||||
|
"work-eap") printf 'work-eap\n' ;;
|
||||||
|
esac ;;
|
||||||
|
*802-11-wireless.hidden*) printf 'no\n' ;;
|
||||||
|
*802-11-wireless-security.psk*)
|
||||||
|
# work-eap is enterprise: no passphrase exists to share.
|
||||||
|
[[ "\$name" == "home net" ]] && printf '%s\n' "$SECRET" ;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
STUB
|
||||||
|
chmod +x "$work/bin/nmcli"
|
||||||
|
|
||||||
|
# Records its argv and its stdin separately, so the test can prove the secret
|
||||||
|
# arrived on stdin and never on the command line -- argv is world-readable
|
||||||
|
# through /proc while a process runs.
|
||||||
|
cat >"$work/bin/qrencode" <<'STUB'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n' "$*" >>"$QRENCODE_ARGV_LOG"
|
||||||
|
out=""
|
||||||
|
prev=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
[[ "$prev" == "-o" ]] && out="$arg"
|
||||||
|
prev="$arg"
|
||||||
|
done
|
||||||
|
cat >"$QRENCODE_STDIN_LOG"
|
||||||
|
printf 'fake-png' >"$out"
|
||||||
|
exit 0
|
||||||
|
STUB
|
||||||
|
chmod +x "$work/bin/qrencode"
|
||||||
|
|
||||||
|
export QRENCODE_ARGV_LOG="$work/argv.log"
|
||||||
|
export QRENCODE_STDIN_LOG="$work/stdin.log"
|
||||||
|
: >"$QRENCODE_ARGV_LOG"
|
||||||
|
: >"$QRENCODE_STDIN_LOG"
|
||||||
|
|
||||||
|
run() { PATH="$work/bin:$PATH" XDG_RUNTIME_DIR="$work/run" "$helper" "$@"; }
|
||||||
|
|
||||||
|
# ── Listing distinguishes shareable from not ────────────────────────────────
|
||||||
|
out="$(run list)"
|
||||||
|
jq -e . >/dev/null 2>&1 <<<"$out" || fail "list did not emit JSON: $out"
|
||||||
|
[[ "$(jq -r '.networks | length' <<<"$out")" == "2" ]] \
|
||||||
|
|| fail "only wireless connections belong in the list: $out"
|
||||||
|
jq -e '.networks[] | select(.name == "home net") | .shareable == true' >/dev/null <<<"$out" \
|
||||||
|
|| fail "a network with a passphrase must be shareable: $out"
|
||||||
|
jq -e '.networks[] | select(.name == "work-eap") | .shareable == false' >/dev/null <<<"$out" \
|
||||||
|
|| fail "an enterprise network has no passphrase, so a QR code for it cannot work: $out"
|
||||||
|
|
||||||
|
# ── The payload ─────────────────────────────────────────────────────────────
|
||||||
|
path="$(run qr 'home net' | jq -r .path)"
|
||||||
|
[[ -n "$path" && -e "$path" ]] || fail 'no image was produced'
|
||||||
|
|
||||||
|
payload="$(cat "$QRENCODE_STDIN_LOG")"
|
||||||
|
grep -q "P:$SECRET;" <<<"$payload" \
|
||||||
|
|| fail 'the passphrase did not reach the payload intact'
|
||||||
|
|
||||||
|
# The SSID is "home;net": unescaped, the semicolon ends the S: field early and
|
||||||
|
# the code describes a different network.
|
||||||
|
grep -qF 'S:home\;net;' <<<"$payload" \
|
||||||
|
|| fail "a reserved character in the SSID was not escaped: $payload"
|
||||||
|
|
||||||
|
[[ "$(wc -l <"$QRENCODE_STDIN_LOG")" == "0" ]] \
|
||||||
|
|| fail "the payload contains a newline; nmcli's trailing newline must be stripped: $(cat -A "$QRENCODE_STDIN_LOG")"
|
||||||
|
|
||||||
|
grep -q ';;$' <<<"$payload" || fail "the WIFI: URI must be terminated with ;;: $payload"
|
||||||
|
|
||||||
|
# ── The secret must never appear in argv ────────────────────────────────────
|
||||||
|
grep -q "$SECRET" "$QRENCODE_ARGV_LOG" \
|
||||||
|
&& fail 'the passphrase was passed as a command-line argument, where /proc exposes it to every process on the machine'
|
||||||
|
|
||||||
|
# ── The image and its directory must not be readable by others ──────────────
|
||||||
|
[[ "$(stat -c '%a' "$path")" == "600" ]] \
|
||||||
|
|| fail "the QR image is mode $(stat -c '%a' "$path"); it contains a password"
|
||||||
|
[[ "$(stat -c '%a' "$(dirname "$path")")" == "700" ]] \
|
||||||
|
|| fail "the directory holding QR images is mode $(stat -c '%a' "$(dirname "$path")")"
|
||||||
|
|
||||||
|
# ── No temporary payload files may survive ──────────────────────────────────
|
||||||
|
leftovers="$(find "$work/run" -name 'payload.*' | wc -l)"
|
||||||
|
[[ "$leftovers" == "0" ]] \
|
||||||
|
|| fail "$leftovers temporary payload file(s) containing the passphrase were left behind"
|
||||||
|
|
||||||
|
# ── An unknown network is an error, not an empty image ──────────────────────
|
||||||
|
out="$(run qr 'no-such-network')"
|
||||||
|
jq -e '.path == "" and .error != ""' >/dev/null <<<"$out" \
|
||||||
|
|| fail "an unknown network must be reported: $out"
|
||||||
|
|
||||||
|
printf 'wifi qr contract: PASS\n'
|
||||||
Reference in New Issue
Block a user