fognode 0.2.2__tar.gz → 0.2.4__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.
- fognode-0.2.4/CHANGELOG.md +53 -0
- fognode-0.2.4/PKG-INFO +233 -0
- fognode-0.2.4/README.md +194 -0
- fognode-0.2.4/docs/fognode_connection.svg +108 -0
- fognode-0.2.4/examples/headless_server.py +21 -0
- {fognode-0.2.2 → fognode-0.2.4}/pyproject.toml +1 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/__init__.py +18 -6
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/app.py +87 -98
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/auth/handshake.py +4 -6
- fognode-0.2.4/src/fognode/cipher.py +65 -0
- fognode-0.2.4/src/fognode/core/events.py +56 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/core/server.py +27 -9
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/crypto/__init__.py +0 -4
- fognode-0.2.4/src/fognode/crypto/channel.py +51 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/crypto/primitives.py +2 -19
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/types/__init__.py +0 -2
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/types/constants.py +1 -3
- fognode-0.2.4/src/fognode/utils/ipwords.py +37 -0
- {fognode-0.2.2 → fognode-0.2.4}/tests/test_crypto.py +6 -13
- {fognode-0.2.2 → fognode-0.2.4}/tests/test_ipwords.py +1 -0
- fognode-0.2.2/CHANGELOG.md +0 -27
- fognode-0.2.2/PKG-INFO +0 -150
- fognode-0.2.2/README.md +0 -112
- fognode-0.2.2/examples/headless_server.py +0 -14
- fognode-0.2.2/src/fognode/ciphers/__init__.py +0 -57
- fognode-0.2.2/src/fognode/ciphers/aesgcm.py +0 -5
- fognode-0.2.2/src/fognode/ciphers/blake2.py +0 -14
- fognode-0.2.2/src/fognode/ciphers/blowfish.py +0 -32
- fognode-0.2.2/src/fognode/ciphers/chacha20.py +0 -23
- fognode-0.2.2/src/fognode/ciphers/ed25519.py +0 -23
- fognode-0.2.2/src/fognode/ciphers/fernet.py +0 -23
- fognode-0.2.2/src/fognode/ciphers/hkdf.py +0 -5
- fognode-0.2.2/src/fognode/ciphers/hmac.py +0 -5
- fognode-0.2.2/src/fognode/ciphers/pbkdf2.py +0 -5
- fognode-0.2.2/src/fognode/ciphers/rsa.py +0 -61
- fognode-0.2.2/src/fognode/ciphers/scrypt.py +0 -15
- fognode-0.2.2/src/fognode/ciphers/sha3.py +0 -18
- fognode-0.2.2/src/fognode/ciphers/x25519.py +0 -5
- fognode-0.2.2/src/fognode/crypto/channel.py +0 -82
- fognode-0.2.2/src/fognode/utils/ipwords.py +0 -286
- {fognode-0.2.2 → fognode-0.2.4}/.github/CODEOWNERS +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/.github/PULL_REQUEST_TEMPLATE/bug_fix.md +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/.github/PULL_REQUEST_TEMPLATE/optimization.md +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/.github/dependabot.yml +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/.github/workflows/build.yml +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/.github/workflows/pr-summary.yml +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/.github/workflows/pr.yml +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/.gitignore +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/AGENTS.md +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/CONTRIBUTING.md +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/LICENSE +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/__main__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/auth/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/cli/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/cli/entrypoint.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/core/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/core/client.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/core/probe.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/crypto/cert.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/crypto/kdf.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/crypto/kx.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/crypto/password.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/decorators.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/exceptions.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/filters/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/filters/base.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/filters/command.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/filters/text.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/handlers/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/handlers/handler.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/types/exceptions.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/types/protocol.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/utils/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/utils/net.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/utils/ratelimit.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/wire/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/src/fognode/wire/framing.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/tests/__init__.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/tests/conftest.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/tests/test_framing.py +0 -0
- {fognode-0.2.2 → fognode-0.2.4}/tests/test_ratelimit.py +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on Keep a Changelog,
|
|
6
|
+
and this project adheres to Semantic Versioning.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [dev] - 2026-05-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- add event classes in core/events.py ([`18bef8c`](https://github.com/reekeer/fognode/commit/18bef8c63a3ecd4b8b568960a5d46bd31ed6baa0))
|
|
15
|
+
- add on_event decorator and event-driven Server/Client API ([`bf132b5`](https://github.com/reekeer/fognode/commit/bf132b5ee0085e365ddfd8bd2523d2740292aa6c))
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- trigger release workflow on tag push instead of main branch ([`6e4087c`](https://github.com/reekeer/fognode/commit/6e4087c94e76a5fa359445ef1a4e853f4ba8483a))
|
|
20
|
+
- run release on main/tag push only when HEAD is tagged ([`2d6b8f2`](https://github.com/reekeer/fognode/commit/2d6b8f22b067cdf68a115217b83a37dfd47cc1cc))
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- disable local version suffix for PyPI compatibility ([`c539563`](https://github.com/reekeer/fognode/commit/c539563b076afe33985d82e967cb5b21181a3136))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## [dev] - 2026-05-16
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- remove chat history and user concept from protocol, server, handshake ([`3c601ed`](https://github.com/reekeer/fognode/commit/3c601eded9dcb68a492042efb338b9921b922618))
|
|
33
|
+
- add Client/Server classes, remove logs, builtin commands, chat state ([`99c74ce`](https://github.com/reekeer/fognode/commit/99c74ce89763dde522415b661d968c1c7ac809bc))
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- auto-release on main push using latest tag ([`a0afea7`](https://github.com/reekeer/fognode/commit/a0afea7183baf99281b6cd4679084942574cd646))
|
|
38
|
+
- add pr summary ([`f733c98`](https://github.com/reekeer/fognode/commit/f733c9870b3c1174f9cb8e35b76a5206187dd9bf))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Changelog
|
|
43
|
+
|
|
44
|
+
## 0.1.0
|
|
45
|
+
|
|
46
|
+
- Initial release.
|
|
47
|
+
- TLS 1.2+ with strong cipher suite.
|
|
48
|
+
- X25519 ephemeral key exchange over TLS.
|
|
49
|
+
- AESGCM-256 + HMAC-SHA256 frame encryption.
|
|
50
|
+
- PBKDF2-HMAC-SHA256 password derivation.
|
|
51
|
+
- Monotonic counter + timestamp replay protection.
|
|
52
|
+
- IP-to-word encoding for human-readable addresses.
|
|
53
|
+
- Rate-limited authentication.
|
fognode-0.2.4/PKG-INFO
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fognode
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: fognode - headless secure encrypted data transmission. TLS + X25519 + AESGCM + PBKDF2.
|
|
5
|
+
Project-URL: Repository, https://github.com/reekeer/fognode
|
|
6
|
+
Author: reekeer
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 reekeer
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Requires-Dist: cryptography>=44.0.0
|
|
31
|
+
Requires-Dist: pycryptodome>=3.20.0
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: black>=24.0.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pyright>=1.1.409; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: ruff>=0.5.0; extra == 'dev'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
<h1 align="center">fognode</h1>
|
|
41
|
+
|
|
42
|
+
<h4 align="center">headless secure encrypted data transmission</h4>
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge&logo=opensourceinitiative&logoColor=FFFFFF" alt="License"></a>
|
|
46
|
+
<img src="https://img.shields.io/badge/Python-3.10%2B-blue?style=for-the-badge&logo=python&logoColor=white" alt="Python">
|
|
47
|
+
<img src="https://img.shields.io/badge/Platform-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey?style=for-the-badge&logo=linux&logoColor=FCC624" alt="Platform">
|
|
48
|
+
<img src="https://img.shields.io/badge/code%20style-black-000000?style=for-the-badge" alt="black">
|
|
49
|
+
<img src="https://img.shields.io/badge/linting-ruff-orange?style=for-the-badge" alt="ruff">
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
Peer-to-peer encrypted channel library for headless projects. Built for speed and zero-trust networking.
|
|
55
|
+
|
|
56
|
+
## Security Stack
|
|
57
|
+
|
|
58
|
+
| Layer | Technology |
|
|
59
|
+
|---|---|
|
|
60
|
+
| Transport | TLS 1.2+ with self-signed RSA-4096 certs |
|
|
61
|
+
| Key Exchange | X25519 ephemeral ECDH |
|
|
62
|
+
| Session Keys | HKDF-SHA256 |
|
|
63
|
+
| Channel Cipher | mini-MTProto (AES-256-IGE + SHA-256 msg_key) |
|
|
64
|
+
| Password Derivation | PBKDF2-HMAC-SHA256 (390k iterations) |
|
|
65
|
+
| Auth Challenge | HMAC-SHA256 over random nonce |
|
|
66
|
+
| Cert Pinning | SHA-256 fingerprint verification |
|
|
67
|
+
|
|
68
|
+
## How connection works
|
|
69
|
+
|
|
70
|
+
<p align="center">
|
|
71
|
+
<img src="./docs/fognode_connection.svg" alt="fognode connection flow" width="900"/>
|
|
72
|
+
</p>
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install fognode
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Quick Start
|
|
81
|
+
|
|
82
|
+
### Server
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from fognode import Server, MessageEvent, ConnectEvent, DisconnectEvent
|
|
86
|
+
|
|
87
|
+
server = Server(host="0.0.0.0", port=9443, password="secret")
|
|
88
|
+
|
|
89
|
+
@server.on_event(MessageEvent)
|
|
90
|
+
async def on_message(ctx):
|
|
91
|
+
if ctx.event.text:
|
|
92
|
+
await ctx.answer(f"echo: {ctx.event.text}")
|
|
93
|
+
|
|
94
|
+
@server.on_event(ConnectEvent)
|
|
95
|
+
async def on_connect(ctx):
|
|
96
|
+
print("+ peer connected")
|
|
97
|
+
|
|
98
|
+
@server.on_event(DisconnectEvent)
|
|
99
|
+
async def on_disconnect(ctx):
|
|
100
|
+
print("- peer disconnected")
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
server.run()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Client
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from fognode import Client, MessageEvent, ClosedEvent, ErrorEvent
|
|
110
|
+
|
|
111
|
+
client = Client(connect_string="oak-pine-stone-field:9443", password="secret")
|
|
112
|
+
|
|
113
|
+
@client.on_event(MessageEvent)
|
|
114
|
+
async def on_message(ctx):
|
|
115
|
+
print(f"msg: {ctx.event.text}")
|
|
116
|
+
|
|
117
|
+
@client.on_event(ClosedEvent)
|
|
118
|
+
async def on_closed(ctx):
|
|
119
|
+
print("connection closed")
|
|
120
|
+
|
|
121
|
+
@client.on_event(ErrorEvent)
|
|
122
|
+
async def on_error(ctx):
|
|
123
|
+
print(f"error: {ctx.event.exception}")
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
client.connect()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Classic API
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from fognode import start_server, client_connect
|
|
133
|
+
|
|
134
|
+
ip, code, fp = start_server("0.0.0.0", 9443, "secret")
|
|
135
|
+
print(f"Server running at {code}:9443")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Events
|
|
139
|
+
|
|
140
|
+
| Event | Server | Client | Description |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| `StartEvent` | yes | yes | Server/client started |
|
|
143
|
+
| `ConnectEvent` | yes | yes | Peer connected |
|
|
144
|
+
| `DisconnectEvent` | yes | yes | Peer disconnected |
|
|
145
|
+
| `MessageEvent` | yes | yes | Message received (`type`, `text`, `ts`) |
|
|
146
|
+
| `ClosedEvent` | no | yes | Connection closed |
|
|
147
|
+
| `ErrorEvent` | no | yes | Error occurred (`exception`) |
|
|
148
|
+
|
|
149
|
+
### Decorators
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
# Specific event
|
|
153
|
+
@server.on_event(MessageEvent)
|
|
154
|
+
async def handler(ctx):
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
# Aliases
|
|
158
|
+
@server.on_connect()
|
|
159
|
+
@server.on_disconnect()
|
|
160
|
+
@server.on_message()
|
|
161
|
+
@client.on_closed()
|
|
162
|
+
@client.on_error()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## How mini-MTProto Works
|
|
166
|
+
|
|
167
|
+
Each message is encrypted independently using a simplified MTProto 2.0 scheme:
|
|
168
|
+
|
|
169
|
+
1. **Serialize** JSON payload to bytes
|
|
170
|
+
2. **Pad** to AES block size (16 bytes)
|
|
171
|
+
3. **Compute msg_key** = `SHA-256(padded)[8:24]` (128 bits)
|
|
172
|
+
4. **Derive AES key** = `SHA-256(auth_key + msg_key)`
|
|
173
|
+
5. **Derive AES IV** = `SHA-256(msg_key + auth_key)`
|
|
174
|
+
6. **Encrypt** with AES-256-IGE
|
|
175
|
+
7. **Send** `length(4 bytes) + msg_key(16 bytes) + ciphertext`
|
|
176
|
+
|
|
177
|
+
Decryption reverses the process and verifies `msg_key` integrity.
|
|
178
|
+
|
|
179
|
+
## CLI
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Run server
|
|
183
|
+
fognode server --host 0.0.0.0 --port 9443 --password secret
|
|
184
|
+
|
|
185
|
+
# Interactive client
|
|
186
|
+
fognode client oak-pine-stone-field:9443 --password secret
|
|
187
|
+
|
|
188
|
+
# Probe TLS fingerprint (no auth)
|
|
189
|
+
fognode probe oak-pine-stone-field:9443
|
|
190
|
+
|
|
191
|
+
# Server status via authenticated channel
|
|
192
|
+
fognode status oak-pine-stone-field:9443 --password secret
|
|
193
|
+
|
|
194
|
+
# Send one message and exit
|
|
195
|
+
fognode send oak-pine-stone-field:9443 --password secret --text "hello"
|
|
196
|
+
|
|
197
|
+
# Monitor messages only
|
|
198
|
+
fognode monitor oak-pine-stone-field:9443 --password secret
|
|
199
|
+
|
|
200
|
+
# Show certificate info
|
|
201
|
+
fognode cert --file fognode_cert.pem
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Project Structure
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
src/fognode/
|
|
208
|
+
├── app.py # Server, Client, Context
|
|
209
|
+
├── cipher.py # mini-MTProto AES-256-IGE encrypt/decrypt
|
|
210
|
+
├── cli/
|
|
211
|
+
│ └── entrypoint.py # argparse CLI
|
|
212
|
+
├── core/
|
|
213
|
+
│ ├── client.py # client_connect()
|
|
214
|
+
│ ├── events.py # Event classes
|
|
215
|
+
│ ├── probe.py # probe_server()
|
|
216
|
+
│ └── server.py # start_server()
|
|
217
|
+
├── crypto/
|
|
218
|
+
│ ├── cert.py # Self-signed cert generation
|
|
219
|
+
│ ├── channel.py # SecureChannel (mini-MTProto)
|
|
220
|
+
│ ├── kdf.py # Session key helpers
|
|
221
|
+
│ ├── kx.py # X25519 keypair + shared secret
|
|
222
|
+
│ ├── password.py # PBKDF2 password store
|
|
223
|
+
│ └── primitives.py # pbkdf2, hmac256, hkdf_expand
|
|
224
|
+
├── filters/ # BaseFilter, Command, Text
|
|
225
|
+
├── handlers/ # HandlerObject
|
|
226
|
+
├── types/ # constants, exceptions, protocol
|
|
227
|
+
├── utils/ # ipwords, ratelimit, net
|
|
228
|
+
└── wire/ # Length-prefixed JSON framing
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
<p align="center"><sub><a href="LICENSE">MIT</a> © reekeer</sub></p>
|
fognode-0.2.4/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
<h1 align="center">fognode</h1>
|
|
2
|
+
|
|
3
|
+
<h4 align="center">headless secure encrypted data transmission</h4>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge&logo=opensourceinitiative&logoColor=FFFFFF" alt="License"></a>
|
|
7
|
+
<img src="https://img.shields.io/badge/Python-3.10%2B-blue?style=for-the-badge&logo=python&logoColor=white" alt="Python">
|
|
8
|
+
<img src="https://img.shields.io/badge/Platform-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey?style=for-the-badge&logo=linux&logoColor=FCC624" alt="Platform">
|
|
9
|
+
<img src="https://img.shields.io/badge/code%20style-black-000000?style=for-the-badge" alt="black">
|
|
10
|
+
<img src="https://img.shields.io/badge/linting-ruff-orange?style=for-the-badge" alt="ruff">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
Peer-to-peer encrypted channel library for headless projects. Built for speed and zero-trust networking.
|
|
16
|
+
|
|
17
|
+
## Security Stack
|
|
18
|
+
|
|
19
|
+
| Layer | Technology |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Transport | TLS 1.2+ with self-signed RSA-4096 certs |
|
|
22
|
+
| Key Exchange | X25519 ephemeral ECDH |
|
|
23
|
+
| Session Keys | HKDF-SHA256 |
|
|
24
|
+
| Channel Cipher | mini-MTProto (AES-256-IGE + SHA-256 msg_key) |
|
|
25
|
+
| Password Derivation | PBKDF2-HMAC-SHA256 (390k iterations) |
|
|
26
|
+
| Auth Challenge | HMAC-SHA256 over random nonce |
|
|
27
|
+
| Cert Pinning | SHA-256 fingerprint verification |
|
|
28
|
+
|
|
29
|
+
## How connection works
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<img src="./docs/fognode_connection.svg" alt="fognode connection flow" width="900"/>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install fognode
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
### Server
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from fognode import Server, MessageEvent, ConnectEvent, DisconnectEvent
|
|
47
|
+
|
|
48
|
+
server = Server(host="0.0.0.0", port=9443, password="secret")
|
|
49
|
+
|
|
50
|
+
@server.on_event(MessageEvent)
|
|
51
|
+
async def on_message(ctx):
|
|
52
|
+
if ctx.event.text:
|
|
53
|
+
await ctx.answer(f"echo: {ctx.event.text}")
|
|
54
|
+
|
|
55
|
+
@server.on_event(ConnectEvent)
|
|
56
|
+
async def on_connect(ctx):
|
|
57
|
+
print("+ peer connected")
|
|
58
|
+
|
|
59
|
+
@server.on_event(DisconnectEvent)
|
|
60
|
+
async def on_disconnect(ctx):
|
|
61
|
+
print("- peer disconnected")
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
server.run()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Client
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from fognode import Client, MessageEvent, ClosedEvent, ErrorEvent
|
|
71
|
+
|
|
72
|
+
client = Client(connect_string="oak-pine-stone-field:9443", password="secret")
|
|
73
|
+
|
|
74
|
+
@client.on_event(MessageEvent)
|
|
75
|
+
async def on_message(ctx):
|
|
76
|
+
print(f"msg: {ctx.event.text}")
|
|
77
|
+
|
|
78
|
+
@client.on_event(ClosedEvent)
|
|
79
|
+
async def on_closed(ctx):
|
|
80
|
+
print("connection closed")
|
|
81
|
+
|
|
82
|
+
@client.on_event(ErrorEvent)
|
|
83
|
+
async def on_error(ctx):
|
|
84
|
+
print(f"error: {ctx.event.exception}")
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
client.connect()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Classic API
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from fognode import start_server, client_connect
|
|
94
|
+
|
|
95
|
+
ip, code, fp = start_server("0.0.0.0", 9443, "secret")
|
|
96
|
+
print(f"Server running at {code}:9443")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Events
|
|
100
|
+
|
|
101
|
+
| Event | Server | Client | Description |
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| `StartEvent` | yes | yes | Server/client started |
|
|
104
|
+
| `ConnectEvent` | yes | yes | Peer connected |
|
|
105
|
+
| `DisconnectEvent` | yes | yes | Peer disconnected |
|
|
106
|
+
| `MessageEvent` | yes | yes | Message received (`type`, `text`, `ts`) |
|
|
107
|
+
| `ClosedEvent` | no | yes | Connection closed |
|
|
108
|
+
| `ErrorEvent` | no | yes | Error occurred (`exception`) |
|
|
109
|
+
|
|
110
|
+
### Decorators
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# Specific event
|
|
114
|
+
@server.on_event(MessageEvent)
|
|
115
|
+
async def handler(ctx):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# Aliases
|
|
119
|
+
@server.on_connect()
|
|
120
|
+
@server.on_disconnect()
|
|
121
|
+
@server.on_message()
|
|
122
|
+
@client.on_closed()
|
|
123
|
+
@client.on_error()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## How mini-MTProto Works
|
|
127
|
+
|
|
128
|
+
Each message is encrypted independently using a simplified MTProto 2.0 scheme:
|
|
129
|
+
|
|
130
|
+
1. **Serialize** JSON payload to bytes
|
|
131
|
+
2. **Pad** to AES block size (16 bytes)
|
|
132
|
+
3. **Compute msg_key** = `SHA-256(padded)[8:24]` (128 bits)
|
|
133
|
+
4. **Derive AES key** = `SHA-256(auth_key + msg_key)`
|
|
134
|
+
5. **Derive AES IV** = `SHA-256(msg_key + auth_key)`
|
|
135
|
+
6. **Encrypt** with AES-256-IGE
|
|
136
|
+
7. **Send** `length(4 bytes) + msg_key(16 bytes) + ciphertext`
|
|
137
|
+
|
|
138
|
+
Decryption reverses the process and verifies `msg_key` integrity.
|
|
139
|
+
|
|
140
|
+
## CLI
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Run server
|
|
144
|
+
fognode server --host 0.0.0.0 --port 9443 --password secret
|
|
145
|
+
|
|
146
|
+
# Interactive client
|
|
147
|
+
fognode client oak-pine-stone-field:9443 --password secret
|
|
148
|
+
|
|
149
|
+
# Probe TLS fingerprint (no auth)
|
|
150
|
+
fognode probe oak-pine-stone-field:9443
|
|
151
|
+
|
|
152
|
+
# Server status via authenticated channel
|
|
153
|
+
fognode status oak-pine-stone-field:9443 --password secret
|
|
154
|
+
|
|
155
|
+
# Send one message and exit
|
|
156
|
+
fognode send oak-pine-stone-field:9443 --password secret --text "hello"
|
|
157
|
+
|
|
158
|
+
# Monitor messages only
|
|
159
|
+
fognode monitor oak-pine-stone-field:9443 --password secret
|
|
160
|
+
|
|
161
|
+
# Show certificate info
|
|
162
|
+
fognode cert --file fognode_cert.pem
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Project Structure
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
src/fognode/
|
|
169
|
+
├── app.py # Server, Client, Context
|
|
170
|
+
├── cipher.py # mini-MTProto AES-256-IGE encrypt/decrypt
|
|
171
|
+
├── cli/
|
|
172
|
+
│ └── entrypoint.py # argparse CLI
|
|
173
|
+
├── core/
|
|
174
|
+
│ ├── client.py # client_connect()
|
|
175
|
+
│ ├── events.py # Event classes
|
|
176
|
+
│ ├── probe.py # probe_server()
|
|
177
|
+
│ └── server.py # start_server()
|
|
178
|
+
├── crypto/
|
|
179
|
+
│ ├── cert.py # Self-signed cert generation
|
|
180
|
+
│ ├── channel.py # SecureChannel (mini-MTProto)
|
|
181
|
+
│ ├── kdf.py # Session key helpers
|
|
182
|
+
│ ├── kx.py # X25519 keypair + shared secret
|
|
183
|
+
│ ├── password.py # PBKDF2 password store
|
|
184
|
+
│ └── primitives.py # pbkdf2, hmac256, hkdf_expand
|
|
185
|
+
├── filters/ # BaseFilter, Command, Text
|
|
186
|
+
├── handlers/ # HandlerObject
|
|
187
|
+
├── types/ # constants, exceptions, protocol
|
|
188
|
+
├── utils/ # ipwords, ratelimit, net
|
|
189
|
+
└── wire/ # Length-prefixed JSON framing
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
<p align="center"><sub><a href="LICENSE">MIT</a> © reekeer</sub></p>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<svg width="900" height="540" viewBox="0 0 900 540" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<marker id="a" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
|
4
|
+
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
</marker>
|
|
6
|
+
<style>
|
|
7
|
+
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
|
|
8
|
+
.lbl { font-size: 13px; fill: #24292f; }
|
|
9
|
+
.sec { font-size: 12px; fill: #8c959f; }
|
|
10
|
+
.hdr { font-size: 15px; font-weight: 600; fill: #24292f; }
|
|
11
|
+
.note { font-size: 12px; fill: #57606a; }
|
|
12
|
+
@media (prefers-color-scheme: dark) {
|
|
13
|
+
.lbl { fill: #e6edf3; }
|
|
14
|
+
.hdr { fill: #f0f6fc; }
|
|
15
|
+
.note { fill: #8b949e; }
|
|
16
|
+
.sec { fill: #6e7681; }
|
|
17
|
+
.box { fill: #21262d !important; stroke: #30363d !important; }
|
|
18
|
+
.pill { fill: #161b22 !important; stroke: #30363d !important; }
|
|
19
|
+
.pgray{ fill: #21262d !important; stroke: #30363d !important; }
|
|
20
|
+
.life { stroke: #30363d !important; }
|
|
21
|
+
.div { stroke: #21262d !important; }
|
|
22
|
+
.arr { stroke: #8b949e !important; }
|
|
23
|
+
.arrb { stroke: #e6edf3 !important; }
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
26
|
+
</defs>
|
|
27
|
+
|
|
28
|
+
<!-- center=450, pill w=360, x=270, x2=630 -->
|
|
29
|
+
<!-- server lifeline x=100, client lifeline x=800 -->
|
|
30
|
+
|
|
31
|
+
<rect class="box" x="40" y="28" width="120" height="40" rx="8" fill="#f6f8fa" stroke="#d0d7de" stroke-width="1"/>
|
|
32
|
+
<text class="hdr" x="100" y="53" text-anchor="middle">Server</text>
|
|
33
|
+
|
|
34
|
+
<rect class="box" x="740" y="28" width="120" height="40" rx="8" fill="#f6f8fa" stroke="#d0d7de" stroke-width="1"/>
|
|
35
|
+
<text class="hdr" x="800" y="53" text-anchor="middle">Client</text>
|
|
36
|
+
|
|
37
|
+
<line class="life" x1="100" y1="68" x2="100" y2="510" stroke="#d0d7de" stroke-width="1" stroke-dasharray="5 4"/>
|
|
38
|
+
<line class="life" x1="800" y1="68" x2="800" y2="510" stroke="#d0d7de" stroke-width="1" stroke-dasharray="5 4"/>
|
|
39
|
+
|
|
40
|
+
<!-- TLS -->
|
|
41
|
+
<text class="sec" x="450" y="96" text-anchor="middle">TLS</text>
|
|
42
|
+
<line class="div" x1="102" y1="92" x2="428" y2="92" stroke="#e0e0e0" stroke-width="0.5"/>
|
|
43
|
+
<line class="div" x1="472" y1="92" x2="798" y2="92" stroke="#e0e0e0" stroke-width="0.5"/>
|
|
44
|
+
|
|
45
|
+
<line class="arr" x1="798" y1="116" x2="632" y2="116" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
46
|
+
<rect class="pill" x="270" y="105" width="360" height="24" rx="5" fill="#fff" stroke="#d0d7de" stroke-width="0.5"/>
|
|
47
|
+
<line class="arr" x1="268" y1="116" x2="102" y2="116" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
48
|
+
<text class="lbl" x="450" y="121" text-anchor="middle">TCP connect</text>
|
|
49
|
+
|
|
50
|
+
<line class="arr" x1="102" y1="152" x2="268" y2="152" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
51
|
+
<rect class="pill" x="270" y="141" width="360" height="24" rx="5" fill="#fff" stroke="#d0d7de" stroke-width="0.5"/>
|
|
52
|
+
<line class="arr" x1="630" y1="152" x2="798" y2="152" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
53
|
+
<text class="lbl" x="450" y="157" text-anchor="middle">TLS 1.2+ handshake — RSA-4096 self-signed cert</text>
|
|
54
|
+
|
|
55
|
+
<line class="arr" x1="798" y1="188" x2="632" y2="188" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
56
|
+
<rect class="pill" x="270" y="177" width="360" height="24" rx="5" fill="#fff" stroke="#d0d7de" stroke-width="0.5"/>
|
|
57
|
+
<line class="arr" x1="268" y1="188" x2="102" y2="188" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
58
|
+
<text class="lbl" x="450" y="193" text-anchor="middle">SHA-256 cert fingerprint verify</text>
|
|
59
|
+
|
|
60
|
+
<!-- Key exchange -->
|
|
61
|
+
<text class="sec" x="450" y="220" text-anchor="middle">Key exchange</text>
|
|
62
|
+
<line class="div" x1="102" y1="216" x2="390" y2="216" stroke="#e0e0e0" stroke-width="0.5"/>
|
|
63
|
+
<line class="div" x1="510" y1="216" x2="798" y2="216" stroke="#e0e0e0" stroke-width="0.5"/>
|
|
64
|
+
|
|
65
|
+
<line class="arr" x1="798" y1="240" x2="632" y2="240" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
66
|
+
<rect class="pill" x="270" y="229" width="360" height="24" rx="5" fill="#fff" stroke="#d0d7de" stroke-width="0.5"/>
|
|
67
|
+
<line class="arr" x1="268" y1="240" x2="102" y2="240" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
68
|
+
<text class="lbl" x="450" y="245" text-anchor="middle">X25519 ephemeral public key</text>
|
|
69
|
+
|
|
70
|
+
<line class="arr" x1="102" y1="276" x2="268" y2="276" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
71
|
+
<rect class="pill" x="270" y="265" width="360" height="24" rx="5" fill="#fff" stroke="#d0d7de" stroke-width="0.5"/>
|
|
72
|
+
<line class="arr" x1="630" y1="276" x2="798" y2="276" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
73
|
+
<text class="lbl" x="450" y="281" text-anchor="middle">X25519 ephemeral public key</text>
|
|
74
|
+
|
|
75
|
+
<rect class="pgray" x="270" y="296" width="360" height="24" rx="5" fill="#f6f8fa" stroke="#ccc" stroke-width="0.5"/>
|
|
76
|
+
<text class="note" x="450" y="312" text-anchor="middle">both sides derive: ECDH → HKDF-SHA256 → session key</text>
|
|
77
|
+
|
|
78
|
+
<!-- Auth -->
|
|
79
|
+
<text class="sec" x="450" y="344" text-anchor="middle">Auth</text>
|
|
80
|
+
<line class="div" x1="102" y1="340" x2="424" y2="340" stroke="#e0e0e0" stroke-width="0.5"/>
|
|
81
|
+
<line class="div" x1="476" y1="340" x2="798" y2="340" stroke="#e0e0e0" stroke-width="0.5"/>
|
|
82
|
+
|
|
83
|
+
<line class="arr" x1="102" y1="364" x2="268" y2="364" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
84
|
+
<rect class="pill" x="270" y="353" width="360" height="24" rx="5" fill="#fff" stroke="#d0d7de" stroke-width="0.5"/>
|
|
85
|
+
<line class="arr" x1="630" y1="364" x2="798" y2="364" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
86
|
+
<text class="lbl" x="450" y="369" text-anchor="middle">random nonce</text>
|
|
87
|
+
|
|
88
|
+
<line class="arr" x1="798" y1="400" x2="632" y2="400" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
89
|
+
<rect class="pill" x="270" y="389" width="360" height="24" rx="5" fill="#fff" stroke="#d0d7de" stroke-width="0.5"/>
|
|
90
|
+
<line class="arr" x1="268" y1="400" x2="102" y2="400" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
91
|
+
<text class="lbl" x="450" y="405" text-anchor="middle">HMAC-SHA256(nonce, PBKDF2 password)</text>
|
|
92
|
+
|
|
93
|
+
<line class="arr" x1="102" y1="436" x2="268" y2="436" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
94
|
+
<rect class="pill" x="270" y="425" width="360" height="24" rx="5" fill="#fff" stroke="#d0d7de" stroke-width="0.5"/>
|
|
95
|
+
<line class="arr" x1="630" y1="436" x2="798" y2="436" stroke="#555" stroke-width="1.2" marker-end="url(#a)"/>
|
|
96
|
+
<text class="lbl" x="450" y="441" text-anchor="middle">auth OK / reject</text>
|
|
97
|
+
|
|
98
|
+
<!-- Messaging -->
|
|
99
|
+
<text class="sec" x="450" y="466" text-anchor="middle">Messaging</text>
|
|
100
|
+
<line class="div" x1="102" y1="462" x2="396" y2="462" stroke="#e0e0e0" stroke-width="0.5"/>
|
|
101
|
+
<line class="div" x1="504" y1="462" x2="798" y2="462" stroke="#e0e0e0" stroke-width="0.5"/>
|
|
102
|
+
|
|
103
|
+
<line class="arrb" x1="798" y1="490" x2="632" y2="490" stroke="#24292f" stroke-width="1.5" marker-end="url(#a)"/>
|
|
104
|
+
<rect class="pill" x="270" y="479" width="360" height="24" rx="5" fill="#fff" stroke="#8c959f" stroke-width="0.8"/>
|
|
105
|
+
<line class="arrb" x1="268" y1="490" x2="102" y2="490" stroke="#24292f" stroke-width="1.5" marker-end="url(#a)"/>
|
|
106
|
+
<text class="lbl" x="450" y="495" text-anchor="middle" style="font-weight:600">mini-MTProto · AES-256-IGE · ↻ until disconnect</text>
|
|
107
|
+
|
|
108
|
+
</svg>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fognode import Server, ConnectEvent, DisconnectEvent, MessageEvent
|
|
4
|
+
|
|
5
|
+
server = Server(host="0.0.0.0", port=9443, password="secret")
|
|
6
|
+
|
|
7
|
+
@server.on_event(ConnectEvent)
|
|
8
|
+
async def on_connect(ctx):
|
|
9
|
+
print("+ peer connected")
|
|
10
|
+
|
|
11
|
+
@server.on_event(DisconnectEvent)
|
|
12
|
+
async def on_disconnect(ctx):
|
|
13
|
+
print("- peer disconnected")
|
|
14
|
+
|
|
15
|
+
@server.on_event(MessageEvent)
|
|
16
|
+
async def on_message(ctx):
|
|
17
|
+
if ctx.event.text:
|
|
18
|
+
await ctx.answer(f"echo: {ctx.event.text}")
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
server.run()
|