summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/src/meshbay_common/protocol.py
blob: 1c6dd3e7131bd494250abc134499b567b1e79d9a (plain) (blame)
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""
MeshBay protocol constants and message type definitions.

MNP (Mesh Node Protocol) — v0.1
MHP (Mesh Bay Hub Protocol) — v0.1

All wire messages are length-prefixed msgpack (4-byte big-endian length header).
Every message carries a "v" field for protocol version.
"""

from dataclasses import dataclass, field
from typing import Any

# The wire versions live in meshbay_common/__init__.py — one source, because a
# second copy here said "0.1" while every message on the wire carried "0.2".
# Nothing imported it, which is the only reason it was harmless.
from meshbay_common import MNP_VERSION, MHP_VERSION   # noqa: F401  (re-export)


# ── MNP message types ─────────────────────────────────────────────────────────

class MNP:
    HANDSHAKE       = "handshake"
    HANDSHAKE_ACK   = "handshake_ack"
    INDEX_SYNC      = "index_sync"       # full Mesh Group Index
    INDEX_DELTA     = "index_delta"      # incremental update
    FILE_REQUEST    = "file_req"         # request chunk(s)
    FILE_CHUNK      = "file_chunk"       # encrypted chunk response
    STREAM_SEGMENT  = "stream_seg"       # HLS/DASH segment
    CHAT_MESSAGE    = "chat_msg"         # Double Ratchet message
    CHAT_ATTACHMENT = "chat_attach"      # attachment metadata
    CHAT_HISTORY    = "chat_hist"       # request message history (newest, or before a cursor)
    CHAT_HISTORY_RESPONSE = "chat_hist_resp"  # history response with messages
    # Liveness on an *already open* channel. A peer that goes away without
    # closing leaves a DataChannel that still reads as connected until the next
    # real request hangs, and there was no way to ask. This is not a discovery
    # mechanism: opening a connection in order to ping costs a full ICE/DTLS
    # handshake (measured at 0.6-7 s across two ISPs), so presence in the group
    # list comes from the hub's socket registry instead.
    PING            = "ping"
    PONG            = "pong"
    # GEK_REQUEST / GEK_RESPONSE were removed (NS3, and finding L1): the node must
    # never serve the GEK in plaintext. Members obtain it by unwrapping their own
    # ECIES bundle. The constants lingered after the handlers were deleted, leaving
    # the wire contract looking as though the endpoint still existed.
    FILE_UPLOAD      = "file_upload"       # client pushes file chunk to node
    FILE_UPLOAD_ACK  = "file_upload_ack"   # node acknowledges chunk receipt
    DIR_CREATE       = "dir_create"        # client → node: make a directory
    DIR_CREATE_ACK   = "dir_create_ack"    # node → client: created
    DIR_DELETE       = "dir_delete"        # client → node: remove an empty directory
    DIR_DELETE_ACK   = "dir_delete_ack"    # node → client: removed
    FILE_DELETE      = "file_delete"       # client requests file deletion
    FILE_DELETE_ACK  = "file_delete_ack"   # node confirms deletion
    STREAM_REQUEST   = "stream_req"        # client requests MSE video stream
    STREAM_INIT      = "stream_init"       # node sends codec info + signals stream start
    STREAM_DATA      = "stream_data"       # node sends encrypted fMP4 segment
    STREAM_END       = "stream_end"        # node signals end of stream
    STREAM_MORE      = "stream_more"       # client → node: room for N more segments
    STREAM_STOP      = "stream_stop"       # client → node: nobody is watching any more
    EPHEMERAL_STREAM = "ephemeral_stream"  # reserved — mobile live push
    HANDSHAKE_CHALLENGE  = "handshake_challenge"   # node → client: GEK proof nonce
    HANDSHAKE_RESPONSE   = "handshake_response"    # client → node: HMAC(GEK, nonce)
    ADMIN_CHALLENGE      = "admin_challenge"       # node → client: Ed25519 sign challenge
    ADMIN_RESPONSE       = "admin_response"        # client → node: Ed25519 signature
    # GEK_BUNDLE_STORE was removed with the invite redesign: the node wraps the GEK
    # itself, for a key the recipient proved possession of, so no member ever hands
    # the node key material (C5b, and the H3 substitution it enabled).
    GEK_BUNDLE_FETCH     = "gek_bundle_fetch"      # client → node: request own wrapped GEK
    GEK_BUNDLE_RESP      = "gek_bundle_resp"       # node → client: wrapped GEK bundle
    KEYPAIR_BUNDLE_STORE = "keypair_bundle_store"  # client → node: store encrypted keypair bundle
    KEYPAIR_BUNDLE_FETCH = "keypair_bundle_fetch"  # client → node: request own keypair bundle
    KEYPAIR_BUNDLE_RESP  = "keypair_bundle_resp"   # node → client: encrypted keypair bundle
    KEYPAIR_BUNDLE_DELETE = "keypair_bundle_delete"  # client → node: withdraw own backup
    JOIN_REQUEST         = "join_request"          # client → node: pair/recognise this identity
    JOIN_RESULT          = "join_result"           # node → client: outcome + wrapped GEK
    INVITE_CREATE        = "invite_create"         # operator → node: issue a pairing code
    MEMBER_REVOKE        = "member_revoke"         # operator → node: stop serving the key
    MEMBER_REVOKE_ACK    = "member_revoke_ack"
    MEMBER_UNPIN         = "member_unpin"          # operator → node: forget an identity
    MEMBER_UNPIN_ACK     = "member_unpin_ack"
    MEMBER_UPLOAD        = "member_upload"         # operator → node: may members upload?
    MEMBER_UPLOAD_ACK    = "member_upload_ack"
    # Device linking. A new device files a request bound to a code it displays;
    # an already-pinned device of the same account approves it. Neither the hub
    # nor the node can produce the countersignature.
    DEVICE_REQUEST       = "device_add_request"    # new device → node
    DEVICE_REQUEST_ACK   = "device_add_request_ack"
    DEVICE_LOOKUP        = "device_lookup"         # approver → node: find by code
    DEVICE_LOOKUP_RESULT = "device_lookup_result"
    DEVICE_ADD           = "device_add"            # approver → node: countersigned
    DEVICE_ADD_ACK       = "device_add_ack"
    DEVICE_LIST          = "device_list"           # anyone → node: my devices
    DEVICE_LIST_RESULT   = "device_list_result"
    DEVICE_REVOKE        = "device_revoke"         # a device retires another
    # Rotation is the half of revocation that revocation cannot do: the node
    # generates a fresh key itself, so no key material crosses the wire.
    GEK_ROTATE           = "gek_rotate"            # operator → node: new group key
    GEK_ROTATE_ACK       = "gek_rotate_ack"
    INVITE_RESULT        = "invite_result"         # node → operator: the code, once


# ── Index entry ───────────────────────────────────────────────────────────────

@dataclass
class IndexEntry:
    id:        str    # blake3 hash of file (hex)
    name:      str    # filename
    path:      str    # path relative to shared directory
    size:      int    # bytes
    type:      str    # video | audio | image | document | archive | other
    added_at:  int    # unix timestamp
    duration:  int | None = None   # seconds, for media
    thumb_hash: str | None = None  # blake3 of thumbnail
    uploader_id: str | None = None  # user_id of who uploaded (None = pre-existing on disk)
    uploader_pk: str | None = None  # Ed25519 public key of uploader (base64 raw 32 bytes)


@dataclass
class IndexDelta:
    base_version: int
    version:      int
    additions:    list[IndexEntry] = field(default_factory=list)
    deletions:    list[str] = field(default_factory=list)  # list of ids


# ── Chunk request/response ────────────────────────────────────────────────────

@dataclass
class ChunkRequest:
    file_id:     str   # blake3 hash of file (hex)
    chunk_index: int

@dataclass
class ChunkResponse:
    chunk_index:    int
    plaintext_size: int
    nonce_b64:      str
    ct_b64:         str
    ct_hash_b64:    str
    pt_hash_b64:    str
    sig_b64:        str
    pk_node_b64:    str
    file_hash_b64:  str