IFTTT, Beebotte, Mosquitto, SSL/TLS を使って, なるべく安全に Google Home から Raspberry Pi や ESP8266 を操作する

# 概要
Google Home や Google Assistant から LAN 内部にある Raspberry Pi や ESP8266 を **なるべく安全に** 操作するための設定です.

特に注意したのは, **トークンやパスワードを通信する際は必ず暗号化された通信経路を使う** ということです. トークンやパスワードが第三者に知られてしまうと, 自宅内のデバイスが勝手に操作されてしまうかもしれません.

IFTTT や Beebotte との連携について丁寧に解説された記事がいくつかあります(各記事へのコメントはこのページ末尾の "参考" に書きました).

- [IFTTTとBeebotteを使ってGoogleHomeからRaspberryPiを操作する](https://qiita.com/msquare33/items/9f0312585bb4707c686b)
- [Google AssistantとRaspberry Piで自宅の家電を操作する](https://qiita.com/104ki/items/9dcfe03246099d03d4dd)
- [IFTTTのトリガーおよびアクションをESP8266で実行する](https://qiita.com/mayfair/items/e761c788a9d8787bc610)

これらを参考にして, さらに通信経路を暗号化することで, Google Home から Raspberry Pi や ESP8266 をなるべく安全に操作できるようにします.


## 検討したこと
### プッシュ通知をどう実現するか (プロトコル, 外部サービス)
1. メッセージの方向が外部サーバ(IFTTT や Beebotte)から LAN 内部(ESP8266 や Raspberry Pi)への, いわゆる「プッシュ通知」
1. でも ESP8266 や Raspberry Pi をサーバにしてポートを外部に公開したくない
1. MQTT サービスを使えばポートを外部に公開せずに比較的簡単に[「プッシュ通知」できる](https://www.google.co.jp/search?q=mqtt+push+%E9%80%9A%E7%9F%A5)らしい
1. Beebotte という MQTT ブローカーサービスを使った[作例が多い](https://qiita.com/search?q=ifttt+beebotte)
1. 当面 Beebotte 経由で「プッシュ通知」を受け取ることにする

### セキュリティをどう確保するか
1. IFTTT の Webhooks は複雑な認証が使えないので, トークンを使った単純な認証にならざるをえない(トークンが第三者に知られると危険)
1. トークンは定期的に変更することにする
1. 最低限, 認証情報(トークンやパスワード)が流れる通信経路は暗号化したい
1. でも ESP8266 はリソースが限られているため[SSL/TLSで安定動作させることが難しいらしい](https://github.com/esp8266/Arduino/issues/2733)
1. HTTP ならば, Raspberry Pi などに [nginx をインストールして SSL/TLS でリバースプロキシとして動作させて ESP8266 の負担を軽くする](https://github.com/esp8266/Arduino/issues/2733#issuecomment-264710703)のがよいらしい
1. MQTT で同様のことを実現するには, [MQTT ブリッジ接続](https://www.google.co.jp/search?q=mqtt+%E3%83%96%E3%83%AA%E3%83%83%E3%82%B8)を SSL/TLS で設定すれば, ESP8266 の負担を軽くしつつ外部との通信経路を暗号化できるはず
1. Raspberry Pi に MQTT ブローカーソフトウェアの Mosquitto をインストールして Beebotte とのブリッジ接続を SSL/TLS で設定して, ESP8266 と Beebotte を安全かつ低負荷で接続する

## システム構成
想定している構成は以下の通りです. 秘密にしておかなければならないトークンが流れる通信経路は SSL/TLS で暗号化されています. Mosquitto と ESP8266 の間は暗号化なしの MQTT ですがトークンは流しません(LAN 内をメッセージが平文で流れますが).

Google Home -- IFTTT --(Webhooks over HTTPS)-- Beebotte --(MQTT over SSL/TLS)-- Raspberry Pi の Mosquitto --(MQTT)-- ESP8266

以下, 各要素をセットアップする方法について説明します.

# Beebotte の設定
MQTT ブローカーサービスの [Beebotte](https://beebotte.com/) のセットアップを行います.

アカウントを作成してチャンネルとリソースを作成して, チャンネル名, リソース名, トークンをメモしておきます. 以下それぞれ `channel`, `resource`, `token_XXXXXXXXXXXXXXXX` として説明します.

詳しい設定方法は[こちらの記事](https://qiita.com/msquare33/items/9f0312585bb4707c686b#beebotte%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97)や[こちらの記事](https://qiita.com/104ki/items/9dcfe03246099d03d4dd#1-beebotte%E8%A8%AD%E5%AE%9A)を参照してください.

また, Beebotte のドキュメント [Beebotte MQTT Support](https://beebotte.com/docs/mqtt) と [Token Based Authentication](https://beebotte.com/docs/tokenauth) に目を通しておくとよいです.

なお, トークンが第三者に知られてしまったら何もかも操作されてしまうので, **トークンは定期的に変更したほうが安全です**.

# IFTTT の設定
Web サービス同士を連携させる Web サービス [IFTTT](https://ifttt.com/) でアカウントを作成して以下のように新規アプレットを作成します. 

**注意: Webhooks の URL の設定で必ず https を指定してください. http だとトークンがインターネット上を平文で流れます.**

|項目|操作または値|
|---|---|
|-|New Applet をクリック|
|New Applet|this をクリック|
|Choose a service|Google Assistant|
|Choose trigger|Say a phrase with a text ingredient|
| What do you want to say?|ラズパイで $|
|Language|Japanese|
|-|Create Trigger をクリック|
|-|that をクリック|
|Choose action service|Webhooks|
|Choose action|Make a web request|
|URL|https://api.beebotte.com/v1/data/publish/channel/resource?token=token_XXXXXXXXXXXX|
|Method|POST|
|Content Type|application/json|
|Body|{"data":"{{TextField}}"}
|-|Create action をクリック|
|Review and finish|Finish をクリック|
|-|Check now をクリック|

## IFTTT と Beebotte の連携テスト
ここまで設定すると, Google Home に "ラズパイで..." と話しかけると "..." 以下が Beebotte にテキストメッセージとして送られるはずです.

[Beebotte の console](https://beebotte.com/dev/console) を開いて `Secret Key`, `Channel`, `Resource` を入力して `Subscribe` を押してしておきます.

Google Home に "ラズパイで..." と話しかけて Beebotte console の Messages に以下のようなメッセージがでれば成功です. `ts` はタイムスタンプです. 

```
{ "channel": "channel", "resource": "resource", "eid": "channel.resource", "data": "...", "ts": 1515487525775 }
```

同様に スマートフォンの Google Assistant からもテストするとよいかもしれません.

# Python で Beebotte と MQTT over SSL/TLS で接続するスクリプト
[Beebotte のドキュメント](https://beebotte.com/docs/mqtt)には　MQTT over SSL/TLS で接続するサンプルがなかったのですが, 以下の Python スクリプトで接続できます.

あらかじめ `paho-mqtt` というパッケージを `pip` でインストールしておきます. また Beebotte の CA 証明書 [mqtt.beebotte.com.pem](https://beebotte.com/certs/mqtt.beebotte.com.pem) を Beebotte からダウンロードしてローカルに保存しておきます.

```python3:mqtt_beebotte.py
#! /usr/bin/env python3
# -*- coding: utf-8 -*-

import paho.mqtt.client as mqtt

HOST = 'mqtt.beebotte.com'
PORT = 8883
CA_CERTS = 'mqtt.beebotte.com.pem'
TOKEN = 'token_XXXXXXXXXXXX'
TOPIC = 'channel/resource'

def on_connect(client, userdata, flags, respons_code):
    print('status {0}'.format(respons_code))

def on_message(client, userdata, msg):
    print(msg.topic + ' ' + str(msg.payload))
    print(msg.payload.decode("utf-8"))

if __name__ == '__main__':
    client = mqtt.Client()
    client.on_connect = on_connect
    client.on_message = on_message
    client.username_pw_set('token:%s' % TOKEN)
    client.tls_set(CA_CERTS)
    client.connect(HOST, PORT)
    client.subscribe(TOPIC)
    client.loop_forever()
```
ターミナルから `mqtt_beebotte.py` を実行させておいて, Google Home に "ラズパイで..." と話しかけてターミナルに以下のようなメッセージがでれば成功です.

```
channel/resource b'{"data":"...","ispublic":true,"ts":1515490359831}'
{"data":"...","ispublic":true,"ts":1515490359831}
```

このスクリプトは [MQTT の Python クライアントで TLS 接続を試す](https://qiita.com/voluntas/items/76f013acdc42fb492b0c) という記事を参考に Beebotte 用に修正しました.

# Mosquitto の設定
MQTT ブローカーソフトウェアの　[Mosquitto](https://mosquitto.org/) を Raspberry Pi にインストールして設定します.

なお, **ESP8266 のような非力なデバイスを使わない場合は Mosquitto の設定は不要です.**　直接 MQTT over SSL/TLS で Beebotte に接続すれば OK です.

## Mosquitto のインストール
Raspberry Pi のターミナルから 

```
sudo apt-get install mosquitto mosquitto-clients
```

設定ファイルは `/etc/mosquitto/` 以下にあります.

## Beebotte の CA 証明書をダウンロード
Beebotte と　SSL/TLS 接続するために, あらかじめ Beebotte から CA 証明書をダウンロードして Mosquitto の証明書用ディレクトリに置きます.

```
curl -O https://beebotte.com/certs/mqtt.beebotte.com.pem
sudo mv mqtt.beebotte.com.pem /etc/mosquitto/certs/
```

詳しくは [Beebotte の MQTT による接続方法](https://beebotte.com/docs/mqtt)を参照してください.

## Mosquitto で　Beebotte と SSL/TLS でブリッジ接続する設定
Mosquitto の README によると `/etc/mosquitto/conf.d/` 以下に `.conf` ファイルを作って個別の設定を書けばよいとあるので, `/etc/mosquitto/mosquitto.conf` を直接編集せずに `/etc/mosquitto/conf.d/bridge-to-beebotte.conf` というファイルを新規作成することにします. 

`channel`, `resource`, `token_XXXXXXXXXXXXXXXX` は環境に合わせて書き換えてください.

```conf:/etc/mosquitto/conf.d/bridge-to-beebotte.conf
connection bridge-to-beebotte
address mqtt.beebotte.com:8883
bridge_cafile /etc/mosquitto/certs/mqtt.beebotte.com.pem
cleansession true
try_private false
bridge_attempt_unsubscribe false
notifications false
remote_username token:token_XXXXXXXXXXXXXXXX
topic channel/resource out 0
topic channel/resource in 0
```

パラメータについては, 主に[このページ](https://www.losant.com/blog/how-to-configure-mosquitto-bridge-to-losant)の設定を参考に Beebotte 用に編集しました. 各パラメータの説明は [Mosquitto のマニュアル](https://mosquitto.org/man/mosquitto-conf-5.html)を参照してください.

## Mosquitto 再起動
`restart` で再起動して `status` でステータスをみて問題なく起動してるか確認します.

```
sudo service mosquitto restart
sudo service mosquitto status
```

## Mosquitto のブリッジ接続のテスト
以下, `channel`, `resource`, `token_XXXXXXXXXXXXXXXX` は環境に合わせて書き換えてください.

テストのためにターミナルを3つ使います.

1つめのターミナルで, 以下のように Raspberry Pi の MQTT を subscribe しておきます (ESP8266 から　Raspberry Pi に MQTT 接続してメッセージを待つことを想定). 

```
mosquitto_sub -h (Raspberry Pi の IP) -t "channel/resource"
```

2つめのターミナルで, 以下のように Beebotte の　MQTT を subscribe しておきます (SSL/TLS が使えるデバイスから直接 Beebotte に MQTT over SSL/TLS 接続してメッセージを待つことを想定).

```
mosquitto_sub -h mqtt.beebotte.com -t "channel/resource" \
  --cafile /etc/mosquitto/certs/mqtt.beebotte.com.pem \
  -u "token:token_XXXXXXXXXXXX" -p 8883
```

3つめのターミナルで, 以下のような MQTT メッセージを publish します(ESP8266 から Raspberry Pi に MQTT メッセージを送ることを想定)

```
mosquitto_pub -h (Raspberry Pi の IP) -t "channel/resource" -m "Hello World"
```

1つめと2つめのターミナルに, それぞれ Hello World が表示されれば成功です. 1つめのターミナルには Hellow World が2回出力されるのですが理由がよくわかりません. どなたか教えてください :)

さらに, 3つめのターミナルで, 以下のような POST リクエストを Beebotte に送ります (IFTTT の Webhooks から Beebotte に HTTPS でメッセージを送ることを想定).

```
curl -d '{"data":"Hello World"}' -H "Content-Type: application/json" -X POST \
  https://api.beebotte.com/v1/data/publish/channel/resource?token=token_XXXXXXXXXXXX
```

1つめと2つめのターミナルに, 以下のような json が表示されれば成功です.

```
{"data":"Hello World","ispublic":true,"ts":1515424078032}
```

詳しくは [Beebotte の publish に関するドキュメント](https://beebotte.com/docs/publish)を参照してください.

さらに, Google Home に "ラズパイで..." と話しかけて, 1つめと2つめのターミナルに以下のような json が表示されれば成功です.

```
{"data":"...","ispublic":true,"ts":1515488512391}
```

ここまでできれば, Google Home から Raspberry Pi まで連携ができていることになります.

# ESP8266 から MQTT ブローカーに接続するスケッチ
Raspberry Pi の MQTT ブローカー(Mosquitto) に接続してメッセージを待ち, メッセージが来たら LED を点灯・消灯するための回路とスケッチです.

## 回路図

```
PIN 13 -- 330 Ω -- LED -- GND
```

## ESP8266 から Raspberry Pi に MQTT 接続するスケッチ
Arduino IDE のメニューから Sketch - Include Library - Manage Libraries... を選択して Library Manager を起動して, `PubSubClient` と `ArduinoJson` をあらかじめインストールしておきます.

`SSID` 等を環境に合わせて書き換えてください.

なお, **このコード中にはトークンが含まれていません(必要ないです)**

```cpp:mqtt_esp8266.ino
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

#define SSID "SSID"
#define PASSWORD "password"

#define MQTT_SERVER "(Raspberry Pi の IP)"
#define MQTT_PORT 1883
#define CLIENT_ID "ESP8266Client"
#define TOPIC "channel/resource"

#define LED_PIN 13

#define MAX_BUF 256

WiFiClient espClient;
PubSubClient client(espClient);

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(74880);

  WiFi.begin(SSID, PASSWORD);
  Serial.println(SSID);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.println(WiFi.localIP());

  client.setServer(MQTT_SERVER, MQTT_PORT);
  client.setCallback(callback);
  if (client.connect(CLIENT_ID)) {
    client.subscribe(TOPIC);
  }
}

void loop() {
  while (!client.connected()) {
    if (client.connect(CLIENT_ID)) {
      client.subscribe(TOPIC);
    } else {
      delay(5000);
    }
  }
  client.loop();
}

void callback(char* topic, byte* payload, unsigned int len) {
  // copy from payload to buf
  char buf[MAX_BUF];
  int end = len < MAX_BUF - 1 ? len : MAX_BUF - 1;
  for (int i = 0; i < end; i++) {
    buf[i] = (char)payload[i];
  }
  buf[end] = 0;
  Serial.println(buf);

  // convert buf to json
  StaticJsonBuffer<MAX_BUF> jsonBuffer;
  JsonObject& json = jsonBuffer.parseObject(buf);
  String data = json["data"];
  Serial.println(data);

  // commands
  if (-1 != data.indexOf("LED を つけ て")) {
    digitalWrite(LED_PIN, HIGH);
  } else if (-1 != data.indexOf("LED を 消し て")) {
    digitalWrite(LED_PIN, LOW);
  }
}
```

## ESP8266 から Beebotte に MQTT over SSL/TLS で直接接続するスケッチ
ついでに ESP8266 から Beebotte に MQTT over SSL/TLS で直接接続するスケッチも書いておきます. 通常の MQTT に比べて負荷が高いため ESP8266 で長期間安定動作するかどうかはわかりません(要検証).

Beebotte に直接接続するのでトークンを送る必要がありますが, トークンを送る前に通信相手が本当に `mqtt.beebotte.com` であるかどうかを検証(verify)する必要があります.

[通常のコンピュータではサーバの CA 証明書を用いて検証を行いますが, リソースの限られている ESP8266 では CA 証明書の代わりに CA 証明書の SHA1 fingerprint で接続先の確認をする](https://github.com/esp8266/Arduino/blob/master/doc/esp8266wifi/client-secure-examples.rst#how-to-verify-server-s-identity)ということらしいです.

まず, ターミナルから以下のコマンドを実行して CA 証明書の fingerprint を取得しておきます.

```
openssl x509 -fingerprint -in mqtt.beebotte.com.pem
```

出力の 1 行目に以下のような文字列があるので, コロンを空白に置換してソース中の `FINGERPRINT` に書いておきます.

```
SHA1 Fingerprint=XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
```

スケッチはこちらです. fingerprint の検証に失敗した場合は `exit(1)` します. **なお[証明書には期限がある](https://github.com/esp8266/Arduino/blob/master/doc/esp8266wifi/client-secure-examples.rst#is-it-that-server)ので, もし検証に失敗したら最新の CA 証明書を取得し直して fingerprint を計算し直してみてください.**

`SSID` 等を環境に合わせて書き換えてください.

```cpp:mqtt_tls_esp8266.ino
// https://github.com/esp8266/Arduino/blob/master/doc/esp8266wifi/client-secure-examples.rst
// https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/examples/HTTPSRequest/HTTPSRequest.ino
// https://github.com/knolleary/pubsubclient/issues/84#issuecomment-173677183
// https://gist.github.com/chaeplin/3223074601733fa46d4a

#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

#define SSID "SSID"
#define PASSWORD "password"

#define MQTT_SERVER "mqtt.beebotte.com"
#define MQTT_PORT 8883
#define CLIENT_ID "ESP8266Client"
#define TOPIC "channel/resource"
#define TOKEN "token:token_XXXXXXXXXXXX
// openssl x509 -fingerprint -in mqtt.beebotte.com.pem
#define FINGERPRINT "XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX"

#define LED_PIN 13

#define MAX_BUF 256

WiFiClientSecure espClient;
PubSubClient client(espClient);

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(74880);

  WiFi.begin(SSID, PASSWORD);
  Serial.println(SSID);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.println(WiFi.localIP());

  // need to verify fingerprint BEFORE sending token
  espClient.connect(MQTT_SERVER, MQTT_PORT);
  if (espClient.verify(FINGERPRINT, MQTT_SERVER)) {
    Serial.println("fingerprint matches");
  } else {
    Serial.println("fingerprint doesn't match");
    exit(1);
  }
  espClient.stop();

  client.setServer(MQTT_SERVER, MQTT_PORT);
  client.setCallback(callback);
  if (client.connect(CLIENT_ID, TOKEN, "")) {
    client.subscribe(TOPIC);
  }
}

void loop() {
  while (!client.connected()) {
    if (client.connect(CLIENT_ID, TOKEN, "")) {
      client.subscribe(TOPIC);
    } else {
      delay(5000);
    }
  }
  client.loop();
}

void callback(char* topic, byte* payload, unsigned int len) {
  // copy from payload to buf
  char buf[MAX_BUF];
  int end = len < MAX_BUF - 1 ? len : MAX_BUF - 1;
  for (int i = 0; i < end; i++) {
    buf[i] = (char)payload[i];
  }
  buf[end] = 0;
  Serial.println(buf);

  // convert buf to json
  StaticJsonBuffer<MAX_BUF> jsonBuffer;
  JsonObject& json = jsonBuffer.parseObject(buf);
  String data = json["data"];
  Serial.println(data);

  // commands
  if (-1 != data.indexOf("LED を つけ て")) {
    digitalWrite(LED_PIN, HIGH);
  } else if (-1 != data.indexOf("LED を 消し て")) {
    digitalWrite(LED_PIN, LOW);
  }
}
```

## テスト
Google Home から "ラズパイでLEDをつけて" と話しかけて ESP8266 の LED が点灯したら成功です. 同様に "ラズパイでLEDを消して" で消灯するはずです.

同様に スマートフォンの Google Assistant からもテストするとよいかもしれません.

"ラズパイで..." と話しかけて ESP8266 を操作するのはちょっと変な感じがするので IFTTT のアプレットを修正して別のメッセージにするとよいかもしれません.

以上で, Google Home から ESP8266 まで連携ができたことになります.

# まとめ
IFTTT, Beebotte, Mosquitto, SSL/TLS を使って, なるべく安全に Google Home から Raspberry Pi や ESP8266 を操作することができました. ポイントは以下です.

1. 秘密にしなければならないトークンを通信する際は必ず暗号化された通信経路を使う
1. 低リソースなデバイスも操作可能 (HTTP より軽い MQTTを使う. 重い SSL/TLS は Raspberry Pi に任せる)
1. トークンを定期的に変更するとより安全

# 参考
- https://qiita.com/msquare33/items/9f0312585bb4707c686b
IFTTTとBeebotteを使ってGoogleHomeからRaspberryPiを操作する という記事です. とても参考になります.　通信経路が暗号化されているのでトークンが安全に送られます.

- https://qiita.com/104ki/items/9dcfe03246099d03d4dd
Google AssistantとRaspberry Piで自宅の家電を操作する という記事です. こちらもとても参考になります. ただし, IFTTT Webhooks の設定が`http://api.beebotte.com/...` となっている(**https**になっていない)ので, IFTTT と Beebotte 間で秘密にするべきトークンが平文で流れることと, `app.py` の設定で `PORT = 1883` となっているので記事をそのまま実行すると Raspberry Pi と Beebotte 間でも同様にトークンが平文で流れてしまいます.

- https://qiita.com/mayfair/items/e761c788a9d8787bc610
IFTTTのトリガーおよびアクションをESP8266で実行する方法. ESP8266 を直接 Beebotte と接続する方法が書かれていてとても参考になります. ただし, この記事も同様に, `IFTTT_Action_Beebotte` で ポート `1883` が指定されているので, 記事をそのまま実行するとトークンが平文で流れてしまいます.

- https://www.mgo-tec.com/blog-entry-esp32-googlehome-ifttt-webhooks-blynk.html/3
Google Home Mini を使って、声で ESP32 の LED を光らせてみた という記事です. この記事では MQTT ではなく [Blynk](http://www.blynk.cc/) を使って IFTTT と ESP32 を中継しています. ただし, この記事も同様に, IFTTT の Webhooks で http を指定しているので認証が平文で流れてしまいます.

- https://qiita.com/voluntas/items/76f013acdc42fb492b0c
MQTT の Python クライアントで TLS 接続を試す という記事です. Beebotte との MQTT over SSL/TLS 接続についてはこのページを参考にしました.

- https://www.losant.com/blog/how-to-configure-mosquitto-bridge-to-losant
Mosquitto をブリッジ接続する方法. Mosquitto のブリッジ設定はここの設定ファイルが1番参考になりました.

- https://mosquitto.org/man/mosquitto-conf-5.html
Mosquitto のドキュメント. 設定ファイルのパラメータの説明.

- https://beebotte.com/docs/mqtt
Beebotte のドキュメント. MQTT で接続する際の認証方法やサンプルプログラムなど.

- http://owntracks.org/booklet/guide/bridge/
Mosquitto をブリッジ接続する際の設定ファイル.

- http://www.steves-internet-guide.com/mosquitto-bridge-configuration/
Mosquitto をブリッジ接続する方法. トピックのリマッピングについて詳しいです.

- http://www.steves-internet-guide.com/mosquitto-bridge-encryption/
Mosquitto を SSL/TLS でブリッジ接続する方法. 結局 Beebotte と接続するにはここに書かれている設定だけではダメでした.

以上