#!/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 $ 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())