message.py 15.8 KB
Newer Older
1 2 3 4
import os

from application.notification import IObserver, NotificationCenter, NotificationData
from application.python import Null
5
from application.system import makedirs, unlink, openfile, FileExistsError
6

7 8
from otr import OTRTransport
from otr.exceptions import IgnoreMessage, UnencryptedMessage, EncryptedMessageError, OTRError, OTRFinishedError
9
from sipsimple.account import AccountManager
10 11
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.streams import IMediaStream, MediaStreamType, UnknownStreamError
12
from sipsimple.streams.msrp.chat import OTREncryption
13 14 15 16 17 18 19 20 21
from sipsimple.threading import run_in_thread
from sipsimple.threading.green import run_in_green_thread

from zope.interface import implementer

from pgpy import PGPKey, PGPUID, PGPMessage
from pgpy.errors import PGPError, PGPDecryptionError
from pgpy.constants import PubKeyAlgorithm, KeyFlags, HashAlgorithm, SymmetricKeyAlgorithm, CompressionAlgorithm

22
from blink.logging import MessagingTrace as log
23
from blink.util import run_in_gui_thread, UniqueFilenameGenerator
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44


__all__ = ['MessageStream']


@implementer(IMediaStream, IObserver)
class MessageStream(object, metaclass=MediaStreamType):
    type = 'messages'
    priority = 20

    hold_supported = False
    on_hold = False
    on_hold_by_local = False
    on_hold_by_remote = False

    def __init__(self, **kw):
        for keyword in kw:
            self.keyword = kw[keyword]
        self.private_key = None
        self.public_key = None
        self.remote_public_key = None
45
        self.other_private_keys = []
46
        self.encryption = OTREncryption(self)
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
        notification_center = NotificationCenter()
        notification_center.add_observer(self, name='PGPKeysShouldReload')

    @run_in_green_thread
    def initialize(self, session, direction):
        pass

    @run_in_green_thread
    def start(self, local_sdp, remote_sdp, stream_index):
        pass

    def validate_update(self, remote_sdp, stream_index):
        return True

    def update(self, local_sdp, remote_sdp, stream_index):
        pass

    def hold(self):
        pass

    def unhold(self):
        pass

    def reset(self, stream_index):
        pass

    def connect(self):
        pass

    def deactivate(self):
        pass

    def end(self):
        pass

82 83 84 85 86 87 88 89
    @property
    def local_uri(self):
        return None

    @property
    def msrp(self):
        return None

90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
    @property
    def can_encrypt(self):
        return self.private_key is not None and self.remote_public_key is not None

    @property
    def can_decrypt(self):
        return self.private_key is not None

    def _get_private_key(self):
        return self.__dict__['private_key']

    def _set_private_key(self, value):
        self.__dict__['private_key'] = value

    private_key = property(_get_private_key, _set_private_key)

    del _get_private_key, _set_private_key

    def _get_public_key(self):
        return self.__dict__['public_key']

    def _set_public_key(self, value):
        self.__dict__['public_key'] = value

    public_key = property(_get_public_key, _set_public_key)

    del _get_public_key, _set_public_key

    def _get_remote_public_key(self):
        return self.__dict__['remote_public_key']

    def _set_remote_public_key(self, value):
        self.__dict__['remote_public_key'] = value

    remote_public_key = property(_get_remote_public_key, _set_remote_public_key)

    del _get_remote_public_key, _set_remote_public_key

    @classmethod
    def new_from_sdp(cls, session, remote_sdp, stream_index):
        raise UnknownStreamError

    @run_in_thread('file-io')
    def generate_keys(self):
        session = self.blink_session
        log.info(f'-- Generating key for {session.account.uri}')
        private_key = PGPKey.new(PubKeyAlgorithm.RSAEncryptOrSign, 4096)
        uid = PGPUID.new(session.account.display_name, comment='Blink QT client', email=session.account.id)
        private_key.add_uid(uid,
                            usage={KeyFlags.Sign, KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
                            hashes=[HashAlgorithm.SHA512],
                            ciphers=[SymmetricKeyAlgorithm.AES256],
                            compression=[CompressionAlgorithm.Uncompressed])

        settings = SIPSimpleSettings()
        directory = os.path.join(settings.chat.keys_directory.normalized, 'private')
        filename = os.path.join(directory, session.account.id)
        makedirs(directory)

        with open(f'{filename}.privkey', 'wb') as f:
            f.write(str(private_key).encode())

        with open(f'{filename}.pubkey', 'wb') as f:
            f.write(str(private_key.pubkey).encode())

        session.account.sms.private_key = f'{filename}.privkey'
        session.account.sms.public_key = f'{filename}.pubkey'
        session.account.save()
        self._load_pgp_keys()

        notification_center = NotificationCenter()
        notification_center.post_notification('PGPKeysDidGenerate', sender=session, data=NotificationData(private_key=private_key, public_key=private_key.pubkey))

163 164 165 166
    def inject_otr_message(self, data):
        from blink.messages import MessageManager
        MessageManager().send_otr_message(self.blink_session, data)

167 168 169
    def enable_pgp(self):
        self._load_pgp_keys()

170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
    def enable_otr(self):
        self.encryption.start()

    def disable_otr(self):
        self.encryption.stop()

    def check_otr(self, message):
        content = None
        notification_center = NotificationCenter()
        try:
            content = self.encryption.otr_session.handle_input(message.content.encode(), message.content_type)
        except IgnoreMessage:
            return None
        except UnencryptedMessage:
            return message
        except EncryptedMessageError as e:
            log.warning(f'OTR encrypted message error: {e}')
            return None
        except OTRFinishedError:
            log.info('OTR has finished')
            return None
        except OTRError as e:
            log.warning(f'OTR message error: {e}')
            return None
        else:
            content = content.decode() if isinstance(content, bytes) else content

            if content.startswith('?OTR:'):
                notification_center.post_notification('ChatStreamOTRError', sender=self, data=NotificationData(error='OTR message could not be decoded'))
                log.warning('OTR message could not be decoded')
                if self.encryption.active:
                    self.encryption.stop()
                return None

            log.info("Message uses OTR encryption, message is decoded")
            message.is_secure = self.encryption.active
            message.content = content.decode() if isinstance(content, bytes) else content
            return message

209
    def encrypt(self, content, content_type=None):
210
        # print('-- Encrypting message')
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
        if self.encryption.active:
            try:
                encrypted_content = self.encryption.otr_session.handle_output(content.encode(), content_type)
            except OTRError as e:
                log.info("Encryption failed OTR encryption has been disabled by remote party")
                self.encryption.stop()
                raise Exception(f"OTR encryption has been disabled by remote party {e}")
            except OTRFinishedError:
                log.info("OTR encryption has been disabled by remote party")
                self.encryption.stop()
                raise Exception("OTR encryption has been disabled by remote party")
            else:
                if not encrypted_content.startswith(b'?OTR:'):
                    self.encryption.stop()
                    log.info("OTR encryption has been stopped")
                    raise Exception("OTR encryption has been stopped")
                return str(encrypted_content.decode())

229 230 231 232
        pgp_message = PGPMessage.new(content, compression=CompressionAlgorithm.Uncompressed)
        cipher = SymmetricKeyAlgorithm.AES256

        sessionkey = cipher.gen_key()
233 234
        encrypted_content = self.public_key.encrypt(pgp_message, cipher=cipher, sessionkey=sessionkey)
        encrypted_content = self.remote_public_key.encrypt(encrypted_content, cipher=cipher, sessionkey=sessionkey)
235 236 237 238 239 240 241 242
        del sessionkey
        return str(encrypted_content)

    @run_in_thread('pgp')
    def decrypt(self, message):
        session = self.blink_session
        notification_center = NotificationCenter()

243
        if self.private_key is None and len(self.other_private_keys) == 0:
244 245 246 247 248 249
            notification_center.post_notification('PGPMessageDidNotDecrypt', sender=session, data=NotificationData(message=message))

        try:
            msg_id = message.message_id
        except AttributeError:
            msg_id = message.id
250
        log.info(f'Trying to decrypt message {msg_id}')
251 252 253

        try:
            pgpMessage = PGPMessage.from_blob(message.content)
254 255 256 257
        except (ValueError) as e:
            log.warning(f'Decryption failed for {msg_id}, this is not a PGPMessage, error: {e}')
            return

258
        key_list = [(session.account, self.private_key)] if self.private_key is not None else []
259 260 261 262 263 264
        key_list.extend(self.other_private_keys)

        error = None
        for (account, key) in key_list:
            try:
                decrypted_message = key.decrypt(pgpMessage)
265 266
            except (PGPDecryptionError, PGPError) as e:
                error = e
267
                log.debug(f'-- Decryption failed for {msg_id} with account key {account.id}, error: {error}')
268 269 270
                continue
            else:
                message.content = decrypted_message.message.decode() if isinstance(decrypted_message.message, bytearray) else decrypted_message.message
271
                log.info(f'Message decrypted: {msg_id}')
272 273 274 275 276
                notification_center.post_notification('PGPMessageDidDecrypt', sender=session, data=NotificationData(message=message, account=account))
                return

        log.warning(f'-- Decryption failed for {msg_id}, error: {error}')
        notification_center.post_notification('PGPMessageDidNotDecrypt', sender=session, data=NotificationData(message=message, error=error))
277

278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
    @run_in_thread('pgp')
    def encrypt_file(self, filename, transfer_session):
        session = self.blink_session
        notification_center = NotificationCenter()
        pgp_message = PGPMessage.new(filename, file=True, compression=CompressionAlgorithm.Uncompressed)
        cipher = SymmetricKeyAlgorithm.AES256

        sessionkey = cipher.gen_key()
        encrypted_content = self.public_key.encrypt(pgp_message, cipher=cipher, sessionkey=sessionkey)
        encrypted_content = self.remote_public_key.encrypt(encrypted_content, cipher=cipher, sessionkey=sessionkey)
        del sessionkey
        notification_center.post_notification('PGPFileDidEncrypt', sender=session, data=NotificationData(filename=f'{filename}.asc', contents=encrypted_content))

    @run_in_thread('pgp')
    def decrypt_file(self, filename, transfer_session):
        session = self.blink_session
        notification_center = NotificationCenter()

        if self.private_key is None and len(self.other_private_keys) == 0:
            notification_center.post_notification('PGPFileDidNotDecrypt', sender=session, data=NotificationData(filename=filename, error="No private keys found"))
            return

        log.info(f'Trying to decrypt file {filename}')

        try:
            pgpMessage = PGPMessage.from_file(filename)
        except (ValueError) as e:
            log.warning(f'Decryption failed for {filename}, this is not a PGP File, error: {e}')
            return

        key_list = [(session.account, self.private_key)] if self.private_key is not None else []
        key_list.extend(self.other_private_keys)

        error = None
        for (account, key) in key_list:
            try:
                decrypted_message = key.decrypt(pgpMessage)
            except (PGPDecryptionError, PGPError) as e:
                error = e
                log.debug(f'-- Decryption failed for {filename} with account key {account.id}, error: {error}')
                continue
            else:
                log.info(f'File decrypted: {decrypted_message.filename}')
                dir = os.path.dirname(filename)
                full_decrypted_filepath = os.path.join(dir, decrypted_message.filename)
                file_contents = decrypted_message.message if isinstance(decrypted_message.message, bytearray) else decrypted_message.message.encode()

                for name in UniqueFilenameGenerator.generate(full_decrypted_filepath):
                    try:
                        openfile(name, 'xb')
                    except FileExistsError:
                        continue
                    else:
                        full_decrypted_filepath = name
                        break

                with open(full_decrypted_filepath, 'wb+') as output_file:
                    output_file.write(file_contents)
                log.info(f'File saved: {full_decrypted_filepath}')
                unlink(filename)

339
                notification_center.post_notification('PGPFileDidDecrypt', sender=session, data=NotificationData(filename=full_decrypted_filepath, account=account))
340 341 342 343 344
                return

        log.warning(f'-- Decryption failed for {filename}, error: {error}')
        notification_center.post_notification('PGPFileDidNotDecrypt', sender=transfer_session, data=NotificationData(filename=filename, error=error))

345 346 347 348 349
    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

350
    def _NH_PGPKeysShouldReload(self, notification):
351 352 353 354 355 356 357 358 359
        if notification.sender is not self.blink_session:
            return

        # print('-- Reload PGP keys in stream')
        session = self.blink_session

        self.remote_public_key = self._load_key(str(session.contact_uri.uri), True)
        self.public_key = self._load_key(str(session.account.id))
        self.private_key = self._load_key(str(session.account.id), public_key=False)
360
        self._load_other_keys(session)
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397

    def _load_key(self, id, remote=False, public_key=True):
        settings = SIPSimpleSettings()
        loaded_key = None
        id = id.replace('/', '_')
        extension = 'pubkey'
        if not public_key:
            extension = 'privkey'

        directory = os.path.join(settings.chat.keys_directory.normalized, 'private')
        if remote:
            directory = settings.chat.keys_directory.normalized

        filename = os.path.join(directory, f'{id}.{extension}')
        if not os.path.exists(filename):
            return loaded_key

        try:
            loaded_key, _ = PGPKey.from_file(filename)
        except Exception as e:
            log.warning(f"Can't load PGP key: {str(e)}")

        return loaded_key

    def _load_pgp_keys(self):
        # print('-- Load PGP keys in stream')
        session = self.blink_session

        if self.remote_public_key is None:
            self.remote_public_key = self._load_key(str(session.contact_uri.uri), True)

        if self.public_key is None:
            self.public_key = self._load_key(str(session.account.id))

        if self.private_key is None:
            self.private_key = self._load_key(str(session.account.id), public_key=False)

398
        if None not in [self.public_key, self.private_key]:
399 400
            notification_center = NotificationCenter()
            notification_center.post_notification('MessageStreamPGPKeysDidLoad', sender=self)
401 402 403 404 405 406 407 408
        self._load_other_keys(session)

    def _load_other_keys(self, session):
        account_manager = AccountManager()
        for account in (account for account in account_manager.iter_accounts() if account is not session.account and account.enabled):
            loaded_key = self._load_key(str(account.id), public_key=False)
            if loaded_key is None:
                continue
409
            self.other_private_keys.append((account, loaded_key))
410 411 412


OTRTransport.register(MessageStream)