149 lines
5.4 KiB
Bash
Executable File
149 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# homekit-decode.sh — decoder for a HomeKit setup payload.
|
|
#
|
|
# It decodes using the *correct* HomeKit bit layout (the one composeSetupUri in
|
|
# the homekit-code package actually encodes), instead of the misaligned bit
|
|
# positions an earlier version used.
|
|
#
|
|
# Correct layout, least-significant bit first:
|
|
# bits 0-26 pin (27 bits)
|
|
# bits 27-30 flag (4 bits: 1=NFC 2=IP 4=BLE 8=WAC/MFi)
|
|
# bits 31-38 categoryId (8 bits)
|
|
# bits 39-42 reserved (4 bits)
|
|
# bits 43-45 version (3 bits)
|
|
#
|
|
# Fixes vs the earlier version:
|
|
# - categoryId was (val >> 27) & 0xFF, which mixed in the flag field; the
|
|
# categoryId field starts at bit 31, so it is now (val >> 31) & 0xFF.
|
|
# - The original "IP/BLE/Thread" flags read bits 35/36/37, which live inside
|
|
# the categoryId field. The real flag field is bits 27-30 with sub-flags
|
|
# NFC(1)/IP(2)/BLE(4)/WAC(8); "Thread" was a misread and is dropped.
|
|
# - version was (val >> 38) & 0xFF, overlapping reserved; it is a 3-bit field
|
|
# at bits 43-45, so it is now (val >> 43) & 0x7.
|
|
# - reserved (bits 39-42) is reported instead of being swallowed by version.
|
|
# - the category id is resolved to its HAP name (e.g. 8 -> "Switch"); ids not
|
|
# in the known table print as "unknown".
|
|
#
|
|
# Usage: ./homekit-decode.sh [PAYLOAD]
|
|
# Defaults to the demo payload "005KEOUCE" when no argument is given.
|
|
# The output also prints the setup id and a reconstructed "X-HM://" payload
|
|
# rebuilt from the decoded fields: a complete record for recreating the barcode.
|
|
|
|
set -euo pipefail
|
|
|
|
# Strip the "X-HM://" prefix if present. The first 9 chars are the fixed
|
|
# parameter block; the rest is the (variable-length) setup id, appended verbatim
|
|
# and not part of the numeric block.
|
|
body="${1:-005KEOUCE}"
|
|
body="${body#X-HM://}"
|
|
clean_str="${body:0:9}"
|
|
setup_id="${body:9}"
|
|
|
|
# Step 1: Base36 -> integer (same as homekit-decode.sh).
|
|
val=0
|
|
for (( i = 0; i < ${#clean_str}; i++ )); do
|
|
code=$(printf '%d' "'${clean_str:i:1}")
|
|
if (( code >= 48 && code <= 57 )); then digit=$(( code - 48 ))
|
|
elif (( code >= 65 && code <= 90 )); then digit=$(( code - 65 + 10 ))
|
|
elif (( code >= 97 && code <= 122 )); then digit=$(( code - 97 + 10 ))
|
|
else
|
|
printf 'error: invalid Base36 character %q\n' "${clean_str:i:1}" >&2
|
|
exit 1
|
|
fi
|
|
val=$(( val * 36 + digit ))
|
|
done
|
|
|
|
# Step 2: Bitmask extraction, corrected to the HomeKit layout.
|
|
pin=$(( val & 0x7FFFFFF )) # bits 0-26
|
|
flag=$(( (val >> 27) & 0xF )) # bits 27-30
|
|
category=$(( (val >> 31) & 0xFF )) # bits 31-38
|
|
reserved=$(( (val >> 39) & 0xF )) # bits 39-42
|
|
version=$(( (val >> 43) & 0x7 )) # bits 43-45
|
|
|
|
# Resolve the numeric category id to its HAP name (homekit_accessory_category_*).
|
|
# https://developer.apple.com/documentation/homekit/hmaccessorycategory
|
|
# A case statement (not a `declare -A` map) keeps this working on the stock
|
|
# macOS bash 3.2 that `#!/usr/bin/env bash` resolves to. "Heater" corrects a
|
|
# "Header" typo in the source table; id 25 is reserved by HomeKit, so unknown.
|
|
cat_name="unknown"
|
|
case "$category" in
|
|
1) cat_name="Other" ;;
|
|
2) cat_name="Bridge" ;;
|
|
3) cat_name="Fan" ;;
|
|
4) cat_name="Garage" ;;
|
|
5) cat_name="Lightbulb" ;;
|
|
6) cat_name="Door Lock" ;;
|
|
7) cat_name="Outlet" ;;
|
|
8) cat_name="Switch" ;;
|
|
9) cat_name="Thermostat" ;;
|
|
10) cat_name="Sensor" ;;
|
|
11) cat_name="Security System" ;;
|
|
12) cat_name="Door" ;;
|
|
13) cat_name="Window" ;;
|
|
14) cat_name="Window Covering" ;;
|
|
15) cat_name="Programmable Switch" ;;
|
|
16) cat_name="Range Extender" ;;
|
|
17) cat_name="IP Camera" ;;
|
|
18) cat_name="Video Door Bell" ;;
|
|
19) cat_name="Air Purifier" ;;
|
|
20) cat_name="Heater" ;;
|
|
21) cat_name="Air Conditioner" ;;
|
|
22) cat_name="Humidifier" ;;
|
|
23) cat_name="Dehumidifier" ;;
|
|
24) cat_name="Apple TV" ;;
|
|
26) cat_name="Speaker" ;;
|
|
27) cat_name="AirPort" ;;
|
|
28) cat_name="Sprinkler" ;;
|
|
29) cat_name="Faucet" ;;
|
|
30) cat_name="Shower Head" ;;
|
|
31) cat_name="Television" ;;
|
|
32) cat_name="Target Controller" ;;
|
|
esac
|
|
|
|
# Python's bool() prints "True"/"False"; treat any non-zero bit as true.
|
|
to_bool() {
|
|
if (( $1 != 0 )); then
|
|
printf 'True'
|
|
else
|
|
printf 'False'
|
|
fi
|
|
}
|
|
|
|
# Base36 integer -> uppercase string (mirrors composeSetupUri's toString(36).toUpperCase()).
|
|
to_base36() {
|
|
local n=$1 digits='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' out=''
|
|
if [ "$n" -eq 0 ]; then
|
|
printf '0'
|
|
return
|
|
fi
|
|
while [ "$n" -gt 0 ]; do
|
|
out="${digits:n%36:1}${out}"
|
|
n=$(( n / 36 ))
|
|
done
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
# Reconstruct the 9-char parameter block from the decoded fields (mirrors
|
|
# composeSetupUri's stepwise packing) so the output is a complete record the
|
|
# barcode can be rebuilt from.
|
|
block=$(( version & 0x7 ))
|
|
block=$(( (block << 4) | (reserved & 0xF) ))
|
|
block=$(( (block << 8) | (category & 0xFF) ))
|
|
block=$(( (block << 4) | (flag & 0xF) ))
|
|
block=$(( (block << 27) | (pin & 0x7FFFFFF) ))
|
|
block36="$(to_base36 "$block")"
|
|
while [ ${#block36} -lt 9 ]; do block36="0${block36}"; done
|
|
|
|
printf 'Setup PIN: %08d\n' "$pin"
|
|
printf 'Flag: %d\n' "$flag"
|
|
printf ' Supports NFC: %s\n' "$(to_bool $(( flag & 1 )))"
|
|
printf ' Supports IP: %s\n' "$(to_bool $(( flag & 2 )))"
|
|
printf ' Supports BLE: %s\n' "$(to_bool $(( flag & 4 )))"
|
|
printf ' Supports WAC/MFi: %s\n' "$(to_bool $(( flag & 8 )))"
|
|
printf 'Category ID: %d (%s)\n' "$category" "$cat_name"
|
|
printf 'Reserved: %d\n' "$reserved"
|
|
printf 'Version: %d\n' "$version"
|
|
printf 'Setup ID: %s\n' "${setup_id:-<none>}"
|
|
printf 'Setup Payload: X-HM://%s%s\n' "$block36" "$setup_id"
|