Edited at

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

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でやってみました。

プログラムはのサンプルはこちらで公開しています。

https://github.com/gaomar/linethings-meetup-demo

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/