diff --git a/analysis_options.yaml b/analysis_options.yaml
index b7c304f..c0e32f0 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -5,5 +5,7 @@ linter:
- camel_case_types
analyzer:
+ errors:
+ todo: ignore
# exclude:
# - path/to/excluded/files/**
\ No newline at end of file
diff --git a/lib/encryption.dart b/lib/encryption.dart
new file mode 100644
index 0000000..ef4f347
--- /dev/null
+++ b/lib/encryption.dart
@@ -0,0 +1,23 @@
+/*
+ * Famedly Matrix SDK
+ * Copyright (C) 2020 Famedly GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+library encryption;
+
+export './encryption/encryption.dart';
+export './encryption/key_manager.dart';
+export './encryption/utils/key_verification.dart';
diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart
new file mode 100644
index 0000000..072d0c8
--- /dev/null
+++ b/lib/encryption/encryption.dart
@@ -0,0 +1,283 @@
+/*
+ * Famedly Matrix SDK
+ * Copyright (C) 2019, 2020 Famedly GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import 'dart:convert';
+
+import 'package:famedlysdk/famedlysdk.dart';
+import 'package:famedlysdk/matrix_api.dart';
+import 'package:pedantic/pedantic.dart';
+import 'key_manager.dart';
+import 'olm_manager.dart';
+import 'key_verification_manager.dart';
+
+class Encryption {
+ final Client client;
+ final bool debug;
+ final bool enableE2eeRecovery;
+
+ bool get enabled => olmManager.enabled;
+
+ /// Returns the base64 encoded keys to store them in a store.
+ /// This String should **never** leave the device!
+ String get pickledOlmAccount => olmManager.pickledOlmAccount;
+
+ String get fingerprintKey => olmManager.fingerprintKey;
+ String get identityKey => olmManager.identityKey;
+
+ KeyManager keyManager;
+ OlmManager olmManager;
+ KeyVerificationManager keyVerificationManager;
+
+ Encryption({
+ this.client,
+ this.debug,
+ this.enableE2eeRecovery,
+ }) {
+ keyManager = KeyManager(this);
+ olmManager = OlmManager(this);
+ keyVerificationManager = KeyVerificationManager(this);
+ }
+
+ Future init(String olmAccount) async {
+ await olmManager.init(olmAccount);
+ }
+
+ void handleDeviceOneTimeKeysCount(Map countJson) {
+ olmManager.handleDeviceOneTimeKeysCount(countJson);
+ }
+
+ void onSync() {
+ keyVerificationManager.cleanup();
+ }
+
+ Future handleToDeviceEvent(ToDeviceEvent event) async {
+ if (['m.room_key', 'm.room_key_request', 'm.forwarded_room_key']
+ .contains(event.type)) {
+ // a new room key or thelike. We need to handle this asap, before other
+ // events in /sync are handled
+ await keyManager.handleToDeviceEvent(event);
+ }
+ if (event.type.startsWith('m.key.verification.')) {
+ // some key verification event. No need to handle it now, we can easily
+ // do this in the background
+ unawaited(keyVerificationManager.handleToDeviceEvent(event));
+ }
+ }
+
+ Future decryptToDeviceEvent(ToDeviceEvent event) async {
+ return await olmManager.decryptToDeviceEvent(event);
+ }
+
+ Event decryptRoomEventSync(String roomId, Event event) {
+ if (event.type != EventTypes.Encrypted ||
+ event.content['ciphertext'] == null) return event;
+ Map decryptedPayload;
+ try {
+ if (event.content['algorithm'] != 'm.megolm.v1.aes-sha2') {
+ throw (DecryptError.UNKNOWN_ALGORITHM);
+ }
+ final String sessionId = event.content['session_id'];
+ final String senderKey = event.content['sender_key'];
+ final inboundGroupSession =
+ keyManager.getInboundGroupSession(roomId, sessionId, senderKey);
+ if (inboundGroupSession == null) {
+ throw (DecryptError.UNKNOWN_SESSION);
+ }
+ final decryptResult = inboundGroupSession.inboundGroupSession
+ .decrypt(event.content['ciphertext']);
+ final messageIndexKey = event.eventId +
+ event.originServerTs.millisecondsSinceEpoch.toString();
+ var haveIndex = inboundGroupSession.indexes.containsKey(messageIndexKey);
+ if (haveIndex &&
+ inboundGroupSession.indexes[messageIndexKey] !=
+ decryptResult.message_index) {
+ // TODO: maybe clear outbound session, if it is ours
+ throw (DecryptError.CHANNEL_CORRUPTED);
+ }
+ inboundGroupSession.indexes[messageIndexKey] =
+ decryptResult.message_index;
+ if (!haveIndex) {
+ // now we persist the udpated indexes into the database.
+ // the entry should always exist. In the case it doesn't, the following
+ // line *could* throw an error. As that is a future, though, and we call
+ // it un-awaited here, nothing happens, which is exactly the result we want
+ client.database?.updateInboundGroupSessionIndexes(
+ json.encode(inboundGroupSession.indexes),
+ client.id,
+ roomId,
+ sessionId);
+ }
+ decryptedPayload = json.decode(decryptResult.plaintext);
+ } catch (exception) {
+ // alright, if this was actually by our own outbound group session, we might as well clear it
+ if (client.enableE2eeRecovery &&
+ (keyManager
+ .getOutboundGroupSession(roomId)
+ ?.outboundGroupSession
+ ?.session_id() ??
+ '') ==
+ event.content['session_id']) {
+ keyManager.clearOutboundGroupSession(roomId, wipe: true);
+ }
+ if (exception.toString() == DecryptError.UNKNOWN_SESSION) {
+ decryptedPayload = {
+ 'content': event.content,
+ 'type': EventTypes.Encrypted,
+ };
+ decryptedPayload['content']['body'] = exception.toString();
+ decryptedPayload['content']['msgtype'] = 'm.bad.encrypted';
+ } else {
+ decryptedPayload = {
+ 'content': {
+ 'msgtype': 'm.bad.encrypted',
+ 'body': exception.toString(),
+ },
+ 'type': EventTypes.Encrypted,
+ };
+ }
+ }
+ if (event.content['m.relates_to'] != null) {
+ decryptedPayload['content']['m.relates_to'] =
+ event.content['m.relates_to'];
+ }
+ return Event(
+ content: decryptedPayload['content'],
+ type: decryptedPayload['type'],
+ senderId: event.senderId,
+ eventId: event.eventId,
+ roomId: event.roomId,
+ room: event.room,
+ originServerTs: event.originServerTs,
+ unsigned: event.unsigned,
+ stateKey: event.stateKey,
+ prevContent: event.prevContent,
+ status: event.status,
+ sortOrder: event.sortOrder,
+ );
+ }
+
+ Future decryptRoomEvent(String roomId, Event event,
+ {bool store = false, String updateType = 'timeline'}) async {
+ final doStore = () async {
+ await client.database?.storeEventUpdate(
+ client.id,
+ EventUpdate(
+ eventType: event.type,
+ content: event.toJson(),
+ roomID: event.roomId,
+ type: updateType,
+ sortOrder: event.sortOrder,
+ ),
+ );
+ if (updateType != 'history') {
+ event.room?.setState(event);
+ }
+ };
+ if (event.type != EventTypes.Encrypted) {
+ return event;
+ }
+ event = decryptRoomEventSync(roomId, event);
+ if (event.type != EventTypes.Encrypted) {
+ if (store) {
+ await doStore();
+ }
+ return event;
+ }
+ if (client.database == null) {
+ return event;
+ }
+ await keyManager.loadInboundGroupSession(
+ roomId, event.content['session_id'], event.content['sender_key']);
+ event = decryptRoomEventSync(roomId, event);
+ if (event.type != EventTypes.Encrypted && store) {
+ await doStore();
+ }
+ return event;
+ }
+
+ /// Encrypts the given json payload and creates a send-ready m.room.encrypted
+ /// payload. This will create a new outgoingGroupSession if necessary.
+ Future