implement new backup mechanism

This commit is contained in:
otsmr 2026-05-12 21:24:49 +02:00
parent f735070a7c
commit 4dbc369003
102 changed files with 4668 additions and 3281 deletions

View file

@ -1,5 +1,11 @@
# Changelog
## 0.2.11
- Improved: Redesigned snackbar notifications
- Improved: New backup mechanism to allow larger backup files
- Improved: Move keys into a centralized Rust-owned structure stored in secure storage
## 0.2.10
- Fix: Issue with push notifications on Android

View file

@ -77,7 +77,6 @@ class _AppState extends State<App> with WidgetsBindingObserver {
if (widget.storageError) {
return MaterialApp(
scaffoldMessengerKey: AppGlobalKeys.scaffoldMessengerKey,
localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales,
@ -91,7 +90,6 @@ class _AppState extends State<App> with WidgetsBindingObserver {
return MaterialApp.router(
routerConfig: routerProvider,
scaffoldMessengerKey: AppGlobalKeys.scaffoldMessengerKey,
localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales,

View file

@ -0,0 +1,29 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import '../lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class BackupPasswordKeys {
final U8Array32 backupId;
final U8Array32 encryptionKey;
const BackupPasswordKeys({
required this.backupId,
required this.encryptionKey,
});
@override
int get hashCode => backupId.hashCode ^ encryptionKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BackupPasswordKeys &&
runtimeType == other.runtimeType &&
backupId == other.backupId &&
encryptionKey == other.encryptionKey;
}

View file

@ -0,0 +1,87 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../../frb_generated.dart';
import '../../keys/backup_password_keys.dart';
import '../../lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class RustBackupArchive {
const RustBackupArchive();
static Future<(String, String)> createBackupArchive() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupArchiveCreateBackupArchive();
static Future<String?> getBackupDownloadToken() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupArchiveGetBackupDownloadToken();
static Future<void> restoreBackupArchive({required String filePath}) =>
RustLib.instance.api
.crateBridgeWrapperBackupRustBackupArchiveRestoreBackupArchive(
filePath: filePath,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustBackupArchive && runtimeType == other.runtimeType;
}
class RustBackupIdentity {
const RustBackupIdentity();
static Future<String?> getBackupId() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityGetBackupId();
static Future<BackupPasswordKeys> getBackupPasswordKeys({
required PlatformInt64 userId,
required String password,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityGetBackupPasswordKeys(
userId: userId,
password: password,
);
static Future<Uint8List> getIdentityBackupBytes() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityGetIdentityBackupBytes();
static Future<void> importBackupPasswordKeys({
required List<int> backupId,
required List<int> encryptionKey,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityImportBackupPasswordKeys(
backupId: backupId,
encryptionKey: encryptionKey,
);
static Future<void> restoreIdentityBackup({
required BackupPasswordKeys keys,
required List<int> encryptedBytes,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityRestoreIdentityBackup(
keys: keys,
encryptedBytes: encryptedBytes,
);
static Future<void> setBackupPasswordKeys({
required PlatformInt64 userId,
required String password,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentitySetBackupPasswordKeys(
userId: userId,
password: password,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustBackupIdentity && runtimeType == other.runtimeType;
}

View file

@ -6,11 +6,55 @@
import '../../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class FlutterKeyManager {
const FlutterKeyManager();
class RustKeyManager {
const RustKeyManager();
static Future<Uint8List> getLoginToken() => RustLib.instance.api
.crateBridgeWrapperKeyManagerFlutterKeyManagerGetLoginToken();
.crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken();
static Future<(Uint8List, PlatformInt64)> getSignalIdentity() => RustLib
.instance
.api
.crateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentity();
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> removeSignedPrekey({
required PlatformInt64 signedPreKeyId,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekey(
signedPreKeyId: signedPreKeyId,
);
static Future<void> storeSignedPrekey({
required PlatformInt64 signedPreKeyId,
required List<int> record,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekey(
signedPreKeyId: signedPreKeyId,
record: record,
);
@override
int get hashCode => 0;
@ -18,5 +62,5 @@ class FlutterKeyManager {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FlutterKeyManager && runtimeType == other.runtimeType;
other is RustKeyManager && runtimeType == other.runtimeType;
}

File diff suppressed because it is too large Load diff

View file

@ -5,12 +5,15 @@
import 'bridge.dart';
import 'bridge/callbacks.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:ffi' as ffi;
import 'frb_generated.dart';
import 'keys/backup_password_keys.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@ -99,6 +102,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Object dco_decode_DartOpaque(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);
@ -108,21 +116,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnnouncedUser dco_decode_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@protected
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected
FlutterKeyManager dco_decode_flutter_key_manager(dynamic raw);
@protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@ -147,6 +158,13 @@ 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
String? dco_decode_opt_String(dynamic raw);
@protected
AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw);
@ -165,12 +183,37 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
OtherPromotion dco_decode_other_promotion(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,
);
@protected
(String, String) dco_decode_record_string_string(dynamic raw);
@protected
RustBackupArchive dco_decode_rust_backup_archive(dynamic raw);
@protected
RustBackupIdentity dco_decode_rust_backup_identity(dynamic raw);
@protected
RustKeyManager dco_decode_rust_key_manager(dynamic raw);
@protected
int dco_decode_u_32(dynamic raw);
@protected
int dco_decode_u_8(dynamic raw);
@protected
U8Array32 dco_decode_u_8_array_32(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@ -183,6 +226,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Object sse_decode_DartOpaque(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,
@ -194,6 +242,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
@protected
BackupPasswordKeys sse_decode_backup_password_keys(
SseDeserializer deserializer,
);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@ -202,17 +255,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
@protected
FlutterKeyManager sse_decode_flutter_key_manager(
SseDeserializer deserializer,
);
@protected
FlutterUserDiscovery sse_decode_flutter_user_discovery(
SseDeserializer deserializer,
@ -243,6 +296,15 @@ 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
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
SseDeserializer deserializer,
@ -267,12 +329,43 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
OtherPromotion sse_decode_other_promotion(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,
);
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
);
@protected
RustBackupArchive sse_decode_rust_backup_archive(
SseDeserializer deserializer,
);
@protected
RustBackupIdentity sse_decode_rust_backup_identity(
SseDeserializer deserializer,
);
@protected
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
@protected
int sse_decode_u_32(SseDeserializer deserializer);
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@ -373,6 +466,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_DartOpaque(Object self, 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,
@ -385,6 +484,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer);
@protected
void sse_encode_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@ -394,6 +499,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
@ -406,12 +517,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_flutter_key_manager(
FlutterKeyManager self,
SseSerializer serializer,
);
@protected
void sse_encode_flutter_user_discovery(
FlutterUserDiscovery self,
@ -448,6 +553,15 @@ 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_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_announced_user(
AnnouncedUser? self,
@ -484,12 +598,51 @@ 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,
SseSerializer serializer,
);
@protected
void sse_encode_record_string_string(
(String, String) self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_archive(
RustBackupArchive self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_identity(
RustBackupIdentity self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_key_manager(
RustKeyManager self,
SseSerializer serializer,
);
@protected
void sse_encode_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);

View file

@ -8,11 +8,14 @@
import 'bridge.dart';
import 'bridge/callbacks.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'dart:async';
import 'dart:convert';
import 'frb_generated.dart';
import 'keys/backup_password_keys.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@ -101,6 +104,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Object dco_decode_DartOpaque(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);
@ -110,21 +118,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnnouncedUser dco_decode_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@protected
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected
FlutterKeyManager dco_decode_flutter_key_manager(dynamic raw);
@protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@ -149,6 +160,13 @@ 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
String? dco_decode_opt_String(dynamic raw);
@protected
AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw);
@ -167,12 +185,37 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
OtherPromotion dco_decode_other_promotion(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,
);
@protected
(String, String) dco_decode_record_string_string(dynamic raw);
@protected
RustBackupArchive dco_decode_rust_backup_archive(dynamic raw);
@protected
RustBackupIdentity dco_decode_rust_backup_identity(dynamic raw);
@protected
RustKeyManager dco_decode_rust_key_manager(dynamic raw);
@protected
int dco_decode_u_32(dynamic raw);
@protected
int dco_decode_u_8(dynamic raw);
@protected
U8Array32 dco_decode_u_8_array_32(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@ -185,6 +228,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Object sse_decode_DartOpaque(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,
@ -196,6 +244,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
@protected
BackupPasswordKeys sse_decode_backup_password_keys(
SseDeserializer deserializer,
);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@ -204,17 +257,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
@protected
FlutterKeyManager sse_decode_flutter_key_manager(
SseDeserializer deserializer,
);
@protected
FlutterUserDiscovery sse_decode_flutter_user_discovery(
SseDeserializer deserializer,
@ -245,6 +298,15 @@ 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
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
SseDeserializer deserializer,
@ -269,12 +331,43 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
OtherPromotion sse_decode_other_promotion(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,
);
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
);
@protected
RustBackupArchive sse_decode_rust_backup_archive(
SseDeserializer deserializer,
);
@protected
RustBackupIdentity sse_decode_rust_backup_identity(
SseDeserializer deserializer,
);
@protected
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
@protected
int sse_decode_u_32(SseDeserializer deserializer);
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@ -375,6 +468,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_DartOpaque(Object self, 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,
@ -387,6 +486,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer);
@protected
void sse_encode_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@ -396,6 +501,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
@ -408,12 +519,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_flutter_key_manager(
FlutterKeyManager self,
SseSerializer serializer,
);
@protected
void sse_encode_flutter_user_discovery(
FlutterUserDiscovery self,
@ -450,6 +555,15 @@ 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_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_announced_user(
AnnouncedUser? self,
@ -486,12 +600,51 @@ 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,
SseSerializer serializer,
);
@protected
void sse_encode_record_string_string(
(String, String) self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_archive(
RustBackupArchive self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_identity(
RustBackupIdentity self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_key_manager(
RustKeyManager self,
SseSerializer serializer,
);
@protected
void sse_encode_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);

View file

@ -0,0 +1,29 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import '../lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class BackupPasswordKeys {
final U8Array32 backupId;
final U8Array32 encryptionKey;
const BackupPasswordKeys({
required this.backupId,
required this.encryptionKey,
});
@override
int get hashCode => backupId.hashCode ^ encryptionKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BackupPasswordKeys &&
runtimeType == other.runtimeType &&
backupId == other.backupId &&
encryptionKey == other.encryptionKey;
}

20
lib/core/lib.dart Normal file
View file

@ -0,0 +1,20 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'frb_generated.dart';
import 'package:collection/collection.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class U8Array32 extends NonGrowableListView<int> {
static const arraySize = 32;
@internal
Uint8List get inner => _inner;
final Uint8List _inner;
U8Array32(this._inner) : assert(_inner.length == arraySize), super(_inner);
U8Array32.init() : this(Uint8List(arraySize));
}

View file

@ -1,13 +1,11 @@
import 'dart:async';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/utils/log.dart';
class AppEnvironment {
static late final String cacheDir;
static late final String supportDir;
static late String cacheDir;
static late String supportDir;
static bool _isInitialized = false;
@ -22,10 +20,9 @@ class AppEnvironment {
_isInitialized = true;
}
static void initTesting() {
if (_isInitialized) return;
cacheDir = '/tmp/twonly_cache';
supportDir = '/tmp/twonly_support';
static void initTesting({String? customCacheDir, String? customSupportDir}) {
cacheDir = customCacheDir ?? '/tmp/twonly_cache';
supportDir = customSupportDir ?? '/tmp/twonly_support';
_isInitialized = true;
}
}
@ -35,9 +32,5 @@ class AppState {
static bool isInBackgroundTask = false;
static bool allowErrorTrackingViaSentry = false;
static bool gotMessageFromServer = false;
static int latestAppVersionId = 111;
}
class AppGlobalKeys {
static final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
static int latestAppVersionId = 112;
}

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
@ -8,11 +9,16 @@ import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/app.dart';
import 'package:twonly/core/bridge.dart' as bridge;
import 'package:twonly/core/bridge/wrapper/key_manager.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/constants/secure_storage.keys.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/model/json/signal_identity.model.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/image_editor.provider.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
@ -21,7 +27,7 @@ import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
@ -185,11 +191,38 @@ Future<void> runMigrations() async {
}
});
}
if (userService.currentUser.appVersion < 111) {
if (userService.currentUser.appVersion < 113) {
final signalIdentity = await SecureStorage.instance.read(
// ignore: deprecated_member_use_from_same_package
key: SecureStorageKeys.signalIdentity,
);
if (signalIdentity != null) {
final decoded = jsonDecode(signalIdentity);
final identity = SignalIdentity.fromJson(decoded as Map<String, dynamic>);
try {
await RustKeyManager.importSignalIdentity(
identityKeyPairStructure: identity.identityKeyPairU8List,
registrationId: identity.registrationId,
signedPreKeyStore: await getSignalSignedPreKeyStoreOld(),
);
Log.info('Importing signal identiy to the rust key manager');
} catch (e) {
Log.error(e);
}
}
await UserService.update((u) {
u
..appVersion = 111
..canUseLoginTokenForAuth = false;
..appVersion = 113
..canUseLoginTokenForAuth = false
// As usernames changes where not considered in the old version force users
// to reenter there passwords.
// ignore: deprecated_member_use_from_same_package
..twonlySafeBackup?.encryptionKey = []
// ignore: deprecated_member_use_from_same_package
..twonlySafeBackup?.backupId = [];
});
}
}
@ -226,6 +259,6 @@ Future<void> postStartupTasks() async {
unawaited(initializeBackgroundTaskManager());
// 3. Delayed tasks (Wait for app to settle)
await Future.delayed(const Duration(minutes: 2));
unawaited(performTwonlySafeBackup());
unawaited(BackupService.makeBackup());
unawaited(cleanLogFile());
}

View file

@ -38,21 +38,29 @@ class UserDiscoveryCallbacks {
Uint8List pubKey,
Uint8List signature,
) async {
return Curve.verifySignature(
IdentityKey.fromBytes(pubKey, 0).publicKey,
inputData,
signature,
);
try {
return Curve.verifySignature(
IdentityKey.fromBytes(pubKey, 0).publicKey,
inputData,
signature,
);
} catch (_) {
return false;
}
}
static Future<bool> verifyStoredPubKey(
int contactId,
Uint8List pubKey,
) async {
final storedPublicKey = await getPublicKeyFromContact(contactId);
if (storedPublicKey != null) {
return storedPublicKey.equals(pubKey);
} else {
try {
final storedPublicKey = await getPublicKeyFromContact(contactId);
if (storedPublicKey != null) {
return storedPublicKey.equals(pubKey);
} else {
return false;
}
} catch (_) {
return false;
}
}

View file

@ -1,4 +1,6 @@
class KeyValueKeys {
static const String lastPeriodicTaskExecution =
'last_periodic_task_execution';
static const String currentBackupState = 'current_backup_state';
static const String backupRecoveryState = 'backup_recovery_state';
}

View file

@ -25,7 +25,6 @@ class Routes {
static const String settingsAccount = '/settings/account';
static const String settingsSubscription = '/settings/subscription';
static const String settingsBackup = '/settings/backup';
static const String settingsBackupServer = '/settings/backup/server';
static const String settingsBackupRecovery = '/settings/backup/recovery';
static const String settingsBackupSetup = '/settings/backup/setup';
static const String settingsAppearance = '/settings/appearance';

View file

@ -1,11 +1,15 @@
class SecureStorageKeys {
@Deprecated('Use the secure storage in rust')
static const String signalIdentity = 'signal_identity';
@Deprecated('Use the secure storage in rust')
static const String signalSignedPreKey = 'signed_pre_key_store';
@Deprecated('Use the login token')
static const String apiAuthToken = 'api_auth_token';
static const String googleFcm = 'google_fcm';
static const String userData = 'userData';
static const String twonlySafeLastBackupHash = 'twonly_safe_last_backup_hash';
@Deprecated('Use user.json file')
static const String userData = 'userData';
// Not required for backup...
static const String receivingPushKeys = 'push_keys_receiving';
static const String sendingPushKeys = 'push_keys_sending';
}

View file

@ -89,10 +89,12 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
),
innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)),
],
)..where(
ur.announcedUserId.equals(contactId) &
ur.publicKeyVerifiedTimestamp.isNotNull(),
);
)
..where(
ur.announcedUserId.equals(contactId) &
ur.publicKeyVerifiedTimestamp.isNotNull(),
)
..groupBy([contacts.userId]);
return query.watch().map((rows) {
return rows.map((row) {
@ -116,7 +118,8 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
..where(
ur.publicKeyVerifiedTimestamp.isNotNull() &
ur.announcedUserId.equalsExp(ur.fromContactId).not(),
);
)
..groupBy([ur.announcedUserId]);
final rows = await query.get();
return rows.length;

View file

@ -3,52 +3,43 @@ import 'dart:convert';
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/constants/secure_storage.keys.dart';
import 'package:twonly/src/utils/secure_storage.dart';
class SignalSignedPreKeyStore extends SignedPreKeyStore {
Future<HashMap<int, Uint8List>> getStore() async {
final storeSerialized = await SecureStorage.instance.read(
key: SecureStorageKeys.signalSignedPreKey,
);
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);
}
Future<HashMap<int, Uint8List>> getSignalSignedPreKeyStoreOld() async {
final storeSerialized = await SecureStorage.instance.read(
key: SecureStorageKeys.signalSignedPreKey,
);
final store = HashMap<int, Uint8List>();
if (storeSerialized == null) {
return store;
}
Future<void> safeStore(HashMap<int, Uint8List> store) async {
final storeHashMap = <List<dynamic>>[];
for (final item in store.entries) {
storeHashMap.add([item.key, base64Encode(item.value)]);
}
final storeSerialized = json.encode(storeHashMap);
await SecureStorage.instance.write(
key: SecureStorageKeys.signalSignedPreKey,
value: storeSerialized,
);
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 store = await getStore();
if (!store.containsKey(signedPreKeyId)) {
final store = await RustKeyManager.loadSignedPrekey(
signedPreKeyId: signedPreKeyId,
);
if (store == null) {
throw InvalidKeyIdException(
'No such signed prekey record! $signedPreKeyId',
);
}
return SignedPreKeyRecord.fromSerialized(store[signedPreKeyId]!);
return SignedPreKeyRecord.fromSerialized(store);
}
@override
Future<List<SignedPreKeyRecord>> loadSignedPreKeys() async {
final store = await getStore();
final store = await RustKeyManager.loadSignedPrekeys();
final results = <SignedPreKeyRecord>[];
for (final serialized in store.values) {
results.add(SignedPreKeyRecord.fromSerialized(serialized));
@ -61,19 +52,21 @@ class SignalSignedPreKeyStore extends SignedPreKeyStore {
int signedPreKeyId,
SignedPreKeyRecord record,
) async {
final store = await getStore();
store[signedPreKeyId] = record.serialize();
await safeStore(store);
await RustKeyManager.storeSignedPrekey(
signedPreKeyId: signedPreKeyId,
record: record.serialize(),
);
}
@override
Future<bool> containsSignedPreKey(int signedPreKeyId) async =>
(await getStore()).containsKey(signedPreKeyId);
await RustKeyManager.loadSignedPrekey(
signedPreKeyId: signedPreKeyId,
) !=
null;
@override
Future<void> removeSignedPreKey(int signedPreKeyId) async {
final store = await getStore();
store.remove(signedPreKeyId);
await safeStore(store);
await RustKeyManager.removeSignedPrekey(signedPreKeyId: signedPreKeyId);
}
}

View file

@ -1286,18 +1286,6 @@ abstract class AppLocalizations {
/// **'Open'**
String get open;
/// No description provided for @createVoucher.
///
/// In en, this message translates to:
/// **'Buy voucher'**
String get createVoucher;
/// No description provided for @redeemVoucher.
///
/// In en, this message translates to:
/// **'Redeem voucher'**
String get redeemVoucher;
/// No description provided for @buy.
///
/// In en, this message translates to:
@ -1412,23 +1400,17 @@ abstract class AppLocalizations {
/// **'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.'**
String get backupNoPasswordRecovery;
/// No description provided for @backupServer.
/// No description provided for @backupIdentityHeader.
///
/// In en, this message translates to:
/// **'Server'**
String get backupServer;
/// **'Identity'**
String get backupIdentityHeader;
/// No description provided for @backupMaxBackupSize.
/// No description provided for @backupArchiveHeader.
///
/// In en, this message translates to:
/// **'max. backup size'**
String get backupMaxBackupSize;
/// No description provided for @backupStorageRetention.
///
/// In en, this message translates to:
/// **'Storage retention'**
String get backupStorageRetention;
/// **'Contacts, Settings and Messages'**
String get backupArchiveHeader;
/// No description provided for @backupLastBackupDate.
///
@ -1448,12 +1430,6 @@ abstract class AppLocalizations {
/// **'Result'**
String get backupLastBackupResult;
/// No description provided for @backupData.
///
/// In en, this message translates to:
/// **'Data-Backup'**
String get backupData;
/// No description provided for @backupInsecurePassword.
///
/// In en, this message translates to:
@ -1514,36 +1490,12 @@ abstract class AppLocalizations {
/// **'Password must be at least 10 characters long.'**
String get backupPasswordRequirement;
/// No description provided for @backupExpertSettings.
///
/// In en, this message translates to:
/// **'Expert settings'**
String get backupExpertSettings;
/// No description provided for @backupEnableBackup.
///
/// In en, this message translates to:
/// **'Activate automatic backup'**
String get backupEnableBackup;
/// No description provided for @backupOwnServerDesc.
///
/// In en, this message translates to:
/// **'Save your twonly Backup at twonly or on any server of your choice.'**
String get backupOwnServerDesc;
/// No description provided for @backupUseOwnServer.
///
/// In en, this message translates to:
/// **'Use server'**
String get backupUseOwnServer;
/// No description provided for @backupResetServer.
///
/// In en, this message translates to:
/// **'Use standard server'**
String get backupResetServer;
/// No description provided for @backupTwonlySaveNow.
///
/// In en, this message translates to:
@ -2330,12 +2282,6 @@ abstract class AppLocalizations {
/// **'Open your own QR code'**
String get openYourOwnQRcode;
/// No description provided for @skipForNow.
///
/// In en, this message translates to:
/// **'Skip for now'**
String get skipForNow;
/// No description provided for @finishSetupCardTitle.
///
/// In en, this message translates to:
@ -2354,6 +2300,24 @@ abstract class AppLocalizations {
/// **'Resume Setup'**
String get finishSetupCardAction;
/// No description provided for @missingBackupCardTitle.
///
/// In en, this message translates to:
/// **'Setup backup'**
String get missingBackupCardTitle;
/// No description provided for @missingBackupCardDesc.
///
/// In en, this message translates to:
/// **'We have improved the backup mechanism, which requires you to set it up again.'**
String get missingBackupCardDesc;
/// No description provided for @missingBackupCardAction.
///
/// In en, this message translates to:
/// **'Set up now'**
String get missingBackupCardAction;
/// No description provided for @onboardingFinishLater.
///
/// In en, this message translates to:
@ -3061,6 +3025,48 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'{maker} changed their display name from {oldName} to {newName}.'**
String makerChangedDisplayName(Object maker, Object oldName, Object newName);
/// No description provided for @recoverErrorNoInternet.
///
/// In en, this message translates to:
/// **'No internet connection. Please check your network and try again.'**
String get recoverErrorNoInternet;
/// No description provided for @recoverErrorUsernameNotValid.
///
/// In en, this message translates to:
/// **'The username provided is not valid or does not exist.'**
String get recoverErrorUsernameNotValid;
/// No description provided for @recoverErrorPasswordInvalid.
///
/// In en, this message translates to:
/// **'The password provided is incorrect.'**
String get recoverErrorPasswordInvalid;
/// No description provided for @recoverErrorTryAgainLater.
///
/// In en, this message translates to:
/// **'The server is currently unavailable. Please try again later.'**
String get recoverErrorTryAgainLater;
/// No description provided for @recoverErrorUnknown.
///
/// In en, this message translates to:
/// **'An unknown error occurred. Please try again.'**
String get recoverErrorUnknown;
/// No description provided for @recoverSuccessTitle.
///
/// In en, this message translates to:
/// **'Backup successfully recovered.'**
String get recoverSuccessTitle;
/// No description provided for @recoverSuccessBody.
///
/// In en, this message translates to:
/// **'Click here to open the app again'**
String get recoverSuccessBody;
}
class _AppLocalizationsDelegate

View file

@ -658,12 +658,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get open => 'Offene';
@override
String get createVoucher => 'Gutschein kaufen';
@override
String get redeemVoucher => 'Gutschein einlösen';
@override
String get buy => 'Kaufen';
@ -725,13 +719,10 @@ class AppLocalizationsDe extends AppLocalizations {
'Aufgrund des Sicherheitssystems von twonly gibt es (derzeit) keine Funktion zur Wiederherstellung des Passworts. Daher musst du dir dein Passwort merken oder, besser noch, aufschreiben.';
@override
String get backupServer => 'Server';
String get backupIdentityHeader => 'Identität';
@override
String get backupMaxBackupSize => 'max. Backup-Größe';
@override
String get backupStorageRetention => 'Speicheraufbewahrung';
String get backupArchiveHeader => 'Kontakte, Einstellungen und Nachrichten';
@override
String get backupLastBackupDate => 'Letztes Backup';
@ -742,9 +733,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get backupLastBackupResult => 'Ergebnis';
@override
String get backupData => 'Daten-Backup';
@override
String get backupInsecurePassword => 'Unsicheres Passwort';
@ -779,22 +767,9 @@ class AppLocalizationsDe extends AppLocalizations {
String get backupPasswordRequirement =>
'Das Passwort muss mindestens 10 Zeichen lang sein.';
@override
String get backupExpertSettings => 'Experteneinstellungen';
@override
String get backupEnableBackup => 'Automatische Sicherung aktivieren';
@override
String get backupOwnServerDesc =>
'Speichere dein twonly Backup auf einem Server deiner Wahl.';
@override
String get backupUseOwnServer => 'Server verwenden';
@override
String get backupResetServer => 'Standardserver verwenden';
@override
String get backupTwonlySaveNow => 'Jetzt speichern';
@ -1271,9 +1246,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get openYourOwnQRcode => 'Eigenen QR-Code öffnen';
@override
String get skipForNow => 'Vorerst überspringen';
@override
String get finishSetupCardTitle => 'Profil vervollständigen';
@ -1284,6 +1256,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get finishSetupCardAction => 'Setup fortsetzen';
@override
String get missingBackupCardTitle => 'Backup einrichten';
@override
String get missingBackupCardDesc =>
'Wir haben den Backup-Mechanismus verbessert, weshalb du ihn erneut einrichten musst.';
@override
String get missingBackupCardAction => 'Jetzt einrichten';
@override
String get onboardingFinishLater => 'Später abschließen';
@ -1714,11 +1696,37 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String makerChangedUsername(Object maker, Object oldName, Object newName) {
return '$maker hat seinen Benutzernamen von $oldName zu $newName geändert.';
return '$maker hat den Benutzernamen von $oldName zu $newName geändert.';
}
@override
String makerChangedDisplayName(Object maker, Object oldName, Object newName) {
return '$maker hat seinen Anzeigenamen von $oldName zu $newName geändert.';
return '$maker hat den Anzeigenamen von $oldName zu $newName geändert.';
}
@override
String get recoverErrorNoInternet =>
'Keine Internetverbindung. Bitte überprüfe deine Netzwerkverbindung und versuche es erneut.';
@override
String get recoverErrorUsernameNotValid =>
'Der eingegebene Benutzername ist ungültig oder existiert nicht.';
@override
String get recoverErrorPasswordInvalid =>
'Das eingegebene Passwort ist falsch.';
@override
String get recoverErrorTryAgainLater =>
'Der Server ist derzeit nicht erreichbar. Bitte versuche es später erneut.';
@override
String get recoverErrorUnknown =>
'Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut.';
@override
String get recoverSuccessTitle => 'Backup erfolgreich wiederhergestellt.';
@override
String get recoverSuccessBody => 'Klicke hier, um die App wieder zu öffnen';
}

View file

@ -652,12 +652,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get open => 'Open';
@override
String get createVoucher => 'Buy voucher';
@override
String get redeemVoucher => 'Redeem voucher';
@override
String get buy => 'Buy';
@ -719,13 +713,10 @@ class AppLocalizationsEn extends AppLocalizations {
'Due to twonly\'s security system, there is (currently) no password recovery function. Therefore, you must remember your password or, better yet, write it down.';
@override
String get backupServer => 'Server';
String get backupIdentityHeader => 'Identity';
@override
String get backupMaxBackupSize => 'max. backup size';
@override
String get backupStorageRetention => 'Storage retention';
String get backupArchiveHeader => 'Contacts, Settings and Messages';
@override
String get backupLastBackupDate => 'Last backup';
@ -736,9 +727,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get backupLastBackupResult => 'Result';
@override
String get backupData => 'Data-Backup';
@override
String get backupInsecurePassword => 'Insecure password';
@ -773,22 +761,9 @@ class AppLocalizationsEn extends AppLocalizations {
String get backupPasswordRequirement =>
'Password must be at least 10 characters long.';
@override
String get backupExpertSettings => 'Expert settings';
@override
String get backupEnableBackup => 'Activate automatic backup';
@override
String get backupOwnServerDesc =>
'Save your twonly Backup at twonly or on any server of your choice.';
@override
String get backupUseOwnServer => 'Use server';
@override
String get backupResetServer => 'Use standard server';
@override
String get backupTwonlySaveNow => 'Save now';
@ -1262,9 +1237,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get openYourOwnQRcode => 'Open your own QR code';
@override
String get skipForNow => 'Skip for now';
@override
String get finishSetupCardTitle => 'Complete your profile';
@ -1275,6 +1247,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get finishSetupCardAction => 'Resume Setup';
@override
String get missingBackupCardTitle => 'Setup backup';
@override
String get missingBackupCardDesc =>
'We have improved the backup mechanism, which requires you to set it up again.';
@override
String get missingBackupCardAction => 'Set up now';
@override
String get onboardingFinishLater => 'Finish later';
@ -1706,4 +1688,30 @@ class AppLocalizationsEn extends AppLocalizations {
String makerChangedDisplayName(Object maker, Object oldName, Object newName) {
return '$maker changed their display name from $oldName to $newName.';
}
@override
String get recoverErrorNoInternet =>
'No internet connection. Please check your network and try again.';
@override
String get recoverErrorUsernameNotValid =>
'The username provided is not valid or does not exist.';
@override
String get recoverErrorPasswordInvalid =>
'The password provided is incorrect.';
@override
String get recoverErrorTryAgainLater =>
'The server is currently unavailable. Please try again later.';
@override
String get recoverErrorUnknown =>
'An unknown error occurred. Please try again.';
@override
String get recoverSuccessTitle => 'Backup successfully recovered.';
@override
String get recoverSuccessBody => 'Click here to open the app again';
}

@ -1 +1 @@
Subproject commit fccd366e119671b96730cb09d8bb8aa1057bd1c5
Subproject commit 9eeb6b5cb46410a1616c0dbd63ce74143dfdfbbc

View file

@ -0,0 +1,51 @@
import 'package:json_annotation/json_annotation.dart';
part 'backup.model.g.dart';
enum LastBackupUploadState { none, pending, failed, success }
@JsonSerializable()
class CurrentBackupStatus {
CurrentBackupStatus();
factory CurrentBackupStatus.fromJson(Map<String, dynamic> json) =>
_$CurrentBackupStatusFromJson(json);
LastBackupUploadState identityState = LastBackupUploadState.none;
DateTime? identityLastSuccessFull;
int? identitySize;
LastBackupUploadState archiveState = LastBackupUploadState.none;
DateTime? archiveLastSuccessFull;
int? archiveSize;
Map<String, dynamic> toJson() => _$CurrentBackupStatusToJson(this);
}
enum BackupRecoveryState {
// The userId was loaded from the server and the user is asked to enter his password.
identityBackupStarted,
// -> Download identity, replace keymanager
// Identity was downloaded and Keymanager was updated
archiveBackupStarted,
// -> Download archive, replace files, restart app
}
@JsonSerializable()
class BackupRecovery {
BackupRecovery({
required this.username,
required this.password,
required this.userId,
});
factory BackupRecovery.fromJson(Map<String, dynamic> json) =>
_$BackupRecoveryFromJson(json);
String username;
String password;
int userId;
BackupRecoveryState state = BackupRecoveryState.identityBackupStarted;
Map<String, dynamic> toJson() => _$BackupRecoveryToJson(this);
}

View file

@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup.model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CurrentBackupStatus _$CurrentBackupStatusFromJson(Map<String, dynamic> json) =>
CurrentBackupStatus()
..identityState = $enumDecode(
_$LastBackupUploadStateEnumMap,
json['identityState'],
)
..identityLastSuccessFull = json['identityLastSuccessFull'] == null
? null
: DateTime.parse(json['identityLastSuccessFull'] as String)
..identitySize = (json['identitySize'] as num?)?.toInt()
..archiveState = $enumDecode(
_$LastBackupUploadStateEnumMap,
json['archiveState'],
)
..archiveLastSuccessFull = json['archiveLastSuccessFull'] == null
? null
: DateTime.parse(json['archiveLastSuccessFull'] as String)
..archiveSize = (json['archiveSize'] as num?)?.toInt();
Map<String, dynamic> _$CurrentBackupStatusToJson(
CurrentBackupStatus instance,
) => <String, dynamic>{
'identityState': _$LastBackupUploadStateEnumMap[instance.identityState]!,
'identityLastSuccessFull': instance.identityLastSuccessFull
?.toIso8601String(),
'identitySize': instance.identitySize,
'archiveState': _$LastBackupUploadStateEnumMap[instance.archiveState]!,
'archiveLastSuccessFull': instance.archiveLastSuccessFull?.toIso8601String(),
'archiveSize': instance.archiveSize,
};
const _$LastBackupUploadStateEnumMap = {
LastBackupUploadState.none: 'none',
LastBackupUploadState.pending: 'pending',
LastBackupUploadState.failed: 'failed',
LastBackupUploadState.success: 'success',
};
BackupRecovery _$BackupRecoveryFromJson(Map<String, dynamic> json) =>
BackupRecovery(
username: json['username'] as String,
password: json['password'] as String,
userId: (json['userId'] as num).toInt(),
)..state = $enumDecode(_$BackupRecoveryStateEnumMap, json['state']);
Map<String, dynamic> _$BackupRecoveryToJson(BackupRecovery instance) =>
<String, dynamic>{
'username': instance.username,
'password': instance.password,
'userId': instance.userId,
'state': _$BackupRecoveryStateEnumMap[instance.state]!,
};
const _$BackupRecoveryStateEnumMap = {
BackupRecoveryState.identityBackupStarted: 'identityBackupStarted',
BackupRecoveryState.archiveBackupStarted: 'archiveBackupStarted',
};

View file

@ -133,10 +133,15 @@ class UserData {
// --- BACKUP ---
DateTime? nextTimeToShowBackupNotice;
BackupServer? backupServer;
@Deprecated('Use the secure storage in rust')
TwonlySafeBackup? twonlySafeBackup;
@JsonKey(defaultValue: false)
bool isBackupEnabled = false;
// Used for push notifcation via FCM.
String? fcmToken;
// For my master thesis I want to create a anonymous user study:
// - users in the "Tester" Plan can, if they want, take part of the user study
@ -178,19 +183,3 @@ class TwonlySafeBackup {
List<int> encryptionKey;
Map<String, dynamic> toJson() => _$TwonlySafeBackupToJson(this);
}
@JsonSerializable()
class BackupServer {
BackupServer({
required this.serverUrl,
required this.retentionDays,
required this.maxBackupBytes,
});
factory BackupServer.fromJson(Map<String, dynamic> json) =>
_$BackupServerFromJson(json);
String serverUrl;
int retentionDays;
int maxBackupBytes;
Map<String, dynamic> toJson() => _$BackupServerToJson(this);
}

View file

@ -71,6 +71,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
json['userDiscoveryRequiresManualApproval'] as bool? ?? false
..userDiscoverySharePromotion =
json['userDiscoverySharePromotion'] as bool? ?? true
..userDiscoveryInitializationError =
json['userDiscoveryInitializationError'] as bool? ?? false
..currentPreKeyIndexStart =
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..currentSignedPreKeyIndexStart =
@ -80,17 +82,15 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
.toList()
..hideChangeLog = json['hideChangeLog'] as bool? ?? true
..updateFCMToken = json['updateFCMToken'] as bool? ?? true
..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null
? null
: DateTime.parse(json['nextTimeToShowBackupNotice'] as String)
..backupServer = json['backupServer'] == null
? null
: BackupServer.fromJson(json['backupServer'] as Map<String, dynamic>)
..canUseLoginTokenForAuth =
json['canUseLoginTokenForAuth'] as bool? ?? true
..twonlySafeBackup = json['twonlySafeBackup'] == null
? null
: TwonlySafeBackup.fromJson(
json['twonlySafeBackup'] as Map<String, dynamic>,
)
..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false
..fcmToken = json['fcmToken'] as String?
..askedForUserStudyPermission =
json['askedForUserStudyPermission'] as bool? ?? false
..userStudyParticipantsToken =
@ -142,15 +142,16 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'userDiscoveryRequiresManualApproval':
instance.userDiscoveryRequiresManualApproval,
'userDiscoverySharePromotion': instance.userDiscoverySharePromotion,
'userDiscoveryInitializationError': instance.userDiscoveryInitializationError,
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash,
'hideChangeLog': instance.hideChangeLog,
'updateFCMToken': instance.updateFCMToken,
'nextTimeToShowBackupNotice': instance.nextTimeToShowBackupNotice
?.toIso8601String(),
'backupServer': instance.backupServer,
'canUseLoginTokenForAuth': instance.canUseLoginTokenForAuth,
'twonlySafeBackup': instance.twonlySafeBackup,
'isBackupEnabled': instance.isBackupEnabled,
'fcmToken': instance.fcmToken,
'askedForUserStudyPermission': instance.askedForUserStudyPermission,
'userStudyParticipantsToken': instance.userStudyParticipantsToken,
'userStudyCountNewFriendsViaSuggestion':
@ -201,16 +202,3 @@ const _$LastBackupUploadStateEnumMap = {
LastBackupUploadState.failed: 'failed',
LastBackupUploadState.success: 'success',
};
BackupServer _$BackupServerFromJson(Map<String, dynamic> json) => BackupServer(
serverUrl: json['serverUrl'] as String,
retentionDays: (json['retentionDays'] as num).toInt(),
maxBackupBytes: (json['maxBackupBytes'] as num).toInt(),
);
Map<String, dynamic> _$BackupServerToJson(BackupServer instance) =>
<String, dynamic>{
'serverUrl': instance.serverUrl,
'retentionDays': instance.retentionDays,
'maxBackupBytes': instance.maxBackupBytes,
};

View file

@ -468,6 +468,64 @@ class Handshake_GetAuthChallenge extends $pb.GeneratedMessage {
static Handshake_GetAuthChallenge? _defaultInstance;
}
class Handshake_GetUserIdByUsername extends $pb.GeneratedMessage {
factory Handshake_GetUserIdByUsername({
$core.String? username,
}) {
final result = create();
if (username != null) result.username = username;
return result;
}
Handshake_GetUserIdByUsername._();
factory Handshake_GetUserIdByUsername.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Handshake_GetUserIdByUsername.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Handshake.GetUserIdByUsername',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'username')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Handshake_GetUserIdByUsername clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Handshake_GetUserIdByUsername copyWith(
void Function(Handshake_GetUserIdByUsername) updates) =>
super.copyWith(
(message) => updates(message as Handshake_GetUserIdByUsername))
as Handshake_GetUserIdByUsername;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Handshake_GetUserIdByUsername create() =>
Handshake_GetUserIdByUsername._();
@$core.override
Handshake_GetUserIdByUsername createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Handshake_GetUserIdByUsername getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Handshake_GetUserIdByUsername>(create);
static Handshake_GetUserIdByUsername? _defaultInstance;
@$pb.TagNumber(1)
$core.String get username => $_getSZ(0);
@$pb.TagNumber(1)
set username($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasUsername() => $_has(0);
@$pb.TagNumber(1)
void clearUsername() => $_clearField(1);
}
class Handshake_GetAuthToken extends $pb.GeneratedMessage {
factory Handshake_GetAuthToken({
$fixnum.Int64? userId,
@ -758,6 +816,7 @@ enum Handshake_Handshake {
authenticate,
requestPOW,
authenticateWithLoginToken,
getUseridByUsername,
notSet
}
@ -769,6 +828,7 @@ class Handshake extends $pb.GeneratedMessage {
Handshake_Authenticate? authenticate,
Handshake_RequestPOW? requestPOW,
Handshake_AuthenticateWithLoginToken? authenticateWithLoginToken,
Handshake_GetUserIdByUsername? getUseridByUsername,
}) {
final result = create();
if (register != null) result.register = register;
@ -778,6 +838,8 @@ class Handshake extends $pb.GeneratedMessage {
if (requestPOW != null) result.requestPOW = requestPOW;
if (authenticateWithLoginToken != null)
result.authenticateWithLoginToken = authenticateWithLoginToken;
if (getUseridByUsername != null)
result.getUseridByUsername = getUseridByUsername;
return result;
}
@ -798,6 +860,7 @@ class Handshake extends $pb.GeneratedMessage {
4: Handshake_Handshake.authenticate,
5: Handshake_Handshake.requestPOW,
6: Handshake_Handshake.authenticateWithLoginToken,
7: Handshake_Handshake.getUseridByUsername,
0: Handshake_Handshake.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -805,7 +868,7 @@ class Handshake extends $pb.GeneratedMessage {
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..oo(0, [1, 2, 3, 4, 5, 6])
..oo(0, [1, 2, 3, 4, 5, 6, 7])
..aOM<Handshake_Register>(1, _omitFieldNames ? '' : 'register',
subBuilder: Handshake_Register.create)
..aOM<Handshake_GetAuthChallenge>(
@ -821,6 +884,9 @@ class Handshake extends $pb.GeneratedMessage {
..aOM<Handshake_AuthenticateWithLoginToken>(
6, _omitFieldNames ? '' : 'authenticateWithLoginToken',
subBuilder: Handshake_AuthenticateWithLoginToken.create)
..aOM<Handshake_GetUserIdByUsername>(
7, _omitFieldNames ? '' : 'getUseridByUsername',
subBuilder: Handshake_GetUserIdByUsername.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -847,6 +913,7 @@ class Handshake extends $pb.GeneratedMessage {
@$pb.TagNumber(4)
@$pb.TagNumber(5)
@$pb.TagNumber(6)
@$pb.TagNumber(7)
Handshake_Handshake whichHandshake() =>
_Handshake_HandshakeByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1)
@ -855,6 +922,7 @@ class Handshake extends $pb.GeneratedMessage {
@$pb.TagNumber(4)
@$pb.TagNumber(5)
@$pb.TagNumber(6)
@$pb.TagNumber(7)
void clearHandshake() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1)
@ -926,6 +994,18 @@ class Handshake extends $pb.GeneratedMessage {
@$pb.TagNumber(6)
Handshake_AuthenticateWithLoginToken ensureAuthenticateWithLoginToken() =>
$_ensure(5);
@$pb.TagNumber(7)
Handshake_GetUserIdByUsername get getUseridByUsername => $_getN(6);
@$pb.TagNumber(7)
set getUseridByUsername(Handshake_GetUserIdByUsername value) =>
$_setField(7, value);
@$pb.TagNumber(7)
$core.bool hasGetUseridByUsername() => $_has(6);
@$pb.TagNumber(7)
void clearGetUseridByUsername() => $_clearField(7);
@$pb.TagNumber(7)
Handshake_GetUserIdByUsername ensureGetUseridByUsername() => $_ensure(6);
}
class ApplicationData_TextMessage extends $pb.GeneratedMessage {

View file

@ -143,11 +143,21 @@ const Handshake$json = {
'9': 0,
'10': 'authenticateWithLoginToken'
},
{
'1': 'get_userid_by_username',
'3': 7,
'4': 1,
'5': 11,
'6': '.client_to_server.Handshake.GetUserIdByUsername',
'9': 0,
'10': 'getUseridByUsername'
},
],
'3': [
Handshake_RequestPOW$json,
Handshake_Register$json,
Handshake_GetAuthChallenge$json,
Handshake_GetUserIdByUsername$json,
Handshake_GetAuthToken$json,
Handshake_Authenticate$json,
Handshake_AuthenticateWithLoginToken$json
@ -217,6 +227,14 @@ const Handshake_GetAuthChallenge$json = {
'1': 'GetAuthChallenge',
};
@$core.Deprecated('Use handshakeDescriptor instead')
const Handshake_GetUserIdByUsername$json = {
'1': 'GetUserIdByUsername',
'2': [
{'1': 'username', '3': 1, '4': 1, '5': 9, '10': 'username'},
],
};
@$core.Deprecated('Use handshakeDescriptor instead')
const Handshake_GetAuthToken$json = {
'1': 'GetAuthToken',
@ -296,25 +314,28 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
'CgpyZXF1ZXN0UE9XGAUgASgLMiYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuUmVxdWVzdF'
'BPV0gAUgpyZXF1ZXN0UE9XEnsKHWF1dGhlbnRpY2F0ZV93aXRoX2xvZ2luX3Rva2VuGAYgASgL'
'MjYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW'
'5IAFIaYXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW4aDAoKUmVxdWVzdFBPVxrKAwoIUmVnaXN0'
'ZXISGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lEiQKC2ludml0ZV9jb2RlGAIgASgJSABSCm'
'ludml0ZUNvZGWIAQESLgoTcHVibGljX2lkZW50aXR5X2tleRgDIAEoDFIRcHVibGljSWRlbnRp'
'dHlLZXkSIwoNc2lnbmVkX3ByZWtleRgEIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcm'
'VrZXlfc2lnbmF0dXJlGAUgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUSKAoQc2lnbmVkX3By'
'ZWtleV9pZBgGIAEoA1IOc2lnbmVkUHJla2V5SWQSJwoPcmVnaXN0cmF0aW9uX2lkGAcgASgDUg'
'5yZWdpc3RyYXRpb25JZBIVCgZpc19pb3MYCCABKAhSBWlzSW9zEhsKCWxhbmdfY29kZRgJIAEo'
'CVIIbGFuZ0NvZGUSIgoNcHJvb2Zfb2Zfd29yaxgKIAEoA1ILcHJvb2ZPZldvcmsSJAoLbG9naW'
'5fdG9rZW4YCyABKAxIAVIKbG9naW5Ub2tlbogBAUIOCgxfaW52aXRlX2NvZGVCDgoMX2xvZ2lu'
'X3Rva2VuGhIKEEdldEF1dGhDaGFsbGVuZ2UaQwoMR2V0QXV0aFRva2VuEhcKB3VzZXJfaWQYAS'
'ABKANSBnVzZXJJZBIaCghyZXNwb25zZRgCIAEoDFIIcmVzcG9uc2Ua6AEKDEF1dGhlbnRpY2F0'
'ZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSHQoKYXV0aF90b2tlbhgCIAEoDFIJYXV0aFRva2'
'VuEiQKC2FwcF92ZXJzaW9uGAMgASgJSABSCmFwcFZlcnNpb26IAQESIAoJZGV2aWNlX2lkGAQg'
'ASgDSAFSCGRldmljZUlkiAEBEigKDWluX2JhY2tncm91bmQYBSABKAhIAlIMaW5CYWNrZ3JvdW'
'5kiAEBQg4KDF9hcHBfdmVyc2lvbkIMCgpfZGV2aWNlX2lkQhAKDl9pbl9iYWNrZ3JvdW5kGsYB'
'ChpBdXRoZW50aWNhdGVXaXRoTG9naW5Ub2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSLA'
'oSc2VjcmV0X2xvZ2luX3Rva2VuGAIgASgMUhBzZWNyZXRMb2dpblRva2VuEh8KC2FwcF92ZXJz'
'aW9uGAMgASgJUgphcHBWZXJzaW9uEhsKCWRldmljZV9pZBgEIAEoA1IIZGV2aWNlSWQSIwoNaW'
'5fYmFja2dyb3VuZBgFIAEoCFIMaW5CYWNrZ3JvdW5kQgsKCUhhbmRzaGFrZQ==');
'5IAFIaYXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW4SZgoWZ2V0X3VzZXJpZF9ieV91c2VybmFt'
'ZRgHIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtlLkdldFVzZXJJZEJ5VXNlcm5hbW'
'VIAFITZ2V0VXNlcmlkQnlVc2VybmFtZRoMCgpSZXF1ZXN0UE9XGsoDCghSZWdpc3RlchIaCgh1'
'c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUSJAoLaW52aXRlX2NvZGUYAiABKAlIAFIKaW52aXRlQ2'
'9kZYgBARIuChNwdWJsaWNfaWRlbnRpdHlfa2V5GAMgASgMUhFwdWJsaWNJZGVudGl0eUtleRIj'
'Cg1zaWduZWRfcHJla2V5GAQgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaW'
'duYXR1cmUYBSABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lk'
'GAYgASgDUg5zaWduZWRQcmVrZXlJZBInCg9yZWdpc3RyYXRpb25faWQYByABKANSDnJlZ2lzdH'
'JhdGlvbklkEhUKBmlzX2lvcxgIIAEoCFIFaXNJb3MSGwoJbGFuZ19jb2RlGAkgASgJUghsYW5n'
'Q29kZRIiCg1wcm9vZl9vZl93b3JrGAogASgDUgtwcm9vZk9mV29yaxIkCgtsb2dpbl90b2tlbh'
'gLIAEoDEgBUgpsb2dpblRva2VuiAEBQg4KDF9pbnZpdGVfY29kZUIOCgxfbG9naW5fdG9rZW4a'
'EgoQR2V0QXV0aENoYWxsZW5nZRoxChNHZXRVc2VySWRCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGA'
'EgASgJUgh1c2VybmFtZRpDCgxHZXRBdXRoVG9rZW4SFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklk'
'EhoKCHJlc3BvbnNlGAIgASgMUghyZXNwb25zZRroAQoMQXV0aGVudGljYXRlEhcKB3VzZXJfaW'
'QYASABKANSBnVzZXJJZBIdCgphdXRoX3Rva2VuGAIgASgMUglhdXRoVG9rZW4SJAoLYXBwX3Zl'
'cnNpb24YAyABKAlIAFIKYXBwVmVyc2lvbogBARIgCglkZXZpY2VfaWQYBCABKANIAVIIZGV2aW'
'NlSWSIAQESKAoNaW5fYmFja2dyb3VuZBgFIAEoCEgCUgxpbkJhY2tncm91bmSIAQFCDgoMX2Fw'
'cF92ZXJzaW9uQgwKCl9kZXZpY2VfaWRCEAoOX2luX2JhY2tncm91bmQaxgEKGkF1dGhlbnRpY2'
'F0ZVdpdGhMb2dpblRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIsChJzZWNyZXRfbG9n'
'aW5fdG9rZW4YAiABKAxSEHNlY3JldExvZ2luVG9rZW4SHwoLYXBwX3ZlcnNpb24YAyABKAlSCm'
'FwcFZlcnNpb24SGwoJZGV2aWNlX2lkGAQgASgDUghkZXZpY2VJZBIjCg1pbl9iYWNrZ3JvdW5k'
'GAUgASgIUgxpbkJhY2tncm91bmRCCwoJSGFuZHNoYWtl');
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData$json = {

View file

@ -16,7 +16,6 @@ import 'package:twonly/src/visual/views/onboarding/recover.view.dart';
import 'package:twonly/src/visual/views/public_profile.view.dart';
import 'package:twonly/src/visual/views/settings/account.view.dart';
import 'package:twonly/src/visual/views/settings/appearance.view.dart';
import 'package:twonly/src/visual/views/settings/backup/backup_server.view.dart';
import 'package:twonly/src/visual/views/settings/backup/backup_settings.view.dart';
import 'package:twonly/src/visual/views/settings/backup/backup_setup.view.dart';
import 'package:twonly/src/visual/views/settings/chat/chat_reactions.view.dart';
@ -165,10 +164,6 @@ final routerProvider = GoRouter(
path: 'backup',
builder: (context, state) => const BackupView(),
routes: [
GoRoute(
path: 'server',
builder: (context, state) => const BackupServerView(),
),
GoRoute(
path: 'recovery',
builder: (context, state) => const BackupRecoveryView(),

View file

@ -32,7 +32,7 @@ import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/api/server_messages.api.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
@ -61,6 +61,8 @@ class ApiService {
// final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev.twonly.eu';
final String apiSecure = kReleaseMode ? 's' : 's';
String get apiEndpoint => 'http$apiSecure://$apiHost/api/';
final _planUpdateController = StreamController<SubscriptionPlan>.broadcast();
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
@ -123,7 +125,7 @@ class ApiService {
twonlyDB.markUpdated();
unawaited(syncFlameCounters());
unawaited(setupNotificationWithUsers());
unawaited(signalHandleNewServerConnection());
unawaited(SignalIdentityService.onAuthenticated());
resetResyncedUsers();
resetUserDiscoveryRequestUpdates();
unawaited(fetchGroupStatesForUnjoinedGroups());
@ -454,7 +456,7 @@ class ApiService {
try {
Log.info('Switching authentication to login token');
final loginToken = await FlutterKeyManager.getLoginToken();
final loginToken = await RustKeyManager.getLoginToken();
final res = await _setLoginToken(loginToken);
if (res.isSuccess) {
Log.info('Switch was successfully.');
@ -484,7 +486,7 @@ class ApiService {
Future<bool> tryAuthenticateWithLoginToken() async {
try {
final loginToken = await FlutterKeyManager.getLoginToken();
final loginToken = await RustKeyManager.getLoginToken();
final authenticate = Handshake_AuthenticateWithLoginToken()
..userId = Int64(userService.currentUser.userId)
@ -527,8 +529,7 @@ class ApiService {
return lockAuthentication.protect(() async {
if (isAuthenticated) return;
if (await getSignalIdentity() == null) {
Log.error('Signal identity not found.');
if (!userService.isUserCreated) {
return;
}
@ -605,7 +606,7 @@ class ApiService {
final signedPreKey = (await signalStore.loadSignedPreKeys())[0];
final loginToken = await FlutterKeyManager.getLoginToken();
final loginToken = await RustKeyManager.getLoginToken();
final register = Handshake_Register()
..username = username
@ -690,6 +691,21 @@ class ApiService {
return sendRequestSync(req);
}
Future<int?> getUserIdFromUsername(String username) async {
final appData = Handshake(
getUseridByUsername: Handshake_GetUserIdByUsername(username: username),
);
final req = createClientToServerFromHandshake(appData);
final res = await sendRequestSync(req);
if (res.isSuccess) {
final ok = res.value as server.Response_Ok;
if (ok.hasUserid()) {
return ok.userid.toInt();
}
}
return null;
}
Future<Response_UserData?> getUserData(String username) async {
final get = ApplicationData_GetUserByUsername()..username = username;
final appData = ApplicationData()..getUserByUsername = get;

View file

@ -143,8 +143,8 @@ Future<void> handleContactUpdate(
groupId: Value(group.groupId),
type: const Value(GroupActionType.updatedContactUsername),
contactId: Value(fromUserId),
oldGroupName: Value('@${contact.username}'),
newGroupName: Value('@${contactUpdate.username}'),
oldGroupName: Value(contact.username),
newGroupName: Value(contactUpdate.username),
),
);
}
@ -157,7 +157,7 @@ Future<void> handleContactUpdate(
groupId: Value(group.groupId),
type: const Value(GroupActionType.updatedContactDisplayName),
contactId: Value(fromUserId),
oldGroupName: Value(contact.displayName ?? ''),
oldGroupName: Value(contact.displayName ?? contact.username),
newGroupName: Value(contactUpdate.displayName),
),
);

View file

@ -8,7 +8,7 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/utils/log.dart';
Future<void> handleGroupCreate(

View file

@ -8,7 +8,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
@ -22,8 +22,11 @@ Future<void> initFileDownloader() async {
if (update.task.taskId.contains('download_')) {
await handleDownloadStatusUpdate(update);
}
if (update.task.taskId.contains('backup')) {
await handleBackupStatusUpdate(update);
if (update.task.taskId.contains('backup_')) {
await BackupService.handleBackupStatusUpdate(
update.task.taskId,
update,
);
}
case TaskProgressUpdate():
Log.info(

View file

@ -69,6 +69,8 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
bool blocking = true,
bool useLock = true,
}) async {
if (apiService.appIsOutdated) return null;
try {
if (receiptId == null && receipt == null) return null;
if (receipt == null) {

View file

@ -28,7 +28,7 @@ import 'package:twonly/src/services/api/client2client/reaction.c2c.dart';
import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/services/key_verification.service.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';

View file

@ -118,7 +118,7 @@ Future<Map<String, String>?> getAuthenticationHeader() async {
var headers = <String, String>{};
if (userService.currentUser.canUseLoginTokenForAuth) {
final loginToken = await FlutterKeyManager.getLoginToken();
final loginToken = await RustKeyManager.getLoginToken();
headers = {
'x-twonly-user-id': userService.currentUser.userId

View file

@ -0,0 +1,361 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:background_downloader/background_downloader.dart';
import 'package:clock/clock.dart' as clock;
import 'package:http/http.dart' as http;
import 'package:mutex/mutex.dart';
import 'package:twonly/core/bridge.dart' as bridge;
import 'package:twonly/core/bridge/wrapper/backup.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/keyvalue.keys.dart';
import 'package:twonly/src/model/json/backup.model.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
class BackupService {
static final Mutex _protected = Mutex();
static String _getIdentityBackupUrl(String backupId) =>
'${apiService.apiEndpoint}/backup/identity/$backupId';
static String _getArchiveBackupUrl(String backupDownloadToken, int? userId) =>
'${apiService.apiEndpoint}/backup/archive/${userId == null ? '' : '${userId.toRadixString(16).padLeft(16, '0').toUpperCase()}/'}$backupDownloadToken';
static final _backupUpdateController = StreamController<void>.broadcast();
static Stream<void> get onBackupUpdated => _backupUpdateController.stream;
static Future<CurrentBackupStatus> getData() async {
return CurrentBackupStatus.fromJson(
(await KeyValueStore.get(KeyValueKeys.currentBackupState)) ??
CurrentBackupStatus().toJson(),
);
}
static Future<void> updateBackupPassword(String password) async {
// Set or reset the backup data...
await KeyValueStore.put(
KeyValueKeys.currentBackupState,
CurrentBackupStatus().toJson(),
);
_backupUpdateController.add(null);
await RustBackupIdentity.setBackupPasswordKeys(
password: password,
// Using the userId is this will never change in a users lifecycle
userId: userService.currentUser.userId,
);
await UserService.update((u) => u.isBackupEnabled = true);
unawaited(makeBackup(force: true));
}
static Future<void> handleBackupStatusUpdate(
String taskId,
TaskStatusUpdate update,
) async {
var status = LastBackupUploadState.success;
if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) {
status = LastBackupUploadState.failed;
} else if (update.status != TaskStatus.complete) {
Log.info('Backup is in state: ${update.status}');
return;
}
await _protected.protect(() async {
final backup = await getData();
if (taskId == 'backup_identity') {
backup
..identityLastSuccessFull = clock.clock.now()
..identityState = status;
} else {
backup
..archiveLastSuccessFull = clock.clock.now()
..archiveState = status;
}
await KeyValueStore.put(
KeyValueKeys.currentBackupState,
backup.toJson(),
);
_backupUpdateController.add(null);
});
}
static Future<void> makeBackup({bool force = false}) async {
await _protected.protect(() async {
final backup = await getData();
final lastDay = clock.clock.now().subtract(const Duration(days: 1));
final lastWeek = clock.clock.now().subtract(const Duration(days: 7));
if (force ||
backup.identityLastSuccessFull == null ||
(backup.identityState != LastBackupUploadState.pending &&
backup.identityLastSuccessFull!.isBefore(lastWeek) ||
backup.identityLastSuccessFull!.isBefore(
lastWeek.subtract(const Duration(days: 1)),
))) {
Log.info('Performing a identity backup.');
final encryptedBackup =
await RustBackupIdentity.getIdentityBackupBytes();
final backupTempFile = File(
'${AppEnvironment.cacheDir}/identity_backup.bin',
)..writeAsBytesSync(encryptedBackup);
Log.info(
'Identity backup has a size of ${backupTempFile.statSync().size}.',
);
final backupId = await RustBackupIdentity.getBackupId();
if (backupId == null) {
Log.error('Got empty backup id.');
backup.identityState = LastBackupUploadState.failed;
} else {
final task = UploadTask.fromFile(
taskId: 'backup_identity',
httpRequestMethod: 'PUT',
file: backupTempFile,
url: _getIdentityBackupUrl(backupId),
post: 'binary',
retries: 2,
headers: {
'Content-Type': 'application/octet-stream',
},
);
if (await FileDownloader().enqueue(task)) {
Log.info('Starting upload from backup identity.');
backup
..identityState = LastBackupUploadState.pending
..identityLastSuccessFull = clock.clock.now()
..identitySize = encryptedBackup.length;
await KeyValueStore.put(
KeyValueKeys.currentBackupState,
backup.toJson(),
);
_backupUpdateController.add(null);
} else {
Log.error('Error starting upload task for backup identity.');
}
}
}
if (force ||
backup.archiveLastSuccessFull == null ||
(backup.archiveState != LastBackupUploadState.pending &&
backup.archiveLastSuccessFull!.isBefore(lastDay) ||
backup.archiveLastSuccessFull!.isBefore(
lastDay.subtract(const Duration(days: 1)),
))) {
Log.info('Creating a archive backup.');
late final String backupArchive;
late final String backupDownloadToken;
try {
(backupDownloadToken, backupArchive) =
await RustBackupArchive.createBackupArchive();
} catch (e) {
Log.error(e);
return;
}
Log.info(
'Archive backup has a size of ${File(backupArchive).statSync().size}.',
);
final headers = await getAuthenticationHeader();
if (headers == null) {
Log.error('Auth headers are empty. Returning');
return;
}
final task = UploadTask.fromFile(
taskId: 'backup_archive',
file: File(backupArchive),
url: _getArchiveBackupUrl(backupDownloadToken, null),
priority: 0,
retries: 10,
headers: headers,
);
if (await FileDownloader().enqueue(task)) {
Log.info('Uploading backup archive.');
backup
..archiveState = LastBackupUploadState.pending
..archiveLastSuccessFull = clock.clock.now()
..archiveSize = File(backupArchive).statSync().size;
await KeyValueStore.put(
KeyValueKeys.currentBackupState,
backup.toJson(),
);
_backupUpdateController.add(null);
} else {
Log.error('Error starting upload task for backup archive.');
}
}
});
}
static Future<BackupRecovery?> getBackupRecoveryData() async {
final stateJson = await KeyValueStore.get(KeyValueKeys.backupRecoveryState);
if (stateJson == null) return null;
return BackupRecovery.fromJson(stateJson);
}
static Future<RecoveryError?> _nextBackupStage() async {
return _protected.protect(() async {
final recoveryData = await getBackupRecoveryData();
if (recoveryData == null) return null;
if (recoveryData.state == BackupRecoveryState.identityBackupStarted) {
// First start to download the identity to restore the KeyManager
final backupKeys = await RustBackupIdentity.getBackupPasswordKeys(
userId: recoveryData.userId,
password: recoveryData.password,
);
final backupId = uint8ListToHex(backupKeys.backupId);
final backupServerUrl = _getIdentityBackupUrl(backupId);
final (encryptedBytes, error) = await _downloadBackup(backupServerUrl);
if (error != null || encryptedBytes == null) {
Log.error(error);
return error;
}
Log.info('Restored identity.');
try {
await RustBackupIdentity.restoreIdentityBackup(
keys: backupKeys,
encryptedBytes: encryptedBytes,
);
recoveryData.state = BackupRecoveryState.archiveBackupStarted;
await KeyValueStore.put(
KeyValueKeys.backupRecoveryState,
recoveryData.toJson(),
);
_backupUpdateController.add(null);
} catch (e) {
Log.error(e);
return RecoveryError.unkownError;
}
}
if (recoveryData.state == BackupRecoveryState.archiveBackupStarted) {
// The KeyManager was restored sucessfully, restore the archive now.
try {
final downloadToken =
await RustBackupArchive.getBackupDownloadToken();
if (downloadToken == null) {
// identity was not restored correctly try this again.
recoveryData.state = BackupRecoveryState.identityBackupStarted;
await KeyValueStore.put(
KeyValueKeys.backupRecoveryState,
recoveryData.toJson(),
);
return RecoveryError.tryAgainLater;
}
final backupServerUrl = _getArchiveBackupUrl(
downloadToken,
recoveryData.userId,
);
final backupArchive = await _downloadBackup(backupServerUrl);
if (backupArchive.$2 != null || backupArchive.$1 == null) {
return backupArchive.$2;
}
final archiveFile = File('${AppEnvironment.cacheDir}/archive.bin')
..writeAsBytesSync(backupArchive.$1!);
await RustBackupArchive.restoreBackupArchive(
filePath: archiveFile.path,
);
await UserService.update((u) {
u.deviceId += 1;
});
await KeyValueStore.delete(
KeyValueKeys.backupRecoveryState,
);
} catch (e) {
Log.error(e);
return RecoveryError.unkownError;
}
}
return null;
});
}
static Future<RecoveryError?> startFullBackupRecovery(
String username,
String password,
) async {
final userId = await apiService.getUserIdFromUsername(username);
if (userId == null) {
return RecoveryError.usernameNotValid;
}
final state = BackupRecovery(
username: username,
userId: userId,
password: password,
);
await deleteLocalUserData();
try {
await bridge.initializeTwonlyFlutter(
config: bridge.InitConfig(
databaseDir: AppEnvironment.supportDir,
dataDir: AppEnvironment.supportDir,
),
);
} catch (e) {
Log.error(e);
return RecoveryError.unkownError;
}
await KeyValueStore.put(KeyValueKeys.backupRecoveryState, state.toJson());
return _nextBackupStage();
}
static Future<(Uint8List?, RecoveryError?)> _downloadBackup(
String backupServerUrl,
) async {
late http.Response response;
try {
response = await http.get(
Uri.parse(backupServerUrl),
headers: {
HttpHeaders.acceptHeader: 'application/octet-stream',
},
);
} catch (e) {
Log.error('Error fetching backup: $e');
return (null, RecoveryError.noInternet);
}
Log.warn('Backup downlaod status: ${response.statusCode}');
switch (response.statusCode) {
case 200:
return (response.bodyBytes, null);
case 404:
return (null, RecoveryError.passwordInvalid);
default:
return (null, RecoveryError.tryAgainLater);
}
}
}
enum RecoveryError {
usernameNotValid,
passwordInvalid,
tryAgainLater,
noInternet,
unkownError,
}

View file

@ -1,89 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:hashlib/hashlib.dart';
import 'package:http/http.dart' as http;
import 'package:twonly/locator.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
Future<void> enableTwonlySafe(String password) async {
final (backupId, encryptionKey) = await getMasterKey(
password,
userService.currentUser.username,
);
await UserService.update((user) {
user.twonlySafeBackup = TwonlySafeBackup(
encryptionKey: encryptionKey,
backupId: backupId,
);
});
unawaited(performTwonlySafeBackup(force: true));
}
Future<void> removeTwonlySafeFromServer() async {
final serverUrl = getTwonlySafeBackupUrl();
if (serverUrl == null) {
Log.error('Could not remove twonly safe as serverUrl is null');
return;
}
try {
final response = await http.delete(
Uri.parse(serverUrl),
headers: {
'Content-Type': 'application/json', // Set the content type if needed
// Add any other headers if required
},
);
Log.info('Download deleted with: ${response.statusCode}');
} catch (e) {
Log.error('Could not connect upload the backup.');
}
}
Future<(Uint8List, Uint8List)> getMasterKey(
String password,
String username,
) async {
final List<int> passwordBytes = utf8.encode(password);
final List<int> saltBytes = utf8.encode(username);
// Values are derived from the Threema Whitepaper
// https://threema.com/assets/documents/cryptography_whitepaper.pdf
final scrypt = Scrypt(
cost: 65536,
salt: saltBytes,
);
final key = scrypt.convert(passwordBytes).bytes;
return (key.sublist(0, 32), key.sublist(32, 64));
}
String? getTwonlySafeBackupUrl() {
if (userService.currentUser.twonlySafeBackup == null) return null;
return getTwonlySafeBackupUrlFromServer(
userService.currentUser.twonlySafeBackup!.backupId,
userService.currentUser.backupServer,
);
}
String? getTwonlySafeBackupUrlFromServer(
List<int> backupId,
BackupServer? backupServer,
) {
var backupServerUrl = 'https://safe.twonly.eu/';
if (backupServer != null) {
backupServerUrl = backupServer.serverUrl;
}
final backupIdHex = uint8ListToHex(backupId).toLowerCase();
return '${backupServerUrl}backups/$backupIdHex';
}

View file

@ -1,238 +0,0 @@
// ignore_for_file: parameter_assignments
import 'dart:convert';
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:clock/clock.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path/path.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/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/secure_storage.dart';
Future<void> performTwonlySafeBackup({bool force = false}) async {
if (userService.currentUser.twonlySafeBackup == null) {
return;
}
if (userService.currentUser.twonlySafeBackup!.backupUploadState ==
LastBackupUploadState.pending) {
Log.warn('Backup upload is already pending.');
return;
}
final lastUpdateTime =
userService.currentUser.twonlySafeBackup!.lastBackupDone;
if (!force && lastUpdateTime != null) {
if (lastUpdateTime.isAfter(clock.now().subtract(const Duration(days: 1)))) {
return;
}
}
Log.info('Starting new twonly Backup!');
final backupDir = Directory(
join(AppEnvironment.supportDir, 'backup_twonly_safe/'),
);
await backupDir.create(recursive: true);
final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite'));
final backupDatabaseFileCleaned = File(
join(backupDir.path, 'twonly.backup.cleaned.sqlite'),
);
// copy database
final originalDatabase = File(
join(AppEnvironment.supportDir, 'twonly.sqlite'),
);
await originalDatabase.copy(backupDatabaseFile.path);
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
final backupDB = TwonlyDB(
driftDatabase(
name: 'twonly.backup',
native: DriftNativeOptions(
databaseDirectory: () async {
return backupDir;
},
),
),
);
await backupDB.deleteDataForTwonlySafe();
await backupDB.customStatement('VACUUM INTO ?', [
backupDatabaseFileCleaned.path,
]);
await backupDB.printTableSizes();
await backupDB.close();
// ignore: inference_failure_on_collection_literal
final secureStorageBackup = {};
secureStorageBackup[SecureStorageKeys.signalIdentity] = await SecureStorage
.instance
.read(
key: SecureStorageKeys.signalIdentity,
);
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
await SecureStorage.instance.read(
key: SecureStorageKeys.signalSignedPreKey,
);
final userBackup = await UserService.getUser();
if (userBackup == null) return;
// FILTER settings which should not be in the backup
userBackup
..twonlySafeBackup = null
..lastImageSend = null
..todaysImageCounter = null
..lastPlanBallance = ''
..additionalUserInvites = ''
..signalLastSignedPreKeyUpdated = null;
secureStorageBackup[SecureStorageKeys.userData] = jsonEncode(userBackup);
// Compress and convert backup data
final twonlyDatabaseBytes = await backupDatabaseFileCleaned.readAsBytes();
await backupDatabaseFile.delete();
await backupDatabaseFileCleaned.delete();
Log.info('twonlyDatabaseLength = ${twonlyDatabaseBytes.lengthInBytes}');
Log.info('secureStorageLength = ${jsonEncode(secureStorageBackup).length}');
final backupProto = TwonlySafeBackupContent(
secureStorageJson: jsonEncode(secureStorageBackup),
twonlyDatabase: twonlyDatabaseBytes,
);
final backupBytes = gzip.encode(backupProto.writeToBuffer());
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
if (userService.currentUser.twonlySafeBackup!.lastBackupDone == null ||
userService.currentUser.twonlySafeBackup!.lastBackupDone!.isAfter(
clock.now().subtract(const Duration(days: 90)),
)) {
force = true;
}
final lastHash = await SecureStorage.instance.read(
key: SecureStorageKeys.twonlySafeLastBackupHash,
);
if (lastHash != null && !force) {
if (backupHash == lastHash) {
Log.info('Since last backup nothing has changed.');
return;
}
}
await SecureStorage.instance.write(
key: SecureStorageKeys.twonlySafeLastBackupHash,
value: backupHash,
);
// Encrypt backup data
final chacha20 = FlutterChacha20.poly1305Aead();
final nonce = chacha20.newNonce();
final secretBox = await chacha20.encrypt(
backupBytes,
secretKey: SecretKey(
userService.currentUser.twonlySafeBackup!.encryptionKey,
),
nonce: nonce,
);
final encryptedBackupBytes = TwonlySafeBackupEncrypted(
mac: secretBox.mac.bytes,
nonce: nonce,
cipherText: secretBox.cipherText,
).writeToBuffer();
Log.info('Backup files created.');
final encryptedBackupBytesFile = File(
join(backupDir.path, 'twonly_safe.backup'),
);
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
Log.info(
'Create twonly Backup with a size of ${encryptedBackupBytes.length} bytes.',
);
if (userService.currentUser.backupServer != null) {
if (encryptedBackupBytes.length >
userService.currentUser.backupServer!.maxBackupBytes) {
Log.error('Backup is to big for the alternative backup server.');
await UserService.update((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
});
return;
}
}
final task = UploadTask.fromFile(
taskId: 'backup',
file: encryptedBackupBytesFile,
httpRequestMethod: 'PUT',
url: getTwonlySafeBackupUrl()!,
post: 'binary',
retries: 2,
headers: {
'Content-Type': 'application/octet-stream',
},
);
if (await FileDownloader().enqueue(task)) {
Log.info('Starting upload from twonly Backup.');
await UserService.update((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending;
user.twonlySafeBackup!.lastBackupDone = clock.now();
user.twonlySafeBackup!.lastBackupSize = encryptedBackupBytes.length;
});
} else {
Log.error('Error starting UploadTask for twonly Backup.');
}
}
Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async {
if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) {
await UserService.update((user) {
if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
}
});
} else if (update.status == TaskStatus.complete) {
Log.info(
'twonly Backup uploaded with status code ${update.responseStatusCode}',
);
await UserService.update((user) {
if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState =
LastBackupUploadState.success;
}
});
} else {
Log.info('Backup is in state: ${update.status}');
return;
}
}

View file

@ -1,117 +0,0 @@
// ignore_for_file: avoid_dynamic_calls
import 'dart:convert';
import 'dart:io';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart';
Future<void> recoverBackup(
String username,
String password,
BackupServer? server,
) async {
final (backupId, encryptionKey) = await getMasterKey(password, username);
final backupServerUrl = getTwonlySafeBackupUrlFromServer(backupId, server);
if (backupServerUrl == null) {
Log.error('Could not create backup url');
throw Exception('Could not create backup server url');
}
late Uint8List backupData;
late http.Response response;
try {
response = await http.get(
Uri.parse(backupServerUrl),
headers: {
HttpHeaders.acceptHeader: 'application/octet-stream',
},
);
} catch (e) {
Log.error('Error fetching backup: $e');
throw Exception('Backup server could not be reached. ($e)');
}
switch (response.statusCode) {
case 200:
backupData = response.bodyBytes;
case 400:
throw Exception('Bad Request: Validation failed.');
case 404:
throw Exception('No backup was found.');
case 429:
throw Exception('Too Many Requests: Rate limit reached.');
default:
throw Exception('Unexpected error: ${response.statusCode}');
}
return handleBackupData(encryptionKey, backupData);
}
Future<void> handleBackupData(
Uint8List encryptionKey,
Uint8List backupData,
) async {
final encryptedBackup = TwonlySafeBackupEncrypted.fromBuffer(
backupData,
);
final secretBox = SecretBox(
encryptedBackup.cipherText,
nonce: encryptedBackup.nonce,
mac: Mac(encryptedBackup.mac),
);
final compressedBytes = await FlutterChacha20.poly1305Aead().decrypt(
secretBox,
secretKey: SecretKeyData(encryptionKey),
);
final plaintextBytes = gzip.decode(compressedBytes);
final backupContent = TwonlySafeBackupContent.fromBuffer(
plaintextBytes,
);
final originalDatabase = File(
join(AppEnvironment.supportDir, 'twonly.sqlite'),
);
// in case there was only a secure storage error, do not replace the original database
if (!originalDatabase.existsSync()) {
await originalDatabase.writeAsBytes(backupContent.twonlyDatabase);
}
const storage = SecureStorage.instance;
final secureStorage = jsonDecode(backupContent.secureStorageJson);
await storage.write(
key: SecureStorageKeys.signalIdentity,
value: secureStorage[SecureStorageKeys.signalIdentity] as String,
);
await storage.write(
key: SecureStorageKeys.signalSignedPreKey,
value: secureStorage[SecureStorageKeys.signalSignedPreKey] as String,
);
final userDataMap = jsonDecode(secureStorage[SecureStorageKeys.userData] as String) as Map<String, dynamic>;
final userData = UserData.fromJson(userDataMap);
await UserService.save(userData);
await UserService.update((u) {
u.deviceId += 1;
});
}

View file

@ -6,11 +6,9 @@ import 'dart:io' show Platform;
import 'package:firebase_app_installations/firebase_app_installations.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/user.service.dart';
@ -21,11 +19,8 @@ import '../../../firebase_options.dart';
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
Future<void> checkForTokenUpdates() async {
const storage = FlutterSecureStorage();
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
try {
if (!userService.isUserCreated) return;
if (Platform.isIOS) {
var apnsToken = await FirebaseMessaging.instance.getAPNSToken();
for (var i = 0; i < 20; i++) {
@ -47,23 +42,22 @@ Future<void> checkForTokenUpdates() async {
Log.info('Loaded FCM token.');
if (storedToken == null || fcmToken != storedToken) {
Log.info('Got new FCM TOKEN.');
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
if (userService.currentUser.fcmToken == null ||
fcmToken != userService.currentUser.fcmToken) {
Log.info('Got new FCM token.');
await UserService.update((u) {
u.updateFCMToken = true;
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
}
FirebaseMessaging.instance.onTokenRefresh
.listen((fcmToken) async {
Log.info('Got new FCM TOKEN.');
await storage.write(
key: SecureStorageKeys.googleFcm,
value: fcmToken,
);
await UserService.update((u) {
u.updateFCMToken = true;
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
})
.onError((err) {
@ -75,21 +69,23 @@ Future<void> checkForTokenUpdates() async {
}
Future<void> initFCMAfterAuthenticated({bool force = false}) async {
final fcmToken = userService.currentUser.fcmToken;
if (userService.currentUser.updateFCMToken || force) {
const storage = FlutterSecureStorage();
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
if (storedToken != null) {
final res = await apiService.updateFCMToken(storedToken);
if (res.isSuccess) {
Log.info('Uploaded new FCM token!');
await UserService.update((u) {
u.updateFCMToken = false;
});
} else {
Log.error('Could not update FCM token!');
}
if (fcmToken == null) {
Log.error('FCM token could not be updated as it is empty');
await checkForTokenUpdates();
return;
}
final res = await apiService.updateFCMToken(
fcmToken,
);
if (res.isSuccess) {
Log.info('Uploaded new FCM token!');
await UserService.update((u) {
u.updateFCMToken = false;
});
} else {
Log.error('Could not send FCM update to server as token is empty.');
Log.error('Could not update FCM token!');
}
}
}
@ -99,7 +95,7 @@ Future<void> resetFCMTokens() async {
Log.info('Firebase Installation successfully deleted.');
await FirebaseMessaging.instance.deleteToken();
Log.info('Old FCM deleted.');
await const FlutterSecureStorage().delete(key: SecureStorageKeys.googleFcm);
await UserService.update((u) => u.fcmToken = null);
await checkForTokenUpdates();
await initFCMAfterAuthenticated(force: true);
}

View file

@ -1,59 +1,51 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:clock/clock.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.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/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart';
Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
final signalIdentity = await getSignalIdentity();
if (signalIdentity == null) return null;
return IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List);
}
// This function runs after the clients authenticated with the server.
// It then checks if it should update a new session key
Future<void> signalHandleNewServerConnection() async {
if (userService.currentUser.signalLastSignedPreKeyUpdated != null) {
final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48));
final isYoungerThan48Hours =
(userService.currentUser.signalLastSignedPreKeyUpdated!).isAfter(
fortyEightHoursAgo,
);
if (isYoungerThan48Hours) {
// The key does live for 48 hours then it expires and a new key is generated.
class SignalIdentityService {
static Future<void> onAuthenticated() async {
if (userService.currentUser.signalLastSignedPreKeyUpdated != null) {
final fortyEightHoursAgo = clock.now().subtract(
const Duration(hours: 48),
);
final isYoungerThan48Hours =
(userService.currentUser.signalLastSignedPreKeyUpdated!).isAfter(
fortyEightHoursAgo,
);
if (isYoungerThan48Hours) {
// The key does live for 48 hours then it expires and a new key is generated.
return;
}
}
final signedPreKey = await _getNewSignalSignedPreKey();
if (signedPreKey == null) {
Log.error('could not generate a new signed pre key!');
return;
}
}
final signedPreKey = await _getNewSignalSignedPreKey();
if (signedPreKey == null) {
Log.error('could not generate a new signed pre key!');
return;
}
await UserService.update((user) {
user.signalLastSignedPreKeyUpdated = clock.now();
});
final res = await apiService.updateSignedPreKey(
signedPreKey.id,
signedPreKey.getKeyPair().publicKey.serialize(),
signedPreKey.signature,
);
if (res.isError) {
Log.error('could not update the signed pre key: ${res.error}');
await UserService.update((user) {
user.signalLastSignedPreKeyUpdated = null;
user.signalLastSignedPreKeyUpdated = clock.now();
});
} else {
Log.info('updated signed pre key');
final res = await apiService.updateSignedPreKey(
signedPreKey.id,
signedPreKey.getKeyPair().publicKey.serialize(),
signedPreKey.signature,
);
if (res.isError) {
Log.error('could not update the signed pre key: ${res.error}');
await UserService.update((user) {
user.signalLastSignedPreKeyUpdated = null;
});
} else {
Log.info('updated signed pre key');
}
}
}
@ -75,64 +67,45 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
Future<SignalIdentity?> getSignalIdentity() async {
try {
var signalIdentityJson = await SecureStorage.instance.read(
key: SecureStorageKeys.signalIdentity,
final identity = await RustKeyManager.getSignalIdentity();
return SignalIdentity(
identityKeyPairU8List: identity.$1,
registrationId: identity.$2,
);
if (signalIdentityJson == null) {
return null;
}
final decoded = jsonDecode(signalIdentityJson);
signalIdentityJson = null;
return SignalIdentity.fromJson(decoded as Map<String, dynamic>);
} 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 {
Log.info('getUserPublicKey: getting identity');
final signalIdentity = (await getSignalIdentity())!;
Log.info('getUserPublicKey: getting signal store');
final signalStore = await getSignalStoreFromIdentity(signalIdentity);
Log.info('getUserPublicKey: getting key pair');
final keyPair = await signalStore.getIdentityKeyPair();
Log.info('getUserPublicKey: serializing public key');
return keyPair.getPublicKey().serialize();
}
Future<void> createIfNotExistsSignalIdentity() async {
final signalIdentity = await SecureStorage.instance.read(
key: SecureStorageKeys.signalIdentity,
);
if (signalIdentity != null) {
return;
}
// check if identity already exists
if (await getSignalIdentity() != null) return;
final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(true);
final signalStore = SignalSignalProtocolStore(
identityKeyPair,
registrationId,
);
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
final signedPreKeyStore = <int, Uint8List>{};
signedPreKeyStore[signedPreKey.id] = signedPreKey.serialize();
await signalStore.signedPreKeyStore.storeSignedPreKey(
signedPreKey.id,
signedPreKey,
);
final storedSignalIdentity = SignalIdentity(
identityKeyPairU8List: identityKeyPair.serialize(),
await RustKeyManager.importSignalIdentity(
identityKeyPairStructure: identityKeyPair.serialize(),
registrationId: registrationId,
);
await SecureStorage.instance.write(
key: SecureStorageKeys.signalIdentity,
value: jsonEncode(storedSignalIdentity),
signedPreKeyStore: signedPreKeyStore,
);
}

View file

@ -5,9 +5,9 @@ import 'package:mutex/mutex.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart';
import 'package:twonly/src/utils/keyvalue.dart';
class UserService {
late UserData currentUser;

View file

@ -32,31 +32,33 @@ class KeyValueStore {
}
});
static Future<Map<String, dynamic>?> get(String key) =>
_exclusive(key, () async {
final file = await _getFilePath(key);
try {
if (file.existsSync()) {
final contents = await file.readAsString();
return jsonDecode(contents) as Map<String, dynamic>;
} else {
return null;
}
} catch (e) {
Log.warn('Error reading file. Deleting it.: $e');
file.deleteSync();
static Future<Map<String, dynamic>?> get(String key) async {
return _exclusive(key, () async {
final file = await _getFilePath(key);
try {
if (file.existsSync()) {
final contents = await file.readAsString();
return jsonDecode(contents) as Map<String, dynamic>;
} else {
return null;
}
});
} catch (e) {
Log.warn('Error reading file. Deleting it.: $e');
file.deleteSync();
return null;
}
});
}
static Future<void> put(String key, Map<String, dynamic> value) =>
_exclusive(key, () async {
try {
final file = await _getFilePath(key);
await file.parent.create(recursive: true);
await file.writeAsString(jsonEncode(value));
} catch (e) {
Log.error('Error writing file: $e');
}
});
static Future<void> put(String key, Map<String, dynamic> value) async {
return _exclusive(key, () async {
try {
final file = await _getFilePath(key);
await file.parent.create(recursive: true);
await file.writeAsString(jsonEncode(value));
} catch (e) {
Log.error('Error writing file: $e');
}
});
}
}

View file

@ -197,12 +197,12 @@ String formatDateTime(BuildContext context, DateTime? dateTime) {
}
}
String formatBytes(int bytes, {int decimalPlaces = 2}) {
String formatBytes(int bytes) {
if (bytes <= 0) return '0 Bytes';
const units = <String>['Bytes', 'KB', 'MB', 'GB', 'TB'];
final unitIndex = (log(bytes) / log(1000)).floor();
final formattedSize = bytes / pow(1000, unitIndex);
return '${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}';
return '${formattedSize.ceil()} ${units[unitIndex]}';
}
bool isUUIDNewer(String uuid1, String uuid2) {

View file

@ -9,7 +9,7 @@ import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
import 'package:twonly/src/visual/views/groups/group.view.dart';

View file

@ -0,0 +1,258 @@
import 'dart:async';
import 'package:flutter/material.dart';
enum SnackbarLevel {
info,
success,
warning,
error,
}
void showSnackbar(
BuildContext context,
String message, {
SnackbarLevel level = SnackbarLevel.error,
}) {
Color backgroundColor;
IconData iconData;
switch (level) {
case SnackbarLevel.info:
backgroundColor = Colors.blue.shade700;
iconData = Icons.info_outline;
case SnackbarLevel.success:
backgroundColor = Colors.green.shade700;
iconData = Icons.check_circle_outline;
case SnackbarLevel.warning:
backgroundColor = Colors.orange.shade800;
iconData = Icons.warning_amber_rounded;
case SnackbarLevel.error:
backgroundColor = Colors.red.shade700;
iconData = Icons.error_outline;
}
AnimationController? localAnimationController;
_showOverlay(
context: context,
animationDuration: const Duration(milliseconds: 1000),
reverseAnimationDuration: const Duration(milliseconds: 350),
displayDuration: const Duration(milliseconds: 3000),
onAnimationControllerInit: (controller) =>
localAnimationController = controller,
child: _SnackbarWidget(
message: message,
backgroundColor: backgroundColor,
icon: Icon(iconData, color: Colors.white, size: 28),
onCloseClick: () {
localAnimationController?.reverse();
},
),
);
}
OverlayEntry? _previousEntry;
void _showOverlay({
required BuildContext context,
required Widget child,
required Duration animationDuration,
required Duration reverseAnimationDuration,
required Duration displayDuration,
required void Function(AnimationController) onAnimationControllerInit,
}) {
final overlayState = Overlay.maybeOf(context);
if (overlayState == null) return;
late OverlayEntry overlayEntry;
overlayEntry = OverlayEntry(
builder: (_) => _AnimatedSnackbar(
animationDuration: animationDuration,
reverseAnimationDuration: reverseAnimationDuration,
displayDuration: displayDuration,
onAnimationControllerInit: onAnimationControllerInit,
onDismissed: () {
if (overlayEntry.mounted) {
overlayEntry.remove();
}
if (_previousEntry == overlayEntry) {
_previousEntry = null;
}
},
child: child,
),
);
if (_previousEntry != null && _previousEntry!.mounted) {
_previousEntry?.remove();
}
overlayState.insert(overlayEntry);
_previousEntry = overlayEntry;
}
class _SnackbarWidget extends StatelessWidget {
const _SnackbarWidget({
required this.message,
required this.backgroundColor,
required this.icon,
required this.onCloseClick,
});
final String message;
final Color backgroundColor;
final Icon icon;
final VoidCallback onCloseClick;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
clipBehavior: Clip.hardEdge,
constraints: const BoxConstraints(minHeight: 70),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: const [
BoxShadow(
color: Colors.black26,
spreadRadius: 1,
blurRadius: 30,
),
],
),
width: double.infinity,
child: Row(
children: [
const SizedBox(width: 16),
icon,
const SizedBox(width: 12),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
message,
style: theme.textTheme.bodyMedium?.merge(
const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white,
),
),
textAlign: TextAlign.start,
),
),
),
GestureDetector(
onTap: onCloseClick,
behavior: HitTestBehavior.opaque,
child: const Padding(
padding: EdgeInsets.all(16),
child: Icon(Icons.close, color: Colors.white70, size: 20),
),
),
],
),
);
}
}
class _AnimatedSnackbar extends StatefulWidget {
const _AnimatedSnackbar({
required this.child,
required this.onDismissed,
required this.animationDuration,
required this.reverseAnimationDuration,
required this.displayDuration,
required this.onAnimationControllerInit,
});
final Widget child;
final VoidCallback onDismissed;
final Duration animationDuration;
final Duration reverseAnimationDuration;
final Duration displayDuration;
final void Function(AnimationController) onAnimationControllerInit;
@override
State<_AnimatedSnackbar> createState() => _AnimatedSnackbarState();
}
class _AnimatedSnackbarState extends State<_AnimatedSnackbar>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
late final Animation<Offset> _offsetAnimation;
Timer? _timer;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: widget.animationDuration,
reverseDuration: widget.reverseAnimationDuration,
);
_animationController.addStatusListener(_handleAnimationStatus);
widget.onAnimationControllerInit(_animationController);
_offsetAnimation =
Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.elasticOut,
reverseCurve: Curves.linearToEaseOut,
),
);
_animationController.forward();
}
void _handleAnimationStatus(AnimationStatus status) {
if (status == AnimationStatus.completed) {
_timer = Timer(widget.displayDuration, () {
if (mounted) {
_animationController.reverse();
}
});
} else if (status == AnimationStatus.dismissed) {
_timer?.cancel();
widget.onDismissed();
}
}
@override
void dispose() {
_animationController.dispose();
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Positioned(
top: 16,
left: 16,
right: 16,
child: SlideTransition(
position: _offsetAnimation,
child: SafeArea(
child: Dismissible(
key: UniqueKey(),
direction: DismissDirection.up,
dismissThresholds: const {DismissDirection.up: 0.2},
confirmDismiss: (_) async {
if (mounted) {
await _animationController.reverse();
}
return false;
},
child: widget.child,
),
),
),
);
}
}

View file

@ -5,6 +5,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/qr.utils.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -46,16 +47,19 @@ class CameraScannedOverlay extends StatelessWidget {
onTap: () async {
c.isLoading = true;
mainController.setState();
showSnackbar(
context,
context.lang.requestedUserToastText(c.profile.username),
level: SnackbarLevel.success,
);
if (await addNewContactFromPublicProfile(c.profile) &&
context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.lang.requestedUserToastText(c.profile.username),
),
duration: const Duration(seconds: 8),
),
);
// showSnackbar(
// context,
// context.lang.requestedUserToastText(c.profile.username),
// level: SnackbarLevel.success,
// );
}
},
child: Container(

View file

@ -19,6 +19,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart';
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
@ -254,14 +255,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
await File(picture.path).delete();
return imageBytes;
} catch (e) {
if (context.mounted) {
// ignore: use_build_context_synchronously
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error loading picture: $e'),
duration: const Duration(seconds: 3),
),
if (mounted) {
showSnackbar(
context,
'Error loading picture: $e',
);
Log.error(e);
}
return null;
}
@ -606,17 +605,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
void _showCameraException(dynamic e) {
Log.error('$e');
try {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: $e'),
duration: const Duration(seconds: 3),
),
);
}
// ignore: empty_catches
} catch (e) {}
if (mounted) showSnackbar(context, 'Error: $e');
}
@override

View file

@ -17,6 +17,7 @@ import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
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/snackbar.dart';
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/face_filters.dart';
@ -373,18 +374,14 @@ class MainCameraController {
);
await HapticFeedback.heavyImpact();
if (verificationOk) {
AppGlobalKeys.scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text(
AppGlobalKeys.scaffoldMessengerKey.currentContext?.lang
.verifiedPublicKey(
getContactDisplayName(contact),
) ??
'',
),
duration: const Duration(seconds: 6),
final context = cameraPreviewKey.currentContext;
if (verificationOk && context != null && context.mounted) {
showSnackbar(
context,
context.lang.verifiedPublicKey(
getContactDisplayName(contact),
),
level: SnackbarLevel.success,
);
}
}

View file

@ -21,6 +21,7 @@ import 'package:twonly/src/visual/themes/light.dart';
import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart';
import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart';
import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart';
import 'package:twonly/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart';
class ChatListView extends StatefulWidget {
const ChatListView({super.key});
@ -215,6 +216,7 @@ class _ChatListViewState extends State<ChatListView> {
child: Column(
children: [
const FinishSetupComp(),
const MissingBackupComp(),
if (_groupsNotPinned.isEmpty &&
_groupsPinned.isEmpty &&
_groupsArchived.isEmpty)

View file

@ -125,44 +125,6 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
}
}
// switch (widget.action.type) {
// case GroupActionType.updatedGroupName:
// text = (contact == null)
// ? 'You have changed the group name to "${widget.action.newGroupName}".'
// : '$maker has changed the group name to "${widget.action.newGroupName}".';
// icon = FontAwesomeIcons.pencil;
// case GroupActionType.createdGroup:
// icon = FontAwesomeIcons.penToSquare;
// text = (contact == null)
// ? 'You have created the group.'
// : '$maker has created the group.';
// case GroupActionType.removedMember:
// icon = FontAwesomeIcons.userMinus;
// text = (contact == null)
// ? 'You have removed $affected from the group.'
// : '$maker has removed $affected from the group.';
// case GroupActionType.addMember:
// icon = FontAwesomeIcons.userPlus;
// text = (contact == null)
// ? 'You have added $affected to the group.'
// : '$maker has added $affected to the group.';
// case GroupActionType.promoteToAdmin:
// icon = FontAwesomeIcons.key;
// text = (contact == null)
// ? 'You made $affected an admin.'
// : '$maker made $affected an admin.';
// case GroupActionType.demoteToMember:
// icon = FontAwesomeIcons.key;
// text = (contact == null)
// ? 'You revoked $affectedR admin rights.'
// : '$maker revoked $affectedR admin rights.';
// case GroupActionType.leftGroup:
// icon = FontAwesomeIcons.userMinus;
// text = (contact == null)
// ? 'You have left the group.'
// : '$maker has left the group.';
// }
return Padding(
padding: const EdgeInsets.all(8),
child: Center(

View file

@ -10,8 +10,10 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart
as server;
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
import 'package:twonly/src/services/api/utils.api.dart';
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/snackbar.dart';
class AddContactViaQrLinkView extends StatefulWidget {
const AddContactViaQrLinkView({
@ -69,11 +71,8 @@ class _AddContactViaQrLinkViewState extends State<AddContactViaQrLinkView> {
context.pop();
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
if (mounted) showSnackbar(context, 'Error: $e');
Log.error(e);
} finally {
if (mounted) {
setState(() {

View file

@ -16,6 +16,7 @@ import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart';
@ -102,12 +103,7 @@ class _ContactViewState extends State<ContactView> {
if (!mounted) return;
if (!delete) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.deleteUserErrorMessage),
duration: const Duration(seconds: 8),
),
);
showSnackbar(context, context.lang.deleteUserErrorMessage);
return;
}
@ -157,11 +153,10 @@ class _ContactViewState extends State<ContactView> {
final res = await apiService.reportUser(contact.userId, reason);
if (!mounted) return;
if (res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.userGotReported),
duration: const Duration(seconds: 3),
),
showSnackbar(
context,
context.lang.userGotReported,
level: SnackbarLevel.info,
);
} else {
showNetworkIssue(context);

View file

@ -7,12 +7,13 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/group.service.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';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/components/verification_badge.comp.dart';
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
import 'package:twonly/src/visual/views/contact/contact.view.dart';
@ -343,10 +344,8 @@ Future<String?> showGroupNameChangeDialog(
}
void showNetworkIssue(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.groupNetworkIssue),
duration: const Duration(seconds: 3),
),
showSnackbar(
context,
context.lang.groupNetworkIssue,
);
}

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';

View file

@ -11,6 +11,7 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/context_menu/user.context_menu.dart';
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
import 'package:twonly/src/visual/views/groups/group_create_select_group_name.view.dart';
@ -88,12 +89,7 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
if (alreadyInGroup.contains(userId)) return;
if (!selectedUsers.contains(userId)) {
if (selectedUsers.length + alreadyInGroup.length > 256) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.groupSizeLimitError(256)),
duration: const Duration(seconds: 3),
),
);
showSnackbar(context, context.lang.groupSizeLimitError(256));
return;
}
selectedUsers.add(userId);

View file

@ -9,9 +9,10 @@ import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
import 'package:twonly/src/visual/views/groups/group.view.dart';
@ -107,11 +108,10 @@ class GroupMemberContextMenu extends StatelessWidget {
),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.contactRequestSend),
duration: const Duration(seconds: 3),
),
showSnackbar(
context,
context.lang.contactRequestSend,
level: SnackbarLevel.success,
);
}
}

View file

@ -1,13 +1,11 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:restart_app/restart_app.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/services/backup/restore.backup.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
import 'package:twonly/src/visual/views/settings/backup/backup_server.view.dart';
class BackupRecoveryView extends StatefulWidget {
const BackupRecoveryView({super.key});
@ -19,7 +17,6 @@ class BackupRecoveryView extends StatefulWidget {
class _BackupRecoveryViewState extends State<BackupRecoveryView> {
bool obscureText = true;
bool isLoading = false;
BackupServer? backupServer;
final TextEditingController usernameCtrl = TextEditingController();
final TextEditingController passwordCtrl = TextEditingController();
@ -28,31 +25,38 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
isLoading = true;
});
try {
await recoverBackup(
usernameCtrl.text,
passwordCtrl.text,
backupServer,
);
final error = await BackupService.startFullBackupRecovery(
usernameCtrl.text,
passwordCtrl.text,
);
if (!mounted) return;
await Restart.restartApp(
notificationTitle: 'Backup successfully recovered.',
notificationBody: 'Click here to open the app again',
forceKill: true,
);
} catch (e) {
// in case something was already written from the backup...
Log.error('$e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$e'),
duration: const Duration(seconds: 3),
),
);
if (error != null) {
String errorMessage;
switch (error) {
case RecoveryError.noInternet:
errorMessage = context.lang.recoverErrorNoInternet;
case RecoveryError.usernameNotValid:
errorMessage = context.lang.recoverErrorUsernameNotValid;
case RecoveryError.passwordInvalid:
errorMessage = context.lang.recoverErrorPasswordInvalid;
case RecoveryError.tryAgainLater:
errorMessage = context.lang.recoverErrorTryAgainLater;
case RecoveryError.unkownError:
errorMessage = context.lang.recoverErrorUnknown;
}
setState(() {
isLoading = false;
});
return showSnackbar(context, errorMessage);
}
await Restart.restartApp(
notificationTitle: context.lang.recoverSuccessTitle,
notificationBody: context.lang.recoverSuccessBody,
forceKill: true,
);
setState(() {
isLoading = false;
});
@ -135,20 +139,6 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
),
],
),
const SizedBox(height: 30),
Center(
child: OutlinedButton(
onPressed: () async {
backupServer =
await context.navPush(
const BackupServerView(),
)
as BackupServer?;
setState(() {});
},
child: Text(context.lang.backupExpertSettings),
),
),
const SizedBox(height: 10),
Center(
child: FilledButton.icon(

View file

@ -1,9 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
@ -19,16 +17,16 @@ class BackupSetupPage extends StatefulWidget {
}
class _BackupSetupPageState extends State<BackupSetupPage> {
bool isLoading = false;
final TextEditingController passwordCtrl = TextEditingController();
final TextEditingController repeatedPasswordCtrl = TextEditingController();
bool _isLoading = false;
final TextEditingController _passwordCtrl = TextEditingController();
final TextEditingController _repeatedPasswordCtrl = TextEditingController();
Future<bool> onPressedEnableTwonlySafe() async {
setState(() {
isLoading = true;
_isLoading = true;
});
if (!await isSecurePassword(passwordCtrl.text)) {
if (!await isSecurePassword(_passwordCtrl.text)) {
if (!mounted) return true;
final ignore = await showAlertDialog(
context,
@ -40,14 +38,14 @@ class _BackupSetupPageState extends State<BackupSetupPage> {
if (!mounted) return true;
if (ignore) {
setState(() {
isLoading = false;
_isLoading = false;
});
return true;
}
}
await Future.delayed(const Duration(milliseconds: 100));
await enableTwonlySafe(passwordCtrl.text);
await BackupService.updateBackupPassword(_passwordCtrl.text);
await UserService.update((user) {
user.currentSetupPage = SetupPages.backup.next()?.name;
@ -55,25 +53,25 @@ class _BackupSetupPageState extends State<BackupSetupPage> {
if (!mounted) return true;
setState(() {
isLoading = false;
_isLoading = false;
});
return false;
}
@override
void dispose() {
passwordCtrl.dispose();
repeatedPasswordCtrl.dispose();
_passwordCtrl.dispose();
_repeatedPasswordCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isPasswordValid = passwordCtrl.text.length >= 10;
final isPasswordValid = _passwordCtrl.text.length >= 10;
final isRepeatedPasswordValid =
passwordCtrl.text == repeatedPasswordCtrl.text;
_passwordCtrl.text == _repeatedPasswordCtrl.text;
final canSubmit =
!isLoading &&
!_isLoading &&
(isPasswordValid && isRepeatedPasswordValid || !kReleaseMode);
return Column(
@ -95,24 +93,24 @@ class _BackupSetupPageState extends State<BackupSetupPage> {
),
const SizedBox(height: 32),
BackupPasswordTextField(
controller: passwordCtrl,
controller: _passwordCtrl,
labelText: context.lang.password,
onChanged: (_) => setState(() {}),
),
PasswordRequirementText(
text: context.lang.backupPasswordRequirement,
showError: passwordCtrl.text.isNotEmpty && !isPasswordValid,
showError: _passwordCtrl.text.isNotEmpty && !isPasswordValid,
),
const SizedBox(height: 8),
BackupPasswordTextField(
controller: repeatedPasswordCtrl,
controller: _repeatedPasswordCtrl,
labelText: context.lang.passwordRepeated,
onChanged: (_) => setState(() {}),
),
PasswordRequirementText(
text: context.lang.passwordRepeatedNotEqual,
showError:
repeatedPasswordCtrl.text.isNotEmpty && !isRepeatedPasswordValid,
_repeatedPasswordCtrl.text.isNotEmpty && !isRepeatedPasswordValid,
),
const SizedBox(height: 10),
Row(
@ -131,16 +129,9 @@ class _BackupSetupPageState extends State<BackupSetupPage> {
),
],
),
const SizedBox(height: 20),
Center(
child: TextButton(
onPressed: () => context.push(Routes.settingsBackupServer),
child: Text(context.lang.backupExpertSettings),
),
),
const SizedBox(height: 40),
NextButtonComp(
isLoading: isLoading,
isLoading: _isLoading,
canSubmit: canSubmit,
onPressed: onPressedEnableTwonlySafe,
),

View file

@ -4,6 +4,7 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
class AccountView extends StatelessWidget {
const AccountView({super.key});
@ -58,13 +59,9 @@ class AccountView extends StatelessWidget {
final res = await apiService.deleteAccount();
if (res.isError) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Could not delete the account. Please ensure you have a internet connection!',
),
duration: Duration(seconds: 3),
),
showSnackbar(
context,
'Could not delete the account. Please ensure you have a internet connection!',
);
return;
}

View file

@ -1,182 +0,0 @@
// ignore_for_file: parameter_assignments, avoid_dynamic_calls
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:twonly/locator.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
class BackupServerView extends StatefulWidget {
const BackupServerView({super.key});
@override
State<BackupServerView> createState() => _BackupServerViewState();
}
class _BackupServerViewState extends State<BackupServerView> {
final TextEditingController _urlController = TextEditingController();
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
void initState() {
super.initState();
_urlController.text = 'https://';
unawaited(initAsync());
}
Future<void> initAsync() async {
if (userService.currentUser.backupServer != null) {
final uri = Uri.parse(userService.currentUser.backupServer!.serverUrl);
// remove user auth data
final serverUrl = Uri(
scheme: uri.scheme,
host: uri.host,
port: uri.port,
path: uri.path,
query: uri.query,
);
_urlController.text = serverUrl.toString();
_usernameController.text = serverUrl.userInfo.split(':')[0];
}
setState(() {});
}
Future<void> checkAndUpdateBackupServer() async {
var serverUrl = _urlController.text;
if (!serverUrl.endsWith('/')) {
serverUrl += '/';
}
final username = _usernameController.text;
final password = _passwordController.text;
if (username.isNotEmpty || password.isNotEmpty) {
serverUrl = serverUrl.replaceAll('https://', '');
serverUrl = 'https://$username@$password$serverUrl';
}
try {
final uri = Uri.parse('${serverUrl}config');
final response = await http.get(
uri,
headers: {
'User-Agent': 'twonly',
'Accept': 'application/json',
},
);
if (response.statusCode == 200) {
// If the server returns a 200 OK response, parse the JSON.
final data = jsonDecode(response.body);
final backupServer = BackupServer(
serverUrl: serverUrl,
retentionDays: data['retentionDays']! as int,
maxBackupBytes: data['maxBackupBytes']! as int,
);
await UserService.update((user) {
user.backupServer = backupServer;
});
if (mounted) Navigator.pop(context, backupServer);
} else {
// If the server did not return a 200 OK response, throw an exception.
throw Exception(
'Got invalid status code ${response.statusCode} from server.',
);
}
} catch (e) {
Log.error('$e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$e'),
duration: const Duration(seconds: 3),
),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('twonly Backup Server'),
),
body: Padding(
padding: const EdgeInsets.all(40),
child: ListView(
children: [
Text(
context.lang.backupOwnServerDesc,
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
TextField(
controller: _urlController,
onChanged: (value) {
if (value.length < 8) {
value = '';
}
value = value.replaceAll('https://', '');
value = value.replaceAll('http://', '');
value = 'https://$value';
_urlController.text = value;
setState(() {});
},
decoration: const InputDecoration(
labelText: 'Server URL',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Username (optional)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password (optional)',
border: OutlineInputBorder(),
),
obscureText: true,
),
const SizedBox(height: 20),
Center(
child: FilledButton.icon(
onPressed: (_urlController.text.length > 8)
? checkAndUpdateBackupServer
: null,
icon: const FaIcon(FontAwesomeIcons.server),
label: Text(context.lang.backupUseOwnServer),
),
),
const SizedBox(height: 10),
Center(
child: OutlinedButton(
onPressed: () async {
await UserService.update((user) {
user.backupServer = null;
});
if (context.mounted) Navigator.pop(context);
},
child: Text(context.lang.backupResetServer),
),
),
],
),
),
);
}
}

View file

@ -1,9 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/model/json/backup.model.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/utils/misc.dart';
class BackupView extends StatefulWidget {
@ -13,16 +15,37 @@ class BackupView extends StatefulWidget {
State<BackupView> createState() => _BackupViewState();
}
BackupServer _defaultBackupServer = BackupServer(
serverUrl: 'Default',
retentionDays: 180,
maxBackupBytes: 2097152,
);
class _BackupViewState extends State<BackupView> {
bool _isLoading = false;
CurrentBackupStatus? _backupStatus;
StreamSubscription<void>? _backupUpdateSub;
String _backupStatus(LastBackupUploadState status) {
@override
void initState() {
super.initState();
_loadBackupStatus();
_backupUpdateSub = BackupService.onBackupUpdated.listen((_) {
_loadBackupStatus();
});
}
@override
void dispose() {
_backupUpdateSub?.cancel();
super.dispose();
}
Future<void> _loadBackupStatus() async {
setState(() => _isLoading = true);
final status = await BackupService.getData();
if (!mounted) return;
setState(() {
_backupStatus = status;
_isLoading = false;
});
}
String _getBackupStatusString(LastBackupUploadState status) {
switch (status) {
case LastBackupUploadState.none:
return context.lang.backupPending;
@ -35,21 +58,41 @@ class _BackupViewState extends State<BackupView> {
}
}
List<TableRow> _buildTableRows(List<(String, String)> rows) {
return rows.map((pair) {
return TableRow(
children: [
TableCell(
child: Text(pair.$1),
),
TableCell(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 4,
),
child: Text(
pair.$2,
textAlign: TextAlign.right,
),
),
),
],
);
}).toList();
}
@override
Widget build(BuildContext context) {
return StreamBuilder<void>(
stream: userService.onUserUpdated,
builder: (context, _) {
final backupServer =
userService.currentUser.backupServer ?? _defaultBackupServer;
return Scaffold(
appBar: AppBar(
title: Text(context.lang.settingsBackup),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: ListView(
children: [
const SizedBox(height: 8),
Text(
@ -57,81 +100,80 @@ class _BackupViewState extends State<BackupView> {
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
if (userService.currentUser.twonlySafeBackup != null)
if (userService.currentUser.isBackupEnabled)
Column(
children: [
const SizedBox(height: 32),
Center(
child: Text(
context.lang.backupIdentityHeader,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 8),
Table(
defaultVerticalAlignment:
TableCellVerticalAlignment.middle,
children: [
...[
(
context.lang.backupServer,
(backupServer.serverUrl.contains('@'))
? backupServer.serverUrl.split('@')[1]
: backupServer.serverUrl.replaceAll(
'https://',
'',
),
children: _buildTableRows([
(
context.lang.backupLastBackupDate,
_backupStatus?.identityLastSuccessFull != null
? formatDateTime(
context,
_backupStatus!.identityLastSuccessFull,
)
: '-',
),
(
context.lang.backupLastBackupSize,
_backupStatus?.identitySize != null
? formatBytes(_backupStatus!.identitySize!)
: '-',
),
(
context.lang.backupLastBackupResult,
_getBackupStatusString(
_backupStatus?.identityState ??
LastBackupUploadState.none,
),
(
context.lang.backupMaxBackupSize,
formatBytes(backupServer.maxBackupBytes),
),
]),
),
const SizedBox(height: 24),
Center(
child: Text(
context.lang.backupArchiveHeader,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 8),
Table(
defaultVerticalAlignment:
TableCellVerticalAlignment.middle,
children: _buildTableRows([
(
context.lang.backupLastBackupDate,
_backupStatus?.archiveLastSuccessFull != null
? formatDateTime(
context,
_backupStatus!.archiveLastSuccessFull,
)
: '-',
),
(
context.lang.backupLastBackupSize,
_backupStatus?.archiveSize != null
? formatBytes(_backupStatus!.archiveSize!)
: '-',
),
(
context.lang.backupLastBackupResult,
_getBackupStatusString(
_backupStatus?.archiveState ??
LastBackupUploadState.none,
),
(
context.lang.backupStorageRetention,
'${backupServer.retentionDays} Days',
),
(
context.lang.backupLastBackupDate,
formatDateTime(
context,
userService
.currentUser
.twonlySafeBackup!
.lastBackupDone,
),
),
(
context.lang.backupLastBackupSize,
formatBytes(
userService
.currentUser
.twonlySafeBackup!
.lastBackupSize,
),
),
(
context.lang.backupLastBackupResult,
_backupStatus(
userService
.currentUser
.twonlySafeBackup!
.backupUploadState,
),
),
].map((pair) {
return TableRow(
children: [
TableCell(
child: Text(pair.$1),
),
TableCell(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 4,
),
child: Text(
pair.$2,
textAlign: TextAlign.right,
),
),
),
],
);
}),
],
),
]),
),
const SizedBox(height: 10),
OutlinedButton(
@ -141,7 +183,7 @@ class _BackupViewState extends State<BackupView> {
setState(() {
_isLoading = true;
});
await performTwonlySafeBackup(force: true);
await BackupService.makeBackup(force: true);
setState(() {
_isLoading = false;
});
@ -156,7 +198,7 @@ class _BackupViewState extends State<BackupView> {
onPressed: () =>
context.push(Routes.settingsBackupSetup, extra: true),
child: Text(
userService.currentUser.twonlySafeBackup == null
!userService.currentUser.isBackupEnabled
? context.lang.backupEnableBackup
: context.lang.backupChangePassword,
),

View file

@ -1,10 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/views/settings/backup/components/backup_setup.comp.dart';
@ -24,15 +23,16 @@ class SetupBackupView extends StatefulWidget {
}
class _SetupBackupViewState extends State<SetupBackupView> {
bool isLoading = false;
final TextEditingController passwordCtrl = TextEditingController();
final TextEditingController repeatedPasswordCtrl = TextEditingController();
bool _isLoading = false;
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _repeadedController = TextEditingController();
Future<void> onPressedEnableTwonlySafe() async {
Future<void> _updateBackupPassword() async {
setState(() {
isLoading = true;
_isLoading = true;
});
if (!await isSecurePassword(passwordCtrl.text)) {
if (!await isSecurePassword(_passwordController.text)) {
if (!mounted) return;
final ignore = await showAlertDialog(
context,
@ -44,7 +44,7 @@ class _SetupBackupViewState extends State<SetupBackupView> {
if (ignore) {
if (mounted) {
setState(() {
isLoading = false;
_isLoading = false;
});
}
return;
@ -52,11 +52,12 @@ class _SetupBackupViewState extends State<SetupBackupView> {
}
await Future.delayed(const Duration(milliseconds: 100));
await enableTwonlySafe(passwordCtrl.text);
await BackupService.updateBackupPassword(_passwordController.text);
await UserService.update((u) => u.isBackupEnabled = true);
if (!mounted) return;
setState(() {
isLoading = false;
_isLoading = false;
});
if (widget.callBack != null) {
@ -100,34 +101,27 @@ class _SetupBackupViewState extends State<SetupBackupView> {
),
const SizedBox(height: 30),
BackupPasswordTextField(
controller: passwordCtrl,
controller: _passwordController,
labelText: context.lang.password,
onChanged: (value) => setState(() {}),
),
PasswordRequirementText(
text: context.lang.backupPasswordRequirement,
showError:
passwordCtrl.text.length < 8 &&
passwordCtrl.text.isNotEmpty,
_passwordController.text.length < 8 &&
_passwordController.text.isNotEmpty,
),
const SizedBox(height: 5),
BackupPasswordTextField(
controller: repeatedPasswordCtrl,
controller: _repeadedController,
labelText: context.lang.passwordRepeated,
onChanged: (value) => setState(() {}),
),
PasswordRequirementText(
text: context.lang.passwordRepeatedNotEqual,
showError:
passwordCtrl.text != repeatedPasswordCtrl.text &&
repeatedPasswordCtrl.text.isNotEmpty,
),
const SizedBox(height: 10),
Center(
child: OutlinedButton(
onPressed: () => context.push(Routes.settingsBackupServer),
child: Text(context.lang.backupExpertSettings),
),
_passwordController.text != _repeadedController.text &&
_repeadedController.text.isNotEmpty,
),
const SizedBox(height: 10),
Text(
@ -139,13 +133,14 @@ class _SetupBackupViewState extends State<SetupBackupView> {
Center(
child: FilledButton.icon(
onPressed:
(!isLoading &&
(passwordCtrl.text == repeatedPasswordCtrl.text &&
passwordCtrl.text.length >= 8 ||
(!_isLoading &&
(_passwordController.text ==
_repeadedController.text &&
_passwordController.text.length >= 8 ||
!kReleaseMode))
? onPressedEnableTwonlySafe
? _updateBackupPassword
: null,
icon: isLoading
icon: _isLoading
? const SizedBox(
height: 12,
width: 12,
@ -159,21 +154,6 @@ class _SetupBackupViewState extends State<SetupBackupView> {
),
),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
if (widget.callBack != null) {
widget.callBack!();
} else {
Navigator.pop(context);
}
},
child: Text(
context.lang.skipForNow,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 8, color: Colors.grey),
),
),
],
),
),

View file

@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/views/settings/backup/backup_settings.view.dart';
class MissingBackupComp extends StatefulWidget {
const MissingBackupComp({super.key});
@override
State<MissingBackupComp> createState() => _MissingBackupCompState();
}
class _MissingBackupCompState extends State<MissingBackupComp> {
Future<void> onTap() async {
await context.navPush(const BackupView());
}
@override
Widget build(BuildContext context) {
return StreamBuilder<void>(
stream: userService.onUserUpdated,
builder: (context, snapshot) {
final user = userService.currentUser;
if (user.currentSetupPage != null || user.isBackupEnabled) {
return const SizedBox.shrink();
}
return Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
gradient: LinearGradient(
colors: [
context.color.primaryContainer.withValues(alpha: 0.2),
context.color.primaryContainer.withValues(alpha: 0.1),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
border: Border.all(
color: context.color.primary.withValues(alpha: 0.15),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: context.color.shadow.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(24),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
SizedBox(
width: 68,
height: 68,
child: Container(
decoration: BoxDecoration(
color: context.color.primary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.shield_rounded,
size: 32,
color: context.color.primary,
),
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.missingBackupCardTitle,
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 17,
color: context.color.onSurface,
letterSpacing: -0.2,
),
),
const SizedBox(height: 6),
Text(
context.lang.missingBackupCardDesc,
style: TextStyle(
fontSize: 13,
color: context.color.onSurfaceVariant,
height: 1.3,
),
),
const SizedBox(height: 14),
FilledButton.icon(
onPressed: onTap,
icon: const Icon(
Icons.arrow_forward_rounded,
size: 18,
),
label: Text(
context.lang.missingBackupCardAction,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
style: FilledButton.styleFrom(
backgroundColor: context.color.primary,
foregroundColor: context.color.onPrimary,
minimumSize: const Size(0, 40),
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
),
],
),
),
],
),
),
),
),
);
},
);
}
}

View file

@ -3,6 +3,7 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
class ChatReactionSelectionView extends StatefulWidget {
const ChatReactionSelectionView({super.key});
@ -39,12 +40,7 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
user.preSelectedEmojies = _selectedEmojis;
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.settingsPreSelectedReactionsError),
duration: const Duration(seconds: 3),
),
);
showSnackbar(context, context.lang.settingsPreSelectedReactionsError);
}
}
setState(() {});

View file

@ -13,6 +13,7 @@ import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/secure_storage.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/views/settings/help/contact_us/submit_message.view.dart';
import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
@ -124,9 +125,7 @@ class _ContactUsState extends State<ContactUsView> {
}
if (token == null) {
if (!mounted) return null;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not upload the debug log!')),
);
showSnackbar(context, 'Could not upload the debug log!');
setState(() {
isLoading = false;
});

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
class SubmitMessage extends StatefulWidget {
const SubmitMessage({required this.fullMessage, super.key});
@ -28,8 +29,10 @@ class _ContactUsState extends State<SubmitMessage> {
});
if (feedback.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter your message.')),
showSnackbar(
context,
'Please enter a message.',
level: SnackbarLevel.info,
);
return;
}
@ -49,15 +52,16 @@ class _ContactUsState extends State<SubmitMessage> {
});
if (response.statusCode == 200) {
// Handle successful response
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.lang.contactUsSuccess)),
showSnackbar(
context,
context.lang.contactUsSuccess,
level: SnackbarLevel.success,
);
Navigator.pop(context, true);
} else {
// Handle error response
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to submit feedback.')),
showSnackbar(
context,
'Failed to submit feedback.',
);
}
}

View file

@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
class DiagnosticsView extends StatefulWidget {
@ -29,21 +30,9 @@ class _DiagnosticsViewState extends State<DiagnosticsView> {
}
Future<void> _deleteDebugLog() async {
if (await deleteLogFile()) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Log file deleted!'),
),
);
} else {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Log file does not exist.'),
),
);
}
await deleteLogFile();
if (!mounted) return;
showSnackbar(context, 'Log file deleted!', level: SnackbarLevel.info);
}
@override
@ -244,8 +233,10 @@ class _LogViewerWidgetState extends State<LogViewerWidget> {
return InkWell(
onLongPress: () {
Clipboard.setData(ClipboardData(text: e.line));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Copied line')),
showSnackbar(
context,
'Copied line',
level: SnackbarLevel.info,
);
},
child: Padding(
@ -307,8 +298,9 @@ class _LogEntry {
var msg = trimmed;
// Try to parse leading timestamp (YYYY-MM-DD HH:MM:SS.mmmmmm)
final tsRegex =
RegExp(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+(.*)$');
final tsRegex = RegExp(
r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+(.*)$',
);
final mTs = tsRegex.firstMatch(trimmed);
if (mTs != null) {
try {

View file

@ -5,12 +5,10 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/secure_storage.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
class NotificationView extends StatefulWidget {
@ -32,15 +30,11 @@ class _NotificationViewState extends State<NotificationView> {
await initFCMAfterAuthenticated(force: true);
final storedToken = await SecureStorage.instance.read(
key: SecureStorageKeys.googleFcm,
);
await setupNotificationWithUsers(force: true);
if (!mounted) return;
if (storedToken == null) {
if (userService.currentUser.fcmToken == null) {
final platform = Platform.isAndroid ? "Google's" : "Apple's";
await showAlertDialog(
context,

View file

@ -8,10 +8,9 @@ import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
import 'package:twonly/src/visual/views/groups/group.view.dart';
@ -71,15 +70,11 @@ class _ProfileViewState extends State<ProfileView> {
if (result.error == ErrorCode.UsernameAlreadyTaken ||
result.error == ErrorCode.UsernameNotValid) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
result.error == ErrorCode.UsernameAlreadyTaken
? context.lang.errorUsernameAlreadyTaken
: context.lang.errorUsernameNotValid,
),
duration: const Duration(seconds: 3),
),
showSnackbar(
context,
result.error == ErrorCode.UsernameAlreadyTaken
? context.lang.errorUsernameAlreadyTaken
: context.lang.errorUsernameNotValid,
);
return;
}
@ -89,10 +84,6 @@ class _ProfileViewState extends State<ProfileView> {
return;
}
// as the username has changes, remove the old from the server and then upload it again.
await removeTwonlySafeFromServer();
unawaited(performTwonlySafeBackup(force: true));
await UserService.update(
(u) => u
..username = username

View file

@ -12,6 +12,7 @@ import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/views/settings/subscription/select_additional_users.view.dart';
class AdditionalUsersView extends StatefulWidget {
@ -80,24 +81,20 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
);
if (contact != null && mounted) {
if (res.error == ErrorCode.UserIsNotInFreePlan) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.lang.additionalUserAddErrorNotInFreePlan(
getContactDisplayName(contact),
),
),
showSnackbar(
context,
context.lang.additionalUserAddErrorNotInFreePlan(
getContactDisplayName(contact),
),
level: SnackbarLevel.info,
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.lang.additionalUserAddError(
getContactDisplayName(contact),
),
),
showSnackbar(
context,
context.lang.additionalUserAddError(
getContactDisplayName(contact),
),
level: SnackbarLevel.info,
);
}
}
@ -231,14 +228,11 @@ class _AdditionalAccountState extends State<AdditionalAccount> {
if (res.isSuccess) {
widget.refresh();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
errorCodeToText(
context,
res.error as ErrorCode,
),
),
showSnackbar(
context,
errorCodeToText(
context,
res.error as ErrorCode,
),
);
}

View file

@ -9,6 +9,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart';
import 'package:twonly/src/visual/helpers/video_player_file.helper.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/save_to_gallery.dart';
@ -82,13 +83,17 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
await saveImageToGallery(imageBytes);
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.lang.galleryExportSuccess)),
showSnackbar(
context,
context.lang.galleryExportSuccess,
level: SnackbarLevel.success,
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$e')),
showSnackbar(
context,
e.toString(),
level: SnackbarLevel.success,
);
}
}

View file

@ -7,6 +7,7 @@ import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/services/user_study.service.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/snackbar.dart';
class UserStudyQuestionnaireView extends StatefulWidget {
const UserStudyQuestionnaireView({super.key});
@ -60,10 +61,11 @@ class _UserStudyQuestionnaireViewState
await handleUserStudyUpload();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Vielen Dank für deine Teilnahme!')),
showSnackbar(
context,
'Vielen Dank für deine Teilnahme!',
level: SnackbarLevel.success,
);
context.pop();
}

1372
rust/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -18,12 +18,18 @@ sqlx = { version = "0.9.0-alpha.1", default-features = false, features = [
"derive",
"json",
] }
mdk-core = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk", rev = "7f809f8549458a0d7f7d885bcdd694023abf299c", features = [
"mip04",
"mip05",
] }
mdk-sqlite-storage = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk", rev = "7f809f8549458a0d7f7d885bcdd694023abf299c" }
mdk-storage-traits = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk", rev = "7f809f8549458a0d7f7d885bcdd694023abf299c" }
# mdk-core = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk", rev = "7f809f8549458a0d7f7d885bcdd694023abf299c", features = [
# "mip04",
# "mip05",
# ] }
# mdk-sqlite-storage = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk", rev = "7f809f8549458a0d7f7d885bcdd694023abf299c" }
# mdk-storage-traits = { version = "0.8.0", git = "https://github.com/marmot-protocol/mdk", rev = "7f809f8549458a0d7f7d885bcdd694023abf299c" }
# nostr-sdk = { version = "0.44", features = [
# "nip04",
# "nip44",
# "nip47",
# "nip59",
# ] }
libsqlite3-sys = { version = "0.35.0", features = [
"bundled-sqlcipher-vendored-openssl",
] }

View file

@ -1,23 +1,25 @@
use crate::context::Context;
use crate::database::Database;
use crate::error::{Result, TwonlyError};
use crate::keys::{DatabaseKey, MainKey};
use crate::error::Result;
use crate::keys::{DatabaseKey, KeyManager};
use std::fs::{remove_file, File};
use std::io::{copy, Cursor};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use zeroize::Zeroize;
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipArchive, ZipWriter};
struct BackupArchive {}
pub(crate) struct BackupArchive {}
impl BackupArchive {
fn get_backup_files(ctx: &Context) -> Result<Vec<(&str, PathBuf, bool, Option<String>)>> {
fn get_backup_files(
ctx: &Context,
keys: &KeyManager,
) -> Result<Vec<(&'static str, PathBuf, bool, Option<String>)>> {
let config = ctx.get_config()?;
let database_dir = PathBuf::from(&config.database_dir);
let data_dir = PathBuf::from(&config.data_dir);
let keys = ctx.get_key_manager()?;
let rust_db_key = keys.main_key.get_database_key(DatabaseKey::RustDb);
Ok(vec![
@ -38,7 +40,11 @@ impl BackupArchive {
}
std::fs::create_dir_all(&backup_data_dir)?;
for (file_name, source_dir, is_db, mut encryption_key) in Self::get_backup_files(ctx)? {
let keys = ctx.get_key_manager().await?;
for (file_name, source_dir, is_db, mut encryption_key) in
Self::get_backup_files(ctx, &keys)?
{
let file_path = source_dir.join(file_name);
if !file_path.exists() {
tracing::warn!(
@ -52,7 +58,7 @@ impl BackupArchive {
let db = Database::new(
&file_path.display().to_string(),
encryption_key.as_deref(),
encryption_key.is_none(),
false,
)
.await?;
let backup_database_file = backup_data_dir.join(file_name).display().to_string();
@ -65,10 +71,6 @@ impl BackupArchive {
encryption_key.zeroize();
}
let mut keys = ctx.get_key_manager()?;
let keys_serialized = postcard::to_allocvec(&keys)?;
let mut zip_data = Vec::new();
{
@ -76,9 +78,6 @@ impl BackupArchive {
let options =
SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
zip.start_file(".key_manager.bin", options)?;
copy(&mut keys_serialized.as_slice(), &mut zip)?;
for entry in WalkDir::new(&backup_data_dir) {
let entry = entry?;
let path = entry.path();
@ -99,26 +98,16 @@ impl BackupArchive {
std::fs::write(&zip_path, keys.main_key.encrypt_backup(&zip_data))?;
std::fs::remove_dir_all(&backup_data_dir)?;
keys.zeroize();
Ok(zip_path)
}
pub(crate) async fn restore_from_backup(
ctx: &Context,
main_key_bytes: &[u8],
file_path: &PathBuf,
) -> Result<()> {
pub(crate) async fn restore_from_backup(ctx: &Context, file_path: &Path) -> Result<()> {
let data_dir = PathBuf::from(&ctx.get_config()?.data_dir);
let main_key_arr: [u8; 32] = main_key_bytes
.try_into()
.map_err(|_| TwonlyError::Generic("Invalid main key length".to_string()))?;
let mut main_key = MainKey::from_main_key(main_key_arr);
let key_manager = ctx.get_key_manager().await?;
let encrypted_zip = std::fs::read(file_path)?;
let zip_content = main_key.decrypt_backup(&encrypted_zip)?;
let zip_content = key_manager.main_key.decrypt_backup(&encrypted_zip)?;
let restore_temp_dir = data_dir.join("restore_temp");
@ -134,15 +123,6 @@ impl BackupArchive {
let mut file = archive.by_index(i)?;
if file.is_file() {
let name = file.name().to_string();
if name == ".key_manager.bin" {
let mut data = Vec::new();
copy(&mut file, &mut data)?;
let key_manager: crate::keys::KeyManager = postcard::from_bytes(&data)?;
key_manager.store_to_keychain(ctx.get_secure_storage()?)?;
continue;
}
let enclosed_name = file.enclosed_name();
if let Some(name) = enclosed_name.as_ref().and_then(|p| p.file_name()) {
let restored_file = restore_temp_dir.join(name);
@ -151,7 +131,7 @@ impl BackupArchive {
}
}
for (file_name, target_dir, is_db, _) in Self::get_backup_files(ctx)? {
for (file_name, target_dir, is_db, _) in Self::get_backup_files(ctx, &key_manager)? {
let src = restore_temp_dir.join(file_name);
if src.exists() {
let dst = target_dir.join(file_name);
@ -166,7 +146,6 @@ impl BackupArchive {
}
}
main_key.zeroize();
std::fs::remove_dir_all(&restore_temp_dir)?;
Ok(())
@ -175,7 +154,9 @@ impl BackupArchive {
#[cfg(test)]
mod tests {
use crate::{database::tables::received_messages::ReceivedMessage, keys::KeyManager};
use crate::{
database::tables::received_messages::ReceivedMessage, secure_storage::SecureStorage,
};
use super::*;
use tempfile::tempdir;
@ -194,16 +175,12 @@ mod tests {
.unwrap();
// 1. Add some data
{
let original_login_token = {
let secure_storage = SecureStorage::new("testing");
let config = ctx.get_config().unwrap();
let rust_db_path = PathBuf::from(&config.database_dir).join("rust_db.sqlite");
let mut key_manager = ctx.get_key_manager().unwrap();
key_manager
.identity_keys
.push(crate::keys::IdentityKey::Nost());
key_manager
.store_to_keychain(ctx.get_secure_storage().unwrap())
.unwrap();
let key_manager = ctx.get_key_manager().await.unwrap();
key_manager.store_to_keychain(&secure_storage).unwrap();
let db = Database::new(
&rust_db_path.display().to_string(),
@ -220,20 +197,18 @@ mod tests {
// Add a file
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
std::fs::write(config_file, "original config").unwrap();
}
key_manager.main_key.get_login_token()
};
// 2. Create backup
let backup_path = BackupArchive::create_backup(&ctx).await.unwrap();
assert!(backup_path.exists());
// Save the original main key bytes
let original_main_key = *ctx.get_key_manager().unwrap().main_key.as_bytes();
// 3. Modify data (to simulate state before restore)
{
let config = ctx.get_config().unwrap();
let rust_db_path = PathBuf::from(&config.database_dir).join("rust_db.sqlite");
let key_manager = ctx.get_key_manager().unwrap();
let key_manager = ctx.get_key_manager().await.unwrap();
let db = Database::new(
&rust_db_path.display().to_string(),
Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)),
@ -248,17 +223,10 @@ mod tests {
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
std::fs::write(config_file, "new config").unwrap();
// Delete old keys to ensure they will be actually restored
let key_manager = KeyManager::generate().unwrap();
key_manager
.store_to_keychain(&ctx.get_secure_storage().unwrap())
.unwrap();
}
// 4. Restore backup
BackupArchive::restore_from_backup(&ctx, &original_main_key, &backup_path)
BackupArchive::restore_from_backup(&ctx, &backup_path)
.await
.unwrap();
@ -266,7 +234,7 @@ mod tests {
{
let config = ctx.get_config().unwrap();
let rust_db_path = PathBuf::from(&config.database_dir).join("rust_db.sqlite");
let key_manager = ctx.get_key_manager().unwrap();
let key_manager = ctx.get_key_manager().await.unwrap();
let db = Database::new(
&rust_db_path.display().to_string(),
Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)),
@ -285,12 +253,7 @@ mod tests {
let config_content = std::fs::read_to_string(config_file).unwrap();
assert_eq!(config_content, "original config");
let key_manager = ctx.get_key_manager().unwrap();
assert_eq!(key_manager.identity_keys.len(), 1);
match &key_manager.identity_keys[0] {
crate::keys::IdentityKey::Nost() => {}
_ => panic!("Wrong identity key!"),
}
assert_eq!(key_manager.main_key.get_login_token(), original_login_token);
}
}
}

View file

@ -0,0 +1,83 @@
use crate::error::{Result, TwonlyError};
use crate::keys::{BackupPasswordKeys, KeyManager};
use crate::secure_storage::SecureStorage;
use aes_gcm::aead::rand_core::RngCore;
use aes_gcm::aead::{Aead, KeyInit, OsRng};
use aes_gcm::{Aes256Gcm, Nonce};
pub(crate) struct BackupIdentity();
impl BackupIdentity {
pub(crate) fn encrypt_key_manager(key_manager: &KeyManager) -> Result<Vec<u8>> {
let Some(keys) = &key_manager.backup_password else {
return Err(TwonlyError::Generic("No backup password".into()));
};
let serialized_bytes = postcard::to_allocvec(key_manager)?;
let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&keys.encryption_key);
let cipher = Aes256Gcm::new(key);
let mut nonce_bytes = [0u8; 12];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher.encrypt(nonce, serialized_bytes.as_slice())?;
let mut encrypted_bytes = vec![];
encrypted_bytes.extend_from_slice(&nonce_bytes);
encrypted_bytes.extend_from_slice(&ciphertext);
Ok(encrypted_bytes)
}
pub(crate) fn restore_key_manager(
secure_storage: &SecureStorage,
backup_password_keys: &BackupPasswordKeys,
encrypted_bytes: &[u8],
) -> Result<()> {
if encrypted_bytes.len() < 12 {
return Err(TwonlyError::Generic(
"Invalid encrypted backup length".into(),
));
}
let (nonce_bytes, ciphertext) = encrypted_bytes.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&backup_password_keys.encryption_key);
let cipher = Aes256Gcm::new(key);
let decrypted_bytes = cipher.decrypt(nonce, ciphertext)?;
let key_manager: KeyManager = postcard::from_bytes(&decrypted_bytes)?;
key_manager.store_to_keychain(&secure_storage)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backup_encryption_decryption() {
let secure_storage = SecureStorage::new("testing");
let mut key_manager = KeyManager::generate().unwrap();
let password = "my_secure_password";
let salt = 10;
let backup_keys = BackupPasswordKeys::from_password(password, salt).unwrap();
key_manager.backup_password = Some(backup_keys.clone());
let encrypted = BackupIdentity::encrypt_key_manager(&key_manager).unwrap();
BackupIdentity::restore_key_manager(&secure_storage, &backup_keys, &encrypted).unwrap();
let restored = KeyManager::try_from_keychain(&secure_storage).unwrap();
assert_eq!(restored, key_manager);
}
}

View file

@ -1,127 +0,0 @@
use crate::error::{Result, TwonlyError};
use crate::keys::KeyManager;
use crate::secure_storage::{self, SecureStorage};
use aes_gcm::aead::rand_core::RngCore;
use aes_gcm::aead::{Aead, KeyInit, OsRng};
use aes_gcm::{Aes256Gcm, Nonce};
use mdk_core::key_packages;
use scrypt::{scrypt, Params};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub(crate) struct BackupPasswordKeys {
backup_id: [u8; 32],
encryption_key: [u8; 32],
}
impl BackupPasswordKeys {
pub(crate) fn new(backup_id: [u8; 32], encryption_key: [u8; 32]) -> Self {
Self {
backup_id,
encryption_key,
}
}
pub(crate) fn from_password(password: &str, username: &str) -> Result<Self> {
let params = Params::new(17, 8, 1)?;
let mut output = [0u8; 64];
scrypt(
password.as_bytes(),
username.as_bytes(),
&params,
&mut output,
)?;
let mut backup_id = [0u8; 32];
let mut encryption_key = [0u8; 32];
backup_id.copy_from_slice(&output[0..32]);
encryption_key.copy_from_slice(&output[32..64]);
Ok(Self::new(backup_id, encryption_key))
}
fn encrypt_key_manager(key_manager: KeyManager) -> Result<Vec<u8>> {
let Some(keys) = &key_manager.backup_password else {
return Err(TwonlyError::Generic("No backup password".into()));
};
let serialized_bytes = postcard::to_allocvec(&key_manager)?;
let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&keys.encryption_key);
let cipher = Aes256Gcm::new(key);
let mut nonce_bytes = [0u8; 12];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher.encrypt(nonce, serialized_bytes.as_slice())?;
let mut encrypted_bytes = vec![];
encrypted_bytes.extend_from_slice(&nonce_bytes);
encrypted_bytes.extend_from_slice(&ciphertext);
Ok(encrypted_bytes)
}
pub(crate) fn restore_key_manager(
secure_storage: SecureStorage,
encrypted_bytes: &[u8],
keys: &BackupPasswordKeys,
) -> Result<()> {
if encrypted_bytes.len() < 12 {
return Err(TwonlyError::Generic(
"Invalid encrypted backup length".into(),
));
}
let (nonce_bytes, ciphertext) = encrypted_bytes.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&keys.encryption_key);
let cipher = Aes256Gcm::new(key);
let decrypted_bytes = cipher.decrypt(nonce, ciphertext)?;
let key_manager: KeyManager = postcard::from_bytes(&decrypted_bytes)?;
key_manager.store_to_keychain(&secure_storage)?;
Ok(())
}
}
#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub(crate) struct BackupPlainTextContent {
pub(crate) user_id: i64,
pub(crate) key_manager: KeyManager,
}
impl BackupPlainTextContent {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backup_encryption_decryption() {
let mut key_manager = KeyManager::generate().unwrap();
let password = "my_secure_password";
let salt = "my_random_salt";
let keys = BackupPasswordKeys::from_password(password, salt).unwrap();
key_manager.backup_password = Some(keys.clone());
let content = BackupPlainTextContent {
user_id: 12345,
key_manager,
};
let encrypted = content.get_encrypted_backup().unwrap();
let decrypted = BackupPlainTextContent::from_encrypted_backup(&encrypted, &keys).unwrap();
assert_eq!(content.user_id, decrypted.user_id);
assert_eq!(content.key_manager.main_key, decrypted.key_manager.main_key);
}
}

View file

@ -1,3 +1,4 @@
#![allow(dead_code)]
use serde::{Deserialize, Serialize};
/// Send from the person who tries to recover their account.

View file

@ -1,3 +1,3 @@
mod backup_archive;
pub(crate) mod backup_password;
mod backup_passwordless;
pub(crate) mod backup_archive;
pub(crate) mod backup_identity;
pub(crate) mod backup_passwordless;

View file

@ -11,6 +11,7 @@ use crate::context::Context;
use crate::database::Database;
use crate::error::Result;
use crate::error::TwonlyError;
use crate::keys::KeyManager;
use crate::secure_storage::SecureStorage;
use crate::utils::Shared;
use flutter_rust_bridge::frb;
@ -18,6 +19,7 @@ use protocols::user_discovery::UserDiscovery;
pub use protocols::user_discovery::traits::AnnouncedUser;
pub use protocols::user_discovery::traits::OtherPromotion;
use tokio::sync::Mutex;
pub struct InitConfig {
pub database_dir: String,
@ -46,8 +48,10 @@ pub(crate) struct TwonlyFlutter {
pub(crate) config: InitConfig,
pub(crate) user_discovery:
Shared<UserDiscovery<UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter>>,
#[allow(dead_code)]
pub(crate) rust_db: Arc<Database>,
pub(crate) secure_storage: SecureStorage,
pub(crate) key_manager: Arc<Mutex<KeyManager>>,
}
pub(super) fn get_twonly_flutter() -> Result<&'static TwonlyFlutter> {

View file

@ -0,0 +1,90 @@
use std::path::PathBuf;
use crate::backup::backup_archive::BackupArchive;
use crate::backup::backup_identity::BackupIdentity;
use crate::bridge::get_twonly_flutter;
use crate::context::Context;
use crate::error::{Result, TwonlyError};
pub use crate::keys::backup_password_keys::BackupPasswordKeys;
pub struct RustBackupIdentity();
pub struct RustBackupArchive();
impl RustBackupIdentity {
pub async fn get_backup_password_keys(
user_id: i64,
password: String,
) -> Result<BackupPasswordKeys> {
BackupPasswordKeys::from_password(&password, user_id)
}
pub async fn get_backup_id() -> Option<String> {
let key_manager = get_twonly_flutter().ok()?.key_manager.lock().await;
Some(hex::encode(key_manager.backup_password.clone()?.backup_id))
}
pub async fn set_backup_password_keys(user_id: i64, password: String) -> Result<()> {
let backup_keys = BackupPasswordKeys::from_password(&password, user_id)?;
let ctx = get_twonly_flutter()?;
let mut key_manager = ctx.key_manager.lock().await;
key_manager.backup_password = Some(backup_keys);
key_manager.store_to_keychain(&ctx.secure_storage)?;
Ok(())
}
pub async fn import_backup_password_keys(
backup_id: Vec<u8>,
encryption_key: Vec<u8>,
) -> Result<()> {
let backup_id: [u8; 32] = backup_id
.try_into()
.map_err(|a: Vec<u8>| TwonlyError::WronKeySize(2, a.len()))?;
let encryption_key: [u8; 32] = encryption_key
.try_into()
.map_err(|a: Vec<u8>| TwonlyError::WronKeySize(2, a.len()))?;
let backup_keys = BackupPasswordKeys::new(backup_id, encryption_key);
let ctx = get_twonly_flutter()?;
let mut key_manager = ctx.key_manager.lock().await;
key_manager.backup_password = Some(backup_keys);
key_manager.store_to_keychain(&ctx.secure_storage)?;
Ok(())
}
pub async fn get_identity_backup_bytes() -> Result<Vec<u8>> {
let key_manager = get_twonly_flutter()?.key_manager.lock().await;
return BackupIdentity::encrypt_key_manager(&key_manager);
}
pub async fn restore_identity_backup(
keys: BackupPasswordKeys,
encrypted_bytes: Vec<u8>,
) -> Result<()> {
let ctx = get_twonly_flutter()?;
BackupIdentity::restore_key_manager(&ctx.secure_storage, &keys, &encrypted_bytes)?;
let restored = crate::keys::KeyManager::try_from_keychain(&ctx.secure_storage)?;
*ctx.key_manager.lock().await = restored;
Ok(())
}
}
impl RustBackupArchive {
pub async fn create_backup_archive() -> Result<(String, String)> {
let ctx = Context::get_static()?;
let path = BackupArchive::create_backup(&ctx).await?;
let key_manager = get_twonly_flutter()?.key_manager.lock().await;
let token = hex::encode(key_manager.main_key.get_backup_download_token());
Ok((token, path.canonicalize()?.to_string_lossy().to_string()))
}
pub async fn restore_backup_archive(file_path: String) -> Result<()> {
let ctx = Context::get_static()?;
BackupArchive::restore_from_backup(ctx, &PathBuf::from(file_path)).await
}
pub async fn get_backup_download_token() -> Option<String> {
let key_manager = get_twonly_flutter().ok()?.key_manager.lock().await;
Some(hex::encode(
key_manager.main_key.get_backup_download_token(),
))
}
}

View file

@ -1,12 +1,92 @@
use crate::error::Result;
use crate::{bridge::get_twonly_flutter, keys::KeyManager};
use std::collections::HashMap;
pub struct FlutterKeyManager {}
use crate::bridge::get_twonly_flutter;
use crate::error::{Result, TwonlyError};
use crate::keys::SignalIdentityKey;
impl FlutterKeyManager {
pub struct RustKeyManager {}
impl RustKeyManager {
pub async fn get_login_token() -> Result<Vec<u8>> {
let ctx = get_twonly_flutter()?;
let key_manager = KeyManager::try_from_keychain(&ctx.secure_storage)?;
let key_manager = get_twonly_flutter()?.key_manager.lock().await;
Ok(key_manager.main_key.get_login_token().to_vec())
}
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 mut key_manager = ctx.key_manager.lock().await;
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)?;
Ok(())
}
pub async fn get_signal_identity() -> Result<(Vec<u8>, i64)> {
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.identity_key_pair_structure.to_owned(),
signal_identity.registration_id,
))
} else {
Err(TwonlyError::SignalIdentityNotFound)
}
}
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)
}
}
}

View file

@ -1,2 +1,3 @@
pub mod backup;
pub mod key_manager;
pub mod user_discovery;

View file

@ -11,7 +11,7 @@ use crate::{
};
use protocols::user_discovery::UserDiscovery;
use std::{path::PathBuf, sync::Arc};
use tokio::sync::OnceCell;
use tokio::sync::{Mutex, OnceCell};
use zeroize::Zeroize;
use crate::{bridge::TwonlyFlutter, secure_storage::SecureStorage, standalone::TwonlyStandalone};
@ -35,6 +35,7 @@ impl Context {
Self::init_common(config, true).await
}
#[allow(dead_code)]
pub(crate) async fn init_standalone(config: InitConfig) -> Result<()> {
Self::init_common(config, false).await
}
@ -44,6 +45,8 @@ impl Context {
database_dir: PathBuf,
data_dir: PathBuf,
) -> Result<Context> {
use tokio::sync::Mutex;
std::fs::create_dir_all(&database_dir)?;
std::fs::create_dir_all(&data_dir)?;
@ -73,6 +76,7 @@ impl Context {
config,
rust_db,
secure_storage,
key_manager: Arc::new(Mutex::new(key_manager)),
}))
}
@ -92,7 +96,7 @@ impl Context {
let rust_db_path = database_dir.join("rust_db.sqlite");
tracing::info!("Initialized twonly workspace.");
let _: Result<&'static Context> = GLOBAL_CONTEXT
let res: Result<&'static Context> = GLOBAL_CONTEXT
.get_or_try_init(|| async {
let key_manager = match KeyManager::try_from_keychain(&secure_storage) {
Ok(key) => key,
@ -127,6 +131,7 @@ impl Context {
config,
secure_storage,
rust_db,
key_manager: Arc::new(Mutex::new(key_manager)),
user_discovery: Shared::new(UserDiscovery::new(
UserDiscoveryStoreFlutter {},
UserDiscoveryUtilsFlutter {},
@ -136,12 +141,13 @@ impl Context {
Ok(Context::Standalone(TwonlyStandalone {
config,
rust_db,
key_manager: Arc::new(Mutex::new(key_manager)),
secure_storage,
}))
}
})
.await;
res?;
Ok(())
}
@ -149,12 +155,12 @@ impl Context {
GLOBAL_CONTEXT.get().ok_or(TwonlyError::Initialization)
}
pub(crate) fn get_secure_storage(&self) -> Result<&SecureStorage> {
match self {
Self::Flutter(twonly) => Ok(&twonly.secure_storage),
Self::Standalone(twonly) => Ok(&twonly.secure_storage),
}
}
// pub(crate) fn get_secure_storage(&self) -> Result<&SecureStorage> {
// match self {
// Self::Flutter(twonly) => Ok(&twonly.secure_storage),
// Self::Standalone(twonly) => Ok(&twonly.secure_storage),
// }
// }
pub(crate) fn get_config(&self) -> Result<&InitConfig> {
match self {
@ -163,7 +169,10 @@ impl Context {
}
}
pub(crate) fn get_key_manager(&self) -> Result<KeyManager> {
KeyManager::try_from_keychain(self.get_secure_storage()?)
pub(crate) async fn get_key_manager(&self) -> Result<tokio::sync::MutexGuard<'_, KeyManager>> {
match self {
Self::Flutter(twonly) => Ok(twonly.key_manager.lock().await),
Self::Standalone(twonly) => Ok(twonly.key_manager.lock().await),
}
}
}

View file

@ -96,7 +96,6 @@ impl Database {
#[cfg(test)]
mod tests {
use crate::database::tables::received_messages::ReceivedMessage;
use chrono::Utc;
use super::*;
use tempfile::tempdir;

View file

@ -1,3 +1,4 @@
#![allow(dead_code)]
use chrono::{DateTime, Utc};
use sqlx::FromRow;

View file

@ -14,9 +14,15 @@ pub enum TwonlyError {
#[error("Tried to access the wrong context")]
WrongContext,
#[error("Tried to access signal identity while it does not exists")]
SignalIdentityNotFound,
#[error("init_flutter_callbacks was not called")]
MissingCallbackInitialization,
#[error("wrong input key size. expected: {0} but got {1}")]
WronKeySize(usize, usize),
#[error("Could not find the given database")]
DatabaseNotFound,

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 = 1007286393;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1215442517;
// Section: executor
@ -46,21 +46,6 @@ flutter_rust_bridge::frb_generated_default_handler!();
// Section: wire_funcs
fn wire__crate__bridge__wrapper__key_manager__flutter_key_manager_get_login_token_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: "flutter_key_manager_get_login_token", 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::FlutterKeyManager::get_login_token().await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -225,6 +210,298 @@ fn wire__crate__bridge__initialize_twonly_flutter_impl(
},
)
}
fn wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_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_backup_archive_create_backup_archive", 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::backup::RustBackupArchive::create_backup_archive().await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_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_backup_archive_get_backup_download_token", 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::<_, ()>((move || async move {
let output_ok = Result::<_,()>::Ok(crate::bridge::wrapper::backup::RustBackupArchive::get_backup_download_token().await)?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_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_backup_archive_restore_backup_archive", 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_file_path = <String>::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::backup::RustBackupArchive::restore_backup_archive(api_file_path).await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_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_backup_identity_get_backup_id",
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::<_, ()>(
(move || async move {
let output_ok = Result::<_, ()>::Ok(
crate::bridge::wrapper::backup::RustBackupIdentity::get_backup_id()
.await,
)?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_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_backup_identity_get_backup_password_keys", 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_user_id = <i64>::sse_decode(&mut deserializer);
let api_password = <String>::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::backup::RustBackupIdentity::get_backup_password_keys(api_user_id, api_password).await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_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_backup_identity_get_identity_backup_bytes", 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::backup::RustBackupIdentity::get_identity_backup_bytes().await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_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_backup_identity_import_backup_password_keys", 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_backup_id = <Vec<u8>>::sse_decode(&mut deserializer);
let api_encryption_key = <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::backup::RustBackupIdentity::import_backup_password_keys(api_backup_id, api_encryption_key).await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_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_backup_identity_restore_identity_backup", 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_keys = <crate::keys::backup_password_keys::BackupPasswordKeys>::sse_decode(&mut deserializer);
let api_encrypted_bytes = <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::backup::RustBackupIdentity::restore_identity_backup(api_keys, api_encrypted_bytes).await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_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_backup_identity_set_backup_password_keys", 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_user_id = <i64>::sse_decode(&mut deserializer);
let api_password = <String>::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::backup::RustBackupIdentity::set_backup_password_keys(api_user_id, api_password).await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_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_get_login_token",
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::get_login_token()
.await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_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_get_signal_identity", 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::get_signal_identity().await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_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_import_signal_identity", 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_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 {
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)
})().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_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)
} })
}
// Section: static_checks
@ -707,6 +984,14 @@ impl SseDecode for flutter_rust_bridge::DartOpaque {
}
}
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 {
@ -737,6 +1022,18 @@ impl SseDecode for crate::bridge::AnnouncedUser {
}
}
impl SseDecode for crate::keys::backup_password_keys::BackupPasswordKeys {
// 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_backupId = <[u8; 32]>::sse_decode(deserializer);
let mut var_encryptionKey = <[u8; 32]>::sse_decode(deserializer);
return crate::keys::backup_password_keys::BackupPasswordKeys {
backup_id: var_backupId,
encryption_key: var_encryptionKey,
};
}
}
impl SseDecode for bool {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -744,13 +1041,6 @@ impl SseDecode for bool {
}
}
impl SseDecode for crate::bridge::wrapper::key_manager::FlutterKeyManager {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
return crate::bridge::wrapper::key_manager::FlutterKeyManager {};
}
}
impl SseDecode for crate::bridge::wrapper::user_discovery::FlutterUserDiscovery {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -820,6 +1110,29 @@ 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 Option<String> {
// 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(<String>::sse_decode(deserializer));
} else {
return None;
}
}
}
impl SseDecode for Option<crate::bridge::AnnouncedUser> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -897,6 +1210,54 @@ impl SseDecode for crate::bridge::OtherPromotion {
}
}
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 {
let mut var_field0 = <Vec<u8>>::sse_decode(deserializer);
let mut var_field1 = <i64>::sse_decode(deserializer);
return (var_field0, var_field1);
}
}
impl SseDecode for (String, String) {
// 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 = <String>::sse_decode(deserializer);
let mut var_field1 = <String>::sse_decode(deserializer);
return (var_field0, var_field1);
}
}
impl SseDecode for crate::bridge::wrapper::backup::RustBackupArchive {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
return crate::bridge::wrapper::backup::RustBackupArchive();
}
}
impl SseDecode for crate::bridge::wrapper::backup::RustBackupIdentity {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
return crate::bridge::wrapper::backup::RustBackupIdentity();
}
}
impl SseDecode for crate::bridge::wrapper::key_manager::RustKeyManager {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
return crate::bridge::wrapper::key_manager::RustKeyManager {};
}
}
impl SseDecode for u32 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -911,6 +1272,14 @@ impl SseDecode for u8 {
}
}
impl SseDecode for [u8; 32] {
// 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<u8>>::sse_decode(deserializer);
return flutter_rust_bridge::for_generated::from_vec_to_array(inner);
}
}
impl SseDecode for () {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {}
@ -939,15 +1308,30 @@ fn pde_ffi_dispatcher_primary_impl(
) {
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
match func_id {
1 => wire__crate__bridge__wrapper__key_manager__flutter_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len),
2 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len),
3 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_new_messages_impl(port, ptr, rust_vec_len, data_len),
4 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_handle_new_messages_impl(port, ptr, rust_vec_len, data_len),
5 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_initialize_or_update_impl(port, ptr, rust_vec_len, data_len),
6 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_should_request_new_messages_impl(port, ptr, rust_vec_len, data_len),
7 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len),
8 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len),
9 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len),
1 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len),
2 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_new_messages_impl(port, ptr, rust_vec_len, data_len),
3 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_handle_new_messages_impl(port, ptr, rust_vec_len, data_len),
4 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_initialize_or_update_impl(port, ptr, rust_vec_len, data_len),
5 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_should_request_new_messages_impl(port, ptr, rust_vec_len, data_len),
6 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len),
7 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len),
8 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len),
9 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len),
10 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len),
11 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len),
12 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
13 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
14 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
15 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
16 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
17 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
18 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len),
19 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
20 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
21 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
22 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len),
23 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
24 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
@ -989,19 +1373,23 @@ impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<crate::bridge::AnnouncedUser>>
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::bridge::wrapper::key_manager::FlutterKeyManager {
impl flutter_rust_bridge::IntoDart for crate::keys::backup_password_keys::BackupPasswordKeys {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
Vec::<u8>::new().into_dart()
[
self.backup_id.into_into_dart().into_dart(),
self.encryption_key.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::bridge::wrapper::key_manager::FlutterKeyManager
for crate::keys::backup_password_keys::BackupPasswordKeys
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::bridge::wrapper::key_manager::FlutterKeyManager>
for crate::bridge::wrapper::key_manager::FlutterKeyManager
impl flutter_rust_bridge::IntoIntoDart<crate::keys::backup_password_keys::BackupPasswordKeys>
for crate::keys::backup_password_keys::BackupPasswordKeys
{
fn into_into_dart(self) -> crate::bridge::wrapper::key_manager::FlutterKeyManager {
fn into_into_dart(self) -> crate::keys::backup_password_keys::BackupPasswordKeys {
self
}
}
@ -1068,6 +1456,57 @@ impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<crate::bridge::OtherPromotion>
self.into()
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::bridge::wrapper::backup::RustBackupArchive {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
Vec::<u8>::new().into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::bridge::wrapper::backup::RustBackupArchive
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::bridge::wrapper::backup::RustBackupArchive>
for crate::bridge::wrapper::backup::RustBackupArchive
{
fn into_into_dart(self) -> crate::bridge::wrapper::backup::RustBackupArchive {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::bridge::wrapper::backup::RustBackupIdentity {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
Vec::<u8>::new().into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::bridge::wrapper::backup::RustBackupIdentity
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::bridge::wrapper::backup::RustBackupIdentity>
for crate::bridge::wrapper::backup::RustBackupIdentity
{
fn into_into_dart(self) -> crate::bridge::wrapper::backup::RustBackupIdentity {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::bridge::wrapper::key_manager::RustKeyManager {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
Vec::<u8>::new().into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::bridge::wrapper::key_manager::RustKeyManager
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::bridge::wrapper::key_manager::RustKeyManager>
for crate::bridge::wrapper::key_manager::RustKeyManager
{
fn into_into_dart(self) -> crate::bridge::wrapper::key_manager::RustKeyManager {
self
}
}
impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error {
// Codec=Sse (Serialization based), see doc to use other codecs
@ -1083,6 +1522,13 @@ impl SseEncode for flutter_rust_bridge::DartOpaque {
}
}
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) {
@ -1106,6 +1552,14 @@ impl SseEncode for crate::bridge::AnnouncedUser {
}
}
impl SseEncode for crate::keys::backup_password_keys::BackupPasswordKeys {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<[u8; 32]>::sse_encode(self.backup_id, serializer);
<[u8; 32]>::sse_encode(self.encryption_key, serializer);
}
}
impl SseEncode for bool {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -1113,11 +1567,6 @@ impl SseEncode for bool {
}
}
impl SseEncode for crate::bridge::wrapper::key_manager::FlutterKeyManager {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
}
impl SseEncode for crate::bridge::wrapper::user_discovery::FlutterUserDiscovery {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
@ -1178,6 +1627,26 @@ 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 Option<String> {
// 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 {
<String>::sse_encode(value, serializer);
}
}
}
impl SseEncode for Option<crate::bridge::AnnouncedUser> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -1240,6 +1709,45 @@ impl SseEncode for crate::bridge::OtherPromotion {
}
}
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) {
<Vec<u8>>::sse_encode(self.0, serializer);
<i64>::sse_encode(self.1, serializer);
}
}
impl SseEncode for (String, String) {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<String>::sse_encode(self.0, serializer);
<String>::sse_encode(self.1, serializer);
}
}
impl SseEncode for crate::bridge::wrapper::backup::RustBackupArchive {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
}
impl SseEncode for crate::bridge::wrapper::backup::RustBackupIdentity {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
}
impl SseEncode for crate::bridge::wrapper::key_manager::RustKeyManager {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
}
impl SseEncode for u32 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -1254,6 +1762,19 @@ impl SseEncode for u8 {
}
}
impl SseEncode for [u8; 32] {
// 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(
{
let boxed: Box<[_]> = Box::new(self);
boxed.into_vec()
},
serializer,
);
}
}
impl SseEncode for () {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}

View file

@ -0,0 +1,38 @@
use crate::error::Result;
use scrypt::{scrypt, Params};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct BackupPasswordKeys {
pub backup_id: [u8; 32],
pub encryption_key: [u8; 32],
}
impl BackupPasswordKeys {
pub(crate) fn new(backup_id: [u8; 32], encryption_key: [u8; 32]) -> Self {
Self {
backup_id,
encryption_key,
}
}
pub(crate) fn from_password(password: &str, user_id: i64) -> Result<Self> {
let params = Params::new(16, 8, 1)?;
let mut output = [0u8; 64];
scrypt(
password.as_bytes(),
&user_id.to_be_bytes(),
&params,
&mut output,
)?;
let mut backup_id = [0u8; 32];
let mut encryption_key = [0u8; 32];
backup_id.copy_from_slice(&output[0..32]);
encryption_key.copy_from_slice(&output[32..64]);
Ok(Self::new(backup_id, encryption_key))
}
}

View file

@ -1,8 +0,0 @@
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub(crate) enum IdentityKey {
Nost(),
Signal(),
}

View file

@ -0,0 +1 @@
pub(crate) mod signal_identity_key;

View file

@ -0,0 +1,33 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct SignalIdentityKey {
// https://github.com/MixinNetwork/libsignal_protocol_dart/blob/c95a1586057022acdbb9c76b1692d94cc549bcc7/protobuf/LocalStorageProtocol.proto#L85
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 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();
}
}
impl Drop for SignalIdentityKey {
fn drop(&mut self) {
self.zeroize();
}
}
impl ZeroizeOnDrop for SignalIdentityKey {}

View file

@ -28,15 +28,6 @@ impl MainKey {
Self { main_key }
}
/// Initializes a MainKey from an existing main key.
pub fn from_main_key(main_key: [u8; 32]) -> Self {
Self { main_key }
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.main_key
}
/// Download token required to download a backup.
/// This ensures that the user who tries to download the backup must have knowledge over the
/// main key
@ -71,22 +62,22 @@ impl MainKey {
}
/// Encrypts a newly generated media key using the derived Media Main Key.
pub fn encrypt_media_key(&self, media_key: &[u8; 32]) -> Vec<u8> {
self.encrypt_with_info(b"media_main_key", media_key)
}
// pub fn encrypt_media_key(&self, media_key: &[u8; 32]) -> Vec<u8> {
// self.encrypt_with_info(b"media_main_key", media_key)
// }
/// Decrypts a wrapped media key using the derived Media Main Key.
pub fn decrypt_media_key(&self, wrapped_media_key: &[u8]) -> Result<[u8; 32]> {
let decrypted = self.decrypt_with_info(b"media_main_key", wrapped_media_key)?;
// pub fn decrypt_media_key(&self, wrapped_media_key: &[u8]) -> Result<[u8; 32]> {
// let decrypted = self.decrypt_with_info(b"media_main_key", wrapped_media_key)?;
if decrypted.len() != 32 {
return Err("Invalid decrypted key length".to_string())?;
}
// if decrypted.len() != 32 {
// return Err("Invalid decrypted key length".to_string())?;
// }
let mut result = [0u8; 32];
result.copy_from_slice(&decrypted);
Ok(result)
}
// let mut result = [0u8; 32];
// result.copy_from_slice(&decrypted);
// Ok(result)
// }
fn derive_key(&self, info: &[u8]) -> [u8; 32] {
let hk = Hkdf::<Sha256>::new(None, &self.main_key);
@ -130,13 +121,6 @@ impl MainKey {
mod tests {
use super::*;
#[test]
fn test_generate_and_from_main_key() {
let km = MainKey::generate();
let km2 = MainKey::from_main_key(km.main_key);
assert_eq!(km.main_key, km2.main_key);
}
#[test]
fn test_backup_encryption_decryption_success() {
let km = MainKey::generate();
@ -176,74 +160,74 @@ mod tests {
);
}
#[test]
fn test_media_key_encryption_decryption_success() {
let km = MainKey::generate();
let mut media_key = [0u8; 32];
OsRng.fill_bytes(&mut media_key);
// #[test]
// fn test_media_key_encryption_decryption_success() {
// let km = MainKey::generate();
// let mut media_key = [0u8; 32];
// OsRng.fill_bytes(&mut media_key);
let encrypted = km.encrypt_media_key(&media_key);
let decrypted = km.decrypt_media_key(&encrypted).unwrap();
// let encrypted = km.encrypt_media_key(&media_key);
// let decrypted = km.decrypt_media_key(&encrypted).unwrap();
assert_eq!(media_key, decrypted);
}
// assert_eq!(media_key, decrypted);
// }
#[test]
fn test_media_key_decryption_tampered_payload_fails() {
let km = MainKey::generate();
let mut media_key = [0u8; 32];
OsRng.fill_bytes(&mut media_key);
// #[test]
// fn test_media_key_decryption_tampered_payload_fails() {
// let km = MainKey::generate();
// let mut media_key = [0u8; 32];
// OsRng.fill_bytes(&mut media_key);
let mut encrypted = km.encrypt_media_key(&media_key);
// let mut encrypted = km.encrypt_media_key(&media_key);
// Tamper with the ciphertext
let last_idx = encrypted.len() - 1;
encrypted[last_idx] ^= 1;
// // Tamper with the ciphertext
// let last_idx = encrypted.len() - 1;
// encrypted[last_idx] ^= 1;
let result = km.decrypt_media_key(&encrypted);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "Decryption failure");
}
// let result = km.decrypt_media_key(&encrypted);
// assert!(result.is_err());
// assert_eq!(result.unwrap_err().to_string(), "Decryption failure");
// }
#[test]
fn test_media_key_decryption_too_short_fails() {
let km = MainKey::generate();
let short_payload = vec![0u8; 10]; // Less than 12 bytes nonce
// #[test]
// fn test_media_key_decryption_too_short_fails() {
// let km = MainKey::generate();
// let short_payload = vec![0u8; 10]; // Less than 12 bytes nonce
let result = km.decrypt_media_key(&short_payload);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Invalid encrypted data length"
);
}
// let result = km.decrypt_media_key(&short_payload);
// assert!(result.is_err());
// assert_eq!(
// result.unwrap_err().to_string(),
// "Invalid encrypted data length"
// );
// }
#[test]
fn test_media_key_decryption_wrong_decrypted_length_fails() {
let km = MainKey::generate();
// #[test]
// fn test_media_key_decryption_wrong_decrypted_length_fails() {
// let km = MainKey::generate();
// Manually encrypt a 31 byte payload
let hk = Hkdf::<Sha256>::new(None, &km.main_key);
let mut media_main_key = [0u8; 32];
hk.expand(b"media_main_key", &mut media_main_key)
.expect("HKDF expand failed");
// // Manually encrypt a 31 byte payload
// let hk = Hkdf::<Sha256>::new(None, &km.main_key);
// let mut media_main_key = [0u8; 32];
// hk.expand(b"media_main_key", &mut media_main_key)
// .expect("HKDF expand failed");
let key = Key::<Aes256Gcm>::from_slice(&media_main_key);
let cipher = Aes256Gcm::new(key);
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let payload = vec![0u8; 31];
let ciphertext = cipher
.encrypt(&nonce, payload.as_ref())
.expect("encryption failure");
// let key = Key::<Aes256Gcm>::from_slice(&media_main_key);
// let cipher = Aes256Gcm::new(key);
// let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
// let payload = vec![0u8; 31];
// let ciphertext = cipher
// .encrypt(&nonce, payload.as_ref())
// .expect("encryption failure");
let mut encrypted = nonce.to_vec();
encrypted.extend_from_slice(&ciphertext);
// let mut encrypted = nonce.to_vec();
// encrypted.extend_from_slice(&ciphertext);
let result = km.decrypt_media_key(&encrypted);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Invalid decrypted key length"
);
}
// let result = km.decrypt_media_key(&encrypted);
// assert!(result.is_err());
// assert_eq!(
// result.unwrap_err().to_string(),
// "Invalid decrypted key length"
// );
// }
}

View file

@ -1,13 +1,13 @@
pub(crate) mod backup_password_keys;
mod identity_key;
mod main_key;
use crate::backup::backup_password::BackupPasswordKeys;
use crate::error::Result;
use crate::error::TwonlyError;
pub(crate) use crate::keys::identity_key::IdentityKey;
pub(crate) use crate::keys::backup_password_keys::BackupPasswordKeys;
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 aes_gcm::Aes256Gcm;
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
@ -17,7 +17,7 @@ const KEY_MANAGER_ID: &str = "twonly_key_manager";
pub(crate) struct KeyManager {
pub(crate) user_id: Option<i64>,
pub(crate) main_key: MainKey,
pub(crate) identity_keys: Vec<IdentityKey>,
pub(crate) signal_identity: Option<SignalIdentityKey>,
pub(crate) backup_password: Option<BackupPasswordKeys>,
}
@ -25,7 +25,7 @@ impl KeyManager {
pub fn generate() -> Result<Self> {
Ok(KeyManager {
main_key: MainKey::generate(),
identity_keys: vec![],
signal_identity: None,
backup_password: None,
user_id: None,
})

View file

@ -92,15 +92,15 @@ impl SecureStorage {
/// Deletes the secret associated with the given key from the secure keyring.
///
/// If the key does not exist, this function returns `Ok(())` (idempotent).
pub fn delete(&self, key: &str) -> Result<(), String> {
let entry = self.get_entry(key)?;
// pub fn delete(&self, key: &str) -> Result<(), String> {
// let entry = self.get_entry(key)?;
match entry.delete_credential() {
Ok(()) => Ok(()),
Err(KeyringError::NoEntry) => Ok(()),
Err(e) => Err(format!("Failed to delete secret from keyring: {}", e)),
}
}
// match entry.delete_credential() {
// Ok(()) => Ok(()),
// Err(KeyringError::NoEntry) => Ok(()),
// Err(e) => Err(format!("Failed to delete secret from keyring: {}", e)),
// }
// }
/// Helper to create a keyring entry with the appropriate platform modifiers.
fn get_entry(&self, key: &str) -> Result<Entry, String> {
@ -142,10 +142,10 @@ mod tests {
assert_eq!(read_val, Some(secret.to_string()));
// 3. Delete the secret
storage.delete(key).expect("Failed to delete secret");
// storage.delete(key).expect("Failed to delete secret");
// 4. Verify the secret is gone
let after_delete = storage.read(key).expect("Failed to read after delete");
assert_eq!(after_delete, None);
// let after_delete = storage.read(key).expect("Failed to read after delete");
// assert_eq!(after_delete, None);
}
}

View file

@ -1,11 +1,17 @@
use tokio::sync::Mutex;
use crate::bridge::InitConfig;
use crate::database::Database;
use crate::keys::KeyManager;
use crate::secure_storage::SecureStorage;
use std::sync::Arc;
pub(crate) struct TwonlyStandalone {
#[allow(dead_code)]
pub(crate) config: InitConfig,
#[allow(dead_code)]
pub(crate) rust_db: Arc<Database>,
#[allow(dead_code)]
pub(crate) secure_storage: SecureStorage,
pub(crate) key_manager: Arc<Mutex<KeyManager>>,
}

View file

@ -0,0 +1,233 @@
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:drift/native.dart';
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/bridge/wrapper/backup.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/json/backup.model.dart';
import 'package:twonly/src/model/json/userdata.model.dart'
hide LastBackupUploadState;
import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/keyvalue.dart';
void main() {
if (!Platform.isMacOS) {
return;
}
TestWidgetsFlutterBinding.ensureInitialized();
late Directory tempDir;
late Map<String, dynamic> initialUserData;
setUpAll(() async {
const channel = MethodChannel('com.bbflight.background_downloader');
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (methodCall) async {
if (methodCall.method == 'enqueue') {
return true;
}
return null;
});
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_backup_test_');
AppEnvironment.initTesting(
customCacheDir: tempDir.path,
customSupportDir: tempDir.path,
);
await bridge.initializeTwonlyFlutter(
config: bridge.InitConfig(
databaseDir: tempDir.path,
dataDir: tempDir.path,
),
);
});
setUp(() async {
await locator.reset();
final dbFile = File('${tempDir.path}/twonly.sqlite');
locator
..registerSingleton<TwonlyDB>(
TwonlyDB(NativeDatabase(dbFile)),
)
..registerSingleton<UserService>(UserService())
..registerSingleton<ApiService>(ApiService());
userService.currentUser = UserData(
userId: 1,
username: 'test_user',
displayName: 'Test User',
subscriptionPlan: 'Free',
currentSetupPage: null,
)..appVersion = 100;
userService.isUserCreated = true;
await UserService.save(userService.currentUser);
initialUserData = (await KeyValueStore.get('user'))!;
await RustBackupIdentity.setBackupPasswordKeys(
password: 'strong_password',
userId: 1,
);
});
tearDown(() async {
try {
await twonlyDB.close();
} catch (_) {}
});
tearDownAll(() async {
if (tempDir.existsSync()) {
try {
tempDir.deleteSync(recursive: true);
} catch (_) {}
}
});
group('BackupService Tests', () {
test('getData returns default backup status initially', () async {
final data = await BackupService.getData();
expect(data.identityState, LastBackupUploadState.none);
expect(data.archiveState, LastBackupUploadState.none);
expect(data.identityLastSuccessFull, isNull);
expect(data.archiveLastSuccessFull, isNull);
});
test(
'onBackupUpdated stream emits events when backup status changes',
() async {
var eventEmitted = false;
final subscription = BackupService.onBackupUpdated.listen((_) {
eventEmitted = true;
});
final dummyTask = UploadTask(url: 'http://localhost', filename: 'test');
await BackupService.handleBackupStatusUpdate(
'backup_identity',
TaskStatusUpdate(dummyTask, TaskStatus.complete),
);
await Future.delayed(Duration.zero);
expect(eventEmitted, isTrue);
await subscription.cancel();
},
);
test(
'handleBackupStatusUpdate updates identity and archive status correctly',
() async {
// Test success update for identity status
final dummyTask1 = UploadTask(
url: 'http://localhost',
filename: 'test',
);
await BackupService.handleBackupStatusUpdate(
'backup_identity',
TaskStatusUpdate(dummyTask1, TaskStatus.complete),
);
var data = await BackupService.getData();
expect(data.identityState, LastBackupUploadState.success);
expect(data.identityLastSuccessFull, isNotNull);
// Test failure update for archive status
final dummyTask2 = UploadTask(
url: 'http://localhost',
filename: 'test',
);
await BackupService.handleBackupStatusUpdate(
'backup_archive',
TaskStatusUpdate(dummyTask2, TaskStatus.failed),
);
data = await BackupService.getData();
expect(data.archiveState, LastBackupUploadState.failed);
expect(data.archiveLastSuccessFull, isNotNull);
},
);
test(
'startFullBackupRecovery returns usernameNotValid for offline/unknown user',
() async {
final error = await BackupService.startFullBackupRecovery(
'unknown_user',
'password',
);
expect(error, RecoveryError.usernameNotValid);
},
);
test(
'Full backup recovery flow restores identity and user.json archive successfully',
() async {
final initialBackupIdStr = await RustBackupIdentity.getBackupId();
// 1. Create backups of baseline state purely natively to avoid background backup races
final identityBytes = await RustBackupIdentity.getIdentityBackupBytes();
final (_, archivePath) = await RustBackupArchive.createBackupArchive();
// 2. Tamper with user.json data and verify alteration
await KeyValueStore.put(
'user',
{'changed': true, 'username': 'tampered'},
);
final changedUserData = await KeyValueStore.get('user');
expect(changedUserData?['changed'], isTrue);
// 3. Trigger a change of the key_manager before restoring
await RustBackupIdentity.importBackupPasswordKeys(
backupId: List.filled(32, 1),
encryptionKey: List.filled(32, 1),
);
final changedBackupIdStr = await RustBackupIdentity.getBackupId();
expect(changedBackupIdStr, isNot(equals(initialBackupIdStr)));
// 4. Restore identity and archive
final backupKeys = await RustBackupIdentity.getBackupPasswordKeys(
userId: 1,
password: 'strong_password',
);
await RustBackupIdentity.restoreIdentityBackup(
keys: backupKeys,
encryptedBytes: identityBytes,
);
await RustBackupArchive.restoreBackupArchive(filePath: archivePath);
final restoredBackupIdStr = await RustBackupIdentity.getBackupId();
expect(restoredBackupIdStr, equals(initialBackupIdStr));
// 5. Verify user.json data is fully restored
final restoredUserData = await KeyValueStore.get('user');
expect(
restoredUserData?['username'],
equals(initialUserData['username']),
);
},
);
});
}

Some files were not shown because too many files have changed in this diff Show more