基於 NXP KW38 RPA 藍牙地址密鑰信息獲取

關鍵字 :NXPBLERPAIRKKW38

一、概述

每個藍牙設備都包含了自己的藍牙地址,地址表現了其設備唯一性,而藍牙地址類型多樣,例如手機端的藍牙地址可能是私有地址類型的,簡單理解,就是它的地址具有一定的統一性,如最高位的兩個字節數據是 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

★博文內容均由個人提供,與平台無關,如有違法或侵權,請與網站管理員聯繫。

★文明上網,請理性發言。內容一周內被舉報5次,發文人進小黑屋喔~

評論