Wednesday, February 21, 2024

Bluetooth Low-Energy Devices, Windows and Linux

 I've attempted, many times, to get bluetooth devices paired to multiple OSes simultaneously, on the same machine, and never succeeded. Usually, the OSes involved are Windows and Linux, as I primarily use Linux, but use Windows for certain special apps that require it.

The user 'Mygod' posted a really, really useful GitHub gist, a number of years ago, that solves the problem, rather neatly:
https://gist.github.com/Mygod/f390aabf53cf1406fc71166a47236ebf

The only dependencies are:

  1. having your Windows partition mounted somewhere on your Linux system
  2. installing the 'chntpw' package installed

Once you have both of those, download the script from the gist, which I have included here, for convenience:

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

Once you have the script copied to your system, do the following, noting that your keys will be different than mine:

$ chmod 0755 export-ble-infos.py
$ ./export-ble-infos.py -s /PATH TO YOUR WINDOWS PARTITION/Windows/System32/config/SYSTEM
reged version 0.1 140201, (c) Petter N Hagen
Exporting to file '/tmp/tmppe0q46xu.reg'...
Exporting key 'Keys' with 1 subkeys and 0 values...
Exporting key '50284a36a81b' with 1 subkeys and 1 values...
Exporting key 'ffffcfd7dd52' with 0 subkeys and 10 values...
Dumping 50:28:4A:36:A8:1B/FF:FF:CF:D7:DD:52...
$ sudo cp -r bluetooth /var/lib/
$ sudo systemctl force-reload bluetooth
Now, your device should be linked on Linux as well as Windows, when you boot each OS.

Thanks MyGod!

- Alex