From 54b724c28b1048d8508f1523693d4f7df340fc13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Thu, 17 Mar 2022 20:56:03 +0100 Subject: examples/http_upload: Add some typing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- examples/http_upload.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/examples/http_upload.py b/examples/http_upload.py index a926fd47..cac8fc7e 100755 --- a/examples/http_upload.py +++ b/examples/http_upload.py @@ -5,11 +5,15 @@ # This file is part of Slixmpp. # See the file LICENSE for copying permission. +from typing import Optional + import logging +from pathlib import Path from getpass import getpass from argparse import ArgumentParser import slixmpp +from slixmpp import JID from slixmpp.exceptions import IqTimeout log = logging.getLogger(__name__) @@ -21,7 +25,14 @@ class HttpUpload(slixmpp.ClientXMPP): A basic client asking an entity if they confirm the access to an HTTP URL. """ - def __init__(self, jid, password, recipient, filename, domain=None): + def __init__( + self, + jid: JID, + password: str, + recipient: JID, + filename: Path, + domain: Optional[JID] = None, + ): slixmpp.ClientXMPP.__init__(self, jid, password) self.recipient = recipient @@ -86,11 +97,21 @@ if __name__ == '__main__': format='%(levelname)-8s %(message)s') if args.jid is None: - args.jid = input("Username: ") + args.jid = JID(input("Username: ")) if args.password is None: args.password = getpass("Password: ") - xmpp = HttpUpload(args.jid, args.password, args.recipient, args.file, args.domain) + domain = args.domain + if domain is not None: + domain = JID(domain) + + xmpp = HttpUpload( + jid=args.jid, + password=args.password, + recipient=JID(args.recipient), + filename=Path(args.file), + domain=domain, + ) xmpp.register_plugin('xep_0066') xmpp.register_plugin('xep_0071') xmpp.register_plugin('xep_0128') -- cgit v1.2.3 From 3c08f471cff6df37ff6ac447b770546e94ef4815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Thu, 17 Mar 2022 20:59:05 +0100 Subject: xep_0363: change filename to Path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This shouldn't break anything as I'm not using Path specific APIs Signed-off-by: Maxime “pep” Buquet --- slixmpp/plugins/xep_0363/http_upload.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/slixmpp/plugins/xep_0363/http_upload.py b/slixmpp/plugins/xep_0363/http_upload.py index bae3ee7d..4a185fb9 100644 --- a/slixmpp/plugins/xep_0363/http_upload.py +++ b/slixmpp/plugins/xep_0363/http_upload.py @@ -14,6 +14,8 @@ from typing import ( IO, ) +from pathlib import Path + from slixmpp import JID, __version__ from slixmpp.stanza import Iq from slixmpp.plugins import BasePlugin @@ -113,7 +115,7 @@ class XEP_0363(BasePlugin): if feature == Request.namespace: return info - def request_slot(self, jid: JID, filename: str, size: int, + def request_slot(self, jid: JID, filename: Path, size: int, content_type: Optional[str] = None, *, ifrom: Optional[JID] = None, **iqkwargs) -> Future: """Request an HTTP upload slot from a service. @@ -125,12 +127,12 @@ class XEP_0363(BasePlugin): """ iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom) request = iq['http_upload_request'] - request['filename'] = filename + request['filename'] = str(filename) request['size'] = str(size) request['content-type'] = content_type or self.default_content_type return iq.send(**iqkwargs) - async def upload_file(self, filename: str, size: Optional[int] = None, + async def upload_file(self, filename: Path, size: Optional[int] = None, content_type: Optional[str] = None, *, input_file: Optional[IO[bytes]]=None, domain: Optional[JID] = None, -- cgit v1.2.3 From bc8af3cc6181248744057f1fb29295508d059d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Thu, 17 Mar 2022 23:06:45 +0100 Subject: xep_0454: new plugin. OMEMO Media Sharing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- slixmpp/plugins/xep_0454/__init__.py | 76 ++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 slixmpp/plugins/xep_0454/__init__.py diff --git a/slixmpp/plugins/xep_0454/__init__.py b/slixmpp/plugins/xep_0454/__init__.py new file mode 100644 index 00000000..a8af6190 --- /dev/null +++ b/slixmpp/plugins/xep_0454/__init__.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# vim:fenc=utf-8 et ts=4 sts=4 sw=4 +# +# Copyright © 2022 Maxime “pep” Buquet +# +# See the LICENSE file for copying permissions. + +""" + XEP-0454: OMEMO Media Sharing +""" + +from typing import IO, Optional, Tuple + +from os import urandom +from pathlib import Path + +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +from slixmpp.plugins import BasePlugin +from slixmpp.plugins.base import register_plugin + + +class InvalidURL(Exception): + """Raised for URLs that either aren't HTTPS or already contain a fragment.""" + + +class XEP_0454(BasePlugin): + """ + XEP-0454: OMEMO Media Sharing + """ + + name = 'xep_0454' + description = 'XEP-0454: OMEMO Media Sharing' + + @classmethod + def encrypt(cls, input_file: Optional[IO[bytes]] = None, filename: Optional[Path] = None) -> Tuple[bytes, str]: + """ + Encrypts file as specified in XEP-0454 for use in file sharing + + :param input_file: Binary file stream on the file. + :param filename: Path to the file to upload. + + One of input_file or filename must be specified. If both are + passed, input_file will be used and filename ignored. + """ + if input_file is None and filename is None: + raise ValueError('Specify either filename or input_file parameter') + + aes_gcm_iv = urandom(12) + aes_gcm_key = urandom(32) + + aes_gcm = Cipher( + algorithms.AES(aes_gcm_key), + modes.GCM(aes_gcm_iv), + ).encryptor() + + # TODO: use streaming API from CipherContext + if input_file: + plain = input_file.read() + else: + with filename.open(mode='rb') as file: + plain = file.read() + + payload = aes_gcm.update(plain) + aes_gcm.tag + aes_gcm.finalize() + fragment = aes_gcm_iv.hex() + aes_gcm_key.hex() + return (payload, fragment) + + @classmethod + def format_url(cls, url: str, fragment: str) -> str: + """Helper to format a HTTPS URL to an AESGCM URI""" + if not url.startswith('https://') or url.find('#') != -1: + raise InvalidURL + url = 'aesgcm://' + url.removeprefix('https://') + '#' + fragment + +register_plugin(XEP_0454) -- cgit v1.2.3 From 51644e301bdcdc373871e3a6e282a3df5c6d61d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Fri, 18 Mar 2022 12:11:41 +0100 Subject: xep_0454: Add wrapper to xep_363's upload_file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- slixmpp/plugins/xep_0454/__init__.py | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/slixmpp/plugins/xep_0454/__init__.py b/slixmpp/plugins/xep_0454/__init__.py index a8af6190..2d3cb8f6 100644 --- a/slixmpp/plugins/xep_0454/__init__.py +++ b/slixmpp/plugins/xep_0454/__init__.py @@ -14,6 +14,7 @@ from typing import IO, Optional, Tuple from os import urandom from pathlib import Path +from io import BytesIO, SEEK_END from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -32,6 +33,7 @@ class XEP_0454(BasePlugin): name = 'xep_0454' description = 'XEP-0454: OMEMO Media Sharing' + dependencies = {'xep_0363'} @classmethod def encrypt(cls, input_file: Optional[IO[bytes]] = None, filename: Optional[Path] = None) -> Tuple[bytes, str]: @@ -73,4 +75,44 @@ class XEP_0454(BasePlugin): raise InvalidURL url = 'aesgcm://' + url.removeprefix('https://') + '#' + fragment + async def upload_file( + self, + filename: Path, + _size: Optional[int] = None, + _content_type: Optional[str] = None, + **kwargs, + ) -> str: + """ + Wrapper to xep_0363 (HTTP Upload)'s upload_file method. + + :param input_file: Binary file stream on the file. + :param filename: Path to the file to upload. + + Same as `XEP_0454.encrypt`, one of input_file or filename must be + specified. If both are passed, input_file will be used and + filename ignored. + + Other arguments passed in are passed to the actual + `XEP_0363.upload_file` call. + """ + input_file = kwargs.get('input_file') + payload, fragment = self.encrypt(input_file, filename) + + # Prepare kwargs for upload_file call + filename = urandom(12).hex() # Random filename to hide user-provided path + kwargs['filename'] = filename + + input_enc = BytesIO(payload) + kwargs['input_file'] = input_enc + + # Size must also be overriden if provided + size = input_enc.seek(0, SEEK_END) + input_enc.seek(0) + kwargs['size'] = size + + kwargs['content_type'] = 'application/octet-stream' + + url = await self.xmpp['xep_0363'].upload_file(**kwargs) + return self.format_url(url, fragment) + register_plugin(XEP_0454) -- cgit v1.2.3 From c1aeab328b8c25d67d29ec7587534e7701007858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Fri, 18 Mar 2022 13:25:00 +0100 Subject: xep_0454: use streaming API from CipherContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- slixmpp/plugins/xep_0454/__init__.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/slixmpp/plugins/xep_0454/__init__.py b/slixmpp/plugins/xep_0454/__init__.py index 2d3cb8f6..9e9c3b02 100644 --- a/slixmpp/plugins/xep_0454/__init__.py +++ b/slixmpp/plugins/xep_0454/__init__.py @@ -57,14 +57,18 @@ class XEP_0454(BasePlugin): modes.GCM(aes_gcm_iv), ).encryptor() - # TODO: use streaming API from CipherContext - if input_file: - plain = input_file.read() - else: - with filename.open(mode='rb') as file: - plain = file.read() - - payload = aes_gcm.update(plain) + aes_gcm.tag + aes_gcm.finalize() + if input_file is None: + input_file = open(filename, 'rb') + + payload = b'' + while True: + buf = input_file.read(4096) + if not buf: + break + payload += aes_gcm.update(buf) + + aes_gcm.finalize() + payload += aes_gcm.tag fragment = aes_gcm_iv.hex() + aes_gcm_key.hex() return (payload, fragment) -- cgit v1.2.3 From b52540e49f4b314640086f8f5845924af68150b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Fri, 18 Mar 2022 14:27:26 +0100 Subject: xep_0454: implement decrypt method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- slixmpp/plugins/xep_0454/__init__.py | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/slixmpp/plugins/xep_0454/__init__.py b/slixmpp/plugins/xep_0454/__init__.py index 9e9c3b02..09d41221 100644 --- a/slixmpp/plugins/xep_0454/__init__.py +++ b/slixmpp/plugins/xep_0454/__init__.py @@ -72,6 +72,46 @@ class XEP_0454(BasePlugin): fragment = aes_gcm_iv.hex() + aes_gcm_key.hex() return (payload, fragment) + @classmethod + def decrypt(cls, input_file: IO[bytes], fragment: str) -> bytes: + """ + Decrypts file-like. + + :param input_file: Binary file stream on the file, containing the + tag (16 bytes) at the end. + :param fragment: 88 hex chars string composed of iv (24 chars) + + key (64 chars). + """ + + assert len(fragment) == 88 + aes_gcm_iv = bytes.fromhex(fragment[:24]) + aes_gcm_key = bytes.fromhex(fragment[24:]) + + # Find 16 bytes tag + input_file.seek(-16, SEEK_END) + tag = input_file.read() + + aes_gcm = Cipher( + algorithms.AES(aes_gcm_key), + modes.GCM(aes_gcm_iv, tag), + ).decryptor() + + size = input_file.seek(0, SEEK_END) + input_file.seek(0) + + count = size - 16 + plain = b'' + while count >= 0: + buf = input_file.read(4096) + count -= len(buf) + if count <= 0: + buf += input_file.read() + buf = buf[:-16] + plain += aes_gcm.update(buf) + plain += aes_gcm.finalize() + + return plain + @classmethod def format_url(cls, url: str, fragment: str) -> str: """Helper to format a HTTPS URL to an AESGCM URI""" -- cgit v1.2.3 From 14a6c7801d8ecaabe69a1e28a26fb0755b5ee9dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Fri, 18 Mar 2022 15:15:24 +0100 Subject: tests: XEP-0454 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- tests/test_xep_0454.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/test_xep_0454.py diff --git a/tests/test_xep_0454.py b/tests/test_xep_0454.py new file mode 100644 index 00000000..3d0860a9 --- /dev/null +++ b/tests/test_xep_0454.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# vim:fenc=utf-8 et ts=4 sts=4 sw=4 +# +# Copyright © 2022 Maxime “pep” Buquet +# +# Distributed under terms of the GPLv3+ license. + +""" + Tests for XEP-0454 (OMEMO Media Sharing) plugin. +""" + +import unittest +from io import BytesIO +from slixmpp.test import SlixTest +from slixmpp.plugins.xep_0454 import XEP_0454 + + +class TestMediaSharing(SlixTest): + + def testEncryptDecryptSmall(self): + plain = b'qwertyuiop' + ciphertext, fragment = XEP_0454.encrypt(input_file=BytesIO(plain)) + result = XEP_0454.decrypt(BytesIO(ciphertext), fragment) + + self.assertEqual(plain, result) + + def testEncryptDecrypt(self): + plain = b'a' * 4096 + b'qwertyuiop' + ciphertext, fragment = XEP_0454.encrypt(input_file=BytesIO(plain)) + result = XEP_0454.decrypt(BytesIO(ciphertext), fragment) + + self.assertEqual(plain, result) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestMediaSharing) -- cgit v1.2.3 From 7222ade0dd252380fb158bb9b1ca1fefec3899a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Fri, 18 Mar 2022 16:11:07 +0100 Subject: xep_0454: Ensure format_url returns a str MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- slixmpp/plugins/xep_0454/__init__.py | 2 +- tests/test_xep_0454.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/slixmpp/plugins/xep_0454/__init__.py b/slixmpp/plugins/xep_0454/__init__.py index 09d41221..34db0c79 100644 --- a/slixmpp/plugins/xep_0454/__init__.py +++ b/slixmpp/plugins/xep_0454/__init__.py @@ -117,7 +117,7 @@ class XEP_0454(BasePlugin): """Helper to format a HTTPS URL to an AESGCM URI""" if not url.startswith('https://') or url.find('#') != -1: raise InvalidURL - url = 'aesgcm://' + url.removeprefix('https://') + '#' + fragment + return 'aesgcm://' + url.removeprefix('https://') + '#' + fragment async def upload_file( self, diff --git a/tests/test_xep_0454.py b/tests/test_xep_0454.py index 3d0860a9..8d43c7e3 100644 --- a/tests/test_xep_0454.py +++ b/tests/test_xep_0454.py @@ -32,4 +32,10 @@ class TestMediaSharing(SlixTest): self.assertEqual(plain, result) + def testFormatURL(self): + url = 'https://foo.bar' + fragment = 'a' * 88 + result = XEP_0454.format_url(url, fragment) + self.assertEqual('aesgcm://foo.bar#' + 'a' * 88, result) + suite = unittest.TestLoader().loadTestsFromTestCase(TestMediaSharing) -- cgit v1.2.3 From bde5aaaf3e197f1fb71bbdabbeb597d8e8f9b4a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Fri, 18 Mar 2022 15:22:52 +0100 Subject: examples/http_upload.py: Add --encrypt parameter to send encrypted files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- examples/http_upload.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/examples/http_upload.py b/examples/http_upload.py index cac8fc7e..7f1c44bc 100755 --- a/examples/http_upload.py +++ b/examples/http_upload.py @@ -7,6 +7,7 @@ from typing import Optional +import sys import logging from pathlib import Path from getpass import getpass @@ -32,20 +33,25 @@ class HttpUpload(slixmpp.ClientXMPP): recipient: JID, filename: Path, domain: Optional[JID] = None, + encrypted: bool = False, ): slixmpp.ClientXMPP.__init__(self, jid, password) self.recipient = recipient self.filename = filename self.domain = domain + self.encrypted = encrypted self.add_event_handler("session_start", self.start) async def start(self, event): log.info('Uploading file %s...', self.filename) try: - url = await self['xep_0363'].upload_file( - self.filename, domain=self.domain, timeout=10 + upload_file = self['xep_0363'].upload_file + if self.encrypted: + upload_file = self['xep_0454'].upload_file + url = await upload_file( + self.filename, domain=self.domain, timeout=10, ) except IqTimeout: raise TimeoutError('Could not send message in time') @@ -90,6 +96,10 @@ if __name__ == '__main__': parser.add_argument("--domain", help="Domain to use for HTTP File Upload (leave out for your own server’s)") + parser.add_argument("-e", "--encrypt", dest="encrypted", + help="Whether to encrypt", action="store_true", + default=False) + args = parser.parse_args() # Setup logging. @@ -105,17 +115,27 @@ if __name__ == '__main__': if domain is not None: domain = JID(domain) + if args.encrypted: + print( + 'You are using the --encrypt flag. ' + 'Be aware that the transport being used is NOT end-to-end ' + 'encrypted. The server will be able to decrypt the file.', + file=sys.stderr, + ) + xmpp = HttpUpload( jid=args.jid, password=args.password, recipient=JID(args.recipient), filename=Path(args.file), domain=domain, + encrypted=args.encrypted, ) xmpp.register_plugin('xep_0066') xmpp.register_plugin('xep_0071') xmpp.register_plugin('xep_0128') xmpp.register_plugin('xep_0363') + xmpp.register_plugin('xep_0454') # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() -- cgit v1.2.3 From acad41f3b73d13e8e9abe5578aceacaded117cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Fri, 18 Mar 2022 18:30:58 +0100 Subject: xep_0454: Don't force content-type to application/octect-stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- slixmpp/plugins/xep_0454/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/slixmpp/plugins/xep_0454/__init__.py b/slixmpp/plugins/xep_0454/__init__.py index 34db0c79..63a34710 100644 --- a/slixmpp/plugins/xep_0454/__init__.py +++ b/slixmpp/plugins/xep_0454/__init__.py @@ -123,7 +123,7 @@ class XEP_0454(BasePlugin): self, filename: Path, _size: Optional[int] = None, - _content_type: Optional[str] = None, + content_type: Optional[str] = None, **kwargs, ) -> str: """ @@ -154,7 +154,7 @@ class XEP_0454(BasePlugin): input_enc.seek(0) kwargs['size'] = size - kwargs['content_type'] = 'application/octet-stream' + kwargs['content_type'] = content_type url = await self.xmpp['xep_0363'].upload_file(**kwargs) return self.format_url(url, fragment) -- cgit v1.2.3 From b899baabd875e465d364b882fc7d19a484a21a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Fri, 18 Mar 2022 18:37:37 +0100 Subject: xep_0454: also include finalize's result in the payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- slixmpp/plugins/xep_0454/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/slixmpp/plugins/xep_0454/__init__.py b/slixmpp/plugins/xep_0454/__init__.py index 63a34710..99e9ff5f 100644 --- a/slixmpp/plugins/xep_0454/__init__.py +++ b/slixmpp/plugins/xep_0454/__init__.py @@ -67,8 +67,7 @@ class XEP_0454(BasePlugin): break payload += aes_gcm.update(buf) - aes_gcm.finalize() - payload += aes_gcm.tag + payload += aes_gcm.finalize() + aes_gcm.tag fragment = aes_gcm_iv.hex() + aes_gcm_key.hex() return (payload, fragment) -- cgit v1.2.3 From 0fba8fd7f842ceb0fd52c0e940d75e4b948e2b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Sat, 19 Mar 2022 10:25:46 +0100 Subject: doap: add 454 entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- doap.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doap.xml b/doap.xml index 36c92b41..2a8a2379 100644 --- a/doap.xml +++ b/doap.xml @@ -892,6 +892,14 @@ 1.6.0 + + + + no thumbnail support + 0.1.0 + 1.8.1 + + -- cgit v1.2.3 From 53d38a81154a0cf5adbe798af6a9af739079e65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Sat, 19 Mar 2022 10:26:09 +0100 Subject: setup.py: add cryptography in extras_require; update example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- .gitlab-ci.yml | 6 +++--- examples/http_upload.py | 18 ++++++++++++++++-- setup.py | 8 +++++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 69bd7b7a..ebcc24eb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,7 @@ test: script: - apt update - apt install -y python3 python3-pip cython3 gpg - - pip3 install emoji aiohttp + - pip3 install emoji aiohttp cryptography - ./run_tests.py test-3.10: @@ -31,7 +31,7 @@ test-3.10: script: - apt update - apt install -y python3 python3-pip cython3 gpg - - pip3 install emoji aiohttp + - pip3 install emoji aiohttp cryptography - ./run_tests.py test-3.11: @@ -43,7 +43,7 @@ test-3.11: script: - apt update - apt install -y python3 python3-pip cython3 gpg - - pip3 install emoji aiohttp + - pip3 install emoji aiohttp cryptography - ./run_tests.py test_integration: diff --git a/examples/http_upload.py b/examples/http_upload.py index 7f1c44bc..b62c736e 100755 --- a/examples/http_upload.py +++ b/examples/http_upload.py @@ -48,7 +48,15 @@ class HttpUpload(slixmpp.ClientXMPP): log.info('Uploading file %s...', self.filename) try: upload_file = self['xep_0363'].upload_file - if self.encrypted: + if self.encrypted and not self['xep_0454']: + print( + 'The xep_0454 module isn\'t available. ' + 'Ensure you have \'cryptography\' ' + 'from extras_require installed.', + file=sys.stderr, + ) + return + elif self.encrypted: upload_file = self['xep_0454'].upload_file url = await upload_file( self.filename, domain=self.domain, timeout=10, @@ -135,7 +143,13 @@ if __name__ == '__main__': xmpp.register_plugin('xep_0071') xmpp.register_plugin('xep_0128') xmpp.register_plugin('xep_0363') - xmpp.register_plugin('xep_0454') + try: + xmpp.register_plugin('xep_0454') + except slixmpp.plugins.base.PluginNotFound: + log.error( + 'Could not load xep_0454. ' + 'Ensure you have \'cryptography\' from extras_require installed.' + ) # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() diff --git a/setup.py b/setup.py index bcbcdf56..4830efcc 100755 --- a/setup.py +++ b/setup.py @@ -86,10 +86,16 @@ setup( package_data={'slixmpp': ['py.typed']}, packages=packages, ext_modules=ext_modules, - install_requires=['aiodns>=1.0', 'pyasn1', 'pyasn1_modules', 'typing_extensions; python_version < "3.8.0"'], + install_requires=[ + 'aiodns>=1.0', + 'pyasn1', + 'pyasn1_modules', + 'typing_extensions; python_version < "3.8.0"', + ], extras_require={ 'XEP-0363': ['aiohttp'], 'XEP-0444 compliance': ['emoji'], + 'XEP-0454': ['cryptography'], 'Safer XML parsing': ['defusedxml'], }, classifiers=CLASSIFIERS, -- cgit v1.2.3 From 82ee250295f02ac2db61a1bbe045ab7686b1892c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Sat, 19 Mar 2022 17:00:03 +0100 Subject: xep_0454: use staticmethods where possible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- slixmpp/plugins/xep_0454/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/slixmpp/plugins/xep_0454/__init__.py b/slixmpp/plugins/xep_0454/__init__.py index 99e9ff5f..cecde480 100644 --- a/slixmpp/plugins/xep_0454/__init__.py +++ b/slixmpp/plugins/xep_0454/__init__.py @@ -35,8 +35,8 @@ class XEP_0454(BasePlugin): description = 'XEP-0454: OMEMO Media Sharing' dependencies = {'xep_0363'} - @classmethod - def encrypt(cls, input_file: Optional[IO[bytes]] = None, filename: Optional[Path] = None) -> Tuple[bytes, str]: + @staticmethod + def encrypt(input_file: Optional[IO[bytes]] = None, filename: Optional[Path] = None) -> Tuple[bytes, str]: """ Encrypts file as specified in XEP-0454 for use in file sharing @@ -71,8 +71,8 @@ class XEP_0454(BasePlugin): fragment = aes_gcm_iv.hex() + aes_gcm_key.hex() return (payload, fragment) - @classmethod - def decrypt(cls, input_file: IO[bytes], fragment: str) -> bytes: + @staticmethod + def decrypt(input_file: IO[bytes], fragment: str) -> bytes: """ Decrypts file-like. @@ -111,8 +111,8 @@ class XEP_0454(BasePlugin): return plain - @classmethod - def format_url(cls, url: str, fragment: str) -> str: + @staticmethod + def format_url(url: str, fragment: str) -> str: """Helper to format a HTTPS URL to an AESGCM URI""" if not url.startswith('https://') or url.find('#') != -1: raise InvalidURL -- cgit v1.2.3 From 06e4e480c14a614cbf00646271fbc9abc6908fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Sat, 19 Mar 2022 17:06:12 +0100 Subject: xep_0454: keep original filename extension if available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- slixmpp/plugins/xep_0454/__init__.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/slixmpp/plugins/xep_0454/__init__.py b/slixmpp/plugins/xep_0454/__init__.py index cecde480..d9292a13 100644 --- a/slixmpp/plugins/xep_0454/__init__.py +++ b/slixmpp/plugins/xep_0454/__init__.py @@ -26,6 +26,11 @@ class InvalidURL(Exception): """Raised for URLs that either aren't HTTPS or already contain a fragment.""" +EXTENSIONS_MAP = { + 'jpeg': 'jpg', + 'text': 'txt', +} + class XEP_0454(BasePlugin): """ XEP-0454: OMEMO Media Sharing @@ -118,6 +123,14 @@ class XEP_0454(BasePlugin): raise InvalidURL return 'aesgcm://' + url.removeprefix('https://') + '#' + fragment + @staticmethod + def map_extensions(ext: str) -> str: + """ + Apply conversions to extensions to reduce the number of + variations, (e.g., JPEG -> jpg). + """ + return EXTENSIONS_MAP.get(ext, ext).lower() + async def upload_file( self, filename: Path, @@ -142,8 +155,10 @@ class XEP_0454(BasePlugin): payload, fragment = self.encrypt(input_file, filename) # Prepare kwargs for upload_file call - filename = urandom(12).hex() # Random filename to hide user-provided path - kwargs['filename'] = filename + new_filename = urandom(12).hex() # Random filename to hide user-provided path + if filename.suffix: + new_filename += self.map_extensions(filename.suffix) + kwargs['filename'] = new_filename input_enc = BytesIO(payload) kwargs['input_file'] = input_enc -- cgit v1.2.3 From 28d44ecf74385e4adeaa03c9fa8b561b3ca4d9a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20=E2=80=9Cpep=E2=80=9D=20Buquet?= Date: Sun, 20 Mar 2022 21:34:55 +0100 Subject: xep_0454: str.removeprefix is available since 3.9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maxime “pep” Buquet --- slixmpp/plugins/xep_0454/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slixmpp/plugins/xep_0454/__init__.py b/slixmpp/plugins/xep_0454/__init__.py index d9292a13..d537432d 100644 --- a/slixmpp/plugins/xep_0454/__init__.py +++ b/slixmpp/plugins/xep_0454/__init__.py @@ -121,7 +121,7 @@ class XEP_0454(BasePlugin): """Helper to format a HTTPS URL to an AESGCM URI""" if not url.startswith('https://') or url.find('#') != -1: raise InvalidURL - return 'aesgcm://' + url.removeprefix('https://') + '#' + fragment + return 'aesgcm://' + url[len('https://'):] + '#' + fragment @staticmethod def map_extensions(ext: str) -> str: -- cgit v1.2.3