11.13. 配對與綁定

到目前為止介紹的一切,都是以明文形式在無線電上傳輸位元組。任何人只要在同一個房間裡有一台支援 BLE 的筆記型電腦,就能在廣播通道上監聽、追蹤一條開放連線的跳頻序列,並讀出來回傳輸的每一次讀取、寫入與通知。對於大多數公開的感測器資料(電池電量、環境溫度)來說,這沒什麼問題。但對於兩個端點想保密的任何內容——一個用來啟動繼電器的控制暫存器、一組密碼、一筆不該被廣泛廣播的量測值——這條連結就必須加密,而且理想情況下相機需要知道它正在和誰通訊。

BLE 透過配對綁定兩者來提供這些保護。

11.13.1. 配對、綁定、加密

三個密切相關的概念:

  • 加密 是最根本的目標。一旦連結被加密,資料通道上的每個封包就只有這兩個端點能解讀;竊聽者看到的只是雜訊。

  • 配對 是兩個端點為了協商出加密所用的金鑰而執行的程序。它是一次性的交換,會產生連結層插入其加密引擎的共用金鑰素材。

  • 綁定 是在配對完成後選擇將金鑰保存到非揮發性儲存空間,如此一來同樣兩台裝置之間的下一次連線就能跳過配對,直接進入加密。

用白話來說:配對是「自我介紹」;綁定是「記住這次介紹」;加密是「從現在起私下交談」。

兩欄分別標示「Central」與 「Peripheral」。靠上方有一條虛線 標示「BLE connection open (unencrypted)」。 其下方有三個箭頭:從 central 指向 peripheral 的「pairing request」、雙向的 「key exchange」、向前的「pairing complete」。 下方第二條虛線標示「link encrypted」。兩個粗的雙向箭頭 承載「encrypted GATT traffic」。側邊有一個選用的 「store keys to flash」方框,標示為 「bonding」。

建立在開放 BLE 連線之上的配對流程。一旦金鑰交換完成,連結層就會加密後續的每個封包。綁定是將金鑰寫入快閃記憶體的額外步驟。

11.13.2. LE Secure Connections

BLE 使用的現代金鑰交換方式是 LE Secure Connections,它建立在橢圓曲線 Diffie-Hellman 之上。雙方各自產生一對臨時金鑰、交換其中的公開部分,再將結果與自己的私鑰結合,得到相同的共用祕密——即使竊聽者完整記錄了整個交換過程,也無法計算出這個祕密。

較舊的 LE Legacy 方法較不安全(取得完整交換內容的竊聽者通常能還原出金鑰),它的存在只是為了與舊的 peripheral 向後相容。aioble 的預設值就是現代方法(le_secure=True);請維持原樣。

11.13.3. 發起配對

central 透過在一條已開放的連線上呼叫 aioble.DeviceConnection.pair() 來進行配對:

async with await device.connect() as connection:
    await connection.pair(bond=True, le_secure=True, mitm=False)
    # ... GATT work, now over an encrypted link ...

pair 回傳之後,連線上的 encryptedauthenticatedbondedkey_size 屬性會反映出協商的結果。

四個最實用的關鍵字引數:

  • bond=True ——將產生的金鑰儲存到快閃記憶體,讓同樣兩台裝置之間的下一次連線跳過配對交握。預設為 True

  • le_secure=True ——使用 LE Secure Connections。預設為 True。請保持開啟。

  • mitm=False ——是否要求中間人(man-in-the-middle)防護。這需要一個頻外(out-of-band)通道(一方顯示、另一方確認的數字代碼,輸入的密碼金鑰,……),讓使用者能驗證配對交握中的這兩台裝置真的就是他們所認為的那兩台。預設為 False(沒有 MITM 防護——被動竊聽者無法讀取連結,但主動重新導向連線的攻擊者卻可能把自己配對進來)。對任何敏感的應用都應設為 True,但要注意這需要 peripheral 實際支援某種 IO 能力。

  • io=3 ——裝置宣稱的 IO 能力。Bluetooth 規格定義了五種:0 僅顯示、1 顯示加是/否、2 僅鍵盤、3 無輸入無輸出、4 鍵盤加顯示。沒有使用者介面的相機通常回報 3;如果相機本身有顯示器,應用程式就能顯示數字確認碼並使用 1。兩端 IO 能力的組合決定了是否能達成真正的 MITM 防護。

peripheral 不會自己呼叫 pair——它們只是回應 central 所發起的任何動作。某個特徵是否需要加密,是它在 GATT 資料庫中宣告方式的一項屬性;需要加密的存取位元屬於低階的 bluetooth API,目前並未透過 aioble 的特徵建構子公開。

11.13.4. 綁定——以及金鑰存放在哪裡

bond=True 時,aioble 會把金鑰寫入本機檔案系統上的一個 JSON 檔。預設檔名是 ble_secrets.json,相對於目前的工作目錄寫入。在一台剛開機的 cam 上,_boot.py 已經選好了該目錄:掛載了記憶卡時為 /sdcard,否則為 /flash——因此檔案會落在 /sdcard/ble_secrets.json/flash/ble_secrets.json。這個檔案保存了下次已綁定對端重新連線時重新加密連結所需的項目,包括對端的識別位址。

有一個不對稱之處要記住:當金鑰變動時,儲存會自動發生,但下次開機時載入檔案卻不會。請在啟動時呼叫一次 aioble.security.load_secrets()(在任何配對或廣播之前),讓先前已綁定的對端能被辨識出來:

import aioble
aioble.security.load_secrets()        # default path: ble_secrets.json

在那之後,下次已綁定的對端出現時,aioble 會重新使用儲存的金鑰,連結便會直接進入加密,無需再進行任何交握。

將金鑰儲存在快閃記憶體上有兩個實際後果:

  • 讓裝置被遺忘。 刪除 ble_secrets.json(或移除相關的項目)即可忘記所有已綁定的對端,然後從頭重新配對。

  • 實體存取會洩漏金鑰。 任何能存取相機檔案系統的人都能讀取這個 JSON 檔。這與網路端 TLS 金鑰所提到的限制屬於同一類(運維作業:金鑰、到期與疑難排解):使用各裝置專屬的金鑰、將任何儲存的金鑰都視為可被取回,並依靠撤銷的能力(此處是在 central 端移除綁定),而非依靠金鑰本身保持祕密。

11.13.5. 加密保證了什麼——又沒保證什麼

一條先配對再加密的連結,依強度由高到低提供:

  • 機密性。 永遠成立。竊聽者無法讀取這些位元組。

  • 完整性。 永遠成立。被修改過的封包無法通過連結層的驗證加密檢查,會被丟棄。

  • 驗證。 只有在 mitm=True 並搭配具備能力的 IO 時才成立。沒有它的話,攔截了原始配對交換的中間人就可能把自己插了進來;在沒有 MITM 防護的情況下,雙方無從得知這件事。

對於大多數相機使用情境——一支手機與相機配對一次,之後再連線——mitm=False 通常就夠了,因為原始配對發生在可控的環境中(使用者在同一個房間裡同時拿著兩台裝置)。對於已配對裝置可能首次是在遠距離或透過不受信任的中介遇到相機的應用而言,MITM 才是正確的設定。

11.13.6. 什麼時候配對是錯誤的答案

配對有實際的代價:首次連線時幾秒鐘的交換、每台已綁定裝置都會持續占用快閃記憶體,以及出狀況時「忘記綁定」的復原流程。對於真正公開的資料——以信標形式發布的環境感測器讀數、一塊顯示自身名稱的看板、任何不會因為被讀取或寫入而改變世界的東西——正確的答案是完全不要加密,讓附近任何掃描器都能讀取這些數值。

至於其他所有情況,在 central 上加一行 connection.pair(bond=True) 就能把這條連結從公開頻道變成私密頻道。