glypha 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
glypha-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: glypha
3
+ Version: 2.0.0
4
+ Summary: P2P encrypted chat from raw Python sockets: E2E encryption, persistent identities, fingerprint verification, peer discovery, NAT traversal with relay fallback
5
+ Author: Adarsh Mishra
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/<your-github-username>/<repo-name>
8
+ Keywords: p2p,encryption,chat,sockets,nacl,networking,e2e
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Communications :: Chat
11
+ Classifier: Topic :: Education
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: pynacl>=1.5
15
+
16
+ # P2P Encrypted Chat
17
+
18
+ An educational peer-to-peer encrypted messaging system built from raw Python TCP sockets — custom message framing, persistent cryptographic identities, fingerprint-based peer authentication, encrypted local history, peer discovery via a cloud rendezvous server, and a measured NAT traversal experiment.
19
+
20
+ This is **not** a WebRTC/PeerJS wrapper. Every layer — framing, encryption, identity, storage, discovery — is built and understood from the socket up.
21
+
22
+ > ⚠️ **Educational project. Not Signal-grade security. Do not use for sensitive communications.** See [Security Model](#security-model) below.
23
+
24
+ ## Table of Contents
25
+
26
+ - [What It Does](#what-it-does)
27
+ - [Architecture](#architecture)
28
+ - [Usage](#usage)
29
+ - [The NAT Traversal Experiment](#the-nat-traversal-experiment)
30
+ - [Security Model](#security-model)
31
+ - [Repository Layout](#repository-layout)
32
+ - [Testing](#testing)
33
+ - [Bugs Found (and Fixed) Along the Way](#bugs-found-and-fixed-along-the-way)
34
+ - [Infrastructure Notes](#infrastructure-notes)
35
+ - [Roadmap (V2)](#roadmap-v2)
36
+
37
+ ## What It Does
38
+
39
+ | Capability | Proof |
40
+ |---|---|
41
+ | Two peers chat E2E-encrypted | Verified on two physical laptops over LAN, recorded on video |
42
+ | Identity survives restarts | Same fingerprint (`c849:a2b7...`) across every session of the build |
43
+ | History persists, encrypted at rest | Restart → history reloads and decrypts; raw SQL shows only ciphertext |
44
+ | Peer discovery by name | `find aadarsh` → endpoint + fingerprint, worked across two Indian carrier networks + Azure |
45
+ | Cross-network discovery over the real Internet | Rendezvous hosted on an Azure VM; peers on home Wi-Fi (Delhi) + mobile hotspot registered and looked each other up |
46
+ | Fingerprint verification | Both sides display fingerprints; out-of-band comparison; pre-connect verification via the registry |
47
+
48
+ ## Demo
49
+
50
+ 🎥 Two physical laptops chatting over LAN, disconnect → reconnect → history reloads.
51
+
52
+ | Fingerprint verification across two screens | Rendezvous log: two peers, two public NAT IPs | Punch verdict on CGNAT |
53
+ |---|---|---|
54
+ | The out-of-band fingerprint check, side by side | The registry holding two different public NAT mappings (home Wi-Fi + mobile hotspot) — discovery across the real Internet | Control pass on loopback vs. failure on real CGNAT |
55
+
56
+ *(Screenshots live in `screenshots/`.)*
57
+
58
+ ## Architecture
59
+
60
+ ```text
61
+ ┌──────────────────────┐
62
+ │ Rendezvous Server │
63
+ │ Discovery/Signaling │ (Azure VM, port 7000)
64
+ └──────────┬───────────┘
65
+ │ endpoint + fingerprint
66
+ ↙ ↘
67
+ ┌────────┐ ┌────────┐
68
+ │ Peer A │◄─►│ Peer B │ direct P2P chat (port 9999)
69
+ └────────┘ └────────┘
70
+ ```
71
+
72
+ Every layer of the transport stack is implemented, not imported:
73
+
74
+ ```text
75
+ Application (chat, history)
76
+
77
+ Peer management (listen / connect / find / punch modes)
78
+
79
+ Cryptographic identity (persistent keypairs, fingerprints)
80
+
81
+ PyNaCl Box (X25519 key exchange + XSalsa20-Poly1305 AEAD)
82
+
83
+ Custom framing ([4-byte big-endian length][payload])
84
+
85
+ TCP sockets (blocking, threaded receive)
86
+
87
+ IP / NAT / firewall
88
+ ```
89
+
90
+ Local storage is a separate concern from transport: every message is passed through `SecretBox.encrypt` before being written to a SQLite `BLOB`, using a storage key that is distinct from the transport identity key.
91
+
92
+ ## Usage
93
+
94
+ ```bash
95
+ git clone <repo> && cd p2p-chat
96
+ pip install pynacl
97
+
98
+ # terminal 1 — rendezvous server (or point at the deployed one)
99
+ python rendezvous_server.py # port 7000
100
+
101
+ # terminal 2 — peer A, discoverable by name
102
+ python peer.py listen 9999 aadarsh <rv_ip>
103
+
104
+ # terminal 3 — peer B finds and connects
105
+ python peer.py find aadarsh <rv_ip>
106
+ python peer.py connect <aadarsh-ip> 9999 ishu
107
+
108
+ # NAT traversal attempt (coordinated simultaneous open)
109
+ python peer.py punch <my_id> <peer_id> <my_port> <rv_ip>
110
+ ```
111
+
112
+ Once connected, both sides display their fingerprints. Compare them out-of-band (e.g. a phone call) before trusting the session. Type `quit` to exit — history reloads automatically the next time you connect to the same peer.
113
+
114
+ ## The NAT Traversal Experiment
115
+
116
+ This is the centerpiece of the V1 build: a real, measured attempt at direct P2P connectivity across the open Internet — not a simulated or hand-waved result.
117
+
118
+ **Setup:** Rendezvous server on an Azure VM (Central India). Peer A on home Wi-Fi (`192.168.1.8` local, NAT public `122.162.151.183`). Peer B on a phone hotspot (`10.197.183.135` local behind carrier-grade NAT, NAT public `157.49.119.123`).
119
+
120
+ | Test | Result |
121
+ |---|---|
122
+ | Baseline: uncoordinated direct dial across the Internet | ❌ `WinError 10060` timeout (~20s) — NAT drops the unsolicited inbound SYN |
123
+ | Control: simultaneous open on loopback | ✅ Punched through — hole-punching mechanics proven correct |
124
+ | Simultaneous open, home NAT ↔ Airtel CGNAT, 60s | ❌ Neither NAT delivered the peer's SYNs |
125
+
126
+ **Conclusion:** the code is correct (the loopback control passes); the network refuses. Carrier-grade NAT shares one public IP across thousands of subscribers with endpoint-dependent mapping, which direct TCP hole punching cannot reliably defeat. This is a measured failure, explained at the NAT level, not a bug.
127
+
128
+ **Fallback strategy (documented, V2):** a relay forwards already-encrypted traffic. The rendezvous/relay server never holds keys — by construction, it doesn't even have PyNaCl installed, so the "phone book" literally cannot read messages even if it wanted to.
129
+
130
+ ## Security Model
131
+
132
+ **Protects against:**
133
+ - Passive network observers (end-to-end AEAD encryption)
134
+ - Peer impersonation across reconnects (persistent identities + fingerprint verification)
135
+ - Plaintext history on disk (SQLite holds only ciphertext)
136
+ - IP changes redefining identity (history is keyed by fingerprint, not IP)
137
+
138
+ **Does NOT protect against:**
139
+ - **No forward secrecy** — static-static ECDH means a later key compromise exposes previously recorded traffic; there is no ratchet
140
+ - Unencrypted key files at rest
141
+ - Replay attacks (no counters or nonces yet)
142
+ - A malicious rendezvous server serving the wrong endpoint (fingerprint comparison mitigates this, but Trust-On-First-Use doesn't eliminate it)
143
+ - Traffic analysis / metadata leakage
144
+ - Machine compromise
145
+ - `peer_id` squatting — names aren't yet bound to keys (V2 pins names to the first-registered key)
146
+ - Implementation bugs not yet found
147
+
148
+ This is an educational project. **Do not use it for sensitive real-world communications.**
149
+
150
+ ## Repository Layout
151
+
152
+ ```text
153
+ p2p-chat/
154
+ ├── peer.py # unified peer: listen | connect | find | punch
155
+ ├── protocol.py # length-prefixed framing (OSError-as-EOF contract)
156
+ ├── identity.py # persistent transport + storage keys
157
+ ├── storage.py # encrypted SQLite history
158
+ ├── rendezvous_server.py # discovery/signaling (port 7000, TTL 90s)
159
+ ├── crypto_test.py # manual crypto sanity check
160
+ ├── tests/ # 19 pytest tests
161
+ ├── project.md # living engineering doc (milestones, threat model)
162
+ ├── README.md # this file
163
+ └── .gitignore # *.bin *.db *.pem __pycache__/ .pytest_cache/
164
+ ```
165
+
166
+ ## Testing
167
+
168
+ 19 automated tests across 3 files, running in ~0.53s:
169
+
170
+ - **`test_protocol.py` (8 tests):** roundtrips (small/empty/binary-256), back-to-back frames in one burst, fragmented delivery (header split mid-stream), 1 MiB through a threaded reader, clean-FIN → `None`, RST → `None` (regression for the M13 shutdown bug, reproduced via `SO_LINGER(0)`)
171
+ - **`test_storage.py` (6 tests):** roundtrip, insertion order (locks `ORDER BY id`), per-fingerprint isolation, message direction, ciphertext-at-rest (reads the raw SQL like an attacker would), wrong key → `CryptoError` (regression for the M12 key-orphaning incident)
172
+ - **`test_identity.py` (5 tests):** key persistence, distinct identities, fingerprint stability across reloads, fingerprint format contract (16×4 hex groups; stripping colons = raw SHA-256), `SecretBox` key size
173
+
174
+ ```bash
175
+ python -m pytest tests/ -v
176
+ # 19 passed in ~0.5s
177
+ ```
178
+
179
+ ## Bugs Found (and Fixed) Along the Way
180
+
181
+ Each of these was caught by the test suite or a real run, and each is now guarded against:
182
+
183
+ - **`Peer.py` vs `peer.py`** — Windows case-insensitivity masked this; caught on the first test run and would have broken on Linux/macOS
184
+ - **Windows `Ctrl+C` sends RST, not FIN** — the receive thread crashed with `ConnectionResetError`; fixed by treating `OSError` as EOF everywhere in `recv_message`, giving one consistent failure convention
185
+ - **Daemon thread killed mid-print at shutdown** (`Fatal Python error: _enter_buffered_busy`) — fixed by joining the receiver thread with a 2s timeout before exit
186
+ - **Phantom "Peer disconnected" on your own `quit`** — fixed with an `if connected:` guard
187
+ - **Storage-key rename orphaned encrypted history** — surfaces as a loud `CryptoError` by design; now locked by a regression test
188
+ - **`ORDER BY timestamp` scrambled messages at second-granularity** — switched to `ORDER BY id`
189
+ - **Fingerprint gate was exact-match and rejected `"y"`** — fixed with an explicit allowlist (`yes`/`y`); anything else still fails closed
190
+ - **Punch-mode split-brain connections** — fixed with identity-proof at accept: a connection must present the public key matching the registry fingerprint, or it's dropped
191
+ - **Windows failed-socket reuse** — fixed by using a fresh socket per punch attempt
192
+ - **`get_lan_ip()` originally used a TCP socket** — it would open a real connection to `8.8.8.8:80` (hanging ~20s when unreachable) instead of doing a route lookup — replaced with a UDP socket, which never sends a packet
193
+ - **Rendezvous lookup race** — fixed with a 20s retry loop so start order no longer matters
194
+ - **Python block-buffers stdout when redirected**, causing empty logs on an otherwise healthy server — fixed by running with `python3 -u`
195
+
196
+ ## Infrastructure Notes
197
+
198
+ Real cloud/ops lessons from deploying the rendezvous server:
199
+
200
+ - **Azure for Students** has region allowlist restrictions (`RequestDisallowedByAzure`) and a B-series vCPU quota of 0 in some regions; worked around with a `B2ats_v2` instance (~$0.0062/hr). No auto-shutdown was available in-region, so a manual portal-stop discipline plus a $15 budget alert was used instead.
201
+ - Two independent firewall layers must both be opened for the server to be reachable: the cloud Network Security Group **and** the OS-level `ufw`.
202
+ - A VM restart kills running processes but not files, so the server is started with `nohup python3 -u rendezvous_server.py &` every time; `pgrep` is checked first to avoid stale double instances (which caused a confusing false failure once).
203
+ - MinTTY/Git Bash swallows `Ctrl+C` for console-less programs — use `winpty`.
204
+ - The VM's public IP can change on deallocate, so it's always re-read from the Azure portal rather than assumed.
205
+ - The rendezvous server records the **observed** source IP of a connecting peer (which can't be lied to) alongside the **peer-advertised** listen port (which it has no way to independently verify). Registering via `127.0.0.1` poisons the registry — the same observed-vs-advertised mechanism is what later reveals real NAT public mappings.
206
+
207
+ ## Roadmap (V2)
208
+
209
+ - **Relay implementation** — the fallback for when punching fails; already-encrypted traffic is relayed, and the rendezvous server is upgraded to hand out relay info. This is what makes cross-Internet chat work reliably, not just when NAT cooperates.
210
+ - **Packaging** — a `pyproject.toml` and a PyPI release, so usage becomes `pip install <name>` followed by `p2pchat listen --name aadarsh` / `p2pchat connect aadarsh`, with no files, ports, or internals exposed to the end user.
211
+ - **Auto-fallback chain** — direct → punch → relay, orchestrated by a connection state machine.
212
+ - **Name pinning** — first registration binds a `peer_id` to a public key, closing the squatting gap.
213
+ - **Hardening** — `MAX_MESSAGE_SIZE`, replay counters, heartbeats/timeouts, and key-file permissions.
214
+
215
+ ---
216
+
217
+ *Stack: Python 3.13, stdlib `socket`/`threading`/`struct`/`sqlite3`/`json`, PyNaCl (libsodium bindings), pytest. No frameworks. ~5 core modules + rendezvous server, 19 automated tests, 15 milestones, ~4 weeks of build time.*
glypha-2.0.0/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # P2P Encrypted Chat
2
+
3
+ An educational peer-to-peer encrypted messaging system built from raw Python TCP sockets — custom message framing, persistent cryptographic identities, fingerprint-based peer authentication, encrypted local history, peer discovery via a cloud rendezvous server, and a measured NAT traversal experiment.
4
+
5
+ This is **not** a WebRTC/PeerJS wrapper. Every layer — framing, encryption, identity, storage, discovery — is built and understood from the socket up.
6
+
7
+ > ⚠️ **Educational project. Not Signal-grade security. Do not use for sensitive communications.** See [Security Model](#security-model) below.
8
+
9
+ ## Table of Contents
10
+
11
+ - [What It Does](#what-it-does)
12
+ - [Architecture](#architecture)
13
+ - [Usage](#usage)
14
+ - [The NAT Traversal Experiment](#the-nat-traversal-experiment)
15
+ - [Security Model](#security-model)
16
+ - [Repository Layout](#repository-layout)
17
+ - [Testing](#testing)
18
+ - [Bugs Found (and Fixed) Along the Way](#bugs-found-and-fixed-along-the-way)
19
+ - [Infrastructure Notes](#infrastructure-notes)
20
+ - [Roadmap (V2)](#roadmap-v2)
21
+
22
+ ## What It Does
23
+
24
+ | Capability | Proof |
25
+ |---|---|
26
+ | Two peers chat E2E-encrypted | Verified on two physical laptops over LAN, recorded on video |
27
+ | Identity survives restarts | Same fingerprint (`c849:a2b7...`) across every session of the build |
28
+ | History persists, encrypted at rest | Restart → history reloads and decrypts; raw SQL shows only ciphertext |
29
+ | Peer discovery by name | `find aadarsh` → endpoint + fingerprint, worked across two Indian carrier networks + Azure |
30
+ | Cross-network discovery over the real Internet | Rendezvous hosted on an Azure VM; peers on home Wi-Fi (Delhi) + mobile hotspot registered and looked each other up |
31
+ | Fingerprint verification | Both sides display fingerprints; out-of-band comparison; pre-connect verification via the registry |
32
+
33
+ ## Demo
34
+
35
+ 🎥 Two physical laptops chatting over LAN, disconnect → reconnect → history reloads.
36
+
37
+ | Fingerprint verification across two screens | Rendezvous log: two peers, two public NAT IPs | Punch verdict on CGNAT |
38
+ |---|---|---|
39
+ | The out-of-band fingerprint check, side by side | The registry holding two different public NAT mappings (home Wi-Fi + mobile hotspot) — discovery across the real Internet | Control pass on loopback vs. failure on real CGNAT |
40
+
41
+ *(Screenshots live in `screenshots/`.)*
42
+
43
+ ## Architecture
44
+
45
+ ```text
46
+ ┌──────────────────────┐
47
+ │ Rendezvous Server │
48
+ │ Discovery/Signaling │ (Azure VM, port 7000)
49
+ └──────────┬───────────┘
50
+ │ endpoint + fingerprint
51
+ ↙ ↘
52
+ ┌────────┐ ┌────────┐
53
+ │ Peer A │◄─►│ Peer B │ direct P2P chat (port 9999)
54
+ └────────┘ └────────┘
55
+ ```
56
+
57
+ Every layer of the transport stack is implemented, not imported:
58
+
59
+ ```text
60
+ Application (chat, history)
61
+
62
+ Peer management (listen / connect / find / punch modes)
63
+
64
+ Cryptographic identity (persistent keypairs, fingerprints)
65
+
66
+ PyNaCl Box (X25519 key exchange + XSalsa20-Poly1305 AEAD)
67
+
68
+ Custom framing ([4-byte big-endian length][payload])
69
+
70
+ TCP sockets (blocking, threaded receive)
71
+
72
+ IP / NAT / firewall
73
+ ```
74
+
75
+ Local storage is a separate concern from transport: every message is passed through `SecretBox.encrypt` before being written to a SQLite `BLOB`, using a storage key that is distinct from the transport identity key.
76
+
77
+ ## Usage
78
+
79
+ ```bash
80
+ git clone <repo> && cd p2p-chat
81
+ pip install pynacl
82
+
83
+ # terminal 1 — rendezvous server (or point at the deployed one)
84
+ python rendezvous_server.py # port 7000
85
+
86
+ # terminal 2 — peer A, discoverable by name
87
+ python peer.py listen 9999 aadarsh <rv_ip>
88
+
89
+ # terminal 3 — peer B finds and connects
90
+ python peer.py find aadarsh <rv_ip>
91
+ python peer.py connect <aadarsh-ip> 9999 ishu
92
+
93
+ # NAT traversal attempt (coordinated simultaneous open)
94
+ python peer.py punch <my_id> <peer_id> <my_port> <rv_ip>
95
+ ```
96
+
97
+ Once connected, both sides display their fingerprints. Compare them out-of-band (e.g. a phone call) before trusting the session. Type `quit` to exit — history reloads automatically the next time you connect to the same peer.
98
+
99
+ ## The NAT Traversal Experiment
100
+
101
+ This is the centerpiece of the V1 build: a real, measured attempt at direct P2P connectivity across the open Internet — not a simulated or hand-waved result.
102
+
103
+ **Setup:** Rendezvous server on an Azure VM (Central India). Peer A on home Wi-Fi (`192.168.1.8` local, NAT public `122.162.151.183`). Peer B on a phone hotspot (`10.197.183.135` local behind carrier-grade NAT, NAT public `157.49.119.123`).
104
+
105
+ | Test | Result |
106
+ |---|---|
107
+ | Baseline: uncoordinated direct dial across the Internet | ❌ `WinError 10060` timeout (~20s) — NAT drops the unsolicited inbound SYN |
108
+ | Control: simultaneous open on loopback | ✅ Punched through — hole-punching mechanics proven correct |
109
+ | Simultaneous open, home NAT ↔ Airtel CGNAT, 60s | ❌ Neither NAT delivered the peer's SYNs |
110
+
111
+ **Conclusion:** the code is correct (the loopback control passes); the network refuses. Carrier-grade NAT shares one public IP across thousands of subscribers with endpoint-dependent mapping, which direct TCP hole punching cannot reliably defeat. This is a measured failure, explained at the NAT level, not a bug.
112
+
113
+ **Fallback strategy (documented, V2):** a relay forwards already-encrypted traffic. The rendezvous/relay server never holds keys — by construction, it doesn't even have PyNaCl installed, so the "phone book" literally cannot read messages even if it wanted to.
114
+
115
+ ## Security Model
116
+
117
+ **Protects against:**
118
+ - Passive network observers (end-to-end AEAD encryption)
119
+ - Peer impersonation across reconnects (persistent identities + fingerprint verification)
120
+ - Plaintext history on disk (SQLite holds only ciphertext)
121
+ - IP changes redefining identity (history is keyed by fingerprint, not IP)
122
+
123
+ **Does NOT protect against:**
124
+ - **No forward secrecy** — static-static ECDH means a later key compromise exposes previously recorded traffic; there is no ratchet
125
+ - Unencrypted key files at rest
126
+ - Replay attacks (no counters or nonces yet)
127
+ - A malicious rendezvous server serving the wrong endpoint (fingerprint comparison mitigates this, but Trust-On-First-Use doesn't eliminate it)
128
+ - Traffic analysis / metadata leakage
129
+ - Machine compromise
130
+ - `peer_id` squatting — names aren't yet bound to keys (V2 pins names to the first-registered key)
131
+ - Implementation bugs not yet found
132
+
133
+ This is an educational project. **Do not use it for sensitive real-world communications.**
134
+
135
+ ## Repository Layout
136
+
137
+ ```text
138
+ p2p-chat/
139
+ ├── peer.py # unified peer: listen | connect | find | punch
140
+ ├── protocol.py # length-prefixed framing (OSError-as-EOF contract)
141
+ ├── identity.py # persistent transport + storage keys
142
+ ├── storage.py # encrypted SQLite history
143
+ ├── rendezvous_server.py # discovery/signaling (port 7000, TTL 90s)
144
+ ├── crypto_test.py # manual crypto sanity check
145
+ ├── tests/ # 19 pytest tests
146
+ ├── project.md # living engineering doc (milestones, threat model)
147
+ ├── README.md # this file
148
+ └── .gitignore # *.bin *.db *.pem __pycache__/ .pytest_cache/
149
+ ```
150
+
151
+ ## Testing
152
+
153
+ 19 automated tests across 3 files, running in ~0.53s:
154
+
155
+ - **`test_protocol.py` (8 tests):** roundtrips (small/empty/binary-256), back-to-back frames in one burst, fragmented delivery (header split mid-stream), 1 MiB through a threaded reader, clean-FIN → `None`, RST → `None` (regression for the M13 shutdown bug, reproduced via `SO_LINGER(0)`)
156
+ - **`test_storage.py` (6 tests):** roundtrip, insertion order (locks `ORDER BY id`), per-fingerprint isolation, message direction, ciphertext-at-rest (reads the raw SQL like an attacker would), wrong key → `CryptoError` (regression for the M12 key-orphaning incident)
157
+ - **`test_identity.py` (5 tests):** key persistence, distinct identities, fingerprint stability across reloads, fingerprint format contract (16×4 hex groups; stripping colons = raw SHA-256), `SecretBox` key size
158
+
159
+ ```bash
160
+ python -m pytest tests/ -v
161
+ # 19 passed in ~0.5s
162
+ ```
163
+
164
+ ## Bugs Found (and Fixed) Along the Way
165
+
166
+ Each of these was caught by the test suite or a real run, and each is now guarded against:
167
+
168
+ - **`Peer.py` vs `peer.py`** — Windows case-insensitivity masked this; caught on the first test run and would have broken on Linux/macOS
169
+ - **Windows `Ctrl+C` sends RST, not FIN** — the receive thread crashed with `ConnectionResetError`; fixed by treating `OSError` as EOF everywhere in `recv_message`, giving one consistent failure convention
170
+ - **Daemon thread killed mid-print at shutdown** (`Fatal Python error: _enter_buffered_busy`) — fixed by joining the receiver thread with a 2s timeout before exit
171
+ - **Phantom "Peer disconnected" on your own `quit`** — fixed with an `if connected:` guard
172
+ - **Storage-key rename orphaned encrypted history** — surfaces as a loud `CryptoError` by design; now locked by a regression test
173
+ - **`ORDER BY timestamp` scrambled messages at second-granularity** — switched to `ORDER BY id`
174
+ - **Fingerprint gate was exact-match and rejected `"y"`** — fixed with an explicit allowlist (`yes`/`y`); anything else still fails closed
175
+ - **Punch-mode split-brain connections** — fixed with identity-proof at accept: a connection must present the public key matching the registry fingerprint, or it's dropped
176
+ - **Windows failed-socket reuse** — fixed by using a fresh socket per punch attempt
177
+ - **`get_lan_ip()` originally used a TCP socket** — it would open a real connection to `8.8.8.8:80` (hanging ~20s when unreachable) instead of doing a route lookup — replaced with a UDP socket, which never sends a packet
178
+ - **Rendezvous lookup race** — fixed with a 20s retry loop so start order no longer matters
179
+ - **Python block-buffers stdout when redirected**, causing empty logs on an otherwise healthy server — fixed by running with `python3 -u`
180
+
181
+ ## Infrastructure Notes
182
+
183
+ Real cloud/ops lessons from deploying the rendezvous server:
184
+
185
+ - **Azure for Students** has region allowlist restrictions (`RequestDisallowedByAzure`) and a B-series vCPU quota of 0 in some regions; worked around with a `B2ats_v2` instance (~$0.0062/hr). No auto-shutdown was available in-region, so a manual portal-stop discipline plus a $15 budget alert was used instead.
186
+ - Two independent firewall layers must both be opened for the server to be reachable: the cloud Network Security Group **and** the OS-level `ufw`.
187
+ - A VM restart kills running processes but not files, so the server is started with `nohup python3 -u rendezvous_server.py &` every time; `pgrep` is checked first to avoid stale double instances (which caused a confusing false failure once).
188
+ - MinTTY/Git Bash swallows `Ctrl+C` for console-less programs — use `winpty`.
189
+ - The VM's public IP can change on deallocate, so it's always re-read from the Azure portal rather than assumed.
190
+ - The rendezvous server records the **observed** source IP of a connecting peer (which can't be lied to) alongside the **peer-advertised** listen port (which it has no way to independently verify). Registering via `127.0.0.1` poisons the registry — the same observed-vs-advertised mechanism is what later reveals real NAT public mappings.
191
+
192
+ ## Roadmap (V2)
193
+
194
+ - **Relay implementation** — the fallback for when punching fails; already-encrypted traffic is relayed, and the rendezvous server is upgraded to hand out relay info. This is what makes cross-Internet chat work reliably, not just when NAT cooperates.
195
+ - **Packaging** — a `pyproject.toml` and a PyPI release, so usage becomes `pip install <name>` followed by `p2pchat listen --name aadarsh` / `p2pchat connect aadarsh`, with no files, ports, or internals exposed to the end user.
196
+ - **Auto-fallback chain** — direct → punch → relay, orchestrated by a connection state machine.
197
+ - **Name pinning** — first registration binds a `peer_id` to a public key, closing the squatting gap.
198
+ - **Hardening** — `MAX_MESSAGE_SIZE`, replay counters, heartbeats/timeouts, and key-file permissions.
199
+
200
+ ---
201
+
202
+ *Stack: Python 3.13, stdlib `socket`/`threading`/`struct`/`sqlite3`/`json`, PyNaCl (libsodium bindings), pytest. No frameworks. ~5 core modules + rendezvous server, 19 automated tests, 15 milestones, ~4 weeks of build time.*
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: glypha
3
+ Version: 2.0.0
4
+ Summary: P2P encrypted chat from raw Python sockets: E2E encryption, persistent identities, fingerprint verification, peer discovery, NAT traversal with relay fallback
5
+ Author: Adarsh Mishra
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/<your-github-username>/<repo-name>
8
+ Keywords: p2p,encryption,chat,sockets,nacl,networking,e2e
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Communications :: Chat
11
+ Classifier: Topic :: Education
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: pynacl>=1.5
15
+
16
+ # P2P Encrypted Chat
17
+
18
+ An educational peer-to-peer encrypted messaging system built from raw Python TCP sockets — custom message framing, persistent cryptographic identities, fingerprint-based peer authentication, encrypted local history, peer discovery via a cloud rendezvous server, and a measured NAT traversal experiment.
19
+
20
+ This is **not** a WebRTC/PeerJS wrapper. Every layer — framing, encryption, identity, storage, discovery — is built and understood from the socket up.
21
+
22
+ > ⚠️ **Educational project. Not Signal-grade security. Do not use for sensitive communications.** See [Security Model](#security-model) below.
23
+
24
+ ## Table of Contents
25
+
26
+ - [What It Does](#what-it-does)
27
+ - [Architecture](#architecture)
28
+ - [Usage](#usage)
29
+ - [The NAT Traversal Experiment](#the-nat-traversal-experiment)
30
+ - [Security Model](#security-model)
31
+ - [Repository Layout](#repository-layout)
32
+ - [Testing](#testing)
33
+ - [Bugs Found (and Fixed) Along the Way](#bugs-found-and-fixed-along-the-way)
34
+ - [Infrastructure Notes](#infrastructure-notes)
35
+ - [Roadmap (V2)](#roadmap-v2)
36
+
37
+ ## What It Does
38
+
39
+ | Capability | Proof |
40
+ |---|---|
41
+ | Two peers chat E2E-encrypted | Verified on two physical laptops over LAN, recorded on video |
42
+ | Identity survives restarts | Same fingerprint (`c849:a2b7...`) across every session of the build |
43
+ | History persists, encrypted at rest | Restart → history reloads and decrypts; raw SQL shows only ciphertext |
44
+ | Peer discovery by name | `find aadarsh` → endpoint + fingerprint, worked across two Indian carrier networks + Azure |
45
+ | Cross-network discovery over the real Internet | Rendezvous hosted on an Azure VM; peers on home Wi-Fi (Delhi) + mobile hotspot registered and looked each other up |
46
+ | Fingerprint verification | Both sides display fingerprints; out-of-band comparison; pre-connect verification via the registry |
47
+
48
+ ## Demo
49
+
50
+ 🎥 Two physical laptops chatting over LAN, disconnect → reconnect → history reloads.
51
+
52
+ | Fingerprint verification across two screens | Rendezvous log: two peers, two public NAT IPs | Punch verdict on CGNAT |
53
+ |---|---|---|
54
+ | The out-of-band fingerprint check, side by side | The registry holding two different public NAT mappings (home Wi-Fi + mobile hotspot) — discovery across the real Internet | Control pass on loopback vs. failure on real CGNAT |
55
+
56
+ *(Screenshots live in `screenshots/`.)*
57
+
58
+ ## Architecture
59
+
60
+ ```text
61
+ ┌──────────────────────┐
62
+ │ Rendezvous Server │
63
+ │ Discovery/Signaling │ (Azure VM, port 7000)
64
+ └──────────┬───────────┘
65
+ │ endpoint + fingerprint
66
+ ↙ ↘
67
+ ┌────────┐ ┌────────┐
68
+ │ Peer A │◄─►│ Peer B │ direct P2P chat (port 9999)
69
+ └────────┘ └────────┘
70
+ ```
71
+
72
+ Every layer of the transport stack is implemented, not imported:
73
+
74
+ ```text
75
+ Application (chat, history)
76
+
77
+ Peer management (listen / connect / find / punch modes)
78
+
79
+ Cryptographic identity (persistent keypairs, fingerprints)
80
+
81
+ PyNaCl Box (X25519 key exchange + XSalsa20-Poly1305 AEAD)
82
+
83
+ Custom framing ([4-byte big-endian length][payload])
84
+
85
+ TCP sockets (blocking, threaded receive)
86
+
87
+ IP / NAT / firewall
88
+ ```
89
+
90
+ Local storage is a separate concern from transport: every message is passed through `SecretBox.encrypt` before being written to a SQLite `BLOB`, using a storage key that is distinct from the transport identity key.
91
+
92
+ ## Usage
93
+
94
+ ```bash
95
+ git clone <repo> && cd p2p-chat
96
+ pip install pynacl
97
+
98
+ # terminal 1 — rendezvous server (or point at the deployed one)
99
+ python rendezvous_server.py # port 7000
100
+
101
+ # terminal 2 — peer A, discoverable by name
102
+ python peer.py listen 9999 aadarsh <rv_ip>
103
+
104
+ # terminal 3 — peer B finds and connects
105
+ python peer.py find aadarsh <rv_ip>
106
+ python peer.py connect <aadarsh-ip> 9999 ishu
107
+
108
+ # NAT traversal attempt (coordinated simultaneous open)
109
+ python peer.py punch <my_id> <peer_id> <my_port> <rv_ip>
110
+ ```
111
+
112
+ Once connected, both sides display their fingerprints. Compare them out-of-band (e.g. a phone call) before trusting the session. Type `quit` to exit — history reloads automatically the next time you connect to the same peer.
113
+
114
+ ## The NAT Traversal Experiment
115
+
116
+ This is the centerpiece of the V1 build: a real, measured attempt at direct P2P connectivity across the open Internet — not a simulated or hand-waved result.
117
+
118
+ **Setup:** Rendezvous server on an Azure VM (Central India). Peer A on home Wi-Fi (`192.168.1.8` local, NAT public `122.162.151.183`). Peer B on a phone hotspot (`10.197.183.135` local behind carrier-grade NAT, NAT public `157.49.119.123`).
119
+
120
+ | Test | Result |
121
+ |---|---|
122
+ | Baseline: uncoordinated direct dial across the Internet | ❌ `WinError 10060` timeout (~20s) — NAT drops the unsolicited inbound SYN |
123
+ | Control: simultaneous open on loopback | ✅ Punched through — hole-punching mechanics proven correct |
124
+ | Simultaneous open, home NAT ↔ Airtel CGNAT, 60s | ❌ Neither NAT delivered the peer's SYNs |
125
+
126
+ **Conclusion:** the code is correct (the loopback control passes); the network refuses. Carrier-grade NAT shares one public IP across thousands of subscribers with endpoint-dependent mapping, which direct TCP hole punching cannot reliably defeat. This is a measured failure, explained at the NAT level, not a bug.
127
+
128
+ **Fallback strategy (documented, V2):** a relay forwards already-encrypted traffic. The rendezvous/relay server never holds keys — by construction, it doesn't even have PyNaCl installed, so the "phone book" literally cannot read messages even if it wanted to.
129
+
130
+ ## Security Model
131
+
132
+ **Protects against:**
133
+ - Passive network observers (end-to-end AEAD encryption)
134
+ - Peer impersonation across reconnects (persistent identities + fingerprint verification)
135
+ - Plaintext history on disk (SQLite holds only ciphertext)
136
+ - IP changes redefining identity (history is keyed by fingerprint, not IP)
137
+
138
+ **Does NOT protect against:**
139
+ - **No forward secrecy** — static-static ECDH means a later key compromise exposes previously recorded traffic; there is no ratchet
140
+ - Unencrypted key files at rest
141
+ - Replay attacks (no counters or nonces yet)
142
+ - A malicious rendezvous server serving the wrong endpoint (fingerprint comparison mitigates this, but Trust-On-First-Use doesn't eliminate it)
143
+ - Traffic analysis / metadata leakage
144
+ - Machine compromise
145
+ - `peer_id` squatting — names aren't yet bound to keys (V2 pins names to the first-registered key)
146
+ - Implementation bugs not yet found
147
+
148
+ This is an educational project. **Do not use it for sensitive real-world communications.**
149
+
150
+ ## Repository Layout
151
+
152
+ ```text
153
+ p2p-chat/
154
+ ├── peer.py # unified peer: listen | connect | find | punch
155
+ ├── protocol.py # length-prefixed framing (OSError-as-EOF contract)
156
+ ├── identity.py # persistent transport + storage keys
157
+ ├── storage.py # encrypted SQLite history
158
+ ├── rendezvous_server.py # discovery/signaling (port 7000, TTL 90s)
159
+ ├── crypto_test.py # manual crypto sanity check
160
+ ├── tests/ # 19 pytest tests
161
+ ├── project.md # living engineering doc (milestones, threat model)
162
+ ├── README.md # this file
163
+ └── .gitignore # *.bin *.db *.pem __pycache__/ .pytest_cache/
164
+ ```
165
+
166
+ ## Testing
167
+
168
+ 19 automated tests across 3 files, running in ~0.53s:
169
+
170
+ - **`test_protocol.py` (8 tests):** roundtrips (small/empty/binary-256), back-to-back frames in one burst, fragmented delivery (header split mid-stream), 1 MiB through a threaded reader, clean-FIN → `None`, RST → `None` (regression for the M13 shutdown bug, reproduced via `SO_LINGER(0)`)
171
+ - **`test_storage.py` (6 tests):** roundtrip, insertion order (locks `ORDER BY id`), per-fingerprint isolation, message direction, ciphertext-at-rest (reads the raw SQL like an attacker would), wrong key → `CryptoError` (regression for the M12 key-orphaning incident)
172
+ - **`test_identity.py` (5 tests):** key persistence, distinct identities, fingerprint stability across reloads, fingerprint format contract (16×4 hex groups; stripping colons = raw SHA-256), `SecretBox` key size
173
+
174
+ ```bash
175
+ python -m pytest tests/ -v
176
+ # 19 passed in ~0.5s
177
+ ```
178
+
179
+ ## Bugs Found (and Fixed) Along the Way
180
+
181
+ Each of these was caught by the test suite or a real run, and each is now guarded against:
182
+
183
+ - **`Peer.py` vs `peer.py`** — Windows case-insensitivity masked this; caught on the first test run and would have broken on Linux/macOS
184
+ - **Windows `Ctrl+C` sends RST, not FIN** — the receive thread crashed with `ConnectionResetError`; fixed by treating `OSError` as EOF everywhere in `recv_message`, giving one consistent failure convention
185
+ - **Daemon thread killed mid-print at shutdown** (`Fatal Python error: _enter_buffered_busy`) — fixed by joining the receiver thread with a 2s timeout before exit
186
+ - **Phantom "Peer disconnected" on your own `quit`** — fixed with an `if connected:` guard
187
+ - **Storage-key rename orphaned encrypted history** — surfaces as a loud `CryptoError` by design; now locked by a regression test
188
+ - **`ORDER BY timestamp` scrambled messages at second-granularity** — switched to `ORDER BY id`
189
+ - **Fingerprint gate was exact-match and rejected `"y"`** — fixed with an explicit allowlist (`yes`/`y`); anything else still fails closed
190
+ - **Punch-mode split-brain connections** — fixed with identity-proof at accept: a connection must present the public key matching the registry fingerprint, or it's dropped
191
+ - **Windows failed-socket reuse** — fixed by using a fresh socket per punch attempt
192
+ - **`get_lan_ip()` originally used a TCP socket** — it would open a real connection to `8.8.8.8:80` (hanging ~20s when unreachable) instead of doing a route lookup — replaced with a UDP socket, which never sends a packet
193
+ - **Rendezvous lookup race** — fixed with a 20s retry loop so start order no longer matters
194
+ - **Python block-buffers stdout when redirected**, causing empty logs on an otherwise healthy server — fixed by running with `python3 -u`
195
+
196
+ ## Infrastructure Notes
197
+
198
+ Real cloud/ops lessons from deploying the rendezvous server:
199
+
200
+ - **Azure for Students** has region allowlist restrictions (`RequestDisallowedByAzure`) and a B-series vCPU quota of 0 in some regions; worked around with a `B2ats_v2` instance (~$0.0062/hr). No auto-shutdown was available in-region, so a manual portal-stop discipline plus a $15 budget alert was used instead.
201
+ - Two independent firewall layers must both be opened for the server to be reachable: the cloud Network Security Group **and** the OS-level `ufw`.
202
+ - A VM restart kills running processes but not files, so the server is started with `nohup python3 -u rendezvous_server.py &` every time; `pgrep` is checked first to avoid stale double instances (which caused a confusing false failure once).
203
+ - MinTTY/Git Bash swallows `Ctrl+C` for console-less programs — use `winpty`.
204
+ - The VM's public IP can change on deallocate, so it's always re-read from the Azure portal rather than assumed.
205
+ - The rendezvous server records the **observed** source IP of a connecting peer (which can't be lied to) alongside the **peer-advertised** listen port (which it has no way to independently verify). Registering via `127.0.0.1` poisons the registry — the same observed-vs-advertised mechanism is what later reveals real NAT public mappings.
206
+
207
+ ## Roadmap (V2)
208
+
209
+ - **Relay implementation** — the fallback for when punching fails; already-encrypted traffic is relayed, and the rendezvous server is upgraded to hand out relay info. This is what makes cross-Internet chat work reliably, not just when NAT cooperates.
210
+ - **Packaging** — a `pyproject.toml` and a PyPI release, so usage becomes `pip install <name>` followed by `p2pchat listen --name aadarsh` / `p2pchat connect aadarsh`, with no files, ports, or internals exposed to the end user.
211
+ - **Auto-fallback chain** — direct → punch → relay, orchestrated by a connection state machine.
212
+ - **Name pinning** — first registration binds a `peer_id` to a public key, closing the squatting gap.
213
+ - **Hardening** — `MAX_MESSAGE_SIZE`, replay counters, heartbeats/timeouts, and key-file permissions.
214
+
215
+ ---
216
+
217
+ *Stack: Python 3.13, stdlib `socket`/`threading`/`struct`/`sqlite3`/`json`, PyNaCl (libsodium bindings), pytest. No frameworks. ~5 core modules + rendezvous server, 19 automated tests, 15 milestones, ~4 weeks of build time.*
@@ -0,0 +1,17 @@
1
+ README.md
2
+ identity.py
3
+ peer.py
4
+ protocol.py
5
+ pyproject.toml
6
+ relay_server.py
7
+ rendezvous_server.py
8
+ storage.py
9
+ glypha.egg-info/PKG-INFO
10
+ glypha.egg-info/SOURCES.txt
11
+ glypha.egg-info/dependency_links.txt
12
+ glypha.egg-info/entry_points.txt
13
+ glypha.egg-info/requires.txt
14
+ glypha.egg-info/top_level.txt
15
+ tests/test_identity.py
16
+ tests/test_protocol.py
17
+ tests/test_storage.py