remove legacy signal encryption

This commit is contained in:
otsmr 2026-08-29 13:44:10 +02:00
parent da9752d317
commit e43169c2d2
94 changed files with 1625 additions and 6501 deletions

View file

@ -63,25 +63,6 @@ dependencies:
git: https://github.com/Pyozer/dots_indicator.git
commit: 508f5883ac79bdbc10254092de3f28f571d261cd
commit: 4a90e557630b28834479ed9c64a9d2d0185d8e48
libsignal_protocol_dart:
git: https://github.com/MixinNetwork/libsignal_protocol_dart.git
dependencies:
adaptive_number:
git: https://github.com/lemoony/adaptive_number_dart
commit: ea9178fdd4d82ac45cf0ec966ac870dae661124f
ed25519_edwards:
git: https://github.com/Tougee/ed25519.git
commit: 7353ba759ea9f4646cbf481c2ef949625c8ce4cf
optional:
git: https://github.com/tonio-ramirez/optional.dart.git
commit: 71c638891ce4f2aff35c7387727989f31f9d877d
pointycastle:
git: https://github.com/bcgit/pc-dart.git
commit: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
x25519:
git: https://github.com/Tougee/curve25519.git
commit: ecb1d357714537bba6e276ef45f093846d4beaee
commit: c95a1586057022acdbb9c76b1692d94cc549bcc7
lottie:
git: https://github.com/xvrh/lottie-flutter.git
commit: 127bc29f2c6bd8b32ec4064a09e54e6b31cd0a88

View file

@ -12,20 +12,10 @@ void main() {
// Initialize global variables
await initBackgroundExecution();
// Try to connect to the API server
final connected = await apiService.connect();
// Check the API connection state
final state = await RustApi.connectionState();
// Print out the result or test it
expect(connected, isA<bool>());
// We can also check if it's connected
// Depending on your test environment, this might be true or false
// if the server is unreachable without further setup
// expect(apiService.isConnected, isA<bool>());
// Close the connection after the test
if (apiService.isConnected) {
await apiService.close(() {});
}
expect(state, isA<ApiConnectionState>());
});
}

View file

@ -122,6 +122,14 @@ class RustApi {
static Future<void> downloadPendingMedia() =>
RustLib.instance.api.crateBridgeApiRustApiDownloadPendingMedia();
static Future<void> establishSignalSession({
required PlatformInt64 contactId,
Uint8List? expectedPublicKey,
}) => RustLib.instance.api.crateBridgeApiRustApiEstablishSignalSession(
contactId: contactId,
expectedPublicKey: expectedPublicKey,
);
static Stream<ApiEvent> events() =>
RustLib.instance.api.crateBridgeApiRustApiEvents();
@ -330,9 +338,9 @@ class RustApi {
required PlatformInt64 contactId,
required List<int> content,
String? messageId,
required bool onlySendIfNoReceiptsAreOpen,
required bool onlyReturnEncryptedData,
required bool blocking,
bool? onlySendIfNoReceiptsAreOpen,
bool? onlyReturnEncryptedData,
bool? blocking,
}) => RustLib.instance.api.crateBridgeApiRustApiSendEncryptedContent(
contactId: contactId,
content: content,
@ -346,7 +354,7 @@ class RustApi {
required String groupId,
required List<int> content,
String? messageId,
required bool onlySendIfNoReceiptsAreOpen,
bool? onlySendIfNoReceiptsAreOpen,
}) => RustLib.instance.api.crateBridgeApiRustApiSendEncryptedContentToGroup(
groupId: groupId,
content: content,
@ -410,6 +418,8 @@ class RustApi {
);
static Future<void> uploadPqcPreKeys({
required List<int> publicIdentityKey,
required PlatformInt64 registrationId,
required PlatformInt64 eccSignedPrekeyId,
required List<int> eccSignedPrekey,
required List<int> eccSignedPrekeySignature,
@ -418,6 +428,8 @@ class RustApi {
required List<int> kyberSignedPrekeySignature,
required List<PqcPreKeyInput> prekeys,
}) => RustLib.instance.api.crateBridgeApiRustApiUploadPqcPreKeys(
publicIdentityKey: publicIdentityKey,
registrationId: registrationId,
eccSignedPrekeyId: eccSignedPrekeyId,
eccSignedPrekey: eccSignedPrekey,
eccSignedPrekeySignature: eccSignedPrekeySignature,

View file

@ -8,24 +8,12 @@ import '../user_config.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `get_callbacks`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `Api`, `FlutterCallbacks`, `LegacySignal`, `Logging`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `Api`, `FlutterCallbacks`, `Logging`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`
Future<void> initFlutterCallbacks({
required int callbackId,
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
required FutureOr<LegacySignalDecryptResult> Function(
PlatformInt64,
Uint8List,
int,
)
legacySignalDecrypt,
required FutureOr<LegacySignalEncryptResult?> Function(
PlatformInt64,
Uint8List,
)
legacySignalEncrypt,
required FutureOr<void> Function(PlatformInt64) apiResyncSignalSession,
required FutureOr<void> Function(String, String, PlatformInt64, String)
apiMediaAction,
required FutureOr<void> Function(PlatformInt64, Uint8List)
@ -36,59 +24,9 @@ Future<void> initFlutterCallbacks({
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
callbackId: callbackId,
loggingGetStreamSink: loggingGetStreamSink,
legacySignalDecrypt: legacySignalDecrypt,
legacySignalEncrypt: legacySignalEncrypt,
apiResyncSignalSession: apiResyncSignalSession,
apiMediaAction: apiMediaAction,
apiVerificationProof: apiVerificationProof,
apiCreatePushAvatars: apiCreatePushAvatars,
apiMediaReceived: apiMediaReceived,
apiUserConfigChanged: apiUserConfigChanged,
);
class LegacySignalDecryptResult {
/// Serialized `EncryptedContent` when legacy Signal decryption succeeded.
final Uint8List? plaintext;
/// Serialized protobuf enum value for `DecryptionErrorMessage.Type`.
final int? decryptionErrorType;
const LegacySignalDecryptResult({
this.plaintext,
this.decryptionErrorType,
});
@override
int get hashCode => plaintext.hashCode ^ decryptionErrorType.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LegacySignalDecryptResult &&
runtimeType == other.runtimeType &&
plaintext == other.plaintext &&
decryptionErrorType == other.decryptionErrorType;
}
class LegacySignalEncryptResult {
final Uint8List ciphertext;
/// `Message.Type.CIPHERTEXT` or `Message.Type.PREKEY_BUNDLE`.
final int messageType;
const LegacySignalEncryptResult({
required this.ciphertext,
required this.messageType,
});
@override
int get hashCode => ciphertext.hashCode ^ messageType.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LegacySignalEncryptResult &&
runtimeType == other.runtimeType &&
ciphertext == other.ciphertext &&
messageType == other.messageType;
}

View file

@ -47,36 +47,15 @@ class RustKeyManager {
static Future<void> importSignalIdentity({
required List<int> identityKeyPairStructure,
required PlatformInt64 registrationId,
required Map<PlatformInt64, Uint8List> signedPreKeyStore,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity(
identityKeyPairStructure: identityKeyPairStructure,
registrationId: registrationId,
signedPreKeyStore: signedPreKeyStore,
);
static Future<Uint8List?> loadSignedPrekey({
required PlatformInt64 signedPreKeyId,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekey(
signedPreKeyId: signedPreKeyId,
);
static Future<Map<PlatformInt64, Uint8List>> loadSignedPrekeys() => RustLib
.instance
.api
.crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeys();
static Future<void> removeKeyManager() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager();
static Future<void> removeSignedPrekey({
required PlatformInt64 signedPreKeyId,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekey(
signedPreKeyId: signedPreKeyId,
);
/// Serialize the key_manager. Needed for the passwordless_recovery feature.
static Future<Uint8List> serialize() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerSerialize();
@ -86,15 +65,6 @@ class RustKeyManager {
.api
.crateBridgeWrapperKeyManagerRustKeyManagerSetUserId(userId: userId);
static Future<void> storeSignedPrekey({
required PlatformInt64 signedPreKeyId,
required List<int> record,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekey(
signedPreKeyId: signedPreKeyId,
record: record,
);
@override
int get hashCode => 0;

View file

@ -36,6 +36,16 @@ class RustSignal {
static Future<List<FrbPqcPreKey>> generatePqcPrekeys() => RustLib.instance.api
.crateBridgeWrapperSignalRustSignalGeneratePqcPrekeys();
static Future<Uint8List?> getContactPublicKey({
required PlatformInt64 contactId,
}) => RustLib.instance.api
.crateBridgeWrapperSignalRustSignalGetContactPublicKey(
contactId: contactId,
);
static Future<Uint8List> getUserPublicKey() =>
RustLib.instance.api.crateBridgeWrapperSignalRustSignalGetUserPublicKey();
static Future<void> processPrekeyBundle({
required String name,
required int deviceId,

File diff suppressed because it is too large Load diff

View file

@ -60,24 +60,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
FutureOr<void> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw);
@protected
FutureOr<LegacySignalEncryptResult?> Function(PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
dynamic raw,
);
@protected
FutureOr<void> Function(PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_unit_AnyhowException(
dynamic raw,
);
@protected
FutureOr<LegacySignalDecryptResult> Function(PlatformInt64, Uint8List, int)
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_i_32_Output_legacy_signal_decrypt_result_AnyhowException(
dynamic raw,
);
@protected
FutureOr<void> Function(UserConfig)
dco_decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(dynamic raw);
@ -88,11 +76,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
@protected
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
dynamic raw,
);
@protected
RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw);
@ -126,26 +109,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected
bool dco_decode_box_autoadd_bool(dynamic raw);
@protected
double dco_decode_box_autoadd_f_64(dynamic raw);
@protected
FrbPreKeyBundle dco_decode_box_autoadd_frb_pre_key_bundle(dynamic raw);
@protected
int dco_decode_box_autoadd_i_32(dynamic raw);
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@protected
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected
LegacySignalEncryptResult dco_decode_box_autoadd_legacy_signal_encrypt_result(
dynamic raw,
);
@protected
PasswordlessRecoveryConfig
dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw);
@ -189,16 +167,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw);
@protected
LegacySignalDecryptResult dco_decode_legacy_signal_decrypt_result(
dynamic raw,
);
@protected
LegacySignalEncryptResult dco_decode_legacy_signal_encrypt_result(
dynamic raw,
);
@protected
LegacyTableMigrationCount dco_decode_legacy_table_migration_count(
dynamic raw,
@ -230,10 +198,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<(PlatformInt64, Uint8List)>
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
@protected
List<(String, List<String>)> dco_decode_list_record_string_list_string(
dynamic raw,
@ -261,19 +225,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
bool? dco_decode_opt_box_autoadd_bool(dynamic raw);
@protected
double? dco_decode_opt_box_autoadd_f_64(dynamic raw);
@protected
int? dco_decode_opt_box_autoadd_i_32(dynamic raw);
@protected
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw);
@protected
LegacySignalEncryptResult?
dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw);
@protected
PasswordlessRecoveryConfig?
dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw);
@ -301,11 +261,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw);
@protected
(PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict(
dynamic raw,
);
@protected
(Uint8List, PlatformInt64) dco_decode_record_list_prim_u_8_strict_i_64(
dynamic raw,
@ -397,11 +352,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
SseDeserializer deserializer,
);
@protected
RustStreamSink<String> sse_decode_StreamSink_String_Sse(
SseDeserializer deserializer,
@ -447,6 +397,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
bool sse_decode_box_autoadd_bool(SseDeserializer deserializer);
@protected
double sse_decode_box_autoadd_f_64(SseDeserializer deserializer);
@ -455,20 +408,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
int sse_decode_box_autoadd_i_32(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
@protected
LegacySignalEncryptResult sse_decode_box_autoadd_legacy_signal_encrypt_result(
SseDeserializer deserializer,
);
@protected
PasswordlessRecoveryConfig
sse_decode_box_autoadd_passwordless_recovery_config(
@ -522,16 +467,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
LegacySignalDecryptResult sse_decode_legacy_signal_decrypt_result(
SseDeserializer deserializer,
);
@protected
LegacySignalEncryptResult sse_decode_legacy_signal_encrypt_result(
SseDeserializer deserializer,
);
@protected
LegacyTableMigrationCount sse_decode_legacy_table_migration_count(
SseDeserializer deserializer,
@ -569,12 +504,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<(PlatformInt64, Uint8List)>
sse_decode_list_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
List<(String, List<String>)> sse_decode_list_record_string_list_string(
SseDeserializer deserializer,
@ -602,21 +531,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
bool? sse_decode_opt_box_autoadd_bool(SseDeserializer deserializer);
@protected
double? sse_decode_opt_box_autoadd_f_64(SseDeserializer deserializer);
@protected
int? sse_decode_opt_box_autoadd_i_32(SseDeserializer deserializer);
@protected
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer);
@protected
LegacySignalEncryptResult?
sse_decode_opt_box_autoadd_legacy_signal_encrypt_result(
SseDeserializer deserializer,
);
@protected
PasswordlessRecoveryConfig?
sse_decode_opt_box_autoadd_passwordless_recovery_config(
@ -650,11 +573,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer);
@protected
(PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
(Uint8List, PlatformInt64) sse_decode_record_list_prim_u_8_strict_i_64(
SseDeserializer deserializer,
@ -776,14 +694,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
FutureOr<LegacySignalEncryptResult?> Function(PlatformInt64, Uint8List)
self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_unit_AnyhowException(
@ -791,14 +701,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_i_32_Output_legacy_signal_decrypt_result_AnyhowException(
FutureOr<LegacySignalDecryptResult> Function(PlatformInt64, Uint8List, int)
self,
SseSerializer serializer,
);
@protected
void sse_encode_DartFn_Inputs_user_config_Output_unit_AnyhowException(
FutureOr<void> Function(UserConfig) self,
@ -814,12 +716,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
Map<PlatformInt64, Uint8List> self,
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_String_Sse(
RustStreamSink<String> self,
@ -874,6 +770,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_f_64(double self, SseSerializer serializer);
@ -883,9 +782,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_32(int self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
@ -898,12 +794,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_legacy_signal_encrypt_result(
LegacySignalEncryptResult self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_passwordless_recovery_config(
PasswordlessRecoveryConfig self,
@ -967,18 +857,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_legacy_signal_decrypt_result(
LegacySignalDecryptResult self,
SseSerializer serializer,
);
@protected
void sse_encode_legacy_signal_encrypt_result(
LegacySignalEncryptResult self,
SseSerializer serializer,
);
@protected
void sse_encode_legacy_table_migration_count(
LegacyTableMigrationCount self,
@ -1027,12 +905,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_list_record_i_64_list_prim_u_8_strict(
List<(PlatformInt64, Uint8List)> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_record_string_list_string(
List<(String, List<String>)> self,
@ -1067,10 +939,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
);
@protected
void sse_encode_opt_box_autoadd_f_64(double? self, SseSerializer serializer);
void sse_encode_opt_box_autoadd_bool(bool? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_i_32(int? self, SseSerializer serializer);
void sse_encode_opt_box_autoadd_f_64(double? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_i_64(
@ -1078,12 +950,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_legacy_signal_encrypt_result(
LegacySignalEncryptResult? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_passwordless_recovery_config(
PasswordlessRecoveryConfig? self,
@ -1126,12 +992,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_record_i_64_list_prim_u_8_strict(
(PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_list_prim_u_8_strict_i_64(
(Uint8List, PlatformInt64) self,

View file

@ -62,24 +62,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
FutureOr<void> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw);
@protected
FutureOr<LegacySignalEncryptResult?> Function(PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
dynamic raw,
);
@protected
FutureOr<void> Function(PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_unit_AnyhowException(
dynamic raw,
);
@protected
FutureOr<LegacySignalDecryptResult> Function(PlatformInt64, Uint8List, int)
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_i_32_Output_legacy_signal_decrypt_result_AnyhowException(
dynamic raw,
);
@protected
FutureOr<void> Function(UserConfig)
dco_decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(dynamic raw);
@ -90,11 +78,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
@protected
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
dynamic raw,
);
@protected
RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw);
@ -128,26 +111,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected
bool dco_decode_box_autoadd_bool(dynamic raw);
@protected
double dco_decode_box_autoadd_f_64(dynamic raw);
@protected
FrbPreKeyBundle dco_decode_box_autoadd_frb_pre_key_bundle(dynamic raw);
@protected
int dco_decode_box_autoadd_i_32(dynamic raw);
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@protected
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected
LegacySignalEncryptResult dco_decode_box_autoadd_legacy_signal_encrypt_result(
dynamic raw,
);
@protected
PasswordlessRecoveryConfig
dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw);
@ -191,16 +169,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw);
@protected
LegacySignalDecryptResult dco_decode_legacy_signal_decrypt_result(
dynamic raw,
);
@protected
LegacySignalEncryptResult dco_decode_legacy_signal_encrypt_result(
dynamic raw,
);
@protected
LegacyTableMigrationCount dco_decode_legacy_table_migration_count(
dynamic raw,
@ -232,10 +200,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<(PlatformInt64, Uint8List)>
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
@protected
List<(String, List<String>)> dco_decode_list_record_string_list_string(
dynamic raw,
@ -263,19 +227,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
bool? dco_decode_opt_box_autoadd_bool(dynamic raw);
@protected
double? dco_decode_opt_box_autoadd_f_64(dynamic raw);
@protected
int? dco_decode_opt_box_autoadd_i_32(dynamic raw);
@protected
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw);
@protected
LegacySignalEncryptResult?
dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw);
@protected
PasswordlessRecoveryConfig?
dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw);
@ -303,11 +263,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw);
@protected
(PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict(
dynamic raw,
);
@protected
(Uint8List, PlatformInt64) dco_decode_record_list_prim_u_8_strict_i_64(
dynamic raw,
@ -399,11 +354,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
SseDeserializer deserializer,
);
@protected
RustStreamSink<String> sse_decode_StreamSink_String_Sse(
SseDeserializer deserializer,
@ -449,6 +399,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
bool sse_decode_box_autoadd_bool(SseDeserializer deserializer);
@protected
double sse_decode_box_autoadd_f_64(SseDeserializer deserializer);
@ -457,20 +410,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
int sse_decode_box_autoadd_i_32(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
@protected
LegacySignalEncryptResult sse_decode_box_autoadd_legacy_signal_encrypt_result(
SseDeserializer deserializer,
);
@protected
PasswordlessRecoveryConfig
sse_decode_box_autoadd_passwordless_recovery_config(
@ -524,16 +469,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
LegacySignalDecryptResult sse_decode_legacy_signal_decrypt_result(
SseDeserializer deserializer,
);
@protected
LegacySignalEncryptResult sse_decode_legacy_signal_encrypt_result(
SseDeserializer deserializer,
);
@protected
LegacyTableMigrationCount sse_decode_legacy_table_migration_count(
SseDeserializer deserializer,
@ -571,12 +506,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<(PlatformInt64, Uint8List)>
sse_decode_list_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
List<(String, List<String>)> sse_decode_list_record_string_list_string(
SseDeserializer deserializer,
@ -604,21 +533,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
bool? sse_decode_opt_box_autoadd_bool(SseDeserializer deserializer);
@protected
double? sse_decode_opt_box_autoadd_f_64(SseDeserializer deserializer);
@protected
int? sse_decode_opt_box_autoadd_i_32(SseDeserializer deserializer);
@protected
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer);
@protected
LegacySignalEncryptResult?
sse_decode_opt_box_autoadd_legacy_signal_encrypt_result(
SseDeserializer deserializer,
);
@protected
PasswordlessRecoveryConfig?
sse_decode_opt_box_autoadd_passwordless_recovery_config(
@ -652,11 +575,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer);
@protected
(PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
(Uint8List, PlatformInt64) sse_decode_record_list_prim_u_8_strict_i_64(
SseDeserializer deserializer,
@ -778,14 +696,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
FutureOr<LegacySignalEncryptResult?> Function(PlatformInt64, Uint8List)
self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_unit_AnyhowException(
@ -793,14 +703,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_i_32_Output_legacy_signal_decrypt_result_AnyhowException(
FutureOr<LegacySignalDecryptResult> Function(PlatformInt64, Uint8List, int)
self,
SseSerializer serializer,
);
@protected
void sse_encode_DartFn_Inputs_user_config_Output_unit_AnyhowException(
FutureOr<void> Function(UserConfig) self,
@ -816,12 +718,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
Map<PlatformInt64, Uint8List> self,
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_String_Sse(
RustStreamSink<String> self,
@ -876,6 +772,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_f_64(double self, SseSerializer serializer);
@ -885,9 +784,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_32(int self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
@ -900,12 +796,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_legacy_signal_encrypt_result(
LegacySignalEncryptResult self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_passwordless_recovery_config(
PasswordlessRecoveryConfig self,
@ -969,18 +859,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_legacy_signal_decrypt_result(
LegacySignalDecryptResult self,
SseSerializer serializer,
);
@protected
void sse_encode_legacy_signal_encrypt_result(
LegacySignalEncryptResult self,
SseSerializer serializer,
);
@protected
void sse_encode_legacy_table_migration_count(
LegacyTableMigrationCount self,
@ -1029,12 +907,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_list_record_i_64_list_prim_u_8_strict(
List<(PlatformInt64, Uint8List)> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_record_string_list_string(
List<(String, List<String>)> self,
@ -1069,10 +941,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
);
@protected
void sse_encode_opt_box_autoadd_f_64(double? self, SseSerializer serializer);
void sse_encode_opt_box_autoadd_bool(bool? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_i_32(int? self, SseSerializer serializer);
void sse_encode_opt_box_autoadd_f_64(double? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_i_64(
@ -1080,12 +952,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_legacy_signal_encrypt_result(
LegacySignalEncryptResult? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_passwordless_recovery_config(
PasswordlessRecoveryConfig? self,
@ -1128,12 +994,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_record_i_64_list_prim_u_8_strict(
(PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_list_prim_u_8_strict_i_64(
(Uint8List, PlatformInt64) self,

View file

@ -1,5 +1,4 @@
import 'package:get_it/get_it.dart';
import 'package:twonly/src/database/signal.db.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/api.service.dart';
import 'package:twonly/src/services/news.service.dart';
@ -16,12 +15,10 @@ void setupLocator() {
..registerLazySingleton<UserService>(UserService.new)
..registerLazySingleton<ApiService>(ApiService.new)
..registerLazySingleton<TwonlyDB>(TwonlyDB.new)
..registerLazySingleton<SignalDB>(SignalDB.new)
..registerLazySingleton<NewsService>(NewsService.new);
}
UserService get userService => locator<UserService>();
ApiService get apiService => locator<ApiService>();
TwonlyDB get twonlyDB => locator<TwonlyDB>();
SignalDB get signalDB => locator<SignalDB>();
NewsService get newsService => locator<NewsService>();

View file

@ -1,13 +1,11 @@
import 'package:twonly/core/bridge/callbacks.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/callbacks/legacy_signal.callbacks.dart';
import 'package:twonly/src/callbacks/logging.callbacks.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/key_verification.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/avatars.dart';
@ -31,9 +29,6 @@ Future<void> initFlutterCallbacksForRust() async {
await initFlutterCallbacks(
callbackId: isolateCallbackId,
loggingGetStreamSink: LoggingCallbacks.getStreamSink,
legacySignalDecrypt: LegacySignalCallbacks.decrypt,
legacySignalEncrypt: LegacySignalCallbacks.encrypt,
apiResyncSignalSession: handleSessionResync,
apiMediaAction: _apiMediaAction,
apiVerificationProof: KeyVerificationService.handleVerificationProof,
apiCreatePushAvatars: (contactId) =>

View file

@ -1,67 +0,0 @@
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:twonly/core/bridge/callbacks.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/utils/log.dart';
/// Flutter boundary for the legacy libsignal_protocol_dart implementation.
///
/// Only V1 CIPHERTEXT and PREKEY_BUNDLE messages may cross this boundary.
/// CIPHERTEXT_V2 remains fully Rust-owned.
abstract final class LegacySignalCallbacks {
static Future<LegacySignalDecryptResult> decrypt(
PlatformInt64 fromUserId,
Uint8List ciphertext,
int messageType,
) async {
if (!_isLegacyMessageType(messageType)) {
return const LegacySignalDecryptResult(
decryptionErrorType: 0,
);
}
try {
final (content, errorType) = await signalDecryptMessageV1(
fromUserId,
ciphertext,
messageType,
);
return LegacySignalDecryptResult(
plaintext: content == null
? null
: Uint8List.fromList(content.writeToBuffer()),
decryptionErrorType: errorType?.value,
);
} catch (error) {
Log.error('Legacy Signal decryption callback failed: $error');
return const LegacySignalDecryptResult(decryptionErrorType: 0);
}
}
static Future<LegacySignalEncryptResult?> encrypt(
PlatformInt64 targetUserId,
Uint8List plaintext,
) async {
try {
final encrypted = await signalEncryptMessage(
targetUserId,
plaintext,
);
if (encrypted == null || !_isLegacyMessageType(encrypted.type.value)) {
return null;
}
return LegacySignalEncryptResult(
ciphertext: encrypted.ciphertext,
messageType: encrypted.type.value,
);
} catch (error) {
Log.error('Legacy Signal encryption callback failed: $error');
return null;
}
}
static bool _isLegacyMessageType(int messageType) =>
messageType == pb.Message_Type.CIPHERTEXT.value ||
messageType == pb.Message_Type.PREKEY_BUNDLE.value;
}

View file

@ -49,10 +49,15 @@ Future<void> deleteGroup(String groupId) async {
groupMembers,
)..where((t) => t.groupId.equals(groupId))).get();
}
Future<Group?> createNewGroup(GroupsCompanion group) async {
Future<Group?> createNewGroup(GroupsCompanion group) async {
return _insertGroup(group);
}
Future<void> insertGroupAction(GroupHistoriesCompanion action) async {
Future<void> insertOrUpdateGroupMember(GroupMembersCompanion members) async {
await into(groupMembers).insertOnConflictUpdate(members);
}
Future<void> insertGroupAction(GroupHistoriesCompanion action) async {
var insertAction = action;
if (!action.groupHistoryId.present) {
insertAction = action.copyWith(

View file

@ -43,7 +43,42 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
/// Returns a map of contactId the verification type of the earliest
/// [KeyVerification] row for that contact.
Stream<List<(KeyVerification, Contact?)>> watchContactVerification(
Future<Map<int, VerificationType>>
getFirstVerificationTypeByContacts() async {
final rows = await (select(
keyVerifications,
)..orderBy([(kv) => OrderingTerm.asc(kv.createdAt)])).get();
final result = <int, VerificationType>{};
for (final row in rows) {
result.putIfAbsent(row.contactId, () => row.type);
}
return result;
}
Future<bool> isContactVerified(int contactId) async {
final verifierKv = alias(keyVerifications, 'verifierKv');
final query = select(keyVerifications).join([
leftOuterJoin(
verifierKv,
verifierKv.contactId.equalsExp(keyVerifications.verifiedBy),
),
])..where(keyVerifications.contactId.equals(contactId));
final rows = await query.get();
for (final row in rows) {
final kv = row.readTable(keyVerifications);
final hasVerifierKv = row.readTableOrNull(verifierKv) != null;
if (kv.type == VerificationType.contactSharedByVerified) {
if (hasVerifierKv) return true;
} else {
return true;
}
}
return false;
}
Stream<List<(KeyVerification, Contact?)>> watchContactVerification(
int contactId,
) {
final verifier = alias(contacts, 'verifier');
@ -291,7 +326,24 @@ Stream<VerificationStatus> watchAllGroupMembersVerified(String groupId) {
Log.error(e);
}
}
Future<void> deleteKeyVerificationById(
Future<void> deleteKeyVerification(int contactId) async {
try {
await (delete(
keyVerifications,
)..where((kv) => kv.contactId.equals(contactId))).go();
if (userService.currentUser.isUserDiscoveryEnabled) {
await FlutterUserDiscovery.updateVerificationStateForUser(
callbackId: isolateCallbackId,
contactId: contactId,
);
}
} catch (e) {
Log.error(e);
}
}
Future<void> deleteKeyVerificationById(
int verificationId,
int contactId,
) async {

View file

@ -1,31 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'signal.dao.dart';
// ignore_for_file: type=lint
mixin _$SignalDaoMixin on DatabaseAccessor<TwonlyDB> {
$ContactsTable get contacts => attachedDatabase.contacts;
$SignalContactPreKeysTable get signalContactPreKeys =>
attachedDatabase.signalContactPreKeys;
$SignalContactSignedPreKeysTable get signalContactSignedPreKeys =>
attachedDatabase.signalContactSignedPreKeys;
SignalDaoManager get managers => SignalDaoManager(this);
}
class SignalDaoManager {
final _$SignalDaoMixin _db;
SignalDaoManager(this._db);
$$ContactsTableTableManager get contacts =>
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$SignalContactPreKeysTableTableManager get signalContactPreKeys =>
$$SignalContactPreKeysTableTableManager(
_db.attachedDatabase,
_db.signalContactPreKeys,
);
$$SignalContactSignedPreKeysTableTableManager
get signalContactSignedPreKeys =>
$$SignalContactSignedPreKeysTableTableManager(
_db.attachedDatabase,
_db.signalContactSignedPreKeys,
);
}

View file

@ -1,47 +0,0 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart'
show DriftNativeOptions, driftDatabase;
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_session_store.table.dart';
import 'package:twonly/src/database/tables/signal_signed_pre_key_store.table.dart';
part 'signal.db.g.dart';
@DriftDatabase(
tables: [
SignalIdentityKeyStores,
SignalPreKeyStores,
SignalSenderKeyStores,
SignalSessionStores,
SignalSignedPreKeyStores,
],
)
class SignalDB extends _$SignalDB {
SignalDB([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
// This database shares the legacy twonly.sqlite file, whose user_version is
// already 25. Keeping that version avoids a false downgrade while only the
// Signal tables remain owned by Drift.
int get schemaVersion => 25;
static QueryExecutor _openConnection() {
return driftDatabase(
name: 'twonly',
native: DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
shareAcrossIsolates: true,
setup: (database) {
database
..execute('PRAGMA journal_mode=DELETE;')
..execute('PRAGMA synchronous=FULL;')
..execute('PRAGMA busy_timeout=5000;')
..execute('PRAGMA foreign_keys=ON;');
},
),
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,81 +0,0 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/signal.db.dart';
class SignalIdentityKeyStore extends IdentityKeyStore {
SignalIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
final IdentityKeyPair identityKeyPair;
final int localRegistrationId;
@override
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
final identity =
await (signalDB.select(signalDB.signalIdentityKeyStores)..where(
(t) =>
t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName()),
))
.getSingleOrNull();
if (identity == null) return null;
return IdentityKey.fromBytes(identity.identityKey, 0);
}
@override
Future<IdentityKeyPair> getIdentityKeyPair() async => identityKeyPair;
@override
Future<int> getLocalRegistrationId() async => localRegistrationId;
@override
Future<bool> isTrustedIdentity(
SignalProtocolAddress address,
IdentityKey? identityKey,
Direction? direction,
) async {
final trusted = await getIdentity(address);
if (identityKey == null) {
return false;
}
return trusted == null ||
const ListEquality<dynamic>().equals(
trusted.serialize(),
identityKey.serialize(),
);
}
@override
Future<bool> saveIdentity(
SignalProtocolAddress address,
IdentityKey? identityKey,
) async {
if (identityKey == null) {
return false;
}
if (await getIdentity(address) == null) {
await signalDB
.into(signalDB.signalIdentityKeyStores)
.insert(
SignalIdentityKeyStoresCompanion(
deviceId: Value(address.getDeviceId()),
name: Value(address.getName()),
identityKey: Value(identityKey.serialize()),
),
);
} else {
await (signalDB.update(signalDB.signalIdentityKeyStores)..where(
(t) =>
t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName()),
))
.write(
SignalIdentityKeyStoresCompanion(
identityKey: Value(identityKey.serialize()),
),
);
}
return true;
}
}

View file

@ -1,52 +0,0 @@
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/signal.db.dart';
import 'package:twonly/src/utils/log.dart';
class SignalPreKeyStore extends PreKeyStore {
@override
Future<bool> containsPreKey(int preKeyId) async {
final preKeyRecord = await (signalDB.select(
signalDB.signalPreKeyStores,
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
return preKeyRecord.isNotEmpty;
}
@override
Future<PreKeyRecord> loadPreKey(int preKeyId) async {
final preKeyRecord = await (signalDB.select(
signalDB.signalPreKeyStores,
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
if (preKeyRecord.isEmpty) {
throw InvalidKeyIdException(
'[PREKEY] No such preKey record!',
);
}
final preKey = preKeyRecord.first.preKey;
return PreKeyRecord.fromBuffer(preKey);
}
@override
Future<void> removePreKey(int preKeyId) async {
await (signalDB.delete(
signalDB.signalPreKeyStores,
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).go();
}
@override
Future<void> storePreKey(int preKeyId, PreKeyRecord record) async {
final preKeyCompanion = SignalPreKeyStoresCompanion(
preKeyId: Value(preKeyId),
preKey: Value(record.serialize()),
);
try {
await signalDB
.into(signalDB.signalPreKeyStores)
.insert(preKeyCompanion, mode: InsertMode.insertOrReplace);
} catch (e) {
Log.error('$e');
}
}
}

View file

@ -1,122 +0,0 @@
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/src/database/signal/signal_identity_key_store.dart';
import 'package:twonly/src/database/signal/signal_pre_key_store.dart';
import 'package:twonly/src/database/signal/signal_session_store.dart';
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart';
class SignalSignalProtocolStore implements SignalProtocolStore {
SignalSignalProtocolStore(
IdentityKeyPair identityKeyPair,
int registrationId,
) {
_identityKeyStore = SignalIdentityKeyStore(
identityKeyPair,
registrationId,
);
}
final preKeyStore = SignalPreKeyStore();
final sessionStore = SignalSessionStore();
final signedPreKeyStore = SignalSignedPreKeyStore();
late IdentityKeyStore _identityKeyStore;
@override
Future<IdentityKeyPair> getIdentityKeyPair() async =>
_identityKeyStore.getIdentityKeyPair();
@override
Future<int> getLocalRegistrationId() async =>
_identityKeyStore.getLocalRegistrationId();
@override
Future<bool> saveIdentity(
SignalProtocolAddress address,
IdentityKey? identityKey,
) async => _identityKeyStore.saveIdentity(address, identityKey);
@override
Future<bool> isTrustedIdentity(
SignalProtocolAddress address,
IdentityKey? identityKey,
Direction direction,
) async =>
_identityKeyStore.isTrustedIdentity(address, identityKey, direction);
@override
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async =>
_identityKeyStore.getIdentity(address);
@override
Future<PreKeyRecord> loadPreKey(int preKeyId) async =>
preKeyStore.loadPreKey(preKeyId);
@override
Future<void> storePreKey(int preKeyId, PreKeyRecord record) async {
await preKeyStore.storePreKey(preKeyId, record);
}
@override
Future<bool> containsPreKey(int preKeyId) async =>
preKeyStore.containsPreKey(preKeyId);
@override
Future<void> removePreKey(int preKeyId) async {
await preKeyStore.removePreKey(preKeyId);
}
@override
Future<SessionRecord> loadSession(SignalProtocolAddress address) async =>
sessionStore.loadSession(address);
@override
Future<List<int>> getSubDeviceSessions(String name) async =>
sessionStore.getSubDeviceSessions(name);
@override
Future<void> storeSession(
SignalProtocolAddress address,
SessionRecord record,
) async {
await sessionStore.storeSession(address, record);
}
@override
Future<bool> containsSession(SignalProtocolAddress address) async =>
sessionStore.containsSession(address);
@override
Future<void> deleteSession(SignalProtocolAddress address) async {
await sessionStore.deleteSession(address);
}
@override
Future<void> deleteAllSessions(String name) async {
await sessionStore.deleteAllSessions(name);
}
@override
Future<SignedPreKeyRecord> loadSignedPreKey(int signedPreKeyId) async =>
signedPreKeyStore.loadSignedPreKey(signedPreKeyId);
@override
Future<List<SignedPreKeyRecord>> loadSignedPreKeys() async =>
signedPreKeyStore.loadSignedPreKeys();
@override
Future<void> storeSignedPreKey(
int signedPreKeyId,
SignedPreKeyRecord record,
) async {
await signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
}
@override
Future<bool> containsSignedPreKey(int signedPreKeyId) async =>
signedPreKeyStore.containsSignedPreKey(signedPreKeyId);
@override
Future<void> removeSignedPreKey(int signedPreKeyId) async {
await signedPreKeyStore.removeSignedPreKey(signedPreKeyId);
}
}

View file

@ -1,35 +0,0 @@
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/signal.db.dart';
class SignalSenderKeyStore extends SenderKeyStore {
@override
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
final identity =
await (signalDB.select(signalDB.signalSenderKeyStores)
..where((t) => t.senderKeyName.equals(senderKeyName.serialize())))
.getSingleOrNull();
if (identity == null) {
throw InvalidKeyIdException(
'No such sender key record! - $senderKeyName',
);
}
return SenderKeyRecord.fromSerialized(identity.senderKey);
}
@override
Future<void> storeSenderKey(
SenderKeyName senderKeyName,
SenderKeyRecord record,
) async {
await signalDB
.into(signalDB.signalSenderKeyStores)
.insert(
SignalSenderKeyStoresCompanion(
senderKey: Value(record.serialize()),
senderKeyName: Value(senderKeyName.serialize()),
),
);
}
}

View file

@ -1,87 +0,0 @@
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/signal.db.dart';
class SignalSessionStore extends SessionStore {
@override
Future<bool> containsSession(SignalProtocolAddress address) async {
final sessions =
await (signalDB.select(signalDB.signalSessionStores)..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.get();
return sessions.isNotEmpty;
}
@override
Future<void> deleteAllSessions(String name) async {
await (signalDB.delete(
signalDB.signalSessionStores,
)..where((tbl) => tbl.name.equals(name))).go();
}
@override
Future<void> deleteSession(SignalProtocolAddress address) async {
await (signalDB.delete(signalDB.signalSessionStores)..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.go();
}
@override
Future<List<int>> getSubDeviceSessions(String name) async {
final deviceIds =
await (signalDB.select(signalDB.signalSessionStores)..where(
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
))
.get();
return deviceIds.map((row) => row.deviceId).toList();
}
@override
Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
final dbSession =
await (signalDB.select(signalDB.signalSessionStores)..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.get();
if (dbSession.isEmpty) {
return SessionRecord();
}
final session = dbSession.first.sessionRecord;
return SessionRecord.fromSerialized(session);
}
@override
Future<void> storeSession(
SignalProtocolAddress address,
SessionRecord record,
) async {
final sessionCompanion = SignalSessionStoresCompanion(
deviceId: Value(address.getDeviceId()),
name: Value(address.getName()),
sessionRecord: Value(record.serialize()),
);
if (!await containsSession(address)) {
await signalDB
.into(signalDB.signalSessionStores)
.insert(sessionCompanion);
} else {
await (signalDB.update(signalDB.signalSessionStores)..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.write(sessionCompanion);
}
}
}

View file

@ -1,84 +0,0 @@
import 'dart:collection';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/signal.db.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart';
Future<HashMap<int, Uint8List>> getSignalSignedPreKeyStoreOld() async {
final storeSerialized = await SecureStorage.instance.read(
key: 'signed_pre_key_store',
);
final store = HashMap<int, Uint8List>();
if (storeSerialized == null) {
return store;
}
final storeHashMap = json.decode(storeSerialized) as List<dynamic>;
for (final item in storeHashMap) {
// ignore: avoid_dynamic_calls
store[item[0] as int] = base64Decode(item[1] as String);
}
return store;
}
class SignalSignedPreKeyStore extends SignedPreKeyStore {
@override
Future<SignedPreKeyRecord> loadSignedPreKey(int signedPreKeyId) async {
final record = await (signalDB.select(
signalDB.signalSignedPreKeyStores,
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).get();
if (record.isEmpty) {
throw InvalidKeyIdException(
'No such signed prekey record! $signedPreKeyId',
);
}
return SignedPreKeyRecord.fromSerialized(record.first.signedPreKey);
}
@override
Future<List<SignedPreKeyRecord>> loadSignedPreKeys() async {
final records = await signalDB
.select(signalDB.signalSignedPreKeyStores)
.get();
return records
.map((r) => SignedPreKeyRecord.fromSerialized(r.signedPreKey))
.toList();
}
@override
Future<void> storeSignedPreKey(
int signedPreKeyId,
SignedPreKeyRecord record,
) async {
final companion = SignalSignedPreKeyStoresCompanion(
signedPreKeyId: Value(signedPreKeyId),
signedPreKey: Value(record.serialize()),
);
try {
await signalDB
.into(signalDB.signalSignedPreKeyStores)
.insert(companion, mode: InsertMode.insertOrReplace);
} catch (e) {
Log.error('$e');
rethrow;
}
}
@override
Future<bool> containsSignedPreKey(int signedPreKeyId) async {
final record = await (signalDB.select(
signalDB.signalSignedPreKeyStores,
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).get();
return record.isNotEmpty;
}
@override
Future<void> removeSignedPreKey(int signedPreKeyId) async {
await (signalDB.delete(
signalDB.signalSignedPreKeyStores,
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).go();
}
}

View file

@ -1,12 +0,0 @@
import 'package:drift/drift.dart';
@DataClassName('SignalIdentityKeyStore')
class SignalIdentityKeyStores extends Table {
IntColumn get deviceId => integer()();
TextColumn get name => text()();
BlobColumn get identityKey => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {deviceId, name};
}

View file

@ -1,11 +0,0 @@
import 'package:drift/drift.dart';
@DataClassName('SignalPreKeyStore')
class SignalPreKeyStores extends Table {
IntColumn get preKeyId => integer()();
BlobColumn get preKey => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {preKeyId};
}

View file

@ -1,10 +0,0 @@
import 'package:drift/drift.dart';
@DataClassName('SignalSenderKeyStore')
class SignalSenderKeyStores extends Table {
TextColumn get senderKeyName => text()();
BlobColumn get senderKey => blob()();
@override
Set<Column> get primaryKey => {senderKeyName};
}

View file

@ -1,12 +0,0 @@
import 'package:drift/drift.dart';
@DataClassName('SignalSessionStore')
class SignalSessionStores extends Table {
IntColumn get deviceId => integer()();
TextColumn get name => text()();
BlobColumn get sessionRecord => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {deviceId, name};
}

View file

@ -1,11 +0,0 @@
import 'package:drift/drift.dart';
@DataClassName('SignalSignedPreKeyStore')
class SignalSignedPreKeyStores extends Table {
IntColumn get signedPreKeyId => integer()();
BlobColumn get signedPreKey => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {signedPreKeyId};
}

View file

@ -1,35 +0,0 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:json_annotation/json_annotation.dart';
part 'signal_identity.model.g.dart';
@JsonSerializable()
class SignalIdentity {
const SignalIdentity({
required this.identityKeyPairU8List,
required this.registrationId,
});
factory SignalIdentity.fromJson(Map<String, dynamic> json) =>
_$SignalIdentityFromJson(json);
final int registrationId;
@Uint8ListConverter()
final Uint8List identityKeyPairU8List;
Map<String, dynamic> toJson() => _$SignalIdentityToJson(this);
}
class Uint8ListConverter implements JsonConverter<Uint8List, String> {
const Uint8ListConverter();
@override
Uint8List fromJson(String json) {
return base64Decode(json);
}
@override
String toJson(Uint8List object) {
return base64Encode(object);
}
}

View file

@ -1,23 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'signal_identity.model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SignalIdentity _$SignalIdentityFromJson(Map<String, dynamic> json) =>
SignalIdentity(
identityKeyPairU8List: const Uint8ListConverter().fromJson(
json['identityKeyPairU8List'] as String,
),
registrationId: (json['registrationId'] as num).toInt(),
);
Map<String, dynamic> _$SignalIdentityToJson(SignalIdentity instance) =>
<String, dynamic>{
'registrationId': instance.registrationId,
'identityKeyPairU8List': const Uint8ListConverter().toJson(
instance.identityKeyPairU8List,
),
};

View file

@ -7,7 +7,6 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/utils/log.dart';
/// The ApiProvider is responsible for communicating with the server.
@ -47,7 +46,6 @@ class ApiService {
unawaited(reuploadMediaFiles());
twonlyDB.markUpdated();
resetResyncedUsers();
// resetUserDiscoveryRequestUpdates();
memoriesCloudService.init();
}

View file

@ -10,7 +10,6 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart
as server;
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
hide Message;
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/secure_storage.dart';
@ -69,18 +68,17 @@ Future<void> handleMediaError(MediaFile media) async {
targetMessageId: message.messageId,
),
).writeToBuffer(),
onlySendIfNoReceiptsAreOpen: false,
onlyReturnEncryptedData: false,
blocking: true,
);
}
Future<bool> importSignalContactAndCreateRequest(
server.Response_UserData userdata,
) async {
if (!await processSignalUserData(userdata)) {
return false;
}
try {
await RustApi.establishSignalSession(
contactId: userdata.userId.toInt(),
expectedPublicKey: Uint8List.fromList(userdata.publicIdentityKey),
);
// 2. Then send user request
await RustApi.sendEncryptedContent(
@ -90,12 +88,13 @@ Future<bool> importSignalContactAndCreateRequest(
type: EncryptedContent_ContactRequest_Type.REQUEST,
),
).writeToBuffer(),
onlySendIfNoReceiptsAreOpen: false,
onlyReturnEncryptedData: false,
blocking: true,
);
return true;
} catch (e) {
Log.error('Failed to establish session and send contact request: $e');
return false;
}
}
Future<Map<String, String>?> getAuthenticationHeader() async {

View file

@ -51,5 +51,6 @@ Future<bool> removeMemberFromGroup(Group group, Uint8List key, int contactId) =>
groupPublicKey: key,
contactId: contactId,
);
Future<bool> leaveAsNonAdminFromGroup(Group group) =>
Future<bool> leaveGroup(Group group) =>
rust_groups.leaveGroup(groupId: group.groupId);
Future<bool> leaveAsNonAdminFromGroup(Group group) => leaveGroup(group);

View file

@ -6,13 +6,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart'
show PasswordlessRecoveryService;
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/qr.utils.dart';
@ -102,7 +101,9 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
if (publicKey != null) {
try {
final contact = contacts.first;
final storedPublicKey = await getPublicKeyFromContact(contact.userId);
final storedPublicKey = await RustSignal.getContactPublicKey(
contactId: contact.userId,
);
final receivedPublicKey = base64Url.decode(publicKey);
if (storedPublicKey == null ||
receivedPublicKey.isEmpty ||

View file

@ -4,14 +4,13 @@ import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter/material.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/providers/routing.provider.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart';
@ -62,9 +61,6 @@ class KeyVerificationService {
calculatedMac: calculatedMac,
),
).writeToBuffer(),
onlySendIfNoReceiptsAreOpen: false,
onlyReturnEncryptedData: false,
blocking: true,
);
}
@ -74,7 +70,9 @@ class KeyVerificationService {
) async {
Log.info('Received a verification proof. Verifying the calculated mac...');
final contactPubKey = await getPublicKeyFromContact(fromUserId);
final contactPubKey = await RustSignal.getContactPublicKey(
contactId: fromUserId,
);
if (contactPubKey == null) {
Log.error('No public key stored..');
return;
@ -121,7 +119,9 @@ class KeyVerificationService {
required List<int> sharedPublicIdentityKey,
required int senderId,
}) async {
final publicIdentityKey = await getPublicKeyFromContact(contactId);
final publicIdentityKey = await RustSignal.getContactPublicKey(
contactId: contactId,
);
if (publicIdentityKey == null) {
Log.info('No public key stored for contact $contactId');
return;
@ -148,7 +148,7 @@ Future<List<int>> _createVerificationBytes(
) async {
final bytes = <int>[];
final userPublicKey = await getUserPublicKey();
final userPublicKey = await RustSignal.getUserPublicKey();
final ownBytes = [
..._userIdToLeBytes(userService.currentUser.userId),

View file

@ -1,24 +1,15 @@
import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/signal.db.dart';
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart'
show getSignalSignedPreKeyStoreOld;
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/signal_identity.model.dart';
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart';
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
Future<void> runMigrations() async {
@ -56,42 +47,6 @@ Future<void> runMigrations() async {
});
}
if (userService.currentUser.appVersion < 113) {
var migrationSuccess = true;
final signalIdentity = await SecureStorage.instance.read(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalIdentity,
);
if (signalIdentity != null) {
try {
final decoded = jsonDecode(signalIdentity);
final identity = SignalIdentity.fromJson(
decoded as Map<String, dynamic>,
);
await RustKeyManager.importSignalIdentity(
identityKeyPairStructure: identity.identityKeyPairU8List,
registrationId: identity.registrationId,
signedPreKeyStore: await getSignalSignedPreKeyStoreOld(),
);
Log.info('Importing signal identify to the rust key manager');
// Clean up old keys after successful migration
await SecureStorage.instance.delete(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalIdentity,
);
await SecureStorage.instance.delete(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalSignedPreKey,
);
} catch (e) {
Log.error('Failed to migrate signal identity: $e');
migrationSuccess = false;
}
}
if (migrationSuccess) {
await UserService.update((u) {
u
..appVersion = 113
@ -102,7 +57,7 @@ Future<void> runMigrations() async {
..twonlySafeBackup?.backupId = Uint8List(0);
});
}
}
if (userService.currentUser.appVersion < 114) {
final allMedia = await twonlyDB.mediaFilesDao
.select(twonlyDB.mediaFiles)
@ -120,30 +75,8 @@ Future<void> runMigrations() async {
}
if (userService.currentUser.appVersion < 115) {
var migrationSuccess = true;
try {
final rustStore = await RustKeyManager.loadSignedPrekeys();
for (final entry in rustStore.entries) {
final companion = SignalSignedPreKeyStoresCompanion(
signedPreKeyId: Value(entry.key),
signedPreKey: Value(entry.value),
);
await signalDB
.into(signalDB.signalSignedPreKeyStores)
.insert(
companion,
mode: InsertMode.insertOrReplace,
);
await RustKeyManager.removeSignedPrekey(signedPreKeyId: entry.key);
}
} catch (e) {
Log.error('Failed to migrate signed prekeys to Drift: $e');
migrationSuccess = false;
}
if (migrationSuccess) {
await UserService.update((u) => u.appVersion = 115);
}
}
if (userService.currentUser.appVersion < 116) {
if (userService.currentUser.userDiscoveryThreshold == 2) {

View file

@ -311,7 +311,11 @@ class PasswordlessRecoveryService {
);
}
unawaited(RustApi.performPasswordlessRecoveryHeartbeat());
unawaited(
RustApi.performPasswordlessRecoveryHeartbeat().catchError((e) {
Log.warn('Failed to perform passwordless recovery heartbeat: $e');
}),
);
// The passwordless is configured successfully.
return true;

View file

@ -1,2 +0,0 @@
// multi device support is not planned, so just set this to one
const int defaultDeviceId = 1;

View file

@ -1,159 +0,0 @@
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/locator.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
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 {
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 for target $target: $e');
return null;
}
});
}
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 RustApi.sendEncryptedContent(
contactId: fromUserId,
content: pb.EncryptedContent(
errorMessages: pb.EncryptedContent_ErrorMessages(
type: pb.EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
),
).writeToBuffer(),
onlySendIfNoReceiptsAreOpen: false,
onlyReturnEncryptedData: false,
blocking: true,
);
}
}
}
return (decryptedContent, errorType);
}

View file

@ -1,72 +0,0 @@
import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart';
import 'package:twonly/src/model/json/signal_identity.model.dart';
import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart';
Future<SignalIdentity?> getSignalIdentity() async {
try {
final identity = await RustKeyManager.getSignalIdentity();
return SignalIdentity(
identityKeyPairU8List: identity.$1,
registrationId: identity.$2,
);
} catch (e) {
Log.error('could not load signal identity: $e');
return null;
}
}
Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
final signalIdentity = await getSignalIdentity();
if (signalIdentity == null) return null;
return IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List);
}
Future<Uint8List> getUserPublicKey() async {
final signalIdentity = (await getSignalIdentity())!;
final signalStore = await getSignalStoreFromIdentity(signalIdentity);
final keyPair = await signalStore.getIdentityKeyPair();
return keyPair.getPublicKey().serialize();
}
Future<void> createIfNotExistsSignalIdentity() async {
// check if identity already exists
final existingIdentity = await getSignalIdentity();
if (existingIdentity != null) {
final store = await getSignalStoreFromIdentity(existingIdentity);
final keys = await store.loadSignedPreKeys();
if (keys.isEmpty) {
Log.warn(
'Signal identity exists but signed prekeys are missing. Generating a new one.',
);
final keyPair = await store.getIdentityKeyPair();
final signedPreKey = generateSignedPreKey(keyPair, defaultDeviceId);
await SignalSignedPreKeyStore().storeSignedPreKey(
signedPreKey.id,
signedPreKey,
);
}
return;
}
final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(true);
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
await SignalSignedPreKeyStore().storeSignedPreKey(
signedPreKey.id,
signedPreKey,
);
await RustKeyManager.importSignalIdentity(
identityKeyPairStructure: identityKeyPair.serialize(),
registrationId: registrationId,
signedPreKeyStore: const {},
);
}

View file

@ -1,38 +0,0 @@
import 'dart:math';
import 'package:mutex/mutex.dart';
/// Unified lock for all Signal protocol operations (encryption, decryption, session management).
final lockingSignalProtocol = Mutex();
/// Tracking users who have already been resynced in the current session.
final Map<int, ({int failureCount, DateTime lastAttempt})> _resyncAttempts = {};
const int maxResyncAttempts = 3;
bool shouldAttemptResync(int userId) {
final attempt = _resyncAttempts[userId];
if (attempt == null) return true;
if (attempt.failureCount >= maxResyncAttempts) return false;
final cooldown = Duration(
minutes: 5 * pow(5, attempt.failureCount - 1).toInt(),
);
return DateTime.now().difference(attempt.lastAttempt) > cooldown;
}
void recordResyncAttempt(int userId, {required bool success}) {
if (success) {
_resyncAttempts.remove(userId);
} else {
final current = _resyncAttempts[userId];
_resyncAttempts[userId] = (
failureCount: (current?.failureCount ?? 0) + 1,
lastAttempt: DateTime.now(),
);
}
}
/// Reset the resync tracking set (currently unused, backoff handles expiry naturally).
void resetResyncedUsers() {
// No-op. We want the backoff state to persist across reconnects.
}

View file

@ -1,219 +0,0 @@
import 'dart:typed_data';
import 'package:drift/drift.dart' as drift;
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/core/signal/engine.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart';
Future<bool> processSignalUserData(Response_UserData userData) async {
return lockingSignalProtocol.protect(() async {
return _processSignalUserData(userData);
});
}
Future<bool> _processSignalUserData(Response_UserData userData) async {
if (userData.hasPqcBundle()) {
return _processSignalUserDataV2(userData);
}
return _processSignalUserDataV1(userData);
}
Future<bool> _processSignalUserDataV2(Response_UserData userData) async {
try {
final tempIdentityKey = IdentityKey(
Curve.decodePoint(
DjbECPublicKey(
Uint8List.fromList(userData.publicIdentityKey),
).serialize(),
1,
),
);
final signalStore = await getSignalStore();
if (signalStore != null) {
final existingIdentity = await signalStore.getIdentity(
SignalProtocolAddress(userData.userId.toString(), defaultDeviceId),
);
if (existingIdentity != null && existingIdentity != tempIdentityKey) {
Log.error(
'Identity key mismatch for contact ${userData.userId}! Existing V1 key does not match the incoming V2 identity key.',
);
return false;
}
}
int? tempEccPreKeyId;
Uint8List? tempEccPreKeyPublic;
if (userData.pqcBundle.hasPrekey()) {
tempEccPreKeyId = userData.pqcBundle.prekey.eccPreKeyId.toInt();
tempEccPreKeyPublic = Uint8List.fromList(
userData.pqcBundle.prekey.eccPreKey,
);
} else if (userData.prekeys.isNotEmpty) {
tempEccPreKeyId = userData.prekeys.first.id.toInt();
tempEccPreKeyPublic = Curve.decodePoint(
DjbECPublicKey(
Uint8List.fromList(userData.prekeys.first.prekey),
).serialize(),
1,
).serialize();
}
final tempKyberPreKeyId = userData.pqcBundle.hasPrekey()
? userData.pqcBundle.prekey.kyberPreKeyId.toInt()
: userData.pqcBundle.kyberSignedPrekeyId.toInt();
final tempKyberPreKeyPublic = userData.pqcBundle.hasPrekey()
? Uint8List.fromList(userData.pqcBundle.prekey.kyberPreKey)
: Uint8List.fromList(userData.pqcBundle.kyberSignedPrekey);
final tempKyberPreKeySignature = userData.pqcBundle.hasPrekey()
? Uint8List.fromList(userData.pqcBundle.prekey.kyberPreKeySignature)
: Uint8List.fromList(userData.pqcBundle.kyberSignedPrekeySignature);
final rustBundle = FrbPreKeyBundle(
registrationId: userData.registrationId.toInt(),
deviceId: 1,
preKeyId: tempEccPreKeyId,
preKeyPublic: tempEccPreKeyPublic,
signedPreKeyId: userData.pqcBundle.eccSignedPrekeyId.toInt(),
signedPreKeyPublic: Uint8List.fromList(
userData.pqcBundle.eccSignedPrekey,
),
signedPreKeySignature: Uint8List.fromList(
userData.pqcBundle.eccSignedPrekeySignature,
),
identityKey: tempIdentityKey.publicKey.serialize(),
kyberPreKeyId: tempKyberPreKeyId,
kyberPreKeyPublic: tempKyberPreKeyPublic,
kyberPreKeySignature: tempKyberPreKeySignature,
);
await RustSignal.processPrekeyBundle(
name: userData.userId.toString(),
deviceId: 1,
bundle: rustBundle,
);
final contact = await twonlyDB.contactsDao.getContactById(
userData.userId.toInt(),
);
if (contact != null && contact.signalVersion != SignalVersion.v2) {
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(signalVersion: drift.Value(SignalVersion.v2)),
);
}
return true;
} catch (e) {
Log.error('could not process pqc bundle: $e');
return false;
}
}
Future<bool> _processSignalUserDataV1(Response_UserData userData) async {
final SignalProtocolStore? signalStore = await getSignalStore();
if (signalStore == null) {
return false;
}
final targetAddress = getSignalAddress(userData.userId.toInt());
final sessionBuilder = SessionBuilder.fromSignalStore(
signalStore,
targetAddress,
);
ECPublicKey? tempPrePublicKey;
int? tempPreKeyId;
if (userData.prekeys.isNotEmpty) {
tempPrePublicKey = Curve.decodePoint(
DjbECPublicKey(
Uint8List.fromList(userData.prekeys.first.prekey),
).serialize(),
1,
);
tempPreKeyId = userData.prekeys.first.id.toInt();
}
final tempSignedPreKeyId = userData.signedPrekeyId.toInt();
final tempSignedPreKeyPublic = Curve.decodePoint(
DjbECPublicKey(Uint8List.fromList(userData.signedPrekey)).serialize(),
1,
);
final tempSignedPreKeySignature = Uint8List.fromList(
userData.signedPrekeySignature,
);
final tempIdentityKey = IdentityKey(
Curve.decodePoint(
DjbECPublicKey(
Uint8List.fromList(userData.publicIdentityKey),
).serialize(),
1,
),
);
final preKeyBundle = PreKeyBundle(
userData.registrationId.toInt(),
defaultDeviceId,
tempPreKeyId,
tempPrePublicKey,
tempSignedPreKeyId,
tempSignedPreKeyPublic,
tempSignedPreKeySignature,
tempIdentityKey,
);
try {
await sessionBuilder.processPreKeyBundle(preKeyBundle);
return true;
} catch (e) {
Log.error('could not process pre key bundle: $e');
return false;
}
}
Future<Uint8List?> getPublicKeyFromContact(int contactId) async {
final signalStore = await getSignalStore();
if (signalStore == null) return null;
try {
final targetIdentity = await signalStore.getIdentity(
SignalProtocolAddress(
contactId.toString(),
defaultDeviceId,
),
);
if (targetIdentity != null) {
return targetIdentity.publicKey.serialize();
}
return null;
} catch (e) {
return null;
}
}
Future<bool> handleSessionResync(int fromUserId) async {
final userData = await rustApiProtobuf(
RustApi.getUserById(userId: fromUserId),
decodeUserData,
);
if (userData != null) {
Log.info('Got new session data from the server to re-sync the session');
return processSignalUserData(userData);
}
Log.info('Could not download userdata from the server.');
return false;
}

View file

@ -1,26 +0,0 @@
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/src/database/signal/signal_protocol_store.dart';
import 'package:twonly/src/model/json/signal_identity.model.dart';
import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
Future<SignalSignalProtocolStore?> getSignalStore() async {
return getSignalStoreFromIdentity((await getSignalIdentity())!);
}
Future<SignalSignalProtocolStore> getSignalStoreFromIdentity(
SignalIdentity signalIdentity,
) async {
final identityKeyPair = IdentityKeyPair.fromSerialized(
signalIdentity.identityKeyPairU8List,
);
return SignalSignalProtocolStore(
identityKeyPair,
signalIdentity.registrationId,
);
}
SignalProtocolAddress getSignalAddress(int userId) {
return SignalProtocolAddress(userId.toString(), defaultDeviceId);
}

View file

@ -94,7 +94,11 @@ class UserService {
}
static Future<void> handleRustUserConfigChanged(UserConfig config) async {
try {
userService._applyRustUserConfig(config);
} catch (e) {
Log.warn(e);
}
}
void _applyRustUserConfig(UserConfig config, {bool notify = true}) {

View file

@ -6,41 +6,27 @@ import 'package:collection/collection.dart' show ListExtensions;
import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/key_verification.service.dart';
import 'package:twonly/src/services/signal/identity.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 QrCodeUtils {
static String linkPrefix = 'https://me.twonly.eu/qr/#';
static Future<String> publicProfileLink() async {
final signalIdentity = (await getSignalIdentity())!;
final signalStore = await getSignalStoreFromIdentity(signalIdentity);
final signedPreKey = (await signalStore.loadSignedPreKeys())[0];
final publicIdentityKey = await RustSignal.getUserPublicKey();
final secretVerificationToken =
await KeyVerificationService.getNewSecretVerificationToken();
final publicProfile = PublicProfile(
userId: Int64(userService.currentUser.userId),
username: userService.currentUser.username,
publicIdentityKey: (await signalStore.getIdentityKeyPair())
.getPublicKey()
.serialize(),
registrationId: Int64(signalIdentity.registrationId),
signedPrekey: signedPreKey.getKeyPair().publicKey.serialize(),
signedPrekeySignature: signedPreKey.signature,
signedPrekeyId: Int64(signedPreKey.id),
publicIdentityKey: publicIdentityKey,
secretVerificationToken: secretVerificationToken,
timestamp: Int64(clock.now().millisecondsSinceEpoch),
);
@ -88,7 +74,9 @@ class QrCodeUtils {
return (profile, null, false);
}
final storedPublicKey = await getPublicKeyFromContact(contact.userId);
final storedPublicKey = await RustSignal.getContactPublicKey(
contactId: contact.userId,
);
if (storedPublicKey == null) return null;
final verificationOk = profile.publicIdentityKey.equals(
@ -127,12 +115,18 @@ class QrCodeUtils {
}
Future<bool> addNewContactFromPublicProfile(PublicProfile profile) async {
final userdata = Response_UserData(
userId: profile.userId,
publicIdentityKey: profile.publicIdentityKey,
signedPrekey: profile.signedPrekey,
signedPrekeyId: profile.signedPrekeyId,
signedPrekeySignature: profile.signedPrekeySignature,
try {
await RustApi.establishSignalSession(
contactId: profile.userId.toInt(),
expectedPublicKey: Uint8List.fromList(profile.publicIdentityKey),
);
await RustApi.sendEncryptedContent(
contactId: profile.userId.toInt(),
content: EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.REQUEST,
),
).writeToBuffer(),
);
final added = await twonlyDB.contactsDao.insertOnConflictUpdate(
@ -145,14 +139,13 @@ Future<bool> addNewContactFromPublicProfile(PublicProfile profile) async {
),
);
if (added > 0) {
// The user was added via the profile scanned from the QR code so the scanned public key was used.
await twonlyDB.keyVerificationDao.addKeyVerification(
profile.userId.toInt(),
VerificationType.qrScanned,
);
if (added > 0) {
if (await importSignalContactAndCreateRequest(userdata)) {
if (profile.hasSecretVerificationToken()) {
await KeyVerificationService.handleScannedVerificationToken(
profile.userId.toInt(),
@ -160,10 +153,10 @@ Future<bool> addNewContactFromPublicProfile(PublicProfile profile) async {
profile.secretVerificationToken,
);
}
}
return true;
}
} catch (e) {
Log.error('Failed to establish session and send contact request: $e');
return false;
}
return false;
}

View file

@ -20,6 +20,7 @@ import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/qr.utils.dart';
import 'package:twonly/src/visual/components/add_contact_dialog.comp.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart';
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
@ -555,12 +556,23 @@ class MainCameraController {
profile.username,
);
if (shouldRequest == true && context.mounted) {
final success = await addNewContactFromPublicProfile(profile);
if (context.mounted) {
if (success) {
showSnackbar(
context,
context.lang.requestedUserToastText(profile.username),
level: SnackbarLevel.success,
);
await addNewContactFromPublicProfile(profile);
} else {
await showAlertDialog(
context,
context.lang.groupNetworkIssue,
context.lang.recoverErrorNoInternet,
customCancel: '',
);
}
}
}
}
continue;

View file

@ -32,4 +32,8 @@ class TwitterParser with BaseMetaInfo {
_url.startsWith('https://x.com/') && _url.contains('/status/')
? Vendor.twitterPosting
: null;
@override
String? get siteName =>
vendor == Vendor.twitterPosting ? 'X (formerly Twitter)' : null;
}

View file

@ -4,9 +4,9 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'
show FaIcon, FontAwesomeIcons;
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart'
show ProfileQrCodeComp;
@ -17,7 +17,7 @@ class EmptyChatListComp extends StatelessWidget {
Future<void> _shareProfile(BuildContext context) async {
try {
final pubKey = await getUserPublicKey();
final pubKey = await RustSignal.getUserPublicKey();
final params = ShareParams(
text:
'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',

View file

@ -8,13 +8,13 @@ import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
@ -94,7 +94,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
}
Future<void> _shareProfile() async {
final pubKey = await getUserPublicKey();
final pubKey = await RustSignal.getUserPublicKey();
final params = ShareParams(
text:
'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',

View file

@ -9,7 +9,6 @@ import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
@ -123,7 +122,9 @@ class _ContactViewState extends State<ContactView> {
Future<void> handleReportUser(Contact contact) async {
final reason = await showReportDialog(context, contact);
if (reason == null) return;
final res = await rustApiResult(RustApi.reportUser(userId: contact.userId, reason: reason));
final res = await rustApiResult(
RustApi.reportUser(userId: contact.userId, reason: reason),
);
if (!mounted) return;
if (res.isSuccess) {
showSnackbar(
@ -281,9 +282,10 @@ class _ContactViewState extends State<ContactView> {
icon: FontAwesomeIcons.arrowsRotate,
text: 'Update Connection to V2 (PQXDH)',
onTap: () async {
final userData = await rustApiProtobuf(RustApi.getUserById(userId: contact.userId), decodeUserData);
if (userData != null) {
await processSignalUserData(userData);
try {
await RustApi.establishSignalSession(
contactId: contact.userId,
);
final updatedContact = await twonlyDB.contactsDao
.getContactById(contact.userId);
final isV2 =
@ -300,6 +302,13 @@ class _ContactViewState extends State<ContactView> {
: SnackbarLevel.error,
);
}
} catch (e) {
if (context.mounted) {
showSnackbar(
context,
'Failed to update connection to V2: $e',
);
}
}
},
),

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
@ -135,21 +134,7 @@ class _GroupViewState extends State<GroupView> {
}
}
late bool success;
if (_group!.isGroupAdmin) {
// Current user is a admin, to the state can be updated by the user him self.
final keyPair = IdentityKeyPair.fromSerialized(
_group!.myGroupPrivateKey!,
);
success = !(await removeMemberFromGroup(
_group!,
keyPair.getPublicKey().serialize(),
userService.currentUser.userId,
));
} else {
success = await leaveAsNonAdminFromGroup(_group!);
}
final success = await leaveGroup(_group!);
if (!success) {
if (mounted) {

View file

@ -12,7 +12,6 @@ import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
@ -99,8 +98,6 @@ class _RegisterViewState extends State<RegisterView> {
Log.info('The result of the POW is $proof');
await createIfNotExistsSignalIdentity();
var userId = 0;
final res = await rustApiResult(

View file

@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/contact_request_badge.comp.dart';
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
@ -31,7 +31,7 @@ class _PublicProfileViewState extends State<PublicProfileView> {
}
Future<void> initAsync() async {
_publicKey = await getUserPublicKey();
_publicKey = await RustSignal.getUserPublicKey();
if (mounted) setState(() {});
}

View file

@ -2,9 +2,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
@ -34,57 +32,6 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
),
body: ListView(
children: [
ListTile(
title: const Text('Trigger Signal Out-Of-Sync'),
onTap: () async {
final username = await showUserNameDialog(context);
if (username == null) return;
final contacts = await twonlyDB.contactsDao.getContactsByUsername(
username.toLowerCase(),
);
if (contacts.length != 1) {
Log.error('No single user fund');
return;
}
final userId = contacts.first.userId;
final group = await twonlyDB.groupsDao.getDirectChat(userId);
if (group == null) {
Log.error('Target user must have a group!');
return;
}
final sessionStore = await getSignalStore();
// 1. Store a valid session
final originalSession = await sessionStore!.loadSession(
getSignalAddress(userId),
);
final serializedSession = originalSession.serialize();
for (var i = 0; i < 10; i++) {
await RustApi.insertAndSendText(
groupId: group.groupId,
text: 'DesyncTest_1',
);
}
final corruptedSession = SessionRecord.fromSerialized(
serializedSession,
);
await sessionStore.storeSession(
getSignalAddress(userId),
corruptedSession,
);
await RustApi.insertAndSendText(
groupId: group.groupId,
text: 'DesyncTest_2',
);
// The other client should res
},
),
ListTile(
title: const Text('Sending a lot of messages.'),
subtitle: Text(lotsOfMessagesStatus),

View file

@ -17,13 +17,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.68"
adaptive_number:
dependency: "direct main"
description:
path: "dependencies/adaptive_number"
relative: true
source: path
version: "1.0.0"
analyzer:
dependency: transitive
description:
@ -411,13 +404,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.0"
ed25519_edwards:
dependency: "direct main"
description:
path: "dependencies/ed25519_edwards"
relative: true
source: path
version: "0.3.1"
emoji_picker_flutter:
dependency: "direct main"
description:
@ -1190,13 +1176,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
libsignal_protocol_dart:
dependency: "direct main"
description:
path: "dependencies/libsignal_protocol_dart"
relative: true
source: path
version: "0.8.0"
lints:
dependency: transitive
description:
@ -1347,13 +1326,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "9.3.0"
optional:
dependency: "direct main"
description:
path: "dependencies/optional"
relative: true
source: path
version: "6.1.0+1"
package_config:
dependency: transitive
description:
@ -1529,13 +1501,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointycastle:
dependency: "direct main"
description:
path: "dependencies/pointycastle"
relative: true
source: path
version: "4.0.0"
pool:
dependency: transitive
description:
@ -2139,13 +2104,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.9.1+1"
x25519:
dependency: "direct main"
description:
path: "dependencies/x25519"
relative: true
source: path
version: "0.1.1"
xdg_directories:
dependency: transitive
description:

View file

@ -110,12 +110,6 @@ dependencies:
image: ^4.9.2
introduction_screen: ^4.0.0
dots_indicator: ^4.0.1
libsignal_protocol_dart: ^0.8.0
adaptive_number: ^1.0.0
ed25519_edwards: ^0.3.1
optional: ^6.1.0+1
pointycastle: ^4.0.0
x25519: ^0.1.1
lottie: ^3.5.1
mutex: ^3.1.0
photo_view: ^0.15.0
@ -176,18 +170,6 @@ dependency_overrides:
path: ./dependencies/introduction_screen
dots_indicator:
path: ./dependencies/dots_indicator
libsignal_protocol_dart:
path: ./dependencies/libsignal_protocol_dart
adaptive_number:
path: ./dependencies/adaptive_number
ed25519_edwards:
path: ./dependencies/ed25519_edwards
optional:
path: ./dependencies/optional
pointycastle:
path: ./dependencies/pointycastle
x25519:
path: ./dependencies/x25519
lottie:
path: ./dependencies/lottie
mutex:

View file

@ -6,6 +6,9 @@ edition = "2021"
[lib]
crate-type = ["cdylib", "staticlib", "rlib"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
[dependencies]
flutter_rust_bridge = { version = "=2.12.0", features = ["chrono"] }
thiserror = "2.0.18"
@ -49,14 +52,19 @@ rand08 = { version = "0.8.5", package = "rand" }
rand = "0.9.4"
uuid = { version = "1", features = ["v4"] }
flate2 = "1"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls",
] }
async-trait = "0.1.91"
futures-util = "0.3.31"
tokio-tungstenite = { version = "0.28.0", default-features = false, features = [
"connect",
"rustls-tls-native-roots",
] }
rustls = { version = "0.23.43", default-features = false, features = ["ring", "std"] }
rustls = { version = "0.23.43", default-features = false, features = [
"ring",
"std",
] }
tokio-util = "0.7.16"
bon = "3.9.3"
stream-tungstenite = "0.6.1"

View file

@ -18,18 +18,18 @@ mod verification;
use crate::api::messages::content_type_kind;
use crate::api::messages::incoming::messages::{
decrypt_legacy_signal_with_error, ensure_contact_exists, handle_plaintext_content,
handle_sender_delivery_receipt, process_encrypted_or_queue_error, queue_decryption_error,
queue_sender_delivery_receipt, retransmit_queued_receipts, spawn_receipt_delivery,
ensure_contact_exists, handle_plaintext_content, handle_sender_delivery_receipt,
process_encrypted_or_queue_error, queue_decryption_error, queue_sender_delivery_receipt,
retransmit_queued_receipts, spawn_receipt_delivery,
};
use crate::api::proto::client as proto;
use crate::api::proto::server_to_client::NewMessage;
use crate::api::proto::{client_to_server, server_to_client};
use crate::bridge::callbacks::get_callbacks;
use crate::context::Context;
use crate::database::app::tables::{Contact, Group, Receipt};
use crate::error::{Result, TwonlyError};
use crate::sealed_sender::SealedSender;
use crate::services::contacts::ContactService;
use client_to_server::response::{ok, Response};
use prost::Message as _;
use proto::message::Type;
@ -210,40 +210,45 @@ pub(crate) async fn handle_decoded_server_message(
handle_sender_delivery_receipt(&mut t, from_user_id, &message.receipt_id).await?;
}
Type::Ciphertext | Type::PrekeyBundle => {
let ciphertext = message.encrypted_content.ok_or_else(|| {
TwonlyError::Generic("legacy encrypted client message has no ciphertext".into())
})?;
tracing::info!("Decrypting legacy signal message...");
match decrypt_legacy_signal_with_error(from_user_id, ciphertext, message.r#type).await?
{
Ok(content) => {
tracing::info!("Decrypted successfully, processing...");
sends_error_response = process_encrypted_or_queue_error(
ctx,
&mut t,
from_user_id,
&message.receipt_id,
content,
tracing::info!("Received legacy signal message; rejecting and upgrading session to v2");
let has_v2_session = {
let rust_database = ctx.rust_db.read().await.clone();
sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM signal_sessions WHERE name = ? AND device_id = 1)",
from_user_id.to_string(),
)
.fetch_one(&rust_database.pool)
.await?
.is_some();
}
Err(error_type) => {
tracing::info!(error_type, "Decryption error");
if error_type
== proto::plaintext_content::decryption_error_message::Type::PrekeyUnknown
as i32
!= 0
};
let is_v2_contact = {
let contact = Contact::get_contact_by_id(&mut t, from_user_id).await?;
contact.as_ref().is_some_and(|c| c.signal_version == "v2")
};
if !has_v2_session || !is_v2_contact {
if let Err(error) = ContactService::new(ctx)
.establish_signal_session(from_user_id, None)
.await
{
if let Ok(callbacks) = get_callbacks() {
(callbacks.api.resync_signal_session)(from_user_id).await;
tracing::warn!(
from_user_id,
"failed to establish v2 signal session from server: {error}"
);
}
}
queue_decryption_error(&mut t, from_user_id, &message.receipt_id, error_type)
sqlx::query!(
"UPDATE contacts SET signal_version = 'v2' WHERE user_id = ?",
from_user_id,
)
.execute(&mut *t)
.await?;
}
queue_decryption_error(&mut t, from_user_id, &message.receipt_id, 0).await?;
sends_error_response = true;
}
}
}
Type::CiphertextV2 => {
let ciphertext = message.encrypted_content.ok_or_else(|| {
TwonlyError::Generic("V2 encrypted client message has no ciphertext".into())

View file

@ -7,10 +7,9 @@ use super::handle_encrypted;
use crate::api::proto::client::{self as proto};
use crate::api::Server;
use crate::bridge::api::ServerResult;
use crate::bridge::callbacks::get_callbacks;
use crate::context::Context;
use crate::database::app::tables::{Contact, MediaFile, NewReceipt, Receipt};
use crate::error::{twonly_error, Result, TwonlyError};
use crate::error::{Result, TwonlyError};
use crate::services::contacts::ContactService;
use crate::utils::new_uuid_v4;
use prost::Message as ProstMessage;
@ -31,16 +30,10 @@ pub(crate) async fn queue_encrypted_content(
content: proto::EncryptedContent,
contact_will_send_receipt: bool,
) -> Result<String> {
let Some(contact) = Contact::get_contact_by_id(t, target_user_id).await? else {
return Err(twonly_error!("missing contact"));
};
Contact::ensure_exists(t, target_user_id).await?;
let message = proto::Message {
r#type: if contact.signal_version == "v2" {
proto::message::Type::CiphertextV2 as i32
} else {
proto::message::Type::Ciphertext as i32
},
r#type: proto::message::Type::CiphertextV2 as i32,
receipt_id: String::new(),
encrypted_content: Some(content.encode_to_vec()),
plaintext_content: None,
@ -97,17 +90,8 @@ pub(crate) async fn process_encrypted_or_queue_error(
..Default::default()
};
let signal_version = Contact::get_contact_by_id(t, from_user_id)
.await?
.map(|c| c.signal_version)
.unwrap_or_else(|| "v2".to_string());
let response = proto::Message {
r#type: if signal_version == "v2" {
proto::message::Type::CiphertextV2 as i32
} else {
proto::message::Type::Ciphertext as i32
},
r#type: proto::message::Type::CiphertextV2 as i32,
receipt_id: String::new(),
encrypted_content: Some(response_content.encode_to_vec()),
plaintext_content: None,
@ -298,7 +282,7 @@ async fn encrypt_v2_with_session_recovery(
);
ContactService::new(ctx)
.establish_signal_session(contact_id)
.establish_signal_session(contact_id, None)
.await?;
encrypt(plaintext).await
@ -307,6 +291,84 @@ async fn encrypt_v2_with_session_recovery(
}
}
pub(crate) struct PreparedQueuedReceipt {
pub contact_id: i64,
pub message_id: Option<String>,
pub contact_will_sends_receipt: i64,
pub account_deleted: i64,
pub payload: Vec<u8>,
}
pub(crate) async fn prepare_queued_receipt_details(
ctx: &Arc<Context>,
receipt_id: &str,
) -> Result<Option<PreparedQueuedReceipt>> {
let app_db = ctx.app_db.read().await.clone();
let row = sqlx::query!(
r#"
SELECT r.contact_id, r.message, r.message_id, r.contact_will_sends_receipt,
r.retry_count, c.account_deleted, c.signal_version
FROM receipts r
JOIN contacts c ON c.user_id = r.contact_id
WHERE r.receipt_id = ?
"#,
receipt_id,
)
.fetch_optional(&app_db.pool)
.await?;
let Some(row) = row else {
return Ok(None);
};
let mut message = proto::Message::decode(row.message.as_slice())
.map_err(|error| TwonlyError::Generic(format!("invalid queued message: {error}")))?;
message.receipt_id = receipt_id.to_owned();
let message_type = proto::message::Type::try_from(message.r#type)
.map_err(|_| TwonlyError::Generic("queued message has invalid type".into()))?;
let is_encrypted = matches!(
message_type,
proto::message::Type::Ciphertext
| proto::message::Type::PrekeyBundle
| proto::message::Type::CiphertextV2
);
if is_encrypted {
if row.signal_version != "v2" {
ContactService::new(ctx)
.establish_signal_session(row.contact_id, None)
.await?;
}
let plaintext = message.encrypted_content.take().ok_or_else(|| {
TwonlyError::Generic("queued encrypted message has no content".into())
})?;
message.encrypted_content =
Some(encrypt_v2_with_session_recovery(ctx, row.contact_id, plaintext).await?);
message.r#type = proto::message::Type::CiphertextV2 as i32;
}
Ok(Some(PreparedQueuedReceipt {
contact_id: row.contact_id,
message_id: row.message_id,
contact_will_sends_receipt: row.contact_will_sends_receipt,
account_deleted: row.account_deleted,
payload: message.encode_to_vec(),
}))
}
pub(crate) async fn prepare_queued_receipt(
ctx: &Arc<Context>,
receipt_id: &str,
) -> Result<Option<Vec<u8>>> {
Ok(prepare_queued_receipt_details(ctx, receipt_id)
.await?
.map(|receipt| receipt.payload))
}
pub(crate) async fn send_queued_receipt(ctx: &Arc<Context>, receipt_id: &str) -> Result<()> {
#[cfg(not(debug_assertions))]
{
@ -324,56 +386,17 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc<Context>, receipt_id: &str) ->
}
let app_db = ctx.app_db.read().await.clone();
let row = sqlx::query!(
r#"
SELECT r.contact_id, r.message, r.message_id, r.contact_will_sends_receipt,
r.retry_count, c.account_deleted
FROM receipts r
JOIN contacts c ON c.user_id = r.contact_id
WHERE r.receipt_id = ?
"#,
receipt_id,
)
.fetch_optional(&app_db.pool)
.await?;
let Some(row) = row else {
let Some(receipt) = prepare_queued_receipt_details(ctx, receipt_id).await? else {
return Ok(());
};
if row.account_deleted != 0 {
if receipt.account_deleted != 0 {
Receipt::delete(&app_db.pool, receipt_id).await?;
app_db.notify_committed(["receipts"]);
return Ok(());
}
let mut message = proto::Message::decode(row.message.as_slice())?;
message.receipt_id = receipt_id.to_owned();
let message_type = proto::message::Type::try_from(message.r#type)?;
match message_type {
proto::message::Type::Ciphertext | proto::message::Type::PrekeyBundle => {
let plaintext = message.encrypted_content.take().ok_or_else(|| {
TwonlyError::Generic("queued legacy message has no plaintext".into())
})?;
let (ciphertext, encrypted_type) =
encrypt_legacy_signal(row.contact_id, plaintext).await?;
message.encrypted_content = Some(ciphertext);
message.r#type = encrypted_type;
}
proto::message::Type::CiphertextV2 => {
let plaintext = message
.encrypted_content
.take()
.ok_or_else(|| TwonlyError::Generic("queued V2 message has no plaintext".into()))?;
message.encrypted_content =
Some(encrypt_v2_with_session_recovery(ctx, row.contact_id, plaintext).await?);
}
_ => {}
}
match Server::send_text_message(ctx, row.contact_id, message.encode_to_vec()).await? {
match Server::send_text_message(ctx, receipt.contact_id, receipt.payload).await? {
ServerResult::Ok(()) => {}
ServerResult::ErrorCode(code) => {
return Err(TwonlyError::Generic(format!(
@ -384,7 +407,7 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc<Context>, receipt_id: &str) ->
}
let mut t = app_db.pool.begin().await?;
if let Some(message_id) = row.message_id {
if let Some(message_id) = receipt.message_id {
sqlx::query!(
r#"
INSERT INTO message_actions(message_id, contact_id, type)
@ -393,12 +416,12 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc<Context>, receipt_id: &str) ->
DO UPDATE SET action_at = CAST(strftime('%s', 'now') AS INTEGER)
"#,
message_id,
row.contact_id,
receipt.contact_id,
)
.execute(&mut *t)
.await?;
}
if row.contact_will_sends_receipt == 0 {
if receipt.contact_will_sends_receipt == 0 {
Receipt::delete(&mut *t, receipt_id).await?;
} else {
sqlx::query!(
@ -420,52 +443,6 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc<Context>, receipt_id: &str) ->
Ok(())
}
pub(crate) async fn prepare_queued_receipt(
ctx: &Arc<Context>,
receipt_id: &str,
) -> Result<Option<Vec<u8>>> {
let database = ctx.app_db.read().await.clone();
let row = sqlx::query!(
r#"SELECT contact_id, message, message_id, retry_count
FROM receipts WHERE receipt_id = ?"#,
receipt_id,
)
.fetch_optional(&database.pool)
.await?;
let Some(row) = row else { return Ok(None) };
let mut message = proto::Message::decode(row.message.as_slice())
.map_err(|error| TwonlyError::Generic(format!("invalid queued message: {error}")))?;
message.receipt_id = receipt_id.to_owned();
match proto::message::Type::try_from(message.r#type)
.map_err(|_| TwonlyError::Generic("queued message has invalid type".into()))?
{
proto::message::Type::Ciphertext => {
let plaintext = message
.encrypted_content
.take()
.ok_or_else(|| TwonlyError::Generic("queued message has no content".into()))?;
let (ciphertext, message_type) =
encrypt_legacy_signal(row.contact_id, plaintext).await?;
message.encrypted_content = Some(ciphertext);
message.r#type = message_type;
}
proto::message::Type::CiphertextV2 => {
let plaintext = message
.encrypted_content
.take()
.ok_or_else(|| TwonlyError::Generic("queued message has no content".into()))?;
message.encrypted_content =
Some(encrypt_v2_with_session_recovery(ctx, row.contact_id, plaintext).await?);
}
_ => {}
}
Ok(Some(message.encode_to_vec()))
}
pub async fn retransmit_queued_receipts(ctx: &Arc<Context>) -> Result<()> {
let database = ctx.app_db.read().await.clone();
let receipt_ids = sqlx::query_scalar!(
@ -490,27 +467,6 @@ pub async fn retransmit_queued_receipts(ctx: &Arc<Context>) -> Result<()> {
Ok(())
}
pub(crate) async fn decrypt_legacy_signal_with_error(
from_user_id: i64,
ciphertext: Vec<u8>,
message_type: i32,
) -> Result<core::result::Result<proto::EncryptedContent, i32>> {
let callbacks = get_callbacks()?;
let decrypted = (callbacks.legacy_signal.decrypt)(from_user_id, ciphertext, message_type).await;
let Some(plaintext) = decrypted.plaintext else {
return Ok(Err(decrypted.decryption_error_type.unwrap_or(0)));
};
proto::EncryptedContent::decode(plaintext.as_slice())
.map(Ok)
.map_err(|error| {
TwonlyError::Generic(format!(
"Flutter returned invalid legacy Signal plaintext: {error}"
))
})
}
pub(crate) async fn queue_decryption_error(
tr: &mut Transaction<'_, Sqlite>,
from_user_id: i64,
@ -536,28 +492,6 @@ pub(crate) async fn queue_decryption_error(
Ok(())
}
pub(crate) async fn encrypt_legacy_signal(
target_user_id: i64,
plaintext: Vec<u8>,
) -> Result<(Vec<u8>, i32)> {
let callbacks = get_callbacks()?;
let encrypted = (callbacks.legacy_signal.encrypt)(target_user_id, plaintext)
.await
.ok_or_else(|| TwonlyError::Generic("Flutter legacy Signal encryption failed".into()))?;
if !is_legacy_signal_type(encrypted.message_type) {
return Err(TwonlyError::Generic(format!(
"Flutter returned non-legacy Signal message type {}",
encrypted.message_type
)));
}
Ok((encrypted.ciphertext, encrypted.message_type))
}
fn is_legacy_signal_type(message_type: i32) -> bool {
use proto::message::Type;
message_type == Type::Ciphertext as i32 || message_type == Type::PrekeyBundle as i32
}
pub(crate) async fn handle_sender_delivery_receipt(
transaction: &mut Transaction<'_, Sqlite>,
from_user_id: i64,

View file

@ -24,15 +24,38 @@ message Handshake {
message Register {
string username = 1;
optional string invite_code = 2;
bytes public_identity_key = 3;
bytes signed_prekey = 4;
bytes signed_prekey_signature = 5;
int64 signed_prekey_id = 6;
int64 registration_id = 7;
// Deprecated legacy Signal fields. The server ignores these fields; they
// remain allocated only to avoid reusing their wire tags.
optional bytes public_identity_key = 3;
optional bytes signed_prekey = 4;
optional bytes signed_prekey_signature = 5;
optional int64 signed_prekey_id = 6;
optional int64 registration_id = 7;
bool is_ios = 8;
string lang_code = 9;
int64 proof_of_work = 10;
optional bytes login_token = 11;
optional InitialPqcKeys initial_pqc_keys = 12;
}
message InitialPqcKeys {
message PqcPreKey {
int64 ecc_pre_key_id = 1;
bytes ecc_pre_key = 2;
int64 kyber_pre_key_id = 3;
bytes kyber_pre_key = 4;
bytes kyber_pre_key_signature = 5;
}
bytes public_identity_key = 1;
int64 registration_id = 2;
int64 ecc_signed_prekey_id = 3;
bytes ecc_signed_prekey = 4;
bytes ecc_signed_prekey_signature = 5;
int64 kyber_signed_prekey_id = 6;
bytes kyber_signed_prekey = 7;
bytes kyber_signed_prekey_signature = 8;
repeated PqcPreKey prekeys = 9;
}
message GetAuthChallenge {}
@ -159,6 +182,10 @@ message ApplicationData {
bytes kyber_signed_prekey = 5;
bytes kyber_signed_prekey_signature = 6;
repeated PqcPreKey prekeys = 7;
// Required when uploading keys for the first time after a keyless
// registration. Optional for later key refreshes.
optional bytes public_identity_key = 8;
optional int64 registration_id = 9;
}
message DownloadDone {

View file

@ -25,8 +25,6 @@ pub(crate) struct ApiAuthHandshaker {
pub context: Arc<Context>,
pub api_client: Weak<ApiClient>,
pub is_authenticated: Arc<AtomicBool>,
pub can_use_login_token_for_auth: bool,
pub legacy_user_app_version: i64,
pub in_background: bool,
pub events: broadcast::Sender<ApiEvent>,
}
@ -308,21 +306,37 @@ impl Handshaker for ApiAuthHandshaker {
receiver: &mut dyn HandshakeReceiver,
_context: &ConnectionContext,
) -> std::result::Result<(), HandshakeError> {
let user_id = match self.context.user_id().await {
Ok(id) => id,
Err(e) => {
tracing::info!("ApiAuthHandshaker skipped: user_id missing ({})", e);
let user = match UserConfig::load_from(&self.context)
.map_err(|e| HandshakeError::Protocol(e.to_string()))?
{
Some(u) => u,
None => {
tracing::info!("ApiAuthHandshaker skipped: user config is not present");
return Ok(());
}
};
let user_id = self
.context
.key_manager
.lock()
.await
.user_id
.filter(|user_id| *user_id > 0);
let Some(user_id) = user_id else {
tracing::info!("ApiAuthHandshaker skipped: KeyManager has no registered user ID");
return Ok(());
};
let can_use_login_token = user.can_use_login_token_for_auth;
let app_version = user.app_version;
let _ = self.events.send(ApiEvent {
kind: ApiEventKind::ConnectionStateChanged,
state: Some(ApiConnectionState::Authenticating),
message: None,
});
if self.can_use_login_token_for_auth {
if can_use_login_token {
match self
.authenticate_with_login_token(sender, receiver, user_id)
.await
@ -337,7 +351,7 @@ impl Handshaker for ApiAuthHandshaker {
return Err(HandshakeError::Protocol(error.to_string()));
}
}
} else if self.legacy_user_app_version >= 62
} else if app_version >= 62
&& self
.authenticate_with_legacy_token(sender, receiver, user_id)
.await
@ -350,10 +364,10 @@ impl Handshaker for ApiAuthHandshaker {
tracing::error!(
"ApiAuthHandshaker: no auth method succeeded. legacy_app_version={}",
self.legacy_user_app_version
app_version
);
if self.legacy_user_app_version < 62 {
if app_version < 62 {
return Err(HandshakeError::Protocol(
"legacy user version is too old for API authentication".into(),
));

View file

@ -92,8 +92,6 @@ impl ApiClient {
context,
api_client: Arc::downgrade(self),
is_authenticated: self.is_authenticated.clone(),
can_use_login_token_for_auth: self.config.can_use_login_token_for_auth,
legacy_user_app_version: self.config.legacy_user_app_version,
in_background: self.in_background.load(Ordering::Acquire),
events: self.events.clone(),
};

View file

@ -16,6 +16,7 @@ use std::pin::Pin;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use stream_tungstenite::error::SendError;
use tokio::sync::oneshot;
fn call_handle_server_message(
@ -80,15 +81,18 @@ impl ApiClient {
pub(crate) async fn send(self: &Arc<Self>, bytes: Vec<u8>) -> Result<()> {
let ws_client = self.ws_client.lock().await.clone();
if let Some(client) = ws_client {
client
.send_async(
stream_tungstenite::tokio_tungstenite::tungstenite::Message::Binary(
bytes.into(),
),
)
.await
.map_err(|e| TwonlyError::Generic(format!("send error: {:?}", e)))?;
Ok(())
let msg =
stream_tungstenite::tokio_tungstenite::tungstenite::Message::Binary(bytes.into());
let start = tokio::time::Instant::now();
loop {
match client.send_async(msg.clone()).await {
Ok(_) => return Ok(()),
Err(SendError::NotConnected) if start.elapsed() < Duration::from_secs(10) => {
tokio::time::sleep(Duration::from_millis(50)).await;
}
Err(e) => return Err(TwonlyError::Generic(format!("send error: {:?}", e))),
}
}
} else {
Err(TwonlyError::Generic("Not connected".into()))
}
@ -147,14 +151,32 @@ impl ApiClient {
let ws_client = self.ws_client.lock().await.clone();
if let Some(client) = ws_client {
client
.send_async(
stream_tungstenite::tokio_tungstenite::tungstenite::Message::Binary(
let msg = stream_tungstenite::tokio_tungstenite::tungstenite::Message::Binary(
request.encode_to_vec().into(),
),
)
.await
.map_err(|e| TwonlyError::Generic(format!("send error: {:?}", e)))?;
);
let start = tokio::time::Instant::now();
let mut sent = false;
while start.elapsed() < Duration::from_secs(10) {
match client.send_async(msg.clone()).await {
Ok(_) => {
sent = true;
break;
}
Err(SendError::NotConnected) => {
tokio::time::sleep(Duration::from_millis(50)).await;
}
Err(e) => {
self.pending.lock().await.remove(&sequence);
return Err(TwonlyError::Generic(format!("send error: {:?}", e)));
}
}
}
if !sent {
self.pending.lock().await.remove(&sequence);
return Err(TwonlyError::Generic(
"send error: NotConnected (timeout)".into(),
));
}
} else {
self.pending.lock().await.remove(&sequence);
return Err(TwonlyError::Generic("Not connected".into()));

View file

@ -10,7 +10,6 @@ use crate::api::runtime::helpers::decode_ok_value;
use crate::bridge::api::ServerResult;
use crate::context::Context;
use crate::error::{Result, TwonlyError};
use libsignal_protocol::{GenericSignedPreKey, IdentityKeyPair, SignedPreKeyRecord};
use std::sync::Arc;
impl Server {
@ -21,44 +20,69 @@ impl Server {
lang_code: String,
is_ios: bool,
) -> Result<ServerResult<i64>> {
let key_manager = ctx.key_manager.lock().await;
let mut key_manager = ctx.key_manager.lock().await;
if key_manager.signal_identity.is_none() {
let identity = crate::keys::SignalIdentityKey::generate()?;
key_manager.signal_identity = Some(identity);
key_manager.store_to_keychain(&ctx.secure_storage)?;
}
let identity = key_manager
.signal_identity
.as_ref()
.ok_or(TwonlyError::SignalIdentityNotFound)?;
let identity_pair =
IdentityKeyPair::try_from(identity.identity_key_pair_structure.as_slice())
.map_err(|error| TwonlyError::Signal(error.to_string()))?;
let (&signed_prekey_id, record) = identity
.pre_key_store
.iter()
.min_by_key(|(id, _)| *id)
.ok_or_else(|| {
TwonlyError::Generic("Signal signed-prekey store is empty".into())
})?;
let signed_prekey = SignedPreKeyRecord::deserialize(record)
.map_err(|error| TwonlyError::Signal(error.to_string()))?;
let identity_key_pair_structure = identity.identity_key_pair_structure.clone();
let registration_id = identity.registration_id;
let login_token = key_manager.main_key.get_login_token().to_vec();
drop(key_manager);
let database = ctx.rust_db.read().await.clone();
let registration_engine = crate::signal::engine::RustSignalEngine::new_with_pool(
database.pool.clone(),
identity_key_pair_structure.clone(),
registration_id as u32,
"pending-registration".to_string(),
)?;
let bundle = registration_engine.generate_bundle().await?;
let prekeys = registration_engine
.generate_pqc_prekeys()
.await?
.into_iter()
.map(
|key| client_to_server::handshake::initial_pqc_keys::PqcPreKey {
ecc_pre_key_id: i64::from(key.ecc_pre_key_id),
ecc_pre_key: key.ecc_pre_key,
kyber_pre_key_id: i64::from(key.kyber_pre_key_id),
kyber_pre_key: key.kyber_pre_key,
kyber_pre_key_signature: key.kyber_pre_key_signature,
},
)
.collect();
let initial_pqc_keys = client_to_server::handshake::InitialPqcKeys {
public_identity_key: bundle.identity_key,
registration_id: i64::from(bundle.registration_id),
ecc_signed_prekey_id: i64::from(bundle.signed_pre_key_id),
ecc_signed_prekey: bundle.signed_pre_key_public,
ecc_signed_prekey_signature: bundle.signed_pre_key_signature,
kyber_signed_prekey_id: i64::from(bundle.kyber_pre_key_id),
kyber_signed_prekey: bundle.kyber_pre_key_public,
kyber_signed_prekey_signature: bundle.kyber_pre_key_signature,
prekeys,
};
let register = client_to_server::handshake::Register {
username,
invite_code: None,
public_identity_key: identity_pair.identity_key().serialize().to_vec(),
signed_prekey: signed_prekey
.public_key()
.map_err(|error| TwonlyError::Signal(error.to_string()))?
.serialize()
.to_vec(),
signed_prekey_signature: signed_prekey
.signature()
.map_err(|error| TwonlyError::Signal(error.to_string()))?,
signed_prekey_id,
registration_id: identity.registration_id,
public_identity_key: None,
signed_prekey: None,
signed_prekey_signature: None,
signed_prekey_id: None,
registration_id: None,
is_ios,
lang_code,
proof_of_work,
login_token: Some(login_token),
initial_pqc_keys: Some(initial_pqc_keys),
};
drop(key_manager);
let bytes = Self::handshake(
ctx,
@ -66,10 +90,43 @@ impl Server {
)
.await?;
decode_ok_value(bytes, |value| match value {
let res = decode_ok_value(bytes, |value| match value {
ResponseOk::Userid(id) => Some(id),
_ => None,
})
})?;
if let ServerResult::Ok(user_id) = res {
let mut key_manager = ctx.key_manager.lock().await;
key_manager.user_id = Some(user_id);
key_manager.store_to_keychain(&ctx.secure_storage)?;
let signal_identity = key_manager.signal_identity.as_ref().map(|identity| {
(
identity.identity_key_pair_structure.clone(),
identity.registration_id,
)
});
drop(key_manager);
if let Some((identity_key_pair_structure, registration_id)) = signal_identity {
let database = ctx.rust_db.read().await.clone();
*ctx.signal_engine.lock().await =
Some(crate::signal::engine::RustSignalEngine::new_with_pool(
database.pool.clone(),
identity_key_pair_structure,
registration_id as u32,
user_id.to_string(),
)?);
}
let now = crate::utils::current_time().with_timezone(&chrono::Utc);
let _ = crate::user_config::UserConfig::update(ctx, |config| {
config.signal_last_signed_pre_key_updated = Some(now);
config.signal_last_pqc_pre_keys_uploaded = Some(now);
});
let _ = crate::api::ApiRuntime::reload_configuration(ctx).await;
}
Ok(res)
}
pub async fn get_proof_of_work(ctx: &Arc<Context>) -> Result<ServerResult<ProofOfWork>> {

View file

@ -26,17 +26,33 @@ impl Server {
.ok_or(crate::error::TwonlyError::SignalIdentityNotFound)?
.generate_bundle()
.await?;
let prekeys = engine
.as_ref()
.ok_or(crate::error::TwonlyError::SignalIdentityNotFound)?
.generate_pqc_prekeys()
.await?
.into_iter()
.map(|key| PqcPreKeyInput {
ecc_pre_key_id: i64::from(key.ecc_pre_key_id),
ecc_pre_key: key.ecc_pre_key,
kyber_pre_key_id: i64::from(key.kyber_pre_key_id),
kyber_pre_key: key.kyber_pre_key,
kyber_pre_key_signature: key.kyber_pre_key_signature,
})
.collect();
drop(engine);
Self::upload_pqc_pre_keys(
ctx,
bundle.identity_key,
i64::from(bundle.registration_id),
i64::from(bundle.signed_pre_key_id),
bundle.signed_pre_key_public,
bundle.signed_pre_key_signature,
i64::from(bundle.kyber_pre_key_id),
bundle.kyber_pre_key_public,
bundle.kyber_pre_key_signature,
Vec::new(),
prekeys,
)
.await
}
@ -63,6 +79,8 @@ impl Server {
#[allow(clippy::too_many_arguments)]
pub async fn upload_pqc_pre_keys(
ctx: &Arc<Context>,
public_identity_key: Vec<u8>,
registration_id: i64,
ecc_signed_prekey_id: i64,
ecc_signed_prekey: Vec<u8>,
ecc_signed_prekey_signature: Vec<u8>,
@ -92,6 +110,8 @@ impl Server {
kyber_signed_prekey,
kyber_signed_prekey_signature,
prekeys,
public_identity_key: Some(public_identity_key),
registration_id: Some(registration_id),
},
),
)

View file

@ -73,6 +73,13 @@ impl Server {
ctx: &Arc<Context>,
value: client_to_server::handshake::Handshake,
) -> Result<Vec<u8>> {
let client = ApiRuntime::client(ctx).await?;
if client
.is_authenticated
.load(std::sync::atomic::Ordering::Acquire)
{
client.close().await;
}
ApiRuntime::request_binary(ctx, Self::handshake_request(value)).await
}
}

View file

@ -18,7 +18,7 @@ impl BackupIdentity {
return Err(TwonlyError::Generic("No backup password".into()));
};
let serialized_bytes = postcard::to_allocvec(key_manager)?;
let serialized_bytes = key_manager.to_bytes()?;
let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&keys.encryption_key);
let cipher = Aes256Gcm::new(key);
@ -55,7 +55,7 @@ impl BackupIdentity {
let decrypted_bytes = cipher.decrypt(nonce, ciphertext)?;
let key_manager: KeyManager = postcard::from_bytes(&decrypted_bytes)?;
let key_manager = KeyManager::from_bytes(&decrypted_bytes)?;
key_manager.store_to_keychain(secure_storage)?;
@ -69,6 +69,7 @@ mod tests {
#[test]
fn test_backup_encryption_decryption() {
SecureStorage::init().unwrap();
let secure_storage = SecureStorage::new("testing");
let mut key_manager = KeyManager::generate().unwrap();
let password = "my_secure_password";

View file

@ -3,16 +3,17 @@
*
*/
use crate::api::messages::incoming::messages;
use crate::api::proto::server_to_client::response::ok::Ok as ResponseOk;
use crate::api::runtime::helpers::decode_ok_value;
use crate::api::ApiRuntime;
pub use crate::api::PqcPreKeyInput;
use crate::api::Server;
use crate::api::messages::incoming::messages;
use crate::api::proto::server_to_client::response::ok::Ok as ResponseOk;
use crate::api::runtime::helpers::decode_ok_value;
use crate::context::Context;
use crate::error::{Result, TwonlyError};
use crate::frb_generated::StreamSink;
use crate::services::contacts::ContactService;
use crate::services::messages::MessageService;
use crate::user_config::UserConfig;
use flutter_rust_bridge::frb;
use prost::Message;
@ -432,6 +433,8 @@ impl RustApi {
}
#[allow(clippy::too_many_arguments)]
pub async fn upload_pqc_pre_keys(
public_identity_key: Vec<u8>,
registration_id: i64,
ecc_signed_prekey_id: i64,
ecc_signed_prekey: Vec<u8>,
ecc_signed_prekey_signature: Vec<u8>,
@ -443,6 +446,8 @@ impl RustApi {
let ctx = Context::get_static()?;
Server::upload_pqc_pre_keys(
ctx,
public_identity_key,
registration_id,
ecc_signed_prekey_id,
ecc_signed_prekey,
ecc_signed_prekey_signature,
@ -465,9 +470,9 @@ impl RustApi {
contact_id: i64,
content: Vec<u8>,
message_id: Option<String>,
only_send_if_no_receipts_are_open: bool,
only_return_encrypted_data: bool,
blocking: bool,
only_send_if_no_receipts_are_open: Option<bool>,
only_return_encrypted_data: Option<bool>,
blocking: Option<bool>,
) -> Result<Option<Vec<u8>>> {
let ctx = Context::get_static()?;
crate::api::messages::outgoing::send_c2c_message_to_contact()
@ -475,9 +480,9 @@ impl RustApi {
.contact_id(contact_id)
.encrypted_content(content)
.maybe_message_id(message_id)
.only_send_if_no_receipts_are_open(only_send_if_no_receipts_are_open)
.only_return_encrypted_data(only_return_encrypted_data)
.blocking(blocking)
.only_send_if_no_receipts_are_open(only_send_if_no_receipts_are_open.unwrap_or(false))
.only_return_encrypted_data(only_return_encrypted_data.unwrap_or(false))
.blocking(blocking.unwrap_or(false))
.call()
.await
.map_err(Into::into)
@ -487,7 +492,7 @@ impl RustApi {
group_id: String,
content: Vec<u8>,
message_id: Option<String>,
only_send_if_no_receipts_are_open: bool,
only_send_if_no_receipts_are_open: Option<bool>,
) -> Result<()> {
let ctx = Context::get_static()?;
crate::services::messages::MessageService::new(ctx)
@ -495,7 +500,7 @@ impl RustApi {
group_id,
content,
message_id,
only_send_if_no_receipts_are_open,
only_send_if_no_receipts_are_open.unwrap_or(false),
)
.await
}
@ -571,15 +576,23 @@ impl RustApi {
pub async fn notify_messages_opened(contact_id: i64, message_ids: Vec<String>) -> Result<()> {
let ctx = Context::get_static()?;
crate::services::messages::MessageService::new(ctx)
MessageService::new(ctx)
.notify_opened(contact_id, message_ids)
.await
}
pub async fn send_contact_profile(contact_id: i64) -> Result<()> {
let ctx = Context::get_static()?;
crate::services::contacts::ContactService::new(ctx)
.send_profile(contact_id)
ContactService::new(ctx).send_profile(contact_id).await
}
pub async fn establish_signal_session(
contact_id: i64,
expected_public_key: Option<Vec<u8>>,
) -> Result<()> {
let ctx = Context::get_static()?;
ContactService::new(ctx)
.establish_signal_session(contact_id, expected_public_key)
.await
}
}

View file

@ -20,33 +20,13 @@ tokio::task_local! {
pub(crate) static FLUTTER_CALLBACKS: std::sync::RwLock<Option<HashMap<u32, FlutterCallbacks>>> =
std::sync::RwLock::new(None);
#[derive(Clone, Debug)]
pub struct LegacySignalDecryptResult {
/// Serialized `EncryptedContent` when legacy Signal decryption succeeded.
pub plaintext: Option<Vec<u8>>,
/// Serialized protobuf enum value for `DecryptionErrorMessage.Type`.
pub decryption_error_type: Option<i32>,
}
#[derive(Clone, Debug)]
pub struct LegacySignalEncryptResult {
pub ciphertext: Vec<u8>,
/// `Message.Type.CIPHERTEXT` or `Message.Type.PREKEY_BUNDLE`.
pub message_type: i32,
}
// This will also generate the function init_flutter_callbacks which MUST be called from Flutter to initialize the callbacks
callback_generator! {
FlutterCallbacks {
Logging logging {
get_stream_sink: () => StreamSink<String>
},
LegacySignal legacy_signal {
decrypt: (i64, Vec<u8>, i32) => LegacySignalDecryptResult,
encrypt: (i64, Vec<u8>) => Option<LegacySignalEncryptResult>
},
Api api {
resync_signal_session: (i64) => (),
media_action: (String, String, i64, String) => (),
verification_proof: (i64, Vec<u8>) => (),
create_push_avatars: (i64) => (),

View file

@ -3,8 +3,6 @@
*
*/
use std::collections::HashMap;
use crate::bridge::get_twonly_flutter;
use crate::error::{Result, TwonlyError};
use crate::keys::SignalIdentityKey;
@ -35,7 +33,10 @@ impl RustKeyManager {
match RustSignalEngine::new(user_id.to_string()).await {
Ok(engine) => *guard = Some(engine),
Err(e) => {
tracing::warn!("Failed to initialize Signal engine on set_user_id: {}. It will be initialized later.", e);
tracing::warn!(
"Failed to initialize Signal engine on set_user_id: {}. It will be initialized later.",
e
);
*guard = None;
}
}
@ -46,7 +47,6 @@ impl RustKeyManager {
pub async fn import_signal_identity(
identity_key_pair_structure: Vec<u8>,
registration_id: i64,
signed_pre_key_store: HashMap<i64, Vec<u8>>,
) -> Result<()> {
let ctx = get_twonly_flutter()?;
let user_id = {
@ -54,7 +54,6 @@ impl RustKeyManager {
key_manager.signal_identity = Some(SignalIdentityKey {
identity_key_pair_structure,
registration_id,
pre_key_store: signed_pre_key_store,
});
key_manager.store_to_keychain(&ctx.secure_storage)?;
key_manager.user_id
@ -90,55 +89,6 @@ impl RustKeyManager {
}
}
pub async fn load_signed_prekey(signed_pre_key_id: i64) -> Result<Option<Vec<u8>>> {
let ctx = get_twonly_flutter()?;
let key_manager = ctx.key_manager.lock().await;
if let Some(signal_identity) = &key_manager.signal_identity {
Ok(signal_identity
.pre_key_store
.get(&signed_pre_key_id)
.cloned())
} else {
Err(TwonlyError::SignalIdentityNotFound)
}
}
pub async fn store_signed_prekey(signed_pre_key_id: i64, record: Vec<u8>) -> Result<()> {
let ctx = get_twonly_flutter()?;
let mut key_manager = ctx.key_manager.lock().await;
if let Some(signal_identity) = &mut key_manager.signal_identity {
signal_identity
.pre_key_store
.insert(signed_pre_key_id, record);
key_manager.store_to_keychain(&ctx.secure_storage)?;
Ok(())
} else {
Err(TwonlyError::SignalIdentityNotFound)
}
}
pub async fn remove_signed_prekey(signed_pre_key_id: i64) -> Result<()> {
let ctx = get_twonly_flutter()?;
let mut key_manager = ctx.key_manager.lock().await;
if let Some(signal_identity) = &mut key_manager.signal_identity {
signal_identity.pre_key_store.remove(&signed_pre_key_id);
key_manager.store_to_keychain(&ctx.secure_storage)?;
Ok(())
} else {
Err(TwonlyError::SignalIdentityNotFound)
}
}
pub async fn load_signed_prekeys() -> Result<HashMap<i64, Vec<u8>>> {
let ctx = get_twonly_flutter()?;
let key_manager = ctx.key_manager.lock().await;
if let Some(signal_identity) = &key_manager.signal_identity {
Ok(signal_identity.pre_key_store.to_owned())
} else {
Err(TwonlyError::SignalIdentityNotFound)
}
}
pub async fn remove_key_manager() -> Result<()> {
let ctx = get_twonly_flutter()?;
crate::keys::KeyManager::remove_from_keychain(&ctx.secure_storage)?;
@ -149,13 +99,12 @@ impl RustKeyManager {
pub async fn serialize() -> Result<Vec<u8>> {
let ctx = get_twonly_flutter()?;
let key_manager = ctx.key_manager.lock().await;
let serialized_bytes = postcard::to_allocvec(&*key_manager)?;
Ok(serialized_bytes)
key_manager.to_bytes()
}
pub async fn import_serialized(serialized_bytes: Vec<u8>) -> Result<()> {
let ctx = get_twonly_flutter()?;
let key_manager: crate::keys::KeyManager = postcard::from_bytes(&serialized_bytes)?;
let key_manager = crate::keys::KeyManager::from_bytes(&serialized_bytes)?;
key_manager.store_to_keychain(&ctx.secure_storage)?;
*ctx.key_manager.lock().await = key_manager;
Ok(())

View file

@ -45,4 +45,16 @@ impl RustSignal {
.decrypt_message(name.clone(), device_id, ciphertext.clone())
.await
}
pub async fn get_user_public_key() -> Result<Vec<u8>> {
let guard = get_twonly_flutter()?.signal_engine.lock().await;
let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?;
engine.get_identity_key().await
}
pub async fn get_contact_public_key(contact_id: i64) -> Result<Option<Vec<u8>>> {
let guard = get_twonly_flutter()?.signal_engine.lock().await;
let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?;
engine.get_contact_identity_key(&contact_id.to_string()).await
}
}

View file

@ -5,7 +5,7 @@
use crate::api::runtime::{ApiClient, ApiRuntime};
use crate::bridge::InitConfig;
use crate::database::app::{AppDatabase, APP_DATABASE_FILE};
use crate::database::app::{APP_DATABASE_FILE, AppDatabase};
use crate::database::signal::Database;
use crate::error::Result;
use crate::error::TwonlyError;
@ -111,13 +111,11 @@ impl Context {
&self,
identity_key_pair_structure: Vec<u8>,
registration_id: i64,
pre_key_store: std::collections::HashMap<i64, Vec<u8>>,
) -> Result<()> {
let mut key_manager = self.key_manager.lock().await;
key_manager.signal_identity = Some(crate::keys::SignalIdentityKey {
identity_key_pair_structure,
registration_id,
pre_key_store,
});
Ok(())
}

View file

@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -937778436;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1286774374;
// Section: executor
@ -327,23 +327,65 @@ fn wire__crate__bridge__callbacks__init_flutter_callbacks_impl(
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "init_flutter_callbacks", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "init_flutter_callbacks",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_callback_id = <u32>::sse_decode(&mut deserializer);
let api_logging_get_stream_sink = decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_legacy_signal_decrypt = decode_DartFn_Inputs_i_64_list_prim_u_8_strict_i_32_Output_legacy_signal_decrypt_result_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_legacy_signal_encrypt = decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_api_resync_signal_session = decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_api_media_action = decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_api_verification_proof = decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_unit_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_api_create_push_avatars = decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_api_media_received = decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_api_user_config_changed = decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));deserializer.end(); move |context| {
let api_logging_get_stream_sink =
decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer),
);
let api_api_media_action =
decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer),
);
let api_api_verification_proof =
decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_unit_AnyhowException(
<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer),
);
let api_api_create_push_avatars = decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer),
);
let api_api_media_received =
decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(
<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer),
);
let api_api_user_config_changed =
decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(
<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer),
);
deserializer.end();
move |context| {
transform_result_sse::<_, ()>((move || {
let output_ok = Result::<_,()>::Ok({ crate::bridge::callbacks::init_flutter_callbacks(api_callback_id, api_logging_get_stream_sink, api_legacy_signal_decrypt, api_legacy_signal_encrypt, api_api_resync_signal_session, api_api_media_action, api_api_verification_proof, api_api_create_push_avatars, api_api_media_received, api_api_user_config_changed); })?; Ok(output_ok)
let output_ok = Result::<_, ()>::Ok({
crate::bridge::callbacks::init_flutter_callbacks(
api_callback_id,
api_logging_get_stream_sink,
api_api_media_action,
api_api_verification_proof,
api_api_create_push_avatars,
api_api_media_received,
api_api_user_config_changed,
);
})?;
Ok(output_ok)
})())
} })
}
},
)
}
fn wire__crate__bridge__initialize_twonly_flutter_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
@ -1124,6 +1166,47 @@ fn wire__crate__bridge__api__rust_api_download_pending_media_impl(
},
)
}
fn wire__crate__bridge__api__rust_api_establish_signal_session_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "rust_api_establish_signal_session",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_contact_id = <i64>::sse_decode(&mut deserializer);
let api_expected_public_key = <Option<Vec<u8>>>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
(move || async move {
let output_ok = crate::bridge::api::RustApi::establish_signal_session(
api_contact_id,
api_expected_public_key,
)
.await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__bridge__api__rust_api_events_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -2404,9 +2487,10 @@ fn wire__crate__bridge__api__rust_api_send_encrypted_content_impl(
let api_contact_id = <i64>::sse_decode(&mut deserializer);
let api_content = <Vec<u8>>::sse_decode(&mut deserializer);
let api_message_id = <Option<String>>::sse_decode(&mut deserializer);
let api_only_send_if_no_receipts_are_open = <bool>::sse_decode(&mut deserializer);
let api_only_return_encrypted_data = <bool>::sse_decode(&mut deserializer);
let api_blocking = <bool>::sse_decode(&mut deserializer);
let api_only_send_if_no_receipts_are_open =
<Option<bool>>::sse_decode(&mut deserializer);
let api_only_return_encrypted_data = <Option<bool>>::sse_decode(&mut deserializer);
let api_blocking = <Option<bool>>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
@ -2453,7 +2537,8 @@ fn wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(
let api_group_id = <String>::sse_decode(&mut deserializer);
let api_content = <Vec<u8>>::sse_decode(&mut deserializer);
let api_message_id = <Option<String>>::sse_decode(&mut deserializer);
let api_only_send_if_no_receipts_are_open = <bool>::sse_decode(&mut deserializer);
let api_only_send_if_no_receipts_are_open =
<Option<bool>>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
@ -2845,6 +2930,8 @@ fn wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_public_identity_key = <Vec<u8>>::sse_decode(&mut deserializer);
let api_registration_id = <i64>::sse_decode(&mut deserializer);
let api_ecc_signed_prekey_id = <i64>::sse_decode(&mut deserializer);
let api_ecc_signed_prekey = <Vec<u8>>::sse_decode(&mut deserializer);
let api_ecc_signed_prekey_signature = <Vec<u8>>::sse_decode(&mut deserializer);
@ -2858,6 +2945,8 @@ fn wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
(move || async move {
let output_ok = crate::bridge::api::RustApi::upload_pqc_pre_keys(
api_public_identity_key,
api_registration_id,
api_ecc_signed_prekey_id,
api_ecc_signed_prekey,
api_ecc_signed_prekey_signature,
@ -3324,40 +3413,9 @@ fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_ide
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_identity_key_pair_structure = <Vec<u8>>::sse_decode(&mut deserializer);
let api_registration_id = <i64>::sse_decode(&mut deserializer);
let api_signed_pre_key_store = <std::collections::HashMap<i64, Vec<u8>>>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
let api_registration_id = <i64>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::import_signal_identity(api_identity_key_pair_structure, api_registration_id, api_signed_pre_key_store).await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "rust_key_manager_load_signed_prekey", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_signed_pre_key_id = <i64>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::load_signed_prekey(api_signed_pre_key_id).await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "rust_key_manager_load_signed_prekeys", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
deserializer.end(); move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::load_signed_prekeys().await?; Ok(output_ok)
let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::import_signal_identity(api_identity_key_pair_structure, api_registration_id).await?; Ok(output_ok)
})().await)
} })
}
@ -3376,21 +3434,6 @@ fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manage
})().await)
} })
}
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "rust_key_manager_remove_signed_prekey", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_signed_pre_key_id = <i64>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::remove_signed_prekey(api_signed_pre_key_id).await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -3468,22 +3511,6 @@ fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(
},
)
}
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "rust_key_manager_store_signed_prekey", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_signed_pre_key_id = <i64>::sse_decode(&mut deserializer);
let api_record = <Vec<u8>>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::store_signed_prekey(api_signed_pre_key_id, api_record).await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -3643,6 +3670,83 @@ fn wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(
},
)
}
fn wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "rust_signal_get_contact_public_key",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_contact_id = <i64>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
(move || async move {
let output_ok =
crate::bridge::wrapper::signal::RustSignal::get_contact_public_key(
api_contact_id,
)
.await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "rust_signal_get_user_public_key",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
deserializer.end();
move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
(move || async move {
let output_ok =
crate::bridge::wrapper::signal::RustSignal::get_user_public_key()
.await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -4229,53 +4333,6 @@ fn decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
))
}
}
fn decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
dart_opaque: flutter_rust_bridge::DartOpaque,
) -> impl Fn(
i64,
Vec<u8>,
) -> flutter_rust_bridge::DartFnFuture<
Option<crate::bridge::callbacks::LegacySignalEncryptResult>,
> {
use flutter_rust_bridge::IntoDart;
async fn body(
dart_opaque: flutter_rust_bridge::DartOpaque,
arg0: i64,
arg1: Vec<u8>,
) -> Option<crate::bridge::callbacks::LegacySignalEncryptResult> {
let args = vec![
arg0.into_into_dart().into_dart(),
arg1.into_into_dart().into_dart(),
];
let message = FLUTTER_RUST_BRIDGE_HANDLER
.dart_fn_invoke(dart_opaque, args)
.await;
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let action = deserializer.cursor.read_u8().unwrap();
let ans = match action {
0 => std::result::Result::Ok(<Option<
crate::bridge::callbacks::LegacySignalEncryptResult,
>>::sse_decode(&mut deserializer)),
1 => std::result::Result::Err(
<flutter_rust_bridge::for_generated::anyhow::Error>::sse_decode(&mut deserializer),
),
_ => unreachable!(),
};
deserializer.end();
let ans = ans.expect("Dart throws exception but Rust side assume it is not failable");
ans
}
move |arg0: i64, arg1: Vec<u8>| {
flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body(
dart_opaque.clone(),
arg0,
arg1,
))
}
}
fn decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_unit_AnyhowException(
dart_opaque: flutter_rust_bridge::DartOpaque,
) -> impl Fn(i64, Vec<u8>) -> flutter_rust_bridge::DartFnFuture<()> {
@ -4312,57 +4369,6 @@ fn decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_unit_AnyhowException(
))
}
}
fn decode_DartFn_Inputs_i_64_list_prim_u_8_strict_i_32_Output_legacy_signal_decrypt_result_AnyhowException(
dart_opaque: flutter_rust_bridge::DartOpaque,
) -> impl Fn(
i64,
Vec<u8>,
i32,
) -> flutter_rust_bridge::DartFnFuture<crate::bridge::callbacks::LegacySignalDecryptResult> {
use flutter_rust_bridge::IntoDart;
async fn body(
dart_opaque: flutter_rust_bridge::DartOpaque,
arg0: i64,
arg1: Vec<u8>,
arg2: i32,
) -> crate::bridge::callbacks::LegacySignalDecryptResult {
let args = vec![
arg0.into_into_dart().into_dart(),
arg1.into_into_dart().into_dart(),
arg2.into_into_dart().into_dart(),
];
let message = FLUTTER_RUST_BRIDGE_HANDLER
.dart_fn_invoke(dart_opaque, args)
.await;
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let action = deserializer.cursor.read_u8().unwrap();
let ans = match action {
0 => std::result::Result::Ok(
<crate::bridge::callbacks::LegacySignalDecryptResult>::sse_decode(
&mut deserializer,
),
),
1 => std::result::Result::Err(
<flutter_rust_bridge::for_generated::anyhow::Error>::sse_decode(&mut deserializer),
),
_ => unreachable!(),
};
deserializer.end();
let ans = ans.expect("Dart throws exception but Rust side assume it is not failable");
ans
}
move |arg0: i64, arg1: Vec<u8>, arg2: i32| {
flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body(
dart_opaque.clone(),
arg0,
arg1,
arg2,
))
}
}
fn decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(
dart_opaque: flutter_rust_bridge::DartOpaque,
) -> impl Fn(crate::user_config::UserConfig) -> flutter_rust_bridge::DartFnFuture<()> {
@ -4438,14 +4444,6 @@ impl SseDecode for std::collections::HashMap<String, Vec<String>> {
}
}
impl SseDecode for std::collections::HashMap<i64, Vec<u8>> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <Vec<(i64, Vec<u8>)>>::sse_decode(deserializer);
return inner.into_iter().collect();
}
}
impl SseDecode for StreamSink<String, flutter_rust_bridge::for_generated::SseCodec> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -4665,30 +4663,6 @@ impl SseDecode for crate::bridge::wrapper::app_database::LegacyMigrationReport {
}
}
impl SseDecode for crate::bridge::callbacks::LegacySignalDecryptResult {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_plaintext = <Option<Vec<u8>>>::sse_decode(deserializer);
let mut var_decryptionErrorType = <Option<i32>>::sse_decode(deserializer);
return crate::bridge::callbacks::LegacySignalDecryptResult {
plaintext: var_plaintext,
decryption_error_type: var_decryptionErrorType,
};
}
}
impl SseDecode for crate::bridge::callbacks::LegacySignalEncryptResult {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_ciphertext = <Vec<u8>>::sse_decode(deserializer);
let mut var_messageType = <i32>::sse_decode(deserializer);
return crate::bridge::callbacks::LegacySignalEncryptResult {
ciphertext: var_ciphertext,
message_type: var_messageType,
};
}
}
impl SseDecode for crate::bridge::wrapper::app_database::LegacyTableMigrationCount {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -4793,18 +4767,6 @@ impl SseDecode for Vec<u8> {
}
}
impl SseDecode for Vec<(i64, Vec<u8>)> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut len_ = <i32>::sse_decode(deserializer);
let mut ans_ = Vec::with_capacity(len_ as usize);
for idx_ in 0..len_ {
ans_.push(<(i64, Vec<u8>)>::sse_decode(deserializer));
}
return ans_;
}
}
impl SseDecode for Vec<(String, Vec<String>)> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -4889,6 +4851,17 @@ impl SseDecode for Option<crate::bridge::api::ApiConnectionState> {
}
}
impl SseDecode for Option<bool> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
if (<bool>::sse_decode(deserializer)) {
return Some(<bool>::sse_decode(deserializer));
} else {
return None;
}
}
}
impl SseDecode for Option<f64> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -4900,17 +4873,6 @@ impl SseDecode for Option<f64> {
}
}
impl SseDecode for Option<i32> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
if (<bool>::sse_decode(deserializer)) {
return Some(<i32>::sse_decode(deserializer));
} else {
return None;
}
}
}
impl SseDecode for Option<i64> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -4922,19 +4884,6 @@ impl SseDecode for Option<i64> {
}
}
impl SseDecode for Option<crate::bridge::callbacks::LegacySignalEncryptResult> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
if (<bool>::sse_decode(deserializer)) {
return Some(
<crate::bridge::callbacks::LegacySignalEncryptResult>::sse_decode(deserializer),
);
} else {
return None;
}
}
}
impl SseDecode for Option<crate::user_config::PasswordlessRecoveryConfig> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -5047,15 +4996,6 @@ impl SseDecode for crate::api::server::prekeys::PqcPreKeyInput {
}
}
impl SseDecode for (i64, Vec<u8>) {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_field0 = <i64>::sse_decode(deserializer);
let mut var_field1 = <Vec<u8>>::sse_decode(deserializer);
return (var_field0, var_field1);
}
}
impl SseDecode for (Vec<u8>, i64) {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -5445,91 +5385,90 @@ fn pde_ffi_dispatcher_primary_impl(
29 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len),
30 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len),
31 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len),
32 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len),
33 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len),
34 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len),
35 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len),
36 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len),
37 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len),
38 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
39 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len),
40 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len),
41 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len),
42 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len),
43 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len),
44 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len),
45 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len),
46 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len),
47 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len),
48 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len),
49 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len),
50 => wire__crate__bridge__api__rust_api_prepare_queued_message_impl(port, ptr, rust_vec_len, data_len),
51 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len),
52 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
53 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
54 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len),
55 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len),
56 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len),
57 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len),
58 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len),
59 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len),
60 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len),
61 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len),
62 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len),
63 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len),
64 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len),
65 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len),
66 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len),
67 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len),
68 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len),
69 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len),
70 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len),
71 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len),
72 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len),
73 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len),
74 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len),
75 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len),
76 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len),
77 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len),
78 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len),
79 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len),
80 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len),
81 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len),
82 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len),
83 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
84 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
85 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
86 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
87 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
88 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
89 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
90 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
91 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len),
92 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
93 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
94 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
95 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
96 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
97 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len),
98 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len),
99 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
100 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
101 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
102 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
103 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len),
104 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len),
105 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
106 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
107 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
108 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
109 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
110 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
111 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
113 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
114 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
115 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
116 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
117 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
32 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len),
33 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len),
34 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len),
35 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len),
36 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len),
37 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len),
38 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len),
39 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
40 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len),
41 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len),
42 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len),
43 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len),
44 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len),
45 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len),
46 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len),
47 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len),
48 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len),
49 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len),
50 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len),
51 => wire__crate__bridge__api__rust_api_prepare_queued_message_impl(port, ptr, rust_vec_len, data_len),
52 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len),
53 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
54 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
55 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len),
56 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len),
57 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len),
58 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len),
59 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len),
60 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len),
61 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len),
62 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len),
63 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len),
64 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len),
65 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len),
66 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len),
67 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len),
68 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len),
69 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len),
70 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len),
71 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len),
72 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len),
73 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len),
74 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len),
75 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len),
76 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len),
77 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len),
78 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len),
79 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len),
80 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len),
81 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len),
82 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len),
83 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len),
84 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
85 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
86 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
87 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
88 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
89 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
90 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
91 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
92 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len),
93 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
94 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
95 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
96 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
97 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len),
98 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
99 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
100 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len),
101 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len),
102 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
103 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
104 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
105 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
106 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
107 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
108 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
109 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
110 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
112 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
113 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
114 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
115 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
116 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
@ -5543,7 +5482,7 @@ fn pde_ffi_dispatcher_sync_impl(
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
match func_id {
18 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len),
112 => wire__crate__bridge__user_config__user_config_api_clone_impl(
111 => wire__crate__bridge__user_config__user_config_api_clone_impl(
ptr,
rust_vec_len,
data_len,
@ -5781,48 +5720,6 @@ impl flutter_rust_bridge::IntoIntoDart<crate::bridge::wrapper::app_database::Leg
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::bridge::callbacks::LegacySignalDecryptResult {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.plaintext.into_into_dart().into_dart(),
self.decryption_error_type.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::bridge::callbacks::LegacySignalDecryptResult
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::bridge::callbacks::LegacySignalDecryptResult>
for crate::bridge::callbacks::LegacySignalDecryptResult
{
fn into_into_dart(self) -> crate::bridge::callbacks::LegacySignalDecryptResult {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::bridge::callbacks::LegacySignalEncryptResult {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.ciphertext.into_into_dart().into_dart(),
self.message_type.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::bridge::callbacks::LegacySignalEncryptResult
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::bridge::callbacks::LegacySignalEncryptResult>
for crate::bridge::callbacks::LegacySignalEncryptResult
{
fn into_into_dart(self) -> crate::bridge::callbacks::LegacySignalEncryptResult {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart
for crate::bridge::wrapper::app_database::LegacyTableMigrationCount
{
@ -6305,13 +6202,6 @@ impl SseEncode for std::collections::HashMap<String, Vec<String>> {
}
}
impl SseEncode for std::collections::HashMap<i64, Vec<u8>> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<Vec<(i64, Vec<u8>)>>::sse_encode(self.into_iter().collect(), serializer);
}
}
impl SseEncode for StreamSink<String, flutter_rust_bridge::for_generated::SseCodec> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -6503,22 +6393,6 @@ impl SseEncode for crate::bridge::wrapper::app_database::LegacyMigrationReport {
}
}
impl SseEncode for crate::bridge::callbacks::LegacySignalDecryptResult {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<Option<Vec<u8>>>::sse_encode(self.plaintext, serializer);
<Option<i32>>::sse_encode(self.decryption_error_type, serializer);
}
}
impl SseEncode for crate::bridge::callbacks::LegacySignalEncryptResult {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<Vec<u8>>::sse_encode(self.ciphertext, serializer);
<i32>::sse_encode(self.message_type, serializer);
}
}
impl SseEncode for crate::bridge::wrapper::app_database::LegacyTableMigrationCount {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -6599,16 +6473,6 @@ impl SseEncode for Vec<u8> {
}
}
impl SseEncode for Vec<(i64, Vec<u8>)> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<i32>::sse_encode(self.len() as _, serializer);
for item in self {
<(i64, Vec<u8>)>::sse_encode(item, serializer);
}
}
}
impl SseEncode for Vec<(String, Vec<String>)> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -6679,6 +6543,16 @@ impl SseEncode for Option<crate::bridge::api::ApiConnectionState> {
}
}
impl SseEncode for Option<bool> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<bool>::sse_encode(self.is_some(), serializer);
if let Some(value) = self {
<bool>::sse_encode(value, serializer);
}
}
}
impl SseEncode for Option<f64> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -6689,16 +6563,6 @@ impl SseEncode for Option<f64> {
}
}
impl SseEncode for Option<i32> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<bool>::sse_encode(self.is_some(), serializer);
if let Some(value) = self {
<i32>::sse_encode(value, serializer);
}
}
}
impl SseEncode for Option<i64> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -6709,16 +6573,6 @@ impl SseEncode for Option<i64> {
}
}
impl SseEncode for Option<crate::bridge::callbacks::LegacySignalEncryptResult> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<bool>::sse_encode(self.is_some(), serializer);
if let Some(value) = self {
<crate::bridge::callbacks::LegacySignalEncryptResult>::sse_encode(value, serializer);
}
}
}
impl SseEncode for Option<crate::user_config::PasswordlessRecoveryConfig> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -6806,14 +6660,6 @@ impl SseEncode for crate::api::server::prekeys::PqcPreKeyInput {
}
}
impl SseEncode for (i64, Vec<u8>) {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<i64>::sse_encode(self.0, serializer);
<Vec<u8>>::sse_encode(self.1, serializer);
}
}
impl SseEncode for (Vec<u8>, i64) {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {

View file

@ -4,7 +4,7 @@
*/
use crate::error::Result;
use scrypt::{scrypt, Params};
use scrypt::{Params, scrypt};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};

View file

@ -3,8 +3,9 @@
*
*/
use std::collections::HashMap;
use crate::error::Result;
use libsignal_protocol::IdentityKeyPair;
use rand::SeedableRng;
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
@ -12,19 +13,24 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
pub(crate) struct SignalIdentityKey {
pub(crate) identity_key_pair_structure: Vec<u8>,
pub(crate) registration_id: i64,
pub(crate) pre_key_store: HashMap<i64, Vec<u8>>,
}
impl SignalIdentityKey {}
impl SignalIdentityKey {
pub(crate) fn generate() -> Result<Self> {
let mut csprng = rand::rngs::StdRng::from_os_rng();
let identity_key_pair = IdentityKeyPair::generate(&mut csprng);
let registration_id = rand::Rng::random::<u32>(&mut csprng) & 0x7fff_ffff;
Ok(Self {
identity_key_pair_structure: identity_key_pair.serialize().to_vec(),
registration_id: i64::from(registration_id),
})
}
}
impl Zeroize for SignalIdentityKey {
fn zeroize(&mut self) {
self.identity_key_pair_structure.zeroize();
self.registration_id.zeroize();
for value in self.pre_key_store.values_mut() {
value.zeroize();
}
self.pre_key_store.clear();
}
}

View file

@ -14,9 +14,11 @@ pub(crate) use crate::keys::identity_key::signal_identity_key::SignalIdentityKey
pub(crate) use crate::keys::main_key::{DatabaseKey, MainKey};
use crate::secure_storage::SecureStorage;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use zeroize::{Zeroize, ZeroizeOnDrop};
const KEY_MANAGER_ID: &str = "twonly_key_manager";
const KEY_MANAGER_V2_PREFIX: &[u8] = b"TWONLY_KEY_MANAGER_V2\0";
#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub(crate) struct KeyManager {
@ -44,14 +46,12 @@ impl KeyManager {
let bytes = hex::decode(hex_key)?;
let main_key: KeyManager = postcard::from_bytes(&bytes)?;
Ok(main_key)
Self::from_bytes(&bytes)
}
/// Stores the main key into the secure keychain/local storage.
pub fn store_to_keychain(&self, storage: &SecureStorage) -> Result<()> {
let serialized = postcard::to_allocvec(self)?;
let serialized = self.to_bytes()?;
let hex_key = hex::encode(serialized);
storage.write(KEY_MANAGER_ID, &hex_key)?;
@ -64,4 +64,130 @@ impl KeyManager {
storage.delete(KEY_MANAGER_ID)?;
Ok(())
}
pub(crate) fn to_bytes(&self) -> Result<Vec<u8>> {
let payload = postcard::to_allocvec(self)?;
let mut serialized = Vec::with_capacity(KEY_MANAGER_V2_PREFIX.len() + payload.len());
serialized.extend_from_slice(KEY_MANAGER_V2_PREFIX);
serialized.extend_from_slice(&payload);
Ok(serialized)
}
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self> {
if let Some(payload) = bytes.strip_prefix(KEY_MANAGER_V2_PREFIX) {
return Ok(postcard::from_bytes(payload)?);
}
// Key managers written before V2 used postcard's positional encoding and
// embedded a signed-prekey HashMap in the Signal identity. Read that
// shape once and discard the obsolete private-key copy during migration.
let legacy: LegacyKeyManager = postcard::from_bytes(bytes)?;
Ok(legacy.into_current())
}
}
#[derive(Deserialize)]
struct LegacyKeyManager {
user_id: Option<i64>,
main_key: MainKey,
signal_identity: Option<LegacySignalIdentityKey>,
backup_password: Option<BackupPasswordKeys>,
}
impl LegacyKeyManager {
fn into_current(self) -> KeyManager {
KeyManager {
user_id: self.user_id,
main_key: self.main_key,
signal_identity: self
.signal_identity
.map(LegacySignalIdentityKey::into_current),
backup_password: self.backup_password,
}
}
}
#[derive(Deserialize)]
struct LegacySignalIdentityKey {
identity_key_pair_structure: Vec<u8>,
registration_id: i64,
pre_key_store: HashMap<i64, Vec<u8>>,
}
impl LegacySignalIdentityKey {
fn into_current(mut self) -> SignalIdentityKey {
for value in self.pre_key_store.values_mut() {
value.zeroize();
}
self.pre_key_store.clear();
SignalIdentityKey {
identity_key_pair_structure: std::mem::take(&mut self.identity_key_pair_structure),
registration_id: self.registration_id,
}
}
}
impl Drop for LegacySignalIdentityKey {
fn drop(&mut self) {
self.identity_key_pair_structure.zeroize();
self.registration_id.zeroize();
for value in self.pre_key_store.values_mut() {
value.zeroize();
}
self.pre_key_store.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Serialize)]
struct OldKeyManager<'a> {
user_id: Option<i64>,
main_key: &'a MainKey,
signal_identity: Option<OldSignalIdentity>,
backup_password: Option<&'a BackupPasswordKeys>,
}
#[derive(Serialize)]
struct OldSignalIdentity {
identity_key_pair_structure: Vec<u8>,
registration_id: i64,
pre_key_store: HashMap<i64, Vec<u8>>,
}
#[test]
fn reads_legacy_key_manager_and_drops_pre_key_store() {
let key_manager = KeyManager::generate().unwrap();
let legacy = OldKeyManager {
user_id: Some(42),
main_key: &key_manager.main_key,
signal_identity: Some(OldSignalIdentity {
identity_key_pair_structure: vec![1, 2, 3],
registration_id: 7,
pre_key_store: HashMap::from([(1, vec![4, 5, 6])]),
}),
backup_password: None,
};
let bytes = postcard::to_allocvec(&legacy).unwrap();
let migrated = KeyManager::from_bytes(&bytes).unwrap();
assert_eq!(migrated.user_id, Some(42));
assert_eq!(
migrated.signal_identity,
Some(SignalIdentityKey {
identity_key_pair_structure: vec![1, 2, 3],
registration_id: 7,
})
);
}
#[test]
fn v2_roundtrip_is_prefixed() {
let key_manager = KeyManager::generate().unwrap();
let bytes = key_manager.to_bytes().unwrap();
assert!(bytes.starts_with(KEY_MANAGER_V2_PREFIX));
assert_eq!(KeyManager::from_bytes(&bytes).unwrap(), key_manager);
}
}

View file

@ -4,8 +4,8 @@
*/
use chacha20poly1305::{
aead::{Aead, Payload},
KeyInit, XChaCha20Poly1305, XNonce,
aead::{Aead, Payload},
};
use hkdf::Hkdf;
use libsignal_protocol::{IdentityKeyPair, KeyPair, PublicKey};
@ -319,7 +319,7 @@ fn derive_encryption_key(
#[cfg(test)]
mod tests {
use super::*;
use rand::{rngs::StdRng, SeedableRng};
use rand::{SeedableRng, rngs::StdRng};
fn test_message() -> proto::Message {
proto::Message {
@ -360,7 +360,6 @@ mod tests {
key_manager.signal_identity = Some(crate::keys::SignalIdentityKey {
identity_key_pair_structure: recipient.serialize().to_vec(),
registration_id: 1,
pre_key_store: Default::default(),
});
}
let database = context.rust_db.read().await.clone();

View file

@ -61,7 +61,11 @@ impl ContactService {
.await
}
pub(crate) async fn establish_signal_session(&self, user_id: i64) -> Result<()> {
pub(crate) async fn establish_signal_session(
&self,
user_id: i64,
expected_public_key: Option<Vec<u8>>,
) -> Result<()> {
let user = match Server::get_user_by_id(&self.ctx, user_id).await? {
ServerResult::Ok(user) => user,
ServerResult::ErrorCode(code) => {
@ -70,7 +74,31 @@ impl ContactService {
)));
}
};
self.process_user_prekey_bundle(&user).await
if let Some(expected_key) = expected_public_key {
let server_key = user
.public_identity_key
.as_ref()
.ok_or(TwonlyError::ApiResponseMissingField("public_identity_key"))?;
if server_key != &expected_key {
return Err(TwonlyError::Generic(format!(
"Public identity key mismatch for user {user_id}"
)));
}
}
self.process_user_prekey_bundle(&user).await?;
let database = self.ctx.app_db.read().await.clone();
sqlx::query!(
"UPDATE contacts SET signal_version = 'v2' WHERE user_id = ?",
user_id
)
.execute(&database.pool)
.await?;
database.notify_committed(["contacts"]);
Ok(())
}
async fn process_user_prekey_bundle(

View file

@ -469,6 +469,14 @@ impl GroupService {
pub async fn leave_group(&self, group_id: String) -> Result<bool> {
let (db, group) = self.load_group(&group_id).await?;
let user_id = self.ctx.user_id().await?;
let (_, group_state) = GroupApi::load_state(&group).await?;
if group_state.admin_ids.contains(&user_id) {
let identity = group.identity()?;
let public_key = identity.identity_key().serialize().to_vec();
return self.remove_member(group_id, public_key, user_id).await;
}
let identity = group.identity()?;
let public_key = identity.identity_key().serialize().to_vec();
let append = EncryptedAppendedGroupState {

View file

@ -8,10 +8,10 @@ use crate::error::{Result, TwonlyError};
use crate::user_config::UserConfig;
use chrono::{Duration, Utc};
use libsignal_protocol::{
message_encrypt, process_prekey_bundle, CiphertextMessageType, DeviceId, GenericSignedPreKey,
IdentityKey, IdentityKeyPair, IdentityKeyStore, KyberPreKeyId, KyberPreKeyStore, PreKeyBundle,
PreKeyId, PreKeySignalMessage, PreKeyStore, ProtocolAddress, PublicKey, SignalMessage,
SignedPreKeyId, SignedPreKeyStore, Timestamp,
CiphertextMessageType, DeviceId, GenericSignedPreKey, IdentityKey, IdentityKeyPair,
IdentityKeyStore, KyberPreKeyId, KyberPreKeyStore, PreKeyBundle, PreKeyId, PreKeySignalMessage,
PreKeyStore, ProtocolAddress, PublicKey, SignalMessage, SignedPreKeyId, SignedPreKeyStore,
Timestamp, message_encrypt, process_prekey_bundle,
};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
@ -82,6 +82,8 @@ impl RustSignalEngine {
if refresh_pqc {
Server::upload_pqc_pre_keys(
ctx,
bundle.identity_key,
i64::from(bundle.registration_id),
i64::from(bundle.signed_pre_key_id),
bundle.signed_pre_key_public,
bundle.signed_pre_key_signature,
@ -141,6 +143,28 @@ impl RustSignalEngine {
})
}
pub async fn get_identity_key(&self) -> Result<Vec<u8>> {
let store = self.store.lock().await;
let key_pair = store
.identity_store
.get_identity_key_pair()
.assert_send()
.await
.map_err(|e| TwonlyError::Signal(e.to_string()))?;
Ok(key_pair.identity_key().serialize().to_vec())
}
pub async fn get_contact_identity_key(&self, name: &str) -> Result<Option<Vec<u8>>> {
let store = self.store.lock().await;
let identity = sqlx::query_scalar!(
r#"SELECT identity_key FROM signal_identities WHERE name = ?"#,
name,
)
.fetch_optional(&store.pool)
.await?;
Ok(identity)
}
#[cfg(test)]
fn generate_identity_key_pair() -> Result<Vec<u8>> {
let mut csprng = rand::rngs::StdRng::from_os_rng();
@ -159,33 +183,21 @@ impl RustSignalEngine {
)
.fetch_one(&store.pool)
.await?;
if id > 16_777_215 {
1
} else {
id + 1
}
if id > 16_777_215 { 1 } else { id + 1 }
};
let signed_pre_key_id: u32 = {
let id = sqlx::query_scalar!(
r#"SELECT COALESCE(MAX(signed_pre_key_id), 0) AS "id!: u32" FROM signal_signed_pre_keys"#,
).fetch_one(&store.pool).await?;
if id > 16_777_215 {
1
} else {
id + 1
}
if id > 16_777_215 { 1 } else { id + 1 }
};
let kyber_pre_key_id: u32 = {
let id = sqlx::query_scalar!(
r#"SELECT COALESCE(MAX(kyber_pre_key_id), 0) AS "id!: u32" FROM signal_kyber_pre_keys"#,
).fetch_one(&store.pool).await?;
if id > 16_777_215 {
1
} else {
id + 1
}
if id > 16_777_215 { 1 } else { id + 1 }
};
let pre_key_pair = libsignal_protocol::KeyPair::generate(&mut csprng);
@ -313,11 +325,7 @@ impl RustSignalEngine {
r#"SELECT COALESCE(MAX(kyber_pre_key_id), 0) AS "id!: u32" FROM signal_kyber_pre_keys"#,
).fetch_one(&store.pool).await?;
if id > 16_777_215 {
1
} else {
id
}
if id > 16_777_215 { 1 } else { id }
};
let mut pre_key_id: u32 = {
@ -327,11 +335,7 @@ impl RustSignalEngine {
.fetch_one(&store.pool)
.await?;
if id > 16_777_215 {
1
} else {
id
}
if id > 16_777_215 { 1 } else { id }
};
for _ in 0..30 {

View file

@ -116,3 +116,130 @@ async fn test_add_hidden_contact() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test]
async fn test_admin_and_non_admin_leave_group() -> anyhow::Result<()> {
let tester_a = create_authenticated_tester().await?;
let tester_b = create_authenticated_tester().await?;
let tester_c = create_authenticated_tester().await?;
// Setup contacts
ContactService::new(&tester_a.context)
.request_by_username(tester_b.username.clone(), true)
.await?;
tester_b
.wait_for_contact_state(tester_a.user_id, false, true)
.await?;
ContactService::new(&tester_b.context)
.accept_request(tester_a.user_id, true)
.await?;
tester_a
.wait_for_contact_state(tester_b.user_id, true, false)
.await?;
ContactService::new(&tester_a.context)
.request_by_username(tester_c.username.clone(), true)
.await?;
tester_c
.wait_for_contact_state(tester_a.user_id, false, true)
.await?;
ContactService::new(&tester_c.context)
.accept_request(tester_a.user_id, true)
.await?;
tester_a
.wait_for_contact_state(tester_c.user_id, true, false)
.await?;
// Tester A creates group with Tester B and Tester C
let group_name = "Leave Test Group";
GroupService::new(&tester_a.context)
.create_group(group_name.into(), vec![tester_b.user_id, tester_c.user_id])
.await?;
let group_id = {
let db_a = tester_a.context.app_db.read().await.clone();
sqlx::query_scalar!(
"SELECT group_id FROM groups WHERE is_direct_chat = 0 ORDER BY rowid DESC LIMIT 1"
)
.fetch_one(&db_a.pool)
.await?
};
tester_b.wait_for_group_exists(&group_id, group_name).await?;
tester_c.wait_for_group_exists(&group_id, group_name).await?;
// Fetch missing public key for tester_b
{
let db_a = tester_a.context.app_db.read().await.clone();
sqlx::query!(
"UPDATE group_members SET last_message = CAST(strftime('%s','now') AS INTEGER) WHERE group_id = ? AND contact_id = ?",
group_id,
tester_b.user_id
)
.execute(&db_a.pool)
.await?;
GroupService::new(&tester_a.context).fetch_missing_group_public_keys().await?;
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
// Promote tester_b to admin so tester_a can leave later with an admin remaining
GroupService::new(&tester_a.context)
.manage_admin(group_id.clone(), tester_b.user_id, false)
.await?;
GroupService::new(&tester_b.context)
.fetch_group_state(group_id.clone())
.await?;
{
let db_b = tester_b.context.app_db.read().await.clone();
let is_admin = sqlx::query_scalar!(
"SELECT is_group_admin FROM groups WHERE group_id = ?",
group_id
)
.fetch_one(&db_b.pool)
.await?;
assert_eq!(is_admin, 1, "tester_b should be admin after promotion");
}
// 1. Non-admin (tester_c) leaves the group
GroupService::new(&tester_c.context)
.leave_group(group_id.clone())
.await?;
tester_c.wait_for_group_left(&group_id).await?;
// Verify tester_a and tester_b see tester_c removed
GroupService::new(&tester_a.context)
.fetch_group_state(group_id.clone())
.await?;
tester_a
.wait_for_group_member_removed(&group_id, tester_c.user_id)
.await?;
// 2. Admin (tester_a) leaves the group using leave_group
GroupService::new(&tester_a.context)
.leave_group(group_id.clone())
.await?;
tester_a.wait_for_group_left(&group_id).await?;
// Verify tester_b (remaining admin) sees tester_a removed and tester_b remains admin
GroupService::new(&tester_b.context)
.fetch_group_state(group_id.clone())
.await?;
tester_b
.wait_for_group_member_removed(&group_id, tester_a.user_id)
.await?;
{
let db_b = tester_b.context.app_db.read().await.clone();
let is_admin = sqlx::query_scalar!(
"SELECT is_group_admin FROM groups WHERE group_id = ?",
group_id
)
.fetch_one(&db_b.pool)
.await?;
assert_eq!(is_admin, 1, "tester_b should still be admin");
}
Ok(())
}

View file

@ -1,4 +1,4 @@
use libsignal_protocol::{GenericSignedPreKey, IdentityKeyPair, KeyPair, SignedPreKeyRecord};
use libsignal_protocol::IdentityKeyPair;
use rand::SeedableRng;
use rust_lib_twonly::api::{ApiRuntime, Server};
use rust_lib_twonly::bridge::api::{ApiConnectionState, ServerResult};
@ -6,7 +6,7 @@ use rust_lib_twonly::context::Context;
use sha2::{Digest, Sha256};
use std::sync::Arc;
use tempfile::TempDir;
use tokio::time::{sleep, Duration};
use tokio::time::{Duration, sleep};
pub(crate) struct Tester {
pub context: Arc<Context>,
@ -378,37 +378,10 @@ impl Tester {
let mut csprng = rand::rngs::StdRng::from_os_rng();
let identity_pair = IdentityKeyPair::generate(&mut csprng);
let registration_id: u32 = rand::Rng::random::<u32>(&mut csprng) & 0x7FFFFFFF;
let timestamp = libsignal_protocol::Timestamp::from_epoch_millis(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64,
);
let signed_pre_key_pair = KeyPair::generate(&mut csprng);
let signature = identity_pair
.private_key()
.calculate_signature_for_multipart_message(
&[&signed_pre_key_pair.public_key.serialize()],
&mut csprng,
)
.map_err(|e| anyhow::anyhow!("Signal error: {}", e))?;
let signed_prekey =
SignedPreKeyRecord::new(1.into(), timestamp, &signed_pre_key_pair, &signature);
let mut pre_key_store = std::collections::HashMap::new();
pre_key_store.insert(
1,
signed_prekey
.serialize()
.map_err(|e| anyhow::anyhow!("Signal error: {}", e))?,
);
context
.inject_test_signal_identity(
identity_pair.serialize().to_vec(),
registration_id as i64,
pre_key_store,
)
.await?;
}
@ -592,7 +565,9 @@ impl Tester {
}
sleep(Duration::from_millis(100)).await;
}
Err(anyhow::anyhow!("message {message_id} was not marked as opened"))
Err(anyhow::anyhow!(
"message {message_id} was not marked as opened"
))
}
pub async fn wait_for_group_chat_deletion_time(

View file

@ -57,7 +57,7 @@ void main() {
Future<int> getAndCreateUserId() async {
return mutex.protect<int>(() async {
final userId = usedUserIds += 1;
await twonlyDB.contactsDao.insertContact(
await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion(userId: Value(userId), username: Value('$userId')),
);
return userId;

View file

@ -81,9 +81,7 @@ void main() {
url: 'https://x.com/netzpolitik_org/status/1162346968124968960',
siteName: 'X (formerly Twitter)',
desc:
'Weil unsere Datenanalyse zum Twitter-Account von Maaßen rechte Millieus und ihre Verbindungen offengelegt hat, haben wir einen rechten Shitstorm an der Backe. Klar ist: Wir lassen uns nicht einschüchtern und freuen uns auf Unterstützung! \n'
'\n'
'https://t.co/MQZ7ulHakF',
'Weil unsere Datenanalyse zum Twitter-Account von Maaßen rechte Millieus und ihre Verbindungen offengelegt hat, haben wir einen rechten Shitstorm an der Backe. Klar ist: Wir lassen uns nicht einsch…',
image: 'https://pbs.twimg.com/media/ECF8Z5KWwAIBZ6o.jpg:large',
vendor: Vendor.twitterPosting,
),

View file

@ -10,10 +10,7 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/api/api.service.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/api/rust_api_result.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/pow.dart';
@ -52,19 +49,15 @@ class TestClient {
api = ApiService();
await run(() async {
await createIfNotExistsSignalIdentity();
Log.info('Connecting to API...');
final connected = await api.connect();
Log.info('Connected: $connected');
if (!connected) throw Exception('Failed to connect to API');
Log.info('Requesting POW...');
final pow = await rustApiProtobuf(
RustApi.getProofOfWork(),
decodeProofOfWork,
);
if (pow == null) throw Exception('Failed to get POW');
final dynamic pow;
try {
final raw = await RustApi.getProofOfWork();
pow = decodeProofOfWork(raw);
} catch (e, st) {
print('POW EXCEPTION: $e\n$st');
rethrow;
}
Log.info('POW result: $pow');
final prefix = pow.prefix;
@ -87,15 +80,12 @@ class TestClient {
appVersion: 100,
);
await UserService.save(userData);
await api.authenticate();
await signalGetPreKeys();
});
}
Future<void> initContact(TestClient other) async {
await run(() async {
await env.db.contactsDao.insertContact(
await env.db.contactsDao.insertOnConflictUpdate(
ContactsCompanion.insert(
userId: Value(other.realUserId),
username: other.username,
@ -107,12 +97,7 @@ class TestClient {
GroupsCompanion(groupName: Value(other.username)),
);
final userData = await rustApiProtobuf(
RustApi.getUserById(userId: other.realUserId),
decodeUserData,
);
final sessionStarted = await processSignalUserData(userData!);
if (!sessionStarted) throw Exception('Failed to start session');
await RustApi.establishSignalSession(contactId: other.realUserId);
});
}
@ -129,14 +114,14 @@ class TestClient {
type: Value(MessageType.text.name),
),
);
await sendCipherText(
target.realUserId,
pb.EncryptedContent(
await RustApi.sendEncryptedContent(
contactId: target.realUserId,
content: pb.EncryptedContent(
groupId: defaultGroup!.groupId,
textMessage: pb.EncryptedContent_TextMessage()
..senderMessageId = m!.messageId
..text = text,
),
).writeToBuffer(),
messageId: m.messageId,
);
return m;
@ -148,7 +133,10 @@ class TestClient {
pb.EncryptedContent content,
) async {
await run(() async {
await sendCipherText(target.realUserId, content);
await RustApi.sendEncryptedContent(
contactId: target.realUserId,
content: content.writeToBuffer(),
);
});
}

View file

@ -3,7 +3,6 @@ import 'dart:io';
import 'package:drift/drift.dart' hide isNotNull, isNull;
import 'package:drift/native.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/api.service.dart';
import 'package:twonly/src/services/user.service.dart';
@ -67,15 +66,11 @@ class UserEnvironment {
required this.username,
required this.db,
required this.userService,
required this.identityKeyPair,
required this.registrationId,
});
final int userId;
final String username;
final TwonlyDB db;
final UserService userService;
final IdentityKeyPair identityKeyPair;
final int registrationId;
static Future<UserEnvironment> create(
int userId,
@ -101,16 +96,11 @@ class UserEnvironment {
us.isUserCreated = true;
final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(true);
return UserEnvironment(
userId: userId,
username: username,
db: db,
userService: us,
identityKeyPair: identityKeyPair,
registrationId: registrationId,
);
}
}

View file

@ -32,7 +32,7 @@ void main() {
];
for (var i = 0; i < users.length; i++) {
await database.contactsDao.insertContact(
await database.contactsDao.insertOnConflictUpdate(
ContactsCompanion(userId: Value(i), username: Value(users[i])),
);
}

View file

@ -123,8 +123,8 @@ void main() {
});
tearDown(() async {
await clientA.run(() async => clientA.api.close(null));
await clientB.run(() async => clientB.api.close(null));
await clientA.run(() async => clientA.api.dispose());
await clientB.run(() async => clientB.api.dispose());
await clientA.env.db.close();
await clientB.env.db.close();
});

View file

@ -9,6 +9,7 @@ import 'package:twonly/core/bridge.dart' as bridge;
import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/callbacks/callbacks.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/api.service.dart';
@ -49,6 +50,7 @@ void main() {
} else {
await RustLib.init();
}
await initFlutterCallbacksForRust();
tempDir = Directory.systemTemp.createTempSync(
'twonly_cloud_backup_test_',
);

View file

@ -2,13 +2,14 @@ import 'dart:io';
import 'package:drift/drift.dart' hide isNotNull, isNull;
import 'package:drift/native.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/key_verification.dao.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/api.service.dart';
import 'package:twonly/src/services/user.service.dart';
import '../mocks/user_config.dart';
@ -20,6 +21,18 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() async {
final dylibPath =
'${Directory.current.path}/rust/target/debug/librust_lib_twonly.dylib';
if (File(dylibPath).existsSync()) {
await RustLib.init(
externalLibrary: ExternalLibrary.open(dylibPath),
);
} else {
await RustLib.init();
}
});
setUp(() async {
await locator.reset();
locator
@ -31,8 +44,7 @@ void main() {
),
),
)
..registerSingleton<UserService>(UserService())
..registerSingleton<ApiService>(ApiService());
..registerSingleton<UserService>(UserService());
// isUserDiscoveryEnabled defaults to false, so no Rust bridge calls happen
// in addKeyVerification / deleteKeyVerification.
@ -55,7 +67,7 @@ void main() {
// Helpers
Future<void> insertContact(int userId, {String? username}) async {
await twonlyDB.contactsDao.insertContact(
await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion.insert(
userId: Value(userId),
username: username ?? 'user$userId',

View file

@ -1,24 +1,18 @@
import 'dart:convert' show utf8;
import 'dart:io';
import 'package:drift/drift.dart' hide isNotNull, isNull;
import 'package:drift/native.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/core/bridge.dart' as bridge;
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/callbacks/callbacks.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as api_pb;
import 'package:twonly/src/services/api/api.service.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
@ -89,8 +83,6 @@ void main() {
);
userService.isUserCreated = true;
await UserService.save(userService.currentUser);
await createIfNotExistsSignalIdentity();
});
tearDown(() async {
@ -108,36 +100,20 @@ void main() {
});
Future<void> setupSignalSession(int contactId) async {
final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(true);
final signedPreKey = generateSignedPreKey(identityKeyPair, 1);
final preKey = generatePreKeys(1, 1).first;
final responseUserData = api_pb.Response_UserData()
..userId = Int64(contactId)
..username = utf8.encode('user_$contactId')
..registrationId = Int64(registrationId)
..publicIdentityKey = identityKeyPair.getPublicKey().serialize()
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
..signedPrekeyId = Int64(signedPreKey.id)
..signedPrekeySignature = signedPreKey.signature;
responseUserData.prekeys.add(
api_pb.Response_PreKey()
..id = Int64(preKey.id)
..prekey = preKey.getKeyPair().publicKey.serialize(),
final bundle = await RustSignal.generateBundle();
await RustSignal.processPrekeyBundle(
name: contactId.toString(),
deviceId: 1,
bundle: bundle,
);
final success = await processSignalUserData(responseUserData);
expect(success, isTrue);
}
group('PasswordlessRecoveryService - enablePasswordlessRecovery', () {
test('works with SecondFactorType.none', () async {
await twonlyDB.contactsDao.insertContact(
await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion.insert(userId: const Value(2), username: 'friend_2'),
);
await twonlyDB.contactsDao.insertContact(
await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion.insert(userId: const Value(3), username: 'friend_3'),
);
@ -163,7 +139,7 @@ void main() {
});
test('works with SecondFactorType.email', () async {
await twonlyDB.contactsDao.insertContact(
await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion.insert(userId: const Value(2), username: 'friend_2'),
);
@ -183,7 +159,7 @@ void main() {
});
test('works with SecondFactorType.pin', () async {
await twonlyDB.contactsDao.insertContact(
await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion.insert(userId: const Value(2), username: 'friend_2'),
);
@ -203,20 +179,17 @@ void main() {
});
test('sends delete messages to old trusted friends', () async {
await twonlyDB.contactsDao.insertContact(
await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion.insert(
userId: const Value(2),
username: 'friend_2',
recoveryIsTrustedFriend: const Value(true),
),
);
await twonlyDB.contactsDao.insertContact(
await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion.insert(userId: const Value(3), username: 'friend_3'),
);
await setupSignalSession(2);
await setupSignalSession(3);
final success =
await PasswordlessRecoveryService.enablePasswordlessRecovery(
trustedFriendIds: [3],

View file

@ -1,25 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
void main() {
group('testing api', () {
test('testing api connection', () async {
const offset = 100;
const count = 400;
var prekeys = generatePreKeys(offset, count);
expect(count, prekeys.length);
for (var i = 0; i < prekeys.length; i++) {
expect(prekeys[i].id, offset + i);
}
prekeys += generatePreKeys(offset + count, count);
expect(count * 2, prekeys.length);
for (var i = 0; i < (count * 2); i++) {
expect(prekeys[i].id, offset + i);
}
});
});
}

View file

@ -1,220 +0,0 @@
// ignore_for_file: avoid_print
import 'dart:async';
import 'dart:io';
import 'package:drift/drift.dart' hide isNotNull, isNull;
import 'package:flutter/services.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:twonly/core/bridge.dart' as bridge;
import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/callbacks/callbacks.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/api.service.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:workmanager/workmanager.dart';
import '../mocks/platform_channels.dart';
import '../mocks/test_client.dart';
import '../mocks/workmanager.dart';
void main() {
if (!Platform.isMacOS) {
return;
}
TestWidgetsFlutterBinding.ensureInitialized();
late Directory tempDir;
setUpAll(() async {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
WorkmanagerPlatform.instance = MockWorkmanagerPlatform();
tempDir = Directory.systemTemp.createTempSync('twonly_pqc_c2c_test_');
AppEnvironment.initTesting(
customCacheDir: tempDir.path,
customSupportDir: tempDir.path,
);
final dylibPath =
'${Directory.current.path}/rust/target/debug/librust_lib_twonly.dylib';
if (File(dylibPath).existsSync()) {
await RustLib.init(externalLibrary: ExternalLibrary.open(dylibPath));
} else {
await RustLib.init();
}
await initFlutterCallbacksForRust();
await bridge.initializeTwonlyFlutter(
config: bridge.InitConfig(
databaseDir: tempDir.path,
dataDir: tempDir.path,
),
);
if (locator.isRegistered<TwonlyDB>()) await locator.unregister<TwonlyDB>();
if (locator.isRegistered<UserService>()) {
await locator.unregister<UserService>();
}
if (locator.isRegistered<ApiService>()) {
await locator.unregister<ApiService>();
}
locator
..registerFactory<TwonlyDB>(() {
final db = Zone.current[#twonlyDB] as TwonlyDB?;
if (db != null) return db;
throw StateError('No TwonlyDB in active Zone.');
})
..registerFactory<UserService>(() {
final us = Zone.current[#userService] as UserService?;
if (us != null) return us;
throw StateError('No UserService in active Zone.');
})
..registerFactory<ApiService>(() {
final api = Zone.current[#apiService] as ApiService?;
if (api != null) return api;
throw StateError('No ApiService in active Zone.');
});
});
tearDownAll(() async {
if (tempDir.existsSync()) {
try {
tempDir.deleteSync(recursive: true);
} catch (_) {}
}
});
group('PQC C2C Migration Protocol Tests', () {
late TestClient clientA;
late TestClient clientB;
setUp(() async {
setupPlatformChannelMocks();
HttpOverrides.global = RealHttpOverrides();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('dev.fluttercommunity.plus/package_info'),
(call) async {
return {
'appName': 'twonly',
'packageName': 'eu.twonly.app',
'version': '1.0.0',
'buildNumber': '100',
};
},
);
await Workmanager().initialize(() {});
clientA = TestClient(3001);
clientB = TestClient(4002);
// Initialize BOTH clients with PQC generation disabled.
await clientA.init(disablePqc: true);
await clientB.init(disablePqc: true);
await clientA.initContact(clientB);
await clientB.initContact(clientA);
});
tearDown(() async {
await clientA.run(() async => clientA.api.close(null));
await clientB.run(() async => clientB.api.close(null));
await clientA.env.db.close();
await clientB.env.db.close();
});
test(
'C2C: V1 to V2 PQC Migration and 100 Messages Exchange',
() async {
print('=== Starting V1 Message Exchange ===');
// Exchange 100 messages in V1
for (var i = 0; i < 100; i++) {
final textAtoB = 'Hello Bob V1 - Message $i';
await clientA.sendText(clientB, textAtoB);
final receivedByB = await clientB.expectMessage(
(m) => m.content == textAtoB && m.senderId == clientA.realUserId,
);
expect(receivedByB.content, textAtoB);
expect(receivedByB.type, MessageType.text.name);
final textBtoA = 'Hello Alice V1 - Reply $i';
await clientB.sendText(clientA, textBtoA);
final receivedByA = await clientA.expectMessage(
(m) => m.content == textBtoA && m.senderId == clientB.realUserId,
);
expect(receivedByA.content, textBtoA);
expect(receivedByA.type, MessageType.text.name);
}
print('=== Migrating to V2 PQC ===');
// Step 2: Migrate to V2
// For Client A
await clientA.run(() async {
await UserService.update((user) {
user.signalLastPqcPreKeysUploaded = null;
});
await createIfNotExistsSignalIdentity(); // This will upload PQC keys
});
// For Client B
await clientB.run(() async {
await UserService.update((user) {
user.signalLastPqcPreKeysUploaded = null;
});
await createIfNotExistsSignalIdentity(); // This will upload PQC keys
});
// Both clients now fetch the updated contacts from the dev server
// This will invoke processSignalUserData and migrate the signal version to V2
await clientA.run(() async {
final userData = await rustApiProtobuf(
RustApi.getUserById(userId: clientB.realUserId),
decodeUserData,
);
if (userData != null) await processSignalUserData(userData);
});
await clientB.run(() async {
final userData = await rustApiProtobuf(
RustApi.getUserById(userId: clientA.realUserId),
decodeUserData,
);
if (userData != null) await processSignalUserData(userData);
});
print('=== Starting V2 PQC Message Exchange ===');
// Step 3: Exchange 100 messages in V2
for (var i = 0; i < 100; i++) {
final textAtoB = 'Hello Bob V2 PQC - Message $i';
await clientA.sendText(clientB, textAtoB);
final receivedByB = await clientB.expectMessage(
(m) => m.content == textAtoB && m.senderId == clientA.realUserId,
);
expect(receivedByB.content, textAtoB);
expect(receivedByB.type, MessageType.text.name);
final textBtoA = 'Hello Alice V2 PQC - Reply $i';
await clientB.sendText(clientA, textBtoA);
final receivedByA = await clientA.expectMessage(
(m) => m.content == textBtoA && m.senderId == clientB.realUserId,
);
expect(receivedByA.content, textBtoA);
expect(receivedByA.type, MessageType.text.name);
}
print('=== PQC Migration and Exchange Completed Successfully ===');
},
timeout: const Timeout(Duration(minutes: 5)),
);
});
}

View file

@ -1,182 +0,0 @@
import 'dart:convert' show utf8;
import 'dart:io';
import 'package:drift/drift.dart' hide isNotNull, isNull;
import 'package:drift/native.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:twonly/core/bridge.dart' as bridge;
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/callbacks/callbacks.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as api_pb;
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as msg_pb;
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
void main() {
if (!Platform.isMacOS) {
return;
}
TestWidgetsFlutterBinding.ensureInitialized();
late Directory tempDir;
late TwonlyDB db;
setUpAll(() async {
Log.init();
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
final dylibPath =
'${Directory.current.path}/rust/target/debug/librust_lib_twonly.dylib';
if (File(dylibPath).existsSync()) {
await RustLib.init(externalLibrary: ExternalLibrary.open(dylibPath));
} else {
await RustLib.init();
}
await initFlutterCallbacksForRust();
tempDir = Directory.systemTemp.createTempSync('twonly_pqc_migration_test_');
AppEnvironment.initTesting(
customCacheDir: tempDir.path,
customSupportDir: tempDir.path,
);
await bridge.initializeTwonlyFlutter(
config: bridge.InitConfig(
databaseDir: tempDir.path,
dataDir: tempDir.path,
),
);
db = TwonlyDB(NativeDatabase.memory());
locator.registerFactory<TwonlyDB>(() => db);
});
setUp(() async {
await createIfNotExistsSignalIdentity();
await RustKeyManager.setUserId(userId: 1);
});
tearDownAll(() async {
await db.close();
if (tempDir.existsSync()) {
try {
tempDir.deleteSync(recursive: true);
} catch (_) {}
}
});
test('PQC Migration Test: V1 to V2', () async {
const contactId = 999;
// 1. Setup contact as V1
await db.contactsDao.insertContact(
ContactsCompanion.insert(
userId: const Value(contactId),
username: 'test_pqc_user',
accepted: const Value(true),
signalVersion: const Value(SignalVersion.v1),
),
);
var contact = await db.contactsDao.getContactById(contactId);
expect(contact, isNotNull);
expect(contact!.signalVersion, SignalVersion.v1);
// 2. Generate a valid PQC bundle via RustSignal
final generatedBundle = await RustSignal.generateBundle();
final userData = api_pb.Response_UserData(
userId: Int64(contactId),
username: utf8.encode('test_pqc_user'),
registrationId: Int64(generatedBundle.registrationId),
publicIdentityKey: generatedBundle.identityKey,
pqcBundle: api_pb.Response_PqcBundle(
prekey: api_pb.Response_PqcPreKey(
eccPreKeyId: generatedBundle.preKeyId != null
? Int64(generatedBundle.preKeyId!)
: null,
eccPreKey: generatedBundle.preKeyPublic,
kyberPreKeyId: Int64(generatedBundle.kyberPreKeyId),
kyberPreKey: generatedBundle.kyberPreKeyPublic,
kyberPreKeySignature: generatedBundle.kyberPreKeySignature,
),
eccSignedPrekeyId: Int64(generatedBundle.signedPreKeyId),
eccSignedPrekey: generatedBundle.signedPreKeyPublic,
eccSignedPrekeySignature: generatedBundle.signedPreKeySignature,
),
);
// 3. Process the UserData containing the PQC bundle
final success = await processSignalUserData(userData);
expect(success, isTrue, reason: 'Failed to process PQC user data');
// 4. Verify contact was upgraded to V2
contact = await db.contactsDao.getContactById(contactId);
expect(contact, isNotNull);
expect(contact!.signalVersion, SignalVersion.v2);
});
test('PQC Encryption and Decryption V2', () async {
const contactId = 888;
// Setup contact as V2
await db.contactsDao.insertContact(
ContactsCompanion.insert(
userId: const Value(contactId),
username: 'test_pqc_user_2',
accepted: const Value(true),
signalVersion: const Value(SignalVersion.v2),
),
);
// Generate bundle and process it for the contact to establish a session
final generatedBundle = await RustSignal.generateBundle();
final userData = api_pb.Response_UserData(
userId: Int64(contactId),
username: utf8.encode('test_pqc_user_2'),
registrationId: Int64(generatedBundle.registrationId),
publicIdentityKey: generatedBundle.identityKey,
pqcBundle: api_pb.Response_PqcBundle(
prekey: api_pb.Response_PqcPreKey(
eccPreKeyId: generatedBundle.preKeyId != null
? Int64(generatedBundle.preKeyId!)
: null,
eccPreKey: generatedBundle.preKeyPublic,
kyberPreKeyId: Int64(generatedBundle.kyberPreKeyId),
kyberPreKey: generatedBundle.kyberPreKeyPublic,
kyberPreKeySignature: generatedBundle.kyberPreKeySignature,
),
eccSignedPrekeyId: Int64(generatedBundle.signedPreKeyId),
eccSignedPrekey: generatedBundle.signedPreKeyPublic,
eccSignedPrekeySignature: generatedBundle.signedPreKeySignature,
),
);
final success = await processSignalUserData(userData);
expect(success, isTrue);
// Encrypt a message
final plaintext = msg_pb.EncryptedContent(
textMessage: msg_pb.EncryptedContent_TextMessage(
text: 'Hello PQC World!',
),
).writeToBuffer();
final encryptedResult = await signalEncryptMessageV2(contactId, plaintext);
expect(encryptedResult, isNotNull);
expect(encryptedResult!.type, msg_pb.Message_Type.CIPHERTEXT_V2);
expect(encryptedResult.ciphertext, isNotEmpty);
});
}