sibna 1.4.0__tar.gz → 1.7.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.
- {sibna-1.4.0/sibna.egg-info → sibna-1.7.0}/PKG-INFO +1 -1
- {sibna-1.4.0 → sibna-1.7.0}/pyproject.toml +1 -1
- sibna-1.7.0/sibna/__init__.py +569 -0
- sibna-1.7.0/sibna/_ed25519.py +163 -0
- sibna-1.7.0/sibna/_websocket.py +318 -0
- sibna-1.7.0/sibna/client.py +604 -0
- {sibna-1.4.0 → sibna-1.7.0/sibna.egg-info}/PKG-INFO +1 -1
- {sibna-1.4.0 → sibna-1.7.0}/sibna.egg-info/SOURCES.txt +2 -0
- sibna-1.4.0/sibna/__init__.py +0 -635
- sibna-1.4.0/sibna/client.py +0 -627
- {sibna-1.4.0 → sibna-1.7.0}/LICENSE +0 -0
- {sibna-1.4.0 → sibna-1.7.0}/MANIFEST.in +0 -0
- {sibna-1.4.0 → sibna-1.7.0}/README.md +0 -0
- {sibna-1.4.0 → sibna-1.7.0}/setup.cfg +0 -0
- {sibna-1.4.0 → sibna-1.7.0}/sibna/sibna_core.dll +0 -0
- {sibna-1.4.0 → sibna-1.7.0}/sibna.egg-info/dependency_links.txt +0 -0
- {sibna-1.4.0 → sibna-1.7.0}/sibna.egg-info/top_level.txt +0 -0
- {sibna-1.4.0 → sibna-1.7.0}/sibna_demo/app.py +0 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sibna Protocol — Python SDK
|
|
3
|
+
============================
|
|
4
|
+
|
|
5
|
+
Python wrapper for the Sibna Rust core via FFI (ctypes).
|
|
6
|
+
No external dependencies — uses only Python stdlib + the compiled Rust library.
|
|
7
|
+
|
|
8
|
+
Requirements:
|
|
9
|
+
- Python 3.8+
|
|
10
|
+
- The compiled native library from sibna-protc/core:
|
|
11
|
+
Windows : sibna_core.dll
|
|
12
|
+
Linux : libsibna_core.so
|
|
13
|
+
macOS : libsibna_core.dylib
|
|
14
|
+
|
|
15
|
+
Build it:
|
|
16
|
+
cd sibna-protc/core
|
|
17
|
+
cargo build --release --features ffi
|
|
18
|
+
|
|
19
|
+
Then place the file next to this sibna/ folder.
|
|
20
|
+
|
|
21
|
+
Quick start:
|
|
22
|
+
import sibna
|
|
23
|
+
|
|
24
|
+
if not sibna.is_available():
|
|
25
|
+
raise RuntimeError("Native library not found")
|
|
26
|
+
|
|
27
|
+
key = sibna.generate_key()
|
|
28
|
+
ct = sibna.encrypt(key, b"Hello")
|
|
29
|
+
pt = sibna.decrypt(key, ct)
|
|
30
|
+
assert pt == b"Hello"
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
__version__ = "1.0.0"
|
|
34
|
+
__author__ = "Sibna Security Team"
|
|
35
|
+
__license__ = "Apache-2.0"
|
|
36
|
+
|
|
37
|
+
import ctypes
|
|
38
|
+
import os
|
|
39
|
+
import platform
|
|
40
|
+
from typing import Optional, Tuple
|
|
41
|
+
|
|
42
|
+
# ── Library loading ────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
def _find_library() -> Optional[str]:
|
|
45
|
+
system = platform.system()
|
|
46
|
+
if system == "Linux":
|
|
47
|
+
names = ["libsibna_core.so"]
|
|
48
|
+
elif system == "Darwin":
|
|
49
|
+
names = ["libsibna_core.dylib"]
|
|
50
|
+
elif system == "Windows":
|
|
51
|
+
names = ["sibna_core.dll"]
|
|
52
|
+
else:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
search_paths = [
|
|
56
|
+
os.path.dirname(os.path.dirname(__file__)), # parent of sibna/
|
|
57
|
+
os.path.dirname(__file__), # sibna/ itself
|
|
58
|
+
os.path.join(os.path.dirname(__file__),
|
|
59
|
+
"..", "..", "core", "target", "release"),
|
|
60
|
+
]
|
|
61
|
+
for folder in search_paths:
|
|
62
|
+
for name in names:
|
|
63
|
+
candidate = os.path.normpath(os.path.join(folder, name))
|
|
64
|
+
if os.path.isfile(candidate):
|
|
65
|
+
return candidate
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _configure(lib: ctypes.CDLL) -> None:
|
|
70
|
+
lib.sibna_generate_key.restype = ctypes.c_int
|
|
71
|
+
lib.sibna_generate_key.argtypes = [ctypes.c_char_p]
|
|
72
|
+
|
|
73
|
+
lib.sibna_random_bytes.restype = ctypes.c_int
|
|
74
|
+
lib.sibna_random_bytes.argtypes = [ctypes.c_size_t, ctypes.c_char_p]
|
|
75
|
+
|
|
76
|
+
lib.sibna_encrypt.restype = ctypes.c_int
|
|
77
|
+
lib.sibna_encrypt.argtypes = [
|
|
78
|
+
ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t,
|
|
79
|
+
ctypes.c_char_p, ctypes.c_size_t, ctypes.c_void_p,
|
|
80
|
+
]
|
|
81
|
+
lib.sibna_decrypt.restype = ctypes.c_int
|
|
82
|
+
lib.sibna_decrypt.argtypes = [
|
|
83
|
+
ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t,
|
|
84
|
+
ctypes.c_char_p, ctypes.c_size_t, ctypes.c_void_p,
|
|
85
|
+
]
|
|
86
|
+
lib.sibna_free_buffer.restype = None
|
|
87
|
+
lib.sibna_free_buffer.argtypes = [ctypes.c_void_p]
|
|
88
|
+
|
|
89
|
+
lib.sibna_version.restype = ctypes.c_int
|
|
90
|
+
lib.sibna_version.argtypes = [ctypes.c_char_p, ctypes.c_size_t]
|
|
91
|
+
|
|
92
|
+
lib.sibna_context_create.restype = ctypes.c_int
|
|
93
|
+
lib.sibna_context_create.argtypes = [
|
|
94
|
+
ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_void_p),
|
|
95
|
+
]
|
|
96
|
+
lib.sibna_context_destroy.restype = None
|
|
97
|
+
lib.sibna_context_destroy.argtypes = [ctypes.c_void_p]
|
|
98
|
+
|
|
99
|
+
lib.sibna_generate_identity.restype = ctypes.c_int
|
|
100
|
+
lib.sibna_generate_identity.argtypes = [
|
|
101
|
+
ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p,
|
|
102
|
+
]
|
|
103
|
+
lib.sibna_generate_prekey_bundle.restype = ctypes.c_int
|
|
104
|
+
lib.sibna_generate_prekey_bundle.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
105
|
+
|
|
106
|
+
lib.sibna_perform_handshake.restype = ctypes.c_int
|
|
107
|
+
lib.sibna_perform_handshake.argtypes = [
|
|
108
|
+
ctypes.c_void_p,
|
|
109
|
+
ctypes.c_char_p, ctypes.c_size_t,
|
|
110
|
+
ctypes.c_char_p, ctypes.c_size_t,
|
|
111
|
+
ctypes.c_uint8,
|
|
112
|
+
]
|
|
113
|
+
lib.sibna_session_encrypt.restype = ctypes.c_int
|
|
114
|
+
lib.sibna_session_encrypt.argtypes = [
|
|
115
|
+
ctypes.c_void_p,
|
|
116
|
+
ctypes.c_char_p, ctypes.c_size_t,
|
|
117
|
+
ctypes.c_char_p, ctypes.c_size_t,
|
|
118
|
+
ctypes.c_char_p, ctypes.c_size_t,
|
|
119
|
+
ctypes.c_void_p,
|
|
120
|
+
]
|
|
121
|
+
lib.sibna_session_decrypt.restype = ctypes.c_int
|
|
122
|
+
lib.sibna_session_decrypt.argtypes = [
|
|
123
|
+
ctypes.c_void_p,
|
|
124
|
+
ctypes.c_char_p, ctypes.c_size_t,
|
|
125
|
+
ctypes.c_char_p, ctypes.c_size_t,
|
|
126
|
+
ctypes.c_char_p, ctypes.c_size_t,
|
|
127
|
+
ctypes.c_void_p,
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _load() -> Optional[ctypes.CDLL]:
|
|
132
|
+
path = _find_library()
|
|
133
|
+
if not path:
|
|
134
|
+
return None
|
|
135
|
+
try:
|
|
136
|
+
lib = ctypes.CDLL(path)
|
|
137
|
+
_configure(lib)
|
|
138
|
+
return lib
|
|
139
|
+
except OSError:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_lib: Optional[ctypes.CDLL] = _load()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def is_available() -> bool:
|
|
147
|
+
"""True if the native Rust library is loaded and ready."""
|
|
148
|
+
return _lib is not None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def library_path() -> Optional[str]:
|
|
152
|
+
"""Full path of the loaded library, or None."""
|
|
153
|
+
return _find_library()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── ByteBuffer — mirrors Rust FFI struct ──────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
class _ByteBuffer(ctypes.Structure):
|
|
159
|
+
_fields_ = [
|
|
160
|
+
("data", ctypes.POINTER(ctypes.c_uint8)),
|
|
161
|
+
("len", ctypes.c_size_t),
|
|
162
|
+
("capacity", ctypes.c_size_t),
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
def to_bytes(self) -> bytes:
|
|
166
|
+
if not self.data or self.len == 0:
|
|
167
|
+
return b""
|
|
168
|
+
return bytes((ctypes.c_uint8 * self.len).from_address(
|
|
169
|
+
ctypes.addressof(self.data.contents)
|
|
170
|
+
))
|
|
171
|
+
|
|
172
|
+
def free(self) -> None:
|
|
173
|
+
if _lib and self.data:
|
|
174
|
+
_lib.sibna_free_buffer(ctypes.byref(self))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ── Errors ────────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
_ERRORS = {
|
|
180
|
+
1: "Invalid argument", 2: "Invalid key", 3: "Encryption failed",
|
|
181
|
+
4: "Decryption failed", 5: "Out of memory", 6: "Invalid state",
|
|
182
|
+
7: "Session not found", 8: "Key not found", 9: "Rate limit exceeded",
|
|
183
|
+
10: "Internal error", 11: "Buffer too small", 12: "Invalid ciphertext",
|
|
184
|
+
13: "Authentication failed",
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class SibnaError(Exception):
|
|
189
|
+
"""Error from the native library."""
|
|
190
|
+
def __init__(self, code: int):
|
|
191
|
+
self.code = code
|
|
192
|
+
super().__init__(f"SibnaError({code}): {_ERRORS.get(code, f'Unknown ({code})')}")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class LibraryNotFoundError(RuntimeError):
|
|
196
|
+
"""
|
|
197
|
+
Raised when a function requires the native library and it is not found.
|
|
198
|
+
|
|
199
|
+
Fix:
|
|
200
|
+
cd sibna-protc/core
|
|
201
|
+
cargo build --release --features ffi
|
|
202
|
+
Then place sibna_core.dll / libsibna_core.so / .dylib
|
|
203
|
+
next to the sibna/ package folder.
|
|
204
|
+
"""
|
|
205
|
+
def __init__(self):
|
|
206
|
+
super().__init__(
|
|
207
|
+
"Native library not found.\n"
|
|
208
|
+
"Build it from sibna-protc:\n"
|
|
209
|
+
" cd sibna-protc/core\n"
|
|
210
|
+
" cargo build --release --features ffi\n"
|
|
211
|
+
"Then place the output file next to this SDK."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _require() -> ctypes.CDLL:
|
|
216
|
+
if _lib is None:
|
|
217
|
+
raise LibraryNotFoundError()
|
|
218
|
+
return _lib
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _check(code: int) -> None:
|
|
222
|
+
if code != 0:
|
|
223
|
+
raise SibnaError(code)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ── Standalone crypto ─────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
def version() -> str:
|
|
229
|
+
"""Protocol version string from the native library."""
|
|
230
|
+
lib = _require()
|
|
231
|
+
buf = ctypes.create_string_buffer(32)
|
|
232
|
+
_check(lib.sibna_version(buf, 32))
|
|
233
|
+
return buf.value.decode()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def generate_key() -> bytes:
|
|
237
|
+
"""
|
|
238
|
+
Generate a cryptographically secure random 32-byte encryption key.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
bytes: 32-byte key for use with encrypt() / decrypt().
|
|
242
|
+
"""
|
|
243
|
+
lib = _require()
|
|
244
|
+
buf = ctypes.create_string_buffer(32)
|
|
245
|
+
_check(lib.sibna_generate_key(buf))
|
|
246
|
+
return buf.raw
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def random_bytes(length: int) -> bytes:
|
|
250
|
+
"""
|
|
251
|
+
Generate cryptographically secure random bytes.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
length: Number of bytes (must be > 0).
|
|
255
|
+
"""
|
|
256
|
+
if length <= 0:
|
|
257
|
+
raise ValueError("length must be > 0")
|
|
258
|
+
lib = _require()
|
|
259
|
+
buf = ctypes.create_string_buffer(length)
|
|
260
|
+
_check(lib.sibna_random_bytes(length, buf))
|
|
261
|
+
return buf.raw
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def encrypt(
|
|
265
|
+
key: bytes,
|
|
266
|
+
plaintext: bytes,
|
|
267
|
+
associated_data: Optional[bytes] = None,
|
|
268
|
+
) -> bytes:
|
|
269
|
+
"""
|
|
270
|
+
Encrypt data with ChaCha20-Poly1305.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
key: 32-byte key from generate_key().
|
|
274
|
+
plaintext: Data to encrypt (must be non-empty).
|
|
275
|
+
associated_data: Optional authenticated-but-not-encrypted context.
|
|
276
|
+
Must pass the same value to decrypt().
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
bytes: nonce (12) + ciphertext + tag (16).
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
ValueError: Wrong key size or empty plaintext.
|
|
283
|
+
SibnaError(3): Encryption failed.
|
|
284
|
+
LibraryNotFoundError if library not loaded.
|
|
285
|
+
"""
|
|
286
|
+
lib = _require()
|
|
287
|
+
if len(key) != 32:
|
|
288
|
+
raise ValueError(f"Key must be 32 bytes (got {len(key)})")
|
|
289
|
+
if not plaintext:
|
|
290
|
+
raise ValueError("plaintext must not be empty")
|
|
291
|
+
out = _ByteBuffer()
|
|
292
|
+
_check(lib.sibna_encrypt(
|
|
293
|
+
key, plaintext, len(plaintext),
|
|
294
|
+
associated_data, len(associated_data) if associated_data else 0,
|
|
295
|
+
ctypes.byref(out),
|
|
296
|
+
))
|
|
297
|
+
try:
|
|
298
|
+
return out.to_bytes()
|
|
299
|
+
finally:
|
|
300
|
+
out.free()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def decrypt(
|
|
304
|
+
key: bytes,
|
|
305
|
+
ciphertext: bytes,
|
|
306
|
+
associated_data: Optional[bytes] = None,
|
|
307
|
+
) -> bytes:
|
|
308
|
+
"""
|
|
309
|
+
Decrypt data with ChaCha20-Poly1305.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
key: Same 32-byte key used in encrypt().
|
|
313
|
+
ciphertext: Output of encrypt().
|
|
314
|
+
associated_data: Same value passed to encrypt() (if any).
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
bytes: Original plaintext.
|
|
318
|
+
|
|
319
|
+
Raises:
|
|
320
|
+
SibnaError(13): Data tampered or wrong key (Authentication failed).
|
|
321
|
+
SibnaError(12): Ciphertext is invalid/truncated.
|
|
322
|
+
LibraryNotFoundError if library not loaded.
|
|
323
|
+
"""
|
|
324
|
+
lib = _require()
|
|
325
|
+
if len(key) != 32:
|
|
326
|
+
raise ValueError(f"Key must be 32 bytes (got {len(key)})")
|
|
327
|
+
if not ciphertext:
|
|
328
|
+
raise ValueError("ciphertext must not be empty")
|
|
329
|
+
out = _ByteBuffer()
|
|
330
|
+
_check(lib.sibna_decrypt(
|
|
331
|
+
key, ciphertext, len(ciphertext),
|
|
332
|
+
associated_data, len(associated_data) if associated_data else 0,
|
|
333
|
+
ctypes.byref(out),
|
|
334
|
+
))
|
|
335
|
+
try:
|
|
336
|
+
return out.to_bytes()
|
|
337
|
+
finally:
|
|
338
|
+
out.free()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ── Context ────────────────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
class Context:
|
|
344
|
+
"""
|
|
345
|
+
Manages identity keypairs and Double Ratchet sessions.
|
|
346
|
+
|
|
347
|
+
Password rules (enforced by Rust core):
|
|
348
|
+
Must contain uppercase + lowercase + digit, minimum 8 characters.
|
|
349
|
+
|
|
350
|
+
Usage:
|
|
351
|
+
ctx = Context(password=b"MyPass1!")
|
|
352
|
+
ed_pub, x_pub = ctx.generate_identity()
|
|
353
|
+
bundle = ctx.generate_prekey_bundle()
|
|
354
|
+
|
|
355
|
+
# After exchanging bundles with a peer:
|
|
356
|
+
ctx.perform_handshake(peer_id=peer_ed_pub, peer_bundle=peer_bundle, initiator=True)
|
|
357
|
+
ct = ctx.session_encrypt(peer_ed_pub, b"Hello!")
|
|
358
|
+
pt = ctx.session_decrypt(peer_ed_pub, ct)
|
|
359
|
+
|
|
360
|
+
ctx.close()
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
def __init__(self, password: Optional[bytes] = None):
|
|
364
|
+
"""
|
|
365
|
+
Args:
|
|
366
|
+
password: Optional master password for the local keystore.
|
|
367
|
+
If None, a random storage key is generated.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
SibnaError(10): Password too weak.
|
|
371
|
+
LibraryNotFoundError: Library not loaded.
|
|
372
|
+
"""
|
|
373
|
+
lib = _require()
|
|
374
|
+
handle = ctypes.c_void_p()
|
|
375
|
+
_check(lib.sibna_context_create(
|
|
376
|
+
password, len(password) if password else 0,
|
|
377
|
+
ctypes.byref(handle),
|
|
378
|
+
))
|
|
379
|
+
self._handle = handle
|
|
380
|
+
self._lib = lib
|
|
381
|
+
self._closed = False
|
|
382
|
+
|
|
383
|
+
def __enter__(self):
|
|
384
|
+
return self
|
|
385
|
+
|
|
386
|
+
def __exit__(self, *_):
|
|
387
|
+
self.close()
|
|
388
|
+
|
|
389
|
+
def generate_identity(self) -> Tuple[bytes, bytes]:
|
|
390
|
+
"""
|
|
391
|
+
Generate an Ed25519 + X25519 identity keypair stored in the keystore.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
(ed25519_public_key, x25519_public_key) — both 32 bytes.
|
|
395
|
+
- ed25519_public_key: for signing / identity verification
|
|
396
|
+
- x25519_public_key: for X3DH key agreement
|
|
397
|
+
|
|
398
|
+
Raises:
|
|
399
|
+
SibnaError: on failure.
|
|
400
|
+
"""
|
|
401
|
+
self._check_open()
|
|
402
|
+
ed_buf = ctypes.create_string_buffer(32)
|
|
403
|
+
x_buf = ctypes.create_string_buffer(32)
|
|
404
|
+
_check(self._lib.sibna_generate_identity(self._handle, ed_buf, x_buf))
|
|
405
|
+
return ed_buf.raw, x_buf.raw
|
|
406
|
+
|
|
407
|
+
def generate_prekey_bundle(self) -> bytes:
|
|
408
|
+
"""
|
|
409
|
+
Generate a PreKey Bundle to upload to the prekey server.
|
|
410
|
+
|
|
411
|
+
Must call generate_identity() first.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
bytes: Serialized bundle — pass directly to your server
|
|
415
|
+
or to SibnaClient.upload_prekey(bundle.hex()).
|
|
416
|
+
|
|
417
|
+
Raises:
|
|
418
|
+
SibnaError(8): No identity generated yet (Key not found).
|
|
419
|
+
"""
|
|
420
|
+
self._check_open()
|
|
421
|
+
out = _ByteBuffer()
|
|
422
|
+
_check(self._lib.sibna_generate_prekey_bundle(
|
|
423
|
+
self._handle, ctypes.byref(out),
|
|
424
|
+
))
|
|
425
|
+
try:
|
|
426
|
+
return out.to_bytes()
|
|
427
|
+
finally:
|
|
428
|
+
out.free()
|
|
429
|
+
|
|
430
|
+
def perform_handshake(
|
|
431
|
+
self,
|
|
432
|
+
peer_id: bytes,
|
|
433
|
+
peer_bundle: bytes,
|
|
434
|
+
initiator: bool,
|
|
435
|
+
) -> None:
|
|
436
|
+
"""
|
|
437
|
+
Run X3DH handshake and create a Double Ratchet session.
|
|
438
|
+
|
|
439
|
+
After success, use session_encrypt() / session_decrypt() with peer_id.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
peer_id: Peer's Ed25519 public key (used as session identifier).
|
|
443
|
+
peer_bundle: Peer's PreKey Bundle bytes (from prekey server).
|
|
444
|
+
initiator: True if you start the conversation, False if responding.
|
|
445
|
+
|
|
446
|
+
Raises:
|
|
447
|
+
SibnaError(2): Invalid bundle or bad signature.
|
|
448
|
+
SibnaError(6): Context in invalid state.
|
|
449
|
+
"""
|
|
450
|
+
self._check_open()
|
|
451
|
+
if not peer_id:
|
|
452
|
+
raise ValueError("peer_id must not be empty")
|
|
453
|
+
if not peer_bundle:
|
|
454
|
+
raise ValueError("peer_bundle must not be empty")
|
|
455
|
+
_check(self._lib.sibna_perform_handshake(
|
|
456
|
+
self._handle,
|
|
457
|
+
peer_bundle, len(peer_bundle),
|
|
458
|
+
peer_id, len(peer_id),
|
|
459
|
+
1 if initiator else 0,
|
|
460
|
+
))
|
|
461
|
+
|
|
462
|
+
def session_encrypt(
|
|
463
|
+
self,
|
|
464
|
+
peer_id: bytes,
|
|
465
|
+
plaintext: bytes,
|
|
466
|
+
associated_data: Optional[bytes] = None,
|
|
467
|
+
) -> bytes:
|
|
468
|
+
"""
|
|
469
|
+
Encrypt a message over an existing Double Ratchet session.
|
|
470
|
+
|
|
471
|
+
The ratchet key advances with every message — forward secrecy
|
|
472
|
+
is automatic.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
peer_id: Same bytes used in perform_handshake().
|
|
476
|
+
plaintext: Message to encrypt (non-empty).
|
|
477
|
+
associated_data: Optional authenticated context (e.g. message ID).
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
bytes: Encrypted message. Pass to peer's session_decrypt().
|
|
481
|
+
|
|
482
|
+
Raises:
|
|
483
|
+
SibnaError(7): No session for this peer. Call perform_handshake() first.
|
|
484
|
+
SibnaError(9): Rate limit exceeded.
|
|
485
|
+
"""
|
|
486
|
+
self._check_open()
|
|
487
|
+
if not peer_id:
|
|
488
|
+
raise ValueError("peer_id must not be empty")
|
|
489
|
+
if not plaintext:
|
|
490
|
+
raise ValueError("plaintext must not be empty")
|
|
491
|
+
out = _ByteBuffer()
|
|
492
|
+
_check(self._lib.sibna_session_encrypt(
|
|
493
|
+
self._handle,
|
|
494
|
+
peer_id, len(peer_id),
|
|
495
|
+
plaintext, len(plaintext),
|
|
496
|
+
associated_data, len(associated_data) if associated_data else 0,
|
|
497
|
+
ctypes.byref(out),
|
|
498
|
+
))
|
|
499
|
+
try:
|
|
500
|
+
return out.to_bytes()
|
|
501
|
+
finally:
|
|
502
|
+
out.free()
|
|
503
|
+
|
|
504
|
+
def session_decrypt(
|
|
505
|
+
self,
|
|
506
|
+
peer_id: bytes,
|
|
507
|
+
ciphertext: bytes,
|
|
508
|
+
associated_data: Optional[bytes] = None,
|
|
509
|
+
) -> bytes:
|
|
510
|
+
"""
|
|
511
|
+
Decrypt a message from a Double Ratchet session.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
peer_id: Peer identifier (same as used in perform_handshake).
|
|
515
|
+
ciphertext: Encrypted message from the peer's session_encrypt().
|
|
516
|
+
associated_data: Same value passed during encryption (if any).
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
bytes: Original plaintext.
|
|
520
|
+
|
|
521
|
+
Raises:
|
|
522
|
+
SibnaError(13): Message tampered or wrong session.
|
|
523
|
+
SibnaError(7): Session not found.
|
|
524
|
+
SibnaError(12): Ciphertext too short / corrupted.
|
|
525
|
+
"""
|
|
526
|
+
self._check_open()
|
|
527
|
+
if not peer_id:
|
|
528
|
+
raise ValueError("peer_id must not be empty")
|
|
529
|
+
if not ciphertext:
|
|
530
|
+
raise ValueError("ciphertext must not be empty")
|
|
531
|
+
out = _ByteBuffer()
|
|
532
|
+
_check(self._lib.sibna_session_decrypt(
|
|
533
|
+
self._handle,
|
|
534
|
+
peer_id, len(peer_id),
|
|
535
|
+
ciphertext, len(ciphertext),
|
|
536
|
+
associated_data, len(associated_data) if associated_data else 0,
|
|
537
|
+
ctypes.byref(out),
|
|
538
|
+
))
|
|
539
|
+
try:
|
|
540
|
+
return out.to_bytes()
|
|
541
|
+
finally:
|
|
542
|
+
out.free()
|
|
543
|
+
|
|
544
|
+
def close(self) -> None:
|
|
545
|
+
"""Free native memory and zero all keys. Call when done."""
|
|
546
|
+
if not self._closed and self._handle:
|
|
547
|
+
self._lib.sibna_context_destroy(self._handle)
|
|
548
|
+
self._closed = True
|
|
549
|
+
|
|
550
|
+
def _check_open(self) -> None:
|
|
551
|
+
if self._closed:
|
|
552
|
+
raise SibnaError(6) # invalid state
|
|
553
|
+
|
|
554
|
+
def __del__(self):
|
|
555
|
+
self.close()
|
|
556
|
+
|
|
557
|
+
def __repr__(self) -> str:
|
|
558
|
+
return f"<sibna.Context closed={self._closed}>"
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# ── Exports ────────────────────────────────────────────────────────────────────
|
|
562
|
+
|
|
563
|
+
__all__ = [
|
|
564
|
+
"is_available", "library_path", "version",
|
|
565
|
+
"SibnaError", "LibraryNotFoundError",
|
|
566
|
+
"generate_key", "random_bytes", "encrypt", "decrypt",
|
|
567
|
+
"Context",
|
|
568
|
+
"__version__",
|
|
569
|
+
]
|