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.
|
/// Whether this client supports end-to-end encryption using olm.
|
||||||
bool get encryptionEnabled => _olmAccount != null;
|
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!
|
/// Warning! This endpoint is for testing only!
|
||||||
set rooms(List<Room> newList) {
|
set rooms(List<Room> newList) {
|
||||||
print("Warning! This endpoint is for testing only!");
|
print("Warning! This endpoint is for testing only!");
|
||||||
|
|
|
@ -22,8 +22,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'package:famedlysdk/famedlysdk.dart';
|
import 'package:famedlysdk/famedlysdk.dart';
|
||||||
import 'package:famedlysdk/src/utils/receipt.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';
|
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.
|
/// 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);
|
toUsers: users);
|
||||||
return;
|
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 {
|
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/matrix_file.dart';
|
||||||
import 'package:famedlysdk/src/utils/mx_content.dart';
|
import 'package:famedlysdk/src/utils/mx_content.dart';
|
||||||
import 'package:famedlysdk/src/utils/session_key.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:mime_type/mime_type.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
|
||||||
|
@ -453,20 +454,29 @@ class Room {
|
||||||
return resp["event_id"];
|
return resp["event_id"];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> sendTextEvent(String message,
|
Future<String> sendTextEvent(String message, {String txid, Event inReplyTo}) {
|
||||||
{String txid, Event inReplyTo}) =>
|
String type = "m.text";
|
||||||
sendEvent({"msgtype": "m.text", "body": message},
|
if (message.startsWith("/me ")) {
|
||||||
txid: txid, inReplyTo: inReplyTo);
|
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
|
/// Sends a [file] to this room after uploading it. The [msgType] is optional
|
||||||
/// and will be detected by the mimetype of the file.
|
/// and will be detected by the mimetype of the file.
|
||||||
Future<String> sendFileEvent(MatrixFile file,
|
Future<String> sendFileEvent(MatrixFile file,
|
||||||
{String msgType = "m.file", String txid, Event inReplyTo}) async {
|
{String msgType = "m.file",
|
||||||
if (msgType == "m.image") return sendImageEvent(file);
|
String txid,
|
||||||
if (msgType == "m.audio") return sendVideoEvent(file);
|
Event inReplyTo,
|
||||||
if (msgType == "m.video") return sendAudioEvent(file);
|
Map<String, dynamic> info}) async {
|
||||||
String fileName = file.path.split("/").last;
|
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);
|
final String uploadResp = await client.upload(file);
|
||||||
|
|
||||||
// Send event
|
// Send event
|
||||||
|
@ -474,48 +484,50 @@ class Room {
|
||||||
"msgtype": msgType,
|
"msgtype": msgType,
|
||||||
"body": fileName,
|
"body": fileName,
|
||||||
"filename": fileName,
|
"filename": fileName,
|
||||||
"url": uploadResp,
|
if (!sendEncrypted) "url": uploadResp,
|
||||||
"info": {
|
if (sendEncrypted)
|
||||||
"mimetype": mime(file.path),
|
"file": {
|
||||||
"size": file.size,
|
"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);
|
return await sendEvent(content, txid: txid, inReplyTo: inReplyTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> sendAudioEvent(MatrixFile file,
|
Future<String> sendAudioEvent(MatrixFile file,
|
||||||
{String txid, int width, int height, Event inReplyTo}) async {
|
{String txid, Event inReplyTo}) async {
|
||||||
String fileName = file.path.split("/").last;
|
return await sendFileEvent(file,
|
||||||
final String uploadResp = await client.upload(file);
|
msgType: "m.audio", txid: txid, inReplyTo: inReplyTo);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> sendImageEvent(MatrixFile file,
|
Future<String> sendImageEvent(MatrixFile file,
|
||||||
{String txid, int width, int height, Event inReplyTo}) async {
|
{String txid, int width, int height, Event inReplyTo}) async {
|
||||||
String fileName = file.path.split("/").last;
|
return await sendFileEvent(file,
|
||||||
final String uploadResp = await client.upload(file);
|
msgType: "m.image",
|
||||||
Map<String, dynamic> content = {
|
txid: txid,
|
||||||
"msgtype": "m.image",
|
inReplyTo: inReplyTo,
|
||||||
"body": fileName,
|
info: {
|
||||||
"url": uploadResp,
|
"size": file.size,
|
||||||
"info": {
|
"mimetype": mime(file.path.split("/").last),
|
||||||
"size": file.size,
|
"w": width,
|
||||||
"mimetype": mime(fileName),
|
"h": height,
|
||||||
"w": width,
|
});
|
||||||
"h": height,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return await sendEvent(content, txid: txid, inReplyTo: inReplyTo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> sendVideoEvent(MatrixFile file,
|
Future<String> sendVideoEvent(MatrixFile file,
|
||||||
|
@ -528,41 +540,37 @@ class Room {
|
||||||
int thumbnailHeight,
|
int thumbnailHeight,
|
||||||
Event inReplyTo}) async {
|
Event inReplyTo}) async {
|
||||||
String fileName = file.path.split("/").last;
|
String fileName = file.path.split("/").last;
|
||||||
final String uploadResp = await client.upload(file);
|
Map<String, dynamic> info = {
|
||||||
Map<String, dynamic> content = {
|
"size": file.size,
|
||||||
"msgtype": "m.video",
|
"mimetype": mime(fileName),
|
||||||
"body": fileName,
|
|
||||||
"url": uploadResp,
|
|
||||||
"info": {
|
|
||||||
"size": file.size,
|
|
||||||
"mimetype": mime(fileName),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
if (videoWidth != null) {
|
if (videoWidth != null) {
|
||||||
content["info"]["w"] = videoWidth;
|
info["w"] = videoWidth;
|
||||||
}
|
}
|
||||||
if (thumbnailHeight != null) {
|
if (thumbnailHeight != null) {
|
||||||
content["info"]["h"] = thumbnailHeight;
|
info["h"] = thumbnailHeight;
|
||||||
}
|
}
|
||||||
if (duration != null) {
|
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;
|
String thumbnailName = file.path.split("/").last;
|
||||||
final String thumbnailUploadResp = await client.upload(file);
|
final String thumbnailUploadResp = await client.upload(thumbnail);
|
||||||
content["info"]["thumbnail_url"] = thumbnailUploadResp;
|
info["thumbnail_url"] = thumbnailUploadResp;
|
||||||
content["info"]["thumbnail_info"] = {
|
info["thumbnail_info"] = {
|
||||||
"size": thumbnail.size,
|
"size": thumbnail.size,
|
||||||
"mimetype": mime(thumbnailName),
|
"mimetype": mime(thumbnailName),
|
||||||
};
|
};
|
||||||
if (thumbnailWidth != null) {
|
if (thumbnailWidth != null) {
|
||||||
content["info"]["thumbnail_info"]["w"] = thumbnailWidth;
|
info["thumbnail_info"]["w"] = thumbnailWidth;
|
||||||
}
|
}
|
||||||
if (thumbnailHeight != null) {
|
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,
|
Future<String> sendEvent(Map<String, dynamic> content,
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:core';
|
import 'dart:core';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'package:famedlysdk/src/account_data.dart';
|
import 'package:famedlysdk/src/account_data.dart';
|
||||||
import 'package:famedlysdk/src/presence.dart';
|
import 'package:famedlysdk/src/presence.dart';
|
||||||
import 'package:famedlysdk/src/utils/device_keys_list.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
|
/// Responsible to store all data persistent and to query objects from the
|
||||||
/// database.
|
/// database.
|
||||||
abstract class ExtendedStoreAPI extends StoreAPI {
|
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
|
/// Whether this is a simple store which only stores the client credentials and
|
||||||
/// end to end encryption stuff or the whole sync payloads.
|
/// end to end encryption stuff or the whole sync payloads.
|
||||||
final bool extended = true;
|
final bool extended = true;
|
||||||
|
@ -114,4 +118,11 @@ abstract class ExtendedStoreAPI extends StoreAPI {
|
||||||
|
|
||||||
/// Removes this event from the store.
|
/// Removes this event from the store.
|
||||||
Future removeEvent(String eventId);
|
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 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||||
|
|
||||||
class MatrixFile {
|
class MatrixFile {
|
||||||
Uint8List bytes;
|
Uint8List bytes;
|
||||||
String path;
|
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();
|
MatrixFile({this.bytes, String path}) : this.path = path.toLowerCase();
|
||||||
int get size => bytes.length;
|
int get size => bytes.length;
|
||||||
}
|
}
|
||||||
|
|
43
pubspec.lock
43
pubspec.lock
|
@ -15,6 +15,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.2"
|
version: "1.5.2"
|
||||||
|
asn1lib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: asn1lib
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.15"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -99,6 +106,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
code_builder:
|
code_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -148,6 +162,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.7"
|
version: "1.2.7"
|
||||||
|
encrypt:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: encrypt
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -253,6 +274,15 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.6"
|
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:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -292,11 +322,11 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: bbc7ce10a52be5d5c10d2eb6c3591aade71356e2
|
ref: "1.x.y"
|
||||||
resolved-ref: bbc7ce10a52be5d5c10d2eb6c3591aade71356e2
|
resolved-ref: "79868b06b3ea156f90b73abafb3bbf3ac4114cc6"
|
||||||
url: "https://gitlab.com/famedly/libraries/dart-olm.git"
|
url: "https://gitlab.com/famedly/libraries/dart-olm.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.0"
|
version: "1.0.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -325,6 +355,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0+1"
|
version: "1.8.0+1"
|
||||||
|
pointycastle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pointycastle
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -15,7 +15,12 @@ dependencies:
|
||||||
olm:
|
olm:
|
||||||
git:
|
git:
|
||||||
url: https://gitlab.com/famedly/libraries/dart-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:
|
dev_dependencies:
|
||||||
test: ^1.0.0
|
test: ^1.0.0
|
||||||
|
|
Loading…
Reference in a new issue