Dual system bluetooth syncronization
sync_bluetooth.sh
463 lines 12 kB view raw
1#!/bin/sh 2# 3# This script updates Bluetooth configuration to use the same device (mouse/keyboard) on both Windows and Linux. 4# It is based on these recommendations: https://wiki.archlinux.org/title/Bluetooth. 5# It works specifically for Logitech Pebble mouse on Linux Mint and it may or may not work for you. 6# It should be relatively easy to add other devices though. 7# 8# Usage: 9# 10# > ./sync_bluetooth.sh [media_path] 11# 12# Script tries to find Windows registry in mounted volumes (/media by default). 13# It finds matching configurations in /var/lib/bluetooth (MAC address may be slightly different 14# so it ignores the last byte), updates the configuration according to this table and restarts 15# the bluetooth service. Previous configuration is saved to info.backup. 16# To try it before using, use DRY_RUN environment variable: 17# 18# > DRY_RUN=1 ./sync_bluetooth.sh [media_path] 19# 20 21MEDIA_PATH=${1:-/media} 22KEYS_PATH=ControlSet001\\Services\\BTHPORT\\Parameters\\Keys 23BT_CONFIG_PATH=/var/lib/bluetooth 24 25# =============== Utils =============== 26 27find_sys32() { 28 while [ "$#" -gt 0 ]; do 29 current="$1" 30 shift 31 32 win_dir="$current/Windows" 33 sys32_dir="$win_dir/System32" 34 35 if [ -d "$sys32_dir" ]; then 36 echo "$sys32_dir" 37 exit 0 38 fi 39 40 if [ -r "$current" ] && [ -x "$current" ]; then 41 for sub in "$current"/*; do 42 if [ -d "$sub" ] && [ -x "$sub" ]; then 43 # Append to the end of the queue 44 set -- "$@" "$sub" 45 fi 46 done 47 fi 48 done 49 exit 1 50} 51 52exec_reg_command() { 53 REG_PATH=$1 54 CMD=$2 55 56 chntpw -e "$REG_PATH" << EOF 57$CMD 58q 59EOF 60} 61 62list_reg_keys() { 63 REG_PATH=$1 64 KEY_PATH=$2 65 66 OUTPUT=$(exec_reg_command "$REG_PATH" "ls $KEY_PATH") 67 68 echo "$OUTPUT" | awk ' 69 BEGIN { in_keys = 0 } 70 / key name/ { in_keys = 1; next } 71 in_keys { 72 if ($0 ~ /^([[:space:]]*|[[:space:]]*size[[:space:]].*)$/) exit 73 if ($0 ~ /<.*>/) { 74 gsub(/[<>[:space:]]/, "", $0) 75 print $0 76 } 77 } 78' 79} 80 81read_reg_value() { 82 REG_PATH=$1 83 KEY_PATH=$2 84 85 OUTPUT=$(exec_reg_command "$REG_PATH" "hex $KEY_PATH") 86 87 echo "$OUTPUT" | awk ' 88 /^(> )?Value .* \[0x[0-9a-fA-F]+\]$/ { 89 match($0, /\[0x[0-9a-fA-F]+\]/) 90 hexlen = substr($0, RSTART+3, RLENGTH-4) 91 bytelen = strtonum("0x" hexlen) 92 getline 93 # Extract hex bytes from column 2 onward 94 n = split($0, fields) 95 hex = "" 96 count = 0 97 for (i = 2; i <= n && count < bytelen; i++) { 98 if (fields[i] ~ /^[0-9A-Fa-f]{2}$/) { 99 hex = hex fields[i] 100 count++ 101 } 102 } 103 print hex 104 exit 105 } 106' 107} 108 109mac_to_hex() { 110 echo "$1" | tr '[:upper:]' '[:lower:]' | tr -d ':' 111} 112 113hex_to_mac() { 114 echo "$1" | sed 's/../&:/g;s/:$//' | tr '[:lower:]' '[:upper:]' 115} 116 117find_matching_keys() { 118 KEYS=$1 119 BT_DEVICES=$2 120 for KEY in $KEYS; do 121 KEY_PREFIX=$(echo "$KEY" | cut -c1-10) 122 for BT in $BT_DEVICES; do 123 BT_CLEAN=$(mac_to_hex "$BT" | cut -c1-10) 124 if [ "$KEY_PREFIX" = "$BT_CLEAN" ]; then 125 echo "${KEY}_${BT}" 126 fi 127 done 128 done 129} 130 131reverse_hex_bytes() { 132 while read -r HEX; do 133 echo "$HEX" | sed 's/../& /g' | awk '{ 134 for (i=NF; i>=1; i--) printf "%s", $i; 135 print ""; 136 }' 137 done 138} 139 140reverse_mac_bytes() { 141 while read -r MAC; do 142 echo "$MAC" | awk -F: '{ 143 for (i=NF; i>=1; i--) { 144 printf "%s", toupper($i); 145 if (i > 1) printf ":"; 146 } 147 print ""; 148 }' 149 done 150} 151 152hex_to_dec() { 153 while read -r HEX; do 154 awk "BEGIN { printf \"%0.f\n\", 0x$HEX }" 155 done 156} 157 158dec_to_hex() { 159 while read -r DEC; do 160 printf "%x\n" "$DEC" 161 done 162} 163 164read_from_config() { 165 FILE="$1" 166 SECTION="$2" 167 key="$3" 168 169 sudo awk -v target_section="$SECTION" -v target_key="$key" ' 170 BEGIN { in_section=0 } 171 { 172 if ($0 == target_section) { 173 in_section=1 174 next 175 } 176 if (in_section) { 177 if ($0 ~ /^\[.*\]$/) { 178 in_section=0 179 next 180 } 181 if ($0 ~ "^" target_key "=") { 182 split($0, parts, "=") 183 print parts[2] 184 exit 185 } 186 } 187 } 188 ' "$FILE" 189} 190 191write_to_config() { 192 FILE="$1" 193 SECTION="$2" 194 KEY="$3" 195 VALUE="$4" 196 197 # Use a temporary file for safe in-place editing 198 TMP_FILE=$(mktemp) || exit 1 199 200 TEXT=$(sudo awk -v target_section="$SECTION" -v target_key="$KEY" -v new_value="$VALUE" ' 201 BEGIN { 202 in_target_section = 0 203 found_section = 0 204 key_found = 0 205 key_added = 0 206 } 207 208 # Match section headers 209 /^\[.*\]$/ { 210 # If we were in target section and key wasnt found/added, add it now 211 if (in_target_section && !key_found && !key_added) { 212 print target_key "=" new_value 213 key_added = 1 214 } 215 216 # Check if this is our target section 217 if ($0 == target_section) { 218 in_target_section = 1 219 found_section = 1 220 } else { 221 in_target_section = 0 222 } 223 print $0 224 next 225 } 226 227 # Process lines within sections 228 { 229 if (in_target_section) { 230 # Check if this is an empty line and we need to add the key 231 if ($0 == "" && !key_found && !key_added) { 232 print target_key "=" new_value 233 key_added = 1 234 print $0 235 } 236 # Check if line contains a key=value pair 237 else if (match($0, /^[^=]+=/)) { 238 # Extract key (everything before first =) 239 split($0, parts, "=") 240 line_key = parts[1] 241 242 if (line_key == target_key) { 243 # Replace the value 244 print target_key "=" new_value 245 key_found = 1 246 key_added = 1 247 } else { 248 print $0 249 } 250 } else { 251 print $0 252 } 253 } else { 254 print $0 255 } 256 } 257 258 END { 259 # If we found the section but never added the key, add it at the end 260 if (found_section && !key_added) { 261 print target_key "=" new_value 262 } 263 264 # Set exit code based on whether section was found 265 if (!found_section) { 266 exit 1 267 } 268 } 269 ' "$FILE") 270 271 echo "$TEXT" > "$TMP_FILE" 272 echo "$SECTION: setting $KEY=$VALUE" 273 274 if [ -z "$DRY_RUN" ]; then 275 sudo mv "$TMP_FILE" "$FILE" 276 sudo chown root:root "$FILE" 277 else 278 rm "$TMP_FILE" 279 fi 280} 281 282select_device_key() { 283 DEVICE_KEYS=$1 284 BT_HOST_CONFIG_PATH=$2 285 echo "Matching devices:" 286 echo "=================" 287 288 # Convert space-separated string to indexed list 289 i=1 290 for KEY in $DEVICE_KEYS; do 291 HEX=$(echo "$KEY" | cut -d "_" -f 1) 292 MAC=$(echo "$KEY" | cut -d "_" -f 2) 293 DEVICE_NAME=$(read_from_config "$BT_HOST_CONFIG_PATH/$MAC/info" "[General]" "Name") 294 echo "$i) $DEVICE_NAME ($HEX => $MAC)" 295 i=$((i + 1)) 296 done 297 298 if [ $((i - 1)) -gt 1 ]; then 299 printf "Please select a device (1-%d) [1]: " $((i - 1)) 300 else 301 printf "Please select a device [1]: " 302 fi 303 read -r choice 304 305 if [ -z "$choice" ]; then 306 choice="1" 307 fi 308 309 # Validate input 310 if ! echo "$choice" | grep -q '^[0-9]\+$'; then 311 echo "Error: Please enter a valid number." 312 return 1 313 fi 314 315 if [ "$choice" -lt 1 ] || [ "$choice" -ge "$i" ]; then 316 echo "Error: Please enter a number between 1 and $((i - 1))." 317 return 1 318 fi 319 320 # Get the selected key 321 j=1 322 for KEY in $DEVICE_KEYS; do 323 if [ "$j" -eq "$choice" ]; then 324 SELECTED_KEY="$KEY" 325 break 326 fi 327 j=$((j + 1)) 328 done 329 330 echo "You selected: $(echo "$SELECTED_KEY" | cut -d " " -f 1)" 331 return 0 332} 333 334save_backup() { 335 FILE=$1 336 if [ -z "$DRY_RUN" ]; then 337 sudo cp "$FILE" "$FILE.backup" 338 echo "Backup saved to $FILE.backup" 339 fi 340} 341 342# =============== Script starts here =============== 343 344WINDOWS_SYS32_PATH=$(find_sys32 "$MEDIA_PATH") 345 346if [ -z "$WINDOWS_SYS32_PATH" ]; then 347 echo "No Windows registry directories found in $MEDIA_PATH" 348 echo "Check that Windows volume is mounted" 349 exit 1 350fi 351 352SYSTEM_PATH="$WINDOWS_SYS32_PATH/config/SYSTEM" 353 354if ! command -v chntpw > /dev/null; then 355 apt install chntpw 356fi 357 358echo "Accessing $SYSTEM_PATH..." 359echo "" 360 361KEYS=$(list_reg_keys "$SYSTEM_PATH" "$KEYS_PATH") 362 363if ! sudo -n true 2>/dev/null; then 364 echo "Note: We need elevated privileges to read Bluetooth data from /var/lib/bluetooth." 365fi 366 367HOST_MACS=$(sudo ls "$BT_CONFIG_PATH") 368 369MATCHING_PAIRS=$(find_matching_keys "$KEYS" "$HOST_MACS") 370 371if [ -z "$MATCHING_PAIRS" ]; then 372 echo "No host matches found :(" 373 exit 1 374fi 375 376HOST_HEX=$(echo "$MATCHING_PAIRS" | head -1 | cut -d "_" -f 1) 377HOST_MAC=$(echo "$MATCHING_PAIRS" | head -1 | cut -d "_" -f 2) 378 379echo "Found matching host MAC: $HOST_MAC ($HOST_HEX)" 380 381BT_DEVICE_KEYS=$(list_reg_keys "$SYSTEM_PATH" "$KEYS_PATH\\$HOST_HEX") 382 383BT_HOST_CONFIG_PATH="$BT_CONFIG_PATH/$HOST_MAC" 384 385BT_DEVICES=$(sudo ls "$BT_HOST_CONFIG_PATH") 386 387MATCHING_PAIRS=$(find_matching_keys "$BT_DEVICE_KEYS" "$BT_DEVICES") 388 389if [ -z "$MATCHING_PAIRS" ]; then 390 echo "No device matches found :(" 391 exit 1 392fi 393 394if ! select_device_key "$MATCHING_PAIRS" "$BT_HOST_CONFIG_PATH"; then 395 echo "No suitable device were selected, quitting..." 396 exit 0 397fi 398 399BT_DEVICE_HEX=$(echo "$SELECTED_KEY" | cut -d "_" -f 1) 400OLD_BT_DEVICE_MAC=$(echo "$SELECTED_KEY" | cut -d "_" -f 2) 401 402DEVICE_NAME=$(read_from_config "$BT_HOST_CONFIG_PATH/$OLD_BT_DEVICE_MAC/info" "[General]" "Name") 403echo "Found matching device: $BT_DEVICE_HEX => $OLD_BT_DEVICE_MAC ($DEVICE_NAME)" 404 405BT_DEVICE_MAC=$(hex_to_mac "$BT_DEVICE_HEX") 406 407BT_DEVICE_CONFIG_PATH="$BT_HOST_CONFIG_PATH/$BT_DEVICE_MAC" 408OLD_BT_DEVICE_CONFIG_PATH="$BT_HOST_CONFIG_PATH/$OLD_BT_DEVICE_MAC" 409 410if ! [ "$OLD_BT_DEVICE_MAC" = "$BT_DEVICE_MAC" ]; then 411 if [ -z "$DRY_RUN" ]; then 412 sudo cp -r "$OLD_BT_DEVICE_CONFIG_PATH" "$BT_DEVICE_CONFIG_PATH" 413 else 414 BT_DEVICE_CONFIG_PATH=$OLD_BT_DEVICE_CONFIG_PATH 415 fi 416fi 417 418INFO_FILE="$BT_DEVICE_CONFIG_PATH/info" 419 420DEVICE_REG_PATH="$KEYS_PATH\\$HOST_HEX\\$BT_DEVICE_HEX" 421 422ERAND=$(read_reg_value "$SYSTEM_PATH" "$DEVICE_REG_PATH\\ERand") 423LTK=$(read_reg_value "$SYSTEM_PATH" "$DEVICE_REG_PATH\\LTK") 424EDIV=$(read_reg_value "$SYSTEM_PATH" "$DEVICE_REG_PATH\\EDIV") 425IRK=$(read_reg_value "$SYSTEM_PATH" "$DEVICE_REG_PATH\\IRK") 426 427case "$DEVICE_NAME" in 428 # =========================================================================== 429 # Add your device here 430 # See https://wiki.archlinux.org/title/Bluetooth#Preparing_Bluetooth_5.1_Keys 431 # for instructions for your device 432 # =========================================================================== 433 434 "Logitech Pebble") 435 436 save_backup "$INFO_FILE" 437 write_to_config "$INFO_FILE" "[IdentityResolvingKey]" "Key" "$(echo "$IRK" | reverse_hex_bytes)" 438 write_to_config "$INFO_FILE" "[LongTermKey]" "Key" "$LTK" 439 write_to_config "$INFO_FILE" "[LongTermKey]" "EDiv" "$(echo "$EDIV" | reverse_hex_bytes | hex_to_dec)" 440 write_to_config "$INFO_FILE" "[LongTermKey]" "Rand" "$(echo "$ERAND" | reverse_hex_bytes | hex_to_dec)" 441 442 ;; 443 444 *) 445 echo "Unknown device: $DEVICE_NAME" 446 exit 1 447 ;; 448esac 449 450if [ -z "$DRY_RUN" ]; then 451 echo "Restarting bluetooth service..." 452 sudo systemctl restart bluetooth 453 454 if ! [ "$OLD_BT_DEVICE_CONFIG_PATH" = "$BT_DEVICE_CONFIG_PATH" ]; then 455 echo "Device configuration was moved from $OLD_BT_DEVICE_CONFIG_PATH to $BT_DEVICE_CONFIG_PATH." 456 printf "Do you want to remove old configuration? [Y/n]" 457 read -r input 458 if [ -z "$input" ] || [ "$input" = "y" ] || [ "$input" = "Y" ]; then 459 echo "Removing old configuration files..." 460 sudo rm -rf "$OLD_BT_DEVICE_CONFIG_PATH" 461 fi 462 fi 463fi