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.
Files changed (44) hide show
  1. btkey_sync-0.2.0/PKG-INFO +141 -0
  2. btkey_sync-0.2.0/README.md +129 -0
  3. btkey_sync-0.2.0/actions.py +146 -0
  4. btkey_sync-0.2.0/actions_classic.py +173 -0
  5. btkey_sync-0.2.0/actions_classic_extra.py +187 -0
  6. btkey_sync-0.2.0/actions_clone.py +166 -0
  7. btkey_sync-0.2.0/actions_clone_loader.py +103 -0
  8. btkey_sync-0.2.0/actions_common.py +184 -0
  9. btkey_sync-0.2.0/actions_help.py +124 -0
  10. btkey_sync-0.2.0/actions_remove.py +57 -0
  11. btkey_sync-0.2.0/actions_show.py +48 -0
  12. btkey_sync-0.2.0/actions_verify.py +177 -0
  13. btkey_sync-0.2.0/actions_windows.py +70 -0
  14. btkey_sync-0.2.0/backends/__init__.py +5 -0
  15. btkey_sync-0.2.0/backends/base.py +74 -0
  16. btkey_sync-0.2.0/backends/linux_backend.py +194 -0
  17. btkey_sync-0.2.0/backends/linux_classic.py +116 -0
  18. btkey_sync-0.2.0/backends/linux_common.py +101 -0
  19. btkey_sync-0.2.0/backends/offline_windows.py +196 -0
  20. btkey_sync-0.2.0/backends/offline_windows_classic.py +63 -0
  21. btkey_sync-0.2.0/backends/windows_backend.py +191 -0
  22. btkey_sync-0.2.0/backends/windows_common.py +182 -0
  23. btkey_sync-0.2.0/btkey_sync.egg-info/PKG-INFO +141 -0
  24. btkey_sync-0.2.0/btkey_sync.egg-info/SOURCES.txt +42 -0
  25. btkey_sync-0.2.0/btkey_sync.egg-info/dependency_links.txt +1 -0
  26. btkey_sync-0.2.0/btkey_sync.egg-info/entry_points.txt +2 -0
  27. btkey_sync-0.2.0/btkey_sync.egg-info/requires.txt +3 -0
  28. btkey_sync-0.2.0/btkey_sync.egg-info/top_level.txt +19 -0
  29. btkey_sync-0.2.0/cli.py +157 -0
  30. btkey_sync-0.2.0/exporters/__init__.py +7 -0
  31. btkey_sync-0.2.0/exporters/classic_exporter.py +59 -0
  32. btkey_sync-0.2.0/exporters/reg_exporter.py +58 -0
  33. btkey_sync-0.2.0/importers/__init__.py +7 -0
  34. btkey_sync-0.2.0/importers/classic_importer.py +37 -0
  35. btkey_sync-0.2.0/importers/reg_importer.py +21 -0
  36. btkey_sync-0.2.0/models.py +187 -0
  37. btkey_sync-0.2.0/platform_detect.py +106 -0
  38. btkey_sync-0.2.0/pyproject.toml +53 -0
  39. btkey_sync-0.2.0/setup.cfg +4 -0
  40. btkey_sync-0.2.0/storage.py +176 -0
  41. btkey_sync-0.2.0/tests/test_parsing.py +75 -0
  42. btkey_sync-0.2.0/tests/test_parsing_ble.py +121 -0
  43. btkey_sync-0.2.0/tests/test_parsing_classic.py +156 -0
  44. 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
+ [![PyPI version](https://img.shields.io/pypi/v/btkey-sync.svg)](https://pypi.org/project/btkey-sync/)
16
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
17
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/btkey-sync.svg)](https://pypi.org/project/btkey-sync/)
4
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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)