11.13. Parowanie i wiązanie

Wszystko, co omówiliśmy do tej pory, przesyła bajty przez radio w postaci jawnej. Każdy, kto ma laptop z obsługą BLE w tym samym pomieszczeniu, może nasłuchiwać na kanałach rozgłoszeniowych, śledzić sekwencję przeskoków otwartego połączenia i odczytać każdy odczyt, zapis i powiadomienie, które przez nie przechodzą. W przypadku większości publicznych danych z sensorów (poziom baterii, temperatura otoczenia) to nie stanowi problemu. Jednak dla wszystkiego, co oba punkty końcowe chcą zachować w prywatności – rejestru sterującego uzbrajającego przekaźnik, hasła, pomiaru, który nie powinien być szeroko rozgłaszany – łącze musi być szyfrowane, a najlepiej, gdyby kamera wiedziała, z kim rozmawia.

BLE zapewnia jedno i drugie poprzez parowanie i wiązanie.

11.13.1. Parowanie, wiązanie, szyfrowanie

Trzy ściśle powiązane pojęcia:

  • Szyfrowanie to nadrzędny cel. Gdy łącze jest zaszyfrowane, każdy pakiet na kanałach danych może odszyfrować tylko dwa punkty końcowe; podsłuchujący widzi szum.

  • Parowanie to procedura, którą oba punkty końcowe wykonują, aby uzgodnić klucze używane przez szyfrowanie. Jest to jednorazowa wymiana, która tworzy wspólny materiał kluczowy podłączany przez warstwę łącza do jej silnika szyfrującego.

  • Wiązanie to decyzja o utrwaleniu kluczy w pamięci nieulotnej po zakończeniu parowania, dzięki czemu następne połączenie między tymi samymi dwoma urządzeniami pomija parowanie i przechodzi bezpośrednio do szyfrowania.

Mówiąc prościej: parowanie to „przedstawcie się”; wiązanie to „zapamiętaj to przedstawienie”; szyfrowanie to „od teraz rozmawiajmy prywatnie”.

Dwie kolumny oznaczone "Central" i "Peripheral". Linia przerywana w pobliżu góry oznaczona "BLE connection open (unencrypted)". Poniżej trzy strzałki: "pairing request" od central do peripheral, "key exchange" w obu kierunkach, "pairing complete" do przodu. Druga linia przerywana poniżej oznaczona "link encrypted". Dwie grube dwukierunkowe strzałki przenoszą "encrypted GATT traffic". Opcjonalny blok "store keys to flash" z boku, oznaczony "bonding".

Przebieg parowania na bazie otwartego połączenia BLE. Po zakończeniu wymiany kluczy warstwa łącza szyfruje każdy kolejny pakiet. Wiązanie to dodatkowy krok zapisania kluczy w pamięci flash.

11.13.2. LE Secure Connections

Nowoczesna wymiana kluczy stosowana przez BLE to LE Secure Connections, zbudowana na bazie Elliptic Curve Diffie-Hellman. Obie strony generują tymczasową parę kluczy, wymieniają się publicznymi połówkami i łączą wynik z własnymi kluczami prywatnymi, aby dojść do tego samego wspólnego sekretu – sekretu, którego podsłuchujący nie jest w stanie obliczyć nawet przy pełnym zapisie wymiany.

Starsza metoda LE Legacy jest mniej bezpieczna (podsłuchujący dysponujący pełną wymianą zwykle może odzyskać klucz) i istnieje wyłącznie ze względu na wsteczną zgodność ze starymi urządzeniami peryferyjnymi. Domyślną metodą w aioble jest metoda nowoczesna (le_secure=True); zostaw ją.

11.13.3. Inicjowanie parowania

Urządzenie centralne paruje się, wywołując aioble.DeviceConnection.pair() na już otwartym połączeniu:

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

Po zwróceniu przez pair atrybuty encrypted, authenticated, bonded oraz key_size połączenia odzwierciedlają to, co zostało wynegocjowane.

Cztery najbardziej przydatne argumenty nazwane:

  • bond=True – zapisuje wynikowe klucze w pamięci flash, aby następne połączenie między tymi samymi dwoma urządzeniami pomijało uzgadnianie parowania. Domyślnie True.

  • le_secure=True – używa LE Secure Connections. Domyślnie True. Pozostaw włączone.

  • mitm=False – czy wymagać ochrony przed atakiem man-in-the-middle. Wymaga to kanału poza pasmem (kodu numerycznego wyświetlanego po jednej stronie i potwierdzanego po drugiej, klucza dostępu wpisanego ręcznie, …), aby użytkownik mógł zweryfikować, że dwa urządzenia biorące udział w uzgadnianiu parowania to faktycznie te, za które je uważa. Domyślnie False (brak ochrony MITM – pasywny podsłuchujący nie może odczytać łącza, ale atakujący aktywnie przekierowujący połączenia mógłby sam się sparować). Ustaw na True w przypadku czegokolwiek wrażliwego, ale pamiętaj, że wymaga to, aby urządzenie peryferyjne faktycznie obsługiwało jakąś zdolność IO.

  • io=3 – zdolność IO deklarowana przez urządzenie. Specyfikacja Bluetooth definiuje pięć: 0 tylko wyświetlacz, 1 wyświetlacz + tak/nie, 2 tylko klawiatura, 3 brak wejścia, brak wyjścia, 4 klawiatura + wyświetlacz. Kamera bez interfejsu użytkownika zazwyczaj zgłasza 3; jeśli sama kamera ma wyświetlacz, aplikacja mogłaby wyświetlić potwierdzenie numeryczne i użyć 1. Kombinacja zdolności IO obu stron decyduje o tym, czy możliwa jest realna ochrona MITM.

Urządzenia peryferyjne same nie wywołują pair – odpowiadają na to, co zainicjuje urządzenie centralne. To, czy dla danej charakterystyki wymagane jest szyfrowanie, jest właściwością sposobu jej zadeklarowania w bazie danych GATT; bity dostępu wymagające szyfrowania są częścią niskopoziomowego API bluetooth i obecnie nie są udostępnione przez konstruktor charakterystyki w aioble.

11.13.4. Wiązanie – i gdzie znajdują się klucze

Gdy bond=True, aioble zapisuje klucze do pliku JSON w lokalnym systemie plików. Domyślna nazwa pliku to ble_secrets.json, zapisywanego względem bieżącego katalogu roboczego. Na świeżo uruchomionej kamerze _boot.py wybrał już ten katalog: /sdcard, gdy zamontowana jest karta, w przeciwnym razie /flash – więc plik trafia do /sdcard/ble_secrets.json lub /flash/ble_secrets.json. Plik przechowuje wpisy potrzebne do ponownego zaszyfrowania łącza przy następnym połączeniu związanego peera, w tym adres tożsamości peera.

Jedna asymetria, o której warto pamiętać: zapisywanie odbywa się automatycznie wraz ze zmianą kluczy, ale wczytywanie pliku przy następnym uruchomieniu już nie. Wywołaj aioble.security.load_secrets() raz przy starcie (przed jakimkolwiek parowaniem lub rozgłaszaniem), aby wcześniej związane peery zostały rozpoznane:

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

Następnie, przy kolejnym pojawieniu się związanego peera, aioble ponownie wykorzystuje zapisane klucze i łącze zostaje zaszyfrowane bez dalszego uzgadniania.

Dwie praktyczne konsekwencje przechowywania kluczy w pamięci flash:

  • Zapominanie urządzenia. Usuń ble_secrets.json (lub usuń odpowiedni wpis), aby zapomnieć wszystkie związane peery, a następnie sparuj ponownie od zera.

  • Dostęp fizyczny ujawnia klucze. Każdy, kto ma dostęp do systemu plików kamery, może odczytać JSON. To takie samo ograniczenie, które pojawiło się po stronie sieci w przypadku kluczy TLS (Operacje: klucze, wygasanie i rozwiązywanie problemów): używaj kluczy per urządzenie, traktuj każdy przechowywany klucz jako możliwy do odzyskania i polegaj na możliwości unieważnienia (tutaj: usunięcia wiązania po stronie urządzenia centralnego), a nie na tym, że klucz pozostanie tajny.

11.13.5. Co gwarantuje szyfrowanie – a czego nie

Łącze typu parowanie-a-następnie-szyfrowanie zapewnia, w kolejności siły:

  • Poufność. Zawsze. Podsłuchujący nie może odczytać bajtów.

  • Integralność. Zawsze. Zmodyfikowane pakiety nie przechodzą kontroli uwierzytelnionego szyfrowania warstwy łącza i są odrzucane.

  • Uwierzytelnienie. Tylko z mitm=True i odpowiednią zdolnością IO. Bez tego man-in-the-middle, który przechwycił pierwotną wymianę parowania, mógł się w nią wstawić; bez ochrony MITM obie strony nie mają możliwości się o tym dowiedzieć.

W większości zastosowań kamery – telefon parujący się z kamerą raz, a następnie łączący się ponownie później – mitm=False zwykle wystarcza, ponieważ pierwotne parowanie odbywa się w kontrolowanym środowisku (użytkownik trzyma oba urządzenia w tym samym pomieszczeniu). W zastosowaniach, gdzie sparowane urządzenie może po raz pierwszy zetknąć się z kamerą na dużą odległość lub przez niezaufanego pośrednika, MITM jest właściwym ustawieniem.

11.13.6. Kiedy parowanie jest złą odpowiedzią

Parowanie ma realny koszt: kilka sekund wymiany przy pierwszym połączeniu, trwałe zużycie pamięci flash dla każdego związanego urządzenia oraz scenariusz odzyskiwania w postaci „zapomnij wiązanie”, gdy coś pójdzie nie tak. W przypadku naprawdę publicznych danych – odczytów z sensora otoczenia publikowanych jako beacon, znaku wyświetlającego swoją nazwę, czegokolwiek, co nie zmienia świata przez sam odczyt lub zapis – właściwą odpowiedzią jest nieszyfrowanie w ogóle i pozwolenie dowolnemu pobliskiemu skanerowi na odczytanie wartości.

We wszystkich pozostałych przypadkach connection.pair(bond=True) po stronie urządzenia centralnego to jednolinijkowy dodatek, który zamienia łącze z kanału publicznego w prywatny.