/*
* Famedly Matrix SDK
* Copyright (C) 2019, 2020, 2021 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:olm/olm.dart' as olm;
import '../matrix.dart';
import '../src/utils/run_in_root.dart';
import 'cross_signing.dart';
import 'key_manager.dart';
import 'key_verification_manager.dart';
import 'olm_manager.dart';
import 'ssss.dart';
import 'utils/bootstrap.dart';
class Encryption {
final Client client;
final bool debug;
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;
late KeyManager keyManager;
late OlmManager olmManager;
late KeyVerificationManager keyVerificationManager;
late CrossSigning crossSigning;
late SSSS ssss;
Encryption({
required this.client,
this.debug = false,
}) {
ssss = SSSS(this);
keyManager = KeyManager(this);
olmManager = OlmManager(this);
keyVerificationManager = KeyVerificationManager(this);
crossSigning = CrossSigning(this);
}
// initial login passes null to init a new olm account
Future init(String? olmAccount) async {
await olmManager.init(olmAccount);
_backgroundTasksRunning = true;
_backgroundTasks(); // start the background tasks
}
bool isMinOlmVersion(int major, int minor, int patch) {
try {
final version = olm.get_library_version();
return version[0] > major ||
(version[0] == major &&
(version[1] > minor ||
(version[1] == minor && version[2] >= patch)));
} catch (_) {
return false;
}
}
Bootstrap bootstrap({void Function()? onUpdate}) => Bootstrap(
encryption: this,
onUpdate: onUpdate,
);
void handleDeviceOneTimeKeysCount(
Map? countJson, List? unusedFallbackKeyTypes) {
runInRoot(() => olmManager.handleDeviceOneTimeKeysCount(
countJson, unusedFallbackKeyTypes));
}
void onSync() {
keyVerificationManager.cleanup();
}
Future handleToDeviceEvent(ToDeviceEvent event) async {
if (event.type == EventTypes.RoomKey) {
// a new room key. We need to handle this asap, before other
// events in /sync are handled
await keyManager.handleToDeviceEvent(event);
}
if ([EventTypes.RoomKeyRequest, EventTypes.ForwardedRoomKey]
.contains(event.type)) {
// "just" room key request things. We don't need these asap, so we handle
// them in the background
// ignore: unawaited_futures
runInRoot(() => keyManager.handleToDeviceEvent(event));
}
if (event.type == EventTypes.Dummy) {
// the previous device just had to create a new olm session, due to olm session
// corruption. We want to try to send it the last message we just sent it, if possible
// ignore: unawaited_futures
runInRoot(() => olmManager.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
// ignore: unawaited_futures
runInRoot(() => keyVerificationManager.handleToDeviceEvent(event));
}
if (event.type.startsWith('m.secret.')) {
// some ssss thing. We can do this in the background
// ignore: unawaited_futures
runInRoot(() => ssss.handleToDeviceEvent(event));
}
if (event.sender == client.userID) {
// maybe we need to re-try SSSS secrets
// ignore: unawaited_futures
runInRoot(() => ssss.periodicallyRequestMissingCache());
}
}
Future handleEventUpdate(EventUpdate update) async {
if (update.type == EventUpdateType.ephemeral ||
update.type == EventUpdateType.history) {
return;
}
if (update.content['type'].startsWith('m.key.verification.') ||
(update.content['type'] == EventTypes.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
// ignore: unawaited_futures
runInRoot(() => keyVerificationManager.handleEventUpdate(update));
}
if (update.content['sender'] == client.userID &&
update.content['unsigned']?['transaction_id'] == null) {
// maybe we need to re-try SSSS secrets
// ignore: unawaited_futures
runInRoot(() => ssss.periodicallyRequestMissingCache());
}
}
Future decryptToDeviceEvent(ToDeviceEvent event) async {
try {
return await olmManager.decryptToDeviceEvent(event);
} catch (e, s) {
Logs().w(
'[LibOlm] Could not decrypt to device event from ${event.sender} with content: ${event.content}',
e,
s);
client.onEncryptionError.add(
SdkError(
exception: e is Exception ? e : Exception(e),
stackTrace: s,
),
);
return event;
}
}
Event decryptRoomEventSync(String roomId, Event event) {
final content = event.parsedRoomEncryptedContent;
if (event.type != EventTypes.Encrypted ||
content.ciphertextMegolm == null) {
return event;
}
Map decryptedPayload;
var canRequestSession = false;
try {
if (content.algorithm != AlgorithmTypes.megolmV1AesSha2) {
throw DecryptException(DecryptException.unknownAlgorithm);
}
final sessionId = content.sessionId;
final senderKey = content.senderKey;
if (sessionId == null) {
throw DecryptException(DecryptException.unknownSession);
}
final inboundGroupSession =
keyManager.getInboundGroupSession(roomId, sessionId, senderKey);
if (!(inboundGroupSession?.isValid ?? false)) {
canRequestSession = true;
throw DecryptException(DecryptException.unknownSession);
}
// decrypt errors here may mean we have a bad session key - others might have a better one
canRequestSession = true;
final decryptResult = inboundGroupSession!.inboundGroupSession!
.decrypt(content.ciphertextMegolm!);
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();
final haveIndex =
inboundGroupSession.indexes.containsKey(messageIndexKey);
if (haveIndex &&
inboundGroupSession.indexes[messageIndexKey] != messageIndexValue) {
Logs().e('[Decrypt] Could not decrypt due to a corrupted session.');
throw DecryptException(DecryptException.channelCorrupted);
}
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), 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 (exception.toString() != DecryptException.unknownSession &&
(keyManager
.getOutboundGroupSession(roomId)
?.outboundGroupSession
?.session_id() ??
'') ==
content.sessionId) {
runInRoot(() =>
keyManager.clearOrUseOutboundGroupSession(roomId, wipe: true));
}
if (canRequestSession) {
decryptedPayload = {
'content': event.content,
'type': EventTypes.Encrypted,
};
decryptedPayload['content']['body'] = exception.toString();
decryptedPayload['content']['msgtype'] = MessageTypes.BadEncrypted;
decryptedPayload['content']['can_request_session'] = true;
} else {
decryptedPayload = {
'content': {
'msgtype': MessageTypes.BadEncrypted,
'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,
room: event.room,
originServerTs: event.originServerTs,
unsigned: event.unsigned,
stateKey: event.stateKey,
prevContent: event.prevContent,
status: event.status,
);
}
Future decryptRoomEvent(String roomId, Event event,
{bool store = false,
EventUpdateType updateType = EventUpdateType.timeline}) async {
if (event.type != EventTypes.Encrypted) {
return event;
}
final content = event.parsedRoomEncryptedContent;
final sessionId = content.sessionId;
try {
if (client.database != null &&
sessionId != null &&
!(keyManager
.getInboundGroupSession(
roomId,
sessionId,
content.senderKey,
)
?.isValid ??
false)) {
await keyManager.loadInboundGroupSession(
roomId,
sessionId,
content.senderKey,
);
}
event = decryptRoomEventSync(roomId, event);
if (event.type == EventTypes.Encrypted &&
event.content['can_request_session'] == true &&
sessionId != null) {
keyManager.maybeAutoRequest(
roomId,
sessionId,
content.senderKey,
);
}
if (event.type != EventTypes.Encrypted && store) {
if (updateType != EventUpdateType.history) {
event.room.setState(event);
}
await client.database?.storeEventUpdate(
EventUpdate(
content: event.toJson(),
roomID: roomId,
type: updateType,
),
client,
);
}
return event;
} catch (e, s) {
Logs().e('[Decrypt] Could not decrpyt event', e, s);
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