diff --git a/lib/src/client.dart b/lib/src/client.dart index f639a14..2938176 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -134,6 +134,9 @@ class Client { /// Whether this client supports end-to-end encryption using olm. bool get encryptionEnabled => _olmAccount != null; + /// Whether this client is able to encrypt and decrypt files. + bool get fileEncryptionEnabled => false; + /// Warning! This endpoint is for testing only! set rooms(List newList) { print("Warning! This endpoint is for testing only!"); diff --git a/lib/src/event.dart b/lib/src/event.dart index 0e8a01f..da07e45 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -22,8 +22,11 @@ */ import 'dart:convert'; +import 'dart:typed_data'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/src/utils/receipt.dart'; +import 'package:http/http.dart' as http; +import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; import './room.dart'; /// All data exchanged over Matrix is expressed as an "event". Typically each client action (e.g. sending a message) correlates with exactly one event. @@ -427,6 +430,59 @@ class Event { toUsers: users); return; } + + /// Downloads (and decryptes if necessary) the attachment of this + /// event and returns it as a [MatrixFile]. If this event doesn't + /// contain an attachment, this throws an error. + Future downloadAndDecryptAttachment() async { + if (![EventTypes.Message, EventTypes.Sticker].contains(this.type)) { + throw ("This event has the type '$typeKey' and so it can't contain an attachment."); + } + if (!content.containsKey("url") && !content.containsKey("file")) { + throw ("This event hasn't any attachment."); + } + final bool isEncrypted = !content.containsKey("url"); + + if (isEncrypted && !room.client.encryptionEnabled) { + throw ("Encryption is not enabled in your Client."); + } + MxContent mxContent = + MxContent(isEncrypted ? content["file"]["url"] : content["url"]); + + Uint8List uint8list; + + // Is this file storeable? + final bool storeable = room.client.storeAPI.extended && + content["info"] is Map && + content["info"]["size"] is int && + content["info"]["size"] <= ExtendedStoreAPI.MAX_FILE_SIZE; + + if (storeable) { + uint8list = await room.client.store.getFile(mxContent.mxc); + } + + // Download the file + if (uint8list == null) { + uint8list = + (await http.get(mxContent.getDownloadLink(room.client))).bodyBytes; + await room.client.store.storeFile(uint8list, mxContent.mxc); + } + + // Decrypt the file + if (isEncrypted) { + if (!content.containsKey("file") || + !content["file"]["key"]["key_ops"].contains("decrypt")) { + throw ("Missing 'decrypt' in 'key_ops'."); + } + final EncryptedFile encryptedFile = EncryptedFile(); + encryptedFile.data = uint8list; + encryptedFile.iv = content["file"]["iv"]; + encryptedFile.k = content["file"]["key"]["k"]; + encryptedFile.sha256 = content["file"]["hashes"]["sha256"]; + uint8list = await decryptFile(encryptedFile); + } + return MatrixFile(bytes: uint8list, path: "/$body"); + } } enum MessageTypes { diff --git a/lib/src/room.dart b/lib/src/room.dart index 3a1386b..9b061cb 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -34,6 +34,7 @@ import 'package:famedlysdk/src/utils/matrix_exception.dart'; import 'package:famedlysdk/src/utils/matrix_file.dart'; import 'package:famedlysdk/src/utils/mx_content.dart'; import 'package:famedlysdk/src/utils/session_key.dart'; +import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; import 'package:mime_type/mime_type.dart'; import 'package:olm/olm.dart' as olm; @@ -453,20 +454,29 @@ class Room { return resp["event_id"]; } - Future sendTextEvent(String message, - {String txid, Event inReplyTo}) => - sendEvent({"msgtype": "m.text", "body": message}, - txid: txid, inReplyTo: inReplyTo); + Future sendTextEvent(String message, {String txid, Event inReplyTo}) { + String type = "m.text"; + if (message.startsWith("/me ")) { + type = "m.emote"; + message = message.substring(4); + } + return sendEvent({"msgtype": type, "body": message}, + txid: txid, inReplyTo: inReplyTo); + } /// Sends a [file] to this room after uploading it. The [msgType] is optional /// and will be detected by the mimetype of the file. Future sendFileEvent(MatrixFile file, - {String msgType = "m.file", String txid, Event inReplyTo}) async { - if (msgType == "m.image") return sendImageEvent(file); - if (msgType == "m.audio") return sendVideoEvent(file); - if (msgType == "m.video") return sendAudioEvent(file); + {String msgType = "m.file", + String txid, + Event inReplyTo, + Map info}) async { String fileName = file.path.split("/").last; - + final bool sendEncrypted = this.encrypted && client.fileEncryptionEnabled; + EncryptedFile encryptedFile; + if (sendEncrypted) { + encryptedFile = await file.encrypt(); + } final String uploadResp = await client.upload(file); // Send event @@ -474,48 +484,50 @@ class Room { "msgtype": msgType, "body": fileName, "filename": fileName, - "url": uploadResp, - "info": { - "mimetype": mime(file.path), - "size": file.size, - } + if (!sendEncrypted) "url": uploadResp, + if (sendEncrypted) + "file": { + "url": uploadResp, + "mimetype": mime(file.path), + "v": "v2", + "key": { + "alg": "A256CTR", + "ext": true, + "k": encryptedFile.k, + "key_ops": ["encrypt", "decrypt"], + "kty": "oct" + }, + "iv": encryptedFile.iv, + "hashes": {"sha256": encryptedFile.sha256} + }, + "info": info != null + ? info + : { + "mimetype": mime(file.path), + "size": file.size, + } }; return await sendEvent(content, txid: txid, inReplyTo: inReplyTo); } Future sendAudioEvent(MatrixFile file, - {String txid, int width, int height, Event inReplyTo}) async { - String fileName = file.path.split("/").last; - final String uploadResp = await client.upload(file); - Map content = { - "msgtype": "m.audio", - "body": fileName, - "filename": fileName, - "url": uploadResp, - "info": { - "mimetype": mime(fileName), - "size": file.size, - } - }; - return await sendEvent(content, txid: txid, inReplyTo: inReplyTo); + {String txid, Event inReplyTo}) async { + return await sendFileEvent(file, + msgType: "m.audio", txid: txid, inReplyTo: inReplyTo); } Future sendImageEvent(MatrixFile file, {String txid, int width, int height, Event inReplyTo}) async { - String fileName = file.path.split("/").last; - final String uploadResp = await client.upload(file); - Map content = { - "msgtype": "m.image", - "body": fileName, - "url": uploadResp, - "info": { - "size": file.size, - "mimetype": mime(fileName), - "w": width, - "h": height, - }, - }; - return await sendEvent(content, txid: txid, inReplyTo: inReplyTo); + return await sendFileEvent(file, + msgType: "m.image", + txid: txid, + inReplyTo: inReplyTo, + info: { + "size": file.size, + "mimetype": mime(file.path.split("/").last), + "w": width, + "h": height, + }); } Future sendVideoEvent(MatrixFile file, @@ -528,41 +540,37 @@ class Room { int thumbnailHeight, Event inReplyTo}) async { String fileName = file.path.split("/").last; - final String uploadResp = await client.upload(file); - Map content = { - "msgtype": "m.video", - "body": fileName, - "url": uploadResp, - "info": { - "size": file.size, - "mimetype": mime(fileName), - }, + Map info = { + "size": file.size, + "mimetype": mime(fileName), }; if (videoWidth != null) { - content["info"]["w"] = videoWidth; + info["w"] = videoWidth; } if (thumbnailHeight != null) { - content["info"]["h"] = thumbnailHeight; + info["h"] = thumbnailHeight; } if (duration != null) { - content["info"]["duration"] = duration; + info["duration"] = duration; } - if (thumbnail != null) { + if (thumbnail != null && !(this.encrypted && client.encryptionEnabled)) { String thumbnailName = file.path.split("/").last; - final String thumbnailUploadResp = await client.upload(file); - content["info"]["thumbnail_url"] = thumbnailUploadResp; - content["info"]["thumbnail_info"] = { + final String thumbnailUploadResp = await client.upload(thumbnail); + info["thumbnail_url"] = thumbnailUploadResp; + info["thumbnail_info"] = { "size": thumbnail.size, "mimetype": mime(thumbnailName), }; if (thumbnailWidth != null) { - content["info"]["thumbnail_info"]["w"] = thumbnailWidth; + info["thumbnail_info"]["w"] = thumbnailWidth; } if (thumbnailHeight != null) { - content["info"]["thumbnail_info"]["h"] = thumbnailHeight; + info["thumbnail_info"]["h"] = thumbnailHeight; } } - return await sendEvent(content, txid: txid, inReplyTo: inReplyTo); + + return await sendFileEvent(file, + msgType: "m.video", txid: txid, inReplyTo: inReplyTo, info: info); } Future sendEvent(Map content, diff --git a/lib/src/store_api.dart b/lib/src/store_api.dart index 29c823b..36ce5bd 100644 --- a/lib/src/store_api.dart +++ b/lib/src/store_api.dart @@ -23,6 +23,7 @@ import 'dart:async'; import 'dart:core'; +import 'dart:typed_data'; import 'package:famedlysdk/src/account_data.dart'; import 'package:famedlysdk/src/presence.dart'; import 'package:famedlysdk/src/utils/device_keys_list.dart'; @@ -60,6 +61,9 @@ abstract class StoreAPI { /// Responsible to store all data persistent and to query objects from the /// database. abstract class ExtendedStoreAPI extends StoreAPI { + /// The maximum size of files which should be stored in bytes. + static const int MAX_FILE_SIZE = 10 * 1024 * 1024; + /// Whether this is a simple store which only stores the client credentials and /// end to end encryption stuff or the whole sync payloads. final bool extended = true; @@ -114,4 +118,11 @@ abstract class ExtendedStoreAPI extends StoreAPI { /// Removes this event from the store. Future removeEvent(String eventId); + + /// Stores the bytes of this file indexed by the [mxcUri]. Throws an + /// exception if the bytes are more than [MAX_FILE_SIZE]. + Future storeFile(Uint8List bytes, String mxcUri); + + /// Returns the file bytes indexed by [mxcUri]. Returns null if not found. + Future getFile(String mxcUri); } diff --git a/lib/src/utils/matrix_file.dart b/lib/src/utils/matrix_file.dart index 91268c6..11b0ea7 100644 --- a/lib/src/utils/matrix_file.dart +++ b/lib/src/utils/matrix_file.dart @@ -2,10 +2,20 @@ import 'dart:typed_data'; +import 'package:matrix_file_e2ee/matrix_file_e2ee.dart'; + class MatrixFile { Uint8List bytes; String path; + Future encrypt() async { + print("[Matrix] Encrypt file with a size of ${bytes.length} bytes"); + final EncryptedFile encryptedFile = await encryptFile(bytes); + print("[Matrix] File encryption successfull"); + this.bytes = encryptedFile.data; + return encryptedFile; + } + MatrixFile({this.bytes, String path}) : this.path = path.toLowerCase(); int get size => bytes.length; } diff --git a/pubspec.lock b/pubspec.lock index f13d0ef..89e440f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -15,6 +15,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.2" + asn1lib: + dependency: transitive + description: + name: asn1lib + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.15" async: dependency: transitive description: @@ -99,6 +106,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.2" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" code_builder: dependency: transitive description: @@ -148,6 +162,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.7" + encrypt: + dependency: transitive + description: + name: encrypt + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" ffi: dependency: transitive description: @@ -253,6 +274,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.6" + matrix_file_e2ee: + dependency: "direct main" + description: + path: "." + ref: "1.x.y" + resolved-ref: "2ca458afed599e1421229460d7c9e9248bb86140" + url: "https://gitlab.com/famedly/libraries/matrix_file_e2ee.git" + source: git + version: "1.0.1" meta: dependency: transitive description: @@ -292,11 +322,11 @@ packages: dependency: "direct main" description: path: "." - ref: bbc7ce10a52be5d5c10d2eb6c3591aade71356e2 - resolved-ref: bbc7ce10a52be5d5c10d2eb6c3591aade71356e2 + ref: "1.x.y" + resolved-ref: "79868b06b3ea156f90b73abafb3bbf3ac4114cc6" url: "https://gitlab.com/famedly/libraries/dart-olm.git" source: git - version: "0.0.0" + version: "1.0.0" package_config: dependency: transitive description: @@ -325,6 +355,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0+1" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" pool: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a859b33..2d97306 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,12 @@ dependencies: olm: git: url: https://gitlab.com/famedly/libraries/dart-olm.git - ref: bbc7ce10a52be5d5c10d2eb6c3591aade71356e2 + ref: 1.x.y + + matrix_file_e2ee: + git: + url: https://gitlab.com/famedly/libraries/matrix_file_e2ee.git + ref: 1.x.y dev_dependencies: test: ^1.0.0