14
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ちょっとだけセキュアな認証システムをLINE Thingsでやってみた

Last updated at Posted at 2019-05-28

LINE Thingsの自動通信機能を応用して、電波が届くエリアに入ったら認証コードを送信して
LINEのLIFFを使って認証している端末に認証コードを送信するシステムを作ってみました。

認証コードはAmazon Connectを使って電話がかかってくるパターンとLINE Notifyを使って通知する2パターンを試しました。

動作デモ

アーキテクチャ

LINE Thingsの自動通信でWebhookのURLへ飛ばして、AWS LambdaからAmazon ConnectとLINE Notifyの処理を実行しています。

s100.png

一連の流れ

1. M5Stackのプログラム

特に難しいことはしていません。LINE Things連携済みの携帯がIoTデバイスに近づいたら認証コードを作成して端末に送信しています。

M5Stack.ino
#include <M5Stack.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// Device Name: Maximum 30 bytes
#define DEVICE_NAME "LINE Things Meetup Demo"

// あなたのサービスUUIDを貼り付けてください
#define USER_SERVICE_UUID "<あなたのサービスUUID>"
// Notify UUID: トライアル版は値が固定される
#define NOTIFY_CHARACTERISTIC_UUID "62FBD229-6EDD-4D1A-B554-5C4E1BB29169"
// PSDI Service UUID: トライアル版は値が固定される
#define PSDI_SERVICE_UUID "E625601E-9E55-4597-A598-76018A0D293D"
// LIFFからのデータ UUID: トライアル版は値が固定される
#define WRITE_CHARACTERISTIC_UUID "E9062E71-9E62-4BC6-B0D3-35CDCD9B027B"
// PSDI CHARACTERISTIC UUID: トライアル版は値が固定される
#define PSDI_CHARACTERISTIC_UUID "26E2B12B-85F0-4F3F-9FDD-91D114270E6E"

BLEServer* thingsServer;
BLESecurity* thingsSecurity;
BLEService* userService;
BLEService* psdiService;
BLECharacteristic* psdiCharacteristic;
BLECharacteristic* notifyCharacteristic;
BLECharacteristic* writeCharacteristic;

bool deviceConnected = false;
bool oldDeviceConnected = false;
bool sendFlg = false;
bool unlockFlg = false;

// 認証コード
int randNumber;

class serverCallbacks: public BLEServerCallbacks {

  // デバイス接続
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;

    // 一度認証されないとコードは生成しない
    if (unlockFlg) {
      sendFlg = false;
      unlockFlg = false;
    }
  };

  // デバイス未接続
  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
    
    if (unlockFlg) {
      sendFlg = false;
      unlockFlg = false;
    }
  }
};

// LIFFから送信されるデータ
class writeCallback: public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *bleWriteCharacteristic) {
    // LIFFから来るデータを取得
    std::string value = bleWriteCharacteristic->getValue();
    int myNum = atoi(value.c_str());

    // 認証コードと一致しているか確認
    if (randNumber == myNum) {
      M5.Lcd.fillScreen(GREEN);
      M5.Lcd.setCursor(0, 30);
      M5.Lcd.print("Unlock!");
      unlockFlg = true;    
    }    
  }
};

void setup() {
  Serial.begin(115200);
  BLEDevice::init("");
  BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT_NO_MITM);

  // Security Settings
  BLESecurity *thingsSecurity = new BLESecurity();
  thingsSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND);
  thingsSecurity->setCapability(ESP_IO_CAP_NONE);
  thingsSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);

  setupServices();
  startAdvertising();
  
  // put your setup code here, to run once:
  M5.begin();  
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextSize(2);

}

void loop() {
  if (!deviceConnected && oldDeviceConnected) {
    delay(500); // Wait for BLE Stack to be ready
    thingsServer->startAdvertising(); // Restart advertising
    oldDeviceConnected = deviceConnected;
    Serial.println("Restart!");
  }

  // Connection
  if (deviceConnected && !oldDeviceConnected) {
    oldDeviceConnected = deviceConnected;
  }

  if (!sendFlg && deviceConnected){
    sendFlg = true;
    delay(5000);

    // 認証コード生成
    randNumber = random(1000, 10000);
    char newValue[16];
    sprintf(newValue, "%d", randNumber);

    // 認証コードをWebhookURLに送信
    notifyCharacteristic->setValue(newValue);
    notifyCharacteristic->notify();
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 30);
    M5.Lcd.print("Send!!");
    delay(5000);
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 30);
    M5.Lcd.print("Connected");  /*表示クリア*/
        
    delay(50);
    
  } else if (!deviceConnected) {
    M5.Lcd.setCursor(0, 30);
    M5.Lcd.print("Not Connected");

  }

  M5.update();
  
}

// サービス初期化
void setupServices(void) {
  // Create BLE Server
  thingsServer = BLEDevice::createServer();
  thingsServer->setCallbacks(new serverCallbacks());

  // Setup User Service
  userService = thingsServer->createService(USER_SERVICE_UUID);

  // LIFFからのデータ受け取り設定
  writeCharacteristic = userService->createCharacteristic(WRITE_CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_WRITE);
  writeCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED);
  writeCharacteristic->setCallbacks(new writeCallback());
  
  // Notifyセットアップ
  notifyCharacteristic = userService->createCharacteristic(NOTIFY_CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_NOTIFY);
  notifyCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED);
  BLE2902* ble9202 = new BLE2902();
  ble9202->setNotifications(true);
  ble9202->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED);
  notifyCharacteristic->addDescriptor(ble9202);

  // Setup PSDI Service
  psdiService = thingsServer->createService(PSDI_SERVICE_UUID);
  psdiCharacteristic = psdiService->createCharacteristic(PSDI_CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ);
  psdiCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED);

  // Set PSDI (Product Specific Device ID) value
  uint64_t macAddress = ESP.getEfuseMac();
  psdiCharacteristic->setValue((uint8_t*) &macAddress, sizeof(macAddress));

  // Start BLE Services
  userService->start();
  psdiService->start();
}

void startAdvertising(void) {
  // Start Advertising
  BLEAdvertisementData scanResponseData = BLEAdvertisementData();
  scanResponseData.setFlags(0x06); // GENERAL_DISC_MODE 0x02 | BR_EDR_NOT_SUPPORTED 0x04
  scanResponseData.setName(DEVICE_NAME);

  thingsServer->getAdvertising()->addServiceUUID(userService->getUUID());
  thingsServer->getAdvertising()->setScanResponseData(scanResponseData);
  thingsServer->getAdvertising()->start();
}

2. Lambdaの処理

Amazon Connectで電話をかける処理とLINE Notifyへ送信する処理を実装しています。

index.js
const Util = require('util.js');

exports.handler = async (event) => {
    
    const body = JSON.parse(event.body);
    const thingsData = body.events[0].things.result;

    if (thingsData.bleNotificationPayload) {
        // LINE Thingsから飛んでくるデータを取得
        const blePayload = thingsData.bleNotificationPayload;
        var buffer1 = new Buffer(blePayload, 'base64');
        var m5Data = buffer1.toString('ascii');  //Base64をデコード
        
        const sendMessage = `認証コードは「${m5Data}」です。`;
        
        // Amazon Connect送信
        await Util.callMessageAction(m5Data);
        
        // LINE Notify送信
        await Util.postNotify(sendMessage);

    }  
};
util.js
'use strict';
const request = require('request');
const AWS = require('aws-sdk');
var connect = new AWS.Connect();

// あなたのトークンを設定してください
const headers = {
  Authorization: `Bearer ${process.env.NOTIFY_TOKEN}`,
  'Content-Type': 'application/x-www-form-urlencoded'
};

// 電話をかける処理
module.exports.callMessageAction = async function callMessageAction(message) {
    return new Promise(((resolve, reject) => {
    
        // 1桁ずつ読ませるために分けている
        const no1 = message.substr(0, 1);
        const no2 = message.substr(1, 1);
        const no3 = message.substr(2, 1);
        const no4 = message.substr(3, 1);
        
        // Attributesに発話する内容を設定
        var params = {
            Attributes: {"message": `<speak><break time='2s' />認証コードは、<prosody rate="65%"><say-as interpret-as="digits">${no1}</say-as>、<say-as interpret-as="digits">${no2}</say-as>、<say-as interpret-as="digits">${no3}</say-as>、<say-as interpret-as="digits">${no4}</say-as></prosody>です。</speak>`},
            InstanceId: process.env.INSTANCEID,
            ContactFlowId: process.env.CONTACTFLOWID,
            DestinationPhoneNumber: process.env.PHONENUMBER,
            SourcePhoneNumber: process.env.SOURCEPHONENUMBER
        };
    
        // 電話をかける
        connect.startOutboundVoiceContact(params, function(err, data) {
            if (err) {
                console.log(err);
                reject();
            } else {
                resolve(data);
            }
        });
        
    }));
};

// LINE Notifyへ送信
module.exports.postNotify = async function postNotify(postMessage) {
    // オプションを定義
    const jsonData =
    {
      'message': postMessage
    };

    const options = {
      url: 'https://notify-api.line.me/api/notify',
      method: 'POST',
      headers: headers,
      form: jsonData
    };

    return new Promise(function (resolve, reject) {
      request(options, function (error, response, body) {
        if (!error) {
          console.log(body.name);
          resolve(true);
        } else {
          console.log('error: ' + response + body);
          resolve(true);
        }
      });
    });
};

3. Amazon Connect側の設定

Attributesのmessageの内容をAmazon Connectの問い合わせフローで発話するように設定しています。
全体のフローは以下の通り。問い合わせ属性の設定部分でmessageの内容を取得します。

聞き逃しがないように認証コードは2回ループさせています。

詳細は下記サイトをご確認ください。
https://dev.classmethod.jp/cloud/aws/ask-linebot-to-call-from-amazon-connect-api/

s101.png

問い合わせ属性の設定は以下の通り。宛先キーと属性にmessageを設定しています。

s102.png

4. LINE Notifyの設定

こちらからトークンを発行します。
https://notify-bot.line.me/my/

s103.png

うまく連携できればLINEに通知されます。
s104.png

5. LIFFアプリの設定

LIFF側は今回Vue.jsでやってみました。
プログラムはのサンプルはこちらで公開しています。

index.htmlでLIFFのSDKを設定しています。UIライブラリはVuetifyを使っています。

public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <script src="https://d.line-scdn.net/liff/1.0/sdk.js"></script>
    <title>linethings-meetup-demo</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
  </head>
  <body>
    <noscript>
      <strong>We're sorry but linethings-meetup-demo doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

実際の画面はこちら

src/components/Helloworld.vue
<template>
  <v-app id="inspire">
    <v-container>
      <v-layout
        text-xs-center
        wrap
      >
        <v-flex mb-4>
          <h1 class="display-2 font-weight-bold mb-3">
            認証画面
          </h1>
        </v-flex>

        <v-flex xs12>
          <v-form>
            <v-container>
              <v-layout row wrap>
                <v-text-field
                  v-model="code"
                  @keyup.enter="say"
                  @keypress="setCanSubmit"
                  outline
                  label="認証コードを入力してください"
                ></v-text-field>
              </v-layout>
            </v-container>
          </v-form>  
        </v-flex>

        <v-flex xs12>
          {{this.bleStatus}}
        </v-flex>
      </v-layout>
    </v-container>
  </v-app>
</template>

<script>
  export default {
    data () {
      return {
        USER_SERVICE_UUID: '<あなたのサービスUUID>',
        LED_CHARACTERISTIC_UUID: 'E9062E71-9E62-4BC6-B0D3-35CDCD9B027B', /* トライアルは固定値 */
        bleConnect: false,
        canSubmit: false,
        bleStatus: '',
        state: false,
        characteristic: '',
        code: '',
        user: {
          image: '',
          userId: ''
        }        
      }
    },
    methods: {
      setCanSubmit () {
        this.canSubmit = true
      },
      say () {
        this.sendData()
      },
      sendData () {
        this.bleStatus = `送信:${this.code}`

        let ch_array = this.code.split("");
        for(let i = 0; i < 16; i = i + 1){
          ch_array[i] = (new TextEncoder('ascii')).encode(ch_array[i]);
        }

        this.characteristic.writeValue(new Uint8Array(ch_array)
        ).catch(error => {
          this.bleStatus = error.message
        })
      },
      // BLEが接続できる状態になるまでリトライ
      liffCheckAvailablityAndDo: async function (callbackIfAvailable) {
        try {
          const isAvailable = await liff.bluetooth.getAvailability();
          if (isAvailable) {
            callbackIfAvailable()
          } else {
            // リトライ
            this.bleStatus = `Bluetoothをオンにしてください。`
            setTimeout(() => this.liffCheckAvailablityAndDo(callbackIfAvailable), 10000)
          }
        } catch (error) {
          alert('Bluetoothをオンにしてください。')
        }
      },
      // サービスとキャラクタリスティックにアクセス
      liffRequestDevice: async function () {
        const device = await liff.bluetooth.requestDevice()
        await device.gatt.connect()
        const service = await device.gatt.getPrimaryService(this.USER_SERVICE_UUID)
        service.getCharacteristic(this.LED_CHARACTERISTIC_UUID).then(characteristic => {
          this.characteristic = characteristic
          this.bleConnect = true
          this.bleStatus = `デバイスに接続しました!`
        }).catch(error => {
          this.bleConnect = true
          this.bleStatus = `デバイス接続に失敗=${error.message}`
        })
      },
      initializeLiff: async function(){
        await liff.initPlugins(['bluetooth']);
        this.liffCheckAvailablityAndDo(() => this.liffRequestDevice())

        // プロフゲット
        const profile = await liff.getProfile()
        this.user.image = profile.pictureUrl
        this.user.userId = profile.userId
      }
    },
    mounted: function () {
      liff.init(
          () => this.initializeLiff()
      )
    }
  }
</script>

まとめ

認証コードはM5Stackで保持しているので、ちょっとだけセキュアです。
一度認証させると次回接続時は別のIDが生成されるようにもしています。一種のワンタイムパスワードみたいなものですね。
LINE Thingsを使えば、様々な連携をすることができます。是非試してみてください!

システム化のご検討やご相談は弊社までお問い合わせください。
https://i-enter.co.jp/contact/

14
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?