#!/usr/bin/env bash # # homekit-decode.fixed.sh — corrected decoder for a HomeKit setup payload. # # This is a fixed copy of homekit-decode.sh. It decodes using the *correct* # HomeKit bit layout (the one composeSetupUri in the homekit-code package # actually encodes), instead of the misaligned bit positions in the original. # # 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 homekit-decode.sh: # - 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.fixed.sh [PAYLOAD] # Defaults to the demo payload "005KEOUCE" when no argument is given, matching # the original so the two scripts decode the same thing by default. # 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:-}" printf 'Setup Payload: X-HM://%s%s\n' "$block36" "$setup_id"