|
#!/bin/bash |
|
|
|
# Bluetooth Pairing Key Sync Script (Windows <-> Linux) |
|
# This script helps you sync Bluetooth device pairing keys between Windows and Linux |
|
# |
|
# Supported distributions: Ubuntu, Debian, Fedora, Arch, openSUSE, and derivatives |
|
# |
|
# Features: |
|
# - Automatic detection and selection of Bluetooth adapters and devices |
|
# - Automatic detection of Windows/NTFS partitions with mount status |
|
# - Automatic extraction of pairing keys from Windows registry |
|
# - Smart mounting (detects already-mounted partitions) |
|
# - Automatic backup of existing Bluetooth configuration |
|
|
|
set -e |
|
|
|
# Colors for output |
|
RED='\033[0;31m' |
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
BLUE='\033[0;34m' |
|
NC='\033[0m' # No Color |
|
|
|
# Function to print colored messages |
|
print_info() { |
|
echo -e "${BLUE}ℹ ${NC}$1" |
|
} |
|
|
|
print_success() { |
|
echo -e "${GREEN}✓${NC} $1" |
|
} |
|
|
|
print_warning() { |
|
echo -e "${YELLOW}⚠${NC} $1" |
|
} |
|
|
|
print_error() { |
|
echo -e "${RED}✗${NC} $1" |
|
} |
|
|
|
print_header() { |
|
echo -e "\n${BLUE}═══════════════════════════════════════════════════════════${NC}" |
|
echo -e "${BLUE} $1${NC}" |
|
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}\n" |
|
} |
|
|
|
# Function to pause and wait for user |
|
pause() { |
|
echo "" |
|
read -p "Press Enter to continue..." |
|
echo "" |
|
} |
|
|
|
# Function to cleanup mount if we created it |
|
cleanup_mount() { |
|
if [ "$SHOULD_UNMOUNT" = true ] && [ -n "$MOUNT_POINT" ]; then |
|
sudo umount "$MOUNT_POINT" 2>/dev/null || true |
|
sudo rmdir "$MOUNT_POINT" 2>/dev/null || true |
|
fi |
|
} |
|
|
|
# Function to detect Linux distribution |
|
detect_distro() { |
|
if [ -f /etc/os-release ]; then |
|
. /etc/os-release |
|
DISTRO_ID="$ID" |
|
DISTRO_LIKE="$ID_LIKE" |
|
elif [ -f /etc/lsb-release ]; then |
|
. /etc/lsb-release |
|
DISTRO_ID="$DISTRIB_ID" |
|
else |
|
DISTRO_ID="unknown" |
|
fi |
|
|
|
# Convert to lowercase |
|
DISTRO_ID=$(echo "$DISTRO_ID" | tr '[:upper:]' '[:lower:]') |
|
DISTRO_LIKE=$(echo "$DISTRO_LIKE" | tr '[:upper:]' '[:lower:]') |
|
} |
|
|
|
# Function to install chntpw based on distribution |
|
install_chntpw() { |
|
case "$DISTRO_ID" in |
|
fedora|rhel|centos|rocky|alma) |
|
print_info "Detected Red Hat-based distribution. Using dnf/yum..." |
|
if command -v dnf &> /dev/null; then |
|
sudo dnf install -y chntpw |
|
else |
|
sudo yum install -y chntpw |
|
fi |
|
;; |
|
ubuntu|debian|linuxmint|pop|elementary) |
|
print_info "Detected Debian-based distribution. Using apt..." |
|
sudo apt update |
|
sudo apt install -y chntpw |
|
;; |
|
arch|manjaro|endeavouros|garuda) |
|
print_info "Detected Arch-based distribution. Using pacman..." |
|
sudo pacman -Sy --noconfirm chntpw |
|
;; |
|
opensuse*|suse|sles) |
|
print_info "Detected SUSE-based distribution. Using zypper..." |
|
sudo zypper install -y chntpw |
|
;; |
|
gentoo) |
|
print_info "Detected Gentoo. Using emerge..." |
|
sudo emerge -av app-admin/chntpw |
|
;; |
|
*) |
|
# Check ID_LIKE for derivative distributions |
|
if [[ "$DISTRO_LIKE" == *"debian"* ]] || [[ "$DISTRO_LIKE" == *"ubuntu"* ]]; then |
|
print_info "Detected Debian-like distribution. Using apt..." |
|
sudo apt update |
|
sudo apt install -y chntpw |
|
elif [[ "$DISTRO_LIKE" == *"fedora"* ]] || [[ "$DISTRO_LIKE" == *"rhel"* ]]; then |
|
print_info "Detected Red Hat-like distribution. Using dnf/yum..." |
|
if command -v dnf &> /dev/null; then |
|
sudo dnf install -y chntpw |
|
else |
|
sudo yum install -y chntpw |
|
fi |
|
elif [[ "$DISTRO_LIKE" == *"arch"* ]]; then |
|
print_info "Detected Arch-like distribution. Using pacman..." |
|
sudo pacman -Sy --noconfirm chntpw |
|
elif [[ "$DISTRO_LIKE" == *"suse"* ]]; then |
|
print_info "Detected SUSE-like distribution. Using zypper..." |
|
sudo zypper install -y chntpw |
|
else |
|
print_error "Unsupported distribution: $DISTRO_ID" |
|
print_warning "Please install 'chntpw' manually using your package manager." |
|
return 1 |
|
fi |
|
;; |
|
esac |
|
} |
|
|
|
# Check if running as root |
|
if [ "$EUID" -eq 0 ]; then |
|
print_error "Please do NOT run this script as root. It will ask for sudo when needed." |
|
exit 1 |
|
fi |
|
|
|
clear |
|
echo -e "${GREEN}" |
|
echo "╔══════════════════════════════════════════════════════════════╗" |
|
echo "║ ║" |
|
echo "║ Bluetooth Pairing Key Sync Tool ║" |
|
echo "║ Windows <-> Linux ║" |
|
echo "║ ║" |
|
echo "╚══════════════════════════════════════════════════════════════╝" |
|
echo -e "${NC}" |
|
|
|
print_info "This script will help you sync Bluetooth pairing keys between Windows and Linux." |
|
print_info "This allows your Bluetooth devices to work seamlessly on both operating systems." |
|
|
|
# Detect distribution |
|
detect_distro |
|
print_info "Detected distribution: $DISTRO_ID" |
|
|
|
pause |
|
|
|
# ==================== PHASE 1: PREREQUISITES ==================== |
|
print_header "PHASE 1: Prerequisites & Setup" |
|
|
|
# Check if chntpw is installed |
|
print_info "Checking if 'chntpw' is installed..." |
|
if ! command -v chntpw &> /dev/null; then |
|
print_warning "'chntpw' is not installed." |
|
read -p "Would you like to install it now? (y/n): " install_choice |
|
if [[ $install_choice =~ ^[Yy]$ ]]; then |
|
print_info "Installing chntpw..." |
|
if install_chntpw; then |
|
print_success "'chntpw' installed successfully!" |
|
else |
|
print_error "Failed to install 'chntpw'. Please install it manually." |
|
exit 1 |
|
fi |
|
else |
|
print_error "Cannot proceed without 'chntpw'. Exiting." |
|
exit 1 |
|
fi |
|
else |
|
print_success "'chntpw' is already installed." |
|
fi |
|
|
|
pause |
|
|
|
# ==================== PHASE 2: IDENTIFY MAC ADDRESSES ==================== |
|
print_header "PHASE 2: Identify MAC Addresses" |
|
|
|
print_info "Detecting Bluetooth adapters..." |
|
echo "" |
|
|
|
# Get list of Bluetooth adapters |
|
mapfile -t ADAPTERS < <(bluetoothctl list | grep -oE '[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}') |
|
mapfile -t ADAPTER_NAMES < <(bluetoothctl list | sed 's/Controller [0-9A-F:]\+ //') |
|
|
|
if [ ${#ADAPTERS[@]} -eq 0 ]; then |
|
print_error "No Bluetooth adapters found!" |
|
exit 1 |
|
elif [ ${#ADAPTERS[@]} -eq 1 ]; then |
|
ADAPTER_MAC="${ADAPTERS[0]}" |
|
print_success "Found 1 Bluetooth adapter: $ADAPTER_MAC (${ADAPTER_NAMES[0]})" |
|
else |
|
bluetoothctl list |
|
echo "" |
|
print_info "Found ${#ADAPTERS[@]} Bluetooth adapters. Please select one:" |
|
for i in "${!ADAPTERS[@]}"; do |
|
echo " $((i+1)). ${ADAPTERS[$i]} - ${ADAPTER_NAMES[$i]}" |
|
done |
|
echo "" |
|
read -p "Select adapter (1-${#ADAPTERS[@]}): " adapter_choice |
|
ADAPTER_MAC="${ADAPTERS[$((adapter_choice-1))]}" |
|
fi |
|
|
|
ADAPTER_MAC_STRIPPED=$(echo "$ADAPTER_MAC" | tr -d ':' | tr '[:upper:]' '[:lower:]') |
|
|
|
print_success "Selected Adapter: $ADAPTER_MAC" |
|
print_info "Adapter MAC (stripped): $ADAPTER_MAC_STRIPPED" |
|
|
|
echo "" |
|
print_info "Detecting paired Bluetooth devices..." |
|
echo "" |
|
|
|
# Get list of Bluetooth devices |
|
mapfile -t DEVICES < <(bluetoothctl devices | grep -oE '[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}') |
|
mapfile -t DEVICE_NAMES < <(bluetoothctl devices | sed 's/Device [0-9A-F:]\+ //') |
|
|
|
if [ ${#DEVICES[@]} -eq 0 ]; then |
|
print_error "No paired Bluetooth devices found!" |
|
print_warning "Please pair your device in Linux first, then run this script again." |
|
exit 1 |
|
elif [ ${#DEVICES[@]} -eq 1 ]; then |
|
DEVICE_MAC="${DEVICES[0]}" |
|
print_success "Found 1 paired device: $DEVICE_MAC (${DEVICE_NAMES[0]})" |
|
else |
|
bluetoothctl devices |
|
echo "" |
|
print_info "Found ${#DEVICES[@]} paired devices. Please select one:" |
|
for i in "${!DEVICES[@]}"; do |
|
echo " $((i+1)). ${DEVICES[$i]} - ${DEVICE_NAMES[$i]}" |
|
done |
|
echo "" |
|
read -p "Select device (1-${#DEVICES[@]}): " device_choice |
|
DEVICE_MAC="${DEVICES[$((device_choice-1))]}" |
|
fi |
|
|
|
DEVICE_MAC_STRIPPED=$(echo "$DEVICE_MAC" | tr -d ':' | tr '[:upper:]' '[:lower:]') |
|
|
|
print_success "Selected Device: $DEVICE_MAC" |
|
print_info "Device MAC (stripped): $DEVICE_MAC_STRIPPED" |
|
|
|
pause |
|
|
|
# ==================== PHASE 3: MOUNT WINDOWS PARTITION ==================== |
|
print_header "PHASE 3: Mount Windows Partition" |
|
|
|
print_info "Detecting Windows/NTFS partitions..." |
|
echo "" |
|
|
|
# Detect NTFS partitions (Windows) |
|
mapfile -t PARTITIONS < <(lsblk -nrpo NAME,FSTYPE,SIZE,LABEL,MOUNTPOINT | grep -E 'ntfs|fuseblk' | awk '{print $1}') |
|
mapfile -t PARTITION_INFO < <(lsblk -nrpo NAME,FSTYPE,SIZE,LABEL,MOUNTPOINT | grep -E 'ntfs|fuseblk' | awk '{print $2" "$3" "$4" "$5}') |
|
|
|
if [ ${#PARTITIONS[@]} -eq 0 ]; then |
|
print_error "No NTFS (Windows) partitions found!" |
|
echo "" |
|
print_info "Showing all available partitions:" |
|
lsblk -f |
|
echo "" |
|
read -p "Enter your Windows partition manually (e.g., /dev/sda2): " WINDOWS_PARTITION |
|
if [ ! -b "$WINDOWS_PARTITION" ]; then |
|
print_error "Partition $WINDOWS_PARTITION does not exist!" |
|
exit 1 |
|
fi |
|
ALREADY_MOUNTED="" |
|
else |
|
print_info "Found ${#PARTITIONS[@]} Windows partition(s):" |
|
echo "" |
|
for i in "${!PARTITIONS[@]}"; do |
|
IFS=' ' read -r FSTYPE SIZE LABEL MOUNTPOINT <<< "${PARTITION_INFO[$i]}" |
|
MOUNTPOINT_DISPLAY="${MOUNTPOINT:-not mounted}" |
|
if [ -n "$LABEL" ]; then |
|
echo " $((i+1)). ${PARTITIONS[$i]} - $FSTYPE, $SIZE, Label: $LABEL [$MOUNTPOINT_DISPLAY]" |
|
else |
|
echo " $((i+1)). ${PARTITIONS[$i]} - $FSTYPE, $SIZE [$MOUNTPOINT_DISPLAY]" |
|
fi |
|
done |
|
echo "" |
|
|
|
if [ ${#PARTITIONS[@]} -eq 1 ]; then |
|
print_info "Only one Windows partition found. Selecting automatically..." |
|
partition_choice=1 |
|
else |
|
read -p "Select partition (1-${#PARTITIONS[@]}): " partition_choice |
|
fi |
|
|
|
WINDOWS_PARTITION="${PARTITIONS[$((partition_choice-1))]}" |
|
IFS=' ' read -r FSTYPE SIZE LABEL ALREADY_MOUNTED <<< "${PARTITION_INFO[$((partition_choice-1))]}" |
|
fi |
|
|
|
print_success "Selected partition: $WINDOWS_PARTITION" |
|
|
|
# Check if partition is already mounted |
|
if [ -n "$ALREADY_MOUNTED" ]; then |
|
print_success "Partition is already mounted at: $ALREADY_MOUNTED" |
|
MOUNT_POINT="$ALREADY_MOUNTED" |
|
SHOULD_UNMOUNT=false |
|
else |
|
MOUNT_POINT="/mnt/windows_bluetooth_temp" |
|
SHOULD_UNMOUNT=true |
|
|
|
print_info "Creating mount point at $MOUNT_POINT..." |
|
sudo mkdir -p "$MOUNT_POINT" |
|
|
|
print_info "Mounting Windows partition..." |
|
if sudo mount -o ro "$WINDOWS_PARTITION" "$MOUNT_POINT" 2>/dev/null; then |
|
print_success "Windows partition mounted successfully!" |
|
else |
|
print_warning "Failed to mount with default options. Trying with NTFS-3G..." |
|
if sudo mount -t ntfs-3g -o ro "$WINDOWS_PARTITION" "$MOUNT_POINT"; then |
|
print_success "Windows partition mounted successfully with NTFS-3G!" |
|
else |
|
print_error "Failed to mount Windows partition. Exiting." |
|
sudo rmdir "$MOUNT_POINT" 2>/dev/null |
|
exit 1 |
|
fi |
|
fi |
|
fi |
|
|
|
pause |
|
|
|
# ==================== PHASE 4: EXTRACT KEY FROM WINDOWS ==================== |
|
print_header "PHASE 4: Extract Bluetooth Key from Windows Registry" |
|
|
|
REGISTRY_PATH="$MOUNT_POINT/Windows/System32/config" |
|
|
|
if [ ! -f "$REGISTRY_PATH/SYSTEM" ]; then |
|
print_error "Windows SYSTEM registry file not found at $REGISTRY_PATH/SYSTEM" |
|
print_error "Please verify your Windows partition is correctly mounted." |
|
cleanup_mount |
|
exit 1 |
|
fi |
|
|
|
print_success "Found Windows SYSTEM registry file." |
|
echo "" |
|
print_info "Automatically extracting the Bluetooth pairing key from Windows..." |
|
|
|
cd "$REGISTRY_PATH" |
|
|
|
# Create a script for chntpw to execute |
|
CHNTPW_SCRIPT=$(mktemp) |
|
cat > "$CHNTPW_SCRIPT" << EOF |
|
cd ControlSet001\\Services\\BTHPORT\\Parameters\\Keys |
|
cd $ADAPTER_MAC_STRIPPED |
|
hex $DEVICE_MAC_STRIPPED |
|
q |
|
EOF |
|
|
|
print_info "Running automated registry extraction..." |
|
|
|
# Run chntpw and capture output |
|
CHNTPW_OUTPUT=$(sudo chntpw -e SYSTEM < "$CHNTPW_SCRIPT" 2>&1) |
|
rm "$CHNTPW_SCRIPT" |
|
|
|
# Extract the hex key from the output |
|
# The 'hex' command outputs 16 bytes in format: XX XX XX XX ... (32 hex chars total) |
|
LINKKEY=$(echo "$CHNTPW_OUTPUT" | grep -oE '([0-9A-Fa-f]{2} ){15}[0-9A-Fa-f]{2}' | head -1 | tr -d ' ' | tr '[:lower:]' '[:upper:]') |
|
|
|
# Validate LinkKey format |
|
if [[ ! $LINKKEY =~ ^[0-9A-F]{32}$ ]]; then |
|
print_error "Automatic extraction failed. The key was not found in the expected format." |
|
echo "" |
|
print_warning "Registry output for debugging:" |
|
echo "$CHNTPW_OUTPUT" |
|
echo "" |
|
print_info "Please enter the LinkKey manually." |
|
print_warning "Enter the hex values WITHOUT spaces (e.g., 1A2B3C4D5E6F708192A3B4C5D6E7F809)" |
|
echo "" |
|
print_info "If you need to extract it manually, run:" |
|
echo " cd $REGISTRY_PATH" |
|
echo " sudo chntpw -e SYSTEM" |
|
echo "" |
|
read -p "LinkKey (or press Ctrl+C to exit): " LINKKEY |
|
LINKKEY=$(echo "$LINKKEY" | tr -d ' ' | tr '[:lower:]' '[:upper:]') |
|
|
|
if [[ ! $LINKKEY =~ ^[0-9A-F]{32}$ ]]; then |
|
print_error "Invalid LinkKey format! It should be 32 hexadecimal characters." |
|
print_error "Example: 1A2B3C4D5E6F708192A3B4C5D6E7F809" |
|
cleanup_mount |
|
exit 1 |
|
fi |
|
fi |
|
|
|
print_success "LinkKey extracted successfully: $LINKKEY" |
|
|
|
pause |
|
|
|
# ==================== PHASE 5: INJECT KEY INTO FEDORA ==================== |
|
print_header "PHASE 5: Inject Key into Linux Bluetooth Configuration" |
|
|
|
LINUX_BT_PATH="/var/lib/bluetooth/$ADAPTER_MAC/$DEVICE_MAC" |
|
|
|
print_info "Checking if device configuration exists..." |
|
|
|
if ! sudo test -d "$LINUX_BT_PATH"; then |
|
print_warning "Device configuration directory not found at $LINUX_BT_PATH" |
|
print_info "You may need to pair the device in Linux first, then run this script again." |
|
read -p "Would you like to continue anyway and create the directory? (y/n): " create_choice |
|
if [[ ! $create_choice =~ ^[Yy]$ ]]; then |
|
print_error "Exiting. Please pair the device in Linux first." |
|
cleanup_mount |
|
exit 1 |
|
fi |
|
sudo mkdir -p "$LINUX_BT_PATH" |
|
fi |
|
|
|
INFO_FILE="$LINUX_BT_PATH/info" |
|
|
|
# Optional backup |
|
if sudo test -f "$INFO_FILE"; then |
|
read -p "A Bluetooth configuration file already exists. Create a backup? (y/n): " backup_choice |
|
if [[ $backup_choice =~ ^[Yy]$ ]]; then |
|
print_info "Creating backup of current info file..." |
|
sudo cp "$INFO_FILE" "$INFO_FILE.backup.$(date +%Y%m%d_%H%M%S)" |
|
print_success "Backup created." |
|
else |
|
print_warning "Skipping backup. Existing configuration will be modified." |
|
fi |
|
fi |
|
|
|
print_info "Updating LinkKey in $INFO_FILE..." |
|
|
|
# Create a temporary file with the updated info |
|
TEMP_FILE=$(mktemp) |
|
|
|
if sudo test -f "$INFO_FILE"; then |
|
# Read existing file and update or add LinkKey section |
|
sudo cat "$INFO_FILE" > "$TEMP_FILE" 2>/dev/null || touch "$TEMP_FILE" |
|
|
|
# Check if [LinkKey] section exists |
|
if grep -q "^\[LinkKey\]" "$TEMP_FILE"; then |
|
# Update existing LinkKey |
|
sed -i "/^\[LinkKey\]/,/^\[/ s/^Key=.*/Key=$LINKKEY/" "$TEMP_FILE" |
|
print_info "Updated existing [LinkKey] section." |
|
else |
|
# Add new LinkKey section |
|
echo "" >> "$TEMP_FILE" |
|
echo "[LinkKey]" >> "$TEMP_FILE" |
|
echo "Key=$LINKKEY" >> "$TEMP_FILE" |
|
echo "Type=4" >> "$TEMP_FILE" |
|
echo "PINLength=0" >> "$TEMP_FILE" |
|
print_info "Added new [LinkKey] section." |
|
fi |
|
else |
|
# Create new info file |
|
echo "[LinkKey]" > "$TEMP_FILE" |
|
echo "Key=$LINKKEY" >> "$TEMP_FILE" |
|
echo "Type=4" >> "$TEMP_FILE" |
|
echo "PINLength=0" >> "$TEMP_FILE" |
|
print_info "Created new info file with LinkKey." |
|
fi |
|
|
|
# Copy temp file to actual location |
|
sudo cp "$TEMP_FILE" "$INFO_FILE" |
|
sudo chown root:root "$INFO_FILE" |
|
sudo chmod 600 "$INFO_FILE" |
|
rm "$TEMP_FILE" |
|
|
|
print_success "LinkKey successfully updated in Linux Bluetooth configuration!" |
|
|
|
pause |
|
|
|
# ==================== PHASE 6: CLEANUP AND RESTART ==================== |
|
print_header "PHASE 6: Cleanup and Restart Bluetooth Service" |
|
|
|
if [ "$SHOULD_UNMOUNT" = true ]; then |
|
print_info "Unmounting Windows partition..." |
|
sudo umount "$MOUNT_POINT" |
|
sudo rmdir "$MOUNT_POINT" |
|
print_success "Windows partition unmounted and mount point removed." |
|
else |
|
print_info "Partition was already mounted. Leaving it mounted at $MOUNT_POINT" |
|
fi |
|
|
|
print_info "Restarting Bluetooth service..." |
|
sudo systemctl restart bluetooth |
|
print_success "Bluetooth service restarted!" |
|
|
|
echo "" |
|
print_header "✓ COMPLETED SUCCESSFULLY!" |
|
|
|
echo "" |
|
print_success "Your Bluetooth device should now work on both Windows and Linux!" |
|
print_info "Device: $DEVICE_MAC" |
|
print_info "Adapter: $ADAPTER_MAC" |
|
print_info "LinkKey: $LINKKEY" |
|
echo "" |
|
print_warning "If the device doesn't connect automatically, try:" |
|
echo " 1. Turn off the Bluetooth device" |
|
echo " 2. Turn it back on" |
|
echo " 3. It should connect automatically" |
|
echo "" |
|
if [[ $backup_choice =~ ^[Yy]$ ]]; then |
|
print_info "A backup of the original configuration was saved at:" |
|
print_info " $INFO_FILE.backup.*" |
|
echo "" |
|
fi |
|
|
|
exit 0 |
Only tested on fedora for now, but should work on other linux system without issues, feel free to comment there if you tested the script on another Linux distro