Files
matterdecode/homekit-decode.sh

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"