aboutsummaryrefslogtreecommitdiffstats
path: root/poc/spike5_client.py
diff options
context:
space:
mode:
Diffstat (limited to 'poc/spike5_client.py')
-rw-r--r--poc/spike5_client.py140
1 files changed, 140 insertions, 0 deletions
diff --git a/poc/spike5_client.py b/poc/spike5_client.py
new file mode 100644
index 0000000..11b3ac0
--- /dev/null
+++ b/poc/spike5_client.py
@@ -0,0 +1,140 @@
+"""
+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, json, base64, struct, time, sys
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
+from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
+from cryptography.hazmat.primitives.kdf.hkdf import HKDF
+from cryptography.hazmat.primitives import hashes
+import blake3
+
+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(f" 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 asyncio.TimeoutError:
+ print(f" {FAIL} Timeout — node did not connect within 30s")
+ sys.exit(1)
+
+asyncio.run(main())