Both Thunderbolt loops reconciled or rescanned every 2 s even on machines with no controller: on an idle M2 Max that was about 1,590 process launches a minute. While nothing is connected or boot-protected and no controller is present, the daemon now reconciles every 10 s, inside a snapshot's 20 s lifetime, and returns to every tick as soon as a controller, a policy change or a boot recovery checkpoint appears. The watcher skips an unchanged, fresh, device-free snapshot, and both loops wait without forking sleep.
272 lines
12 KiB
Bash
272 lines
12 KiB
Bash
# Session-side notification and terminal dialog functions.
|
|
|
|
tb_requests() {
|
|
TB_REQUESTS="$HOME/.local/state/omarchy/thunderbolt-authorization/requests"
|
|
mkdir -p -m 700 "$TB_REQUESTS"
|
|
chmod 700 "${TB_REQUESTS%/*}" "$TB_REQUESTS"
|
|
}
|
|
|
|
tb_safe_text() {
|
|
jq -r 'gsub("[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]"; "?")'
|
|
}
|
|
|
|
tb_notify() {
|
|
local token=$1 retry=${2:-false} title='Thunderbolt accessory blocked' description
|
|
description='Click to review it. Keep it blocked unless you recognize the device.'
|
|
if [[ $retry == "true" ]]; then
|
|
title='Thunderbolt approval needs retry'
|
|
description='The last approval could not be confirmed. Click to retry it.'
|
|
fi
|
|
omarchy-notification-wait 1 >/dev/null || return 1
|
|
timeout 10 omarchy-notification-send --app-name omarchy-action --urgency critical --glyph '' \
|
|
"$title" "$description" --exec omarchy-launch-floating-terminal-with-presentation \
|
|
omarchy-thunderbolt-authorization-review "$token"
|
|
}
|
|
|
|
tb_attention() {
|
|
omarchy-notification-wait 1 >/dev/null || return 1
|
|
timeout 10 omarchy-notification-send --app-name omarchy-action --urgency critical \
|
|
'Thunderbolt protection needs attention' 'The approval or firmware policy could not be confirmed. Click for details.' \
|
|
--exec omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-authorization status
|
|
}
|
|
|
|
tb_setup_notice() {
|
|
omarchy-notification-wait 1 >/dev/null || return 1
|
|
timeout 10 omarchy-notification-send --app-name omarchy-action --urgency critical \
|
|
'Finish Thunderbolt protection setup' 'A connected accessory needs secure enrollment. Protection is not enabled. Click to finish safely.' \
|
|
--exec omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-thunderbolt-authorization
|
|
}
|
|
|
|
tb_token() {
|
|
local digest
|
|
digest=$(jq -cS --arg watch "$2" '{watch:$watch,identity:.}' <<< "$1" | sha256sum) || return 1
|
|
printf 'request-%s\n' "${digest%% *}"
|
|
}
|
|
|
|
tb_request_read() {
|
|
[[ -f $1 && ! -L $1 ]] || return 1
|
|
jq -ce 'select(.identity | type == "object")' "$1"
|
|
}
|
|
|
|
tb_deliver() {
|
|
local path=$1 request token
|
|
request=$(tb_request_read "$path") || return 1
|
|
if ! jq -e '.notified // false' <<< "$request" >/dev/null; then
|
|
token=${path##*/}
|
|
if tb_notify "${token%.json}" "$(jq -r '.approval != null' <<< "$request")"; then
|
|
jq '.notified=true' <<< "$request" | tb_json_write "$path"
|
|
else
|
|
return 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
tb_request_apply() {
|
|
# The watcher has no terminal. Polkit permits this one fixed helper for an
|
|
# active local administrator; it cannot dispatch general setup operations.
|
|
pkexec --disable-internal-agent /usr/bin/omarchy-thunderbolt-authorization-approve "$1" "$2"
|
|
}
|
|
|
|
tb_request_process() {
|
|
local path=$1 request identity permanent result error_file error
|
|
request=$(tb_request_read "$path") || return 1
|
|
if jq -e '.pending // false' <<< "$request" >/dev/null; then
|
|
identity=$(jq -c .identity <<< "$request")
|
|
permanent=$(jq -r '.approval == "Always allow this device"' <<< "$request")
|
|
error_file=$(mktemp "$TB_REQUESTS/.error.XXXXXX") || return 1
|
|
if result=$(tb_request_apply "$permanent" "$identity" 2> "$error_file") &&
|
|
jq -e --argjson identity "$identity" --argjson permanent "$permanent" \
|
|
'any(.devices[]; .identity==$identity and (.status | IN("authorized","authorized-newkey","authorized-secure")) and
|
|
($permanent == false or .trusted))' <<< "$result" >/dev/null; then
|
|
jq --argjson result "$result" '.pending=false | .result=$result | del(.error)' <<< "$request" |
|
|
tb_json_write "$path" || return 1
|
|
else
|
|
error=$(cat "$error_file")
|
|
[[ -n $error ]] || error='Could not confirm approval. Click the retry notification.'
|
|
jq --arg error "$error" '.pending=false | .notified=false | .error=$error' <<< "$request" |
|
|
tb_json_write "$path" || return 1
|
|
fi
|
|
rm -f -- "$error_file"
|
|
fi
|
|
tb_deliver "$path"
|
|
}
|
|
|
|
tb_scan() {
|
|
local watch=$1 snapshot path request device identity token existing
|
|
snapshot=$(tb_public_snapshot) || return 1
|
|
(
|
|
flock -x 9
|
|
for path in "$TB_REQUESTS"/request-*.json; do
|
|
[[ -e $path || -L $path ]] || continue
|
|
if ! request=$(tb_request_read "$path"); then
|
|
rm -f -- "$path"
|
|
continue
|
|
fi
|
|
identity=$(jq -c .identity <<< "$request")
|
|
device=$(jq -c --argjson identity "$identity" '.devices[] | select(.identity==$identity)' <<< "$snapshot") || exit 1
|
|
if [[ -z $device ]] || { ! jq -e '.status | IN("connected","auth-error")' <<< "$device" >/dev/null &&
|
|
! jq -e '.approval != null' <<< "$request" >/dev/null; }; then
|
|
rm -- "$path"
|
|
continue
|
|
fi
|
|
if [[ $(jq -r .watch <<< "$request") != "$watch" ]]; then
|
|
rm -- "$path"
|
|
# Resume an interrupted attempt only for the same exact connection.
|
|
jq -e '.approval != null and .result == null' <<< "$request" >/dev/null || continue
|
|
token=$(tb_token "$identity" "$watch") || exit 1
|
|
path="$TB_REQUESTS/$token.json"
|
|
request=$(jq -c --arg watch "$watch" '.watch=$watch | .notified=false' <<< "$request") || exit 1
|
|
tb_json_write "$path" <<< "$request" || exit 1
|
|
fi
|
|
tb_request_process "$path" || true
|
|
done
|
|
snapshot=$(tb_public_snapshot) || exit 1
|
|
while IFS= read -r device; do
|
|
identity=$(jq -c .identity <<< "$device")
|
|
token=$(tb_token "$identity" "$watch") || exit 1
|
|
path="$TB_REQUESTS/$token.json"
|
|
if [[ ! -f $path ]]; then
|
|
jq -cn --argjson identity "$identity" --arg watch "$watch" \
|
|
'{identity:$identity,watch:$watch,notified:false}' | tb_json_write "$path" || exit 1
|
|
if ! tb_deliver "$path"; then
|
|
rm -f -- "$path"
|
|
fi
|
|
fi
|
|
done < <(jq -c '.devices[] | select((.trusted | not) and (.status | IN("connected","auth-error")) and
|
|
(.flags | contains("nopcie") | not))' <<< "$snapshot")
|
|
) 9> "$TB_REQUESTS/.lock" || return 1
|
|
printf '%s\n' "$snapshot"
|
|
}
|
|
|
|
tb_watch() {
|
|
local watch snapshot warning warned='' failures=0 text generation clean='' clean_generation='' clean_time=0
|
|
tb_requests
|
|
watch=$(cat /proc/sys/kernel/random/uuid)
|
|
while true; do
|
|
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
|
|
if [[ $warned != "pending" ]] && tb_setup_notice; then warned=pending; fi
|
|
clean=''
|
|
tb_wait 2
|
|
continue
|
|
fi
|
|
# The last scan's snapshot, unchanged, still fresh and from the same daemon,
|
|
# with no device, warning or request in flight, has nothing to deliver.
|
|
text='' generation=''
|
|
[[ -r $TB_RUNTIME/snapshot.json ]] && IFS= read -r -d '' text < "$TB_RUNTIME/snapshot.json" || true
|
|
[[ -r $TB_RUNTIME/generation ]] && IFS= read -r generation < "$TB_RUNTIME/generation" || true
|
|
if [[ -n $clean && ${text%$'\n'} == "$clean" && $generation == "$clean_generation" ]] &&
|
|
(( EPOCHSECONDS - clean_time >= 0 && EPOCHSECONDS - clean_time < 20 )) &&
|
|
! compgen -G "$TB_REQUESTS/request-*.json" >/dev/null; then
|
|
tb_wait 2
|
|
continue
|
|
fi
|
|
clean=''
|
|
if snapshot=$(tb_scan "$watch"); then
|
|
failures=0
|
|
warning=$(jq -c '[.generation,.warnings,.error]' <<< "$snapshot")
|
|
if jq -e '(.warnings | length > 0) or .error != ""' <<< "$snapshot" >/dev/null; then
|
|
if [[ $warning != "$warned" ]] && tb_attention; then
|
|
warned=$warning
|
|
fi
|
|
else
|
|
warned=''
|
|
if jq -e '.devices == []' <<< "$snapshot" >/dev/null; then
|
|
IFS=' ' read -r clean_generation clean_time <<< "$(jq -r '"\(.generation) \(.time)"' <<< "$snapshot")"
|
|
[[ $clean_time =~ ^[0-9]+$ ]] && clean=$snapshot
|
|
fi
|
|
fi
|
|
else
|
|
failures=$((failures + 1))
|
|
if (( failures >= 3 )) && [[ $warned != "unavailable" ]] && tb_attention; then
|
|
warned=unavailable
|
|
fi
|
|
fi
|
|
tb_wait 2
|
|
done
|
|
}
|
|
|
|
tb_find_device() {
|
|
local snapshot
|
|
snapshot=$(tb_public_snapshot) || return 1
|
|
jq -ce --argjson identity "$1" '.devices[] | select(.identity==$identity)' <<< "$snapshot" ||
|
|
tb_fail 'That Thunderbolt device was removed or changed. Open its latest notification.'
|
|
}
|
|
|
|
tb_review() {
|
|
local token=$1 path request identity device choice latest index
|
|
[[ $token =~ ^request-[0-9a-f]{64}$ ]] || { tb_fail 'Invalid or expired Thunderbolt request'; return 1; }
|
|
tb_requests
|
|
path="$TB_REQUESTS/$token.json"
|
|
request=$(tb_request_read "$path") || return 1
|
|
identity=$(jq -c .identity <<< "$request")
|
|
device=$(tb_find_device "$identity") || return 1
|
|
choice=$(jq -r '.approval // empty' <<< "$request")
|
|
if [[ -z $choice ]]; then
|
|
jq -e '.status | IN("connected","auth-error")' <<< "$device" >/dev/null || { tb_fail 'This device is no longer blocked'; return 1; }
|
|
gum style --foreground 212 --bold 'Blocked Thunderbolt accessory'
|
|
printf '\nThese details come from the device and can be forged:\n'
|
|
for index in Name Vendor Uid; do
|
|
printf '%s: ' "$index"
|
|
jq --arg key "$index" '.[$key]' <<< "$identity" | tb_safe_text
|
|
done
|
|
printf '\nApproval gives this accessory access to its PCIe drivers.\n\n'
|
|
choice=$(gum choose 'Allow once' 'Always allow this device' 'Keep blocked') || return 0
|
|
fi
|
|
case "$choice" in
|
|
'Allow once'|'Always allow this device'|'Keep blocked') ;;
|
|
*) tb_fail 'Invalid approval choice'; return 1 ;;
|
|
esac
|
|
(
|
|
flock -x 9
|
|
latest=$(tb_request_read "$path") || exit 1
|
|
[[ $latest == "$request" ]] || { tb_fail 'This request changed. Open the latest notification.'; exit 1; }
|
|
device=$(tb_find_device "$identity") || exit 1
|
|
if [[ $choice == "Keep blocked" ]]; then
|
|
printf 'Thunderbolt accessory remains blocked.\n'
|
|
else
|
|
if ! jq -e '.approval != null' <<< "$request" >/dev/null; then
|
|
jq -e '.status | IN("connected","auth-error")' <<< "$device" >/dev/null || exit 1
|
|
fi
|
|
jq --arg choice "$choice" '.approval=$choice | .pending=true | del(.error,.result)' <<< "$request" |
|
|
tb_json_write "$path"
|
|
fi
|
|
) 9> "$TB_REQUESTS/.lock" || return 1
|
|
if [[ $choice != "Keep blocked" ]]; then
|
|
for ((index=0; index<60; index++)); do
|
|
latest=$(tb_request_read "$path") || return 1
|
|
if jq -e '.result != null' <<< "$latest" >/dev/null; then
|
|
rm -- "$path"
|
|
if [[ $choice == "Always allow this device" ]]; then
|
|
printf 'Thunderbolt accessory trusted.\n'
|
|
else
|
|
printf 'Thunderbolt accessory allowed for this connection.\n'
|
|
fi
|
|
return 0
|
|
elif jq -e '.error != null' <<< "$latest" >/dev/null; then
|
|
jq .error <<< "$latest" | tb_safe_text >&2
|
|
return 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
tb_fail 'Approval is still pending. The watcher will retain and retry the request.'
|
|
fi
|
|
}
|
|
|
|
tb_status() {
|
|
local snapshot
|
|
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
|
|
printf 'Thunderbolt protection is deferred. Previous Bolt behavior remains active.\nKeep a keyboard/input device available independently of Thunderbolt. Safely disconnect the Thunderbolt accessories, then run Setup > Security > Thunderbolt. Reconnect each accessory and choose Always allow to save its secure key.\n'
|
|
return 0
|
|
fi
|
|
snapshot=$(tb_public_snapshot) || return 1
|
|
printf 'Thunderbolt device approval is enabled.\nTrusted accessories reconnect automatically; new accessories require approval.\n'
|
|
if jq -e '.domains | length == 0' <<< "$snapshot" >/dev/null; then
|
|
printf 'No Thunderbolt controller is currently visible. Its firmware policy will be checked when it appears.\n'
|
|
fi
|
|
jq '.warnings[], (.error | select(. != ""))' <<< "$snapshot" | while IFS= read -r warning; do
|
|
printf 'Attention: '
|
|
tb_safe_text <<< "$warning"
|
|
done
|
|
printf 'Firmware boot access is separate; see the Security manual before relying on pre-boot protection.\n'
|
|
}
|