Merge branch 'event-feature-file-encryption' into 'master'
[Event] Implement file encryption See merge request famedly/famedlysdk!232
This commit is contained in:
commit
67416b6e3a
|
@ -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!");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
43
pubspec.lock
43
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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue