Skip to content

Instantly share code, notes, and snippets.

@ZaWertun
Forked from Mygod/export-ble-infos.py
Created February 12, 2023 20:32
Show Gist options
  • Save ZaWertun/e89a4dd60b150a67206816f064e8597b to your computer and use it in GitHub Desktop.
Save ZaWertun/e89a4dd60b150a67206816f064e8597b to your computer and use it in GitHub Desktop.
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())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment