You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/src/trezor/wire/__init__.py

169 lines
6.2 KiB

"""
# Wire
Handles on-the-wire communication with a host computer. The communication is:
- Request / response.
- Protobuf-encoded, see `protobuf.py`.
- Wrapped in a simple envelope format, see `trezor/wire/codec_v1.py` or `trezor/wire/thp_v1.py`.
- Transferred over USB interface, or UDP in case of Unix emulation.
This module:
1. Runs workflows, also called `handlers`, to process the message.
2. Creates and passes the `Context` object to the handlers. This provides an interface to
wait, read, write etc. on the wire.
## Session handler
When the `wire.setup` is called the `handle_session` coroutine is scheduled. The
`handle_session` waits for some messages to be received on some particular interface and
reads the message's header. When the message type is known the first handler is called. This way the
`handle_session` goes through all the workflows.
"""
from typing import TYPE_CHECKING # pyright: ignore[reportShadowedImports]
from trezor import log, loop, protobuf, utils
from trezor.wire import context, message_handler, protocol_common, thp_v1
from trezor.wire.message_handler import (
AVOID_RESTARTING_FOR,
WIRE_BUFFER,
WIRE_BUFFER_DEBUG,
failure,
)
# Import all errors into namespace, so that `wire.Error` is available from
# other packages.
from trezor.wire.errors import * # isort:skip # noqa: F401,F403
if TYPE_CHECKING:
from trezorio import WireInterface # pyright: ignore[reportMissingImports]
from typing import ( # pyright: ignore[reportShadowedImports]
Any,
Callable,
Coroutine,
TypeVar,
)
Msg = TypeVar("Msg", bound=protobuf.MessageType)
HandlerTask = Coroutine[Any, Any, protobuf.MessageType]
Handler = Callable[[Msg], HandlerTask]
LoadedMessageType = TypeVar("LoadedMessageType", bound=protobuf.MessageType)
# If set to False protobuf messages marked with "experimental_message" option are rejected.
EXPERIMENTAL_ENABLED = False
def setup(iface: WireInterface, is_debug_session: bool = False) -> None:
"""Initialize the wire stack on passed WireInterface."""
if utils.USE_THP and not is_debug_session:
loop.schedule(handle_thp_session(iface, is_debug_session))
else:
loop.schedule(handle_session(iface, is_debug_session))
async def handle_thp_session(iface: WireInterface, is_debug_session: bool = False):
if __debug__ and is_debug_session:
ctx_buffer = WIRE_BUFFER_DEBUG
else:
ctx_buffer = WIRE_BUFFER
thp_v1.set_buffer(ctx_buffer)
# Take a mark of modules that are imported at this point, so we can
# roll back and un-import any others.
modules = utils.unimport_begin()
while True:
try:
await thp_v1.thp_main_loop(iface, is_debug_session)
if not __debug__ or not is_debug_session:
# Unload modules imported by the workflow. Should not raise.
# This is not done for the debug session because the snapshot taken
# in a debug session would clear modules which are in use by the
# workflow running on wire.
utils.unimport_end(modules)
loop.clear()
return
except Exception as exc:
# Log and try again. The session handler can only exit explicitly via
# loop.clear() above.
if __debug__:
log.exception(__name__, exc)
async def handle_session(iface: WireInterface, is_debug_session: bool = False) -> None:
if __debug__ and is_debug_session:
ctx_buffer = WIRE_BUFFER_DEBUG
else:
ctx_buffer = WIRE_BUFFER
ctx = context.CodecContext(iface, ctx_buffer)
next_msg: protocol_common.MessageWithId | None = None
if __debug__ and is_debug_session:
import apps.debug
apps.debug.DEBUG_CONTEXT = ctx
# Take a mark of modules that are imported at this point, so we can
# roll back and un-import any others.
modules = utils.unimport_begin()
while True:
try:
if next_msg is None:
# If the previous run did not keep an unprocessed message for us,
# wait for a new one coming from the wire.
try:
msg = await ctx.read_from_wire()
except protocol_common.WireError as exc:
if __debug__:
log.exception(__name__, exc)
await ctx.write(failure(exc))
continue
else:
# Process the message from previous run.
msg = next_msg
next_msg = None
try:
next_msg_without_id = await message_handler.handle_single_message(
ctx, msg, use_workflow=not is_debug_session
)
if next_msg_without_id is not None:
next_msg = protocol_common.MessageWithId(
next_msg_without_id.type,
next_msg_without_id.data,
bytearray(ctx.channel_id),
)
except Exception as exc:
# Log and ignore. The session handler can only exit explicitly in the
# following finally block.
if __debug__:
log.exception(__name__, exc)
finally:
if not __debug__ or not is_debug_session:
# Unload modules imported by the workflow. Should not raise.
# This is not done for the debug session because the snapshot taken
# in a debug session would clear modules which are in use by the
# workflow running on wire.
utils.unimport_end(modules)
if next_msg is None and msg.type not in AVOID_RESTARTING_FOR:
# Shut down the loop if there is no next message waiting.
# Let the session be restarted from `main`.
loop.clear()
return # pylint: disable=lost-exception
except Exception as exc:
# Log and try again. The session handler can only exit explicitly via
# loop.clear() above.
if __debug__:
log.exception(__name__, exc)