Add authenticated WiFi transport with BLE fallback

This commit is contained in:
2026-10-01 14:32:18 +08:00
parent 071f06d365
commit f011d8357d
55 changed files with 6327 additions and 29 deletions
@@ -0,0 +1,124 @@
import json
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.mobile.wifi import wifi_cipher, encode, install_wifi_route
from app.mobile.protocol import ProtocolError
from test_mobile_session import connection, request
import base64
def opened(now=None):
now = now or [0]
manager, ble, calls = connection(lambda: now[0])
manager.wifi.address = lambda: '192.0.2.10'
request(manager, ble, 1, '1', 'session.open', {'client_name': '手机'})
return manager, ble, calls, now
def offer(manager, ble):
result = request(manager, ble, 2, '2', 'transport.offer')['result']
client = wifi_cipher(base64.b64decode(result['secret']), result['channel_id'], server=False)
hello = result['channel_id'].encode() + client.encrypt(encode({'device_id': 'test'}))
return result, client, hello
def test_upgrade_rpc_fallback_and_ble_lifetime():
m, ble, calls, now = opened()
generation = m.generation
o, client, hello = offer(m, ble)
channel, ack = m.wifi.attach(hello)
assert json.loads(client.decrypt(ack)) == {'device_id': 'test', 'ready': True}
assert m.status()['active_transport'] == 'wifi' and m.generation == generation
for number, method in enumerate(('status.get', 'content.play', 'transport.ping'), 1):
now[0] += 5
packet = client.encrypt(encode(dict(id=str(number), method=method, params={})))
assert json.loads(client.decrypt(m.wifi.accept(channel, packet)))['ok']
assert calls == ['status.get', 'content.play']
now[0] = 20
assert m.expired() # WiFi traffic never refreshes BLE activity.
with pytest.raises(ProtocolError): m.wifi.accept(channel, b'bad')
m.disconnect('a')
assert m.status()['active_transport'] is None
def test_offer_expiry_identity_tamper_and_single_use():
m, ble, _, now = opened()
o, c, hello = offer(m, ble)
with pytest.raises(ProtocolError): m.wifi.attach(hello[:-1] + bytes([hello[-1] ^ 1]))
assert m.wifi.pending is not None
channel, _ = m.wifi.attach(hello)
with pytest.raises(ProtocolError): m.wifi.attach(hello)
assert m.wifi.valid(channel) # Attacker cannot evict the valid owner.
assert request(m, ble, 3, '3', 'transport.close')['ok']
assert m.status()['active_transport'] == 'ble'
with pytest.raises(ProtocolError): m.wifi.attach(hello)
m, ble, _, now = opened()
_, _, hello = offer(m, ble)
now[0] = 10
with pytest.raises(ProtocolError): m.wifi.attach(hello)
def test_no_wifi_address_and_wrong_identity():
m, ble, _, _ = opened()
m.wifi.address = lambda: None
assert not request(m, ble, 2, '2', 'transport.offer')['ok']
m.wifi.address = lambda: '198.51.100.20'
o = request(m, ble, 3, '3', 'transport.offer')['result']
c = wifi_cipher(base64.b64decode(o['secret']), o['channel_id'], server=False)
with pytest.raises(ProtocolError):
m.wifi.attach(o['channel_id'].encode() + c.encrypt(encode({'device_id': 'other'})))
def test_duplicate_write_not_replayed_and_ble_only_commands_rejected():
m, ble, calls, _ = opened()
o, c, hello = offer(m, ble)
channel, ack = m.wifi.attach(hello); c.decrypt(ack)
for rid, method, expected in [('1', 'content.play', True), ('1', 'content.play', False),
('2', 'wifi.set', False), ('3', 'session.ping', False)]:
packet = c.encrypt(encode(dict(id=rid, method=method, params={})))
assert json.loads(c.decrypt(m.wifi.accept(channel, packet)))['ok'] is expected
assert calls == ['content.play']
def test_real_websocket_records_and_disconnect():
m, ble, calls, _ = opened()
app = FastAPI(); app.state.mobile_sessions = m; install_wifi_route(app)
o, c, hello = offer(m, ble)
with TestClient(app) as client:
with client.websocket_connect('/ws/mobile') as ws:
ws.send_bytes(hello)
assert json.loads(c.decrypt(ws.receive_bytes()))['ready']
ws.send_bytes(c.encrypt(encode(dict(id='1', method='status.get', params={}))))
assert json.loads(c.decrypt(ws.receive_bytes()))['ok']
m.disconnect('a')
assert ws.receive()['type'] == 'websocket.close'
assert calls == ['status.get']
def test_wifi_idle_expiry_and_cross_subnet_not_filtered():
m, ble, _, now = opened()
m.wifi.address = lambda: '198.51.100.42'
o, c, hello = offer(m, ble)
assert o['host'] == '198.51.100.42'
m.wifi.attach(hello)
now[0] = 10
assert not m.wifi.valid()
assert m.status()['active_transport'] == 'ble'
def test_runtime_preserves_existing_transport_statistics():
from app.mobile.bluez import BluezRuntime
m, ble, _, _ = opened()
_, _, hello = offer(m, ble)
m.wifi.attach(hello)
status = BluezRuntime(m).status()
assert status['active_transport'] == 'wifi'
assert isinstance(status['transport'], dict)
assert 'rx_bytes' in status['transport']
def test_public_wifi_vector_is_reproducible():
from pathlib import Path
from generate_wifi_vector import generate
assert generate() == json.loads((Path(__file__).parent / 'fixtures/protocol-wifi-v1.json').read_text(encoding='utf-8'))