Merge branch 'event-feature-file-encryption' into 'master'

[Event] Implement file encryption

See merge request famedly/famedlysdk!232
This commit is contained in:
Christian Pauly 2020-03-16 10:38:03 +00:00
commit 67416b6e3a
7 changed files with 195 additions and 65 deletions

View file

@ -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<Room> newList) {
print("Warning! This endpoint is for testing only!");

View file

@ -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<MatrixFile> 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<String, dynamic> &&
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 {

View file

@ -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<String> sendTextEvent(String message,
{String txid, Event inReplyTo}) =>
sendEvent({"msgtype": "m.text", "body": message},
txid: txid, inReplyTo: inReplyTo);
Future<String> 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<String> 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<String, dynamic> 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<String> 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<String, dynamic> 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<String> 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<String, dynamic> 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<String> 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<String, dynamic> content = {
"msgtype": "m.video",
"body": fileName,
"url": uploadResp,
"info": {
"size": file.size,
"mimetype": mime(fileName),
},
Map<String, dynamic> 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<String> sendEvent(Map<String, dynamic> content,

View file

@ -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<void> storeFile(Uint8List bytes, String mxcUri);
/// Returns the file bytes indexed by [mxcUri]. Returns null if not found.
Future<Uint8List> getFile(String mxcUri);
}

View file

@ -2,10 +2,20 @@
import 'dart:typed_data';
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
class MatrixFile {
Uint8List bytes;
String path;
Future<EncryptedFile> 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;
}

View file

@ -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:

View file

@ -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