mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
161 lines
6.9 KiB
Python
161 lines
6.9 KiB
Python
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "bridge"))
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "vendor"))
|
|
from pairing import app_only, link_address, remove_by_address, add_phone, ensure_store, forget_all, introduce, load_keys, load_phones, remove_phone, LEGACY_ID
|
|
from server import Gateway
|
|
from protocol import packets, seal
|
|
from dbus_next import Variant, DBusError
|
|
|
|
|
|
class Proxy:
|
|
def __init__(self):
|
|
self.calls = 0
|
|
|
|
def handle(self, request):
|
|
self.calls += 1
|
|
return {"id": request["id"], "status": 200, "headers": {}, "body": ""}
|
|
|
|
|
|
async def send(gateway, options, key, counter=1):
|
|
state = gateway.session(options)
|
|
request = {"id": "test", "session": state.challenge.hex(), "counter": counter, "path": "/api/params", "method": "PUT", "body": ""}
|
|
for packet in packets(seal(request, key, "request")):
|
|
gateway.write(packet, options)
|
|
await state.task
|
|
return state
|
|
|
|
|
|
class PairingTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_each_phone_has_its_own_key_and_can_be_forgotten_alone(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "pairing.json"
|
|
self.assertEqual(ensure_store(path), [])
|
|
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
|
|
iphone_id, iphone = add_phone("iPhone", path)
|
|
_, android = add_phone("Android phone", path)
|
|
self.assertNotEqual(iphone, android)
|
|
|
|
proxy = Proxy()
|
|
gateway = Gateway(load_keys(path), proxy, path)
|
|
iphone_options = {"device": Variant("o", "/org/bluez/hci0/dev_iphone")}
|
|
android_options = {"device": Variant("o", "/org/bluez/hci0/dev_android")}
|
|
iphone_state = await send(gateway, iphone_options, iphone)
|
|
android_state = await send(gateway, android_options, android)
|
|
self.assertEqual(proxy.calls, 2)
|
|
self.assertTrue(all(phone["last_used"] for phone in load_phones(path)))
|
|
|
|
remove_phone(iphone_id, path)
|
|
gateway.refresh_key()
|
|
self.assertTrue(iphone_state.closed)
|
|
self.assertFalse(android_state.closed) # the other phone stays connected
|
|
|
|
# The forgotten phone's key cannot change settings, even with a fresh challenge.
|
|
await send(gateway, iphone_options, iphone)
|
|
self.assertEqual(proxy.calls, 2)
|
|
await send(gateway, {"device": Variant("o", "/org/bluez/hci0/dev_android_again")}, android)
|
|
self.assertEqual(proxy.calls, 3)
|
|
|
|
forget_all(path)
|
|
with self.assertRaises(DBusError):
|
|
gateway.session(android_options)
|
|
self.assertTrue(android_state.closed)
|
|
|
|
async def test_phone_names_itself_and_replaces_its_old_entry(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "pairing.json"
|
|
old_id, _ = add_phone("Phone", path)
|
|
other_id, _ = add_phone("Phone", path)
|
|
new_id, new = add_phone("Phone", path)
|
|
proxy = Proxy()
|
|
gateway = Gateway(load_keys(path), proxy, path)
|
|
options = {"device": Variant("o", "/org/bluez/hci0/dev_iphone")}
|
|
state = gateway.session(options)
|
|
request = {"id": "t", "session": state.challenge.hex(), "counter": 1, "path": "/api/params", "method": "GET", "body": "",
|
|
"phone": {"name": "Danny\u2019s iPhone\n", "replaces": old_id}}
|
|
for packet in packets(seal(request, new, "request")):
|
|
gateway.write(packet, options)
|
|
await state.task
|
|
phones = {phone["id"]: phone for phone in load_phones(path)}
|
|
self.assertEqual(set(phones), {other_id, new_id})
|
|
self.assertEqual(phones[new_id]["label"], "Danny\u2019s iPhone")
|
|
self.assertEqual(proxy.calls, 1)
|
|
# A phone can't drop the shared pre-update entry or itself, and bad ids are ignored.
|
|
self.assertFalse(introduce(new_id, replaces=new_id, path=path))
|
|
self.assertFalse(introduce(new_id, replaces="legacy", path=path))
|
|
self.assertFalse(introduce(new_id, replaces="../../x", path=path))
|
|
|
|
async def test_single_key_file_from_before_update_still_works(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "pairing.json"
|
|
key = os.urandom(32)
|
|
path.write_text(json.dumps({"key": key.hex()}))
|
|
os.chmod(path, 0o600)
|
|
self.assertEqual(load_keys(path), {LEGACY_ID: key})
|
|
proxy = Proxy()
|
|
gateway = Gateway(load_keys(path), proxy, path)
|
|
await send(gateway, {"device": Variant("o", "/org/bluez/hci0/dev_old")}, key)
|
|
self.assertEqual(proxy.calls, 1)
|
|
add_phone("iPhone", path) # rewrites in the new format and keeps the old phones
|
|
self.assertEqual(load_keys(path)[LEGACY_ID], key)
|
|
self.assertEqual(len(load_keys(path)), 2)
|
|
|
|
async def test_bad_or_missing_file_revokes_sessions_and_fails_closed(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "pairing.json"
|
|
add_phone("iPhone", path)
|
|
gateway = Gateway(load_keys(path), None, path)
|
|
options = {"device": Variant("o", "/org/bluez/hci0/dev_test")}
|
|
state = gateway.session(options)
|
|
os.chmod(path, 0o644)
|
|
with self.assertRaises(DBusError):
|
|
gateway.session(options)
|
|
self.assertTrue(state.closed)
|
|
with self.assertRaises(ValueError):
|
|
add_phone("iPhone", path) # Never silently replace a damaged existing credential.
|
|
path.unlink()
|
|
with self.assertRaises(DBusError):
|
|
gateway.session(options)
|
|
|
|
async def test_symlink_is_rejected(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "pairing.json"
|
|
add_phone("iPhone", path)
|
|
link = Path(directory) / "link.json"
|
|
link.symlink_to(path)
|
|
with self.assertRaises(OSError):
|
|
load_keys(link)
|
|
|
|
def test_forgetting_a_bluetooth_device_forgets_its_app_key_only(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "pairing.json"
|
|
first_id, _ = add_phone("Pixel", path)
|
|
second_id, second = add_phone("iPhone", path)
|
|
self.assertFalse(link_address(first_id, "not an address", path))
|
|
self.assertTrue(link_address(first_id, "11:22:33:44:55:66", path))
|
|
self.assertEqual([phone["id"] for phone in app_only(path)], [second_id])
|
|
self.assertEqual(remove_by_address("aa:bb:cc:dd:ee:ff", path), 0)
|
|
self.assertEqual(remove_by_address("11:22:33:44:55:66", path), 1)
|
|
self.assertEqual(list(load_keys(path).values()), [second])
|
|
|
|
def test_address_survives_rewrites_and_is_never_trusted_when_malformed(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / "pairing.json"
|
|
phone_id, _ = add_phone("Pixel", path)
|
|
link_address(phone_id, "11:22:33:44:55:66", path)
|
|
add_phone("Other", path)
|
|
self.assertEqual(load_phones(path)[0]["bt_address"], "11:22:33:44:55:66")
|
|
raw = json.loads(path.read_text())
|
|
raw["phones"][0]["bt_address"] = "garbage"
|
|
path.write_text(json.dumps(raw))
|
|
self.assertNotIn("bt_address", load_phones(path)[0])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|