–
Photo by David Clode on Unsplash . The Gopher character is based on the Go mascot designed by Renee French .
※本記事で紹介している認証方法はFelicaのIDmのみを使用しております。IDm単体での認証はセキュリティ上望ましくないため、本記事を参考にされる方はご注意ください。
0. はじめに はじめまして、2021年4月入社TIG/DXユニット所属の宮永です。
夏の自由研究ブログ連載2021 の第2本目の投稿として、Sesame3にFelicaによる施錠解錠を実装しました。
作成したプログラムは orangekame3/gopy-sesame3: Sesame3のAPIをたたくクライアントアプリ にて公開しています。
1. 動機 私が所属しているプロジェクトではGoをメイン言語としています。Go未経験者である私は身の回りで楽しみながら言語を学べるブツはないかと探しました。
するとそこにはSesame3がありました。
2. Sesame3とは
Sesame3はCANDY HOUSE JAPAN が開発、販売しているスマートロックです。Sesame3本体に加えてwifiモジュールを購入すると、外出先から鍵の施錠/解錠を行うことができます。
また、6月にCANDY HOUSE公式でWeb API が公開されており、鍵の状態確認、施錠/解錠などを行うことができます。
今回はこのデバイスを使ってFelicaによる施錠と解錠の機能を実装したいと思います。
3. 必要なもの 実装に使用したものを列挙します
3.1. ハード
3.2. ソフト 開発はWindows10環境、WSL2上で行いました。
Go1.16.6 linux/amd64
Python 3.8.10
Sesame3を動かすWebAPIはこちら からAPI_TOKENを発行してください。 API_TOKENの発行の方法はこちら のブログが参考になりました。
施錠/解錠に必要な情報は
API_TOKEN
UUID
SECRET_KEY
の3つです。
4. 構成 PythonでカードリーダーによるIDmの読み取りとSECRET_KEY
の暗号化を行い、GoでHTTPリクエストを行うという構成にしました。 この構成にした理由は..
Pythonに便利なモジュールがあった
Goに少しでも慣れたかった
cgoというものを見つけてしまった
の3点です。本来であれば素直にPython1本、Go1本に絞ったほうが良いと思います….
4.1 システム概要図 以下システムの概要図です。
Raspberry Piにカードリーダー、スピーカーを接続しています。PythonでカードーリーダーからFelicaのIDmを取得し、暗号化したSECRET_KEYとAPI_TOKENをGo側に渡します。また、IDmの検知をユーザーに通知音で知らせています。GOではCANDY HOUSEが公開しているWeb APIに向けてHTTPリクエストを行います。リクエストに応じて、SESAME3を開閉することができるという構成になっています。
4.2. ディレクトリの構成 ビルド前のディレクトリの構成です。
1 2 3 4 5 6 7 8 . ├── README.md ├── export │ ├── export.go │ └── go.mod ├── main.py ├── nfcreader.py └── notify.wav
5. 実装 APIの使用方法は公式 にて、PythonおよびJavaScriptで公開されています。
今回は公式に記載された方法を手掛かりにコーディングしました。
5.1. HTTPリクエスト GoでHTTPリクエストを実装します。今回はcgo
を使用するため、構造体はなるべく使わずメソッドのみで完結させます。
まずは変数定義です。rootUrl
は公式に記載されたendpointです。コマンドに応じてrootUrl
に追記していきます。cmd_unlock
、cmd_lock
は公式ページに指定された解錠コマンドおよび施錠コマンドです。src
にはアプリに登録する履歴名を指定しています。今回はby Felica
という名前で登録しています。
export.go 1 2 3 4 5 6 7 8 9 package mainvar ( rootUrl = "https://app.candyhouse.co/api/sesame2/" cmd_unlock = 83 cmd_lock = 82 src = []byte ("by Felica" ) history = base64.StdEncoding.EncodeToString(src) )
続いて、HTTPリクエストに使用するJSONを格納するための構造体を定義します。こちらも 公式に記載されているJSONの定義を参考にしました。
export.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 type RequestBody struct { Cmd int `json:"cmd"` History string `json:"history"` Sign string `json:"sign"` } type ResponseBody struct { BatteryPercentage int `json:"batteryPercentage"` BatteryVoltage float64 `json:"batteryVoltage"` Position int `json:"position"` CHSesame2Status string `json:"CHSesame2Status"` Timestamp int `json:"timestamp"` Wm2State bool `json:"wm2State"` }
それでは、鍵の開閉を行う関数executeSesame3
を実装します。関数内で指定された引数signPtr
、apiPtr
、uuidPtr
はPythonから渡されることを想定しています。C.
を指定することでcgo
内の関数を使用することができます。
ここで一つ注意が必要です。cgo
を利用する際はメソッドの上のコメントを関数名にそろえる必要があります。
開閉の流れとしては「施錠中/解錠中の確認fetchStatus
」→「解錠中isUnlocked
であればexecuteLock
を実行」、「施錠中であればexecuteUnlock
を実行する」という構成です。
export.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func executeSesame3 (signPtr, apiPtr, uuidPtr *C.char) { sign := C.GoString(signPtr) api := C.GoString(apiPtr) uuid := C.GoString(uuidPtr) key_status := fetchStatus(api, uuid) if isUnlocked(key_status) { fmt.Println("Key is " + key_status + ". Locking ..." ) executeResponse := executeLock(sign, api, uuid) fmt.Println(executeResponse) } else { fmt.Println("Key is " + key_status + ". Unlocking ..." ) executeResponse := executeUnlock(sign, api, uuid) fmt.Println(executeResponse) } }
次に、fetchStatus
をコーディングします。fetchStatus
は鍵の状態を取得する関数です。鍵の状態取得のHTTPリクエストに必要な情報はUUID
とAPI_TOKEN
です。rootUrl
に自分のデバイスのUUID
を追加したものがendpointです。
export.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func fetchStatus (api, uuid string ) string { fetchUrl := rootUrl + uuid req, err := http.NewRequest("GET" , fetchUrl, nil ) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key" , api) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() respbody, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } var statusResponse ResponseBody if err := json.Unmarshal(respbody, &statusResponse); err != nil { fmt.Println("JSON Unmarshal error:" , err) } key_status := string (statusResponse.CHSesame2Status) return key_status }
最後に施錠と解錠の関数executeLock
、executeUnlock
をコーディングします。こちらの2つはほぼ同じ内容です。
export.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 func executeUnlock (sign, api, uuid string ) string { cmdUrl := rootUrl + uuid + "/cmd" requestBody := RequestBody{ Cmd: cmd_unlock, History: history, Sign: sign, } jsonBody, err := json.Marshal(requestBody) if err != nil { log.Fatal(err) } req, err := http.NewRequest("POST" , cmdUrl, bytes.NewBuffer(jsonBody)) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key" , api) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() return "Unlock command was executed." } func executeLock (sign, api, uuid string ) string { cmdUrl := rootUrl + uuid + "/cmd" requestBody := RequestBody{ Cmd: cmd_lock, History: history, Sign: sign, } jsonBody, err := json.Marshal(requestBody) if err != nil { log.Fatal(err) } req, err := http.NewRequest("POST" , cmdUrl, bytes.NewBuffer(jsonBody)) if err != nil { log.Fatal(err) } req.Header.Set("x-api-key" , api) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() return "Lock command was executed." }
以上でGo側のコーディングは完成です。 Pythonのコーディングを始める前にexport.go
をビルドします。/exoprt
にて以下コマンドを実行します。
1 $ go build -buildmode=c-shared -o export.so
ビルド後、export配下に新たにexport.so
、export.h
が出力されていることが確認できます。
1 2 3 4 5 6 7 8 9 10 . ├── README.md ├── export │ ├── export.go │ ├── export.h │ ├── export.so │ └── go.mod ├── main.py ├── nfcreader.py └── notify.wav
次にPython側をコーディングしていきます。
5.2 NFCの読み込み Python側ではカードリーダーの制御、環境変数の引き渡し、SECRET_KEY
の暗号化を行います。また、ICカード検知の通知音を出すために、スピーカーの制御も行っています。
以下必要となるモジュールを読み込みます。自作したモジュールはnfcreader.py
のみです。
main.py 1 2 3 4 5 6 7 8 9 10 11 from ctypes import *import ctypesimport datetimefrom Crypto.Hash import CMACfrom Crypto.Cipher import AESimport structfrom time import sleepimport osfrom dotenv import load_dotenvimport subprocessfrom nfcreader import CardReader
まずは環境変数の読み込みです。
ここで環境変数とはSECRET_KEY
、API_TOKEN
、UUID
、ICカードのIDm
を指しています。環境変数は誤ってGitHubなどに公開しないようにまとめて管理します。
環境変数の管理にはpython-dotenv
を使用しました。ソースコードでは相対ディレクトリでプロジェクトディレクトリの直上に配置しています。
main.py 1 2 3 4 5 6 load_dotenv('../.env' ) SECRET_KEY = os.environ["SECRET_KEY" ] API_TOKEN = os.environ["API_TOKEN" ] UUID = os.environ["UUID" ] ANDROIDO = os.environ["ANDROIDO" ].encode() SUICA = os.environ["SUICA" ].encode()
まずはMySesame3
クラスを定義します。
MySesame3
はコンストラクタにて冒頭で読み込んだ環境変数を格納しています。暗号化する際にtimestamp
が必要となるため、sign
(署名)のみ空にしています。また、先ほどビルドすることによって生成されたexport.so
ファイルを読み込んでいます。メソッドとしてSECRET_KEY
の暗号化encryptmyKey
、および施錠と解錠lockOrunlock
を持っています。
main.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class MySesame3 : '''docstring ・セサミ3のクラス、libはGoでビルドしたバイナリファイルを読み込んでいる ・API_TOKENはこちら(https://dash.candyhouse.co/login)で取得する ・UUIDはアプリに記載されている対象のセサミ3のUUID ・SECRET_KEYはアプリを立ち上げて「鍵のシェア(オーナー)」で生成されるQRコードを読み込んで得られる文字列 ・施錠/解錠の際はSECRET_KEYとタイムスタンプをAES-CMACによって暗号化する必要がある(encryptmyKey) ・作成した暗号キーをsignとしてAPI_TOKEN,UUIDとともにPOSTすることで施錠解錠ができる(lockOrunlock) ・現在の鍵の状態(施錠中/解除中)はgolang側で判断する ・施錠中であれば解錠コマンドを、解錠中であれば施錠コマンドを打ち込む ''' def __init__ (self ): self.lib = cdll.LoadLibrary("./export/export.so" ) self.key = SECRET_KEY self.api = API_TOKEN self.uuid = UUID self.sign = "" def encyptmyKey (self ): timestamp = int (datetime.datetime.now().timestamp()).to_bytes(4 , 'little' , signed=False )[1 :4 ] cmac = CMAC.new(bytes .fromhex(self.key), ciphermod=AES) cmac.update(timestamp) self.sign = cmac.hexdigest() def lockOrunlock (self ): self.lib.executeSesame3.restype=c_char_p self.lib.executeSesame3(self.sign.encode('utf-8' ),self.api.encode('utf-8' ),self.uuid.encode('utf-8' ))
次に一度main.py
からは離れて、カードリーダーのクラスを定義します。こちらは別ファイルnfcreader.py
にコーディングします。nfcreader.py
をコーディングするにあたってこちら の方の記事を参考にしました。
nfcreader.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import nfcimport binasciiclass CardReader (): '''docstring ・カードリーダークラス ''' def __init__ (self ): self.idm = 0 def on_startup (self,targets ): for target in targets: target.sensef_req = bytearray .fromhex("0000030000" ) return targets def on_connect (self,tag ): print ("Detected!!" ) self.idm = binascii.hexlify(tag.idm) def read_id (self ): clf = nfc.ContactlessFrontend('usb' ) print ("Waiting Felica..." ) clf.connect(rdwr = {'targets' :['212F' ],'on-startup' :self.on_startup,'on-connect' :self.on_connect}) print (str (self.idm)) clf.close()
main.py
の本体を記述します。
先ほど定義したクラス、およびカードリーダーのクラスを使用します。
構成としては「Mysesame3
およびCardReader
インスタンス生成」→「CardReader
インスタンスに格納されたIDmを取得」→「受信を検知したらスピーカーから音で通知」→「環境変数に登録したIDmを参照」→「一致したら施錠/解錠リクエストを送信」といった流れになっています。スピーカーの音源はnotify.wav
という名前で同main.py
と同階層に配置しています。
main.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def ismyID (id ): return bool (id ==ANDROIDO or id ==SUICA) if __name__ == '__main__' : mySesame3 = MySesame3() try : while True : myreader = CardReader() myreader.read_id() detectedID = myreader.idm subprocess.call("aplay notify.wav" ,shell=True ) if ismyID(detectedID): mySesame3.encyptmyKey() mySesame3.lockOrunlock() detectedID = 0 sleep(2 ) except KeyboardInterrupt: print ("KeyboardInterrupt!!" )
6. スピーカーから音を出す 最後にラズパイから音を出すためにスピーカーを取り付けます。高価なスピーカーはもったいないのでこちらはアンプを取り付けて自作します。スピーカーの取り付けはこちら の記事を参考にしました。
7. 取り付け スピーカーを取り付けたらとりあえず、新聞受けに投げ入れます。玄関まで電源コードを延長するのが大変でしたが、こちら の延長コードでどうにか電源供給できました。
カードリーダーはコクヨのマグネットプレート を使って取り付けました。
8. 動作確認 動作確認の結果です。待機Waiting Felica...
から検知Detected!!
→Lock command was executed.
と正しく動作していることがわかります。
こちらは施錠時のスマホの通知画面です。export.go
に定義した文字列by Felica
が正しく表示されています。
9. まとめ Sesame3のWeb APIを利用して、Felicaによる施錠解錠の機能を実装しました。
今回認証に使用したIDmはスマホアプリでも簡単に取得することができます。そのため、IDm単体に認証を任せてしまうのはセキュリティの観点から適切ではありません。実用に耐えうるにはさらなる工夫が求められます。とはいえ、GoとPythonを使って楽しみながらコーディングできたため、夏休みの自由研究の目的は達成できたと思います。
次は大野さんによる最高の持ち歩きキーボード考 です。