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 ")) { | ||||||
|  |       type = "m.emote"; | ||||||
|  |       message = message.substring(4); | ||||||
|  |     } | ||||||
|  |     return sendEvent({"msgtype": type, "body": message}, | ||||||
|         txid: txid, inReplyTo: inReplyTo); |         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,8 +484,25 @@ class Room { | ||||||
|       "msgtype": msgType, |       "msgtype": msgType, | ||||||
|       "body": fileName, |       "body": fileName, | ||||||
|       "filename": fileName, |       "filename": fileName, | ||||||
|  |       if (!sendEncrypted) "url": uploadResp, | ||||||
|  |       if (sendEncrypted) | ||||||
|  |         "file": { | ||||||
|           "url": uploadResp, |           "url": uploadResp, | ||||||
|       "info": { |           "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), |               "mimetype": mime(file.path), | ||||||
|               "size": file.size, |               "size": file.size, | ||||||
|             } |             } | ||||||
|  | @ -484,38 +511,23 @@ class Room { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   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, |  | ||||||
|       "info": { |  | ||||||
|           "size": file.size, |           "size": file.size, | ||||||
|         "mimetype": mime(fileName), |           "mimetype": mime(file.path.split("/").last), | ||||||
|           "w": width, |           "w": width, | ||||||
|           "h": height, |           "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 = { |  | ||||||
|       "msgtype": "m.video", |  | ||||||
|       "body": fileName, |  | ||||||
|       "url": uploadResp, |  | ||||||
|       "info": { |  | ||||||
|       "size": file.size, |       "size": file.size, | ||||||
|       "mimetype": mime(fileName), |       "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 New Issue