mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-10-09 02:54:14 +00:00
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
234 lines
7.3 KiB
Dart
234 lines
7.3 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
|
// ignore: implementation_imports
|
|
import 'package:libsignal_protocol_dart/src/invalid_message_exception.dart';
|
|
import 'package:twonly/core/bridge/wrapper/signal.dart';
|
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
|
as pb;
|
|
import 'package:twonly/src/services/api/messages.api.dart';
|
|
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
|
|
import 'package:twonly/src/services/signal/session.signal.dart';
|
|
import 'package:twonly/src/services/signal/utils.signal.dart';
|
|
import 'package:twonly/src/utils/log.dart';
|
|
|
|
class SignalEncryptResult {
|
|
SignalEncryptResult(this.ciphertext, this.type);
|
|
final Uint8List ciphertext;
|
|
final pb.Message_Type type;
|
|
}
|
|
|
|
Future<SignalEncryptResult?> signalEncryptMessage(
|
|
int target,
|
|
Uint8List plaintextContent,
|
|
) async {
|
|
return lockingSignalProtocol.protect<SignalEncryptResult?>(() async {
|
|
return _signalEncryptMessageV1(target, plaintextContent);
|
|
});
|
|
}
|
|
|
|
Future<SignalEncryptResult?> signalEncryptMessageV2(
|
|
int target,
|
|
Uint8List plaintextContent,
|
|
) async {
|
|
try {
|
|
final res = await RustSignal.encrypt(
|
|
name: target.toString(),
|
|
deviceId: 1,
|
|
plaintext: plaintextContent,
|
|
);
|
|
return SignalEncryptResult(res, pb.Message_Type.CIPHERTEXT_V2);
|
|
} catch (e) {
|
|
Log.error('Could not encrypt message (V2) for target $target: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<SignalEncryptResult?> _signalEncryptMessageV1(
|
|
int target,
|
|
Uint8List plaintextContent,
|
|
) async {
|
|
try {
|
|
final signalStore = (await getSignalStore())!;
|
|
final address = getSignalAddress(target);
|
|
final session = SessionCipher.fromStore(signalStore, address);
|
|
final cipherText = await session.encrypt(plaintextContent);
|
|
|
|
pb.Message_Type type;
|
|
switch (cipherText.getType()) {
|
|
case CiphertextMessage.prekeyType:
|
|
type = pb.Message_Type.PREKEY_BUNDLE;
|
|
case CiphertextMessage.whisperType:
|
|
type = pb.Message_Type.CIPHERTEXT;
|
|
default:
|
|
Log.error('Invalid ciphertext type: ${cipherText.getType()}.');
|
|
return null;
|
|
}
|
|
|
|
return SignalEncryptResult(cipherText.serialize(), type);
|
|
} catch (e) {
|
|
Log.error('Could not encrypt message (V1) for target $target: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<(pb.EncryptedContent?, pb.PlaintextContent_DecryptionErrorMessage_Type?)>
|
|
signalDecryptMessageV2(
|
|
int fromUserId,
|
|
Uint8List encryptedContentRaw, {
|
|
Set<int>? brokenSessionsInCurrentBatch,
|
|
}) async {
|
|
Log.info('Acquiring lockingSignalProtocol for $fromUserId (V2)');
|
|
final (
|
|
decryptedContent,
|
|
errorType,
|
|
needsResync,
|
|
) = await lockingSignalProtocol.protect(() async {
|
|
Log.info('Lock acquired for $fromUserId (V2)');
|
|
try {
|
|
final plaintext = await RustSignal.decrypt(
|
|
name: fromUserId.toString(),
|
|
deviceId: 1,
|
|
ciphertext: encryptedContentRaw,
|
|
);
|
|
recordResyncAttempt(fromUserId, success: true);
|
|
return (pb.EncryptedContent.fromBuffer(plaintext), null, false);
|
|
} catch (e) {
|
|
Log.error('Could not decrypt message (V2) from $fromUserId: $e');
|
|
return (
|
|
null,
|
|
pb.PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
|
true, // Needs resync on failure
|
|
);
|
|
}
|
|
});
|
|
|
|
Log.info('Released lockingSignalProtocol for $fromUserId (V2)');
|
|
|
|
if (needsResync) {
|
|
brokenSessionsInCurrentBatch?.add(fromUserId);
|
|
if (shouldAttemptResync(fromUserId)) {
|
|
if (await handleSessionResync(fromUserId)) {
|
|
recordResyncAttempt(fromUserId, success: false);
|
|
await sendCipherText(
|
|
fromUserId,
|
|
pb.EncryptedContent(
|
|
errorMessages: pb.EncryptedContent_ErrorMessages(
|
|
type: pb.EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return (decryptedContent, errorType);
|
|
}
|
|
|
|
Future<(pb.EncryptedContent?, pb.PlaintextContent_DecryptionErrorMessage_Type?)>
|
|
signalDecryptMessageV1(
|
|
int fromUserId,
|
|
Uint8List encryptedContentRaw,
|
|
int type, {
|
|
Set<int>? brokenSessionsInCurrentBatch,
|
|
}) async {
|
|
Log.info('Acquiring lockingSignalProtocol for $fromUserId (V1)');
|
|
final (
|
|
decryptedContent,
|
|
errorType,
|
|
needsResync,
|
|
) = await lockingSignalProtocol.protect(() async {
|
|
Log.info('Lock acquired for $fromUserId (V1)');
|
|
try {
|
|
// Yield execution to the event loop to prevent UI freezing during bulk decryption
|
|
await Future.delayed(Duration.zero);
|
|
|
|
final session = SessionCipher.fromStore(
|
|
(await getSignalStore())!,
|
|
getSignalAddress(fromUserId),
|
|
);
|
|
|
|
Uint8List plaintext;
|
|
|
|
switch (type) {
|
|
case CiphertextMessage.prekeyType:
|
|
plaintext = await session.decrypt(
|
|
PreKeySignalMessage(encryptedContentRaw),
|
|
);
|
|
case CiphertextMessage.whisperType:
|
|
plaintext = await session.decryptFromSignal(
|
|
SignalMessage.fromSerialized(encryptedContentRaw),
|
|
);
|
|
default:
|
|
Log.error('Unknown Message Decryption Type: $type');
|
|
return (
|
|
null,
|
|
pb.PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
|
false,
|
|
);
|
|
}
|
|
|
|
recordResyncAttempt(fromUserId, success: true);
|
|
return (pb.EncryptedContent.fromBuffer(plaintext), null, false);
|
|
} on InvalidKeyIdException catch (e) {
|
|
Log.warn(e);
|
|
return (
|
|
null,
|
|
pb.PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN,
|
|
false,
|
|
);
|
|
} on DuplicateMessageException catch (e) {
|
|
// This is normal behavior: This can happen in case a message was decrypted, but before further processing
|
|
// the user killed the app. This results in a new transmission from the server, but as the message was already
|
|
// decrypted, this error happens. In this case, request the message again.
|
|
Log.info(e);
|
|
return (
|
|
null,
|
|
pb.PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
|
false,
|
|
);
|
|
} on InvalidMessageException catch (e) {
|
|
Log.warn(e);
|
|
return (
|
|
null,
|
|
pb.PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
|
true,
|
|
);
|
|
} catch (e) {
|
|
Log.error(e);
|
|
return (
|
|
null,
|
|
pb.PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
|
|
false,
|
|
);
|
|
}
|
|
});
|
|
|
|
Log.info('Released lockingSignalProtocol for $fromUserId (V1)');
|
|
|
|
// Handle session resync OUTSIDE the lock to avoid holding it during
|
|
// network round-trips (which can block for up to 60 seconds)
|
|
if (needsResync) {
|
|
brokenSessionsInCurrentBatch?.add(fromUserId);
|
|
if (shouldAttemptResync(fromUserId)) {
|
|
if (await handleSessionResync(fromUserId)) {
|
|
// This flag prevents from resyncing the session the client received
|
|
// multiple new messages from the server he could not decrypt
|
|
recordResyncAttempt(fromUserId, success: false);
|
|
|
|
// This message contains a new PreKeyBundle establishing a new signal
|
|
// session
|
|
await sendCipherText(
|
|
fromUserId,
|
|
pb.EncryptedContent(
|
|
errorMessages: pb.EncryptedContent_ErrorMessages(
|
|
type: pb.EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return (decryptedContent, errorType);
|
|
}
|