fognode 0.1.0__py3-none-any.whl

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.
Files changed (56) hide show
  1. fognode/__init__.py +120 -0
  2. fognode/__main__.py +5 -0
  3. fognode/app.py +270 -0
  4. fognode/auth/__init__.py +5 -0
  5. fognode/auth/handshake.py +107 -0
  6. fognode/ciphers/__init__.py +57 -0
  7. fognode/ciphers/aesgcm.py +5 -0
  8. fognode/ciphers/blake2.py +14 -0
  9. fognode/ciphers/blowfish.py +32 -0
  10. fognode/ciphers/chacha20.py +23 -0
  11. fognode/ciphers/ed25519.py +23 -0
  12. fognode/ciphers/fernet.py +23 -0
  13. fognode/ciphers/hkdf.py +5 -0
  14. fognode/ciphers/hmac.py +5 -0
  15. fognode/ciphers/pbkdf2.py +5 -0
  16. fognode/ciphers/rsa.py +61 -0
  17. fognode/ciphers/scrypt.py +15 -0
  18. fognode/ciphers/sha3.py +18 -0
  19. fognode/ciphers/x25519.py +5 -0
  20. fognode/cli/__init__.py +5 -0
  21. fognode/cli/entrypoint.py +398 -0
  22. fognode/core/__init__.py +8 -0
  23. fognode/core/client.py +54 -0
  24. fognode/core/probe.py +42 -0
  25. fognode/core/server.py +182 -0
  26. fognode/core/state.py +66 -0
  27. fognode/crypto/__init__.py +43 -0
  28. fognode/crypto/cert.py +159 -0
  29. fognode/crypto/channel.py +82 -0
  30. fognode/crypto/kdf.py +7 -0
  31. fognode/crypto/kx.py +15 -0
  32. fognode/crypto/password.py +21 -0
  33. fognode/crypto/primitives.py +63 -0
  34. fognode/decorators.py +66 -0
  35. fognode/exceptions.py +12 -0
  36. fognode/filters/__init__.py +7 -0
  37. fognode/filters/base.py +8 -0
  38. fognode/filters/command.py +15 -0
  39. fognode/filters/text.py +11 -0
  40. fognode/handlers/__init__.py +5 -0
  41. fognode/handlers/handler.py +36 -0
  42. fognode/types/__init__.py +105 -0
  43. fognode/types/constants.py +25 -0
  44. fognode/types/exceptions.py +25 -0
  45. fognode/types/protocol.py +128 -0
  46. fognode/utils/__init__.py +7 -0
  47. fognode/utils/ipwords.py +286 -0
  48. fognode/utils/net.py +12 -0
  49. fognode/utils/ratelimit.py +43 -0
  50. fognode/wire/__init__.py +5 -0
  51. fognode/wire/framing.py +33 -0
  52. fognode-0.1.0.dist-info/METADATA +150 -0
  53. fognode-0.1.0.dist-info/RECORD +56 -0
  54. fognode-0.1.0.dist-info/WHEEL +4 -0
  55. fognode-0.1.0.dist-info/entry_points.txt +2 -0
  56. fognode-0.1.0.dist-info/licenses/LICENSE +21 -0
fognode/__init__.py ADDED
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from fognode import ciphers, decorators, exceptions, filters, types
4
+ from fognode.app import App, Context, Message, Router
5
+ from fognode.core.client import client_connect
6
+ from fognode.core.server import start_server
7
+ from fognode.core.state import ChatState
8
+ from fognode.crypto.channel import SecureChannel
9
+ from fognode.types import (
10
+ AESGCM_NONCE_LENGTH,
11
+ CERT_VALIDITY_DAYS,
12
+ CHAT_HISTORY_LIMIT,
13
+ CHAT_MAX_TEXT_LENGTH,
14
+ DEFAULT_HOST,
15
+ DEFAULT_PORT,
16
+ MAX_MESSAGE_SIZE,
17
+ NONCE_LENGTH,
18
+ PBKDF2_ITERATIONS,
19
+ RATE_LIMIT_MAX_ATTEMPTS,
20
+ RATE_LIMIT_WINDOW,
21
+ REPLAY_WINDOW,
22
+ RSA_KEY_SIZE,
23
+ SALT_LENGTH,
24
+ TOKEN_LENGTH,
25
+ AuthError,
26
+ AuthResult,
27
+ ByeMsg,
28
+ Bytes32,
29
+ CertError,
30
+ ChallengeMsg,
31
+ ChatMsg,
32
+ ChatMsgDict,
33
+ Cipher,
34
+ CmdMsg,
35
+ CodeName,
36
+ ConnectionInfo,
37
+ ConnectString,
38
+ Fingerprint,
39
+ FrameError,
40
+ HandshakeError,
41
+ HistoryMsg,
42
+ InfoMsg,
43
+ IPAddress,
44
+ MessageHandler,
45
+ OnConnect,
46
+ OnDisconnect,
47
+ OnMessage,
48
+ PongMsg,
49
+ Port,
50
+ ResponseMsg,
51
+ SecurityError,
52
+ ServerInfo,
53
+ User,
54
+ UsersMsg,
55
+ WelcomeMsg,
56
+ WireError,
57
+ )
58
+
59
+ __all__ = [
60
+ "AESGCM_NONCE_LENGTH",
61
+ "App",
62
+ "AuthError",
63
+ "AuthResult",
64
+ "Bytes32",
65
+ "ByeMsg",
66
+ "CERT_VALIDITY_DAYS",
67
+ "CHAT_HISTORY_LIMIT",
68
+ "CHAT_MAX_TEXT_LENGTH",
69
+ "CertError",
70
+ "ChallengeMsg",
71
+ "ChatMsg",
72
+ "ChatMsgDict",
73
+ "ChatState",
74
+ "Cipher",
75
+ "CmdMsg",
76
+ "CodeName",
77
+ "ConnectString",
78
+ "ConnectionInfo",
79
+ "Context",
80
+ "DEFAULT_HOST",
81
+ "DEFAULT_PORT",
82
+ "Fingerprint",
83
+ "FrameError",
84
+ "HandshakeError",
85
+ "HistoryMsg",
86
+ "InfoMsg",
87
+ "IPAddress",
88
+ "MAX_MESSAGE_SIZE",
89
+ "Message",
90
+ "MessageHandler",
91
+ "NONCE_LENGTH",
92
+ "OnConnect",
93
+ "OnDisconnect",
94
+ "OnMessage",
95
+ "PBKDF2_ITERATIONS",
96
+ "PongMsg",
97
+ "Port",
98
+ "RATE_LIMIT_MAX_ATTEMPTS",
99
+ "RATE_LIMIT_WINDOW",
100
+ "REPLAY_WINDOW",
101
+ "RSA_KEY_SIZE",
102
+ "ResponseMsg",
103
+ "Router",
104
+ "SALT_LENGTH",
105
+ "SecureChannel",
106
+ "SecurityError",
107
+ "ServerInfo",
108
+ "TOKEN_LENGTH",
109
+ "User",
110
+ "UsersMsg",
111
+ "WelcomeMsg",
112
+ "WireError",
113
+ "ciphers",
114
+ "client_connect",
115
+ "decorators",
116
+ "exceptions",
117
+ "filters",
118
+ "start_server",
119
+ "types",
120
+ ]
fognode/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from fognode.cli.entrypoint import main
4
+
5
+ main()
fognode/app.py ADDED
@@ -0,0 +1,270 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import threading
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Any, Callable
7
+
8
+ from fognode.core.client import client_connect
9
+ from fognode.core.server import _state, start_server
10
+ from fognode.crypto.channel import SecureChannel
11
+ from fognode.filters import Command
12
+ from fognode.handlers import HandlerObject
13
+ from fognode.types.constants import DEFAULT_HOST, DEFAULT_PORT, Cipher
14
+ from fognode.types.protocol import ChatMsg
15
+
16
+ if TYPE_CHECKING:
17
+ from fognode.app import App
18
+
19
+
20
+ @dataclass(slots=True, frozen=True)
21
+ class Message:
22
+ type: str
23
+ user: str
24
+ text: str
25
+ ts: float
26
+ raw: dict[str, Any]
27
+
28
+ @classmethod
29
+ def from_dict(cls, data: dict[str, Any]) -> Message:
30
+ return cls(
31
+ type=data.get("type", ""),
32
+ user=data.get("user", ""),
33
+ text=data.get("text", ""),
34
+ ts=data.get("ts", 0.0),
35
+ raw=data,
36
+ )
37
+
38
+
39
+ @dataclass(slots=True)
40
+ class Context:
41
+ app: App
42
+ channel: SecureChannel | None
43
+ user: str
44
+ message: Message | None = None
45
+
46
+ async def answer(self, text: str) -> None:
47
+ if self.channel is None:
48
+ raise RuntimeError("no channel available")
49
+ self.channel.send({"type": "chat", "text": text})
50
+
51
+ async def send(self, data: dict[str, Any]) -> None:
52
+ if self.channel is None:
53
+ raise RuntimeError("no channel available")
54
+ self.channel.send(data)
55
+
56
+ async def reply(self, data: dict[str, Any]) -> None:
57
+ if self.channel is None:
58
+ raise RuntimeError("no channel available")
59
+ self.channel.send(data)
60
+
61
+
62
+ class Router:
63
+ def __init__(self) -> None:
64
+ self._handlers: dict[str, list[HandlerObject]] = {
65
+ "connect": [],
66
+ "disconnect": [],
67
+ "message": [],
68
+ }
69
+
70
+ def on_connect(self) -> Callable[..., Any]:
71
+ def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
72
+ self._handlers["connect"].append(HandlerObject(callback))
73
+ return callback
74
+
75
+ return decorator
76
+
77
+ def on_disconnect(self) -> Callable[..., Any]:
78
+ def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
79
+ self._handlers["disconnect"].append(HandlerObject(callback))
80
+ return callback
81
+
82
+ return decorator
83
+
84
+ def on_message(self, *filters: Any) -> Callable[..., Any]:
85
+ def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
86
+ self._handlers["message"].append(HandlerObject(callback, list(filters)))
87
+ return callback
88
+
89
+ return decorator
90
+
91
+ def on_command(self, commands: str | list[str]) -> Callable[..., Any]:
92
+ def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
93
+ self._handlers["message"].append(HandlerObject(callback, [Command(commands)]))
94
+ return callback
95
+
96
+ return decorator
97
+
98
+ def include(self, app: App) -> None:
99
+ for event, handlers in self._handlers.items():
100
+ app._handlers[event].extend(handlers)
101
+
102
+
103
+ class App:
104
+ def __init__(
105
+ self,
106
+ host: str = DEFAULT_HOST,
107
+ port: int = DEFAULT_PORT,
108
+ user: str | None = None,
109
+ password: str | None = None,
110
+ cipher: Cipher = Cipher.AESGCM,
111
+ mode: str = "server",
112
+ connect_string: str | None = None,
113
+ ) -> None:
114
+ self.host = host
115
+ self.port = port
116
+ self.user = user
117
+ self.password = password
118
+ self.cipher = cipher
119
+ self.mode = mode
120
+ self.connect_string = connect_string
121
+ self._handlers: dict[str, list[HandlerObject]] = {
122
+ "connect": [],
123
+ "disconnect": [],
124
+ "message": [],
125
+ }
126
+ self._loop: asyncio.AbstractEventLoop | None = None
127
+ self._channel: SecureChannel | None = None
128
+
129
+ @classmethod
130
+ def client(
131
+ cls,
132
+ connect_string: str,
133
+ password: str,
134
+ cipher: Cipher = Cipher.AESGCM,
135
+ ) -> App:
136
+ return cls(
137
+ mode="client",
138
+ connect_string=connect_string,
139
+ password=password,
140
+ cipher=cipher,
141
+ )
142
+
143
+ def on_connect(self) -> Callable[..., Any]:
144
+ def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
145
+ self._handlers["connect"].append(HandlerObject(callback))
146
+ return callback
147
+
148
+ return decorator
149
+
150
+ def on_disconnect(self) -> Callable[..., Any]:
151
+ def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
152
+ self._handlers["disconnect"].append(HandlerObject(callback))
153
+ return callback
154
+
155
+ return decorator
156
+
157
+ def on_message(self, *filters: Any) -> Callable[..., Any]:
158
+ def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
159
+ self._handlers["message"].append(HandlerObject(callback, list(filters)))
160
+ return callback
161
+
162
+ return decorator
163
+
164
+ def on_command(self, commands: str | list[str]) -> Callable[..., Any]:
165
+ def decorator(callback: Callable[..., Any]) -> Callable[..., Any]:
166
+ self._handlers["message"].append(HandlerObject(callback, [Command(commands)]))
167
+ return callback
168
+
169
+ return decorator
170
+
171
+ def include_router(self, router: Router) -> None:
172
+ router.include(self)
173
+
174
+ def run(self) -> None:
175
+ self._loop = asyncio.new_event_loop()
176
+ asyncio.set_event_loop(self._loop)
177
+ if self.mode == "server":
178
+ self._setup_server()
179
+ else:
180
+ self._setup_client()
181
+ try:
182
+ self._loop.run_forever()
183
+ except KeyboardInterrupt:
184
+ pass
185
+ finally:
186
+ self._loop.close()
187
+
188
+ def _setup_server(self) -> None:
189
+ if not self.user or not self.password:
190
+ raise ValueError("user and password required for server mode")
191
+
192
+ def _on_connect(user: str) -> None:
193
+ if self._loop is not None:
194
+ self._loop.call_soon_threadsafe(self._process_connect, user)
195
+
196
+ def _on_disconnect(user: str) -> None:
197
+ if self._loop is not None:
198
+ self._loop.call_soon_threadsafe(self._process_disconnect, user)
199
+
200
+ def _on_msg(msg: ChatMsg) -> None:
201
+ if self._loop is not None:
202
+ self._loop.call_soon_threadsafe(self._process_chat, msg)
203
+
204
+ start_server(
205
+ self.host,
206
+ self.port,
207
+ self.user,
208
+ self.password,
209
+ on_connect=_on_connect,
210
+ on_disconnect=_on_disconnect,
211
+ )
212
+ _state.on_message(_on_msg)
213
+
214
+ def _setup_client(self) -> None:
215
+ if not self.connect_string or not self.password:
216
+ raise ValueError("connect_string and password required for client mode")
217
+
218
+ ch, _fp = client_connect(self.connect_string, self.password)
219
+ self._channel = ch
220
+
221
+ welcome = ch.recv()
222
+ if welcome.get("type") == "welcome":
223
+ for m in welcome.get("history", []):
224
+ self._process_raw_msg(ch, m)
225
+
226
+ def _recv() -> None:
227
+ while True:
228
+ try:
229
+ msg = ch.recv()
230
+ if self._loop is not None:
231
+ self._loop.call_soon_threadsafe(self._process_raw_msg, ch, msg)
232
+ except Exception:
233
+ break
234
+
235
+ threading.Thread(target=_recv, daemon=True).start()
236
+
237
+ def _process_connect(self, user: str) -> None:
238
+ ch = _state.get_channel(user)
239
+ ctx = Context(self, ch, user)
240
+ for handler in self._handlers["connect"]:
241
+ asyncio.create_task(handler.call(ctx))
242
+
243
+ def _process_disconnect(self, user: str) -> None:
244
+ ch = _state.get_channel(user)
245
+ ctx = Context(self, ch, user)
246
+ for handler in self._handlers["disconnect"]:
247
+ asyncio.create_task(handler.call(ctx))
248
+
249
+ def _process_chat(self, m: ChatMsg) -> None:
250
+ ch = _state.get_channel(m.user)
251
+ if ch is None:
252
+ return
253
+ msg = Message.from_dict(m.as_dict())
254
+ ctx = Context(self, ch, m.user, msg)
255
+ for handler in self._handlers["message"]:
256
+ asyncio.create_task(self._run_handler(handler, msg.raw, ctx))
257
+
258
+ def _process_raw_msg(self, ch: SecureChannel, msg: dict[str, Any]) -> None:
259
+ mtype = msg.get("type", "")
260
+ if mtype == "chat":
261
+ message = Message.from_dict(msg)
262
+ ctx = Context(self, ch, msg.get("user", ""), message)
263
+ for handler in self._handlers["message"]:
264
+ asyncio.create_task(self._run_handler(handler, msg, ctx))
265
+
266
+ async def _run_handler(
267
+ self, handler: HandlerObject, data: dict[str, Any], ctx: Context
268
+ ) -> None:
269
+ if await handler.check(data):
270
+ await handler.call(ctx)
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from fognode.auth.handshake import client_handshake, server_handshake
4
+
5
+ __all__ = ["client_handshake", "server_handshake"]
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import os
5
+ import ssl
6
+
7
+ from fognode.crypto.cert import cert_fingerprint, cert_paths
8
+ from fognode.crypto.channel import SecureChannel
9
+ from fognode.crypto.kx import x25519_keypair, x25519_shared
10
+ from fognode.crypto.password import load_password_key
11
+ from fognode.crypto.primitives import hkdf_expand, hmac256, hmac256_verify, pbkdf2
12
+ from fognode.types.constants import NONCE_LENGTH, TOKEN_LENGTH
13
+ from fognode.types.exceptions import AuthError, SecurityError
14
+ from fognode.types.protocol import IPAddress, User
15
+ from fognode.utils.ratelimit import RateLimiter
16
+ from fognode.wire.framing import wire_recv, wire_send
17
+
18
+ _rl = RateLimiter()
19
+
20
+
21
+ def server_handshake(
22
+ tls: ssl.SSLSocket, client_ip: IPAddress, expected_user: User
23
+ ) -> SecureChannel:
24
+ salt, stored_key = load_password_key()
25
+ server_fp = cert_fingerprint(cert_paths()[0])
26
+ nonce = os.urandom(NONCE_LENGTH)
27
+ sv_pub, sv_priv = x25519_keypair()
28
+
29
+ wire_send(
30
+ tls,
31
+ {
32
+ "type": "challenge",
33
+ "nonce": nonce.hex(),
34
+ "salt": salt.hex(),
35
+ "fp": server_fp,
36
+ "ecdh_pub": base64.b64encode(sv_pub).decode(),
37
+ "ver": "1",
38
+ },
39
+ )
40
+
41
+ resp = wire_recv(tls)
42
+ client_user = resp.get("user", "")
43
+ hmac_resp = bytes.fromhex(resp.get("hmac_resp", ""))
44
+ client_fp = resp.get("cert_fp", "")
45
+ cl_pub = base64.b64decode(resp.get("ecdh_pub", ""))
46
+
47
+ if client_fp.upper() != server_fp.upper():
48
+ wire_send(tls, {"ok": False, "error": "fp_mismatch"})
49
+ raise AuthError("fp mismatch")
50
+
51
+ if client_user.encode() != expected_user.encode():
52
+ wire_send(tls, {"ok": False, "error": "bad_user"})
53
+ raise AuthError("bad user")
54
+
55
+ if not hmac256_verify(nonce, stored_key, hmac_resp):
56
+ wire_send(tls, {"ok": False, "error": "bad_password"})
57
+ raise AuthError("bad password")
58
+
59
+ session_token = os.urandom(TOKEN_LENGTH)
60
+ shared = x25519_shared(sv_priv, cl_pub)
61
+ key_material = hkdf_expand(shared, session_token, b"fognode_v1_channel_keys", length=64)
62
+ enc_key, mac_key = key_material[:32], key_material[32:]
63
+
64
+ wire_send(tls, {"ok": True, "token": session_token.hex()})
65
+ _rl.ok(client_ip)
66
+ return SecureChannel(tls, enc_key, mac_key)
67
+
68
+
69
+ def client_handshake(
70
+ tls: ssl.SSLSocket, username: User, password: str, measured_fp: str
71
+ ) -> SecureChannel:
72
+ chal = wire_recv(tls)
73
+ if chal.get("type") != "challenge":
74
+ raise ConnectionError("expected challenge")
75
+
76
+ nonce = bytes.fromhex(chal["nonce"])
77
+ salt = bytes.fromhex(chal["salt"])
78
+ server_fp = chal.get("fp", "")
79
+ sv_pub = base64.b64decode(chal.get("ecdh_pub", ""))
80
+
81
+ if measured_fp.upper() != server_fp.upper():
82
+ raise SecurityError("fingerprint mismatch")
83
+
84
+ key = pbkdf2(password, salt)
85
+ hmac_resp = hmac256(nonce, key)
86
+ cl_pub, cl_priv = x25519_keypair()
87
+
88
+ wire_send(
89
+ tls,
90
+ {
91
+ "user": username,
92
+ "hmac_resp": hmac_resp.hex(),
93
+ "cert_fp": measured_fp,
94
+ "ecdh_pub": base64.b64encode(cl_pub).decode(),
95
+ },
96
+ )
97
+
98
+ result = wire_recv(tls)
99
+ if not result.get("ok"):
100
+ raise AuthError(f"auth rejected: {result.get('error')}")
101
+
102
+ session_token = bytes.fromhex(result["token"])
103
+ shared = x25519_shared(cl_priv, sv_pub)
104
+ key_material = hkdf_expand(shared, session_token, b"fognode_v1_channel_keys", length=64)
105
+ enc_key, mac_key = key_material[:32], key_material[32:]
106
+
107
+ return SecureChannel(tls, enc_key, mac_key)
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from fognode.ciphers.aesgcm import aes_decrypt, aes_encrypt
4
+ from fognode.ciphers.blake2 import blake2b, blake2s
5
+ from fognode.ciphers.blowfish import blowfish_decrypt, blowfish_encrypt
6
+ from fognode.ciphers.chacha20 import chacha_decrypt, chacha_encrypt
7
+ from fognode.ciphers.ed25519 import ed25519_generate_key, ed25519_sign, ed25519_verify
8
+ from fognode.ciphers.fernet import fernet_decrypt, fernet_encrypt, fernet_generate_key
9
+ from fognode.ciphers.hkdf import hkdf_expand
10
+ from fognode.ciphers.hmac import hmac256, hmac256_verify
11
+ from fognode.ciphers.pbkdf2 import pbkdf2
12
+ from fognode.ciphers.rsa import (
13
+ rsa_decrypt,
14
+ rsa_encrypt,
15
+ rsa_generate_key,
16
+ rsa_serialize_private,
17
+ rsa_serialize_public,
18
+ rsa_sign,
19
+ rsa_verify,
20
+ )
21
+ from fognode.ciphers.scrypt import scrypt_kdf
22
+ from fognode.ciphers.sha3 import sha3_256, sha3_512, sha512
23
+ from fognode.ciphers.x25519 import x25519_keypair, x25519_shared
24
+
25
+ __all__ = [
26
+ "aes_decrypt",
27
+ "aes_encrypt",
28
+ "blake2b",
29
+ "blake2s",
30
+ "blowfish_decrypt",
31
+ "blowfish_encrypt",
32
+ "chacha_decrypt",
33
+ "chacha_encrypt",
34
+ "ed25519_generate_key",
35
+ "ed25519_sign",
36
+ "ed25519_verify",
37
+ "fernet_decrypt",
38
+ "fernet_encrypt",
39
+ "fernet_generate_key",
40
+ "hkdf_expand",
41
+ "hmac256",
42
+ "hmac256_verify",
43
+ "pbkdf2",
44
+ "rsa_decrypt",
45
+ "rsa_encrypt",
46
+ "rsa_generate_key",
47
+ "rsa_sign",
48
+ "rsa_serialize_private",
49
+ "rsa_serialize_public",
50
+ "rsa_verify",
51
+ "scrypt_kdf",
52
+ "sha3_256",
53
+ "sha3_512",
54
+ "sha512",
55
+ "x25519_keypair",
56
+ "x25519_shared",
57
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from fognode.crypto.primitives import aes_decrypt, aes_encrypt
4
+
5
+ __all__ = ["aes_decrypt", "aes_encrypt"]
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+
5
+
6
+ def blake2b(data: bytes, key: bytes = b"") -> bytes:
7
+ return hashlib.blake2b(data, key=key).digest()
8
+
9
+
10
+ def blake2s(data: bytes, key: bytes = b"") -> bytes:
11
+ return hashlib.blake2s(data, key=key).digest()
12
+
13
+
14
+ __all__ = ["blake2b", "blake2s"]
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from cryptography.hazmat.primitives.ciphers import Cipher as _Cipher
6
+ from cryptography.hazmat.primitives.ciphers import algorithms, modes
7
+
8
+ from fognode.types.exceptions import SecurityError
9
+
10
+
11
+ def blowfish_encrypt(key: bytes, plaintext: bytes) -> tuple[bytes, bytes]:
12
+ iv = os.urandom(8)
13
+ cipher = _Cipher(algorithms.Blowfish(key), modes.CBC(iv))
14
+ encryptor = cipher.encryptor()
15
+ pad_len = 8 - (len(plaintext) % 8)
16
+ padded = plaintext + bytes([pad_len] * pad_len)
17
+ ct = encryptor.update(padded) + encryptor.finalize()
18
+ return iv, ct
19
+
20
+
21
+ def blowfish_decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes:
22
+ try:
23
+ cipher = _Cipher(algorithms.Blowfish(key), modes.CBC(iv))
24
+ decryptor = cipher.decryptor()
25
+ padded = decryptor.update(ciphertext) + decryptor.finalize()
26
+ pad_len = padded[-1]
27
+ return padded[:-pad_len]
28
+ except Exception:
29
+ raise SecurityError("blowfish decrypt failed") from None
30
+
31
+
32
+ __all__ = ["blowfish_decrypt", "blowfish_encrypt"]
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
6
+
7
+ from fognode.types.exceptions import SecurityError
8
+
9
+
10
+ def chacha_encrypt(key: bytes, plaintext: bytes, aad: bytes) -> tuple[bytes, bytes]:
11
+ nonce = os.urandom(12)
12
+ ct = ChaCha20Poly1305(key).encrypt(nonce, plaintext, aad)
13
+ return nonce, ct
14
+
15
+
16
+ def chacha_decrypt(key: bytes, nonce: bytes, ciphertext: bytes, aad: bytes) -> bytes:
17
+ try:
18
+ return ChaCha20Poly1305(key).decrypt(nonce, ciphertext, aad)
19
+ except Exception:
20
+ raise SecurityError("ChaCha20-Poly1305 tag invalid") from None
21
+
22
+
23
+ __all__ = ["chacha_decrypt", "chacha_encrypt"]
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
4
+
5
+
6
+ def ed25519_generate_key() -> tuple[Ed25519PrivateKey, Ed25519PublicKey]:
7
+ private_key = Ed25519PrivateKey.generate()
8
+ return private_key, private_key.public_key()
9
+
10
+
11
+ def ed25519_sign(private_key: Ed25519PrivateKey, data: bytes) -> bytes:
12
+ return private_key.sign(data)
13
+
14
+
15
+ def ed25519_verify(public_key: Ed25519PublicKey, data: bytes, signature: bytes) -> bool:
16
+ try:
17
+ public_key.verify(signature, data)
18
+ return True
19
+ except Exception:
20
+ return False
21
+
22
+
23
+ __all__ = ["ed25519_generate_key", "ed25519_sign", "ed25519_verify"]
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from cryptography.fernet import Fernet, InvalidToken
4
+
5
+ from fognode.types.exceptions import SecurityError
6
+
7
+
8
+ def fernet_generate_key() -> bytes:
9
+ return Fernet.generate_key()
10
+
11
+
12
+ def fernet_encrypt(key: bytes, plaintext: bytes) -> bytes:
13
+ return Fernet(key).encrypt(plaintext)
14
+
15
+
16
+ def fernet_decrypt(key: bytes, ciphertext: bytes) -> bytes:
17
+ try:
18
+ return Fernet(key).decrypt(ciphertext)
19
+ except InvalidToken:
20
+ raise SecurityError("fernet token invalid") from None
21
+
22
+
23
+ __all__ = ["fernet_decrypt", "fernet_encrypt", "fernet_generate_key"]