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.
- btkey_sync-0.1.1/Btkey_Sync.egg-info/PKG-INFO +144 -0
- btkey_sync-0.1.1/Btkey_Sync.egg-info/SOURCES.txt +38 -0
- btkey_sync-0.1.1/Btkey_Sync.egg-info/dependency_links.txt +1 -0
- btkey_sync-0.1.1/Btkey_Sync.egg-info/entry_points.txt +2 -0
- btkey_sync-0.1.1/Btkey_Sync.egg-info/requires.txt +3 -0
- btkey_sync-0.1.1/Btkey_Sync.egg-info/top_level.txt +18 -0
- btkey_sync-0.1.1/PKG-INFO +144 -0
- btkey_sync-0.1.1/README.md +132 -0
- btkey_sync-0.1.1/actions.py +193 -0
- btkey_sync-0.1.1/actions_classic.py +173 -0
- btkey_sync-0.1.1/actions_classic_extra.py +187 -0
- btkey_sync-0.1.1/actions_clone.py +229 -0
- btkey_sync-0.1.1/actions_common.py +193 -0
- btkey_sync-0.1.1/actions_help.py +124 -0
- btkey_sync-0.1.1/actions_remove.py +57 -0
- btkey_sync-0.1.1/actions_show.py +48 -0
- btkey_sync-0.1.1/actions_verify.py +144 -0
- btkey_sync-0.1.1/actions_windows.py +70 -0
- btkey_sync-0.1.1/backends/__init__.py +5 -0
- btkey_sync-0.1.1/backends/base.py +74 -0
- btkey_sync-0.1.1/backends/linux_backend.py +237 -0
- btkey_sync-0.1.1/backends/linux_classic.py +205 -0
- btkey_sync-0.1.1/backends/offline_windows.py +196 -0
- btkey_sync-0.1.1/backends/offline_windows_classic.py +63 -0
- btkey_sync-0.1.1/backends/windows_backend.py +191 -0
- btkey_sync-0.1.1/backends/windows_common.py +182 -0
- btkey_sync-0.1.1/cli.py +147 -0
- btkey_sync-0.1.1/exporters/__init__.py +7 -0
- btkey_sync-0.1.1/exporters/classic_exporter.py +59 -0
- btkey_sync-0.1.1/exporters/reg_exporter.py +58 -0
- btkey_sync-0.1.1/importers/__init__.py +7 -0
- btkey_sync-0.1.1/importers/classic_importer.py +37 -0
- btkey_sync-0.1.1/importers/reg_importer.py +21 -0
- btkey_sync-0.1.1/models.py +187 -0
- btkey_sync-0.1.1/platform_detect.py +106 -0
- btkey_sync-0.1.1/pyproject.toml +47 -0
- btkey_sync-0.1.1/setup.cfg +4 -0
- btkey_sync-0.1.1/storage.py +176 -0
- btkey_sync-0.1.1/tests/test_parsing.py +308 -0
- 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 @@
|
|
|
1
|
+
|
|
@@ -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)
|