Btkey-Sync 0.1.1__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 (40) hide show
  1. btkey_sync-0.1.1/Btkey_Sync.egg-info/PKG-INFO +144 -0
  2. btkey_sync-0.1.1/Btkey_Sync.egg-info/SOURCES.txt +38 -0
  3. btkey_sync-0.1.1/Btkey_Sync.egg-info/dependency_links.txt +1 -0
  4. btkey_sync-0.1.1/Btkey_Sync.egg-info/entry_points.txt +2 -0
  5. btkey_sync-0.1.1/Btkey_Sync.egg-info/requires.txt +3 -0
  6. btkey_sync-0.1.1/Btkey_Sync.egg-info/top_level.txt +18 -0
  7. btkey_sync-0.1.1/PKG-INFO +144 -0
  8. btkey_sync-0.1.1/README.md +132 -0
  9. btkey_sync-0.1.1/actions.py +193 -0
  10. btkey_sync-0.1.1/actions_classic.py +173 -0
  11. btkey_sync-0.1.1/actions_classic_extra.py +187 -0
  12. btkey_sync-0.1.1/actions_clone.py +229 -0
  13. btkey_sync-0.1.1/actions_common.py +193 -0
  14. btkey_sync-0.1.1/actions_help.py +124 -0
  15. btkey_sync-0.1.1/actions_remove.py +57 -0
  16. btkey_sync-0.1.1/actions_show.py +48 -0
  17. btkey_sync-0.1.1/actions_verify.py +144 -0
  18. btkey_sync-0.1.1/actions_windows.py +70 -0
  19. btkey_sync-0.1.1/backends/__init__.py +5 -0
  20. btkey_sync-0.1.1/backends/base.py +74 -0
  21. btkey_sync-0.1.1/backends/linux_backend.py +237 -0
  22. btkey_sync-0.1.1/backends/linux_classic.py +205 -0
  23. btkey_sync-0.1.1/backends/offline_windows.py +196 -0
  24. btkey_sync-0.1.1/backends/offline_windows_classic.py +63 -0
  25. btkey_sync-0.1.1/backends/windows_backend.py +191 -0
  26. btkey_sync-0.1.1/backends/windows_common.py +182 -0
  27. btkey_sync-0.1.1/cli.py +147 -0
  28. btkey_sync-0.1.1/exporters/__init__.py +7 -0
  29. btkey_sync-0.1.1/exporters/classic_exporter.py +59 -0
  30. btkey_sync-0.1.1/exporters/reg_exporter.py +58 -0
  31. btkey_sync-0.1.1/importers/__init__.py +7 -0
  32. btkey_sync-0.1.1/importers/classic_importer.py +37 -0
  33. btkey_sync-0.1.1/importers/reg_importer.py +21 -0
  34. btkey_sync-0.1.1/models.py +187 -0
  35. btkey_sync-0.1.1/platform_detect.py +106 -0
  36. btkey_sync-0.1.1/pyproject.toml +47 -0
  37. btkey_sync-0.1.1/setup.cfg +4 -0
  38. btkey_sync-0.1.1/storage.py +176 -0
  39. btkey_sync-0.1.1/tests/test_parsing.py +308 -0
  40. btkey_sync-0.1.1/tui_helpers.py +149 -0
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: Btkey-Sync
3
+ Version: 0.1.1
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
+ Syncs Bluetooth LE bonding (LTK / EDIV / ERand) from BLE devices between
16
+ any two "sides": dual-boot operating systems, two partitions/installs of
17
+ the same OS, or two separate physical machines — without having to re-pair
18
+ the physical device on every switch.
19
+
20
+ > See `REQUIREMENTS.md` for the full functional scope and `AGENTS.md` if
21
+ > you're using an AI agent (Claude Code, etc.) to maintain/extend this project.
22
+
23
+ ## Why this exists
24
+
25
+ Some budget BLE devices (mice, keyboards) use private MAC addresses that
26
+ rotate between pairing sessions (RPA). Windows and Linux store bonding
27
+ data in completely different formats and paths:
28
+
29
+ | | Windows | Linux (BlueZ) |
30
+ | ------------------ | ---------------------------------------------------------------------------- | ------------------------------------------- |
31
+ | Location | `HKLM\SYSTEM\CurrentControlSet\Services\BTHPORT\Parameters\Keys` | `/var/lib/bluetooth/<adapter>/<device>/info` |
32
+ | Required access | SYSTEM account (Administrator is not enough) | root |
33
+ | EDIV/ERand format | Hexadecimal | Decimal |
34
+ | Hot reload | — | No: requires `systemctl restart bluetooth` |
35
+
36
+ This project automates extraction, conversion, and writing on both sides,
37
+ leaving the exchangeable files in an `exports/` folder intended to be copied
38
+ manually between partitions (there's no way to sync this live without a
39
+ daemon running on both OSes simultaneously).
40
+
41
+ ## Installation
42
+
43
+ No external dependencies required for normal use (only Python ≥3.10 stdlib).
44
+
45
+ ```bash
46
+ git clone <this-repo>
47
+ cd btkey_sync
48
+ # Optional, only if running tests with pytest:
49
+ pip install -e ".[dev]" --break-system-packages
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ### Export (on the system where the device IS connecting successfully)
55
+
56
+ **Linux:**
57
+
58
+ ```bash
59
+ sudo python3 -m btkey_sync
60
+ ```
61
+
62
+ **Windows** (PowerShell or CMD as Administrator):
63
+
64
+ ```powershell
65
+ python -m btkey_sync
66
+ ```
67
+
68
+ Interactive flow:
69
+
70
+ 1. Detects the OS automatically.
71
+ 2. Lists BLE devices with bonding found.
72
+ 3. You choose which one to export.
73
+ 4. Generates `exports/<MAC>__<os>__<timestamp>.reg` + `.json` with the same info in plain text.
74
+
75
+ ### Copy the file to the destination
76
+
77
+ Copy the generated `.reg` file (USB, shared partition, local network,
78
+ whatever you have at hand) to the `exports/` folder of the target
79
+ installation — this can be the other OS (dual boot), another
80
+ partition/install of the same OS, or the equivalent on another machine.
81
+
82
+ ### Import (on the system that needs the bonding)
83
+
84
+ ```bash
85
+ sudo python3 -m btkey_sync --import filename.reg
86
+ ```
87
+
88
+ Shows locally known devices, lets you confirm or change the destination MAC
89
+ (important if the device rotated its RPA address since last time), writes
90
+ the bonding, and automatically restarts the Bluetooth stack.
91
+
92
+ ## Project structure
93
+
94
+ ```
95
+ btkey_sync/
96
+ ├── cli.py # orchestrates the 4-step flow
97
+ ├── models.py # BondKey: OS-agnostic bonding representation
98
+ ├── platform_detect.py # Windows/Linux detection + environment validation
99
+ ├── storage.py # manages the exports/ folder and naming convention
100
+ ├── backends/
101
+ │ ├── base.py # interface all backends must implement
102
+ │ ├── windows_backend.py # Windows registry via SYSTEM scheduled task
103
+ │ └── linux_backend.py # BlueZ info files in /var/lib/bluetooth
104
+ ├── exporters/reg_exporter.py # BondKey -> .reg content
105
+ └── importers/reg_importer.py # .reg -> BondKey
106
+ ```
107
+
108
+ ## Testing
109
+
110
+ ```bash
111
+ python3 tests/test_parsing.py
112
+ # or, if you installed pytest:
113
+ pytest
114
+ ```
115
+
116
+ Tests use data from a real confirmed-working migration
117
+ (`tests/fixtures/sample_mouse.reg`), including the round-trip case with
118
+ a MAC change (rotated RPA).
119
+
120
+ To test a backend without touching your real system, inject a temporary
121
+ directory:
122
+
123
+ ```python
124
+ from pathlib import Path
125
+ from btkey_sync.backends.linux_backend import LinuxBluetoothBackend
126
+
127
+ backend = LinuxBluetoothBackend(bluetooth_dir=Path("/tmp/fake_bluetooth"))
128
+ ```
129
+
130
+ ## Known limitations
131
+
132
+ - No automatic/live sync between the two sides — always requires a manual
133
+ step of copying the exported file to the destination.
134
+ - BLE only (SMP/LTK). Does not cover Bluetooth Classic (BR/EDR).
135
+ - If the device uses RPA and rotates its address, the export→copy→import
136
+ cycle must be repeated; this project does not resolve IRK automatically.
137
+ - Windows and Linux only. No macOS backend (see `AGENTS.md` if you want
138
+ to add one).
139
+
140
+ ## License / use
141
+
142
+ Personal project for managing your own equipment. Use at your own
143
+ discretion; you are touching internal structures not officially documented
144
+ by Microsoft or the BlueZ project.
@@ -0,0 +1,38 @@
1
+ README.md
2
+ actions.py
3
+ actions_classic.py
4
+ actions_classic_extra.py
5
+ actions_clone.py
6
+ actions_common.py
7
+ actions_help.py
8
+ actions_remove.py
9
+ actions_show.py
10
+ actions_verify.py
11
+ actions_windows.py
12
+ cli.py
13
+ models.py
14
+ platform_detect.py
15
+ pyproject.toml
16
+ storage.py
17
+ tui_helpers.py
18
+ Btkey_Sync.egg-info/PKG-INFO
19
+ Btkey_Sync.egg-info/SOURCES.txt
20
+ Btkey_Sync.egg-info/dependency_links.txt
21
+ Btkey_Sync.egg-info/entry_points.txt
22
+ Btkey_Sync.egg-info/requires.txt
23
+ Btkey_Sync.egg-info/top_level.txt
24
+ backends/__init__.py
25
+ backends/base.py
26
+ backends/linux_backend.py
27
+ backends/linux_classic.py
28
+ backends/offline_windows.py
29
+ backends/offline_windows_classic.py
30
+ backends/windows_backend.py
31
+ backends/windows_common.py
32
+ exporters/__init__.py
33
+ exporters/classic_exporter.py
34
+ exporters/reg_exporter.py
35
+ importers/__init__.py
36
+ importers/classic_importer.py
37
+ importers/reg_importer.py
38
+ tests/test_parsing.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ btkey-sync = cli:main
@@ -0,0 +1,3 @@
1
+
2
+ [dev]
3
+ pytest>=7.0
@@ -0,0 +1,18 @@
1
+ actions
2
+ actions_classic
3
+ actions_classic_extra
4
+ actions_clone
5
+ actions_common
6
+ actions_help
7
+ actions_remove
8
+ actions_show
9
+ actions_verify
10
+ actions_windows
11
+ backends
12
+ cli
13
+ exporters
14
+ importers
15
+ models
16
+ platform_detect
17
+ storage
18
+ tui_helpers
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: Btkey-Sync
3
+ Version: 0.1.1
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
+ Syncs Bluetooth LE bonding (LTK / EDIV / ERand) from BLE devices between
16
+ any two "sides": dual-boot operating systems, two partitions/installs of
17
+ the same OS, or two separate physical machines — without having to re-pair
18
+ the physical device on every switch.
19
+
20
+ > See `REQUIREMENTS.md` for the full functional scope and `AGENTS.md` if
21
+ > you're using an AI agent (Claude Code, etc.) to maintain/extend this project.
22
+
23
+ ## Why this exists
24
+
25
+ Some budget BLE devices (mice, keyboards) use private MAC addresses that
26
+ rotate between pairing sessions (RPA). Windows and Linux store bonding
27
+ data in completely different formats and paths:
28
+
29
+ | | Windows | Linux (BlueZ) |
30
+ | ------------------ | ---------------------------------------------------------------------------- | ------------------------------------------- |
31
+ | Location | `HKLM\SYSTEM\CurrentControlSet\Services\BTHPORT\Parameters\Keys` | `/var/lib/bluetooth/<adapter>/<device>/info` |
32
+ | Required access | SYSTEM account (Administrator is not enough) | root |
33
+ | EDIV/ERand format | Hexadecimal | Decimal |
34
+ | Hot reload | — | No: requires `systemctl restart bluetooth` |
35
+
36
+ This project automates extraction, conversion, and writing on both sides,
37
+ leaving the exchangeable files in an `exports/` folder intended to be copied
38
+ manually between partitions (there's no way to sync this live without a
39
+ daemon running on both OSes simultaneously).
40
+
41
+ ## Installation
42
+
43
+ No external dependencies required for normal use (only Python ≥3.10 stdlib).
44
+
45
+ ```bash
46
+ git clone <this-repo>
47
+ cd btkey_sync
48
+ # Optional, only if running tests with pytest:
49
+ pip install -e ".[dev]" --break-system-packages
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ### Export (on the system where the device IS connecting successfully)
55
+
56
+ **Linux:**
57
+
58
+ ```bash
59
+ sudo python3 -m btkey_sync
60
+ ```
61
+
62
+ **Windows** (PowerShell or CMD as Administrator):
63
+
64
+ ```powershell
65
+ python -m btkey_sync
66
+ ```
67
+
68
+ Interactive flow:
69
+
70
+ 1. Detects the OS automatically.
71
+ 2. Lists BLE devices with bonding found.
72
+ 3. You choose which one to export.
73
+ 4. Generates `exports/<MAC>__<os>__<timestamp>.reg` + `.json` with the same info in plain text.
74
+
75
+ ### Copy the file to the destination
76
+
77
+ Copy the generated `.reg` file (USB, shared partition, local network,
78
+ whatever you have at hand) to the `exports/` folder of the target
79
+ installation — this can be the other OS (dual boot), another
80
+ partition/install of the same OS, or the equivalent on another machine.
81
+
82
+ ### Import (on the system that needs the bonding)
83
+
84
+ ```bash
85
+ sudo python3 -m btkey_sync --import filename.reg
86
+ ```
87
+
88
+ Shows locally known devices, lets you confirm or change the destination MAC
89
+ (important if the device rotated its RPA address since last time), writes
90
+ the bonding, and automatically restarts the Bluetooth stack.
91
+
92
+ ## Project structure
93
+
94
+ ```
95
+ btkey_sync/
96
+ ├── cli.py # orchestrates the 4-step flow
97
+ ├── models.py # BondKey: OS-agnostic bonding representation
98
+ ├── platform_detect.py # Windows/Linux detection + environment validation
99
+ ├── storage.py # manages the exports/ folder and naming convention
100
+ ├── backends/
101
+ │ ├── base.py # interface all backends must implement
102
+ │ ├── windows_backend.py # Windows registry via SYSTEM scheduled task
103
+ │ └── linux_backend.py # BlueZ info files in /var/lib/bluetooth
104
+ ├── exporters/reg_exporter.py # BondKey -> .reg content
105
+ └── importers/reg_importer.py # .reg -> BondKey
106
+ ```
107
+
108
+ ## Testing
109
+
110
+ ```bash
111
+ python3 tests/test_parsing.py
112
+ # or, if you installed pytest:
113
+ pytest
114
+ ```
115
+
116
+ Tests use data from a real confirmed-working migration
117
+ (`tests/fixtures/sample_mouse.reg`), including the round-trip case with
118
+ a MAC change (rotated RPA).
119
+
120
+ To test a backend without touching your real system, inject a temporary
121
+ directory:
122
+
123
+ ```python
124
+ from pathlib import Path
125
+ from btkey_sync.backends.linux_backend import LinuxBluetoothBackend
126
+
127
+ backend = LinuxBluetoothBackend(bluetooth_dir=Path("/tmp/fake_bluetooth"))
128
+ ```
129
+
130
+ ## Known limitations
131
+
132
+ - No automatic/live sync between the two sides — always requires a manual
133
+ step of copying the exported file to the destination.
134
+ - BLE only (SMP/LTK). Does not cover Bluetooth Classic (BR/EDR).
135
+ - If the device uses RPA and rotates its address, the export→copy→import
136
+ cycle must be repeated; this project does not resolve IRK automatically.
137
+ - Windows and Linux only. No macOS backend (see `AGENTS.md` if you want
138
+ to add one).
139
+
140
+ ## License / use
141
+
142
+ Personal project for managing your own equipment. Use at your own
143
+ discretion; you are touching internal structures not officially documented
144
+ by Microsoft or the BlueZ project.
@@ -0,0 +1,132 @@
1
+ # btkey_sync
2
+
3
+ Syncs Bluetooth LE bonding (LTK / EDIV / ERand) from BLE devices between
4
+ any two "sides": dual-boot operating systems, two partitions/installs of
5
+ the same OS, or two separate physical machines — without having to re-pair
6
+ the physical device on every switch.
7
+
8
+ > See `REQUIREMENTS.md` for the full functional scope and `AGENTS.md` if
9
+ > you're using an AI agent (Claude Code, etc.) to maintain/extend this project.
10
+
11
+ ## Why this exists
12
+
13
+ Some budget BLE devices (mice, keyboards) use private MAC addresses that
14
+ rotate between pairing sessions (RPA). Windows and Linux store bonding
15
+ data in completely different formats and paths:
16
+
17
+ | | Windows | Linux (BlueZ) |
18
+ | ------------------ | ---------------------------------------------------------------------------- | ------------------------------------------- |
19
+ | Location | `HKLM\SYSTEM\CurrentControlSet\Services\BTHPORT\Parameters\Keys` | `/var/lib/bluetooth/<adapter>/<device>/info` |
20
+ | Required access | SYSTEM account (Administrator is not enough) | root |
21
+ | EDIV/ERand format | Hexadecimal | Decimal |
22
+ | Hot reload | — | No: requires `systemctl restart bluetooth` |
23
+
24
+ This project automates extraction, conversion, and writing on both sides,
25
+ leaving the exchangeable files in an `exports/` folder intended to be copied
26
+ manually between partitions (there's no way to sync this live without a
27
+ daemon running on both OSes simultaneously).
28
+
29
+ ## Installation
30
+
31
+ No external dependencies required for normal use (only Python ≥3.10 stdlib).
32
+
33
+ ```bash
34
+ git clone <this-repo>
35
+ cd btkey_sync
36
+ # Optional, only if running tests with pytest:
37
+ pip install -e ".[dev]" --break-system-packages
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Export (on the system where the device IS connecting successfully)
43
+
44
+ **Linux:**
45
+
46
+ ```bash
47
+ sudo python3 -m btkey_sync
48
+ ```
49
+
50
+ **Windows** (PowerShell or CMD as Administrator):
51
+
52
+ ```powershell
53
+ python -m btkey_sync
54
+ ```
55
+
56
+ Interactive flow:
57
+
58
+ 1. Detects the OS automatically.
59
+ 2. Lists BLE devices with bonding found.
60
+ 3. You choose which one to export.
61
+ 4. Generates `exports/<MAC>__<os>__<timestamp>.reg` + `.json` with the same info in plain text.
62
+
63
+ ### Copy the file to the destination
64
+
65
+ Copy the generated `.reg` file (USB, shared partition, local network,
66
+ whatever you have at hand) to the `exports/` folder of the target
67
+ installation — this can be the other OS (dual boot), another
68
+ partition/install of the same OS, or the equivalent on another machine.
69
+
70
+ ### Import (on the system that needs the bonding)
71
+
72
+ ```bash
73
+ sudo python3 -m btkey_sync --import filename.reg
74
+ ```
75
+
76
+ Shows locally known devices, lets you confirm or change the destination MAC
77
+ (important if the device rotated its RPA address since last time), writes
78
+ the bonding, and automatically restarts the Bluetooth stack.
79
+
80
+ ## Project structure
81
+
82
+ ```
83
+ btkey_sync/
84
+ ├── cli.py # orchestrates the 4-step flow
85
+ ├── models.py # BondKey: OS-agnostic bonding representation
86
+ ├── platform_detect.py # Windows/Linux detection + environment validation
87
+ ├── storage.py # manages the exports/ folder and naming convention
88
+ ├── backends/
89
+ │ ├── base.py # interface all backends must implement
90
+ │ ├── windows_backend.py # Windows registry via SYSTEM scheduled task
91
+ │ └── linux_backend.py # BlueZ info files in /var/lib/bluetooth
92
+ ├── exporters/reg_exporter.py # BondKey -> .reg content
93
+ └── importers/reg_importer.py # .reg -> BondKey
94
+ ```
95
+
96
+ ## Testing
97
+
98
+ ```bash
99
+ python3 tests/test_parsing.py
100
+ # or, if you installed pytest:
101
+ pytest
102
+ ```
103
+
104
+ Tests use data from a real confirmed-working migration
105
+ (`tests/fixtures/sample_mouse.reg`), including the round-trip case with
106
+ a MAC change (rotated RPA).
107
+
108
+ To test a backend without touching your real system, inject a temporary
109
+ directory:
110
+
111
+ ```python
112
+ from pathlib import Path
113
+ from btkey_sync.backends.linux_backend import LinuxBluetoothBackend
114
+
115
+ backend = LinuxBluetoothBackend(bluetooth_dir=Path("/tmp/fake_bluetooth"))
116
+ ```
117
+
118
+ ## Known limitations
119
+
120
+ - No automatic/live sync between the two sides — always requires a manual
121
+ step of copying the exported file to the destination.
122
+ - BLE only (SMP/LTK). Does not cover Bluetooth Classic (BR/EDR).
123
+ - If the device uses RPA and rotates its address, the export→copy→import
124
+ cycle must be repeated; this project does not resolve IRK automatically.
125
+ - Windows and Linux only. No macOS backend (see `AGENTS.md` if you want
126
+ to add one).
127
+
128
+ ## License / use
129
+
130
+ Personal project for managing your own equipment. Use at your own
131
+ discretion; you are touching internal structures not officially documented
132
+ by Microsoft or the BlueZ project.
@@ -0,0 +1,193 @@
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_verify import verify_and_connect, run_verify_flow # noqa: F401
10
+ from actions_remove import run_remove_flow # noqa: F401
11
+
12
+
13
+ def _ensure_device_name(bond) -> None:
14
+ """If bond has no name, prompt the user to enter one interactively."""
15
+ if not bond.device_name:
16
+ name = tui.ask("Device has no name — enter a label (Enter to skip)", default="")
17
+ if name.strip():
18
+ bond.device_name = name.strip()
19
+
20
+
21
+ def run_export_flow() -> None:
22
+ tui.header("Select & Extract — Step 1: System detection")
23
+ env, backend = common.detect_and_validate()
24
+ tui.header("Select & Extract — Step 2: Choose extraction source")
25
+ source_os, win_mount = common.prompt_source_os(env.os_kind)
26
+ if source_os == OSKind.WINDOWS and env.os_kind == OSKind.LINUX:
27
+ common.run_offline_windows_export(win_mount, export_bond_key, ensure_exports_dir)
28
+ return
29
+ tui.header("Select & Extract — Step 3: Choose a device")
30
+ with tui.Spinner("Scanning bonded BLE devices…"):
31
+ devices = backend.list_devices()
32
+ chosen = common.prompt_select_device(devices) if devices else None
33
+ if not chosen:
34
+ tui.info("No devices or cancelled.")
35
+ return
36
+ tui.header("Select & Extract — Step 4: Extract and save")
37
+ with tui.Spinner(f"Extracting bonding for {chosen.device_mac}…"):
38
+ bond = backend.extract_bond_key(chosen)
39
+ bond.source_os = source_os.value
40
+ common.print_bond_summary(bond)
41
+ with tui.Spinner("Writing export files…"):
42
+ exports_dir = ensure_exports_dir()
43
+ reg_path = export_bond_key(bond)
44
+ tui.ok(f"Exported to: {tui.bold(reg_path.name)}")
45
+
46
+
47
+ def run_import_flow(reg_file_arg: str | None = None) -> None:
48
+ tui.header("Import — Step 1: System detection")
49
+ env, backend = common.detect_and_validate()
50
+ reg_path = Path(reg_file_arg) if reg_file_arg else common.prompt_select_export_file()
51
+ if not reg_path:
52
+ tui.info("Cancelled.")
53
+ return
54
+ if not reg_path.exists():
55
+ tui.err(f"File not found: {reg_path}")
56
+ return
57
+
58
+ tui.header("Import — Step 3: Load bond key")
59
+ with tui.Spinner(f"Parsing {reg_path.name}…"):
60
+ bond = load_bond_from_reg_file(reg_path)
61
+ common.print_bond_summary(bond)
62
+ _ensure_device_name(bond)
63
+
64
+ tui.header("Import — Step 4: Compare with local device")
65
+ with tui.Spinner("Looking up local bonding for this device…"):
66
+ existing = backend.list_devices()
67
+
68
+ local_match = next(
69
+ (d for d in existing if d.device_mac.upper() == bond.device_mac.upper()), None
70
+ )
71
+ ltk_match = None
72
+ if not local_match:
73
+ for d in existing:
74
+ if d.has_ltk:
75
+ try:
76
+ local_bond = backend.extract_bond_key(d)
77
+ if (local_bond.ltk_hex.upper() == bond.ltk_hex.upper() or
78
+ (bond.irk_hex and local_bond.irk_hex and
79
+ local_bond.irk_hex.upper() == bond.irk_hex.upper())):
80
+ ltk_match = d
81
+ # Propagate name if source has no name
82
+ if not bond.device_name and local_bond.device_name:
83
+ bond.device_name = local_bond.device_name
84
+ break
85
+ except Exception:
86
+ pass
87
+
88
+ source_device_mac = None
89
+ if local_match and local_match.has_ltk:
90
+ try:
91
+ local_bond = backend.extract_bond_key(local_match)
92
+ if local_bond.ltk_hex.upper() == bond.ltk_hex.upper():
93
+ tui.ok("Keys are already in sync — LTK matches. No import needed.")
94
+ return
95
+ tui.warn(f"Key mismatch!\n Local LTK: {local_bond.ltk_hex}\n New LTK: {bond.ltk_hex}")
96
+ except Exception:
97
+ pass
98
+ elif ltk_match:
99
+ tui.info(f"Matched physical device under different MAC: {ltk_match.device_mac}")
100
+ q = f"Migrate profile/cache from {ltk_match.device_mac} to {bond.device_mac}? [Y/n]"
101
+ if tui.ask(q, default="Y").upper() == "Y":
102
+ source_device_mac = ltk_match.device_mac
103
+ elif local_match:
104
+ tui.info("Device found locally but has no LTK.")
105
+ else:
106
+ tui.info("Device not paired locally yet.")
107
+
108
+ print()
109
+ target_mac = tui.ask("Destination MAC (Enter to keep original)", default=bond.device_mac)
110
+ target_mac = target_mac if target_mac != bond.device_mac else None
111
+
112
+ tui.header("Import — Step 5: Write and restart Bluetooth")
113
+ with tui.Spinner("Writing bonding to disk…"):
114
+ written_path = backend.import_bond_key(
115
+ bond, target_device_mac=target_mac, source_device_mac=source_device_mac
116
+ )
117
+ tui.ok(f"Written to: {tui.dim(str(written_path))}")
118
+ tui.info("Restarting Bluetooth service to apply changes. This may take a moment…")
119
+ with tui.Spinner("Restarting Bluetooth service…"):
120
+ backend.restart_bluetooth_stack()
121
+ tui.ok("Bluetooth service restarted.")
122
+ verify_and_connect(backend, target_mac or bond.device_mac)
123
+
124
+
125
+ def _pick_source_bond(source_os: OSKind, win_mount, env) -> "BondKey | None":
126
+ """Extract a bond from the chosen source (Linux or Windows partition)."""
127
+ if source_os == OSKind.WINDOWS and env.os_kind == OSKind.LINUX:
128
+ return common.pick_bond_from_windows(win_mount)
129
+ src_backend = common.get_backend(source_os)
130
+ with tui.Spinner("Scanning bonded BLE devices…"):
131
+ devices = src_backend.list_devices()
132
+ if not devices:
133
+ tui.warn("No bonded BLE devices found.")
134
+ return None
135
+ source = common.prompt_select_device(devices)
136
+ if source is None:
137
+ tui.info("Cancelled.")
138
+ return None
139
+ with tui.Spinner(f"Extracting bonding for {source.device_mac}…"):
140
+ bond = src_backend.extract_bond_key(source)
141
+ bond.source_os = source_os.value
142
+ return bond
143
+
144
+ def run_clone_flow() -> None:
145
+ tui.header("Clone — Step 1: System detection")
146
+ env, backend = common.detect_and_validate()
147
+ if env.os_kind != OSKind.LINUX:
148
+ tui.err("Clone (write destination) is only supported on Linux.")
149
+ return
150
+ tui.header("Clone — Step 2: Choose source")
151
+ source_os, win_mount = common.prompt_source_os(env.os_kind)
152
+ bond = _pick_source_bond(source_os, win_mount, env)
153
+ if bond is None:
154
+ return
155
+ common.print_bond_summary(bond)
156
+ _ensure_device_name(bond)
157
+ tui.header("Clone — Step 3: Destination MAC")
158
+ target_mac = tui.ask("Destination MAC (Enter = same)", default=bond.device_mac)
159
+ with tui.Spinner("Scanning local devices…"):
160
+ existing = backend.list_devices()
161
+ local_match = next((d for d in existing if d.device_mac.upper() == target_mac.upper()), None)
162
+ ltk_match = None
163
+ if not local_match:
164
+ for d in existing:
165
+ if d.has_ltk:
166
+ try:
167
+ local_bond = backend.extract_bond_key(d)
168
+ if (local_bond.ltk_hex.upper() == bond.ltk_hex.upper() or
169
+ (bond.irk_hex and local_bond.irk_hex and
170
+ local_bond.irk_hex.upper() == bond.irk_hex.upper())):
171
+ ltk_match = d
172
+ if not bond.device_name and local_bond.device_name:
173
+ bond.device_name = local_bond.device_name
174
+ break
175
+ except Exception:
176
+ pass
177
+ source_device_mac = None
178
+ if ltk_match:
179
+ tui.info(f"Matched physical device under different MAC: {ltk_match.device_mac}")
180
+ q = f"Migrate profile/cache from {ltk_match.device_mac} to {target_mac}? [Y/n]"
181
+ if tui.ask(q, default="Y").upper() == "Y":
182
+ source_device_mac = ltk_match.device_mac
183
+ tui.header("Clone — Step 4: Write and restart Bluetooth")
184
+ with tui.Spinner("Writing cloned bonding to disk…"):
185
+ written_path = backend.import_bond_key(
186
+ bond, target_device_mac=target_mac, source_device_mac=source_device_mac
187
+ )
188
+ tui.ok(f"Cloned bonding written to: {tui.dim(str(written_path))}")
189
+ tui.info("Restarting Bluetooth service to apply changes. This may take a moment…")
190
+ with tui.Spinner("Restarting Bluetooth service…"):
191
+ backend.restart_bluetooth_stack()
192
+ tui.ok("Bluetooth service restarted.")
193
+ verify_and_connect(backend, target_mac)