uploads: Add support for encrypted uploads.

This commit is contained in:
Damir Jelić 2019-02-17 22:45:49 +01:00
parent 6a1901c0ac
commit 01a23e8221
4 changed files with 151 additions and 32 deletions

View file

@ -20,9 +20,12 @@ import magic
import requests import requests
import argparse import argparse
from urllib.parse import urlparse from urllib.parse import urlparse
from itertools import zip_longest
import urllib3 import urllib3
from nio import Api, UploadResponse, UploadError from nio import Api, UploadResponse, UploadError
from nio.crypto import encrypt_attachment
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
urllib3.disable_warnings() urllib3.disable_warnings()
@ -95,6 +98,41 @@ class Upload(object):
return self.totalsize return self.totalsize
def chunk_bytes(iterable, n):
args = [iter(iterable)] * n
return (
bytes(
(filter(lambda x: x is not None, chunk))
) for chunk in zip_longest(*args)
)
class EncryptedUpload(Upload):
def __init__(self, file, chunksize=1 << 13):
super().__init__(file, chunksize)
self.source_mimetype = self.mimetype
self.mimetype = "application/octet-stream"
with open(self.filename, "rb") as file:
self.ciphertext, self.file_keys = encrypt_attachment(file.read())
def send_progress(self):
message = {
"type": "progress",
"data": self.readsofar
}
to_stdout(message)
def __iter__(self):
for chunk in chunk_bytes(self.ciphertext, self.chunksize):
self.readsofar += len(chunk)
self.send_progress()
yield chunk
def __len__(self):
return len(self.ciphertext)
class IterableToFileAdapter(object): class IterableToFileAdapter(object):
def __init__(self, iterable): def __init__(self, iterable):
self.iterator = iter(iterable) self.iterator = iter(iterable)
@ -109,9 +147,18 @@ class IterableToFileAdapter(object):
def upload_process(args): def upload_process(args):
file_path = os.path.expanduser(args.file) file_path = os.path.expanduser(args.file)
thumbnail = None
try: try:
upload = Upload(file_path, 10) if args.encrypt:
upload = EncryptedUpload(file_path)
if upload.source_mimetype.startswith("image"):
# TODO create a thumbnail
thumbnail = None
else:
upload = Upload(file_path)
except (FileNotFoundError, OSError, IOError) as e: except (FileNotFoundError, OSError, IOError) as e:
error(e) error(e)
@ -153,9 +200,14 @@ def upload_process(args):
"type": "status", "type": "status",
"status": "started", "status": "started",
"total": upload.totalsize, "total": upload.totalsize,
"mimetype": upload.mimetype,
"file_name": upload.filename, "file_name": upload.filename,
} }
if isinstance(upload, EncryptedUpload):
message["mimetype"] = upload.source_mimetype
else:
message["mimetype"] = upload.mimetype
to_stdout(message) to_stdout(message)
session = requests.Session() session = requests.Session()
@ -189,6 +241,9 @@ def upload_process(args):
"url": response.content_uri "url": response.content_uri
} }
if isinstance(upload, EncryptedUpload):
message["file_keys"] = upload.file_keys
to_stdout(message) to_stdout(message)
return 0 return 0

View file

@ -1071,11 +1071,6 @@ def matrix_upload_command_cb(data, buffer, args):
if not room_buffer: if not room_buffer:
continue continue
if room_buffer.room.encrypted:
room_buffer.error("Uploading to encrypted rooms is "
"not yet implemented")
return W.WEECHAT_RC_OK
upload = Upload( upload = Upload(
server.name, server.name,
server.config.address, server.config.address,

View file

@ -24,7 +24,16 @@ import time
import copy import copy
from collections import defaultdict, deque from collections import defaultdict, deque
from atomicwrites import atomic_write from atomicwrites import atomic_write
from typing import Any, Deque, Dict, Optional, List, NamedTuple, DefaultDict from typing import (
Any,
Deque,
Dict,
Optional,
List,
NamedTuple,
DefaultDict,
Union
)
from uuid import UUID from uuid import UUID
@ -64,6 +73,7 @@ from .config import ConfigSection, Option, ServerBufferType
from .globals import SCRIPT_NAME, SERVERS, W, MAX_EVENTS, TYPING_NOTICE_TIMEOUT from .globals import SCRIPT_NAME, SERVERS, W, MAX_EVENTS, TYPING_NOTICE_TIMEOUT
from .utf import utf8_decode from .utf import utf8_decode
from .utils import create_server_buffer, key_from_value, server_buffer_prnt from .utils import create_server_buffer, key_from_value, server_buffer_prnt
from .uploads import Upload
from .colors import Formatted, FormattedString, DEFAULT_ATTRIBUTES from .colors import Formatted, FormattedString, DEFAULT_ATTRIBUTES
@ -82,7 +92,7 @@ EncrytpionQueueItem = NamedTuple(
"EncrytpionQueueItem", "EncrytpionQueueItem",
[ [
("message_type", str), ("message_type", str),
("formatted_message", Formatted), ("message", Union[Formatted, Upload]),
], ],
) )
@ -797,38 +807,25 @@ class MatrixServer(object):
try: try:
room_buffer = self.find_room_from_id(upload.room_id) room_buffer = self.find_room_from_id(upload.room_id)
except (ValueError, KeyError): except (ValueError, KeyError):
return False return True
assert self.client assert self.client
if room_buffer.room.encrypted: if room_buffer.room.encrypted:
room_buffer.error("Uploading to encrypted rooms is " assert upload.encrypt
"not yet implemented")
return False
# TODO the content is different if the room is encrypted. content = upload.content
content = {
"msgtype": Api.mimetype_to_msgtype(upload.mimetype),
"body": upload.file_name,
"url": upload.content_uri,
}
try: try:
uuid = self.room_send_event(upload.room_id, content) uuid = self.room_send_event(upload.room_id, content)
except (EncryptionError, GroupEncryptionError): except (EncryptionError, GroupEncryptionError):
# TODO put the message in a queue to resend after group sessions message = EncrytpionQueueItem(upload.msgtype, upload)
# are shared self.encryption_queue[upload.room_id].append(message)
# message = EncrytpionQueueItem(msgtype, formatted)
# self.encryption_queue[room.room_id].append(message)
return False return False
http_url = Api.mxc_to_http(upload.content_uri)
description = ("/{}".format(upload.file_name) if upload.file_name
else "")
attributes = DEFAULT_ATTRIBUTES.copy() attributes = DEFAULT_ATTRIBUTES.copy()
formatted = Formatted([FormattedString( formatted = Formatted([FormattedString(
"{url}{desc}".format(url=http_url, desc=description), upload.render,
attributes attributes
)]) )])
@ -1386,14 +1383,27 @@ class MatrixServer(object):
room_buffer = self.room_buffers[room_id] room_buffer = self.room_buffers[room_id]
while self.encryption_queue[room_id]: while self.encryption_queue[room_id]:
message = self.encryption_queue[room_id].popleft() item = self.encryption_queue[room_id].popleft()
try: try:
if not self.room_send_message(room_buffer, if item.message_type in [
message.formatted_message, "m.file",
message.message_type): "m.video",
"m.audio",
"m.image"
]:
ret = self.room_send_upload(item.message)
else:
ret = self.room_send_message(
room_buffer,
item.message,
item.message_type
)
if not ret:
self.encryption_queue[room_id].pop() self.encryption_queue[room_id].pop()
self.encryption_queue[room_id].appendleft(message) self.encryption_queue[room_id].appendleft(message)
break break
except OlmTrustError: except OlmTrustError:
self.encryption_queue[room_id].clear() self.encryption_queue[room_id].clear()
break break

View file

@ -21,6 +21,7 @@ from __future__ import unicode_literals
import attr import attr
import time import time
import json import json
from typing import Dict, Any
from uuid import uuid1, UUID from uuid import uuid1, UUID
from enum import Enum from enum import Enum
@ -32,6 +33,7 @@ except ImportError:
from .globals import SCRIPT_NAME, SERVERS, W, UPLOADS from .globals import SCRIPT_NAME, SERVERS, W, UPLOADS
from .utf import utf8_decode from .utf import utf8_decode
from matrix import globals as G from matrix import globals as G
from nio import Api
class UploadState(Enum): class UploadState(Enum):
@ -168,6 +170,62 @@ class Upload(object):
def abort(self): def abort(self):
pass pass
@property
def msgtype(self):
# type: () -> str
assert self.mimetype
return Api.mimetype_to_msgtype(self.mimetype)
@property
def content(self):
# type: () -> Dict[Any, Any]
assert self.content_uri
if self.encrypt:
content = {
"body": self.file_name,
"msgtype": self.msgtype,
"file": self.file_keys,
}
content["file"]["url"] = self.content_uri
content["file"]["mimetype"] = self.mimetype
# TODO thumbnail if it's an image
return content
return {
"msgtype": self.msgtype,
"body": self.file_name,
"url": self.content_uri,
}
@property
def render(self):
# type: () -> str
assert self.content_uri
if self.encrypt:
http_url = Api.encrypted_mxc_to_plumb(
self.content_uri,
self.file_keys["key"]["k"],
self.file_keys["hashes"]["sha256"],
self.file_keys["iv"]
)
url = http_url if http_url else self.content_uri
description = "{}".format(self.file_name)
return ("{del_color}<{ncolor}{desc}{del_color}>{ncolor} "
"{del_color}[{ncolor}{url}{del_color}]{ncolor}").format(
del_color=W.color("chat_delimiters"),
ncolor=W.color("reset"),
desc=description, url=url)
http_url = Api.mxc_to_http(self.content_uri)
description = ("/{}".format(self.file_name) if self.file_name
else "")
return "{url}{desc}".format(url=http_url, desc=description)
@attr.s @attr.s
class UploadsBuffer(object): class UploadsBuffer(object):
@ -293,6 +351,7 @@ def handle_child_message(upload, message):
elif message["status"] == "done": elif message["status"] == "done":
upload.state = UploadState.finished upload.state = UploadState.finished
upload.content_uri = message["url"] upload.content_uri = message["url"]
upload.file_keys = message.get("file_keys", None)
server = SERVERS.get(upload.server_name, None) server = SERVERS.get(upload.server_name, None)