mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-24 23:32:13 +00:00
implement new backup mechanism
This commit is contained in:
parent
f735070a7c
commit
4dbc369003
102 changed files with 4668 additions and 3281 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
29
lib/core/backup/backup_password.dart
Normal file
29
lib/core/backup/backup_password.dart
Normal 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;
|
||||
}
|
||||
87
lib/core/bridge/wrapper/backup.dart
Normal file
87
lib/core/bridge/wrapper/backup.dart
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
29
lib/core/keys/backup_password_keys.dart
Normal file
29
lib/core/keys/backup_password_keys.dart
Normal 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
20
lib/core/lib.dart
Normal 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));
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
51
lib/src/model/json/backup.model.dart
Normal file
51
lib/src/model/json/backup.model.dart
Normal 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);
|
||||
}
|
||||
65
lib/src/model/json/backup.model.g.dart
Normal file
65
lib/src/model/json/backup.model.g.dart
Normal 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',
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
361
lib/src/services/backup.service.dart
Normal file
361
lib/src/services/backup.service.dart
Normal 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,
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
258
lib/src/visual/components/snackbar.dart
Normal file
258
lib/src/visual/components/snackbar.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(() {});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
1372
rust/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
] }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
83
rust/src/backup/backup_identity.rs
Normal file
83
rust/src/backup/backup_identity.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
¶ms,
|
||||
&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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(dead_code)]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Send from the person who tries to recover their account.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
90
rust/src/bridge/wrapper/backup.rs
Normal file
90
rust/src/bridge/wrapper/backup.rs
Normal 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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod backup;
|
||||
pub mod key_manager;
|
||||
pub mod user_discovery;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(dead_code)]
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::FromRow;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
|
|
|||
38
rust/src/keys/backup_password_keys.rs
Normal file
38
rust/src/keys/backup_password_keys.rs
Normal 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(),
|
||||
¶ms,
|
||||
&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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
}
|
||||
1
rust/src/keys/identity_key/mod.rs
Normal file
1
rust/src/keys/identity_key/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub(crate) mod signal_identity_key;
|
||||
33
rust/src/keys/identity_key/signal_identity_key.rs
Normal file
33
rust/src/keys/identity_key/signal_identity_key.rs
Normal 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 {}
|
||||
|
|
@ -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"
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
}
|
||||
|
|
|
|||
233
test/services/backup_service_test.dart
Normal file
233
test/services/backup_service_test.dart
Normal 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
Loading…
Reference in a new issue