comparison sat/plugins/plugin_xep_0384.py @ 2648:0f76813afc57

plugin XEP-0384: OMEMO implementation first draft: this is the initial implementation of OMEMO encryption using python omemo module. /!\ This implementation is not yet working /!\
author Goffi <goffi@goffi.org>
date Sun, 29 Jul 2018 19:24:21 +0200
parents
children e7bfbded652a
comparison
equal deleted inserted replaced
2647:1bf7e89fded0 2648:0f76813afc57
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for OMEMO encryption
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger
23 from sat.core import exceptions
24 from twisted.internet import defer
25 from twisted.words.xish import domish
26 from twisted.words.protocols.jabber import jid
27 from twisted.words.protocols.jabber import error
28 from sat.memory import persistent
29 from functools import partial
30 import random
31 import base64
32 try:
33 import omemo
34 from omemo.extendedpublicbundle import ExtendedPublicBundle
35 from omemo import wireformat
36 except ImportError:
37 raise exceptions.MissingModule(
38 u'Missing module omemo, please download/install it. You can use '
39 u'"pip install omemo"'
40 )
41
42 log = getLogger(__name__)
43
44
45 PLUGIN_INFO = {
46 C.PI_NAME: u"OMEMO",
47 C.PI_IMPORT_NAME: u"OMEMO",
48 C.PI_TYPE: u"SEC",
49 C.PI_PROTOCOLS: [u"XEP-0384"],
50 C.PI_DEPENDENCIES: [u"XEP-0280", u"XEP-0334", u"XEP-0060"],
51 C.PI_MAIN: u"OMEMO",
52 C.PI_HANDLER: u"no",
53 C.PI_DESCRIPTION: _(u"""Implementation of OMEMO"""),
54 }
55
56 NS_OMEMO = "eu.siacs.conversations.axolotl"
57 NS_OMEMO_DEVICES = NS_OMEMO + ".devicelist"
58 NS_OMEMO_BUNDLE = NS_OMEMO + ".bundles:{device_id}"
59 KEY_STATE = "STATE"
60 KEY_DEVICE_ID = "DEVICE_ID"
61 KEY_SESSION = "SESSION"
62 KEY_ACTIVE_DEVICES = "DEVICES"
63 KEY_INACTIVE_DEVICES = "DEVICES"
64
65
66 def b64enc(data):
67 return base64.b64encode(bytes(bytearray(data))).decode("ASCII")
68
69
70 class OmemoStorage(omemo.Storage):
71
72 def __init__(self, persistent_dict):
73 """
74 @param persistent_dict(persistent.LazyPersistentBinaryDict): object which will
75 store data in SàT database
76 """
77 self.data = persistent_dict
78
79 @property
80 def is_async(self):
81 return True
82
83 def setCb(self, deferred, callback):
84 """Associate Deferred and callback
85
86 callback of omemo.Storage expect a boolean with success state then result
87 Deferred on the other hand use 2 methods for callback and errback
88 This method use partial to call callback with boolean then result when
89 Deferred is called
90 """
91 deferred.addCallback(partial(callback, True))
92 deferred.addErrback(partial(callback, False))
93
94 def loadState(self, callback):
95 d = self.data.get(KEY_STATE)
96 self.setCb(d, callback)
97
98 def storeState(self, callback, state, device_id):
99 d = self.data.force(KEY_STATE, {'state': state, 'device_id': device_id})
100 self.setCb(d, callback)
101
102 def loadSession(self, callback, jid, device_id):
103 key = u'\n'.join([KEY_SESSION, jid, unicode(device_id)])
104 d = self.data.get(key)
105 self.setCb(d, callback)
106
107 def storeSession(self, callback, jid, device_id, session):
108 key = u'\n'.join([KEY_SESSION, jid, unicode(device_id)])
109 d = self.data.force(key, session)
110 self.setCb(d, callback)
111
112 def loadActiveDevices(self, callback, jid):
113 key = u'\n'.join([KEY_ACTIVE_DEVICES, jid])
114 d = self.data.get(key, {})
115 self.setCb(d, callback)
116
117 def loadInactiveDevices(self, callback, jid):
118 key = u'\n'.join([KEY_INACTIVE_DEVICES, jid])
119 d = self.data.get(key, {})
120 self.setCb(d, callback)
121
122 def storeActiveDevices(self, callback, jid, devices):
123 key = u'\n'.join([KEY_ACTIVE_DEVICES, jid])
124 d = self.data.force(key, devices)
125 self.setCb(d, callback)
126
127 def storeInactiveDevices(self, callback, jid, devices):
128 key = u'\n'.join([KEY_INACTIVE_DEVICES, jid])
129 d = self.data.force(key, devices)
130 self.setCb(d, callback)
131
132 def isTrusted(self, callback, jid, device):
133 trusted = True
134 callback(True, trusted)
135
136
137 class SatOTPKPolicy(omemo.OTPKPolicy):
138
139 @staticmethod
140 def decideOTPK(preKeyMessages):
141 # Always just delete the OTPK.
142 # This is the behaviour described in the original X3DH specification.
143 return True
144
145
146 class OmemoSession(object):
147 """Wrapper to use omemo.OmemoSession with Deferred"""
148
149 def __init__(self, session):
150 self._session = session
151
152 @property
153 def state(self):
154 return self._session.state
155
156 @staticmethod
157 def promise2Deferred(promise_):
158 """Create a Deferred and fire it when promise is resolved
159
160 @param promise_(promise.Promise): promise to convert
161 @return (defer.Deferred): deferred instance linked to the promise
162 """
163 d = defer.Deferred()
164 promise_.then(d.callback, d.errback)
165 return d
166
167 @classmethod
168 def create(cls, client, omemo_storage, device_id):
169 omemo_session_p = client._xep_0384_session = omemo.SessionManager.create(
170 client.jid.userhost(), omemo_storage, SatOTPKPolicy, my_device_id=device_id)
171 d = cls.promise2Deferred(omemo_session_p)
172 d.addCallback(lambda session: cls(session))
173 return d
174
175 def newDeviceList(self, devices, jid=None):
176 if jid is not None:
177 jid = jid.userhost()
178 new_device_p = self._session.newDeviceList(devices, jid)
179 return self.promise2Deferred(new_device_p)
180
181 def getDevices(self, bare_jid=None):
182 get_devices_p = self._session.getDevices(bare_jid=bare_jid)
183 return self.promise2Deferred(get_devices_p)
184
185 def buildSession(self, bare_jid, device, bundle):
186 bare_jid = bare_jid.userhost()
187 build_session_p = self._session.buildSession(bare_jid, device, bundle)
188 return self.promise2Deferred(build_session_p)
189
190 def encryptMessage(self, bare_jids, message, bundles=None, devices=None,
191 always_trust = False):
192 """Encrypt a message
193
194 @param bare_jids(iterable[jid.JID]): destinees of the message
195 @param message(unicode): message to encode
196 @param bundles(dict[jid.JID, dict[int, ExtendedPublicBundle]):
197 entities => devices => bundles map
198 @param devices(iterable[int], None): devices to encode for
199 @param always_trust(bool): TODO
200 @return D(dict): encryption data
201 """
202 if isinstance(bare_jids, jid.JID):
203 bare_jids = bare_jids.userhost()
204 else:
205 bare_jids = [e.userhost() for e in bare_jids]
206 if bundles is not None:
207 bundles = {e.userhost(): v for e, v in bundles.iteritems()}
208 encrypt_mess_p = self._session.encryptMessage(
209 bare_jids=bare_jids,
210 plaintext=message.encode('utf-8'),
211 bundles=bundles,
212 devices=devices,
213 always_trust=always_trust)
214 return self.promise2Deferred(encrypt_mess_p)
215
216 def decryptMessage(self, bare_jid, device, iv, message, is_pre_key_message,
217 payload=None, from_storage=False):
218 bare_jid = bare_jid.userhost()
219 decrypt_mess_p = self._session.decryptMessage(
220 bare_jid=bare_jid,
221 device=device,
222 iv=iv,
223 message=message,
224 is_pre_key_message=is_pre_key_message,
225 payload=payload,
226 from_storage=from_storage)
227 return self.promise2Deferred(decrypt_mess_p)
228
229
230 class OMEMO(object):
231 def __init__(self, host):
232 log.info(_(u"OMEMO plugin initialization"))
233 self.host = host
234 self._p_hints = host.plugins[u"XEP-0334"]
235 self._p_carbons = host.plugins[u"XEP-0280"]
236 self._p = host.plugins[u"XEP-0060"]
237 host.trigger.add("MessageReceived", self._messageReceivedTrigger, priority=100050)
238 host.trigger.add("sendMessageData", self._sendMessageDataTrigger)
239 self.host.registerEncryptionPlugin(self, "OMEMO", NS_OMEMO, 100)
240
241 @defer.inlineCallbacks
242 def profileConnected(self, client):
243 # we first need to get devices ids (including our own)
244 persistent_dict = persistent.LazyPersistentBinaryDict("XEP-0384", client.profile)
245 # all known devices of profile
246 devices = yield self.getDevices(client)
247 # and our own device id
248 device_id = yield persistent_dict.get(KEY_DEVICE_ID)
249 if device_id is None:
250 # we have a new device, we create device_id
251 device_id = random.randint(1, 2**31-1)
252 # we check that it's really unique
253 while device_id in devices:
254 device_id = random.randint(1, 2**31-1)
255 # and we save it
256 persistent_dict[KEY_DEVICE_ID] = device_id
257
258 if device_id not in devices:
259 devices.add(device_id)
260 yield self.setDevices(client, devices)
261
262 omemo_storage = OmemoStorage(persistent_dict)
263 omemo_session = yield OmemoSession.create(client, omemo_storage, device_id)
264 client._xep_0384_session = omemo_session
265 client._xep_0384_device_id = device_id
266 yield omemo_session.newDeviceList(devices)
267 if omemo_session.state.changed:
268 log.info(_(u"Saving public bundle for this device ({device_id})").format(
269 device_id=device_id))
270 bundle = omemo_session.state.getPublicBundle()
271 yield self.setBundle(client, bundle, device_id)
272
273 ## XMPP PEP nodes manipulation
274
275 # devices
276
277 @defer.inlineCallbacks
278 def getDevices(self, client, entity_jid=None):
279 """Retrieve list of registered OMEMO devices
280
281 @param entity_jid(jid.JID, None): get devices from this entity
282 None to get our own devices
283 @return (set(int)): list of devices
284 """
285 if entity_jid is not None:
286 assert not entity_jid.resource
287 devices = set()
288 try:
289 items, metadata = yield self._p.getItems(client, entity_jid, NS_OMEMO_DEVICES)
290 except error.StanzaError as e:
291 if e.condition == 'item-not-found':
292 log.info(_(u"there is no node to handle OMEMO devices"))
293 defer.returnValue(devices)
294
295 if len(items) > 1:
296 log.warning(_(u"OMEMO devices list is stored in more that one items, "
297 u"this is not expected"))
298 if items:
299 try:
300 list_elt = next(items[0].elements(NS_OMEMO, 'list'))
301 except StopIteration:
302 log.warning(_(u"no list element found in OMEMO devices list"))
303 return
304 for device_elt in list_elt.elements(NS_OMEMO, 'device'):
305 try:
306 device_id = int(device_elt['id'])
307 except KeyError:
308 log.warning(_(u'device element is missing "id" attribute: {elt}')
309 .format(elt=device_elt.toXml()))
310 except ValueError:
311 log.warning(_(u'invalid device id: {device_id}').format(
312 device_id=device_elt['id']))
313 else:
314 devices.add(device_id)
315 defer.returnValue(devices)
316
317 def setDevicesEb(self, failure_):
318 log.warning(_(u"Can't set devices: {reason}").format(reason=failure_))
319
320 def setDevices(self, client, devices):
321 list_elt = domish.Element((NS_OMEMO, 'list'))
322 for device in devices:
323 device_elt = list_elt.addElement('device')
324 device_elt['id'] = unicode(device)
325 d = self._p.sendItem(
326 client, None, NS_OMEMO_DEVICES, list_elt, item_id=self._p.ID_SINGLETON)
327 d.addErrback(self.setDevicesEb)
328 return d
329
330 # bundles
331
332 @defer.inlineCallbacks
333 def getBundles(self, client, entity_jid, devices_ids):
334 """Retrieve public bundles of an entity devices
335
336 @param entity_jid(jid.JID): bare jid of entity
337 @param devices_id(iterable[int]): ids of the devices bundles to retrieve
338 @return (dict[int, ExtendedPublicBundle]): bundles collection
339 key is device_id
340 value is parsed bundle
341 """
342 assert not entity_jid.resource
343 bundles = {}
344 for device_id in devices_ids:
345 node = NS_OMEMO_BUNDLE.format(device_id=device_id)
346 try:
347 items, metadata = yield self._p.getItems(client, entity_jid, node)
348 except Exception as e:
349 log.warning(_(u"Can't get bundle for device {device_id}: {reason}")
350 .format(device_id=device_id, reason=e))
351 continue
352 if not items:
353 log.warning(_(u"no item found in node {node}, can't get public bundle "
354 u"for device {device_id}").format(node=node,
355 device_id=device_id))
356 continue
357 if len(items) > 1:
358 log.warning(_(u"more than one item found in {node},"
359 u"this is not expected").format(node=node))
360 item = items[0]
361 try:
362 bundle_elt = next(item.elements(NS_OMEMO, 'bundle'))
363 signedPreKeyPublic_elt = next(bundle_elt.elements(
364 NS_OMEMO, 'signedPreKeyPublic'))
365 signedPreKeySignature_elt = next(bundle_elt.elements(
366 NS_OMEMO, 'signedPreKeySignature'))
367 identityKey_elt = next(bundle_elt.elements(
368 NS_OMEMO, 'identityKey'))
369 prekeys_elt = next(bundle_elt.elements(
370 NS_OMEMO, 'prekeys'))
371 except StopIteration:
372 log.warning(_(u"invalid bundle for device {device_id}, ignoring").format(
373 device_id=device_id))
374 continue
375
376 try:
377 spkPublic = base64.b64decode(unicode(signedPreKeyPublic_elt))
378 spkSignature = base64.b64decode(
379 unicode(signedPreKeySignature_elt))
380
381 identityKey = base64.b64decode(unicode(identityKey_elt))
382 spk = {
383 "key": wireformat.decodePublicKey(spkPublic),
384 "id": int(signedPreKeyPublic_elt['signedPreKeyId'])
385 }
386 ik = wireformat.decodePublicKey(identityKey)
387 otpks = []
388 for preKeyPublic_elt in prekeys_elt.elements(NS_OMEMO, 'preKeyPublic'):
389 preKeyPublic = base64.b64decode(unicode(preKeyPublic_elt))
390 otpk = {
391 "key": wireformat.decodePublicKey(preKeyPublic),
392 "id": int(preKeyPublic_elt['preKeyId'])
393 }
394 otpks.append(otpk)
395
396 except Exception as e:
397 log.warning(_(u"error while decoding key for device {devide_id}: {msg}")
398 .format(device_id=device_id, msg=e))
399 continue
400
401 bundles[device_id] = ExtendedPublicBundle(ik, spk, spkSignature, otpks)
402
403 defer.returnValue(bundles)
404
405 def setBundleEb(self, failure_):
406 log.warning(_(u"Can't set bundle: {reason}").format(reason=failure_))
407
408 def setBundle(self, client, bundle, device_id):
409 """Set public bundle for this device.
410
411 @param bundle(ExtendedPublicBundle): bundle to publish
412 """
413 log.debug(_(u"updating bundle for {device_id}").format(device_id=device_id))
414 bundle_elt = domish.Element((NS_OMEMO, 'bundle'))
415 signedPreKeyPublic_elt = bundle_elt.addElement(
416 "signedPreKeyPublic",
417 content=b64enc(wireformat.encodePublicKey(bundle.spk['key'])))
418 signedPreKeyPublic_elt['signedPreKeyId'] = unicode(bundle.spk['id'])
419
420 bundle_elt.addElement(
421 "signedPreKeySignature",
422 content=b64enc(bundle.spk_signature))
423
424 bundle_elt.addElement(
425 "identityKey",
426 content=b64enc(wireformat.encodePublicKey(bundle.ik)))
427
428 prekeys_elt = bundle_elt.addElement('prekeys')
429 for otpk in bundle.otpks:
430 preKeyPublic_elt = prekeys_elt.addElement(
431 'preKeyPublic',
432 content=b64enc(wireformat.encodePublicKey(otpk["key"])))
433 preKeyPublic_elt['preKeyId'] = unicode(otpk['id'])
434
435 node = NS_OMEMO_BUNDLE.format(device_id=device_id)
436 d = self._p.sendItem(client, None, node, bundle_elt, item_id=self._p.ID_SINGLETON)
437 d.addErrback(self.setBundleEb)
438 return d
439
440 ## triggers
441
442 @defer.inlineCallbacks
443 def encryptMessage(self, client, entity_bare_jid, message):
444 omemo_session = client._xep_0384_session
445 devices = yield self.getDevices(client, entity_bare_jid)
446 omemo_session.newDeviceList(devices, entity_bare_jid)
447 bundles = yield self.getBundles(client, entity_bare_jid, devices)
448 encrypted = yield omemo_session.encryptMessage(
449 entity_bare_jid,
450 message,
451 {entity_bare_jid: bundles})
452 defer.returnValue(encrypted)
453
454 @defer.inlineCallbacks
455 def _messageReceivedTrigger(self, client, message_elt, post_treat):
456 if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
457 defer.returnValue(True)
458 try:
459 encrypted_elt = next(message_elt.elements(NS_OMEMO, u"encrypted"))
460 except StopIteration:
461 # no OMEMO message here
462 defer.returnValue(True)
463
464 # we have an encrypted message let's decrypt it
465 # from_jid = jid.JID(message_elt['from'])
466 omemo_session = client._xep_0384_session
467 device_id = client._xep_0384_device_id
468 try:
469 header_elt = next(encrypted_elt.elements(NS_OMEMO, u'header'))
470 iv_elt = next(header_elt.elements(NS_OMEMO, u'iv'))
471 except StopIteration:
472 log.warning(_(u"Invalid OMEMO encrypted stanza, ignoring: {xml}")
473 .format(xml=message_elt.toXml()))
474 defer.returnValue(False)
475 try:
476 key_elt = next((e for e in header_elt.elements(NS_OMEMO, u'key')
477 if int(e[u'rid']) == device_id))
478 except StopIteration:
479 log.warning(_(u"This OMEMO encrypted stanza has not been encrypted"
480 u"for our device ({device_id}): {xml}").format(
481 device_id=device_id, xml=encrypted_elt.toXml()))
482 defer.returnValue(False)
483 except ValueError as e:
484 log.warning(_(u"Invalid recipient ID: {msg}".format(msg=e)))
485 defer.returnValue(False)
486 is_pre_key = C.bool(key_elt.getAttribute('prekey', 'false'))
487 payload_elt = next(encrypted_elt.elements(NS_OMEMO, u'payload'), None)
488
489 try:
490 cipher, plaintext = yield omemo_session.decryptMessage(
491 bare_jid=client.jid.userhostJID(),
492 device=device_id,
493 iv=base64.b64decode(bytes(iv_elt)),
494 message=base64.b64decode(bytes(key_elt)),
495 is_pre_key_message=is_pre_key,
496 payload=base64.b64decode(bytes(payload_elt))
497 if payload_elt is not None else None,
498 from_storage=False
499 )
500 except Exception as e:
501 log.error(_(u"Can't decrypt message: {reason}\n{xml}").format(
502 reason=e, xml=message_elt.toXml()))
503 defer.returnValue(False)
504 if omemo_session.state.changed:
505 bundle = omemo_session.state.getPublicBundle()
506 # we don't wait for the Deferred (i.e. no yield) on purpose
507 # there is no need to block the whole message workflow while
508 # updating the bundle
509 self.setBundle(client, bundle, device_id)
510
511 message_elt.children.remove(encrypted_elt)
512 if plaintext:
513 message_elt.addElement("body", content=plaintext.decode('utf-8'))
514 defer.returnValue(True)
515
516 @defer.inlineCallbacks
517 def _sendMessageDataTrigger(self, client, mess_data):
518 encryption = mess_data.get(C.MESS_KEY_ENCRYPTION)
519 if encryption is None or encryption['plugin'].namespace != NS_OMEMO:
520 return
521 message_elt = mess_data["xml"]
522 to_jid = mess_data["to"].userhostJID()
523 log.debug(u"encrypting message")
524 body = None
525 for child in list(message_elt.children):
526 if child.name == "body":
527 # we remove all unencrypted body,
528 # and will only encrypt the first one
529 if body is None:
530 body = child
531 message_elt.children.remove(child)
532 elif child.name == "html":
533 # we don't want any XHTML-IM element
534 message_elt.children.remove(child)
535
536 if body is None:
537 log.warning(u"No message found")
538 return
539
540 encryption_data = yield self.encryptMessage(client, to_jid, unicode(body))
541
542 encrypted_elt = message_elt.addElement((NS_OMEMO, 'encrypted'))
543 header_elt = encrypted_elt.addElement('header')
544 header_elt['sid'] = unicode(encryption_data['sid'])
545 bare_jid_s = to_jid.userhost()
546
547 for message in (m for m in encryption_data['messages']
548 if m['bare_jid'] == bare_jid_s):
549 key_elt = header_elt.addElement(
550 'key',
551 content=b64enc(message['message']))
552 key_elt['rid'] = unicode(message['rid'])
553 if message['pre_key']:
554 key_elt['prekey'] = 'true'
555
556 header_elt.addElement(
557 'iv',
558 content=b64enc(encryption_data['iv']))
559 try:
560 encrypted_elt.addElement(
561 'payload',
562 content=b64enc(encryption_data['payload']))
563 except KeyError:
564 pass