/* * 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 'dart:async'; import 'package:pedantic/pedantic.dart'; import '../famedlysdk.dart'; import '../matrix_api.dart'; import '../src/utils/run_in_root.dart'; import '../src/utils/logs.dart'; import 'cross_signing.dart'; import 'key_manager.dart'; import 'key_verification_manager.dart'; import 'olm_manager.dart'; import 'ssss.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; CrossSigning crossSigning; SSSS ssss; Encryption({ this.client, this.debug, this.enableE2eeRecovery, }) { ssss = SSSS(this); keyManager = KeyManager(this); olmManager = OlmManager(this); keyVerificationManager = KeyVerificationManager(this); crossSigning = CrossSigning(this); } Future init(String olmAccount) async { await olmManager.init(olmAccount); _backgroundTasksRunning = true; _backgroundTasks(); // start the background tasks } void handleDeviceOneTimeKeysCount(Map countJson) { runInRoot(() => olmManager.handleDeviceOneTimeKeysCount(countJson)); } void onSync() { keyVerificationManager.cleanup(); } Future handleToDeviceEvent(ToDeviceEvent event) async { if (event.type == 'm.room_key') { // a new room key. We need to handle this asap, before other // events in /sync are handled await keyManager.handleToDeviceEvent(event); } if (['m.room_key_request', 'm.forwarded_room_key'].contains(event.type)) { // "just" room key request things. We don't need these asap, so we handle // them in the background unawaited(runInRoot(() => 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( runInRoot(() => keyVerificationManager.handleToDeviceEvent(event))); } if (event.type.startsWith('m.secret.')) { // some ssss thing. We can do this in the background unawaited(runInRoot(() => ssss.handleToDeviceEvent(event))); } if (event.sender == client.userID) { // maybe we need to re-try SSSS secrets unawaited(runInRoot(() => ssss.periodicallyRequestMissingCache())); } } Future handleEventUpdate(EventUpdate update) async { if (update.type == 'ephemeral') { return; } if (update.eventType.startsWith('m.key.verification.') || (update.eventType == 'm.room.message' && (update.content['content']['msgtype'] is String) && update.content['content']['msgtype'] .startsWith('m.key.verification.'))) { // "just" key verification, no need to do this in sync unawaited( runInRoot(() => keyVerificationManager.handleEventUpdate(update))); } if (update.content['sender'] == client.userID && !update.content['unsigned'].containsKey('transaction_id')) { // maybe we need to re-try SSSS secrets unawaited(runInRoot(() => ssss.periodicallyRequestMissingCache())); } } 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; var canRequestSession = false; 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) { canRequestSession = true; throw (DecryptError.UNKNOWN_SESSION); } // decrypt errors here may mean we have a bad session key - others might have a better one canRequestSession = true; final decryptResult = inboundGroupSession.inboundGroupSession .decrypt(event.content['ciphertext']); canRequestSession = false; // we can't have the key be an int, else json-serializing will fail, thus we need it to be a string final messageIndexKey = 'key-' + decryptResult.message_index.toString(); final messageIndexValue = event.eventId + '|' + event.originServerTs.millisecondsSinceEpoch.toString(); var haveIndex = inboundGroupSession.indexes.containsKey(messageIndexKey); if (haveIndex && inboundGroupSession.indexes[messageIndexKey] != messageIndexValue) { // TODO: maybe clear outbound session, if it is ours // TODO: Make it so that we can't re-request the session keys, this is just for debugging Logs.error('[Decrypt] Could not decrypt due to a corrupted session.'); Logs.error('[Decrypt] Want session: $roomId $sessionId $senderKey'); Logs.error( '[Decrypt] Have sessoin: ${inboundGroupSession.roomId} ${inboundGroupSession.sessionId} ${inboundGroupSession.senderKey}'); Logs.error( '[Decrypt] Want indexes: $messageIndexKey $messageIndexValue'); Logs.error( '[Decrypt] Have indexes: $messageIndexKey ${inboundGroupSession.indexes[messageIndexKey]}'); canRequestSession = true; throw (DecryptError.CHANNEL_CORRUPTED); } inboundGroupSession.indexes[messageIndexKey] = messageIndexValue; 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 (canRequestSession) { decryptedPayload = { 'content': event.content, 'type': EventTypes.Encrypted, }; decryptedPayload['content']['body'] = exception.toString(); decryptedPayload['content']['msgtype'] = 'm.bad.encrypted'; decryptedPayload['content']['can_request_session'] = true; } 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> encryptGroupMessagePayload( String roomId, Map payload, {String type = EventTypes.Message}) async { final room = client.getRoomById(roomId); if (room == null || !room.encrypted || !enabled) { return payload; } if (room.encryptionAlgorithm != 'm.megolm.v1.aes-sha2') { throw ('Unknown encryption algorithm'); } if (keyManager.getOutboundGroupSession(roomId) == null) { await keyManager.loadOutboundGroupSession(roomId); } await keyManager.clearOutboundGroupSession(roomId); if (keyManager.getOutboundGroupSession(roomId) == null) { await keyManager.createOutboundGroupSession(roomId); } final sess = keyManager.getOutboundGroupSession(roomId); if (sess == null) { throw ('Unable to create new outbound group session'); } // we clone the payload as we do not want to remove 'm.relates_to' from the // original payload passed into this function payload = Map.from(payload); final Map mRelatesTo = payload.remove('m.relates_to'); final payloadContent = { 'content': payload, 'type': type, 'room_id': roomId, }; var encryptedPayload = { 'algorithm': 'm.megolm.v1.aes-sha2', 'ciphertext': sess.outboundGroupSession.encrypt(json.encode(payloadContent)), 'device_id': client.deviceID, 'sender_key': identityKey, 'session_id': sess.outboundGroupSession.session_id(), if (mRelatesTo != null) 'm.relates_to': mRelatesTo, }; sess.sentMessages++; await keyManager.storeOutboundGroupSession(roomId, sess); return encryptedPayload; } Future> encryptToDeviceMessagePayload( DeviceKeys device, String type, Map payload) async { return await olmManager.encryptToDeviceMessagePayload( device, type, payload); } Future> encryptToDeviceMessage( List deviceKeys, String type, Map payload) async { return await olmManager.encryptToDeviceMessage(deviceKeys, type, payload); } Future autovalidateMasterOwnKey() async { // check if we can set our own master key as verified, if it isn't yet if (client.database != null && client.userDeviceKeys.containsKey(client.userID)) { final masterKey = client.userDeviceKeys[client.userID].masterKey; if (masterKey != null && !masterKey.directVerified && masterKey .hasValidSignatureChain(onlyValidateUserIds: {client.userID})) { await masterKey.setVerified(true); } } } // this method is responsible for all background tasks, such as uploading online key backups bool _backgroundTasksRunning = true; void _backgroundTasks() { if (!_backgroundTasksRunning) { return; } keyManager.backgroundTasks(); autovalidateMasterOwnKey(); if (_backgroundTasksRunning) { Timer(Duration(seconds: 10), _backgroundTasks); } } void dispose() { _backgroundTasksRunning = false; keyManager.dispose(); olmManager.dispose(); keyVerificationManager.dispose(); } } abstract class DecryptError { static const String NOT_ENABLED = 'Encryption is not enabled in your client.'; static const String UNKNOWN_ALGORITHM = 'Unknown encryption algorithm.'; static const String UNKNOWN_SESSION = 'The sender has not sent us the session key.'; static const String CHANNEL_CORRUPTED = 'The secure channel with the sender was corrupted.'; }