feat(wear): key watch allow-list on ro.serialno, not adb transport serial

The ALLOWED_SERIALS guard matched the adb transport serial, which over
WiFi is a rotating ip:port (changes on every wireless-debugging restart)
and over USB is the bare hardware serial. Pinning the transport serial
meant every port rotation broke the deploy until the profile was hand-edited.

Resolve the device's stable ro.serialno (getprop) after target resolution
and match that against ALLOWED_SERIALS instead. The transport serial still
drives adb/gradle/ANDROID_SERIAL; only the wrong-watch guard switches to the
stable identity, so it survives port rotation and is identical over WiFi/USB.

Also: 'devices' now prints 'transport -> ro.serialno' per device to make the
value for ALLOWED_SERIALS easy to read off. Help text and the example profile
are updated to the ro.serialno form.

closes #6
This commit is contained in:
2026-05-30 12:57:42 +02:00
parent 8a0134c7e8
commit 11de27d435
2 changed files with 59 additions and 14 deletions
@@ -17,9 +17,12 @@ SERVER_URL="http://minerva.lan:3000"
# Generate with: openssl rand -base64 24
API_KEY="REPLACE_ME_WITH_PLAINTEXT_API_KEY"
# Bash array of ADB serials (one per attached watch this profile may target).
# Find a watch's serial with: ./run.sh devices
# Bash array of stable hardware serials (ro.serialno) — one per attached watch
# this profile may target. NOT the adb transport serial (the rotating ip:port
# you see over WiFi): the guard reads ro.serialno so it survives port rotation
# and is identical over WiFi and USB.
# Find a watch's ro.serialno with: ./run.sh devices (prints "transport -> ro.serialno").
# Brummel's watch can appear in multiple profiles; Krey's watch only in hers.
ALLOWED_SERIALS=(
"adb-XXXXXXXX-yyyy._adb-tls-connect._tcp"
"XXXXXXXXXXXXXX"
)
+53 -11
View File
@@ -13,9 +13,10 @@
# Real Pixel Watch over ADB-WiFi. <profile> is the basename of
# a file in profiles.d/<profile>.sh that holds SERVER_URL,
# API_KEY, and ALLOWED_SERIALS (see profiles.d/*.sh.example).
# The build refuses to proceed if the attached watch's ADB
# serial is not listed in the profile's ALLOWED_SERIALS — this
# protects against installing the wrong key on the wrong watch.
# The build refuses to proceed if the attached watch's stable
# hardware serial (ro.serialno) is not listed in the profile's
# ALLOWED_SERIALS — this protects against installing the wrong
# key on the wrong watch.
# emulator Wear OS AVD on this host. No profile — uses the hardcoded
# emulator-loopback URL (10.0.2.2:3000) and the build's
# MISSING_API_KEY sentinel.
@@ -278,10 +279,30 @@ resolve_watch_serial() {
RESOLVED_SERIAL="${PICKED%% *}"
}
# Reads the stable hardware serial (ro.serialno) of the device addressed by the
# adb transport serial $1. The transport serial depends on the connection:
# ip:port over WiFi (rotates on every wireless-debugging restart) or the bare
# hardware serial over USB. ro.serialno is burned into the device and constant
# across both — the right identity for the wrong-watch guard. Writes the result
# to the global RESOLVED_HW_SERIAL (not stdout+$(), so a `die` here aborts the
# main shell rather than being swallowed by a subshell — same rationale as
# RESOLVED_SERIAL above). Aborts if the device can't be reached.
RESOLVED_HW_SERIAL=""
resolve_hardware_serial() {
detect_adb
local transport="$1"
local hw
hw=$("$ADB" -s "$transport" shell getprop ro.serialno 2>/dev/null | tr -d '\r\n' || true)
[[ -n "$hw" ]] || die "Could not read ro.serialno from '$transport'. Is the watch attached and authorized? Check './run.sh devices'."
RESOLVED_HW_SERIAL="$hw"
}
# --- Profile loading (watch target only) -----------------------------------
# Profiles live in profiles.d/<name>.sh and define SERVER_URL, API_KEY, and
# ALLOWED_SERIALS as a bash array. The .sh files are gitignored — see
# profiles.d/example-profile.sh.example for the format.
# ALLOWED_SERIALS as a bash array. ALLOWED_SERIALS holds stable hardware serials
# (ro.serialno), NOT adb transport serials — see resolve_hardware_serial above
# for why. The .sh files are gitignored — see profiles.d/example-profile.sh.example
# for the format.
PROFILE_NAME=""
SERVER_URL=""
@@ -308,16 +329,18 @@ load_profile() {
PROFILE_NAME="$name"
}
# Aborts (via die) if $1 is not present in ALLOWED_SERIALS. The error message
# names BOTH the attached serial and the allow-list, so the user can see at a
# glance whether they grabbed the wrong watch or typed the wrong profile.
# Aborts (via die) if $1 is not present in ALLOWED_SERIALS. The caller passes
# the device's stable hardware serial (ro.serialno), never the rotating adb
# transport serial. The error message names BOTH the hardware serial and the
# allow-list, so the user can see at a glance whether they grabbed the wrong
# watch or typed the wrong profile.
verify_serial_allowed() {
local serial="$1"
local s
for s in "${ALLOWED_SERIALS[@]}"; do
[[ "$s" == "$serial" ]] && return 0
done
die "Watch serial '$serial' is NOT in ALLOWED_SERIALS for profile '$PROFILE_NAME'.
die "Watch hardware serial (ro.serialno) '$serial' is NOT in ALLOWED_SERIALS for profile '$PROFILE_NAME'.
Allowed: ${ALLOWED_SERIALS[*]}
This refusal protects against installing the wrong key on the wrong watch
(e.g. Brummel-key on Krey's watch → patient data goes to dev server)."
@@ -334,13 +357,18 @@ Available profiles: $(cd "$SCRIPT_DIR/profiles.d" 2>/dev/null && ls *.sh 2>/dev/
resolve_watch_serial
ADB_SERIAL="$RESOLVED_SERIAL"
fi
verify_serial_allowed "$ADB_SERIAL"
# ADB_SERIAL stays the transport serial (adb addressing, gradle,
# ANDROID_SERIAL). The wrong-watch guard checks the stable hardware
# serial instead, so it survives WiFi port rotation and matches the
# same value whether connected over WiFi or USB.
resolve_hardware_serial "$ADB_SERIAL"
verify_serial_allowed "$RESOLVED_HW_SERIAL"
GRADLE_PROPS+=(
"-Pdoctate.serverUrl=$SERVER_URL"
"-Pdoctate.apiKey=$API_KEY"
"-Pdoctate.profileName=$DOCTATE_PROFILE"
)
info "Target: watch (profile=$DOCTATE_PROFILE, serial=$ADB_SERIAL, server=$SERVER_URL)"
info "Target: watch (profile=$DOCTATE_PROFILE, transport=$ADB_SERIAL, hw=$RESOLVED_HW_SERIAL, server=$SERVER_URL)"
;;
emulator)
if [[ -z "$ADB_SERIAL" ]]; then
@@ -514,6 +542,20 @@ cmd_devices() {
detect_adb
info "Attached devices:"
"$ADB" devices -l | sed '1d;/^$/d' | sed 's/^/ /'
# Map each transport serial to its stable hardware serial (ro.serialno) —
# the value that belongs in a profile's ALLOWED_SERIALS. Over WiFi the
# transport serial is a rotating ip:port; over USB it already equals the
# hardware serial. getprop makes the right value explicit either way.
local -a transports
mapfile -t transports < <("$ADB" devices | awk 'NR>1 && $2=="device"{print $1}')
if [[ "${#transports[@]}" -gt 0 ]]; then
info "Hardware serials (ro.serialno — use these in ALLOWED_SERIALS):"
local t hw
for t in "${transports[@]}"; do
hw=$("$ADB" -s "$t" shell getprop ro.serialno 2>/dev/null | tr -d '\r\n' || true)
printf " %s -> %s\n" "$t" "${hw:-<unreachable>}"
done
fi
if [[ -n "$ADB_SERIAL" ]]; then
info "Pinned by current target: $ADB_SERIAL"
fi