Merge branch 'quattro' into unified-input-method-setup

This commit is contained in:
David Heinemeier Hansson committed 2026-10-10 15:51:52 +02:00
commit 101bb547bb
245 files changed
+19949 -375

No files matched your search

+3
View File
@@ -6,6 +6,9 @@ run_logged "$OMARCHY_INSTALL/config/fix-powerprofilesctl-shebang.sh"
run_logged "$OMARCHY_INSTALL/config/ssh-command-path.sh"
run_logged "$OMARCHY_INSTALL/config/ssh-keepalive.sh"
run_logged "$OMARCHY_INSTALL/config/docker.sh"
run_logged "$OMARCHY_INSTALL/config/locale.sh"
run_logged "$OMARCHY_INSTALL/config/snapper.sh"
run_logged "$OMARCHY_INSTALL/config/usb-authorization.sh"
run_logged "$OMARCHY_INSTALL/config/enable-services.sh"
run_logged "$OMARCHY_INSTALL/config/firewall.sh"
run_logged "$OMARCHY_INSTALL/config/thunderbolt-authorization.sh"
+50
View File
@@ -0,0 +1,50 @@
# An image built from a distribution's root tarball rather than the ISO (Arch
# Linux ARM's ships LANG=C) never went through the ISO's locale step, so it
# runs non-UTF-8: byte-wise sorting, ASCII-only \u escapes, and any tool that
# reads the locale for its encoding.
# Root always writes the real files; the overrides are for unprivileged tests.
locale_conf=/etc/locale.conf
locale_gen=/etc/locale.gen
if (( EUID != 0 )); then
locale_conf=${OMARCHY_LOCALE_CONF:-$locale_conf}
locale_gen=${OMARCHY_LOCALE_GEN:-$locale_gen}
fi
# Repair only the stock state -- an unset LANG, or the bare C/POSIX the image
# ships. Any named locale is somebody's choice, C.UTF-8 included, so leave it.
# A machine with no locale.conf at all reads as unset, not as a failure:
# under pipefail the missing file would otherwise abort the installer.
current=$(sed -n 's/^LANG=//p' "$locale_conf" 2>/dev/null | tail -1 | tr -d '"') || current=""
case ${current:-C} in
C | POSIX) ;;
*)
echo "Leaving the locale as $current"
return 0 2>/dev/null || exit 0
;;
esac
echo "Setting up locale (en_US.UTF-8)..."
if ! locale -a 2>/dev/null | grep -qi "en_US.utf-\?8"; then
if grep -q '^#en_US.UTF-8' "$locale_gen" 2>/dev/null; then
sed -i 's/^#en_US.UTF-8/en_US.UTF-8/' "$locale_gen"
elif ! grep -q '^en_US.UTF-8' "$locale_gen" 2>/dev/null; then
echo "en_US.UTF-8 UTF-8" >>"$locale_gen"
fi
locale-gen >/dev/null 2>&1
fi
# Only LANG changes; LC_* lines somebody set stay.
if grep -q '^LANG=' "$locale_conf" 2>/dev/null; then
sed -i 's/^LANG=.*/LANG=en_US.UTF-8/' "$locale_conf"
else
echo "LANG=en_US.UTF-8" >>"$locale_conf"
fi
# The session that ran this keeps its inherited LANG; everything after it here
# should see the new one.
export LANG=en_US.UTF-8
echo "Locale set to en_US.UTF-8"
@@ -0,0 +1,10 @@
echo "Configure Thunderbolt device authorization"
install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.thunderbolt.policy" /usr/share/polkit-1/actions/org.omarchy.thunderbolt.policy
# The builder's accessories must not become the new owner's trust policy.
# Keep ordinary Bolt behavior until interactive owner setup has finished.
if [[ -n ${OMARCHY_INSTALL_USER:-} ]]; then
/usr/bin/omarchy-thunderbolt-authorization-admin prepare
systemctl enable omarchy-thunderbolt-authorization.service
fi
+26
View File
@@ -0,0 +1,26 @@
echo "Configure USB device authorization"
install -Dm644 "$OMARCHY_PATH/default/polkit/org.omarchy.usb.policy" /usr/share/polkit-1/actions/org.omarchy.usb.policy
# A deferred install has only the builder's peripherals attached. The owner
# needs working input before there is an account or a session for approvals.
if [[ -z ${OMARCHY_INSTALL_USER:-} ]]; then
systemctl disable usbguard.service
return 0
fi
rules_file="${OMARCHY_USB_AUTHORIZATION_RULES_FILE:-/etc/usbguard/rules.conf}"
daemon_config="${OMARCHY_USB_AUTHORIZATION_DAEMON_CONFIG:-/etc/usbguard/usbguard-daemon.conf}"
source "$OMARCHY_INSTALL/helpers/usb-authorization.sh"
usb_authorization_require_secure_settings "$daemon_config"
if [[ ! -s $rules_file ]]; then
usb_authorization_install_policy "$rules_file"
fi
usb_authorization_add_user "$OMARCHY_INSTALL_USER"
# Installs are followed by a reboot, so enable the daemon without starting it
# in the live ISO's chroot.
systemctl enable usbguard.service
+2
View File
@@ -45,3 +45,5 @@ run_logged "$OMARCHY_INSTALL/hardware/fix-yt6801-ethernet-adapter.sh"
run_logged "$OMARCHY_INSTALL/hardware/fix-tuxedo-backlight.sh"
run_logged "$OMARCHY_INSTALL/hardware/speaker-tuning.sh"
run_logged "$OMARCHY_INSTALL/hardware/pacman.sh"
run_logged "$OMARCHY_INSTALL/hardware/platform-setup.sh"
@@ -18,6 +18,14 @@
# machines including the T2-less iMac19,1 and iMac19,2, and BCM4377/4378/4387
# from the T2 era on. The BCM4360 in 2013-2015 Macs is deliberately absent: it
# runs the out-of-tree wl driver, which never reads a brcmfmac option.
#
# Apple Silicon is excluded even though its BCM4387 is in that list: there the
# firmware supplicant is what works, and disabling it makes firmware commands
# time out and breaks scanning entirely.
if omarchy-hw-aarch64-apple; then
return 0
fi
sys_vendor="$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || true)"
if lspci -nn | grep "106b:180[12]" >/dev/null ||
+4 -2
View File
@@ -1,5 +1,7 @@
# Detect MacBook models that need SPI keyboard modules
product_name="$(cat /sys/class/dmi/id/product_name 2>/dev/null)"
# Detect MacBook models that need SPI keyboard modules.
# Not every machine has a DMI product name, and under set -e an unguarded read
# of a missing one aborts the hardware pass.
product_name="$(cat /sys/class/dmi/id/product_name 2>/dev/null || true)"
if [[ $product_name =~ MacBook[89],1|MacBook1[02],1|MacBookPro13,[123]|MacBookPro14,[123] ]]; then
echo "Detected MacBook with SPI keyboard"
+10
View File
@@ -1,5 +1,15 @@
systemctl enable bluetooth.service
# An image's first boot runs this after the adapter has brought up (or queued)
# bluetooth.target, and a target never starts a unit enabled after its start
# job was made, so Bluetooth would stay off until the next boot. Only there
# (omarchy-provision-hardware sets the marker): an install or a rerun of
# hardware setup leaves the service as it finds it. Without Bluetooth support
# the unit's own condition skips it.
if [[ ${OMARCHY_IMAGE_DEFERRED_HARDWARE:-} == "1" ]]; then
systemctl start --no-block bluetooth.service || true
fi
# AutoEnable stays at its stock default on purpose. It was set to false here to
# persist the power state, which it never did: BlueZ has no such behaviour, so
# all it bought was Bluetooth coming up off on every boot. omarchy-bluetooth-power
+6 -4
View File
@@ -1,5 +1,7 @@
# Ensure that F-keys on Apple-like keyboards (such as Lofree Flow84) are always F-keys
if [[ ! -f /etc/modprobe.d/hid_apple.conf ]]; then
sudo mkdir -p /etc/modprobe.d
echo "options hid_apple fnmode=2" | sudo tee /etc/modprobe.d/hid_apple.conf >/dev/null
# Ensure that F-keys on Apple-like keyboards (such as Lofree Flow84) are always F-keys.
# Apple Silicon is left alone: the omarchy-mac package owns its keyboard mode.
conf="${OMARCHY_HID_APPLE_CONF:-/etc/modprobe.d/hid_apple.conf}"
if [[ ! -f $conf ]] && ! omarchy-hw-aarch64-apple; then
sudo mkdir -p "$(dirname "$conf")"
echo "options hid_apple fnmode=2" | sudo tee "$conf" >/dev/null
fi
+13
View File
@@ -0,0 +1,13 @@
# The platform packages' own setup, last so it builds on every other leaf
# (omarchy-lifecycle-dispatch setup-boot and setup-system, see
# docs/lifecycle-dispatch.md): the boot package's first, then the runtime
# package's. A no-op where the platform registers none. An image's first boot
# says so: there the network may be down, and a boot-file rebuild is asked for
# rather than built, since the deferred setup rebuilds once after its last step.
if [[ ${OMARCHY_IMAGE_DEFERRED_HARDWARE:-} == "1" ]]; then
omarchy-lifecycle-dispatch setup-boot image-first-boot
omarchy-lifecycle-dispatch setup-system image-first-boot
else
omarchy-lifecycle-dispatch setup-boot
omarchy-lifecycle-dispatch setup-system
fi
+164
View File
@@ -0,0 +1,164 @@
# Image builds and the hardware setup they defer to the machine's first boot.
# Sourced by omarchy-apply-hardware and omarchy-provision-hardware; no shebang.
#
# An image is built away from the machine it will run on, so hardware setup
# during the build would describe the builder, not the target. The builder
# declares the build in a root-owned manifest before it runs
# omarchy-apply-system in the image root:
#
# /var/lib/omarchy/image/target
# format=1
# platform=x86|aarch64|aarch64-apple
#
# While it exists, omarchy-apply-hardware queues each hardware leaf in
# /var/lib/omarchy/image/deferred-steps instead of running it, and arms
# omarchy-provision-hardware.service. On the machine's first boot that service,
# or the platform's own first boot, retires the manifest (to target.booted) and
# runs the queue on the real hardware. Unknown manifest keys are ignored so the
# format can grow.
#
# An image carries no pacman master key of its own, or every machine flashed
# from it would share one: where install finalization would make the keyring
# (install/post-install/pacman.sh), a build requests it in
# /var/lib/omarchy/image/pacman-keyring instead, and the first boot makes it
# before any deferred step runs.
#
# The manifest is also the only source of the image's platform: until that boot,
# omarchy-hw-platform reports its platform instead of the build host's, so the
# initramfs, services and packages the build sets up are the target's. It reads
# the manifest by the same rules as omarchy_image_read_manifest below.
#
# Root always uses the fixed paths. Only a non-root test may move them under
# OMARCHY_IMAGE_ROOT, so no environment variable can make a live system defer,
# skip or replay its hardware setup.
omarchy_image_init() {
if (( EUID == 0 )); then
omarchy_image_root=""
elif [[ -n ${OMARCHY_IMAGE_ROOT:-} ]]; then
omarchy_image_root=$OMARCHY_IMAGE_ROOT
else
echo "Error: image setup state is root-owned; run as root" >&2
return 1
fi
omarchy_image_dir=$omarchy_image_root/var/lib/omarchy/image
omarchy_image_manifest=$omarchy_image_dir/target
omarchy_image_queue=$omarchy_image_dir/deferred-steps
omarchy_image_initramfs_baseline=$omarchy_image_dir/initramfs-inputs
omarchy_image_boot_rebuild=$omarchy_image_dir/boot-rebuild
omarchy_image_keyring_request=$omarchy_image_dir/pacman-keyring
omarchy_image_unit=omarchy-provision-hardware.service
omarchy_image_systemd_dir=$omarchy_image_root/etc/systemd/system
}
# Owned by whoever runs this (root on a live system), not a symlink, and not
# writable by anyone else.
omarchy_image_trusted() {
local path=$1 owner mode
[[ -e $path && ! -L $path ]] || return 1
read -r owner mode < <(stat -c '%u %a' -- "$path") || return 1
(( owner == EUID && (8#$mode & 8#022) == 0 ))
}
omarchy_image_manifest_present() {
[[ -e $omarchy_image_manifest || -L $omarchy_image_manifest ]]
}
# Sets omarchy_image_platform, or fails without guessing whether this is a build.
omarchy_image_read_manifest() {
local line key value format="" platform=""
if [[ ! -d $omarchy_image_dir || ! -f $omarchy_image_manifest ]] || ! omarchy_image_trusted "$omarchy_image_dir" ||
! omarchy_image_trusted "$omarchy_image_manifest"; then
echo "Error: $omarchy_image_manifest is not a root-owned regular file" >&2
return 1
fi
while IFS= read -r line || [[ -n $line ]]; do
[[ -n $line && $line != \#* ]] || continue
if [[ $line != *=* ]]; then
echo "Error: $omarchy_image_manifest is malformed: $line" >&2
return 1
fi
key=${line%%=*}
value=${line#*=}
case $key in
format) format=$value ;;
platform) platform=$value ;;
esac
done <"$omarchy_image_manifest"
if [[ $format != "1" ]]; then
echo "Error: $omarchy_image_manifest is not format=1" >&2
return 1
fi
# Builders written before the platform names settled say apple-silicon,
# generic-aarch64 or generic (as omarchy-hw-platform reads them too).
case $platform in
apple-silicon) platform=aarch64-apple ;;
generic-aarch64) platform=aarch64 ;;
generic) platform=x86 ;;
esac
case $platform in
x86 | aarch64 | aarch64-apple) ;;
*)
echo "Error: $omarchy_image_manifest names no known platform: ${platform:-none}" >&2
return 1
;;
esac
omarchy_image_platform=$platform
}
# Holds the image state still for the rest of the caller, so hardware setup and
# a first-boot run never overlap or decide from a manifest the other retires.
# A machine that was never an image has no state and nothing to lock.
omarchy_image_lock() {
[[ -e $omarchy_image_dir || -L $omarchy_image_dir ]] || return 0
if [[ ! -d $omarchy_image_dir ]] || ! omarchy_image_trusted "$omarchy_image_dir"; then
echo "Error: $omarchy_image_dir is not a root-owned directory" >&2
return 1
fi
exec {omarchy_image_lock_fd}>>"$omarchy_image_dir/lock" || return 1
flock "$omarchy_image_lock_fd"
}
omarchy_image_write_queue() {
local tmp
tmp=$(mktemp "$omarchy_image_dir/.deferred-steps.XXXXXX") || return 1
if (( $# )); then
printf '%s\n' "$@" >"$tmp" || { rm -f "$tmp"; return 1; }
fi
chmod 0644 "$tmp" && mv -f "$tmp" "$omarchy_image_queue"
}
# Queue every leaf install/hardware/all.sh would run, in its order, running
# none of them, then arm the first-boot service. Rerunning rewrites the same queue.
omarchy_image_defer_hardware() {
local listing
local -a steps=()
listing=$(
run_logged() {
[[ $1 == "$OMARCHY_INSTALL"/hardware/*.sh && $1 != *..* ]] || {
echo "Error: cannot defer a step outside the hardware setup: $1" >&2
exit 1
}
printf 'install/%s\n' "${1#"$OMARCHY_INSTALL"/}"
}
source "$OMARCHY_INSTALL/hardware/all.sh"
) || return 1
[[ -z $listing ]] || mapfile -t steps <<<"$listing"
omarchy_image_write_queue "${steps[@]}" || return 1
install -d -m 0755 "$omarchy_image_systemd_dir/multi-user.target.wants" &&
install -m 0644 "$OMARCHY_INSTALL/provisioning/$omarchy_image_unit" "$omarchy_image_systemd_dir/$omarchy_image_unit" &&
ln -sfn "/etc/systemd/system/$omarchy_image_unit" "$omarchy_image_systemd_dir/multi-user.target.wants/$omarchy_image_unit" ||
return 1
echo "Image build for $omarchy_image_platform: deferred ${#steps[@]} hardware steps to first boot"
}
+32
View File
@@ -0,0 +1,32 @@
# Pacman repository templates. Every platform has a pacman.conf and mirrorlist
# for each channel it offers, copied into place whole, as x86's always were:
# x86's in default/pacman, each ARM platform's in a directory named for it.
# Sourcing this file only defines functions; it never changes the system.
# The directory holding <platform>'s pacman-<channel>.conf and
# mirrorlist-<channel>. A channel without both files isn't offered there.
omarchy_pacman_templates() {
case ${1:-} in
x86) echo "$OMARCHY_PATH/default/pacman" ;;
aarch64 | aarch64-apple) echo "$OMARCHY_PATH/default/pacman/$1" ;;
*)
echo "Error: Unknown platform '${1:-}'." >&2
return 1
;;
esac
}
# The channel a machine takes when none is named: stable, except on aarch64.
# Omarchy publishes aarch64 packages on edge alone so far, and the stable and rc
# packages there are the release line, which has no aarch64 support, so ARM
# platforms have edge templates only until a release does.
omarchy_pacman_default_channel() {
case ${1:-} in
x86) echo stable ;;
aarch64 | aarch64-apple) echo edge ;;
*)
echo "Error: Unknown platform '${1:-}'." >&2
return 1
;;
esac
}
+319
View File
@@ -0,0 +1,319 @@
# Shared by the root controller and session commands. These paths are fixed at
# privileged entrypoints; tests may source the functions in an isolated fixture.
TB_STATE=/var/lib/omarchy/thunderbolt-authorization/policy.json
TB_CONFIG=/var/lib/boltd/boltd.conf
TB_MARKER=/etc/omarchy/thunderbolt-authorization.enabled
TB_PENDING=/etc/omarchy/thunderbolt-authorization.pending
TB_RUNTIME=/run/omarchy-thunderbolt-authorization
TB_LOCK=/run/lock/omarchy-thunderbolt-authorization.lock
TB_SERVICE=omarchy-thunderbolt-authorization.service
TB_BOLT=org.freedesktop.bolt
TB_MANAGER=org.freedesktop.bolt1.Manager
TB_DEVICE=org.freedesktop.bolt1.Device
TB_DOMAIN=org.freedesktop.bolt1.Domain
TB_PATH=/org/freedesktop/bolt
TB_SYSFS=/sys/bus/thunderbolt/devices
TB_TRUST_JQ='def trusted($state): $state.trusted[.Uid] == {Uid,Name,Vendor} and
((.Secure | not) or (.Stored and (.Key | IN("have","new"))));'
tb_fail() { printf '%s\n' "$*" >&2; return 1; }
tb_json_write() {
local target=$1 mode=${2:-600} temporary
temporary=$(mktemp "${target%/*}/.thunderbolt.XXXXXX") || return 1
if jq -cS . > "$temporary" && chmod "$mode" "$temporary" && sync "$temporary" &&
mv -fT -- "$temporary" "$target" && sync "${target%/*}"; then
return 0
else
rm -f -- "$temporary"
return 1
fi
}
tb_state() {
jq -ce 'select(.version == 1 and (.trusted | type == "object") and (.enabled | type == "boolean"))' "$TB_STATE"
}
tb_bus() {
busctl --system --auto-start=no --timeout=10 --json=short "$@"
}
tb_owner() {
tb_bus call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus GetNameOwner s "$TB_BOLT" | jq -er '.data[0]'
}
tb_properties() {
tb_bus call "$1" "$2" org.freedesktop.DBus.Properties GetAll s "$3" | jq -ce '.data[0] | map_values(.data)'
}
tb_set() {
local owner=$1 path=$2 interface=$3 key=$4 value=$5 signature=${6:-s} current
local -a values=()
current=$(tb_properties "$owner" "$path" "$interface") || return 1
if ! jq -e --arg key "$key" --argjson value "$value" '.[$key] == $value' <<< "$current" >/dev/null; then
if [[ $signature == "as" ]]; then
mapfile -t values < <(jq -r '.[]' <<< "$value")
tb_bus set-property "$owner" "$path" "$interface" "$key" as "${#values[@]}" "${values[@]}" >/dev/null || return 1
else
tb_bus set-property "$owner" "$path" "$interface" "$key" s "$(jq -r . <<< "$value")" >/dev/null || return 1
fi
fi
# Bolt rejects no-op BootACL writes; even skipped writes are read back.
current=$(tb_properties "$owner" "$path" "$interface") || return 1
jq -e --arg key "$key" --argjson value "$value" '.[$key] == $value' <<< "$current" >/dev/null ||
tb_fail "Bolt did not retain the requested $key"
}
tb_inventory() {
local owner manager paths path item inode domains='[]' devices='[]' stored='[]'
owner=$(tb_owner) || return 1
manager=$(tb_properties "$owner" "$TB_PATH" "$TB_MANAGER") || return 1
paths=$(tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" ListDomains | jq -cer '.data[0]') || return 1
while IFS= read -r path; do
item=$(tb_properties "$owner" "$path" "$TB_DOMAIN" | jq -c --arg path "$path" '. + {path:$path}') || return 1
domains=$(jq -c --argjson item "$item" '. + [$item]' <<< "$domains") || return 1
done < <(jq -r '.[]' <<< "$paths")
paths=$(tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" ListDevices | jq -cer '.data[0]') || return 1
while IFS= read -r path; do
item=$(tb_properties "$owner" "$path" "$TB_DEVICE" | jq -c --arg path "$path" '. + {path:$path}') || return 1
[[ $(jq -r .Type <<< "$item") == "peripheral" ]] || continue
if jq -e .Stored <<< "$item" >/dev/null; then
stored=$(jq -c --argjson item "$item" '. + [$item]' <<< "$stored") || return 1
fi
[[ $(jq -r .Status <<< "$item") != "disconnected" ]] || continue
inode=$(stat -Lc '%i' -- "$(jq -r .SysfsPath <<< "$item")") || return 1
item=$(jq -c --arg inode "$inode" '. + {inode:$inode}' <<< "$item") || return 1
devices=$(jq -c --argjson item "$item" '. + [$item]' <<< "$devices") || return 1
done < <(jq -r '.[]' <<< "$paths")
[[ $(tb_owner) == "$owner" ]] || { tb_fail "Bolt restarted while reading its inventory"; return 1; }
jq -cn --arg owner "$owner" --argjson manager "$manager" --argjson domains "$domains" \
--argjson devices "$devices" --argjson stored "$stored" '
{owner:$owner,manager:$manager,domains:$domains,stored:$stored,devices:[$devices[] |
. + {Secure:((.Domain as $domain | any($domains[]; .Uid == $domain and .SecurityLevel == "secure"))
and (.AuthFlags | contains("nokey") | not))}]}'
}
tb_read_inventory() {
local inventory
inventory=$(tb_inventory) || return 1
jq -e '.manager.AuthMode == "disabled"' <<< "$inventory" >/dev/null || {
tb_fail "Bolt automatic authorization is enabled; protection is not active"
return 1
}
printf '%s\n' "$inventory"
}
tb_identity() {
jq -c --arg owner "$1" --arg generation "$2" \
'{owner:$owner,generation:$generation,path,inode,Uid,Name,Vendor,Parent,SysfsPath,ConnectTime}'
}
tb_snapshot() {
local state inventory generation recovery=false
state=$(tb_state) || return 1
inventory=$(tb_read_inventory) || return 1
generation=$(< "$TB_RUNTIME/generation") || return 1
[[ -f ${TB_STATE%/*}/boot-recovery.json ]] && recovery=true
jq -c --argjson state "$state" --arg generation "$generation" --argjson recovery "$recovery" \
--arg error "${1:-}" --argjson time "$(date +%s)" "$TB_TRUST_JQ"'
.owner as $owner |
[.devices[] | . as $d | {
identity: ({owner:$owner,generation:$generation} + ($d | {path,inode,Uid,Name,Vendor,Parent,SysfsPath,ConnectTime})),
trusted: ($d | trusted($state)),
status:.Status,stored:.Stored,policy:.Policy,flags:.AuthFlags
}] as $devices |
{available:true,time:$time,owner:$owner,generation:$generation,devices:$devices,error:$error,
boot_protection:($state.boot_protection // false),
domains:[.domains[] | {Uid,SecurityLevel,IOMMU,BootACL}],
warnings: ([
(if $recovery then "A firmware boot-access change needs recovery. Boot protection is not confirmed." else empty end),
(.domains[] | if .SecurityLevel == "none" then
"This controller allows PCIe connections in firmware. Select user authorization in firmware settings; Omarchy cannot block its devices before their drivers load."
else empty end),
(.domains[] | .SecurityLevel as $level | select(["none","user","secure","dponly","usbonly","nopcie"] | index($level) | not) |
"The controller authorization mode could not be verified."),
(.domains[] | select($state.boot_protection and (.SysfsPath == "" or
(.SecurityLevel != "user" and .SecurityLevel != "secure") or (.BootACL | length == 0) or any(.BootACL[]; . != ""))) |
"The firmware boot allowlist is not confirmed empty on every controller. Check Thunderbolt Boot before relying on boot protection."),
($devices[] | select((.status | startswith("authorized")) and (.trusted | not) and (.flags | contains("boot"))) |
"An untrusted device was authorized by firmware before Linux started. Disconnect it safely to apply approval on its next connection.")
] | unique)}' <<< "$inventory"
}
tb_current() {
local expected=$1 inventory owner generation device identity
inventory=$(tb_read_inventory) || return 1
owner=$(jq -r .owner <<< "$inventory")
generation=$(< "$TB_RUNTIME/generation") || return 1
while IFS= read -r device; do
identity=$(tb_identity "$owner" "$generation" <<< "$device") || return 1
if jq -e --argjson expected "$expected" '. == $expected' <<< "$identity" >/dev/null; then
jq -cn --arg owner "$owner" --argjson device "$device" '{owner:$owner,device:$device}'
return 0
fi
done < <(jq -c '.devices[]' <<< "$inventory")
tb_fail "That Thunderbolt device was removed or changed. Open its latest notification."
}
tb_prepare_key() (
local identity=$1 current key key_fd
current=$(tb_current "$identity") || return 1
# Bolt creates keys for stored devices itself. For a new device, disabled
# AuthMode prevents enrollment before authorization. Seed the kernel key so
# Authorize provisions it and the subsequent enrollment imports it into Bolt.
jq -e '.device.Secure and (.device.Stored | not)' <<< "$current" >/dev/null || return 0
cd -- "$(jq -r .device.SysfsPath <<< "$current")" || return 1
[[ $(stat -Lc '%i' .) == "$(jq -r .device.inode <<< "$current")" &&
$(cat unique_id) == "$(jq -r .device.Uid <<< "$current")" ]] || {
tb_fail 'The Thunderbolt connection changed before key setup'; return 1;
}
# Pin the attribute in the original directory; do not follow a replacement
# connection at the same path, or truncate an existing key on a retry.
exec {key_fd}<>key || return 1
key=$(cat key) || return 1
if [[ -z $key ]]; then
[[ $(cat authorized) == "0" ]] || { tb_fail 'Reconnect the device before saving secure approval'; return 1; }
key=$(openssl rand -hex 32) || return 1
[[ $key =~ ^[0-9a-f]{64}$ ]] || return 1
tb_current "$identity" >/dev/null || return 1
printf '%s' "$key" >&"$key_fd" || return 1
[[ $(cat key) == "$key" ]] || { tb_fail 'Could not verify the Thunderbolt key'; return 1; }
else
[[ $key =~ ^[0-9a-fA-F]{64}$ ]] || { tb_fail 'The Thunderbolt key is invalid'; return 1; }
fi
)
tb_approve() {
local identity=$1 permanent=$2 initialize=${3:-false} current owner path status state
state=$(tb_state) || return 1
jq -e .enabled <<< "$state" >/dev/null || { tb_fail "Thunderbolt protection is disabled"; return 1; }
current=$(tb_current "$identity") || return 1
owner=$(jq -r .owner <<< "$current")
path=$(jq -r .device.path <<< "$current")
status=$(jq -r .device.Status <<< "$current")
case "$status" in
connected|auth-error)
if [[ $permanent == "true" ]]; then
if [[ $initialize == "true" ]]; then
tb_prepare_key "$identity" || return 1
else
jq -e '(.device.Secure | not) or (.device.Stored and (.device.Key | IN("have","new")))' <<< "$current" >/dev/null || {
tb_fail 'A secure key requires explicit device approval'; return 1;
}
fi
fi
current=$(tb_current "$identity") || return 1
tb_bus call "$owner" "$path" "$TB_DEVICE" Authorize s '' >/dev/null || return 1 ;;
authorized|authorized-newkey|authorized-secure) ;;
*) tb_fail "Device authorization is still in progress; try again"; return 1 ;;
esac
current=$(tb_current "$identity") || return 1
jq -e '.device.Status | IN("authorized","authorized-newkey","authorized-secure")' <<< "$current" >/dev/null || {
tb_fail "Could not confirm device authorization; try again"; return 1;
}
if [[ $permanent == "true" ]]; then
if ! jq -e .device.Stored <<< "$current" >/dev/null; then
# Save enrollment intent before Bolt writes its record, so a lost reply
# can be retried without confusing our record with a pre-existing one.
state=$(jq --argjson device "$(jq -c .device <<< "$current")" \
'.enrolled[$device.Uid] = ($device | {Uid,Name,Vendor})' <<< "$state") || return 1
tb_json_write "$TB_STATE" <<< "$state" || return 1
tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" EnrollDevice sss "$(jq -r .device.Uid <<< "$current")" manual '' >/dev/null || return 1
fi
current=$(tb_current "$identity") || return 1
jq -e .device.Stored <<< "$current" >/dev/null || { tb_fail "Could not confirm saved device information; try again"; return 1; }
jq -e '(.device.Secure | not) or (.device.Key | IN("have","new"))' <<< "$current" >/dev/null || {
tb_fail 'Secure approval has no saved key. Safely disconnect, reconnect, and approve the device again.'; return 1;
}
jq --argjson device "$(jq -c .device <<< "$current")" '
if .enrolled[$device.Uid] == ($device | {Uid,Name,Vendor}) then
.enrolled[$device.Uid] = ($device | {Uid,Name,Vendor,StoreTime})
else . end |
.trusted[$device.Uid] = ($device | {Uid,Name,Vendor})' <<< "$state" |
tb_json_write "$TB_STATE" || return 1
fi
tb_snapshot
}
tb_boot_apply() {
local saved=$1 inventory owner item uid policies acls value path
inventory=$(tb_inventory) || return 1
owner=$(jq -r .owner <<< "$inventory")
jq -e --argjson saved "$saved" '
(($saved.policies | keys) - [.stored[].Uid] | length == 0) and
(($saved.acls | keys) - [.domains[] | select(.SysfsPath != "") | .Uid] | length == 0)' <<< "$inventory" >/dev/null || {
tb_fail "Reconnect the original controllers and restore missing Bolt records before recovery"; return 1;
}
while IFS= read -r item; do
uid=$(jq -r .Uid <<< "$item")
value=$(jq -c --arg uid "$uid" '.policies[$uid] // empty' <<< "$saved") || return 1
[[ -n $value ]] || continue
tb_set "$owner" "$(jq -r .path <<< "$item")" "$TB_DEVICE" Policy "$value" || return 1
done < <(jq -c '.stored[]' <<< "$inventory")
# Changing a device policy can update firmware ACLs, so restore ACLs last.
while IFS= read -r item; do
uid=$(jq -r .Uid <<< "$item")
value=$(jq -c --arg uid "$uid" '.acls[$uid] // empty' <<< "$saved") || return 1
[[ -n $value ]] || continue
[[ $(jq length <<< "$value") == "$(jq '.BootACL | length' <<< "$item")" ]] || {
tb_fail "Firmware boot allowlist capacity changed"; return 1;
}
tb_set "$owner" "$(jq -r .path <<< "$item")" "$TB_DOMAIN" BootACL "$value" as || return 1
done < <(jq -c '.domains[]' <<< "$inventory")
}
tb_reconcile() {
local inventory state owner generation device identity error='' saved
state=$(tb_state) || return 1
inventory=$(tb_read_inventory) || return 1
owner=$(jq -r .owner <<< "$inventory")
generation=$(< "$TB_RUNTIME/generation") || return 1
if jq -e .boot_protection <<< "$state" >/dev/null; then
saved=$(jq -c '{policies:(.stored | map({key:.Uid,value:"manual"}) | from_entries),
acls:(.domains | map(select(.SysfsPath != "" and (.SecurityLevel == "user" or .SecurityLevel == "secure") and (.BootACL|length>0))) |
map({key:.Uid,value:(.BootACL | map(""))}) | from_entries)}' <<< "$inventory") || return 1
tb_boot_apply "$saved" || return 1
fi
while IFS= read -r device; do
identity=$(tb_identity "$owner" "$generation" <<< "$device") || return 1
if ! tb_approve "$identity" true >/dev/null; then
error='A trusted Thunderbolt device could not be restored. Check the system service log.'
fi
done < <(jq -c --argjson state "$state" "$TB_TRUST_JQ"'.devices | sort_by(.SysfsPath | length)[] |
select((.Status == "connected" or .Status == "auth-error") and (.AuthFlags | contains("nopcie") | not)) |
# Never silently initialize a missing secure binding on a later connection.
select(trusted($state))' <<< "$inventory")
tb_snapshot "$error"
}
tb_daemon() {
local snapshot error
install -d -m 755 "$TB_RUNTIME"
cat /proc/sys/kernel/random/uuid > "$TB_RUNTIME/generation"
chmod 644 "$TB_RUNTIME/generation"
while [[ -f $TB_MARKER ]]; do
(
flock -x 9
if snapshot=$(tb_reconcile 2> "$TB_RUNTIME/error"); then
tb_json_write "$TB_RUNTIME/snapshot.json" 644 <<< "$snapshot"
else
cat "$TB_RUNTIME/error" >&2
error=$(< "$TB_RUNTIME/error")
jq -cn --arg error "$error" --argjson time "$(date +%s)" '{available:false,time:$time,error:$error}' |
tb_json_write "$TB_RUNTIME/snapshot.json" 644
fi
) 9> "$TB_LOCK"
sleep 2
done
}
tb_public_snapshot() {
local snapshot generation
snapshot=$(cat "$TB_RUNTIME/snapshot.json") || return 1
generation=$(< "$TB_RUNTIME/generation") || return 1
jq -e --argjson now "$(date +%s)" --arg generation "$generation" \
'.available and .generation == $generation and .time <= $now and $now - .time < 20' <<< "$snapshot" >/dev/null || {
tb_fail 'Thunderbolt policy is unavailable or could not be verified.'; return 1;
}
printf '%s\n' "$snapshot"
}
+254
View File
@@ -0,0 +1,254 @@
# 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
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
sleep 2
continue
fi
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=''
fi
else
failures=$((failures + 1))
if (( failures >= 3 )) && [[ $warned != "unavailable" ]] && tb_attention; then
warned=unavailable
fi
fi
sleep 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'
}
+398
View File
@@ -0,0 +1,398 @@
# Root setup functions. The caller sources thunderbolt-policy.sh first.
tb_config_authmode() {
local path=$1
if [[ -f $path ]]; then
awk '
/^[[:space:]]*\[/ { section = ($0 ~ /^[[:space:]]*\[config\][[:space:]]*$/) }
section && /^[[:space:]]*AuthMode[[:space:]]*=/ {
sub(/^[^=]*=[[:space:]]*/, ""); sub(/[[:space:]]*$/, ""); mode=$0
}
END { print mode == "" ? "enabled" : mode }
' "$path"
else
printf 'enabled\n'
fi
}
tb_config_write() {
local path=$1 mode=$2 temporary
[[ $mode == "enabled" || $mode == "disabled" ]] || return 1
install -d -m 700 "${path%/*}" || return 1
temporary=$(mktemp "${path%/*}/.config.XXXXXX") || return 1
if awk -v mode="$mode" '
/^[[:space:]]*\[/ {
section = ($0 ~ /^[[:space:]]*\[config\][[:space:]]*$/)
if (section) { found=1; print; print "AuthMode = " mode; next }
}
section && /^[[:space:]]*AuthMode[[:space:]]*=/ { next }
{ print }
END { if (!found) print "\n[config]\nAuthMode = " mode }
' "$(if [[ -e $path ]]; then printf '%s' "$path"; else printf /dev/null; fi)" > "$temporary" &&
sync "$temporary" && mv -fT -- "$temporary" "$path" && sync "${path%/*}"; then
return 0
else
rm -f -- "$temporary"
return 1
fi
}
tb_guard() {
if [[ -f $TB_MARKER ]]; then
tb_state | jq -e .enabled >/dev/null || { tb_fail 'Thunderbolt policy and enable marker disagree'; return 1; }
tb_config_write "$TB_CONFIG" disabled
fi
}
tb_capture() {
local path uid name vendor trusted='{}'
for path in "$TB_SYSFS"/*; do
[[ -f $path/unique_id && $path != *-0 ]] || continue
uid=$(cat "$path/unique_id") || return 1
# Bolt uses the numeric attribute when a descriptive name is absent.
if [[ -e $path/device_name ]]; then
name=$(cat "$path/device_name") || return 1
else
name=$(cat "$path/device") || return 1
fi
if [[ -e $path/vendor_name ]]; then
vendor=$(cat "$path/vendor_name") || return 1
else
vendor=$(cat "$path/vendor") || return 1
fi
[[ -n $uid ]] || { tb_fail 'A connected Thunderbolt device has no identity'; return 1; }
trusted=$(jq -c --arg uid "$uid" --arg name "$name" --arg vendor "$vendor" \
'.[$uid] = {Uid:$uid,Name:$name,Vendor:$vendor}' <<< "$trusted") || return 1
done
printf '%s\n' "$trusted"
}
tb_capture_ready() {
local fresh=${1:-false} offline=${2:-false} path uid inode inventory=''
# Do not relax an active policy, or capture new devices when re-enabling an
# existing policy. This preflight only guards the initial trust inventory.
[[ ! -f $TB_MARKER ]] || return 0
if [[ -f $TB_STATE && ! -f $TB_PENDING && $fresh != "true" ]]; then
tb_state | jq -e '.enrollment_pending // false' >/dev/null || return 0
fi
for path in "$TB_SYSFS"/*; do
[[ -f $path/unique_id && $path != *-0 && -e $path/key ]] || continue
uid=$(cat "$path/unique_id") || return 1
[[ $uid =~ ^[0-9a-fA-F-]{36}$ ]] || { tb_fail 'A connected Thunderbolt device has an invalid identity'; return 1; }
path=$(realpath -e -- "$path") || return 1
inode=$(stat -Lc '%i' -- "$path") || return 1
# A key file alone can be orphaned after a failed Bolt record write. Ask
# Bolt whether this exact connection has a stored record and key. Offline
# prepare must not accept the live installer's daemon as the target store.
if [[ $offline != "true" && -z $inventory ]]; then
inventory=$(tb_inventory) || inventory='{}'
fi
if [[ $offline == "true" ]] || ! jq -e --arg uid "$uid" --arg path "$path" --arg inode "$inode" \
'any(.devices[]?; .Uid==$uid and .SysfsPath==$path and .inode==$inode and .Stored and (.Key | IN("have","new")))' <<< "$inventory" >/dev/null; then
# An active keyless dock cannot receive a key without disconnecting it.
# Preserve input/storage and finish installation with setup deferred.
install -d -m 755 "${TB_PENDING%/*}" || return 1
install -m 644 /dev/null "$TB_PENDING" || return 1
printf 'Thunderbolt protection is deferred: safely disconnect accessories without saved secure keys, keep independent input available, then run Setup > Security > Thunderbolt.\n' >&2
return 2
fi
done
}
tb_prepare() {
local fresh=${1:-false} state='{}' trusted mode
if [[ -f $TB_STATE ]]; then
state=$(tb_state) || return 1
fi
if [[ ! -f $TB_STATE || -f $TB_PENDING || $fresh == "true" ]] || jq -e '.enrollment_pending // false' <<< "$state" >/dev/null; then
trusted=$(tb_capture) || return 1
mode=$(jq -r '.original_authmode // empty' <<< "$state") || return 1
[[ -n $mode ]] || mode=$(tb_config_authmode "$TB_CONFIG") || return 1
[[ $mode == "enabled" || $mode == "disabled" ]] || { tb_fail 'Unsupported existing Bolt authorization mode'; return 1; }
state=$(jq -c --arg mode "$mode" --argjson trusted "$trusted" \
'del(.enrollment_pending) | . + {version:1,enabled:true,trusted:$trusted,original_authmode:$mode}' <<< "$state") || return 1
else
mode=$(jq -r .original_authmode <<< "$state") || return 1
[[ $mode == "enabled" || $mode == "disabled" ]] || { tb_fail 'Unsupported existing Bolt authorization mode'; return 1; }
state=$(jq -c '.enabled=true' <<< "$state") || return 1
fi
install -d -m 700 "${TB_STATE%/*}" || return 1
tb_json_write "$TB_STATE" <<< "$state" || return 1
tb_config_write "$TB_CONFIG" disabled || return 1
install -d -m 755 "${TB_MARKER%/*}" || return 1
install -m 644 /dev/null "$TB_MARKER"
}
tb_boot_saved() {
jq -c '{policies:(.stored | map({key:.Uid,value:.Policy}) | from_entries),
acls:(.domains | map({key:.Uid,value:.BootACL}) | from_entries)}'
}
tb_boot_change() {
local enabled=$1 state inventory saved after desired backup="${TB_STATE%/*}/boot-recovery.json"
state=$(tb_state) || return 1
jq -e .enabled <<< "$state" >/dev/null || { tb_fail 'Enable Thunderbolt device authorization first'; return 1; }
if [[ $enabled == "false" ]] && ! jq -e .boot_protection <<< "$state" >/dev/null; then
return 0
fi
[[ ! -e $backup ]] || { tb_fail 'Recover the previous firmware boot-access change before retrying'; return 1; }
inventory=$(tb_read_inventory) || return 1
jq -e '.domains | length > 0 and all(.[]; .SysfsPath != "")' <<< "$inventory" >/dev/null || {
tb_fail 'Connect every Thunderbolt controller before changing firmware boot access'; return 1;
}
saved=$(tb_boot_saved <<< "$inventory" | jq -c --argjson state "$state" '. + {state:$state}') || return 1
if [[ $enabled == "true" ]]; then
jq -e 'all(.domains[]; (.SecurityLevel == "user" or .SecurityLevel == "secure") and (.BootACL|length > 0))' <<< "$inventory" >/dev/null || {
tb_fail 'This controller has no supported firmware boot allowlist. Disable Thunderbolt pre-boot/PCIe boot support in firmware settings; Omarchy cannot verify that setting.'; return 1;
}
desired=$(jq -c '{policies:(.stored | map({key:.Uid,value:"manual"}) | from_entries),
acls:(.domains | map({key:.Uid,value:(.BootACL | map(""))}) | from_entries)}' <<< "$inventory") || return 1
after=$(jq -c --argjson saved "$saved" --argjson devices "$(jq -c .devices <<< "$inventory")" '
if .boot_protection then . else .boot_original=($saved | {policies,acls}) end |
.boot_protection=true | .trusted += ($devices | map({key:.Uid,value:{Uid,Name,Vendor}}) | from_entries)' <<< "$state") || return 1
else
desired=$(jq -ce .boot_original <<< "$state") || return 1
after=$(jq -c 'del(.boot_original) | .boot_protection=false' <<< "$state") || return 1
fi
tb_json_write "$backup" <<< "$saved" || return 1
if tb_boot_apply "$desired" && tb_json_write "$TB_STATE" <<< "$after"; then
rm -- "$backup"
else
if tb_boot_apply "$saved" && tb_json_write "$TB_STATE" <<< "$state"; then
rm -- "$backup" || return 1
tb_fail 'Firmware change failed; previous state restored'
else
tb_fail "Firmware change failed; recovery is incomplete. Recovery data retained at $backup"
fi
return 1
fi
}
tb_boot_recover() {
local backup="${TB_STATE%/*}/boot-recovery.json" saved
saved=$(jq -ce . "$backup") || return 1
tb_boot_apply "$saved" || return 1
jq .state <<< "$saved" | tb_json_write "$TB_STATE" || return 1
rm -- "$backup"
}
tb_unit_state() {
local properties
properties=$(systemctl show -p ActiveState -p UnitFileState "$1") || return 1
jq -Rn '[inputs | split("=") | {key:.[0],value:.[1]}] | from_entries |
{active:(.ActiveState == "active"),enabled:(.UnitFileState == "enabled")}' <<< "$properties"
}
tb_handoff_policies() {
local state=$1
# StoreTime distinguishes a record recreated outside Omarchy. A completed
# enrollment and matching consent are both required, including offline
# accessories. Never promote somebody else's manual policy.
jq -c --argjson state "$state" '{policies:([.stored[] |
select(.Policy == "manual" and .StoreTime > 0 and
$state.enrolled[.Uid] == {Uid,Name,Vendor,StoreTime} and
$state.trusted[.Uid] == {Uid,Name,Vendor}) |
{key:.Uid,value:"auto"}] | from_entries),acls:{}}'
}
tb_config_checkpoint() {
local backup="${TB_STATE%/*}/setup-recovery.json" path original files='{}' units='{}' unit value saved inventory data mode state
[[ ! -e $backup && ! -e ${TB_STATE%/*}/boot-recovery.json ]] || {
tb_fail 'A previous setup change needs recovery before continuing'; return 1;
}
for path in "$TB_STATE" "$TB_CONFIG" "$TB_MARKER"; do
original=null
if [[ -e $path ]]; then
data=$(base64 -w0 -- "$path") || return 1
mode=$(stat -c '%a' -- "$path") || return 1
original=$(jq -cn --arg data "$data" --arg mode "$mode" '{data:$data,mode:$mode}') || return 1
fi
files=$(jq -c --arg path "$path" --argjson original "$original" '.[$path]=$original' <<< "$files") || return 1
done
for unit in bolt.service "$TB_SERVICE"; do
value=$(tb_unit_state "$unit") || return 1
units=$(jq -c --arg unit "$unit" --argjson value "$value" '.[$unit]=$value' <<< "$units") || return 1
done
saved=$(jq -cn --argjson files "$files" --argjson units "$units" '{files:$files,units:$units}') || return 1
state='{}'
if [[ -f $TB_STATE ]]; then state=$(tb_state) || return 1; fi
# A handoff to auto also changes Bolt's firmware allowlists. Checkpoint the
# policies and ACLs even when the optional boot protection was never used.
if jq -e '.enabled and (.boot_protection or ((.enrolled // {}) | length > 0))' <<< "$state" >/dev/null; then
inventory=$(tb_read_inventory) || return 1
jq -e 'all(.domains[]; .SysfsPath != "")' <<< "$inventory" >/dev/null || {
tb_fail 'Reconnect every Thunderbolt controller before changing device policies'; return 1;
}
saved=$(jq -c --argjson firmware "$(tb_boot_saved <<< "$inventory")" '.firmware=$firmware' <<< "$saved") || return 1
fi
install -d -m 700 "${TB_STATE%/*}" || return 1
tb_json_write "$backup" <<< "$saved"
}
tb_config_restore() {
local backup="${TB_STATE%/*}/setup-recovery.json" saved path original temporary unit
saved=$(jq -ce . "$backup") || return 1
systemctl stop "$TB_SERVICE" bolt.service || return 1
for path in "$TB_STATE" "$TB_CONFIG" "$TB_MARKER"; do
original=$(jq -c --arg path "$path" '.files[$path]' <<< "$saved") || return 1
if [[ $original == "null" ]]; then
rm -f -- "$path" || return 1
else
mkdir -p -- "${path%/*}" || return 1
temporary=$(mktemp "${path%/*}/.restore.XXXXXX") || return 1
if jq -r .data <<< "$original" | base64 -d > "$temporary" &&
chmod "$(jq -r .mode <<< "$original")" "$temporary" && sync "$temporary" &&
mv -fT -- "$temporary" "$path" && sync "${path%/*}"; then
:
else
rm -f -- "$temporary"
return 1
fi
fi
done
if jq -e .firmware <<< "$saved" >/dev/null; then
systemctl start bolt.service || return 1
tb_boot_apply "$(jq -c .firmware <<< "$saved")" || return 1
fi
rm -f -- "${TB_STATE%/*}/boot-recovery.json" || return 1
if jq -e --arg unit "$TB_SERVICE" '.units[$unit].enabled' <<< "$saved" >/dev/null; then
systemctl enable "$TB_SERVICE" || return 1
else
systemctl disable "$TB_SERVICE" || return 1
fi
for unit in bolt.service "$TB_SERVICE"; do
if jq -e --arg unit "$unit" '.units[$unit].active' <<< "$saved" >/dev/null; then
systemctl start "$unit" || return 1
else
systemctl stop "$unit" || return 1
fi
done
rm -- "$backup"
}
tb_transaction() {
tb_config_checkpoint || return 1
if "$@"; then
rm -- "${TB_STATE%/*}/setup-recovery.json"
else
if tb_config_restore; then
tb_fail 'Thunderbolt setup failed; previous configuration restored'
else
tb_fail 'Thunderbolt setup failed; restoration is incomplete. Run the setup recovery command.'
fi
return 1
fi
}
tb_enable() {
systemctl stop "$TB_SERVICE" bolt.service || return 1
tb_prepare "${1:-false}" || return 1
systemctl daemon-reload || return 1
systemctl start bolt.service || return 1
tb_read_inventory >/dev/null || return 1
systemctl enable --now "$TB_SERVICE"
}
tb_disable() {
local state mode policies
systemctl stop "$TB_SERVICE" || return 1
tb_boot_change false || return 1
state=$(tb_state) || return 1
mode=$(jq -er .original_authmode <<< "$state") || return 1
policies=$(tb_read_inventory | tb_handoff_policies "$state") || return 1
tb_boot_apply "$policies" || return 1
systemctl stop bolt.service || return 1
tb_config_write "$TB_CONFIG" "$mode" || return 1
rm -f -- "$TB_MARKER" || return 1
jq '.enabled=false | del(.enrolled)' <<< "$state" | tb_json_write "$TB_STATE" || return 1
systemctl disable "$TB_SERVICE" || return 1
systemctl start bolt.service || return 1
tb_inventory | jq -e --arg mode "$mode" --argjson policies "$policies" '
.manager.AuthMode==$mode and
(([.stored[] | {key:.Uid,value:.Policy}] | from_entries) as $actual |
all($policies.policies | to_entries[]; $actual[.key]==.value))' >/dev/null
}
tb_cancel_pending() {
local state
if [[ -f $TB_STATE ]]; then
state=$(tb_state) || return 1
else
state=$(jq -cn --arg mode "$(tb_config_authmode "$TB_CONFIG")" \
'{version:1,enabled:false,trusted:{},enrollment_pending:true,original_authmode:$mode}') || return 1
fi
install -d -m 700 "${TB_STATE%/*}" || return 1
jq '.enabled=false | .enrollment_pending=true' <<< "$state" | tb_json_write "$TB_STATE" || return 1
rm -- "$TB_PENDING"
}
tb_reset_root() {
local root directory
root=$(realpath -e -- "$1") || return 1
[[ $root != "/" && -d $root ]] || { tb_fail 'Refusing to reset the running system'; return 1; }
[[ -e $root$TB_MARKER || -e $root$TB_PENDING || -d $root${TB_STATE%/*} ]] || return 0
# Resolve fixed paths inside the cloned root, never through an outside link.
for directory in etc etc/omarchy var var/lib var/lib/omarchy var/lib/boltd; do
[[ ! -L $root/$directory ]] || { tb_fail 'Refusing a linked factory policy directory'; return 1; }
done
systemctl --root="$root" disable "$TB_SERVICE" || return 1
rm -f -- "$root$TB_MARKER" || return 1
rm -f -- "$root$TB_PENDING" || return 1
rm -rf -- "$root${TB_STATE%/*}" || return 1
for directory in devices keys domains; do
rm -rf -- "$root/var/lib/boltd/$directory" || return 1
done
tb_config_write "$root/var/lib/boltd/boltd.conf" enabled
}
tb_admin() {
local action=${1:-} status=0 state
if [[ $action != "guard" ]]; then
exec 9> "$TB_LOCK"
flock -x 9 || return 1
fi
case "$action" in
prepare|enable|owner|migrate)
# A deferred setup needs explicit consent after the accessories have
# been disconnected; a later migration must not silently finish it.
if [[ -f $TB_PENDING && $action != "enable" ]]; then return 0; fi
if [[ $action == "migrate" && -f $TB_STATE ]] && tb_state | jq -e '.enabled == false' >/dev/null; then return 0; fi
if tb_capture_ready "$(if [[ $action == "owner" ]]; then echo true; else echo false; fi)" \
"$(if [[ $action == "prepare" ]]; then echo true; else echo false; fi)"; then
:
else
status=$?
if (( status == 2 )); then return 0; else return "$status"; fi
fi ;;
esac
case "$action" in
guard) tb_guard ;;
prepare) tb_prepare && rm -f -- "$TB_PENDING" ;;
enable) tb_transaction tb_enable && rm -f -- "$TB_PENDING" ;;
owner) tb_transaction tb_enable true && rm -f -- "$TB_PENDING" ;;
migrate)
state='{"enabled":true}'
if [[ -f $TB_STATE ]]; then state=$(tb_state) || return 1; fi
if jq -e .enabled <<< "$state" >/dev/null; then
tb_transaction tb_enable
fi ;;
disable)
if [[ -f $TB_PENDING && ! -f $TB_MARKER ]]; then
tb_cancel_pending
else
tb_transaction tb_disable
fi ;;
recover) tb_config_restore ;;
boot-enable|boot-disable|boot-recover)
systemctl stop "$TB_SERVICE" || return 1
case "$action" in
boot-enable) tb_boot_change true || status=$? ;;
boot-disable) tb_boot_change false || status=$? ;;
boot-recover) tb_boot_recover || status=$? ;;
esac
systemctl start "$TB_SERVICE" || return 1
return "$status" ;;
reset-root) [[ $# == 2 ]] && tb_reset_root "$2" ;;
*) tb_fail 'Unknown Thunderbolt setup action' ;;
esac
}
@@ -0,0 +1,95 @@
# Device identity is portable; the full live rule is still used when granting
# access to the connection shown in the approval dialog.
usb_authorization_portable_rule() {
local rule="$1" token="" char quoted=0 escaped=0 index
local -a tokens=() portable=()
# Split canonical USBGuard output without evaluating device-controlled text.
# An attribute name inside a quoted name/serial is never a rule attribute.
for (( index=0; index<${#rule}; index++ )); do
char=${rule:index:1}
if (( escaped )); then
token+="$char"
escaped=0
elif (( quoted )) && [[ $char == '\' ]]; then
token+="$char"
escaped=1
elif [[ $char == '"' ]]; then
token+="$char"
quoted=$((1 - quoted))
elif (( ! quoted )) && [[ $char == " " ]]; then
if [[ -n $token ]]; then tokens+=("$token"); token=""; fi
else
token+="$char"
fi
done
(( ! quoted && ! escaped )) || return 1
if [[ -n $token ]]; then tokens+=("$token"); fi
[[ ${tokens[0]:-} == "allow" || ${tokens[0]:-} == "block" ]] || return 1
portable=(allow)
for (( index=1; index<${#tokens[@]}; index++ )); do
case "${tokens[index]}" in
via-port | parent-hash)
(( ++index < ${#tokens[@]} )) || return 1
[[ ${tokens[index]} == \"*\" ]] || return 1
;;
label) return 1 ;;
*) portable+=("${tokens[index]}") ;;
esac
done
local IFS=" "
printf '%s label "omarchy-usb-authorization-v1"\n' "${portable[*]}"
}
usb_authorization_permanent() {
local id="$1" expected="$2" devices current="" allowed portable policy line target new_id matches rule saved=0
[[ $id =~ ^[0-9]+$ && ( $expected == block\ * || $expected == allow\ * ) ]] || return 1
(( ${#expected} <= 16384 )) || return 1
devices=$(usbguard list-devices) || return 1
while IFS= read -r line; do
if [[ $line == "$id: "* ]]; then current=${line#"$id: "}; fi
done <<<"$devices"
[[ $current == "$expected" ]] || return 1
allowed="allow ${expected#* }"
portable=$(usb_authorization_portable_rule "$current") || return 1
usbguard-rule-parser "$portable" >/dev/null || return 1
# Respect earlier administrator deny rules. Do not report permanent success
# when a saved allow would lose to an explicit block/reject on reconnect.
policy=$(usbguard list-rules) || return 1
while IFS= read -r line; do
if [[ $line =~ ^[0-9]+:[[:space:]](allow|block|reject)([[:space:]]|$) ]]; then
target=${BASH_REMATCH[1]}
rule=${line#*: }
# --show-devices filters by current target, hiding deny rules for a
# temporarily allowed device. A match query compares identity instead.
matches=$(usbguard list-devices "match${rule#"$target"}") || return 1
if grep -Fqx -- "$id: $current" <<<"$matches"; then
if [[ $target != "allow" ]]; then
echo "An administrator rule blocks this device. Review that rule before saving approval." >&2
return 1
fi
break
fi
fi
done <<<"$policy"
while IFS= read -r line; do
if [[ $line =~ ^([0-9]+):[[:space:]] && ${line#*: } == "$portable" ]]; then
saved=1
fi
done <<<"$policy"
# A retry can reuse only an exact owned rule present in both the daemon and
# its persistent file. Never remove/reorder rules around manual restrictions.
if (( ! saved )) || ! grep -Fqx -- "$portable" /etc/usbguard/rules.conf; then
new_id=$(usbguard append-rule "$portable") || return 1
[[ $new_id =~ ^[0-9]+$ ]] || return 1
grep -Fqx -- "$portable" /etc/usbguard/rules.conf || return 1
fi
if [[ $current == block\ * ]]; then
usbguard allow-device "$expected" || return 1
fi
devices=$(usbguard list-devices --allowed) || return 1
grep -Fqx -- "$1: $allowed" <<<"$devices"
}
+146
View File
@@ -0,0 +1,146 @@
source "${BASH_SOURCE[0]%/*}/usb-authorization-policy.sh"
usb_authorization_config_value() {
local config="$1" key="$2"
local awk_command=(awk)
if [[ ! -r $config ]]; then
if [[ $config == "/etc/usbguard/usbguard-daemon.conf" ]]; then
awk_command=(sudo awk)
else
echo "Cannot read USBGuard configuration: $config" >&2
return 1
fi
fi
"${awk_command[@]}" -F= -v key="$key" '
/^[[:space:]]*#/ { next }
$1 ~ "^[[:space:]]*" key "[[:space:]]*$" {
value=$0
sub(/^[^=]*=[[:space:]]*/, "", value)
sub(/[[:space:]]*$/, "", value)
print value
exit
}
' "$config"
}
usb_authorization_rules_present() {
local rules="$1"
if [[ -r $rules ]]; then
[[ -s $rules ]]
elif [[ $rules == "/etc/usbguard/rules.conf" ]]; then
sudo test -s "$rules"
else
return 1
fi
}
usb_authorization_require_secure_setting() {
local config="$1" key="$2" expected="$3" actual
actual=$(usb_authorization_config_value "$config" "$key")
if [[ $actual != "$expected" ]]; then
echo "USBGuard's $key must be '$expected' before Omarchy enables it." >&2
echo "Current value: ${actual:-<unset>} in $config" >&2
return 1
fi
}
usb_authorization_require_secure_settings() {
local config="$1"
usb_authorization_require_secure_setting "$config" ImplicitPolicyTarget block || return 1
usb_authorization_require_secure_setting "$config" PresentDevicePolicy apply-policy || return 1
usb_authorization_require_secure_setting "$config" InsertedDevicePolicy apply-policy || return 1
usb_authorization_require_secure_setting "$config" AuthorizedDefault none || return 1
usb_authorization_require_secure_setting "$config" RestoreControllerDeviceState false || return 1
usb_authorization_require_secure_setting "$config" RuleFile /etc/usbguard/rules.conf || return 1
}
usb_authorization_generate_policy() {
local policy="$1" generated line
if ! generated=$(usbguard generate-policy); then
echo "USBGuard could not enumerate devices; nothing was enabled." >&2
return 1
fi
: >"$policy"
while IFS= read -r line; do
if [[ $line == allow\ * ]]; then
usb_authorization_portable_rule "$line" >>"$policy" || return 1
elif [[ -n $line ]]; then
printf '%s\n' "$line" >>"$policy"
fi
done <<<"$generated"
if ! grep -q '^allow ' "$policy"; then
if grep -q '^[[:space:]]*[^#[:space:]]' "$policy"; then
echo "USBGuard did not generate a usable policy; nothing was enabled." >&2
return 1
fi
# A successful empty inventory is valid on machines without USB controllers.
# Keep the file nonempty so re-enabling preserves this default-deny policy.
echo '# No USB devices were present during enrollment.' >>"$policy"
fi
usbguard-rule-parser -f "$policy" >/dev/null
}
usb_authorization_add_user() {
local user="$1"
# This user can inspect USBGuard events and approve the device named by an
# event. Policy and daemon parameters remain unavailable through the IPC ACL.
usbguard add-user "$user" \
--devices=list,listen,modify \
--policy=list \
--exceptions=listen
}
usb_authorization_install_policy() {
local rules="$1" policy status=0
policy=$(mktemp "${TMPDIR:-/tmp}/omarchy-usb-policy.XXXXXXXXXX") || return 1
if usb_authorization_generate_policy "$policy"; then
install -Dm600 -o root -g root "$policy" "$rules" || status=$?
else
status=1
fi
rm -f "$policy"
return "$status"
}
usb_authorization_provision_owner() {
local user="$1"
local rules="${2:-/etc/usbguard/rules.conf}"
local config="${3:-/etc/usbguard/usbguard-daemon.conf}"
usb_authorization_require_secure_settings "$config" || return 1
# Enroll the owner's hardware, replacing any policy inherited from the
# builder. Do this only after the interactive provisioning steps finish.
usb_authorization_install_policy "$rules" || return 1
usb_authorization_add_user "$user" || return 1
systemctl enable usbguard.service || return 1
systemctl restart usbguard.service
}
usb_authorization_trust_present_devices() {
local line id devices
local count=0
devices=$(usbguard list-devices) || return 1
local -a inventory
mapfile -t inventory <<<"$devices"
for line in "${inventory[@]}"; do
if [[ $line =~ ^([0-9]+):[[:space:]] ]]; then
id="${BASH_REMATCH[1]}"
sudo /usr/bin/omarchy-usb-authorization-approve "$id" "${line#*: }" || return 1
(( ++count ))
fi
done
if (( count == 0 )); then
echo "USBGuard did not report any connected USB devices; boot authorization was not enabled." >&2
return 1
fi
}
+26
View File
@@ -0,0 +1,26 @@
# Apple Silicon additions, composed after omarchy-base.packages and
# omarchy-aarch64.packages (omarchy-pkg-defaults aarch64-apple); never on Intel
# Macs or other aarch64 machines. The kernel and boot chain come with the
# Mac's image, as the kernel and bootloader do from the ISO on x86_64. What a
# Mac cannot work without (its speaker stack, audio server and GPU driver)
# comes as omarchy-mac's dependencies, so a new Mac driver needs no change
# here; the rest are defaults an owner may remove.
# The Mac's runtime and boot packages (omacom/omarchy-mac-pkgs).
omarchy-mac
omarchy-mac-boot
# Picks the startup volume (macOS or Omarchy) from Linux.
asahi-bless
# Hardware video decode.
avd-fw
libva-v4l2_request-avd
# Widevine for DRM streaming.
widevine
# Screen recording: gpu-screen-recorder finds no display device to capture on
# Apple Silicon, whose display and GPU are separate DRM devices, so recording
# falls back to wf-recorder (omarchy-capture-screenrecording).
wf-recorder
+4
View File
@@ -0,0 +1,4 @@
# Additions for every aarch64 platform, composed after omarchy-base.packages
# (omarchy-pkg-defaults). omarchy-settings configures swap on zram, and the
# x86_64 installer provides zram-generator for it; aarch64 gets it here.
zram-generator
+2
View File
@@ -47,6 +47,7 @@ fontconfig
foot
fzf
git
gliff
gnome-keyring
gnome-themes-extra
grim
@@ -147,6 +148,7 @@ udiskie
ufw
ufw-docker
unzip
usbguard
usage
uwsm
vi
+4
View File
@@ -0,0 +1,4 @@
# Base packages with no aarch64 build in any repository Omarchy configures.
# omarchy-pkg-defaults leaves them out of every aarch64 platform's set.
gliff
superwhisper-bin
+35 -3
View File
@@ -1,7 +1,39 @@
# Configure pacman after package installation completes. Offline target package
# installs use the live ISO's offline pacman.conf until this final restore.
cp -f "$OMARCHY_PATH/default/pacman/pacman-${OMARCHY_MIRROR:-stable}.conf" /etc/pacman.conf
cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-${OMARCHY_MIRROR:-stable}" /etc/pacman.d/mirrorlist
# installs use the live ISO's offline pacman.conf until this final restore,
# which copies the platform's channel templates (install/helpers/pacman.sh). A
# Mac's image brings its keyrings, as the ISO does on x86; every other aarch64
# platform installs Arch Linux ARM's before its repositories replace the
# offline ones, and trust every installed keyring before the first signed sync.
source "$OMARCHY_PATH/install/helpers/pacman.sh"
source "$OMARCHY_PATH/install/helpers/image-target.sh"
platform=$(omarchy-hw-platform)
templates=$(omarchy_pacman_templates "$platform")
alarm_keyring=0
if [[ $platform == aarch64* && $platform != "aarch64-apple" ]]; then
alarm_keyring=1
fi
if (( alarm_keyring )); then
omarchy-pkg-add archlinuxarm-keyring
fi
# An install built for a channel this platform has no templates for (stable or
# rc on aarch64) gets the platform's default channel instead.
channel=${OMARCHY_MIRROR:-}
[[ -n $channel && -f $templates/pacman-$channel.conf ]] || channel=$(omarchy_pacman_default_channel "$platform")
cp -f "$templates/pacman-$channel.conf" /etc/pacman.conf
cp -f "$templates/mirrorlist-$channel" /etc/pacman.d/mirrorlist
# An image build leaves the keyring to each machine's first boot, so no two
# share a master key (install/helpers/image-target.sh).
if (( alarm_keyring )); then
if omarchy_image_init && omarchy_image_manifest_present; then
install -m 0644 /dev/null "$omarchy_image_keyring_request"
else
pacman-key --init
pacman-key --populate
fi
fi
# Wait for CUPS to own the file, the way omarchy-settings does, so pacman does
# not turn the override into a .pacnew during ISO package installation.
+290
View File
@@ -0,0 +1,290 @@
# Resumable re-key of the root LUKS volume from the staged install key to the
# owner's password. Sourcing performs no setup.
#
# Caller contract:
# - Set PROVISIONING_DIR (holds the staged luks-key), REKEY_STATE, LOG_FILE and
# password. Secrets reach cryptsetup through key files and process
# substitution, never argv, and the entry points turn tracing off.
# - Define luks_boot_layout, printing a fingerprint of the keyboard layout the
# boot files ask for the disk password in (the same value while it is
# unchanged, and never the layout's name).
# - Define log_step and say, plus the platform's boot-time unlock:
# luks_auto_unlock_present succeeds while any boot-time copy of the staged
# key or its unlock configuration remains; luks_auto_unlock_drop removes
# them and rebuilds the boot files, restoring the unlock before it fails.
# luks_record_slots <owner> records the kept slot wherever the platform's
# boot checks look for it, and succeeds where nothing does.
#
# The journal (REKEY_STATE) records only the phase, slot numbers and the layout
# fingerprint, never key material. Phases advance staged → owner → boot → done, each written durably
# after its step, so an attempt interrupted anywhere resumes from the last one;
# the retire and record steps before done are idempotent. A retry may still
# choose a new password while the staged key opens the disk, even after the
# boot step, so the kept slot is recorded only once it is final. The
# caller removes the journal with the rest of its provisioning state, so until
# then a retry can only use the password the disk holds.
# cryptsetup open tries enrolled tokens (TPM2, FIDO2, keyring) before the key
# and reports a token's slot whatever key it was given. Restricting it to a
# token type no token has leaves only the key to decide.
luks_key_slot() {
local out
out=$(LC_ALL=C cryptsetup open --test-passphrase --verbose --token-type passphrase-only --key-file "$1" "$2" 2>&1) || return 0
grep -o 'Key slot [0-9]* unlocked' <<<"$out" | grep -o '[0-9]*' | head -1 || true
}
luks_slot_for() {
local -
set +x
luks_key_slot <(printf '%s' "$1") "$2"
}
# LUKS2 lists keyslots under "Keyslots:", and a luks2-keyring token under
# "Tokens:" looks the same, so read only the keyslot section.
luks_dump_slots() {
cryptsetup luksDump "$1" | awk '
/^[^ \t]/ { keyslots = ($0 == "Keyslots:") }
keyslots && /^ +[0-9]+: luks2/ { sub(":", "", $1); print $1 }
/^Key Slot [0-9]+: ENABLED/ { sub(":", "", $3); print $3 }'
}
rekey_state_get() {
[[ -f $REKEY_STATE ]] || return 1
awk -F= -v k="$1" '$1 == k { print $2; exit }' "$REKEY_STATE"
}
# Set key/value pairs, keeping every other recorded key.
rekey_state_put() {
local line key tmp
local -A updates=()
local -a lines=()
while (( $# >= 2 )); do
updates[$1]=$2
shift 2
done
if [[ -f $REKEY_STATE ]]; then
while IFS= read -r line || [[ -n $line ]]; do
if [[ -n $line && -z ${updates[${line%%=*}]+set} ]]; then
lines+=("$line")
fi
done <"$REKEY_STATE"
fi
for key in "${!updates[@]}"; do
lines+=("$key=${updates[$key]}")
done
tmp=$(mktemp "$REKEY_STATE.XXXXXX") || return 1
if printf '%s\n' "${lines[@]}" >"$tmp" && chmod 600 "$tmp" && sync "$tmp" && mv -f "$tmp" "$REKEY_STATE"; then
sync "$(dirname "$REKEY_STATE")"
return
fi
rm -f "$tmp"
return 1
}
staged_key_slot() {
[[ -f $PROVISIONING_DIR/luks-key ]] || return 0
luks_key_slot "$PROVISIONING_DIR/luks-key" "$1"
}
# The staged key or its boot-time auto-unlock is still on this machine.
luks_staged_unlock_remains() {
[[ -e $PROVISIONING_DIR/luks-key ]] || luks_auto_unlock_present
}
# Re-key work is due while the staged unlock remains or the journal is open.
luks_rekey_pending() {
[[ -e $REKEY_STATE ]] || luks_staged_unlock_remains
}
# Whether an interrupted re-key can finish with $password. Until the staged key
# is retired it can add any password; afterwards only one the disk holds.
luks_rekey_accepts_password() {
local -
set +x
local device=$1
[[ -f $REKEY_STATE ]] || return 0
[[ -n $(luks_slot_for "$password" "$device") || -n $(staged_key_slot "$device") ]]
}
# Record the slot $password opens, adding it with the staged key when none does.
# A retry after a crash between the add and the journal write finds the slot
# instead of adding a duplicate, and a retry with a new password adds its own
# while the staged key still works; the retire step removes the stale one.
luks_rekey_owner() {
local device=$1 phase=$2 owner
owner=$(luks_slot_for "$password" "$device")
if [[ -z $owner ]]; then
if [[ -z $(staged_key_slot "$device") ]]; then
log_step "the password opens no LUKS slot and the staged key no longer unlocks $device"
say --foreground 1 "Use the disk password chosen earlier in setup."
return 1
fi
if ! cryptsetup luksAddKey --key-file "$PROVISIONING_DIR/luks-key" "$device" <(printf '%s' "$password"); then
log_step "could not add the owner's key to $device"
say --foreground 1 "Could not add your password to the disk; will retry."
return 1
fi
owner=$(luks_slot_for "$password" "$device")
if [[ -z $owner ]]; then
log_step "could not identify the owner's LUKS slot after adding it"
say --foreground 1 "Could not confirm the LUKS re-key; will retry."
return 1
fi
fi
if [[ $owner == "$(rekey_state_get staged_slot || true)" ]]; then
log_step "the owner's password is the staged install key; refusing to keep it"
say --foreground 1 "Choose a disk password different from the temporary install key."
return 1
fi
if [[ $phase == "staged" ]]; then
rekey_state_put owner_slot "$owner" phase owner
else
rekey_state_put owner_slot "$owner"
fi
}
luks_rekey_retire() {
local device=$1 owner slot slots
owner=$(rekey_state_get owner_slot || true)
if [[ -z $owner ]]; then
log_step "no owner slot is recorded; refusing to retire LUKS slots"
return 1
fi
if ! slots=$(luks_dump_slots "$device"); then
log_step "luksDump failed while retiring slots; keeping the staged key for retry"
say --foreground 1 "Could not enumerate LUKS slots; will retry."
return 1
fi
for slot in $slots; do
[[ $slot == "$owner" ]] && continue
if ! cryptsetup luksKillSlot -q --key-file <(printf '%s' "$password") "$device" "$slot"; then
log_step "failed to kill LUKS slot $slot; keeping the staged key for retry"
say --foreground 1 "Could not remove the throwaway LUKS key; will retry."
return 1
fi
done
}
# The staged key must open nothing before it is destroyed: only the owner slot
# remains and no boot-time copy or unlock configuration is left behind.
luks_rekey_verify() {
local device=$1 owner slots
owner=$(rekey_state_get owner_slot || true)
if ! slots=$(luks_dump_slots "$device") || [[ -z $owner || $slots != "$owner" ]]; then
log_step "LUKS slots other than the owner's remain on $device"
return 1
fi
if [[ -n $(staged_key_slot "$device") ]]; then
log_step "the staged LUKS key still unlocks $device"
return 1
fi
if luks_auto_unlock_present; then
log_step "the boot-time auto-unlock is still configured"
return 1
fi
}
# The boot files carry the console keyboard layout the disk password is typed
# in at boot, so rebuild them again when the owner chose another layout since
# they were built, or when an older journal recorded none. The phase stays.
luks_rekey_boot_layout() {
local layout=$1
[[ $(rekey_state_get boot_layout || true) != "$layout" ]] || return 0
log_step "the boot files were not built with the current keyboard layout; rebuilding them"
if ! luks_auto_unlock_drop; then
say --foreground 1 "Could not rebuild the boot files for your keyboard layout; will retry."
return 1
fi
sync
rekey_state_put boot_layout "$layout"
}
# Order: record the staged slot, add the owner's key, rebuild boot without the
# auto-unlock (keeping the staged slot as the fallback while that can fail),
# retire every other slot, then verify, record the kept slot for the platform,
# destroy the staged key and record done.
# Failing is loud: silently keeping the staged key would leave the disk
# effectively unencrypted.
luks_rekey() {
local -
set +x
local device=$1 phase staged layout
if ! layout=$(luks_boot_layout) || [[ -z $layout ]]; then
log_step "could not fingerprint the keyboard layout for the boot files"
say --foreground 1 "Could not read the keyboard layout for the boot files; will retry."
return 1
fi
phase=$(rekey_state_get phase || true)
case $phase in
"")
staged=$(staged_key_slot "$device")
if [[ -z $staged ]]; then
log_step "the staged LUKS key is missing or does not unlock $device"
say --foreground 1 "The staged LUKS key no longer unlocks $device."
return 1
fi
rekey_state_put staged_slot "$staged" phase staged || return 1
phase=staged
;;
staged | owner | boot | done) ;;
*)
log_step "unknown LUKS re-key phase '$phase' in $REKEY_STATE"
say --foreground 1 "The LUKS re-key journal is unreadable."
return 1
;;
esac
if [[ $phase != "done" ]]; then
luks_rekey_owner "$device" "$phase" || return 1
[[ $phase == "staged" ]] && phase=owner
if [[ $phase == "owner" ]] || luks_auto_unlock_present; then
if ! luks_auto_unlock_drop; then
say --foreground 1 "Could not rebuild the boot files without the install key; will retry."
return 1
fi
sync
rekey_state_put phase boot boot_layout "$layout" || return 1
else
luks_rekey_boot_layout "$layout" || return 1
fi
luks_rekey_retire "$device" || return 1
else
if [[ $(luks_slot_for "$password" "$device") != "$(rekey_state_get owner_slot || true)" ]]; then
log_step "the password does not open the owner slot the finished re-key recorded on $device"
say --foreground 1 "Use the disk password chosen earlier in setup."
return 1
fi
luks_rekey_boot_layout "$layout" || return 1
fi
if ! luks_rekey_verify "$device"; then
say --foreground 1 "Could not confirm the temporary install key was removed; will retry."
return 1
fi
if ! luks_record_slots "$(rekey_state_get owner_slot)"; then
log_step "the platform could not record the kept LUKS slot"
say --foreground 1 "Could not record the disk's key slot for the boot checks; will retry."
return 1
fi
if [[ -e $PROVISIONING_DIR/luks-key ]]; then
shred -u "$PROVISIONING_DIR/luks-key" 2>/dev/null || rm -f "$PROVISIONING_DIR/luks-key" || return 1
fi
rm -f "$REKEY_STATE".??????
[[ $phase == "done" ]] || rekey_state_put phase done
}
@@ -0,0 +1,21 @@
# Runs the hardware setup an image build deferred, on the machine it was built
# for, before owner setup and the login screen. Armed with its queue by
# omarchy-apply-hardware during the build (install/helpers/image-target.sh);
# retried on every boot until the queue is empty and any keyring an image build
# asked for exists, then it disables itself (the unit file stays; with neither
# file left it no longer runs).
[Unit]
Description=Omarchy first-boot hardware setup deferred from the image build
ConditionPathExists=|/var/lib/omarchy/image/deferred-steps
ConditionPathExists=|/var/lib/omarchy/image/pacman-keyring
Before=omarchy-provision-owner.service display-manager.service
[Service]
Type=oneshot
ExecStart=/usr/bin/omarchy-provision-hardware
TimeoutStartSec=infinity
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+3
View File
@@ -14,3 +14,6 @@ run_logged "$OMARCHY_INSTALL/user/hardware/vm-no-animations.sh"
run_logged "$OMARCHY_INSTALL/user/default-keyring.sh"
run_logged "$OMARCHY_INSTALL/user/mise.sh"
run_logged "$OMARCHY_INSTALL/user/dictation-default.sh"
run_logged "$OMARCHY_INSTALL/user/platform-setup.sh"
+8
View File
@@ -0,0 +1,8 @@
# The settings package presets Superwhisper as the dictation backend, and it has
# no aarch64 build. A new user there starts with no backend selected and picks
# one in Setup > Defaults > Dictation. Any other selection is left alone.
dictation_default="${XDG_CONFIG_HOME:-$HOME/.config}/omarchy/defaults/dictation"
if ! omarchy-hw-x86 && omarchy-cmd-missing superwhisper &&
[[ -f $dictation_default && $(<"$dictation_default") == "superwhisper" ]]; then
rm -f "$dictation_default"
fi
+11 -2
View File
@@ -14,13 +14,22 @@ set -euo pipefail
omarchy-input-method configure --defaults || echo "Input method defaults deferred; run omarchy setup input after login." >&2
systemctl --user daemon-reload
systemctl --user enable --now \
# One at a time: given a list, systemctl enables none of it when it cannot find
# one unit, and that should not cost the session its bluetooth agent as well.
failed=0
for unit in \
bt-agent.service \
owed.service \
omarchy-recover-internal-monitor.service \
omarchy-sleep-lock.service \
omarchy-migrate-notify.service \
omarchy-fcitx5.service \
omarchy-crash-watch.service
omarchy-crash-watch.service \
omarchy-usb-authorization.service \
omarchy-thunderbolt-authorization.service; do
systemctl --user enable --now "$unit" || failed=1
done
omarchy-hook-install theme-set /usr/share/owe/10-owe-sync
exit "$failed"
+10
View File
@@ -0,0 +1,10 @@
# The platform package's own setup for this user, last so it builds on every
# other leaf (omarchy-lifecycle-dispatch setup-user, see
# docs/lifecycle-dispatch.md). A no-op where the platform registers none. In an
# image build the detector would read the build host, so it waits for first run
# on the machine, which runs it again.
if [[ -e ${OMARCHY_IMAGE_ROOT:-}/var/lib/omarchy/image/target ]]; then
echo "Image build: the platform's user setup waits for first run on the machine"
else
omarchy-lifecycle-dispatch setup-user
fi