Files
matterdecode/matter-decode.sh
T
2026-08-23 20:48:38 -04:00

159 lines
4.8 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# matter-decode.sh — decode a Matter commissioning QR code (MT: URI) from the CLI.
#
# This is a bash port of the decode logic in matter-qr-decoder.html (the browser
# app's camera/QR-scanning feature has no shell equivalent and is intentionally
# omitted). It decodes the 84-bit payload and prints the same fields the web UI shows.
#
# Usage:
# ./matter-decode.sh "MT:..."
# echo "MT:..." | ./matter-decode.sh
#
# A companion file manufacturers.tsv (same directory) maps vendor IDs to names.
# It is derived from project-chip/connectedhomeip's manufacturers.xml and is
# optional: without it, all fields still decode — the vendor name line is simply
# skipped. Regenerate it with: python3 gen-manufacturers.py (see that file).
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)"
# Base38 charset, per the Matter spec: 0-9, A-Z, '-', '.' (38 symbols).
CHARS=( 0 1 2 3 4 5 6 7 8 9 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z - . )
declare -A IDX # char -> position
for (( _i=0; _i<${#CHARS[@]}; _i++ )); do IDX["${CHARS[_i]}"]=$_i; done
# Powers of 38 for the 5-char (3-byte) chunk; 38^0..38^4.
POW38=( 1 38 1444 54872 2085136 )
BYTES=() # decoded payload as a byte array, little-endian
# base38_decode STRING -> fills global BYTES.
# Greedy chunking: 5 chars -> 3 bytes, 4 -> 2, 2 -> 1, matching the reference.
base38_decode() {
local s=$1
local i=0 len=${#s} j value char idx chunkSize byteCount
BYTES=()
while (( i < len )); do
if (( i + 5 <= len )); then chunkSize=5; byteCount=3
elif (( i + 4 <= len )); then chunkSize=4; byteCount=2
elif (( i + 2 <= len )); then chunkSize=2; byteCount=1
else
echo "Error: Invalid base38 string length" >&2
return 1
fi
value=0
for (( j=0; j<chunkSize; j++ ))
do
char=${s:i+j:1}
idx=${IDX[$char]:-_} # resolve in param-expansion context;
if [[ $idx == _ ]]; then # avoids an operator-char array key inside $(( ))
echo "Error: Invalid character: $char" >&2
return 1
fi
value=$(( value + idx * POW38[j] ))
done
for (( j=0; j<byteCount; j++ ))
do
BYTES+=( $(( (value >> (8*j)) & 0xFF )) )
done
i=$(( i + chunkSize ))
done
}
# extract_bits START COUNT -> prints COUNT bits of BYTES, LSB-first, as a number.
extract_bits() {
local start=$1 count=$2 i result=0 bitIndex byteIndex bitOffset bit
for (( i=0; i<count; i++ )); do
bitIndex=$(( start + i ))
byteIndex=$(( bitIndex / 8 ))
bitOffset=$(( bitIndex % 8 ))
if (( byteIndex < ${#BYTES[@]} ))
then
bit=$(( (BYTES[byteIndex] >> bitOffset) & 1 ))
result=$(( result | (bit << i) ))
fi
done
printf '%d' "$result"
}
# get_vendor_name DECID -> prints name if in manufacturers.tsv, else nothing.
get_vendor_name() {
local tsv="$SCRIPT_DIR/manufacturers.tsv"
[[ -f $tsv ]] || return 0
awk -F'\t' -v id="$1" '$1==id { print $2; exit }' "$tsv"
}
main() {
local uri payload
uri=${1:-}
[[ -z $uri ]] && uri=$(cat)
# Trim leading/trailing whitespace
uri="${uri#"${uri%%[![:space:]]*}"}"
uri="${uri%"${uri##*[![:space:]]}"}"
if [[ $uri != MT:* ]]; then
echo 'Error: URI must start with "MT:"' >&2
return 1
fi
payload=${uri#MT:}
[[ -n $payload ]] || { echo 'Error: No data found after "MT:"' >&2; return 1; }
base38_decode "$payload" || return 1
local version vendor product customFlow discovery discriminator passcode
local cf caps="" vname
version=$(extract_bits 0 3)
vendor=$(extract_bits 3 16)
product=$(extract_bits 19 16)
customFlow=$(extract_bits 35 2)
discovery=$(extract_bits 37 8)
discriminator=$(extract_bits 45 12)
passcode=$(extract_bits 57 27)
case $customFlow in
0) cf="Standard Commissioning Flow" ;;
1) cf="User Intent Commissioning Flow" ;;
2) cf="Custom Commissioning Flow" ;;
3) cf="Reserved" ;;
*) cf="Unknown ($customFlow)" ;;
esac
(( discovery & 0x01 )) && caps="SoftAP"
(( discovery & 0x02 )) && caps="${caps:+$caps, }BLE"
(( discovery & 0x04 )) && caps="${caps:+$caps, }On IP Network"
[[ -z $caps ]] && caps="None"
vname=$(get_vendor_name "$vendor")
printf 'Version: %s\n' "$version"
printf 'Vendor ID: %s (0x%04X)' "$vendor" "$vendor"
[[ -n $vname ]] && printf ' %s' "$vname"
printf '\n'
printf 'Product ID: %s (0x%04X)\n' "$product" "$product"
printf 'Custom Flow: %s\n' "$cf"
printf 'Discovery: %s\n' "$caps"
printf 'Discriminator: %s\n' "$discriminator"
printf 'Passcode: %s\n' "$passcode"
}
main "$@"