btkey-sync 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- btkey_sync-0.2.0/PKG-INFO +141 -0
- btkey_sync-0.2.0/README.md +129 -0
- btkey_sync-0.2.0/actions.py +146 -0
- btkey_sync-0.2.0/actions_classic.py +173 -0
- btkey_sync-0.2.0/actions_classic_extra.py +187 -0
- btkey_sync-0.2.0/actions_clone.py +166 -0
- btkey_sync-0.2.0/actions_clone_loader.py +103 -0
- btkey_sync-0.2.0/actions_common.py +184 -0
- btkey_sync-0.2.0/actions_help.py +124 -0
- btkey_sync-0.2.0/actions_remove.py +57 -0
- btkey_sync-0.2.0/actions_show.py +48 -0
- btkey_sync-0.2.0/actions_verify.py +177 -0
- btkey_sync-0.2.0/actions_windows.py +70 -0
- btkey_sync-0.2.0/backends/__init__.py +5 -0
- btkey_sync-0.2.0/backends/base.py +74 -0
- btkey_sync-0.2.0/backends/linux_backend.py +194 -0
- btkey_sync-0.2.0/backends/linux_classic.py +116 -0
- btkey_sync-0.2.0/backends/linux_common.py +101 -0
- btkey_sync-0.2.0/backends/offline_windows.py +196 -0
- btkey_sync-0.2.0/backends/offline_windows_classic.py +63 -0
- btkey_sync-0.2.0/backends/windows_backend.py +191 -0
- btkey_sync-0.2.0/backends/windows_common.py +182 -0
- btkey_sync-0.2.0/btkey_sync.egg-info/PKG-INFO +141 -0
- btkey_sync-0.2.0/btkey_sync.egg-info/SOURCES.txt +42 -0
- btkey_sync-0.2.0/btkey_sync.egg-info/dependency_links.txt +1 -0
- btkey_sync-0.2.0/btkey_sync.egg-info/entry_points.txt +2 -0
- btkey_sync-0.2.0/btkey_sync.egg-info/requires.txt +3 -0
- btkey_sync-0.2.0/btkey_sync.egg-info/top_level.txt +19 -0
- btkey_sync-0.2.0/cli.py +157 -0
- btkey_sync-0.2.0/exporters/__init__.py +7 -0
- btkey_sync-0.2.0/exporters/classic_exporter.py +59 -0
- btkey_sync-0.2.0/exporters/reg_exporter.py +58 -0
- btkey_sync-0.2.0/importers/__init__.py +7 -0
- btkey_sync-0.2.0/importers/classic_importer.py +37 -0
- btkey_sync-0.2.0/importers/reg_importer.py +21 -0
- btkey_sync-0.2.0/models.py +187 -0
- btkey_sync-0.2.0/platform_detect.py +106 -0
- btkey_sync-0.2.0/pyproject.toml +53 -0
- btkey_sync-0.2.0/setup.cfg +4 -0
- btkey_sync-0.2.0/storage.py +176 -0
- btkey_sync-0.2.0/tests/test_parsing.py +75 -0
- btkey_sync-0.2.0/tests/test_parsing_ble.py +121 -0
- btkey_sync-0.2.0/tests/test_parsing_classic.py +156 -0
- btkey_sync-0.2.0/tui_helpers.py +149 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: btkey-sync
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Synchronize Bluetooth LE and Classic bond keys between Windows and Linux in dual-boot setups
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/netssv/btkey_sync
|
|
7
|
+
Project-URL: Repository, https://github.com/netssv/btkey_sync
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# btkey-sync
|
|
14
|
+
|
|
15
|
+
[](https://pypi.org/project/btkey-sync/)
|
|
16
|
+
[](https://www.python.org/downloads/)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
|
|
19
|
+
Synchronize Bluetooth pairing and cryptographic bonding keys (**BLE** and **Classic BR/EDR**) between operating systems in dual-boot setups (Windows & Linux) or across partitions and physical machines — **without having to re-pair devices on every reboot**.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Why This Exists
|
|
24
|
+
|
|
25
|
+
Bluetooth peripherals (keyboards, mice, headsets) generate cryptographic keys during pairing. Dual-booting causes devices to fail on OS switches because each OS stores independent keys:
|
|
26
|
+
|
|
27
|
+
| Feature | Windows | Linux (BlueZ) |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| **Storage Location** | `HKLM\SYSTEM\...\BTHPORT\Parameters\Keys` | `/var/lib/bluetooth/<adapter>/<device>/info` |
|
|
30
|
+
| **Required Privileges** | SYSTEM account (automated via scheduled task) | `root` (`sudo`) |
|
|
31
|
+
| **Numeric Formats** | Hexadecimal (`dword`, `qword`, `hex`) | Decimal (`EDiv`, `Rand`) |
|
|
32
|
+
| **Daemon Reload** | Immediate | Requires `systemctl restart bluetooth` |
|
|
33
|
+
|
|
34
|
+
`btkey-sync` automates extracting, converting, importing, and validating keys across systems.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
### From PyPI
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install btkey-sync
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
*(Or via `pipx install btkey-sync` for isolated CLI environments)*
|
|
47
|
+
|
|
48
|
+
### From Source
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/netssv/btkey_sync.git
|
|
52
|
+
cd btkey_sync
|
|
53
|
+
pip install .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Optional Dependencies (Linux Offline Windows Reading)
|
|
57
|
+
To clone directly from a mounted Windows partition on Linux without booting Windows:
|
|
58
|
+
```bash
|
|
59
|
+
sudo apt install chntpw bluetooth # Debian/Ubuntu/Mint
|
|
60
|
+
sudo dnf install chntpw bluez # Fedora/RHEL
|
|
61
|
+
sudo pacman -S chntpw bluez-utils # Arch Linux
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### 1. Interactive TUI Menu (Recommended)
|
|
69
|
+
|
|
70
|
+
Run the command with elevated privileges:
|
|
71
|
+
|
|
72
|
+
**Linux:**
|
|
73
|
+
```bash
|
|
74
|
+
sudo btkey-sync
|
|
75
|
+
```
|
|
76
|
+
*(If run as normal user, `btkey-sync` will offer to re-launch with `sudo` automatically).*
|
|
77
|
+
|
|
78
|
+
**Windows (PowerShell / Command Prompt as Administrator):**
|
|
79
|
+
```powershell
|
|
80
|
+
btkey-sync
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The menu provides:
|
|
84
|
+
- **[1] Clone Device**: Directly sync BLE, Classic, or Dual-Mode devices from a mounted Windows partition (`/mnt/windows`) or local installation.
|
|
85
|
+
- **[2] Export Key to File**: Export device pairing data to `.reg` + `.json` sidecar files.
|
|
86
|
+
- **[3] Import Key from File**: Load a `.reg` file and inject keys into the host Bluetooth stack.
|
|
87
|
+
- **[4] Force Push Sync**: Clear daemon caches, restart BlueZ, set trust, and reconnect.
|
|
88
|
+
- **[5] Remove Device**: Delete bonding for a device cleanly with automatic backup.
|
|
89
|
+
- **[6] Show Keys**: Inspect all locally stored bonding keys and parameters.
|
|
90
|
+
- **[7] Device Help & Advice**: Guidance on BLE vs. Classic single-slot devices.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### 2. Direct CLI Flags
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Non-interactive import of an exported .reg file
|
|
98
|
+
sudo btkey-sync --import exports/aabbccddeeff__windows__20260829.reg
|
|
99
|
+
|
|
100
|
+
# Force reload BlueZ stack, trust, and reconnect to a specific MAC
|
|
101
|
+
sudo btkey-sync --push-sync AA:BB:CC:DD:EE:FF
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Dual-Boot Migration Workflows
|
|
107
|
+
|
|
108
|
+
### Method A: Offline Cloning on Linux (Fastest)
|
|
109
|
+
1. Pair device in **Windows**.
|
|
110
|
+
2. Boot into **Linux** and mount your Windows partition (e.g. at `/mnt/windows`).
|
|
111
|
+
3. Run `sudo btkey-sync` and select **Option 1 (Clone Device)**.
|
|
112
|
+
4. Select the detected Windows partition. `btkey-sync` reads the keys offline and sets up BlueZ automatically.
|
|
113
|
+
|
|
114
|
+
### Method B: Export / Import via `.reg` File
|
|
115
|
+
1. Pair device on **Source OS** (e.g. Windows).
|
|
116
|
+
2. Run `btkey-sync` → **Option 2 (Export)** to generate a `.reg` file in `exports/`.
|
|
117
|
+
3. Copy the `.reg` file to the **Destination OS** (via USB drive or shared partition).
|
|
118
|
+
4. On destination, run `sudo btkey-sync` → **Option 3 (Import)**.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Documentation Wiki
|
|
123
|
+
|
|
124
|
+
Detailed guides are available in the [docs/](docs/index.md) directory:
|
|
125
|
+
- [Architecture & Design](docs/architecture.md)
|
|
126
|
+
- [BLE Synchronization (LTK/EDIV/ERand/IRK)](docs/ble_sync.md)
|
|
127
|
+
- [Classic BR/EDR Sync (Link Keys)](docs/classic_sync.md)
|
|
128
|
+
- [Dual-Boot Cloning Guide](docs/device_cloning.md)
|
|
129
|
+
- [Troubleshooting & Diagnostics](docs/troubleshooting.md)
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Testing
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python3 tests/test_parsing.py
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# btkey-sync
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/btkey-sync/)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Synchronize Bluetooth pairing and cryptographic bonding keys (**BLE** and **Classic BR/EDR**) between operating systems in dual-boot setups (Windows & Linux) or across partitions and physical machines — **without having to re-pair devices on every reboot**.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why This Exists
|
|
12
|
+
|
|
13
|
+
Bluetooth peripherals (keyboards, mice, headsets) generate cryptographic keys during pairing. Dual-booting causes devices to fail on OS switches because each OS stores independent keys:
|
|
14
|
+
|
|
15
|
+
| Feature | Windows | Linux (BlueZ) |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| **Storage Location** | `HKLM\SYSTEM\...\BTHPORT\Parameters\Keys` | `/var/lib/bluetooth/<adapter>/<device>/info` |
|
|
18
|
+
| **Required Privileges** | SYSTEM account (automated via scheduled task) | `root` (`sudo`) |
|
|
19
|
+
| **Numeric Formats** | Hexadecimal (`dword`, `qword`, `hex`) | Decimal (`EDiv`, `Rand`) |
|
|
20
|
+
| **Daemon Reload** | Immediate | Requires `systemctl restart bluetooth` |
|
|
21
|
+
|
|
22
|
+
`btkey-sync` automates extracting, converting, importing, and validating keys across systems.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
### From PyPI
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install btkey-sync
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
*(Or via `pipx install btkey-sync` for isolated CLI environments)*
|
|
35
|
+
|
|
36
|
+
### From Source
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/netssv/btkey_sync.git
|
|
40
|
+
cd btkey_sync
|
|
41
|
+
pip install .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Optional Dependencies (Linux Offline Windows Reading)
|
|
45
|
+
To clone directly from a mounted Windows partition on Linux without booting Windows:
|
|
46
|
+
```bash
|
|
47
|
+
sudo apt install chntpw bluetooth # Debian/Ubuntu/Mint
|
|
48
|
+
sudo dnf install chntpw bluez # Fedora/RHEL
|
|
49
|
+
sudo pacman -S chntpw bluez-utils # Arch Linux
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### 1. Interactive TUI Menu (Recommended)
|
|
57
|
+
|
|
58
|
+
Run the command with elevated privileges:
|
|
59
|
+
|
|
60
|
+
**Linux:**
|
|
61
|
+
```bash
|
|
62
|
+
sudo btkey-sync
|
|
63
|
+
```
|
|
64
|
+
*(If run as normal user, `btkey-sync` will offer to re-launch with `sudo` automatically).*
|
|
65
|
+
|
|
66
|
+
**Windows (PowerShell / Command Prompt as Administrator):**
|
|
67
|
+
```powershell
|
|
68
|
+
btkey-sync
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The menu provides:
|
|
72
|
+
- **[1] Clone Device**: Directly sync BLE, Classic, or Dual-Mode devices from a mounted Windows partition (`/mnt/windows`) or local installation.
|
|
73
|
+
- **[2] Export Key to File**: Export device pairing data to `.reg` + `.json` sidecar files.
|
|
74
|
+
- **[3] Import Key from File**: Load a `.reg` file and inject keys into the host Bluetooth stack.
|
|
75
|
+
- **[4] Force Push Sync**: Clear daemon caches, restart BlueZ, set trust, and reconnect.
|
|
76
|
+
- **[5] Remove Device**: Delete bonding for a device cleanly with automatic backup.
|
|
77
|
+
- **[6] Show Keys**: Inspect all locally stored bonding keys and parameters.
|
|
78
|
+
- **[7] Device Help & Advice**: Guidance on BLE vs. Classic single-slot devices.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### 2. Direct CLI Flags
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Non-interactive import of an exported .reg file
|
|
86
|
+
sudo btkey-sync --import exports/aabbccddeeff__windows__20260829.reg
|
|
87
|
+
|
|
88
|
+
# Force reload BlueZ stack, trust, and reconnect to a specific MAC
|
|
89
|
+
sudo btkey-sync --push-sync AA:BB:CC:DD:EE:FF
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Dual-Boot Migration Workflows
|
|
95
|
+
|
|
96
|
+
### Method A: Offline Cloning on Linux (Fastest)
|
|
97
|
+
1. Pair device in **Windows**.
|
|
98
|
+
2. Boot into **Linux** and mount your Windows partition (e.g. at `/mnt/windows`).
|
|
99
|
+
3. Run `sudo btkey-sync` and select **Option 1 (Clone Device)**.
|
|
100
|
+
4. Select the detected Windows partition. `btkey-sync` reads the keys offline and sets up BlueZ automatically.
|
|
101
|
+
|
|
102
|
+
### Method B: Export / Import via `.reg` File
|
|
103
|
+
1. Pair device on **Source OS** (e.g. Windows).
|
|
104
|
+
2. Run `btkey-sync` → **Option 2 (Export)** to generate a `.reg` file in `exports/`.
|
|
105
|
+
3. Copy the `.reg` file to the **Destination OS** (via USB drive or shared partition).
|
|
106
|
+
4. On destination, run `sudo btkey-sync` → **Option 3 (Import)**.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Documentation Wiki
|
|
111
|
+
|
|
112
|
+
Detailed guides are available in the [docs/](docs/index.md) directory:
|
|
113
|
+
- [Architecture & Design](docs/architecture.md)
|
|
114
|
+
- [BLE Synchronization (LTK/EDIV/ERand/IRK)](docs/ble_sync.md)
|
|
115
|
+
- [Classic BR/EDR Sync (Link Keys)](docs/classic_sync.md)
|
|
116
|
+
- [Dual-Boot Cloning Guide](docs/device_cloning.md)
|
|
117
|
+
- [Troubleshooting & Diagnostics](docs/troubleshooting.md)
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Testing
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
python3 tests/test_parsing.py
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from exporters import export_bond_key
|
|
4
|
+
from importers import load_bond_from_reg_file
|
|
5
|
+
from platform_detect import OSKind
|
|
6
|
+
from storage import ensure_exports_dir
|
|
7
|
+
import tui_helpers as tui
|
|
8
|
+
import actions_common as common
|
|
9
|
+
from actions_windows import run_offline_windows_export
|
|
10
|
+
from actions_verify import verify_and_connect, run_verify_flow # noqa: F401
|
|
11
|
+
from actions_remove import run_remove_flow # noqa: F401
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _ensure_device_name(bond) -> None:
|
|
15
|
+
"""If bond has no name, prompt the user to enter one interactively."""
|
|
16
|
+
if not bond.device_name:
|
|
17
|
+
name = tui.ask("Device has no name — enter a label (Enter to skip)", default="")
|
|
18
|
+
if name.strip():
|
|
19
|
+
bond.device_name = name.strip()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_export_flow() -> None:
|
|
23
|
+
tui.header("Select & Extract — Step 1: System detection")
|
|
24
|
+
env, backend = common.detect_and_validate()
|
|
25
|
+
tui.header("Select & Extract — Step 2: Choose extraction source")
|
|
26
|
+
source_os, win_mount = common.prompt_source_os(env.os_kind)
|
|
27
|
+
if source_os == OSKind.WINDOWS and env.os_kind == OSKind.LINUX:
|
|
28
|
+
run_offline_windows_export(win_mount, export_bond_key, ensure_exports_dir)
|
|
29
|
+
return
|
|
30
|
+
tui.header("Select & Extract — Step 3: Choose a device")
|
|
31
|
+
with tui.Spinner("Scanning bonded BLE devices…"):
|
|
32
|
+
devices = backend.list_devices()
|
|
33
|
+
chosen = common.prompt_select_device(devices) if devices else None
|
|
34
|
+
if not chosen:
|
|
35
|
+
tui.info("No devices or cancelled.")
|
|
36
|
+
return
|
|
37
|
+
tui.header("Select & Extract — Step 4: Extract and save")
|
|
38
|
+
with tui.Spinner(f"Extracting bonding for {chosen.device_mac}…"):
|
|
39
|
+
bond = backend.extract_bond_key(chosen)
|
|
40
|
+
bond.source_os = source_os.value
|
|
41
|
+
common.print_bond_summary(bond)
|
|
42
|
+
_ensure_device_name(bond)
|
|
43
|
+
with tui.Spinner("Writing export files…"):
|
|
44
|
+
exports_dir = ensure_exports_dir()
|
|
45
|
+
reg_path = export_bond_key(bond)
|
|
46
|
+
print()
|
|
47
|
+
tui.ok(f"Exports folder : {tui.dim(str(exports_dir))}")
|
|
48
|
+
tui.ok(f"File generated : {tui.bold(reg_path.name)}")
|
|
49
|
+
tui.ok(f"Metadata (JSON): {tui.bold(reg_path.with_suffix('.json').name)}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _pick_adapter(backend, default_adapter: str | None = None) -> str | None:
|
|
53
|
+
adapters = backend._list_adapters()
|
|
54
|
+
if not adapters:
|
|
55
|
+
tui.err("No Bluetooth adapters found in /var/lib/bluetooth.")
|
|
56
|
+
return None
|
|
57
|
+
if len(adapters) == 1:
|
|
58
|
+
return adapters[0]
|
|
59
|
+
print(f"\n {tui.bold('Multiple Bluetooth adapters found on this system:')}\n")
|
|
60
|
+
for i, a in enumerate(adapters, 1):
|
|
61
|
+
mark = tui.green(" (matches file)") if a == default_adapter else ""
|
|
62
|
+
print(f" {tui.cyan(f'[{i}]')} {a}{mark}")
|
|
63
|
+
print()
|
|
64
|
+
while True:
|
|
65
|
+
raw = tui.ask("Adapter index")
|
|
66
|
+
if raw.isdigit():
|
|
67
|
+
idx = int(raw)
|
|
68
|
+
if 1 <= idx <= len(adapters):
|
|
69
|
+
return adapters[idx - 1]
|
|
70
|
+
tui.warn("Invalid index, try again.")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_import_flow(reg_file_arg: str | None = None) -> None:
|
|
74
|
+
tui.header("Import & Reconnect — Step 1: System detection")
|
|
75
|
+
env, backend = common.detect_and_validate()
|
|
76
|
+
if env.os_kind != OSKind.LINUX:
|
|
77
|
+
tui.err("Import is only supported when running on Linux (target system).")
|
|
78
|
+
return
|
|
79
|
+
tui.header("Import & Reconnect — Step 2: Select file")
|
|
80
|
+
reg_path = Path(reg_file_arg) if reg_file_arg else common.prompt_select_export_file()
|
|
81
|
+
if not reg_path or not reg_path.exists():
|
|
82
|
+
tui.err(f"File not found: {reg_path}")
|
|
83
|
+
return
|
|
84
|
+
tui.ok(f"File: {tui.bold(reg_path.name)}")
|
|
85
|
+
tui.header("Import & Reconnect — Step 3: Inspect key")
|
|
86
|
+
try:
|
|
87
|
+
bond = load_bond_from_reg_file(reg_path)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
tui.err(f"Failed to parse {reg_path.name}: {e}")
|
|
90
|
+
return
|
|
91
|
+
common.print_bond_summary(bond)
|
|
92
|
+
tui.header("Import & Reconnect — Step 4: Map local device")
|
|
93
|
+
chosen_adapter = _pick_adapter(backend, default_adapter=bond.adapter_mac)
|
|
94
|
+
if not chosen_adapter:
|
|
95
|
+
return
|
|
96
|
+
bond.adapter_mac = chosen_adapter
|
|
97
|
+
with tui.Spinner("Scanning existing devices in BlueZ…"):
|
|
98
|
+
existing = backend.list_devices()
|
|
99
|
+
local_match = next((d for d in existing if d.device_mac.upper() == bond.device_mac.upper()), None)
|
|
100
|
+
ltk_match = None
|
|
101
|
+
if not local_match:
|
|
102
|
+
for d in existing:
|
|
103
|
+
if d.has_ltk:
|
|
104
|
+
try:
|
|
105
|
+
local_bond = backend.extract_bond_key(d)
|
|
106
|
+
if (local_bond.ltk_hex.upper() == bond.ltk_hex.upper() or
|
|
107
|
+
(bond.irk_hex and local_bond.irk_hex and
|
|
108
|
+
local_bond.irk_hex.upper() == bond.irk_hex.upper())):
|
|
109
|
+
ltk_match = d
|
|
110
|
+
if not bond.device_name and local_bond.device_name:
|
|
111
|
+
bond.device_name = local_bond.device_name
|
|
112
|
+
break
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
source_device_mac = None
|
|
116
|
+
if ltk_match:
|
|
117
|
+
tui.info(f"Matched physical device under a different MAC address: {ltk_match.device_mac}")
|
|
118
|
+
q = f"Migrate profile/cache from {ltk_match.device_mac} to {bond.device_mac}? [Y/n]"
|
|
119
|
+
if tui.ask(q, default="Y").upper() == "Y":
|
|
120
|
+
source_device_mac = ltk_match.device_mac
|
|
121
|
+
elif local_match:
|
|
122
|
+
tui.info("Device found locally but has no LTK.")
|
|
123
|
+
else:
|
|
124
|
+
tui.info("Device not paired locally yet.")
|
|
125
|
+
|
|
126
|
+
print()
|
|
127
|
+
target_mac = tui.ask("Destination MAC (Enter to keep original)", default=bond.device_mac)
|
|
128
|
+
target_mac = target_mac if target_mac != bond.device_mac else None
|
|
129
|
+
|
|
130
|
+
tui.header("Import — Step 5: Write and restart Bluetooth")
|
|
131
|
+
with tui.Spinner("Writing bonding to disk…"):
|
|
132
|
+
written_path = backend.import_bond_key(
|
|
133
|
+
bond, target_device_mac=target_mac, source_device_mac=source_device_mac
|
|
134
|
+
)
|
|
135
|
+
tui.ok(f"Written to: {tui.dim(str(written_path))}")
|
|
136
|
+
tui.info("Restarting Bluetooth service to apply changes. This may take a moment…")
|
|
137
|
+
with tui.Spinner("Restarting Bluetooth service…"):
|
|
138
|
+
backend.restart_bluetooth_stack()
|
|
139
|
+
tui.ok("Bluetooth service restarted.")
|
|
140
|
+
verify_and_connect(backend, target_mac or bond.device_mac)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def run_clone_flow() -> None:
|
|
144
|
+
"""Delegates to unified clone flow for BLE, Classic, and Dual-Mode."""
|
|
145
|
+
from actions_clone import run_unified_clone_flow
|
|
146
|
+
run_unified_clone_flow()
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
actions_classic.py
|
|
3
|
+
|
|
4
|
+
Top-level orchestration for Classic (BR/EDR) export and import flows.
|
|
5
|
+
|
|
6
|
+
Implements RC5, RC6, RC7, RC9, RC11 from CLASSIC_SUPPORT.agent.md.
|
|
7
|
+
Each step of the bonding lifecycle is sequenced explicitly here — the backend
|
|
8
|
+
exposes discrete methods (RC9) and this layer calls them in order.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import tui_helpers as tui
|
|
16
|
+
import actions_common as common
|
|
17
|
+
from exporters.classic_exporter import export_classic_bond
|
|
18
|
+
from importers.classic_importer import load_classic_bond_from_reg_file
|
|
19
|
+
from platform_detect import OSKind
|
|
20
|
+
from actions_verify import verify_and_connect
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _filter_classic_devices(backend, devices):
|
|
24
|
+
"""Return only devices whose info file has a [LinkKey] section."""
|
|
25
|
+
import configparser
|
|
26
|
+
classic = []
|
|
27
|
+
for d in devices:
|
|
28
|
+
info_path = Path(d.raw_source_path)
|
|
29
|
+
if not info_path.exists():
|
|
30
|
+
continue
|
|
31
|
+
cfg = configparser.ConfigParser()
|
|
32
|
+
try:
|
|
33
|
+
cfg.read(info_path)
|
|
34
|
+
if "LinkKey" in cfg:
|
|
35
|
+
classic.append(d)
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
return classic
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_classic_export_flow() -> None:
|
|
42
|
+
"""
|
|
43
|
+
Full Classic export flow:
|
|
44
|
+
pre-flight → pick device → extract bond → write .reg + .json.
|
|
45
|
+
"""
|
|
46
|
+
tui.header("Classic Export — Step 1: System detection")
|
|
47
|
+
env, backend = common.detect_and_validate()
|
|
48
|
+
|
|
49
|
+
tui.header("Classic Export — Pre-flight Check")
|
|
50
|
+
if not tui.warn_classic_preflight():
|
|
51
|
+
tui.info("Aborted — pre-flight not acknowledged.")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
tui.header("Classic Export — Step 2: Choose device")
|
|
55
|
+
with tui.Spinner("Scanning bonded devices…"):
|
|
56
|
+
all_devices = backend.list_devices()
|
|
57
|
+
|
|
58
|
+
classic_devices = _filter_classic_devices(backend, all_devices)
|
|
59
|
+
if not classic_devices:
|
|
60
|
+
tui.warn("No Classic (BR/EDR) bonded devices found.")
|
|
61
|
+
tui.info("Classic devices have a [LinkKey] section in their BlueZ info file.")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
chosen = common.prompt_select_device(classic_devices)
|
|
65
|
+
if not chosen:
|
|
66
|
+
tui.info("Cancelled.")
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
tui.header("Classic Export — Step 3: Extract and save")
|
|
70
|
+
with tui.Spinner(f"Extracting Classic bond for {chosen.device_mac}…"):
|
|
71
|
+
bond = backend.extract_classic_bond(chosen)
|
|
72
|
+
|
|
73
|
+
tui.info(bond.summary())
|
|
74
|
+
|
|
75
|
+
with tui.Spinner("Writing export files…"):
|
|
76
|
+
reg_path = export_classic_bond(bond)
|
|
77
|
+
|
|
78
|
+
tui.ok(f"Exported to: {tui.bold(reg_path.name)}")
|
|
79
|
+
tui.warn("Keep the device powered ON. Do NOT power-cycle it before importing.")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def run_classic_import_flow(reg_file_arg: str | None = None) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Full Classic import flow (RC5, RC9, RC11):
|
|
85
|
+
pre-flight → load .reg → check active link → confirm remove →
|
|
86
|
+
stop BT → remove → write info → set perms → start BT → post-import warnings.
|
|
87
|
+
"""
|
|
88
|
+
tui.header("Classic Import — Step 1: System detection")
|
|
89
|
+
env, backend = common.detect_and_validate()
|
|
90
|
+
|
|
91
|
+
if env.os_kind != OSKind.LINUX:
|
|
92
|
+
tui.err("Classic import (write destination) is only supported on Linux.")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
tui.header("Classic Import — Pre-flight Check")
|
|
96
|
+
if not tui.warn_classic_preflight():
|
|
97
|
+
tui.info("Aborted — pre-flight not acknowledged.")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
tui.header("Classic Import — Step 2: Load bond key")
|
|
101
|
+
reg_path = Path(reg_file_arg) if reg_file_arg else common.prompt_select_export_file()
|
|
102
|
+
if not reg_path:
|
|
103
|
+
tui.info("Cancelled.")
|
|
104
|
+
return
|
|
105
|
+
if not reg_path.exists():
|
|
106
|
+
tui.err(f"File not found: {reg_path}")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
with tui.Spinner(f"Parsing {reg_path.name}…"):
|
|
110
|
+
bond = load_classic_bond_from_reg_file(reg_path)
|
|
111
|
+
|
|
112
|
+
tui.info(bond.summary())
|
|
113
|
+
|
|
114
|
+
if not bond.device_name:
|
|
115
|
+
name = tui.ask("Device has no name — enter a label (Enter to skip)", default="")
|
|
116
|
+
if name.strip():
|
|
117
|
+
bond.device_name = name.strip()
|
|
118
|
+
|
|
119
|
+
tui.header("Classic Import — Step 3: Pre-write checks")
|
|
120
|
+
|
|
121
|
+
# RC11: check for active connection before touching bonding state
|
|
122
|
+
with tui.Spinner(f"Checking if {bond.device_mac} is currently connected…"):
|
|
123
|
+
is_connected = backend.check_active_link(bond.device_mac)
|
|
124
|
+
|
|
125
|
+
if is_connected:
|
|
126
|
+
tui.warn(f"Device {bond.device_mac} is currently connected!")
|
|
127
|
+
tui.info("Writing bonding state over an active connection may leave BlueZ inconsistent.")
|
|
128
|
+
confirm = tui.ask("Proceed anyway? [y/N]", default="N")
|
|
129
|
+
if confirm.strip().lower() != "y":
|
|
130
|
+
tui.info("Aborted. Disconnect the device first, then re-run.")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# RC5: remove existing bonding folder with explicit user confirmation
|
|
134
|
+
target_dir = (backend.bluetooth_dir / bond.adapter_mac / bond.device_mac)
|
|
135
|
+
if target_dir.exists():
|
|
136
|
+
tui.warn(f"Existing bonding folder found: {target_dir}")
|
|
137
|
+
tui.info("It must be removed before writing the new Classic bond (RC5).")
|
|
138
|
+
tui.info("A timestamped backup will be kept automatically.")
|
|
139
|
+
confirm_rm = tui.ask("Remove existing bonding? [y/N]", default="N")
|
|
140
|
+
if confirm_rm.strip().lower() != "y":
|
|
141
|
+
tui.info("Aborted. Existing bonding preserved.")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
tui.header("Classic Import — Step 4: Write bond to disk")
|
|
145
|
+
|
|
146
|
+
with tui.Spinner("Stopping Bluetooth service…"):
|
|
147
|
+
backend.stop_bluetooth()
|
|
148
|
+
tui.ok("Bluetooth service stopped.")
|
|
149
|
+
|
|
150
|
+
if target_dir.exists():
|
|
151
|
+
with tui.Spinner(f"Removing {bond.device_mac} bonding folder…"):
|
|
152
|
+
backend.remove_bond_key(bond.adapter_mac, bond.device_mac)
|
|
153
|
+
tui.ok("Old bonding removed (backup kept).")
|
|
154
|
+
|
|
155
|
+
with tui.Spinner("Disabling discovery scan if active…"):
|
|
156
|
+
disabled = backend.disable_discovery_if_active()
|
|
157
|
+
if disabled:
|
|
158
|
+
tui.info("Discovery scan disabled.")
|
|
159
|
+
|
|
160
|
+
with tui.Spinner(f"Writing Classic info for {bond.device_mac}…"):
|
|
161
|
+
write_dir = backend.bluetooth_dir / bond.adapter_mac / bond.device_mac
|
|
162
|
+
info_path = backend.write_classic_info(bond, write_dir)
|
|
163
|
+
backend.set_info_permissions(info_path)
|
|
164
|
+
tui.ok(f"Written to: {tui.dim(str(info_path))}")
|
|
165
|
+
|
|
166
|
+
with tui.Spinner("Starting Bluetooth service…"):
|
|
167
|
+
backend.start_bluetooth()
|
|
168
|
+
tui.ok("Bluetooth service started.")
|
|
169
|
+
|
|
170
|
+
# RC7: mandatory post-import instructions
|
|
171
|
+
tui.warn_classic_post_import(bond.device_mac)
|
|
172
|
+
|
|
173
|
+
verify_and_connect(backend, bond.device_mac)
|