BLE蓝牙键盘在macOS/Windows双启动场景下的配对

Posted on Jan 30, 2023

起因

最近两把老键盘陆续挂掉,于是先后入手了两把新的:NIZ宁芝的X87EC三模1Keydous的NJ81双模2。用下来都还可以,特别是两把的产地都是本地海边小渔村,感觉就更亲切了。

然而好景不长。我的电脑是 MacBookPro (16-inch, 2019),平时主要用 macOS (Catalina 10.15.7),偶尔会要切到 BootCamp 的 Windows 10 中做一些测试的事情。这两把键盘都支持3套蓝牙Profile,也就是可以保存3套蓝牙配对信息并快速切换连接。在macOS下我都使用键盘的BT1来配对使用;当reboot到Windows时,按我之前使用其它蓝牙外设(例如Logitech MX Master 3)的经验,我应该切换到BT2在Windows中再进行一次配对,然后就视当前启动的是哪个OS,就切到对应的Profile来连接使用即可。这套用法在两把新键盘这边卡壳了:当我在Windows下用BT2配对好正常使用后,再reboot回macOS,发现BT1已经无法连接;重新用BT1再进行一次配对,然后reboot到Windows,BT2也无法连接… 也就是每次切换OS想用键盘都需要重新配对~~~

简单分析一下。电脑是同一台,无论是哪一个OS,电脑端的BTADDR是不变的。蓝牙键盘与电脑配对,是一个协商通讯密钥的过程,每一次配对应该都会生成不同的密钥。这两把键盘在BT2配对后,会影响到BT1,可能是因为这两组配对中,对端的BTADDR相同。猜测键盘为了实现某些自动连接切换的功能,内部以REMOTE_BTADDR为Key进行配对数据的存储,因此后一次的配对(生成了新的密钥)会影响到前一次的配对

双启动场景蓝牙配对问题

能支持多套蓝牙配对的设备是近些年才多起来的,那么之前的老蓝牙设备在双启动场景下如何配对使用呢?搜索了一下,被这个问题困扰的人还真不少:

特别的,这一篇刚好讲的就是macOS和BootCamp的场景:macOS 与 BootCamp 双系统共用同一蓝牙设备指引

那么回到之前的问题,是不是我固定用BT1,然后按前述文章进行系统配置的调整,就能够愉快的玩耍了呢?

仔细看了这几篇文章,简单小结一下:

  1. 蓝牙设备与电脑配对时,会动态生成一组LinkKey用于后继的连接与通讯,这个数据会被保存在蓝牙设备的Device memory和电脑OS的配置文件/注册表中;
  2. Windows中的LinkKeys保存在注册表的HKLM\CurrentControlSet\Services\BTHPORT\Parameters\Keys\{PC_BTADDR}\下,以蓝牙设备的地址为Name。这个注册表路径的拥有者是SYSTEM账户,普通用户或Administrator用户是看不到的,但可以使用PsExec工具以SYSTEM账户身份启动regedit.exe来查看/修改;
  3. macOS的LinkKeys保存在配置文件/private/var/root/Library/Preferences/com.apple.bluetoothd.plist中,可以用sudo来操作;
  4. 将蓝牙设备先与OS1进行配对,然后切换到OS2再次配对,再将OS2中此蓝牙设备所对应的LinkKey数据提取出来(一段16进制数据),回到OS1,将对应LinkKey修改为所提取的值,重启;
  5. 完工!

这么简单,那就动手吧。。。然而(总是会有个然而的)惊喜的发现,无论在macOS下还是Windows下,对应位置的LinkKeys中都没有发现这两把键盘对应的数据:

~ sudo defaults read /private/var/root/Library/Preferences/com.apple.bluetoothd.plist
{
    DaemonIDSPairedDevices =     (
    ...
    );
    LinkKeys =     {
        "f8-ff-c2-11-7b-48" =         {
            // 蓝牙耳机1
            "40-ed-98-18-1a-0e" = {length = 16, bytes = 0xae19dc9d31d5c191f8bbf41156237e1e};
            // 蓝牙耳机2
            "48-d8-45-21-5a-06" = {length = 16, bytes = 0x1caca400190ad53d083a746c746dd39f};
        };
    };
    ...
    SMPDistributionKeys =     {
        "f8-ff-c2-11-7b-48" =         {
            ...
            "d8-55-68-45-f2-8f" =             {  // NIZ
                EDIV = {length = 2, bytes = 0x8311};
                LTK = {length = 16, bytes = 0xff47efec32f655fd21b566f59229629a};
                LTKLength = {length = 1, bytes = 0x10};
                LocalEDIV = {length = 2, bytes = 0x2f2e};
                LocalLTK = {length = 16, bytes = 0x9df745c1b4c02a7615283952aaa743a9};
                LocalLTKLength = {length = 1, bytes = 0x10};
                LocalRAND = {length = 8, bytes = 0xee7fd5113ca1000b};
                MITMProtection = 1;
                OriginalAddressType = 1;
                RAND = {length = 8, bytes = 0x5db7379e20a85841};
            };
            "dc-e7-4e-b7-87-a3" =             {  // Keydous
                Address = {length = 6, bytes = 0xdce74eb787a3};
                AddressType = 1;
                EDIV = {length = 2, bytes = 0x2775};
                IRK = {length = 16, bytes = 0xc462bdac49bd3ba83413d7275b6c907b};
                LTK = {length = 16, bytes = 0x643c1ac0f28aba9c172712d07989bdcd};
                LTKLength = {length = 1, bytes = 0x10};
                LocalEDIV = {length = 2, bytes = 0xfdc0};
                LocalLTK = {length = 16, bytes = 0x49b1071161884e1cf197d070bfd59c57};
                LocalLTKLength = {length = 1, bytes = 0x10};
                LocalRAND = {length = 8, bytes = 0x499b792c837eba4d};
                MITMProtection = 1;
                OriginalAddressType = 1;
                RAND = {length = 8, bytes = 0x23c17164bf0de0fb};
            };
            ...

啥情况?对应的LinkKey不存在,反而在SMPDistributionKeys下有一堆数据。这些EDIV, IRK, LTK, RAND又是什么鬼?

继续爬文…

原来LinkKey是老版本蓝牙设备(例如我的蓝牙耳机)所使用的,Bluetooth LE设备,会使用新的密钥分发机制。因为涉及到硬件和通讯,要理解细节是有难度的,但为了解决这个问题去通读Specification3应该也是没必要。这篇文章4对BLE的安全连接流程有个概要性的描述,而这一篇5中有对上述术语的具体解释:

  • Identity Resolving Key (IRK): 128-bit key used to generate and resolve random address.
  • Connection Signature Resolving Key (CSRK): 128-bit key used to sign data and verify signatures on the receiving device.
  • Long Term Key (LTK): 128-bit key used to generate the session key for an encrypted connection.
  • Encrypted Diversifier (EDIV): 16-bit stored value used to identify the LTK. A new EDIV is generated each time a new LTK is distributed.
  • Random Number (RAND): 64-bit stored value used to identify the LTK. A new RAND is generated each time a unique LTK is distributed.

从实际用途来说,LTK类似于之前的LinkKey,LTK/EDIV/RAND这三者的关系,在这一篇6中有说明。

上面还出现了LocalLTK/LocalEDIV/LocalRAND,这些值在Windows的注册表中没有存储,看起来像是配对过程中本地生成的数据,可以先忽略。

最后,找到了这个链接:How to fix BLE devices #12,其中foxevereg的回答说的很详细。

实践

理论基础准备的差不多了,那就实际开工吧。

从原理上说,既可以选择将LTK/EDIV/RAND等从macOS同步到Windows,也可以从Windows同步到macOS。不过通过测试发现,NIZ这把键盘每配对一次,其BTADDR的最后一个字节会变化(一般是递增++),这样的话,还需要将BTADDR也进行同步的修改。通过搜索Windows注册表可知,有相当多的地方记录/引用了BTADDR。而在macOS中,只有两个plist文件中记录BTADDR,改动起来工作量小很多,不容易出错。因此,采用先在macOS中配对->切换到Windows中再次配对->从Windows注册表中提取数据->回到macOS中修改的方式。

NIZ X87EC

  1. 将它的BT1依次与macOS和Windows进行配对。
  2. 在Windows中以管理员身份启动一个命令行窗口,用cd命令进入到提前下载/解压好的PsExec工具目录,执行:
    psexec.exe -s -i regedit.exe
    
    这样就以SYSTEM账户身份启动了注册表编辑器。
  3. 找到HKLM\CurrentControlSet\Services\BTHPORT\Parameters\Keys\f8ffc2117b48\,这里的f8ffc2117b48是我笔记本的蓝牙地址。在这下面找d8556845f2开头的key,只有一个,就是这把NIZ。将这个key导出为注册表文件,保存到C盘根目录:
    Windows Registry Editor Version 5.00
    
    [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\BTHPORT\Parameters\Keys\f8ffc2117b48\d8556845f290]
    "LTK"=hex:68,19,78,48,fd,cb,eb,7a,46,dd,9d,65,87,79,b2,f7
    "KeyLength"=dword:00000010
    "ERand"=hex(b):db,5d,9e,37,20,a8,41,58
    "EDIV"=dword:0000fc1e
    "MasterIRKStatus"=dword:00000001
    "AuthReq"=dword:0000002d
    
  4. Reboot回到macOS,此时BT1是连接不上的。在Desktop上新建一个目录niz_bt,挂载BOOTCAMP分区,将上一步导出的注册表文件拷贝到这个目录。
  5. 首先看一下两个蓝牙相关配置文件的权限设置和Owner,后面修改完需要改成同样的再覆盖回去:
     sudo ls -al /private/var/root/Library/Preferences/com.apple.bluetoothd.plist
    -rw-------  1 root  wheel  2767 Jan 29 20:19 /private/var/root/Library/Preferences/com.apple.bluetoothd.plist
     sudo ls -al /Library/Preferences/com.apple.Bluetooth.plist
    -rw-r--r--  1 root  wheel  64988 Jan 29 20:19 /Library/Preferences/com.apple.Bluetooth.plist
    
  6. 将两个配置文件复制到niz_bt目录,修改Owner为当前用户使得它们可以被编辑:
     sudo cp /private/var/root/Library/Preferences/com.apple.bluetoothd.plist .
     sudo cp /Library/Preferences/com.apple.Bluetooth.plist .
     sudo chown zhuyie:staff com.apple.bluetoothd.plist
     sudo chown zhuyie:staff com.apple.Bluetooth.plist
     ls -al
    total 160
    drwxr-xr-x   6 zhuyie  staff    192 Jan 29 21:02 .
    drwx------@ 12 zhuyie  staff    384 Jan 29 21:02 ..
    -rw-r--r--   1 zhuyie  staff  64988 Jan 29 21:02 com.apple.Bluetooth.plist
    -rw-------   1 zhuyie  staff   2767 Jan 29 21:02 com.apple.bluetoothd.plist
    -rwxr-xr-x@  1 zhuyie  staff    710 Jan 29 21:01 niz.reg
    
  7. 首先修改com.apple.bluetoothd.plist,可以用Xcode直接编辑。需要修改的地方包括:蓝牙地址/LTK/EDIV/RAND。RAND对应到Windows注册表中的值名称是"ERand",EDIV的值需要调转字节序,也就是由注册表中的fc1e改为plist中的1EFC
  8. 然后修改com.apple.Bluetooth.plist。用"查找-替换"的方式将所有原蓝牙地址修改为和Windows一致的新地址。
  9. 禁用蓝牙!!!
  10. 将修改后的plist文件的Owner恢复为原始状态,覆盖回原路径,重启系统:
     sudo chown root:wheel com.apple.bluetoothd.plist
     sudo chown root:wheel com.apple.Bluetooth.plist
     sudo cp com.apple.bluetoothd.plist /private/var/root/Library/Preferences/com.apple.bluetoothd.plist
     sudo cp com.apple.Bluetooth.plist /Library/Preferences/com.apple.Bluetooth.plist
    
  11. 重新启用蓝牙,此时NIZ应该可以连接上啦!Reboot到Windows,仍然可以正常连接!

Keydous NJ81

  1. Keydous就更简单一点,因为它多次配对时BTADDR不会变,因此就省去了修改com.apple.Bluetooth.plist的功夫。
  2. 数据上Keydous这把会多出一个值LRK需要同步修改,而且这个值和EDIV一样需要调转字节序
  3. 其它步骤和前面一致。

References