ホーム » Swift » 【Swift×BLE】iOSのBLEのセキュリティ関連の振る舞いを調べてみるby M5Stack

【Swift×BLE】iOSのBLEのセキュリティ関連の振る舞いを調べてみるby M5Stack

【Swift×BLE】CoreBluetoothの使い方 Swift

はじめに

CoreBluetoothの実装について記載していきます。

今回はCoreBluetoothの使いかたというよりもiOSのBLEのセキュリティ関連の振る舞いについて記述していきます。

【Swift】CoreBluetoothの使い方1:BLEにおける役割

【Swift】CoreBluetoothの使い方 その2:セントラルの実装例

【Swift】CoreBluetoothの使い方 その3:ペリフェラルの実装

【Swift】CoreBluetoothの使い方 その4:セントラルとペリフェラルで通信させてみる

【iOS】iOSのBLEのセキュリティ関連の振る舞いを調べてみるby M5Stack ←今回はここ

【Swift×CoreBluetooth】複数の画面(View)でBLE機能を使う場合の実装方法

【SwiftUI】SwiftUIにおけるCoreBluetoothセントラル機能の実装方法

【Swift×BLE】CoreBluetoothセントラルのバックグラウンドモード有効化方法と挙動

開発環境

  • iOS 15.5
  • M5Stack-Core2

導入

BLEの機器をiOSと接続する時に機器によって振る舞いが違います。

  • ペアリングの有無
  • ペアリング要求が出るタイミング
  • ボンディングの有無
  • パスキーの有無

それぞれに関して書いていきます。
また上記は全てM5StackにBLEペリフェラル側のコードを実装して検証しました。
なぜかというとセキュリティ関連の細かい設定はiOSのCoreBluetoothでは設定出来ないためです。

ペアリングの有無

最近の機器は基本的にペアリング有りだと思いますが、
ペアリング有りの機器と接続する時は以下のような表示がOSから出ます。

これは接続する機器のCharacteristicのPermissionによって決まります。
CharacteristicのPermissionにはAccess PermissionとSecurity modeがあります。

AccessPermission

  • NONE:読むことも書くこともできない
  • Readable:読み込みのみ可能
  • Writable:書込みのみ可能
  • Readable and writable:読み書き可能

SecurityMode

  • Security mode Lv1:暗号化なし
  • Security mode Lv2:このattributeにアクセスするには暗号化された接続が必要。ただし、暗号鍵による認証は必要としない。
  • Security mode Lv3:このattributeにアクセスするには認証された鍵で暗号化された接続が必要。

この中でSecurity modeの設定が暗号化有りで設定されている場合に上記ペアリングの要求が発生します

ペアリング要求が発生するタイミング

iOSの場合どのタイミングでペアリング要求が発生するのかというと、上記SecurityModeが設定されているキャラクタリスティックにアクセスしたタイミングです。接続したタイミングではありません。

そのためCoreBluetoothでアプリを作るとき、どのタイミングで接続完了とするかについては、必要なキャラクタリスティック全てにアクセスすることが出来るようになってからが良いと思います。

ちなみに機器側から暗号化要求を出すことも可能です、接続後すぐに機器側から暗号化要求を出せば、接続後すぐにペアリングメッセージを表示することが出来ます。
なお、ペアリングメッセージはiOSが出します。

ボンディングの有無

ボンディングとはペアリングで交換した暗号鍵を覚えておくかどうかです。
ペアリングとボンディングを同義に書いているものもありますが厳密に違います。

ボンディング有の場合は以下のように接続が切断された後も自分のデバイスに表示されたままとなります。

ボンディング無の場合は接続中は自分のデバイスに表示されますが、切断されると自分のデバイスから無くなります。

有無が何で決まるかというとペアリングを実施したときにペリフェラル側でボンディング有無をセントラルに送っています。その情報でセントラルはボンディングを実行するかどうか判断します。

なお、こちらはiOSが処理をするためアプリが側で何か処理を入れる必要はありません。

パスキーの有無

パスキーとは機器側で事前に設定したペアリングに必要なキーで、パスキーが必要な機器と接続した場合、ペアリング時に以下のメッセージが表示されます。

有無が何で決まるかというとペアリングを実施したときにペリフェラル側でパスキー有無をセントラルに送っています。その情報でセントラルはパスキーが必要かどうかを判断します。

なお、こちらはiOSが処理をするためアプリが側で何か処理を入れる必要はありません。

検証用のM5Stackのソースコード

セキュリティ関連の検証は手軽に実装出来るM5Stackがおすすめです。
また、使用するライブラリはNimBLEがおすすめです。
細かい設定が可能です。

NimBLE-Arduino - Arduino Reference
The Arduino programming language Reference, organized into Functions, Variable and Constant, and Structure keywords.

今回検証用に作成したソースコードを下記します。


#include <M5Core2.h>
#include <NimBLEDevice.h>

#define LOCAL_NAME                  "M5Stack-Secure"
#define COMPLETE_LOCAL_NAME         "M5Stack-Secure"

#define SERVICE_UUID                "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA"
#define CHARACTERISTIC_UUID_RX      "AAAAAAAA-BBBB-AAAA-AAAA-AAAAAAAAAAAA"//out
#define CHARACTERISTIC_UUID_NOTIFY  "AAAAAAAA-CCCC-AAAA-AAAA-AAAAAAAAAAAA"//in


//画像
#include "image.cpp"
//Characteristic
NimBLECharacteristic * pNotifyCharacteristic;
//NimBLEServer
NimBLEServer *pServer = NULL;

bool deviceConnected = false;
bool oldDeviceConnected = false;

bool isButtonAPressed = false;
bool isButtonBPressed = false;
bool isButtonCPressed = false;

uint8_t data_buff[2];  // データ通知用バッファ

class ServerCallbacks: public NimBLEServerCallbacks {
    void onConnect(NimBLEServer* pServer) {
        Serial.println("Client connected");
        deviceConnected = true;
    };
    //SecurytyRequestをペリフェラルから送るとき
//    void onConnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) {
//        Serial.print("Client address: ");
//        Serial.println(NimBLEAddress(desc->peer_ota_addr).toString().c_str());
//
//        NimBLEDevice::startSecurity(desc->conn_handle);
//
//        pServer->updateConnParams(desc->conn_handle, 24, 48, 0, 60);
//        deviceConnected = true;
//    };
    void onDisconnect(NimBLEServer* pServer) {
        Serial.println("Client disconnected - start advertising");
        //NimBLEDevice::startAdvertising();
        deviceConnected = false;
    };
    void onMTUChange(uint16_t MTU, ble_gap_conn_desc* desc) {
        Serial.printf("MTU updated: %u for connection ID: %u\n", MTU, desc->conn_handle);
    };

    uint32_t onPassKeyRequest(){
        Serial.println("Server Passkey Request");
        return 123456; 
    };

    bool onConfirmPIN(uint32_t pass_key){
        Serial.print("The passkey YES/NO number: ");Serial.println(pass_key);
        return true; 
    };

    void onAuthenticationComplete(ble_gap_conn_desc* desc){
        if(!desc->sec_state.encrypted) {
            NimBLEDevice::getServer()->disconnect(desc->conn_handle);
            Serial.println("Encrypt connection failed - disconnecting client");
            return;
        }
        Serial.println("Starting BLE work!");
        M5.Lcd.drawString("Secure     ", 100, 95);
    };
};

//Bluetooth LE Recive
class MyCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      std::string rxValue = pCharacteristic->getValue();

      pCharacteristic->notify();
      
      unsigned char buffer[rxValue.length()];
      int intbuffer[rxValue.length()];
      memcpy(buffer, rxValue.data(), rxValue.length());
      int t;
      for (int i = 0;i<rxValue.length();i++){
        intbuffer[i] = (unsigned char)buffer[i];
        t = (unsigned char)buffer[i];
        Serial.println(t);
      }
    }
};


// Bluetooth LE loop
void loopBLE() {
    // disconnecting
    if (!deviceConnected && oldDeviceConnected) {
        delay(500); // give the bluetooth stack the chance to get things ready
        pServer->startAdvertising(); // restart advertising
        Serial.println("startAdvertising");
        oldDeviceConnected = deviceConnected;
        M5.Lcd.setTextSize(2);
        M5.Lcd.drawString("DisConnected    ", 90, 115);
        M5.Lcd.drawString("            ", 100, 95);
        M5.Lcd.fillRect(2, 10, 43, 54, BLACK);

    }
    // connecting
    if (deviceConnected && !oldDeviceConnected) {
    // do stuff here on connecting
       oldDeviceConnected = deviceConnected;
       M5.Lcd.setTextSize(2);
       M5.Lcd.drawString("Connected     ", 90, 115);
       M5.Lcd.drawBitmap(3,10,41,52,bleimage);
    }

    if (M5.BtnA.pressedFor(100)) {
      if(isButtonAPressed == false){
        Serial.println("BtnA pressed");
        isButtonAPressed = true;
        data_buff[0] = (int16_t)(11);
        data_buff[1] = (int16_t)(11);
        pNotifyCharacteristic->setValue(data_buff, 2);
        pNotifyCharacteristic->notify();
      }
    }else if(M5.BtnB.pressedFor(100)){
      if(isButtonBPressed == false){
        Serial.println("BtnB pressed");
        isButtonBPressed = true;
        data_buff[0] = (int16_t)(22);
        data_buff[1] = (int16_t)(22);
        pNotifyCharacteristic->setValue(data_buff, 2);
        pNotifyCharacteristic->notify();
        
      }
    }else if(M5.BtnC.pressedFor(100)){
      if(isButtonCPressed == false){
        Serial.println("BtnC pressed");
        isButtonCPressed = true;
        data_buff[0] = (int16_t)(22);
        data_buff[1] = (int16_t)(22);
        pNotifyCharacteristic->setValue(data_buff, 2);
        pNotifyCharacteristic->notify();
      }
    }else{
    }
    
    if (M5.BtnA.isReleased()) {
      if(isButtonAPressed == true){
        isButtonAPressed = false;
        Serial.println("BtnA rereiced");
        
      }
    }
    
    if(M5.BtnB.isReleased()){
      if(isButtonBPressed == true){
        isButtonBPressed = false;
        Serial.println("BtnB rereiced");
      }
    }

    if(M5.BtnC.isReleased()){
        if(isButtonCPressed == true){
        isButtonCPressed = false;
        Serial.println("BtnC rereiced");
      }
    }

    
}

void setup() {
    // Initialize the M5Stack object
  M5.begin();
  Serial.begin(115200);
  M5.Lcd.print("Setup....");

  M5.Lcd.println("Done");
  M5.Lcd.clear(TFT_BLACK);

  M5.Lcd.setTextSize(2);
  M5.Lcd.drawString("DisConnected    ", 90, 115);
  
  Serial.println("Starting NimBLE Server");
  //CompleteLocalNameのセット
  NimBLEDevice::init(COMPLETE_LOCAL_NAME);
  //TxPowerのセット
  NimBLEDevice::setPower(ESP_PWR_LVL_P9);

  //セキュリティセッティング
  //bonding,MITM,sc
  //セキュリティ無し
  NimBLEDevice::setSecurityAuth(false, false, false);
  //ボンディング有り
  //NimBLEDevice::setSecurityAuth(true, false, false);
  //ボンディング有り、mitm有り
  //NimBLEDevice::setSecurityAuth(true, true, false);
  //ボンディング有り、mitm有り,sc有り
  //NimBLEDevice::setSecurityAuth(true, true, true);
  //PassKeyのセット
  //NimBLEDevice::setSecurityPasskey(123456);
  //パラメータでディスプレイ有りに設定
  //NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY);
  //パラメータでInOut無しに設定
  //NimBLEDevice::setSecurityIOCap(BLE_HS_IO_NO_INPUT_OUTPUT);
  pServer = NimBLEDevice::createServer();
  pServer->setCallbacks(new ServerCallbacks());

  NimBLEService *pService = pServer->createService(SERVICE_UUID);

  //RxCharacteristic
  NimBLECharacteristic *pRxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_RX, NIMBLE_PROPERTY::WRITE);
  //NotifyCharacteristic
  //NoSec
  //pNotifyCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_NOTIFY, NIMBLE_PROPERTY::NOTIFY);
  //Need Enc
  //pNotifyCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_NOTIFY, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC);
  //Need Authen
  //pNotifyCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_NOTIFY, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_AUTHEN);
  //Need Enc Authen
  pNotifyCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_NOTIFY, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::READ_AUTHEN);

  //RxCharacteristicにコールバックをセット
  pRxCharacteristic->setCallbacks(new MyCallbacks());
  //Serivice開始
  pService->start();
  //アドバタイズの設定
  NimBLEAdvertising *pNimBleAdvertising = NimBLEDevice::getAdvertising();
  //アドバタイズするUUIDのセット
  pNimBleAdvertising->addServiceUUID(SERVICE_UUID);
  //アドバタイズにTxPowerセット
  pNimBleAdvertising->addTxPower();

  //アドバタイズデータ作成
  NimBLEAdvertisementData advertisementData;
  //アドバタイズにCompleteLoacaNameセット
  advertisementData.setName(COMPLETE_LOCAL_NAME);  
  //アドバタイズのManufactureSpecificにデータセット
  advertisementData.setManufacturerData("NORA");  
  //ScanResponseを行う
  pNimBleAdvertising->setScanResponse(true);
  //ScanResponseにアドバタイズデータセット
  pNimBleAdvertising->setScanResponseData(advertisementData);  
  //アドバタイズ開始
  pNimBleAdvertising->start();
  
}

void loop() {
  M5.update();
  loopBLE();
}

ボンディングの検証

//ボンディング有り,
NimBLEDevice::setSecurityAuth(true, false, false);

パスキーの検証

//ボンディング有り,mitm,sc
NimBLEDevice::setSecurityAuth(true, true, true);
//SecuritySetting;個別
//PassKeySet
NimBLEDevice::setSecurityPasskey(123456);
//DISPLAY有り
NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY);

さいごに

機器側の設定がどうなっているかを正しく把握することでアプリの振る舞いがどうなるかを把握できるので、BLEアプリを作る場合はペリフェラルも自分で作って検証することをお勧めします。

ios_practice/CoreBluetooth/M5Stack_sec_test/bleSec at main · junnonaka/ios_practice
iOS Practice Repository for my own. Contribute to junnonaka/ios_practice development by creating an account on GitHub.

iOSでBLEの勉強をするなら?

古い本ですが以下の書籍がおすすめです!
シーケンス図を用いたBLE自体の詳細な説明があり、その上でCoreBluetoothがどういった振る舞いをするのか、各メソッドがいつ呼ばれるかを実例を交えて説明しており、めちゃくちゃ参考になります。個人的にiOSでBLEやる上で必読だと思っています。

コメント

タイトルとURLをコピーしました