一、概述
每个蓝牙设备都包含了自己的蓝牙地址,地址表现了其设备唯一性,而蓝牙地址类型多样,例如手机端的蓝牙地址可能是私有地址类型的,简单理解,就是它的地址具有一定的统一性,如最高位的两个字节数据是 0 或 1 都表示了不同的地址类型,而可解析私有地址,其特点就是可解析,因此,在外在看来,它的地址具有一定的伪装性,需要通过密钥解析才能获取到设备真正的地址。在客户支持中,部分客户要实现蓝牙 RSSI 定位功能,而其定位模块不与手机端进行连接,只负责扫描手机广播,但如果安卓手机地址类型为可解析地址,其 MAC 地址会周期性改变,因此需要手机连接的蓝牙设备端将地址进行获取和解析,然后将地址传输给定位模块端去进行白名单过滤,从而达到只测量目标设备位置的效果。以下我们就来熟悉 RPA 地址信息的获取。
二、开发环境搭建(SDK、硬件、开发工具 IDE)
- SDK 下载
本文基于 NXP KW38 IC,SDK 可在官网的 Select Board | MCUXpresso SDK Builder (nxp.com) 进行下载,也可在 MCUXpresso IDE 中下载。
- 硬件
本文基于 KW38 EVK 开发板进行开发,EVK 板如下图所示。
图一
- 开发工具 IDE
SDK 支持 IAR、MCUXpresso IDE,本文基于 MCUXpresso 为开发环境进行讲解,测试采用 KW38 HID Device 例程。
三、RPA 地址概念熟悉
私有地址:设备如果使用私有地址广播,其实容易导致一些混乱的情况。
私有地址是指周期性变化的随机地址,比如每隔 15 分钟变换一次。这意味着,即使你发现了一个目前正处于广播态的设备,30 分钟之后,它可能换了个完全不一样的地址,届时你将难以判断那个设备是否还在附近。乍一看来,这似乎是一个不可能解决的问题。
要解决该问题,需要执行三个步骤:
第一步是在绑定时保存 IRK 密钥;
第二步是使用该密钥生成一个可解析的私有地址;
最后,主设备必须扫描所有的设备,利用其所有的 IRK 解析每一个地址。只有能够验明身份的地址才能进行连接。
可解析的私有地址是一类随机地址,包括三个部分。
第一部分是固定模式的两位组,用来表明该随机地址是可解析的私有地址。这就减少了扫描设备的运算负担,它们只需要解析那些可解析的私有地址即可。
第二部分是一个 22 位的随机数。
第三部分是随机数与在绑定时分享的 IRK 的哈希值。
将随机数和 IRK 相结合,意味着每一个使用私有地址的设备实际上拥有四百万个可用地址作为指纹。要连接使用私有地址的从设备,主设备必须扫描所有的可解析的私有地址,取出其随机数,并结合每一个可能使用私有地址的设备的 IRK,分别计算对应该设备的哈希值。如果得到的哈希值与可解析地址中的哈希值相匹配,那么有相当高的概率可以确定此设备即为密钥对应的那台设备。
当然,万事无绝对。也有可能另一台设备拥有不同的 IRK 和相同的随机数,也能生成完全相同的哈希值。然而,快速的连接和加密技术还能够进一步检查该设备正确与否。IRK、用于加密的 LTK 以及用于签名认证的 CSRK 在不同设备上各不相同,它们可以很快地确认到底连接的是正确的设备,还是发生了哈希值重复的小概率事件,以致连接了一个错误的设备。
私有地址存在一些缺点。最大的缺点在于,主设备每次收到一个可解析的私有地址,都要依次结合各个 IRK 来执行暴力校验。如果主机知道许多私有的设备,可能就要耗费大量的时间。在这种情况下,HCI 的加密命令将会非常有用,尤其是在主机的计算资源比较有限的时候。
私有地址的另一个缺点是,无法利用白名单来简化连接。连接到私有设备的唯一方法是先扫描可解析的私有地址,计算其是否属于可以建立连接的私有地址,而后手动执行连接。由于主机必须执行许多地址解析的工作,这无形中增加了主机的能量消耗。
使用设备地址和地址类型识别设备;地址类型可以是公网设备地址,也可以是随机设备地址。公共设备地址和随机设备地址的长度都是 48 位。
一个设备至少要使用一种类型的设备地址,也可以同时包含两种类型的地址。设备的身份地址是它在传输的数据包中使用的公共设备地址或随机静态设备地址。如果一个设备正在使用可解析私有地址,它也应该有一个身份地址。当对两个设备地址进行比较时,比较中必须包含设备地址类型(即如果两个地址类型不同,即使两个 48 位地址相同,它们也不相同)。
Public Address:一般写入在固件中,不能改变的地址。
图二
Random Address:
另一种类型的地址,可分为:Static Address 和 Private Address
Random Static Address 一般是开机自动产生,由 Host 通过 Set Random Address 传递给 Controller。
图三
Private Address 可分为 Resolvable 和 Non-Resolvable 两种类型,当设备同时拥有对端 IRK 和本地 IRK 的时候,就可以将 Resolvable Private Address 解析
成 Identify Address。
Private Device Address 生成:
图四
如何生成 RPA(Resolvable Private Address):
为了连接过程中的安全,我们可以使用 RPA 地址与对端设备连接。每次连接,RPA 地址并不是固定的地址,但是拥有 IRK 的设备,能够解析 RPA 地址,指向相同的设备。
RPA 通过 IRK 和 prand 产生。可以产生自己的 RPA,也可以产生对端设备的 RPA。
图五
Private Device Address 解析:
图六
RPA 解析方式:
LocalHash = ah(IRK,prand),接收到对端的 RPA 之后,可以计算出对端的 peerHash 值,前 24bit 即为对端的 hash。
localHash 与 peerHash 对比,就可以解析出是否是曾经配对过的设备。
RPA 解析的目的是:将 Random Address 转化为 Identify Address,然后获取到正确的 LTK 或者 GATT Cache。
Resolving List:
这个列表保护一些列对端和自己的 IRK 配对记录,列表维护在 Controller 中,可以不经过 Host,完成解析 RPA 功能。
此列表中的地址是 Identify Address,一个设备一个地址,通过确定的地址,找到正确的 IRK。
图七
Identify Address:
这类地址是一个抽象的概念,其作用就是识别设备的地址。
如果一个设备仅支持 Public Address,那么该 Public Address 可作为这个设备的 Identify Address,在配对过程中使用。
如果一个设备支持 Random Static Address,同样可以作为 Identify Address 使用。
如果一个设备使用的是 Resolvable Private Address,通过 IRK 解析之后的地址,才是 Identify Address。
四、API 使用说明
以下为获取对端设备 IRK 和 蓝牙地址的 API:
图八
在设备之间建立配对连接绑定时,设备端需要保存对端的绑定信息。在某些情况下,我们需要通过这些 API 对绑定数据进行管理,Gap_LoadKeys API 可以加载绑定设备的 Key,用户可以调用这个 API 函数来读取配对过程中交换的密钥,并在配对完成时由蓝牙协议栈存储在绑定区域中。API 参数包含一个输入参数,四个输出参数,首先是 nvmIndex ,表示 NVM 绑定区域的设备索引,如果是第一台绑定的设备,那么这个值为 0,第二台绑定设备是 1,以此类推。其余四个输出参数分别为配对时候分发的密钥,声明的哪些密钥值是可用的,LE 安全配对的标志,MITM 防止中间人攻击的标志位。需要注意的是,密钥为指针变量,在初始化的时候,不单要分配密钥的地址,也要分配密钥数据的地址,否则 API 的返回值是 gBleInvalidParameter_c 无效参数。
五、实际代码测试
SDK 中默认打开配对和绑定功能,我们可以添加 RTT 驱动进行数据打印。 RTT 参考官网可访问:J-Link RTT – Real Time Transfer (segger.com) 首先在
BleConnManager_GapPeripheralEvent 函数的 case gConnEvtKeysReceived_c 事件中设备已经收到对端在配对过程中分发的 SMP 密钥,我们可以将配对过程
中交互的密钥数据打印出来,用于首次配对的 RPA 地址获取和 Gap_LoadKey API 数据的对比验证。
case gConnEvtKeysReceived_c:
{
/* Copy peer device address information when IRK is used */
if (pConnectionEvent->eventData.keysReceivedEvent.pKeys->aIrk != NULL)
{
#if gAppUseBonding_d
mPeerDeviceAddressType = pConnectionEvent->eventData.keysReceivedEvent.pKeys->addressType;
//add by rambo
log_debug(" gConnEvtKeysReceived_c Event: \r\n");
log_debug_array_ex("IRK:",pConnectionEvent->eventData.keysReceivedEvent.pKeys->aIrk,gcSmpIrkSize_c);
log_debug_array_ex("Peer Address:",pConnectionEvent->eventData.keysReceivedEvent.pKeys->aAddress, sizeof(bleDeviceAddress_t));
log_debug("\r\n");
#endif /* gAppUseBonding_d */
FLib_MemCpy(maPeerDeviceAddress, pConnectionEvent->eventData.keysReceivedEvent.pKeys->aAddress, sizeof(bleDeviceAddress_t));
}
}
break;
在 case gConnEvtConnected_c: 中,我们每次连接都可以通过 Gap_LoadKey API 获取相关地址和密钥信息,先定义一个输出的指针结构体,再通过数组去初始化指针,具体代码如下:
case gConnEvtConnected_c:
{
#ifndef gCentralInitiatedPairing_d
#if (defined(gAppUsePairing_d) && (gAppUsePairing_d == 1U))
#if (defined(gRepeatedAttempts_d) && (gRepeatedAttempts_d == 1U))
FLib_MemCpy(maPeerDeviceOriginalAddress, pConnectionEvent->eventData.connectedEvent.peerAddress, sizeof(bleDeviceAddress_t));
#endif
#if (defined(gAppUseBonding_d) && (gAppUseBonding_d == 1U))
bool_t isBonded = FALSE;
uint8_t nvmIndex = gInvalidNvmIndex_c;
/* Copy peer device address information */
mPeerDeviceAddressType = pConnectionEvent->eventData.connectedEvent.peerAddressType;
FLib_MemCpy(maPeerDeviceAddress, pConnectionEvent->eventData.connectedEvent.peerAddress, sizeof(bleDeviceAddress_t));
/* Perform pairing if peer is not bonded or resolution procedure for its address failed */
if ((gBleSuccess_c == Gap_CheckIfBonded(peerDeviceId, &isBonded, &nvmIndex) && FALSE == isBonded) ||
(Ble_IsPrivateResolvableDeviceAddress(maPeerDeviceAddress) &&
FALSE == pConnectionEvent->eventData.connectedEvent.peerRpaResolved))
#endif
{
(void)Gap_SendSlaveSecurityRequest(peerDeviceId, &gPairingParameters);
}
#endif
#endif
#if 1//定个数据,把out keys里的irk指针初始化一下
uint8_t aLtkTemp[16];
uint8_t aIrkTemp[16];
uint8_t aCsrkTemp[32];
uint8_t aRandTemp[16];
uint8_t aAddressTemp[6];
gapSmpKeys_t out_keys = { 0 };
out_keys.aAddress=aAddressTemp;
out_keys.aCsrk=aCsrkTemp;
out_keys.aIrk=aIrkTemp;
out_keys.aLtk=aLtkTemp;
out_keys.aRand=aRandTemp;
gapSmpKeyFlags_t out_key_flag = 0;
bool_t out_lesc = 0;
bool_t out_auth = 0;
if (gBleSuccess_c == Gap_CheckIfBonded(peerDeviceId, &isBonded, &nvmIndex) )
{
bleResult_t result = Gap_LoadKeys(nvmIndex, &out_keys, &out_key_flag, &out_lesc, &out_auth);
if(result == gBleSuccess_c)
{
log_debug("gConnEvtConnected_c Event:\r\n");
log_debug("Gap_LoadKeys result: %d,nvmIndex: %d.\r\n", result, nvmIndex);
log_debug_array_ex("aIrk stored:", out_keys.aIrk, gcSmpIrkSize_c);
log_debug_array_ex("Address stored:", out_keys.aAddress, 6);
log_debug("out_key_flag: %d,out_lesc: %d,out_auth: %d.\r\n",out_key_flag, out_lesc, out_auth);
log_debug("\r\n");
}
}
#endif
#if gConnUpdateAlwaysAccept_d
(void)Gap_EnableUpdateConnectionParameters(peerDeviceId, TRUE);
#endif
/* Initiate Data Length Update Procedure */
BleConnManager_DataLengthUpdateProcedure(peerDeviceId);
#if gConnInitiatePhyUpdateRequest_c
if ((mSupportedFeatures & ((uint32_t)gLe2MbPhy_c | (uint32_t)gLeCodedPhy_c)) != 0U)
{
(void)Gap_LeSetPhy(FALSE, peerDeviceId, 0, gConnPhyUpdateReqTxPhySettings_c, gConnPhyUpdateReqRxPhySettings_c, (uint16_t)gConnPhyUpdateReqPhyOptions_c);
}
#endif
}
break;
通过 Gap_CheckIfBonded(peerDeviceId, &isBonded, &nvmIndex) 检索设备是否处于绑定,以及获取 nvmIndex,通过 nvmIndex 去索引设备序号,最后通过 Gap_LoadKeys(nvmIndex, &out_keys, &out_key_flag, &out_lesc, &out_auth) 获取我们所需的密钥信息和设备蓝牙地址。
图九
通过 Log 可以看出,gConnEvtConnected_c 和 gConnEvtKeysReceived_c 事件中获取到的 IRK 和蓝牙地址是完全匹配的,其余的密钥信息也可以打印出来。
RPA 地址的解析是通过 Controller 控制器进行操作的,并不需要我们去计算,这大大简化了过程。
六、参考文档
【1】Bluetooth Low Energy Application Developer Guide.pdf
【2】Core_V5.0.pdf
【3】低功耗蓝牙开发权威指南.pdf
评论