Sesame5というのはCANDY HOUSE社が販売しているスマートキーのことです。
スマートキーの中では非常にリーズナブルで、Sesame3を4年ほど使用していますが、今のところ特に問題もなく使えています。
このたびSesame5が発表され、乗り換えようと購入をしたのですが、諸般の事情によりSesame3はRaspberryPi Zero WHからBluetoothを使ってアクセスをしており、乗り換えにはこれをクリアする必要がありました。
(もちろん普通に公式アプリを使ったりもしてます)
Sesame3をラズパイのBluetoothを使ってPythonでアクセスを行うには、pysesameos2というライブラリを使用すると便利です。
しかしSesame5になり、SesameOSが2から3にバージョンアップしたため、このライブラリでは動かなくなりました。
というわけで、公式が提供している仕様書とESP32向けのC言語で書かれたサンプルを参考にしながら自前でアクセスを行ってみます。
PythonでBluetoothにアクセスするには、pysesameos2ではBleakを使用しているのですが、今回はBluepyを使用しました。
Sesame5に接続
notifyDelegate = NotifyDelegate()
peri = btle.Peripheral()
peri.connect(sesame5_address, btle.ADDR_TYPE_RANDOM)
peri.withDelegate(notifyDelegate)
本来はスキャンを行ってMACアドレスを取得します。
アドレスタイプをRANDOMに指定しないと接続しませんでした。
でも設定リセットなどを行っても実験中にMACアドレスが変わることはありませんでした。
アドバタイジングデータは、未登録と登録後では内容が変わるのでチェックをする際は注意が必要です。
Notifyを有効にする
ここが第1の苦戦でした。
Sesame5ではサービスが取得できず、有効なキャラクタリスティックが分かりません。
そのため、サンプルやbluetoothctlで得られたキャラクタリスティックのハンドルを直値で叩くという荒業を行うことになります。
data = bytes([0x01, 0x00])
peri.writeCharacteristic(0x0010, data, True)
notifyDelegate.recieve(peri, 0x0e)
Notifyを有効にするとSesameから4バイトのランダムコードが送られてきます。
このランダムコードと予め公式アプリから取得しておいたPrivateKeyを組わせてトークンを作成します。
PrivateKeyは、オーナー鍵かマネージャー鍵のQRから取得できます。
cobj = CMAC.new(bytes.fromhex(private_key), ciphermod=AES)
cobj.update(self._random_code)
_token = cobj.digest()
ログインを行う
先ほど作成したトークンの先頭4バイトをログインコマンドと一緒に平文でSesame5に送ります。
するとタイムスタンプを返してきます。
command = bytes([0x02, token[0], token[1], token[2], token[3]])
print("LoginData:" + data.hex())
notifyDelegate.send(peri, command, False)
notifyDelegate.recieve(peri, 0x02)
解錠する
解錠コマンドは全体を暗号化して送ります。
tag = '自前プログラムから解錠'.encode()
command = bytes([0x53, len(tag)]) + tag
notifyDelegate.send(peri, command, True)
notifyDelegate.recieve(peri, 0x53)
暗号化と復号化
ここも苦労ポイントです。
暗号には、CBC-MACという形式を使っています。CCMモードともいうようです。
暗号化と復号化には、それぞれカウンターを持っていて、暗号化や復号化が行われるたびにカウントが1上がります。
カウンターは、ログインの際に0に初期化されるようです。
この暗号化の仕方がどうもSesameOS2と異なるようですね。
def encrypt(data):
iv = _encrypt_counter.to_bytes(9, "little")
iv += _random_code
cobj = AES.new(_token, AES.MODE_CCM, iv, mac_len=4,msg_len=len(data), assoc_len=1)
_encrypt_counter += 1
cobj.update(bytes([0]))
enc_data, tag = cobj.encrypt_and_digest(data)
tag4 = tag[0:4]
return enc_data + tag4
def decrypt(data):
iv = _decrypt_counter.to_bytes(9, "little")
iv += _random_code
cobj = AES.new(_token, AES.MODE_CCM, iv)
_decrypt_counter += 1
decode_data = cobj.decrypt(data[0:-4])
return decode_data
送信
Sesame5にデータを送信する場合、データを19バイトで分割して送ります。
def send(peri, send_data, is_encrypt):
if is_encrypt:
send_data = self.encrypt(send_data)
remain = len(send_data)
offset = 0
while remain != 0:
header = 0
if offset == 0:
header += 1
if remain <= 19:
buffer = send_data[offset:]
remain = 0
if is_encrypt:
header += 4
else:
header += 2
else:
buffer = send_data[offset:(offset+20)]
offset += 19
remain -= 19
buffer = bytes([header]) + buffer
peri.writeCharacteristic(0x000d, buffer, False)
受信
受信もデータが分割されて送られてきます。
def handleNotification(cHandle, data):
if (data[0] & 1) != 0:
_buffer = bytes()
_buffer += data[1:]
if (data[0] >> 1) == 0:
return
if (data[0] >> 1) == 2:
data = self.decrypt(_buffer)
else:
data = _buffer
op_code = data[0]
_last_item_code = data[1]
(op_codeとitem_codeに応じて処理を書く)
という感じで、何とかラズパイのBluetoothとPythonを使ってSesame5を動かすことができました。
サンプルやドキュメントがあるとはいえ、色んな知識が欠けていたので手探りで試行錯誤していたので、1か月以上はかかりました。
初めてSesameが反応してくれた時は嬉しかったですね。
というわけで、この情報が誰かの何かの役に立てば幸いです。
コメント