Skip to content

Instantly share code, notes, and snippets.

@Mygod
Last active May 19, 2023 10:51
Embed
What would you like to do?
Export your Windows Bluetooth LE keys into Linux!
#!/usr/bin/python3
"""
Copyright 2021 Mygod
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
What is this: Export your Windows Bluetooth LE keys into Linux!
Thanks to:
* http://console.systems/2014/09/how-to-pair-low-energy-le-bluetooth.html
* https://gist.github.com/corecoding/eac76d3da20c7e427a1848b8aed8e334/revisions#diff-6eeb0d27c24cc10680e8574f75648585
Usage:
$ ./export-ble-infos.py <args>
$ sudo bash -c 'cp -r ./bluetooth /var/lib && service bluetooth force-reload'
$ rm -r bluetooth
"""
import os
import shutil
import subprocess
import sys
import tempfile
from configparser import ConfigParser
from optparse import OptionParser
default_template = """
[General]
Name=Designer Mouse
Appearance=0x03c2
AddressType=static
SupportedTechnologies=LE;
Trusted=true
Blocked=false
Services=00001800-0000-1000-8000-00805f9b34fb;00001801-0000-1000-8000-00805f9b34fb;0000180a-0000-1000-8000-00805f9b34fb;0000180f-0000-1000-8000-00805f9b34fb;00001812-0000-1000-8000-00805f9b34fb;
[IdentityResolvingKey]
Key=
[LocalSignatureKey]
Key=
Counter=0
Authenticated=false
[LongTermKey]
Key=
Authenticated=0
EncSize=16
EDiv=
Rand=
[DeviceID]
Source=2
Vendor=1118
Product=2053
Version=272
[ConnectionParameters]
MinInterval=6
MaxInterval=6
Latency=60
Timeout=300
"""
def main():
parser = OptionParser()
parser.add_option("-v", "--verbose", action='store_true', dest='verbose')
parser.add_option("-s", "--system", dest="system", metavar="FILE",
default="/mnt/Windows/System32/config/SYSTEM",
help="SYSTEM file in Windows. Usually at /Windows/System32/config/system.")
parser.add_option("-k", "--key", dest="key", metavar="KEY",
default=r"ControlSet001\Services\BTHPORT\Parameters\Keys",
help="Registry key for BT. [default: %default]")
parser.add_option("-o", "--output", dest="output", metavar="DIR", default="bluetooth",
help="Output directory. [default: %default]")
parser.add_option("-t", "--template", dest="template", metavar="FILE", help="Template file.")
parser.add_option("-a", "--attributes", dest='attributes', help="Additional attributes file to be copied.")
options, args = parser.parse_args()
if options.template:
with open(options.template) as file:
template = file.read()
else:
template = default_template
out = tempfile.mktemp(".reg")
reged = subprocess.Popen(["reged", "-x", options.system, '\\', options.key, out], stdout=sys.stderr)
reged.wait()
if reged.returncode:
return reged.returncode
dump = ConfigParser()
with open(out) as file:
reged_out = file.read()
if options.verbose:
print(reged_out)
dump.read_string(reged_out.split('\n', 1)[1])
os.unlink(out)
for section in dump:
path = section[len(options.key) + 2:].split('\\')
assert not path[0]
if len(path) == 3:
path[1] = ':'.join([path[1][i:i + 2] for i in range(0, len(path[1]), 2)]).upper()
path[2] = ':'.join([path[2][i:i + 2] for i in range(0, len(path[2]), 2)]).upper()
print("Dumping {}/{}...".format(path[1], path[2]))
config = ConfigParser()
config.optionxform = str
# See if device has been paired in Linux before
existing_template = '/var/lib/bluetooth/{}/{}/info'.format(path[1], path[2])
if (os.path.exists(existing_template)):
with open(existing_template) as file:
config.read_string(file.read())
else:
config.read_string(template)
def read_reg(key, expected_type):
def read_reg_actual(key, expected_type):
actual_type, content = dump[section]['"{}"'.format(key)].split(':', 1)
if expected_type == 'hex16':
assert actual_type == 'hex'
content = content.split(',')
assert len(content) == 16
return ''.join(content).upper()
if expected_type == 'qword':
assert actual_type == 'hex(b)'
content = content.split(',')
assert len(content) == 8
return str(int(''.join(content[::-1]), 16))
if expected_type == 'dword':
assert actual_type == expected_type
return str(int(content, 16))
assert False
result = read_reg_actual(key, expected_type)
if options.verbose:
print("{} of type {}: {}".format(key, expected_type, result))
return result
config['LongTermKey']['Key'] = read_reg('LTK', 'hex16')
# KeyLength ignored for now
config['LongTermKey']['Rand'] = read_reg('ERand', 'qword')
config['LongTermKey']['EDiv'] = read_reg('EDIV', 'dword')
if '"IRK"' in dump[section]:
config['IdentityResolvingKey']['Key'] = read_reg('IRK', 'hex16')
if '"CSRK"' in dump[section]:
config['LocalSignatureKey']['Key'] = read_reg('CSRK', 'hex16')
output_dir = os.path.join(options.output, path[1], path[2])
os.makedirs(output_dir, exist_ok=True)
with open(os.path.join(output_dir, 'info'), 'w') as file:
config.write(file, False)
if options.attributes:
shutil.copyfile(options.attributes, os.path.join(output_dir, 'attributes'))
if __name__ == "__main__":
sys.exit(main())
@smac89
Copy link

smac89 commented Feb 11, 2021

Is this supposed to be run from Windows or Linux?

@jamincollins
Copy link

@Mygod what license is this under?

@Mygod
Copy link
Author

Mygod commented Feb 25, 2021

@jamincollins Hi I added APL-2.0 license for this. Cheers!

@fentras
Copy link

fentras commented Mar 4, 2021

This is exactly what I needed. Just ran the script, copied the values and it just works. Thank you so much!

@Mygod
Copy link
Author

Mygod commented Apr 15, 2021

@lathiat Thanks! I took your patch.

@escape0707
Copy link

escape0707 commented Aug 9, 2022

My new ROG GLADIUS III WL don't have an IRK on both Windows 10 and Linux, I did the same trick Mygod did to CSRK in L157 to make IRK optional and it works.

Just for the record, I get KeyError: '"IRK"' when the script is parsing the dumped registry.

@Mygod
Copy link
Author

Mygod commented Aug 9, 2022

@escape0707 Did you mean IRK?

@escape0707
Copy link

@escape0707 Did you mean IRK?

Oh, yes. Sorry for the confusion. I’ve corrected my original comment.

@Mygod
Copy link
Author

Mygod commented Aug 28, 2022

@escape0707 Added your fix as suggested.

@Farthing-0
Copy link

Thank you! That's just what I need, and it works very well for my little manjaro laptop.

@caiobvilar
Copy link

This used to work for me, but after upgrading to Fedora 37 (not sure if its related), the following fields appears on my info file:

  • [RemoteSignatureKey]
  • [PeripheralLongTermKey]
  • [SlaveLongTermKey]

Both [PeripheralLongTermKey] and [SlaveLongTermKey] with their own EDiv and ERand, but there's no corresponding Key on the Windows registry. Have you guys ever had this happen?

@Sukid
Copy link

Sukid commented Apr 22, 2023

THANK YOU SO MUCH, I had to change the windows path in the script but after that it worked like a charm. This is the only tool I've used so far that was able to sync my Logi M550L in both OSes.

@Sukid
Copy link

Sukid commented Apr 22, 2023

This used to work for me, but after upgrading to Fedora 37 (not sure if its related), the following fields appears on my info file:

* [RemoteSignatureKey]

* [PeripheralLongTermKey]

* [SlaveLongTermKey]

Both [PeripheralLongTermKey] and [SlaveLongTermKey] with their own EDiv and ERand, but there's no corresponding Key on the Windows registry. Have you guys ever had this happen?

This script actually does account for this, can attest to it working.

@Konfekt
Copy link

Konfekt commented May 16, 2023

I'm assuming that the path I see in the script isn't working.. i mout windows and linux but maybe it's not enough. I can see my windows file through: **/media/$user/OS ... and so on ..

The MS Windows partition must be mounted to /mnt.
Maybe this could be either made configurable or mentioned in the usage section?

@Konfekt
Copy link

Konfekt commented May 16, 2023

Reading

parser.add_option("-s", "--system", dest="system", metavar="FILE",
                      default="/mnt/Windows/System32/config/SYSTEM",
                      help="SYSTEM file in Windows. Usually at /Windows/System32/config/system.")

it actually is by the -s option but the help entry suggests that the mount point is not.

@pramodhrachuri
Copy link

Any guide on how to use this script?

@Konfekt
Copy link

Konfekt commented May 19, 2023

Download it as export-ble-infos.py, and then run lsblk to find out your Microsoft Windows partition, say /dev/sdb1, mount it by sudo mount /dev/sdb1 /mnt and then run

$ chmod a+x ./export-ble-infos.py
$ ./export-ble-infos.py <args>
$ sudo bash -c 'cp -r ./bluetooth /var/lib && service bluetooth force-reload'
$ rm -r bluetooth

to import all Bluetooth Low Energy devices, such as Microsoft Designer Mouse and Keyboard, from Microsoft Windows >= 10 to Linux.

@escape0707
Copy link

escape0707 commented May 19, 2023

@pramodhrachuri I mean it's literally written in the first screen you'll see when opening this page. If you can read others reply, you can read it too. https://gist.github.com/Mygod/f390aabf53cf1406fc71166a47236ebf#file-export-ble-infos-py-L24

And also python ./export-ble-infos.py --help...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment