2018-05-11 01:51:37 +00:00
|
|
|
"""
|
|
|
|
Slixmpp: The Slick XMPP Library
|
|
|
|
Copyright (C) 2018 Maxime “pep” Buquet <pep@bouah.net>
|
|
|
|
This file is part of Slixmpp.
|
|
|
|
|
|
|
|
See the file LICENSE for copying permission.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
2018-11-25 22:17:47 +00:00
|
|
|
from typing import Dict, List, Union
|
2018-11-18 16:48:16 +00:00
|
|
|
|
2018-11-18 11:00:25 +00:00
|
|
|
import os
|
|
|
|
import json
|
2018-05-14 09:16:54 +00:00
|
|
|
import base64
|
2018-05-14 10:16:14 +00:00
|
|
|
import asyncio
|
2018-05-11 01:51:37 +00:00
|
|
|
from slixmpp.plugins.xep_0384.stanza import OMEMO_BASE_NS
|
2018-05-14 10:21:27 +00:00
|
|
|
from slixmpp.plugins.xep_0384.stanza import OMEMO_DEVICES_NS, OMEMO_BUNDLES_NS
|
2018-11-25 22:20:05 +00:00
|
|
|
from slixmpp.plugins.xep_0384.stanza import Bundle, Devices, Device, Encrypted, Key, PreKeyPublic
|
2018-05-11 01:51:37 +00:00
|
|
|
from slixmpp.plugins.base import BasePlugin, register_plugin
|
2018-05-23 21:17:42 +00:00
|
|
|
from slixmpp.exceptions import IqError
|
2018-11-19 21:04:31 +00:00
|
|
|
from slixmpp.stanza import Message, Iq
|
2018-11-25 22:17:47 +00:00
|
|
|
from slixmpp.jid import JID
|
2018-05-11 01:51:37 +00:00
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
2018-05-13 11:12:05 +00:00
|
|
|
HAS_OMEMO = True
|
|
|
|
try:
|
2018-11-25 22:20:05 +00:00
|
|
|
from omemo.exceptions import MissingBundleException
|
|
|
|
from omemo import SessionManager, ExtendedPublicBundle
|
2018-11-18 11:00:25 +00:00
|
|
|
from omemo.util import generateDeviceID
|
2018-11-25 22:20:05 +00:00
|
|
|
from omemo.backends import Backend
|
2018-11-18 11:00:25 +00:00
|
|
|
from omemo_backend_signal import BACKEND as SignalBackend
|
2018-11-19 00:37:07 +00:00
|
|
|
from slixmpp.plugins.xep_0384.storage import SyncFileStorage
|
2018-11-18 11:00:25 +00:00
|
|
|
from slixmpp.plugins.xep_0384.otpkpolicy import KeepingOTPKPolicy
|
2018-11-19 21:04:31 +00:00
|
|
|
except (ImportError,):
|
2018-05-13 11:12:05 +00:00
|
|
|
HAS_OMEMO = False
|
2018-05-11 01:51:37 +00:00
|
|
|
|
2018-05-23 20:13:06 +00:00
|
|
|
TRUE_VALUES = {True, 'true', '1'}
|
|
|
|
|
2018-05-11 01:51:37 +00:00
|
|
|
|
2018-11-19 21:04:31 +00:00
|
|
|
def b64enc(data: bytes) -> str:
|
2018-05-14 09:16:54 +00:00
|
|
|
return base64.b64encode(bytes(bytearray(data))).decode('ASCII')
|
|
|
|
|
|
|
|
|
2018-11-19 21:04:31 +00:00
|
|
|
def b64dec(data: str) -> bytes:
|
2018-11-18 23:22:50 +00:00
|
|
|
return base64.b64decode(data)
|
2018-05-14 09:16:54 +00:00
|
|
|
|
|
|
|
|
2018-11-19 21:04:31 +00:00
|
|
|
def _load_device_id(cache_dir: str) -> int:
|
|
|
|
filepath = os.path.join(cache_dir, 'device_id.json')
|
|
|
|
# Try reading file first, decoding, and if file was empty generate
|
|
|
|
# new DeviceID
|
|
|
|
try:
|
|
|
|
with open(filepath, 'r') as f:
|
|
|
|
did = json.load(f)
|
|
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
|
|
did = generateDeviceID()
|
|
|
|
with open(filepath, 'w') as f:
|
|
|
|
json.dump(did, f)
|
|
|
|
|
|
|
|
return did
|
|
|
|
|
|
|
|
|
2018-11-18 11:00:25 +00:00
|
|
|
# XXX: This should probably be moved in plugins/base.py?
|
|
|
|
class PluginCouldNotLoad(Exception): pass
|
|
|
|
|
|
|
|
|
2018-11-19 21:10:41 +00:00
|
|
|
# Generic exception
|
|
|
|
class XEP0384(Exception): pass
|
|
|
|
|
|
|
|
|
|
|
|
class MissingOwnKey(XEP0384): pass
|
|
|
|
|
|
|
|
|
2018-05-11 01:51:37 +00:00
|
|
|
class XEP_0384(BasePlugin):
|
|
|
|
|
|
|
|
"""
|
|
|
|
XEP-0384: OMEMO
|
|
|
|
"""
|
|
|
|
|
|
|
|
name = 'xep_0384'
|
|
|
|
description = 'XEP-0384 OMEMO'
|
|
|
|
dependencies = {'xep_0163'}
|
2018-05-14 10:07:32 +00:00
|
|
|
default_config = {
|
|
|
|
'cache_dir': None,
|
|
|
|
}
|
|
|
|
|
2018-05-13 11:12:05 +00:00
|
|
|
backend_loaded = HAS_OMEMO
|
2018-05-11 01:51:37 +00:00
|
|
|
|
|
|
|
def plugin_init(self):
|
2018-05-13 11:12:05 +00:00
|
|
|
if not self.backend_loaded:
|
2018-11-18 11:00:25 +00:00
|
|
|
log.info("xep_0384 cannot be loaded as the backend omemo library "
|
2018-11-19 21:04:31 +00:00
|
|
|
"is not available")
|
2018-05-13 11:12:05 +00:00
|
|
|
return
|
|
|
|
|
2018-11-19 00:37:07 +00:00
|
|
|
storage = SyncFileStorage(self.cache_dir)
|
2018-11-18 11:00:25 +00:00
|
|
|
otpkpolicy = KeepingOTPKPolicy()
|
2018-11-22 19:53:56 +00:00
|
|
|
self._omemo_backend = SignalBackend
|
2018-11-18 11:00:25 +00:00
|
|
|
bare_jid = self.xmpp.boundjid.bare
|
2018-11-19 21:04:31 +00:00
|
|
|
self._device_id = _load_device_id(self.cache_dir)
|
2018-11-18 11:00:25 +00:00
|
|
|
|
|
|
|
try:
|
2018-11-19 00:37:07 +00:00
|
|
|
self._omemo = SessionManager.create(
|
|
|
|
storage,
|
|
|
|
otpkpolicy,
|
2018-11-22 19:53:56 +00:00
|
|
|
self._omemo_backend,
|
2018-11-19 00:37:07 +00:00
|
|
|
bare_jid,
|
|
|
|
self._device_id,
|
|
|
|
)
|
2018-11-18 11:00:25 +00:00
|
|
|
except:
|
2018-11-18 17:04:58 +00:00
|
|
|
log.error("Couldn't load the OMEMO object; ¯\\_(ツ)_/¯")
|
2018-11-18 11:00:25 +00:00
|
|
|
raise PluginCouldNotLoad
|
2018-05-11 01:51:37 +00:00
|
|
|
|
2018-11-18 16:48:16 +00:00
|
|
|
self.xmpp.add_event_handler('pubsub_publish', self._receive_device_list)
|
|
|
|
asyncio.ensure_future(self._set_device_list())
|
2018-11-18 22:45:31 +00:00
|
|
|
asyncio.ensure_future(self._publish_bundle())
|
2018-11-18 16:48:16 +00:00
|
|
|
|
2018-05-11 01:51:37 +00:00
|
|
|
def plugin_end(self):
|
2018-05-13 11:12:05 +00:00
|
|
|
if not self.backend_loaded:
|
|
|
|
return
|
|
|
|
|
2018-11-18 16:48:16 +00:00
|
|
|
self.xmpp.del_event_handler('pubsub_publish', self._receive_device_list)
|
2018-05-11 01:51:37 +00:00
|
|
|
self.xmpp['xep_0163'].remove_interest(OMEMO_DEVICES_NS)
|
|
|
|
|
|
|
|
def session_bind(self, _jid):
|
|
|
|
self.xmpp['xep_0163'].add_interest(OMEMO_DEVICES_NS)
|
|
|
|
|
2018-11-19 21:04:31 +00:00
|
|
|
def my_device_id(self) -> int:
|
2018-05-23 21:19:38 +00:00
|
|
|
return self._device_id
|
|
|
|
|
2018-11-19 21:04:31 +00:00
|
|
|
def _generate_bundle_iq(self) -> Iq:
|
2018-11-22 19:53:56 +00:00
|
|
|
bundle = self._omemo.public_bundle.serialize(self._omemo_backend)
|
2018-05-14 10:21:27 +00:00
|
|
|
|
|
|
|
iq = self.xmpp.Iq(stype='set')
|
|
|
|
publish = iq['pubsub']['publish']
|
|
|
|
publish['node'] = '%s:%d' % (OMEMO_BUNDLES_NS, self._device_id)
|
|
|
|
payload = publish['item']['bundle']
|
2018-11-22 19:53:56 +00:00
|
|
|
signedPreKeyPublic = b64enc(bundle['spk']['key'])
|
2018-05-14 10:21:27 +00:00
|
|
|
payload['signedPreKeyPublic']['value'] = signedPreKeyPublic
|
2018-11-22 19:53:56 +00:00
|
|
|
payload['signedPreKeyPublic']['signedPreKeyId'] = str(bundle['spk']['id'])
|
2018-05-14 10:21:27 +00:00
|
|
|
payload['signedPreKeySignature']['value'] = b64enc(
|
2018-11-22 19:53:56 +00:00
|
|
|
bundle['spk_signature']
|
2018-05-14 10:21:27 +00:00
|
|
|
)
|
2018-11-22 19:53:56 +00:00
|
|
|
identityKey = b64enc(bundle['ik'])
|
2018-05-14 10:21:27 +00:00
|
|
|
payload['identityKey']['value'] = identityKey
|
|
|
|
|
|
|
|
prekeys = []
|
2018-11-22 19:53:56 +00:00
|
|
|
for otpk in bundle['otpks']:
|
2018-05-14 10:21:27 +00:00
|
|
|
prekey = PreKeyPublic()
|
|
|
|
prekey['preKeyId'] = str(otpk['id'])
|
2018-11-22 19:53:56 +00:00
|
|
|
prekey['value'] = b64enc(otpk['key'])
|
2018-05-14 10:21:27 +00:00
|
|
|
prekeys.append(prekey)
|
|
|
|
payload['prekeys'] = prekeys
|
|
|
|
|
|
|
|
return iq
|
|
|
|
|
2018-11-19 21:04:31 +00:00
|
|
|
async def _publish_bundle(self) -> None:
|
2018-11-18 22:45:31 +00:00
|
|
|
if self._omemo.republish_bundle:
|
|
|
|
iq = self._generate_bundle_iq()
|
|
|
|
await iq.send()
|
2018-05-14 10:21:27 +00:00
|
|
|
|
2018-11-25 22:20:05 +00:00
|
|
|
async def _fetch_bundle(self, jid: str, device_id: int) -> Union[None, ExtendedPublicBundle]:
|
|
|
|
node = '%s:%d' % (OMEMO_BUNDLES_NS, device_id)
|
|
|
|
iq = await self.xmpp['xep_0060'].get_items(jid, node)
|
|
|
|
bundle = iq['pubsub']['items']['item']['bundle']
|
|
|
|
|
|
|
|
return self._parse_bundle(self._omemo_backend, bundle)
|
|
|
|
|
|
|
|
def _parse_bundle(self, backend: Backend, bundle: Bundle) -> ExtendedPublicBundle:
|
|
|
|
ik = b64dec(bundle['identityKey']['value'].strip())
|
|
|
|
spk = {
|
|
|
|
'id': int(bundle['signedPreKeyPublic']['signedPreKeyId']),
|
|
|
|
'key': b64dec(bundle['signedPreKeyPublic']['value'].strip()),
|
|
|
|
}
|
|
|
|
spk_signature = b64dec(bundle['signedPreKeySignature']['value'].strip())
|
|
|
|
|
|
|
|
otpks = []
|
|
|
|
for prekey in bundle['prekeys']:
|
|
|
|
otpks.append({
|
|
|
|
'id': int(prekey['preKeyId']),
|
|
|
|
'key': b64dec(prekey['value'].strip()),
|
|
|
|
})
|
|
|
|
|
|
|
|
return ExtendedPublicBundle.parse(backend, ik, spk, spk_signature, otpks)
|
|
|
|
|
2018-11-19 21:04:31 +00:00
|
|
|
def _store_device_ids(self, jid: str, items) -> None:
|
|
|
|
device_ids = [] # type: List[int]
|
2018-05-14 10:16:14 +00:00
|
|
|
for item in items:
|
|
|
|
device_ids = [int(d['id']) for d in item['devices']]
|
|
|
|
|
|
|
|
# XXX: There should only be one item so this is fine, but slixmpp
|
|
|
|
# loops forever otherwise. ???
|
|
|
|
break
|
2018-11-19 00:37:07 +00:00
|
|
|
return self._omemo.newDeviceList(device_ids, str(jid))
|
2018-05-14 10:16:14 +00:00
|
|
|
|
2018-11-19 21:04:31 +00:00
|
|
|
def _receive_device_list(self, msg: Message) -> None:
|
2018-05-11 01:51:37 +00:00
|
|
|
if msg['pubsub_event']['items']['node'] != OMEMO_DEVICES_NS:
|
|
|
|
return
|
|
|
|
|
2018-05-14 10:12:04 +00:00
|
|
|
jid = msg['from'].bare
|
2018-05-11 01:51:37 +00:00
|
|
|
items = msg['pubsub_event']['items']
|
2018-11-19 00:37:07 +00:00
|
|
|
self._store_device_ids(jid, items)
|
2018-05-11 01:51:37 +00:00
|
|
|
|
2018-11-19 00:37:07 +00:00
|
|
|
device_ids = self.get_device_list(jid)
|
2018-11-18 22:53:31 +00:00
|
|
|
active_devices = device_ids['active']
|
2018-11-18 16:48:16 +00:00
|
|
|
|
2018-05-14 10:16:14 +00:00
|
|
|
if jid == self.xmpp.boundjid.bare and \
|
2018-11-18 22:53:31 +00:00
|
|
|
self._device_id not in active_devices:
|
2018-05-14 10:16:14 +00:00
|
|
|
asyncio.ensure_future(self._set_device_list())
|
|
|
|
|
2018-11-19 21:04:31 +00:00
|
|
|
async def _set_device_list(self) -> None:
|
2018-05-14 10:16:14 +00:00
|
|
|
jid = self.xmpp.boundjid.bare
|
2018-05-23 21:17:42 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
iq = await self.xmpp['xep_0060'].get_items(
|
|
|
|
self.xmpp.boundjid.bare, OMEMO_DEVICES_NS,
|
|
|
|
)
|
|
|
|
items = iq['pubsub']['items']
|
2018-11-19 00:37:07 +00:00
|
|
|
self._store_device_ids(jid, items)
|
2018-05-23 21:17:42 +00:00
|
|
|
except IqError as iq_err:
|
|
|
|
if iq_err.condition == "item-not-found":
|
2018-11-19 00:37:07 +00:00
|
|
|
self._store_device_ids(jid, [])
|
2018-05-23 21:17:42 +00:00
|
|
|
else:
|
|
|
|
return # XXX: Handle this!
|
2018-05-14 10:16:14 +00:00
|
|
|
|
2018-11-19 00:37:07 +00:00
|
|
|
device_ids = self.get_device_list(jid)
|
2018-11-18 16:48:16 +00:00
|
|
|
|
2018-05-14 10:16:14 +00:00
|
|
|
# Verify that this device in the list and set it if necessary
|
2018-11-18 16:48:16 +00:00
|
|
|
if self._device_id in device_ids:
|
2018-05-11 01:51:37 +00:00
|
|
|
return
|
|
|
|
|
2018-11-18 22:53:31 +00:00
|
|
|
device_ids['active'].add(self._device_id)
|
2018-05-14 10:16:14 +00:00
|
|
|
|
|
|
|
devices = []
|
2018-11-18 22:53:31 +00:00
|
|
|
for i in device_ids['active']:
|
2018-05-14 10:16:14 +00:00
|
|
|
d = Device()
|
|
|
|
d['id'] = str(i)
|
|
|
|
devices.append(d)
|
|
|
|
payload = Devices()
|
|
|
|
payload['devices'] = devices
|
|
|
|
|
|
|
|
await self.xmpp['xep_0060'].publish(
|
|
|
|
jid, OMEMO_DEVICES_NS, payload=payload,
|
|
|
|
)
|
2018-05-11 01:51:37 +00:00
|
|
|
|
2018-11-19 21:04:31 +00:00
|
|
|
def get_device_list(self, jid: str) -> List[str]:
|
2018-11-18 16:48:16 +00:00
|
|
|
# XXX: Maybe someday worry about inactive devices somehow
|
2018-11-19 00:37:07 +00:00
|
|
|
return self._omemo.getDevices(jid)
|
2018-11-18 16:48:16 +00:00
|
|
|
|
2018-11-19 21:04:31 +00:00
|
|
|
def is_encrypted(self, msg: Message) -> bool:
|
2018-05-14 10:24:13 +00:00
|
|
|
return msg.xml.find('{%s}encrypted' % OMEMO_BASE_NS) is not None
|
|
|
|
|
2018-11-19 21:04:31 +00:00
|
|
|
def decrypt_message(self, msg: Message) -> Union[None, str]:
|
2018-05-23 20:13:06 +00:00
|
|
|
header = msg['omemo_encrypted']['header']
|
2018-11-18 23:22:50 +00:00
|
|
|
payload = b64dec(msg['omemo_encrypted']['payload']['value'])
|
2018-05-23 20:13:06 +00:00
|
|
|
|
2018-05-23 20:32:29 +00:00
|
|
|
jid = msg['from'].bare
|
2018-05-23 20:13:06 +00:00
|
|
|
sid = header['sid']
|
|
|
|
|
|
|
|
key = header.xml.find("{%s}key[@rid='%s']" % (
|
|
|
|
OMEMO_BASE_NS, self._device_id))
|
|
|
|
if key is None:
|
2018-11-19 21:10:41 +00:00
|
|
|
raise MissingOwnKey("Encrypted message is not for us")
|
2018-05-23 20:13:06 +00:00
|
|
|
|
|
|
|
key = Key(key)
|
2018-11-18 22:54:30 +00:00
|
|
|
isPrekeyMessage = key['prekey'] in TRUE_VALUES
|
2018-11-18 23:22:50 +00:00
|
|
|
message = b64dec(key['value'])
|
|
|
|
iv = b64dec(header['iv']['value'])
|
2018-05-23 20:13:06 +00:00
|
|
|
|
2018-11-19 00:39:56 +00:00
|
|
|
# XXX: 'cipher' is part of KeyTransportMessages and is used when no payload
|
|
|
|
# is passed. We do not implement this yet.
|
|
|
|
_cipher, body = self._omemo.decryptMessage(
|
2018-11-18 22:54:30 +00:00
|
|
|
jid,
|
|
|
|
sid,
|
|
|
|
iv,
|
|
|
|
message,
|
|
|
|
isPrekeyMessage,
|
|
|
|
payload,
|
|
|
|
)
|
2018-11-19 00:39:56 +00:00
|
|
|
return body
|
2018-05-23 20:13:06 +00:00
|
|
|
|
2018-11-25 22:17:47 +00:00
|
|
|
async def encrypt_message(self, plaintext: str, recipients: List[JID]) -> Encrypted:
|
|
|
|
"""
|
|
|
|
Returns an encrypted payload to be placed into a message.
|
|
|
|
|
|
|
|
The API for getting an encrypted payload consists of trying first
|
|
|
|
and fixing errors progressively. The actual sending happens once the
|
|
|
|
application (us) thinks we're good to go.
|
|
|
|
"""
|
|
|
|
|
|
|
|
recipients = [jid.bare for jid in recipients]
|
|
|
|
bundles = {} # type: Dict[str, Dict[str, ExtendedPublicBundle]]
|
|
|
|
|
|
|
|
while True:
|
|
|
|
errors = []
|
|
|
|
|
|
|
|
self._omemo.encryptMessage(
|
|
|
|
recipients,
|
|
|
|
plaintext.encode('utf-8'),
|
|
|
|
bundles,
|
|
|
|
callback=lambda *args: errors.append(args),
|
|
|
|
always_trust=True,
|
|
|
|
dry_run=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
if not errors:
|
|
|
|
break
|
|
|
|
|
|
|
|
for (exn, key, val) in errors:
|
2018-11-25 22:20:05 +00:00
|
|
|
if isinstance(exn, MissingBundleException):
|
|
|
|
bundle = await self._fetch_bundle(key, val)
|
|
|
|
if bundle is not None:
|
|
|
|
devices = bundles.setdefault(key, {})
|
|
|
|
devices[val] = bundle
|
2018-11-25 22:17:47 +00:00
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
# Attempt encryption
|
|
|
|
payload = Encrypted()
|
|
|
|
payload['omemo_encrypted'] = self._omemo.encryptMessage(
|
|
|
|
recipients,
|
|
|
|
plaintext.encode('utf-8'),
|
|
|
|
bundles,
|
|
|
|
always_trust=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
return payload
|
2018-05-14 10:24:13 +00:00
|
|
|
|
2018-05-11 01:51:37 +00:00
|
|
|
register_plugin(XEP_0384)
|