python3-olm 3.2.19__cp311-cp311-win_amd64.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.
olm/group_session.py ADDED
@@ -0,0 +1,531 @@
1
+ # -*- coding: utf-8 -*-
2
+ # libolm python bindings
3
+ # Copyright © 2015-2017 OpenMarket Ltd
4
+ # Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ """libolm Group session module.
18
+
19
+ This module contains the group session part of the Olm library. It contains two
20
+ classes for creating inbound and outbound group sessions.
21
+
22
+ Examples:
23
+ >>> outbound = OutboundGroupSession()
24
+ >>> InboundGroupSession(outbound.session_key)
25
+ """
26
+
27
+ # pylint: disable=redefined-builtin,unused-import
28
+ from builtins import bytes, super
29
+ from typing import AnyStr, Optional, Tuple, Type
30
+
31
+ # pylint: disable=no-name-in-module
32
+ from _libolm import ffi, lib # type: ignore
33
+
34
+ from ._compat import URANDOM, to_bytearray, to_bytes, to_unicode_str
35
+ from ._finalize import track_for_finalization
36
+
37
+
38
+ def _clear_inbound_group_session(session):
39
+ # type: (ffi.cdata) -> None
40
+ lib.olm_clear_inbound_group_session(session)
41
+
42
+
43
+ def _clear_outbound_group_session(session):
44
+ # type: (ffi.cdata) -> None
45
+ lib.olm_clear_outbound_group_session(session)
46
+
47
+
48
+ class OlmGroupSessionError(Exception):
49
+ """libolm Group session error exception."""
50
+
51
+
52
+ class InboundGroupSession(object):
53
+ """Inbound group session for encrypted multiuser communication."""
54
+
55
+ def __new__(
56
+ cls, # type: Type[InboundGroupSession]
57
+ session_key=None # type: Optional[str]
58
+ ):
59
+ # type: (...) -> InboundGroupSession
60
+ obj = super().__new__(cls)
61
+ obj._buf = ffi.new("char[]", lib.olm_inbound_group_session_size())
62
+ obj._session = lib.olm_inbound_group_session(obj._buf)
63
+ track_for_finalization(obj, obj._session, _clear_inbound_group_session)
64
+ return obj
65
+
66
+ def __init__(self, session_key):
67
+ # type: (AnyStr) -> None
68
+ """Create a new inbound group session.
69
+ Start a new inbound group session, from a key exported from
70
+ an outbound group session.
71
+
72
+ Raises OlmGroupSessionError on failure. The error message of the
73
+ exception will be "OLM_INVALID_BASE64" if the session key is not valid
74
+ base64 and "OLM_BAD_SESSION_KEY" if the session key is invalid.
75
+ """
76
+ if False: # pragma: no cover
77
+ self._session = self._session # type: ffi.cdata
78
+
79
+ byte_session_key = to_bytearray(session_key)
80
+
81
+ try:
82
+ ret = lib.olm_init_inbound_group_session(
83
+ self._session,
84
+ ffi.from_buffer(byte_session_key), len(byte_session_key)
85
+ )
86
+ finally:
87
+ if byte_session_key is not session_key:
88
+ for i in range(0, len(byte_session_key)):
89
+ byte_session_key[i] = 0
90
+ self._check_error(ret)
91
+
92
+ def pickle(self, passphrase=""):
93
+ # type: (Optional[str]) -> bytes
94
+ """Store an inbound group session.
95
+
96
+ Stores a group session as a base64 string. Encrypts the session using
97
+ the supplied passphrase. Returns a byte object containing the base64
98
+ encoded string of the pickled session.
99
+
100
+ Args:
101
+ passphrase(str, optional): The passphrase to be used to encrypt
102
+ the session.
103
+ """
104
+ byte_passphrase = bytearray(passphrase, "utf-8") if passphrase else b""
105
+
106
+ pickle_length = lib.olm_pickle_inbound_group_session_length(
107
+ self._session)
108
+ pickle_buffer = ffi.new("char[]", pickle_length)
109
+
110
+ try:
111
+ ret = lib.olm_pickle_inbound_group_session(
112
+ self._session,
113
+ ffi.from_buffer(byte_passphrase), len(byte_passphrase),
114
+ pickle_buffer, pickle_length
115
+ )
116
+ self._check_error(ret)
117
+ finally:
118
+ # clear out copies of the passphrase
119
+ for i in range(0, len(byte_passphrase)):
120
+ byte_passphrase[i] = 0
121
+
122
+ return ffi.unpack(pickle_buffer, pickle_length)
123
+
124
+ @classmethod
125
+ def from_pickle(cls, pickle, passphrase=""):
126
+ # type: (bytes, Optional[str]) -> InboundGroupSession
127
+ """Load a previously stored inbound group session.
128
+
129
+ Loads an inbound group session from a pickled base64 string and returns
130
+ an InboundGroupSession object. Decrypts the session using the supplied
131
+ passphrase. Raises OlmSessionError on failure. If the passphrase
132
+ doesn't match the one used to encrypt the session then the error
133
+ message for the exception will be "BAD_ACCOUNT_KEY". If the base64
134
+ couldn't be decoded then the error message will be "INVALID_BASE64".
135
+
136
+ Args:
137
+ pickle(bytes): Base64 encoded byte string containing the pickled
138
+ session
139
+ passphrase(str, optional): The passphrase used to encrypt the
140
+ session
141
+ """
142
+ if not pickle:
143
+ raise ValueError("Pickle can't be empty")
144
+
145
+ byte_passphrase = bytearray(passphrase, "utf-8") if passphrase else b""
146
+ # copy because unpickle will destroy the buffer
147
+ pickle_buffer = ffi.new("char[]", pickle)
148
+
149
+ obj = cls.__new__(cls)
150
+
151
+ try:
152
+ ret = lib.olm_unpickle_inbound_group_session(
153
+ obj._session,
154
+ ffi.from_buffer(byte_passphrase),
155
+ len(byte_passphrase),
156
+ pickle_buffer,
157
+ len(pickle)
158
+ )
159
+ obj._check_error(ret)
160
+ finally:
161
+ # clear out copies of the passphrase
162
+ for i in range(0, len(byte_passphrase)):
163
+ byte_passphrase[i] = 0
164
+
165
+ return obj
166
+
167
+ def _check_error(self, ret):
168
+ # type: (int) -> None
169
+ if ret != lib.olm_error():
170
+ return
171
+
172
+ last_error = ffi.string(
173
+ lib.olm_inbound_group_session_last_error(self._session)
174
+ ).decode()
175
+
176
+ raise OlmGroupSessionError(last_error)
177
+
178
+ def decrypt(self, ciphertext, unicode_errors="replace"):
179
+ # type: (AnyStr, str) -> Tuple[str, int]
180
+ """Decrypt a message
181
+
182
+ Returns a tuple of the decrypted plain-text and the message index of
183
+ the decrypted message or raises OlmGroupSessionError on failure.
184
+ On failure the error message of the exception will be:
185
+
186
+ * OLM_INVALID_BASE64 if the message is not valid base64
187
+ * OLM_BAD_MESSAGE_VERSION if the message was encrypted with an
188
+ unsupported version of the protocol
189
+ * OLM_BAD_MESSAGE_FORMAT if the message headers could not be
190
+ decoded
191
+ * OLM_BAD_MESSAGE_MAC if the message could not be verified
192
+ * OLM_UNKNOWN_MESSAGE_INDEX if we do not have a session key
193
+ corresponding to the message's index (i.e., it was sent before
194
+ the session key was shared with us)
195
+
196
+ Args:
197
+ ciphertext(str): Base64 encoded ciphertext containing the encrypted
198
+ message
199
+ unicode_errors(str, optional): The error handling scheme to use for
200
+ unicode decoding errors. The default is "replace" meaning that
201
+ the character that was unable to decode will be replaced with
202
+ the unicode replacement character (U+FFFD). Other possible
203
+ values are "strict", "ignore" and "xmlcharrefreplace" as well
204
+ as any other name registered with codecs.register_error that
205
+ can handle UnicodeEncodeErrors.
206
+ """
207
+ if not ciphertext:
208
+ raise ValueError("Ciphertext can't be empty.")
209
+
210
+ byte_ciphertext = to_bytes(ciphertext)
211
+
212
+ # copy because max_plaintext_length will destroy the buffer
213
+ ciphertext_buffer = ffi.new("char[]", byte_ciphertext)
214
+
215
+ max_plaintext_length = lib.olm_group_decrypt_max_plaintext_length(
216
+ self._session, ciphertext_buffer, len(byte_ciphertext)
217
+ )
218
+ self._check_error(max_plaintext_length)
219
+ plaintext_buffer = ffi.new("char[]", max_plaintext_length)
220
+ # copy because max_plaintext_length will destroy the buffer
221
+ ciphertext_buffer = ffi.new("char[]", byte_ciphertext)
222
+
223
+ message_index = ffi.new("uint32_t*")
224
+ plaintext_length = lib.olm_group_decrypt(
225
+ self._session, ciphertext_buffer, len(byte_ciphertext),
226
+ plaintext_buffer, max_plaintext_length,
227
+ message_index
228
+ )
229
+
230
+ self._check_error(plaintext_length)
231
+
232
+ plaintext = to_unicode_str(
233
+ ffi.unpack(plaintext_buffer, plaintext_length),
234
+ errors=unicode_errors
235
+ )
236
+
237
+ # clear out copies of the plaintext
238
+ lib.memset(plaintext_buffer, 0, max_plaintext_length)
239
+
240
+ return plaintext, message_index[0]
241
+
242
+ @property
243
+ def id(self):
244
+ # type: () -> str
245
+ """str: A base64 encoded identifier for this session."""
246
+ id_length = lib.olm_inbound_group_session_id_length(self._session)
247
+ id_buffer = ffi.new("char[]", id_length)
248
+ ret = lib.olm_inbound_group_session_id(
249
+ self._session,
250
+ id_buffer,
251
+ id_length
252
+ )
253
+ self._check_error(ret)
254
+ return ffi.unpack(id_buffer, id_length).decode()
255
+
256
+ @property
257
+ def first_known_index(self):
258
+ # type: () -> int
259
+ """int: The first message index we know how to decrypt."""
260
+ return lib.olm_inbound_group_session_first_known_index(self._session)
261
+
262
+ def export_session(self, message_index):
263
+ # type: (int) -> str
264
+ """Export an inbound group session
265
+
266
+ Export the base64-encoded ratchet key for this session, at the given
267
+ index, in a format which can be used by import_session().
268
+
269
+ Raises OlmGroupSessionError on failure. The error message for the
270
+ exception will be:
271
+
272
+ * OLM_UNKNOWN_MESSAGE_INDEX if we do not have a session key
273
+ corresponding to the given index (ie, it was sent before the
274
+ session key was shared with us)
275
+
276
+ Args:
277
+ message_index(int): The message index at which the session should
278
+ be exported.
279
+ """
280
+
281
+ export_length = lib.olm_export_inbound_group_session_length(
282
+ self._session)
283
+
284
+ export_buffer = ffi.new("char[]", export_length)
285
+ ret = lib.olm_export_inbound_group_session(
286
+ self._session,
287
+ export_buffer,
288
+ export_length,
289
+ message_index
290
+ )
291
+ self._check_error(ret)
292
+ export_str = ffi.unpack(export_buffer, export_length).decode()
293
+
294
+ # clear out copies of the key
295
+ lib.memset(export_buffer, 0, export_length)
296
+
297
+ return export_str
298
+
299
+ @classmethod
300
+ def import_session(cls, session_key):
301
+ # type: (AnyStr) -> InboundGroupSession
302
+ """Create an InboundGroupSession from an exported session key.
303
+
304
+ Creates an InboundGroupSession with an previously exported session key,
305
+ raises OlmGroupSessionError on failure. The error message for the
306
+ exception will be:
307
+
308
+ * OLM_INVALID_BASE64 if the session_key is not valid base64
309
+ * OLM_BAD_SESSION_KEY if the session_key is invalid
310
+
311
+ Args:
312
+ session_key(str): The exported session key with which the inbound
313
+ group session will be created
314
+ """
315
+ obj = cls.__new__(cls)
316
+
317
+ byte_session_key = to_bytearray(session_key)
318
+
319
+ try:
320
+ ret = lib.olm_import_inbound_group_session(
321
+ obj._session,
322
+ ffi.from_buffer(byte_session_key),
323
+ len(byte_session_key)
324
+ )
325
+ obj._check_error(ret)
326
+ finally:
327
+ # clear out copies of the key
328
+ if byte_session_key is not session_key:
329
+ for i in range(0, len(byte_session_key)):
330
+ byte_session_key[i] = 0
331
+
332
+ return obj
333
+
334
+
335
+ class OutboundGroupSession(object):
336
+ """Outbound group session for encrypted multiuser communication."""
337
+
338
+ def __new__(cls):
339
+ # type: (Type[OutboundGroupSession]) -> OutboundGroupSession
340
+ obj = super().__new__(cls)
341
+ obj._buf = ffi.new("char[]", lib.olm_outbound_group_session_size())
342
+ obj._session = lib.olm_outbound_group_session(obj._buf)
343
+ track_for_finalization(
344
+ obj,
345
+ obj._session,
346
+ _clear_outbound_group_session
347
+ )
348
+ return obj
349
+
350
+ def __init__(self):
351
+ # type: () -> None
352
+ """Create a new outbound group session.
353
+
354
+ Start a new outbound group session. Raises OlmGroupSessionError on
355
+ failure.
356
+ """
357
+ if False: # pragma: no cover
358
+ self._session = self._session # type: ffi.cdata
359
+
360
+ random_length = lib.olm_init_outbound_group_session_random_length(
361
+ self._session
362
+ )
363
+ random = URANDOM(random_length)
364
+
365
+ ret = lib.olm_init_outbound_group_session(
366
+ self._session, ffi.from_buffer(random), random_length
367
+ )
368
+ self._check_error(ret)
369
+
370
+ def _check_error(self, ret):
371
+ # type: (int) -> None
372
+ if ret != lib.olm_error():
373
+ return
374
+
375
+ last_error = ffi.string(
376
+ lib.olm_outbound_group_session_last_error(self._session)
377
+ ).decode()
378
+
379
+ raise OlmGroupSessionError(last_error)
380
+
381
+ def pickle(self, passphrase=""):
382
+ # type: (Optional[str]) -> bytes
383
+ """Store an outbound group session.
384
+
385
+ Stores a group session as a base64 string. Encrypts the session using
386
+ the supplied passphrase. Returns a byte object containing the base64
387
+ encoded string of the pickled session.
388
+
389
+ Args:
390
+ passphrase(str, optional): The passphrase to be used to encrypt
391
+ the session.
392
+ """
393
+ byte_passphrase = bytearray(passphrase, "utf-8") if passphrase else b""
394
+ pickle_length = lib.olm_pickle_outbound_group_session_length(
395
+ self._session)
396
+ pickle_buffer = ffi.new("char[]", pickle_length)
397
+
398
+ try:
399
+ ret = lib.olm_pickle_outbound_group_session(
400
+ self._session,
401
+ ffi.from_buffer(byte_passphrase), len(byte_passphrase),
402
+ pickle_buffer, pickle_length
403
+ )
404
+ self._check_error(ret)
405
+ finally:
406
+ # clear out copies of the passphrase
407
+ for i in range(0, len(byte_passphrase)):
408
+ byte_passphrase[i] = 0
409
+
410
+ return ffi.unpack(pickle_buffer, pickle_length)
411
+
412
+ @classmethod
413
+ def from_pickle(cls, pickle, passphrase=""):
414
+ # type: (bytes, Optional[str]) -> OutboundGroupSession
415
+ """Load a previously stored outbound group session.
416
+
417
+ Loads an outbound group session from a pickled base64 string and
418
+ returns an OutboundGroupSession object. Decrypts the session using the
419
+ supplied passphrase. Raises OlmSessionError on failure. If the
420
+ passphrase doesn't match the one used to encrypt the session then the
421
+ error message for the exception will be "BAD_ACCOUNT_KEY". If the
422
+ base64 couldn't be decoded then the error message will be
423
+ "INVALID_BASE64".
424
+
425
+ Args:
426
+ pickle(bytes): Base64 encoded byte string containing the pickled
427
+ session
428
+ passphrase(str, optional): The passphrase used to encrypt the
429
+ """
430
+ if not pickle:
431
+ raise ValueError("Pickle can't be empty")
432
+
433
+ byte_passphrase = bytearray(passphrase, "utf-8") if passphrase else b""
434
+ # copy because unpickle will destroy the buffer
435
+ pickle_buffer = ffi.new("char[]", pickle)
436
+
437
+ obj = cls.__new__(cls)
438
+
439
+ try:
440
+ ret = lib.olm_unpickle_outbound_group_session(
441
+ obj._session,
442
+ ffi.from_buffer(byte_passphrase),
443
+ len(byte_passphrase),
444
+ pickle_buffer,
445
+ len(pickle)
446
+ )
447
+ obj._check_error(ret)
448
+ finally:
449
+ # clear out copies of the passphrase
450
+ for i in range(0, len(byte_passphrase)):
451
+ byte_passphrase[i] = 0
452
+
453
+ return obj
454
+
455
+ def encrypt(self, plaintext):
456
+ # type: (AnyStr) -> str
457
+ """Encrypt a message.
458
+
459
+ Returns the encrypted ciphertext.
460
+
461
+ Args:
462
+ plaintext(str): A string that will be encrypted using the group
463
+ session.
464
+ """
465
+ byte_plaintext = to_bytearray(plaintext)
466
+ message_length = lib.olm_group_encrypt_message_length(
467
+ self._session, len(byte_plaintext)
468
+ )
469
+
470
+ message_buffer = ffi.new("char[]", message_length)
471
+
472
+ try:
473
+ ret = lib.olm_group_encrypt(
474
+ self._session,
475
+ ffi.from_buffer(byte_plaintext), len(byte_plaintext),
476
+ message_buffer, message_length,
477
+ )
478
+ self._check_error(ret)
479
+ finally:
480
+ # clear out copies of plaintext
481
+ if byte_plaintext is not plaintext:
482
+ for i in range(0, len(byte_plaintext)):
483
+ byte_plaintext[i] = 0
484
+
485
+ return ffi.unpack(message_buffer, message_length).decode()
486
+
487
+ @property
488
+ def id(self):
489
+ # type: () -> str
490
+ """str: A base64 encoded identifier for this session."""
491
+ id_length = lib.olm_outbound_group_session_id_length(self._session)
492
+ id_buffer = ffi.new("char[]", id_length)
493
+
494
+ ret = lib.olm_outbound_group_session_id(
495
+ self._session,
496
+ id_buffer,
497
+ id_length
498
+ )
499
+ self._check_error(ret)
500
+
501
+ return ffi.unpack(id_buffer, id_length).decode()
502
+
503
+ @property
504
+ def message_index(self):
505
+ # type: () -> int
506
+ """int: The current message index of the session.
507
+
508
+ Each message is encrypted with an increasing index. This is the index
509
+ for the next message.
510
+ """
511
+ return lib.olm_outbound_group_session_message_index(self._session)
512
+
513
+ @property
514
+ def session_key(self):
515
+ # type: () -> str
516
+ """The base64-encoded current ratchet key for this session.
517
+
518
+ Each message is encrypted with a different ratchet key. This function
519
+ returns the ratchet key that will be used for the next message.
520
+ """
521
+ key_length = lib.olm_outbound_group_session_key_length(self._session)
522
+ key_buffer = ffi.new("char[]", key_length)
523
+
524
+ ret = lib.olm_outbound_group_session_key(
525
+ self._session,
526
+ key_buffer,
527
+ key_length
528
+ )
529
+ self._check_error(ret)
530
+
531
+ return ffi.unpack(key_buffer, key_length).decode()