comparison sat/plugins/plugin_sec_aesgcm.py @ 3174:c90f27ce52b0

plugin aesgcm: look for "aesgcm" links in body to use them as attachments
author Goffi <goffi@goffi.org>
date Tue, 18 Feb 2020 18:17:18 +0100
parents 9d0df638c8b4
children 98b321234068
comparison
equal deleted inserted replaced
3173:343b8076e967 3174:c90f27ce52b0
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 2
3 # SAT plugin for handling AES-GCM file encryption 3 # SàT plugin for handling AES-GCM file encryption
4 # Copyright (C) 2009-2020 Jérôme Poisson (goffi@goffi.org) 4 # Copyright (C) 2009-2020 Jérôme Poisson (goffi@goffi.org)
5 5
6 # This program is free software: you can redistribute it and/or modify 6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by 7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or 8 # the Free Software Foundation, either version 3 of the License, or
14 # GNU Affero General Public License for more details. 14 # GNU Affero General Public License for more details.
15 15
16 # You should have received a copy of the GNU Affero General Public License 16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. 17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 18
19 import re
19 from textwrap import dedent 20 from textwrap import dedent
20 from functools import partial 21 from functools import partial
22 from urllib.parse import urlparse
23 import mimetypes
21 import secrets 24 import secrets
22 from cryptography.hazmat.primitives import ciphers 25 from cryptography.hazmat.primitives import ciphers
23 from cryptography.hazmat.primitives.ciphers import modes 26 from cryptography.hazmat.primitives.ciphers import modes
24 from cryptography.hazmat import backends 27 from cryptography.hazmat import backends
25 from cryptography.exceptions import AlreadyFinalized 28 from cryptography.exceptions import AlreadyFinalized
45 Implementation of AES-GCM scheme, a way to encrypt files (not official XMPP standard). 48 Implementation of AES-GCM scheme, a way to encrypt files (not official XMPP standard).
46 See https://xmpp.org/extensions/inbox/omemo-media-sharing.html for details 49 See https://xmpp.org/extensions/inbox/omemo-media-sharing.html for details
47 """)), 50 """)),
48 } 51 }
49 52
53 AESGCM_RE = re.compile(
54 r'aesgcm:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9'
55 r'()@:%_\+.~#?&\/\/=]*)')
56
50 57
51 class AESGCM(object): 58 class AESGCM(object):
52 59
53 def __init__(self, host): 60 def __init__(self, host):
54 self.host = host 61 self.host = host
56 host.plugins["DOWNLOAD"].registerScheme( 63 host.plugins["DOWNLOAD"].registerScheme(
57 "aesgcm", self.download 64 "aesgcm", self.download
58 ) 65 )
59 host.trigger.add("XEP-0363_upload_size", self._uploadSizeTrigger) 66 host.trigger.add("XEP-0363_upload_size", self._uploadSizeTrigger)
60 host.trigger.add("XEP-0363_upload", self._uploadTrigger) 67 host.trigger.add("XEP-0363_upload", self._uploadTrigger)
68 host.trigger.add("messageReceived", self._messageReceivedTrigger)
61 69
62 async def download(self, client, uri_parsed, dest_path, options): 70 async def download(self, client, uri_parsed, dest_path, options):
63 fragment = bytes.fromhex(uri_parsed.fragment) 71 fragment = bytes.fromhex(uri_parsed.fragment)
64 72
65 # legacy method use 16 bits IV, but OMEMO media sharing published spec indicates 73 # legacy method use 16 bits IV, but OMEMO media sharing published spec indicates
196 f"data_cb was expected to be None, it is set to {sat_file.data_cb}") 204 f"data_cb was expected to be None, it is set to {sat_file.data_cb}")
197 205
198 # with data_cb we encrypt the file on the fly 206 # with data_cb we encrypt the file on the fly
199 sat_file.data_cb = partial(self._encrypt, encryptor=encryptor) 207 sat_file.data_cb = partial(self._encrypt, encryptor=encryptor)
200 return True 208 return True
209
210
211 def _popAESGCMLinks(self, match, links):
212 link = match.group()
213 if link not in links:
214 links.append(link)
215 return ""
216
217 def _checkAESGCMAttachments(self, client, data):
218 if not data.get('message'):
219 return data
220 links = []
221
222 for lang, message in list(data['message'].items()):
223 message = AESGCM_RE.sub(
224 partial(self._popAESGCMLinks, links=links),
225 message)
226 if links:
227 message = message.strip()
228 if not message:
229 del data['message'][lang]
230 else:
231 data['message'][lang] = message
232 mess_encrypted = client.encryption.isEncrypted(data)
233 attachments = data['extra'].setdefault(C.MESS_KEY_ATTACHMENTS, [])
234 for link in links:
235 path = urlparse(link).path
236 attachment = {
237 "url": link,
238 }
239 media_type = mimetypes.guess_type(path, strict=False)[0]
240 if media_type is not None:
241 attachment[C.MESS_KEY_MEDIA_TYPE] = media_type
242
243 if mess_encrypted:
244 # we don't add the encrypted flag if the message itself is not
245 # encrypted, because the decryption key is part of the link,
246 # so sending it over unencrypted channel is like having no
247 # encryption at all.
248 attachment['encrypted'] = True
249 attachments.append(attachment)
250
251 return data
252
253 def _messageReceivedTrigger(self, client, message_elt, post_treat):
254 # we use a post_treat callback instead of "message_parse" trigger because we need
255 # to check if the "encrypted" flag is set to decide if we add the same flag to the
256 # attachment
257 post_treat.addCallback(partial(self._checkAESGCMAttachments, client))
258 return True