summaryrefslogtreecommitdiffstats
path: root/poc/spike5_client.py
blob: cc79ebff8a1dd81a5fade2a23cdac41e0824f556 (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
144
145
146
147
"""
MeshBay Spike 5 — Encrypted Transfer CLIENT (runs on meshbay.org)

Acts as the "file requester":
  - Opens TCP server on :19003, waits for the node to connect
  - Sends a chunk request
  - Receives the encrypted chunk
  - Verifies Ed25519 signature
  - Verifies blake3 ciphertext hash
  - Decrypts with GEK (included in POC response — never in production)
  - Verifies plaintext integrity via blake3
  - Reports timing and results
"""

import asyncio
import base64
import json
import struct
import sys
import time

import blake3
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

PORT        = 19003
CHUNK_INDEX = 0
PASS = "✓"
FAIL = "✗"

# ── Wire helpers ───────────────────────────────────────────────────────────────

async def send_msg(writer, obj: dict):
    data = json.dumps(obj).encode()
    writer.write(struct.pack('>I', len(data)) + data)
    await writer.drain()

async def recv_msg(reader) -> dict:
    raw_len = await reader.readexactly(4)
    length  = struct.unpack('>I', raw_len)[0]
    data    = await reader.readexactly(length)
    return json.loads(data)

# ── Handler ────────────────────────────────────────────────────────────────────

async def handle_node(reader, writer):
    peer = writer.get_extra_info('peername')
    print(f"  Node connected from {peer[0]}:{peer[1]}")

    # 1. Send chunk request
    await send_msg(writer, {
        "type":        "chunk_req",
        "file_id":     "testfile.bin",
        "chunk_index": CHUNK_INDEX,
    })
    print(f"  Chunk request sent (file=testfile.bin, chunk={CHUNK_INDEX})")

    # 2. Receive encrypted chunk
    t_recv_start = time.perf_counter()
    chunk = await recv_msg(reader)
    recv_ms = (time.perf_counter() - t_recv_start) * 1000
    writer.close()

    ct       = base64.b64decode(chunk["ct_b64"])
    nonce    = base64.b64decode(chunk["nonce_b64"])
    ct_hash  = base64.b64decode(chunk["ct_hash_b64"])
    sig      = base64.b64decode(chunk["sig_b64"])
    pk_bytes = base64.b64decode(chunk["pk_node_b64"])
    gek_raw  = base64.b64decode(chunk["gek_b64"])         # POC only
    file_hash= base64.b64decode(chunk["file_hash_b64"])
    ci       = chunk["chunk_index"]
    chunk_sz = chunk["plaintext_size"]

    print(f"\n  Received {len(ct)/1024:.1f} KB ciphertext in {recv_ms:.0f}ms")
    print(f"  chunk_index={ci}  plaintext_size={chunk_sz/1024:.1f} KB")

    t_verify = time.perf_counter()

    # 3. Verify Ed25519 signature: sig covers (chunk_index || nonce || ct_hash)
    pk_node = Ed25519PublicKey.from_public_bytes(pk_bytes)
    sig_payload = ci.to_bytes(4, 'big') + nonce + ct_hash
    try:
        pk_node.verify(sig, sig_payload)
        print(f"  {PASS} Ed25519 signature valid")
    except Exception as e:
        print(f"  {FAIL} Signature INVALID: {e}")
        sys.exit(1)

    # 4. Verify ciphertext hash
    computed_hash = blake3.blake3(ct).digest()
    if computed_hash == ct_hash:
        print(f"  {PASS} blake3(ciphertext) matches")
    else:
        print(f"  {FAIL} Ciphertext hash MISMATCH")
        sys.exit(1)

    # 5. Derive per-chunk key and decrypt
    chunk_key = HKDF(
        algorithm=hashes.SHA256(), length=32, salt=None,
        info=b"file:" + file_hash + b":chunk:" + ci.to_bytes(4, 'big')
    ).derive(gek_raw)

    try:
        plaintext = ChaCha20Poly1305(chunk_key).decrypt(nonce, ct, None)
        print(f"  {PASS} Decrypted {len(plaintext)/1024:.1f} KB")
    except Exception as e:
        print(f"  {FAIL} Decryption FAILED: {e}")
        sys.exit(1)

    # 6. Verify plaintext hash
    pt_hash = blake3.blake3(plaintext).digest()
    expected_pt_hash = base64.b64decode(chunk["pt_hash_b64"])
    if pt_hash == expected_pt_hash:
        print(f"  {PASS} blake3(plaintext) matches — data integrity confirmed")
    else:
        print(f"  {FAIL} Plaintext hash MISMATCH — corruption or wrong key")
        sys.exit(1)

    verify_ms = (time.perf_counter() - t_verify) * 1000
    total_ms  = recv_ms + verify_ms

    print(f"\n  {'='*50}")
    print("  Timings:")
    print(f"    Network receive : {recv_ms:.0f} ms")
    print(f"    Verify+decrypt  : {verify_ms:.1f} ms")
    print(f"    Total           : {total_ms:.0f} ms")
    throughput = (chunk_sz / 1024 / 1024) / (recv_ms / 1000)
    print(f"    Throughput      : {throughput:.1f} MB/s")
    print(f"\n  {PASS} Spike 5 CLIENT — all checks passed.")
    sys.exit(0)


async def main():
    print("\n=== MeshBay Spike 5 — Encrypted Transfer CLIENT ===")
    print(f"Waiting for node on TCP :{PORT} ...")

    server = await asyncio.start_server(handle_node, '0.0.0.0', PORT)
    async with server:
        try:
            await asyncio.wait_for(server.serve_forever(), timeout=30)
        except TimeoutError:
            print(f"  {FAIL} Timeout — node did not connect within 30s")
            sys.exit(1)

asyncio.run(main())