Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions bt_dualboot/bluetooth_device.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from bt_dualboot.bt_windows.convert import mac_to_reg_key, hex_string_to_reg_value, hex_string_to_reg_hex_b
import struct

class BluetoothDevice:
"""Representation of bluetooth device

Expand All @@ -6,6 +9,9 @@ class BluetoothDevice:
mac (str)
name (str)
pairing_key (str)
ltk (str)
rand (int)
edir (int)
adapter_mac (str)
source (str): kind of 'Windows', 'Linux'

Expand All @@ -19,6 +25,9 @@ def __init__(
adapter_mac=None,
device_class=None,
source=None,
ltk=None,
rand=None,
ediv=None,
):
# fmt: off
self.source = source
Expand All @@ -27,13 +36,63 @@ def __init__(
self.name = name
self.pairing_key = pairing_key
self.adapter_mac = adapter_mac
# bluetooth 5.1
self.ltk = ltk
self.rand = rand
self.ediv = ediv
# fmt: on

def __repr__(self):
source = "?"
if self.source is not None:
source = self.source[0]
return f"{self.__class__} {source} [{self.mac}] {self.name}"

def _get_reg_adapter_section_key(self):
if self.pairing_key:
return (
r"ControlSet001\Services\BTHPORT\Parameters\Keys" + "\\" + mac_to_reg_key(self.adapter_mac)
)
if self.ltk:
return (
r"ControlSet001\Services\BTHPORT\Parameters\Keys" + "\\" + mac_to_reg_key(self.adapter_mac) + "\\" + mac_to_reg_key(self.mac)
)
raise Exception(f"Missing pairing key or long term key for {self.adapter_mac}/{self.mac}")

def rand_to_erand(self):
# hex(b) type -> 64 bit -> 8 byte in little endian
hex_string = struct.pack("<Q", self.rand).hex().upper()
return hex_string_to_reg_hex_b(hex_string)

def ediv_to_dword(self):
# dword alias hex(4) type -> 32 bit -> 4 byte in little endian
# Microsoft says it should be little endian: https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types
# they are liars
dword_val = struct.pack(">I", self.ediv).hex()
return f"dword:{dword_val}"

def for_win_import(self):
if self.pairing_key:
device_key = f'"{mac_to_reg_key(self.mac)}"'
pairing_key = hex_string_to_reg_value(self.pairing_key)
return self._get_reg_adapter_section_key(), {
device_key: pairing_key
}
elif self.ltk:
return self._get_reg_adapter_section_key(), {
'"LTK"': hex_string_to_reg_value(self.ltk),
'"ERand"': self.rand_to_erand(),
'"EDIV"': self.ediv_to_dword()
}
raise KeyError(f"Device {self.mac} has neither pairing key, nor long term key")

def synced(self, other):
if self.pairing_key:
return self.pairing_key == other.pairing_key
if self.ltk and self.rand and self.ediv:
return (self.ltk == other.ltk and
self.rand == other.rand and
self.ediv == other.ediv)

@classmethod
def source_linux(cls):
Expand Down
17 changes: 15 additions & 2 deletions bt_dualboot/bt_linux/bluetooth_device_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,25 @@ def extract_info(device_info_path):
device_info_path (str): Kind of .../foo/A4:6B:6C:9D:E2:FB/B6:C2:D3:E5:F2:0D/info

Returns:
hash: Kind of { name:, class:, pairing_key: }
hash: Kind of { name:, class:, pairing_key: , long_term_key:, ediv:, rand:}
"""
config = ConfigParser()
config.read(device_info_path)
link_key = config.get("LinkKey", "Key", fallback=None)
long_term_key = config.get("LongTermKey", "Key", fallback=None)
ediv = config.get("LongTermKey", "EDiv", fallback=None)
rand = config.get("LongTermKey", "Rand", fallback=None)

if not link_key and not long_term_key:
raise KeyError("Neither LinkKey->Key nor LongTermKey->Key exist")
# fmt: off
return {
"name": config.get("General", "Name"),
"class": config.get("General", "Class", fallback=None),
"pairing_key": config.get("LinkKey", "Key"),
"pairing_key": link_key,
"long_term_key": long_term_key,
"ediv": ediv,
"rand": rand
}
# fmt: on

Expand All @@ -61,4 +71,7 @@ def bluetooth_device_factory(device_info_path):
name=info["name"],
pairing_key=info["pairing_key"],
adapter_mac=macs["adapter_mac"],
ltk=info["long_term_key"],
ediv=int(info["ediv"]) if info["ediv"] else None,
rand=int(info["rand"]) if info["rand"] else None
)
13 changes: 7 additions & 6 deletions bt_dualboot/bt_sync_manager/bt_sync_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def devices_both_synced(self):
synced_devices = [
index[mac][0]
for mac in common_devices_macs
if index[mac][0].pairing_key == index[mac][1].pairing_key
if index[mac][0].synced(index[mac][1])
]

return synced_devices
Expand All @@ -132,7 +132,7 @@ def devices_needs_sync(self):
needs_sync_devices = [
index[mac][0]
for mac in common_devices_macs
if index[mac][0].pairing_key != index[mac][1].pairing_key
if not index[mac][0].synced(index[mac][1])
]

return needs_sync_devices
Expand Down Expand Up @@ -187,10 +187,8 @@ def _update_windows_registry(self, devices):
"""
for_import = {}
for device in devices:
section_key = self._get_reg_adapter_section_key(device)
device_key = f'"{mac_to_reg_key(device.mac)}"'
pairing_key = hex_string_to_reg_value(device.pairing_key)
for_import[section_key] = {device_key: pairing_key}
sec_key, value = device.for_win_import()
for_import[sec_key] = value

self.windows_registry.import_dict(for_import)

Expand Down Expand Up @@ -230,6 +228,9 @@ def push(self, device_or_mac_or_list, dry_run=False):
raise DeviceNotFoundError(f"Can't push {device_mac}! Not found on Windows!")

device_windows.pairing_key = device_linux.pairing_key
device_windows.ltk = device_linux.ltk
device_windows.rand = device_linux.rand
device_windows.ediv = device_linux.ediv
devices_for_update.append(device_windows)

if dry_run is not True:
Expand Down
35 changes: 35 additions & 0 deletions bt_dualboot/bt_windows/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,31 @@ def mac_to_reg_key(mac):

return "".join(mac.split(":")).lower()

def int_from_hex_b_reg(hex_b_string_reg):
"""Convert hex b string from Windows registry format
Args:
hex_b_string_reg (str): kind of 'hex(b):34,f5,ae,1f,df,cd,00,00'

Returns:
int
"""
_, value = hex_b_string_reg.split(":")
hex_string = "".join(list(value.split(",")))
return int.from_bytes(bytes.fromhex(hex_string), "little")

def int_from_dword_reg(dword_string_reg):
"""Convert hex b string from Windows registry format
Args:
hex_b_string_reg (str): kind of 'hex(b):34,f5,ae,1f,df,cd,00,00'

Returns:
int
"""
_, value = dword_string_reg.split(":")
# it is supposed to be little endian
# see: https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types
# however, Microsoft is lying and it is in fact big endian
return int.from_bytes(bytes.fromhex(value), "big")

def hex_string_from_reg(hex_string_reg):
"""Convert hex string from Windows registry format
Expand All @@ -87,6 +112,16 @@ def hex_string_to_reg_value(hex_string):
value = ",".join(hex_string_to_pairs(hex_string.lower()))
return f"hex:{value}"

def hex_string_to_reg_hex_b(hex_string):
"""Convert hex string to Windows registry value (b)
Args:
hex_string_reg (str): kind of 'A61B7F1BD9A35F3CF7E675EF2161A836'

Returns:
str: kind of 'hex(b):a6,1b,7f,1b,d9,a3,5f,3c,f7,e6,75,ef,21,61,a8,36'
"""
value = ",".join(hex_string_to_pairs(hex_string.lower()))
return f"hex(b):{value}"

def _unquote(value):
"""unquote value is quoted
Expand Down
57 changes: 43 additions & 14 deletions bt_dualboot/bt_windows/devices.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import re
from .convert import mac_from_reg_key, hex_string_from_reg, is_mac_reg_key
from .convert import mac_from_reg_key, hex_string_from_reg, is_mac_reg_key, int_from_dword_reg, int_from_hex_b_reg
from bt_dualboot.bluetooth_device import BluetoothDevice

REG_KEY__BLUETOOTH_PAIRING_KEYS = r"ControlSet001\Services\BTHPORT\Parameters\Keys"

def get_device_nodes(from_sections):
"""Extract devices nodes from a section list
Args:
from_sections (list[str]): result of 'sections()' function on ...\\Services\\BTHPORT\\Parameters\\Keys section

Returns:
list[tuple]: tuple with three elements (adapter mac address, device mac address, section key)
"""
nodes = []
search_re = r"Services.BTHPORT.Parameters.Keys.([a-f0-9]+)\\([a-f0-9]+)$"
for sec in from_sections:
node = re.search(search_re, sec)
if node is not None:
nodes.append((mac_from_reg_key(node.groups()[0]), mac_from_reg_key(node.groups()[1]), sec))

return nodes

def extract_adapter_mac(from_section_key):
"""Extracts adapter MAC from section key
Expand All @@ -14,7 +30,7 @@ def extract_adapter_mac(from_section_key):
str: adapter MAC kind of 'D4:6D:6D:97:62:9B'
"""

res = re.search("Services.BTHPORT.Parameters.Keys.([a-f0-9]+)$", from_section_key)
res = re.search("Services.BTHPORT.Parameters.Keys.([a-f0-9]+)(.[a-f0-9]+)?$", from_section_key)
if res is None:
return None

Expand All @@ -28,29 +44,42 @@ def get_devices(windows_registry):

Returns:
list<BluetoothDevice>
NOTE: filled only `mac`, `adapter_mac` and `pairing_key`
NOTE: If bluetooth < 5.1: filled only `mac`, `adapter_mac` and `pairing_key`
Else: filled only `mac`, `adapter_mac`, `ltk`, `rand`, `ediv`
"""

reg_data = windows_registry.export_as_config(REG_KEY__BLUETOOTH_PAIRING_KEYS)

bluetooth_devices = []

for section_key in reg_data.keys():
adapter_mac = extract_adapter_mac(section_key)
if adapter_mac is None:
continue

section = reg_data[section_key]
for device_mac_raw, pairing_key_raw in section.items():
if not is_mac_reg_key(device_mac_raw):
continue

bluetooth_devices.append(
BluetoothDevice(
source=BluetoothDevice.source_windows(),
mac=mac_from_reg_key(device_mac_raw),
adapter_mac=adapter_mac,
pairing_key=hex_string_from_reg(pairing_key_raw),
if is_mac_reg_key(device_mac_raw):
bluetooth_devices.append(
BluetoothDevice(
source=BluetoothDevice.source_windows(),
mac=mac_from_reg_key(device_mac_raw),
adapter_mac=adapter_mac,
pairing_key=hex_string_from_reg(pairing_key_raw),
)
)
# bluetooth 5.1 devices
for adapter_mac, device_mac, section_key in get_device_nodes(reg_data.sections()):
ltk = reg_data.get(section_key, '"ltk"')
erand = reg_data.get(section_key, '"erand"')
ediv = reg_data.get(section_key, '"ediv"')
bluetooth_devices.append(
BluetoothDevice(
source=BluetoothDevice.source_windows(),
mac=device_mac,
adapter_mac=adapter_mac,
ltk=hex_string_from_reg(ltk),
rand=int_from_hex_b_reg(erand),
ediv=int_from_dword_reg(ediv)
)
)

return bluetooth_devices