mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:58:40 +00:00
recovery does work #121
This commit is contained in:
parent
78ca650166
commit
4108d9a798
24 changed files with 598 additions and 56 deletions
16
lib/app.dart
16
lib/app.dart
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
|
|
@ -6,22 +7,13 @@ import 'package:twonly/src/providers/settings.provider.dart';
|
|||
import 'package:twonly/src/services/api/media_upload.dart';
|
||||
import 'package:twonly/src/services/notification.service.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/onboarding.view.dart';
|
||||
import 'package:twonly/src/views/onboarding/onboarding.view.dart';
|
||||
import 'package:twonly/src/views/home.view.dart';
|
||||
import 'package:twonly/src/views/register.view.dart';
|
||||
import 'package:twonly/src/views/onboarding/register.view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'dart:async';
|
||||
|
||||
// these global function can be called from anywhere to update
|
||||
// the ui when something changed. The callbacks will be set by
|
||||
// App widget.
|
||||
|
||||
// this callback is called by the apiProvider
|
||||
Function(bool) globalCallbackConnectionState = (a) {};
|
||||
bool globalIsAppInBackground = true;
|
||||
int globalBestFriendUserId = -1;
|
||||
|
||||
// these two callbacks are called on updated to the corresponding database
|
||||
|
||||
/// The Widget that configures your application.
|
||||
|
|
@ -161,7 +153,7 @@ class AppMainWidget extends StatefulWidget {
|
|||
|
||||
class _AppMainWidgetState extends State<AppMainWidget> {
|
||||
Future<bool> userCreated = isUserCreated();
|
||||
bool showOnboarding = true;
|
||||
bool showOnboarding = kReleaseMode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
|||
|
|
@ -9,3 +9,13 @@ late TwonlyDatabase twonlyDB;
|
|||
|
||||
List<CameraDescription> gCameras = <CameraDescription>[];
|
||||
bool gIsDemoUser = false;
|
||||
|
||||
// The following global function can be called from anywhere to update
|
||||
// the UI when something changed. The callbacks will be set by
|
||||
// App widget.
|
||||
|
||||
// This callback called by the apiProvider
|
||||
Function(bool) globalCallbackConnectionState = (a) {};
|
||||
|
||||
bool globalIsAppInBackground = true;
|
||||
int globalBestFriendUserId = -1;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import 'package:twonly/src/providers/connection.provider.dart';
|
|||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
import 'package:twonly/src/services/fcm.service.dart';
|
||||
import 'package:twonly/src/services/notification.service.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/create_backup.service.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'app.dart';
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ enum MessageKind {
|
|||
ack,
|
||||
pushKey,
|
||||
receiveMediaError,
|
||||
signalDecryptError
|
||||
}
|
||||
|
||||
enum DownloadState {
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@
|
|||
"backupInsecurePasswordDesc": "Das gewählte Passwort ist sehr unsicher und kann daher leicht von Angreifern erraten werden. Bitte wähle ein sicheres Passwort.",
|
||||
"backupInsecurePasswordOk": "Trotzdem fortfahren",
|
||||
"backupInsecurePasswordCancel": "Erneut versuchen",
|
||||
"backupTwonlySafeLongDesc": "twonly hat keine zentralen Benutzerkonten. Ein Schlüsselpaar wird während der Installation erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Safe erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.",
|
||||
"backupTwonlySafeLongDesc": "twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Safe erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.",
|
||||
"backupSelectStrongPassword": "Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Safe-Backup wiederherstellen möchtest.",
|
||||
"password": "Passwort",
|
||||
"passwordRepeated": "Passwort wiederholen",
|
||||
|
|
|
|||
|
|
@ -449,5 +449,8 @@
|
|||
"backupOwnServerDesc": "Save your twonly safe backups at twonly or on any server of your choice.",
|
||||
"backupUseOwnServer": "Use server",
|
||||
"backupResetServer": "Use standard server",
|
||||
"backupTwonlySaveNow": "Save now"
|
||||
"backupTwonlySaveNow": "Save now",
|
||||
"twonlySafeRecoverTitle": "Recovery",
|
||||
"twonlySafeRecoverDesc": "If you have created a backup with twonly Safe, you can restore it here.",
|
||||
"twonlySafeRecoverBtn": "Restore backup"
|
||||
}
|
||||
|
|
@ -1795,6 +1795,24 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Save now'**
|
||||
String get backupTwonlySaveNow;
|
||||
|
||||
/// No description provided for @twonlySafeRecoverTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Recovery'**
|
||||
String get twonlySafeRecoverTitle;
|
||||
|
||||
/// No description provided for @twonlySafeRecoverDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'If you have created a backup with twonly Safe, you can restore it here.'**
|
||||
String get twonlySafeRecoverDesc;
|
||||
|
||||
/// No description provided for @twonlySafeRecoverBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Restore backup'**
|
||||
String get twonlySafeRecoverBtn;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -916,7 +916,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get backupTwonlySafeLongDesc =>
|
||||
'twonly hat keine zentralen Benutzerkonten. Ein Schlüsselpaar wird während der Installation erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Safe erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.';
|
||||
'twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Safe erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.';
|
||||
|
||||
@override
|
||||
String get backupSelectStrongPassword =>
|
||||
|
|
@ -953,4 +953,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get backupTwonlySaveNow => 'Jetzt speichern';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverTitle => 'Recovery';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverDesc =>
|
||||
'If you have created a backup with twonly Safe, you can restore it here.';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverBtn => 'Restore backup';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -947,4 +947,14 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get backupTwonlySaveNow => 'Save now';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverTitle => 'Recovery';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverDesc =>
|
||||
'If you have created a backup with twonly Safe, you can restore it here.';
|
||||
|
||||
@override
|
||||
String get twonlySafeRecoverBtn => 'Restore backup';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||
import 'package:mutex/mutex.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/app.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
|||
import 'package:twonly/src/services/api/media_download.dart';
|
||||
import 'package:twonly/src/services/notification.service.dart';
|
||||
import 'package:twonly/src/services/signal/encryption.signal.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/create_backup.service.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
|
|
@ -8,6 +9,7 @@ import 'package:twonly/src/database/tables/messages_table.dart';
|
|||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/services/api/media_upload.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/signal/encryption.signal.dart';
|
||||
import 'package:twonly/src/services/notification.service.dart';
|
||||
|
|
@ -98,6 +100,10 @@ Future sendRetransmitMessage(int retransId) async {
|
|||
return;
|
||||
}
|
||||
|
||||
var hash = uint8ListToHex(
|
||||
Uint8List.fromList((await Sha256().hash(encryptedBytes)).bytes));
|
||||
Log.info("Sending message: ${hash.substring(0, 10)}");
|
||||
|
||||
Result resp = await apiService.sendTextMessage(
|
||||
retrans.contactId,
|
||||
encryptedBytes,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ 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:twonly/globals.dart';
|
||||
import 'package:twonly/app.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/services/notification.service.dart';
|
||||
|
|
|
|||
|
|
@ -4,26 +4,10 @@ import 'package:hashlib/hashlib.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/services/api/media_upload.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/create_backup.service.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
Future<String?> getTwonlySafeBackupUrl() async {
|
||||
final user = await getUser();
|
||||
if (user == null || user.twonlySafeBackup == null) return null;
|
||||
|
||||
String backupServerUrl = "https://safe.twonly.eu/";
|
||||
|
||||
if (user.backupServer != null) {
|
||||
backupServerUrl = user.backupServer!.serverUrl;
|
||||
}
|
||||
|
||||
String backupId =
|
||||
uint8ListToHex(user.twonlySafeBackup!.backupId).toLowerCase();
|
||||
|
||||
return "${backupServerUrl}backups/$backupId";
|
||||
}
|
||||
|
||||
Future enableTwonlySafe(String password) async {
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
|
|
@ -69,9 +53,9 @@ Future<(Uint8List, Uint8List)> getMasterKey(
|
|||
List<int> passwordBytes = utf8.encode(password);
|
||||
List<int> saltBytes = utf8.encode(username);
|
||||
|
||||
// Parameters for scrypt
|
||||
// Values are derived from the Threema Whitepaper
|
||||
// https://threema.com/assets/documents/cryptography_whitepaper.pdf
|
||||
|
||||
// Create an instance of Scrypt
|
||||
final scrypt = Scrypt(
|
||||
cost: 65536,
|
||||
blockSize: 8,
|
||||
|
|
@ -80,8 +64,30 @@ Future<(Uint8List, Uint8List)> getMasterKey(
|
|||
salt: saltBytes,
|
||||
);
|
||||
|
||||
// Derive the key
|
||||
// final key = (await compute(scrypt.convert, passwordBytes)).bytes;
|
||||
final key = (scrypt.convert(passwordBytes)).bytes;
|
||||
return (key.sublist(0, 32), key.sublist(32, 64));
|
||||
}
|
||||
|
||||
Future<String?> getTwonlySafeBackupUrl() async {
|
||||
final user = await getUser();
|
||||
if (user == null || user.twonlySafeBackup == null) return null;
|
||||
return getTwonlySafeBackupUrlFromServer(
|
||||
user.twonlySafeBackup!.backupId,
|
||||
user.backupServer,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> getTwonlySafeBackupUrlFromServer(
|
||||
List<int> backupId,
|
||||
BackupServer? backupServer,
|
||||
) async {
|
||||
String backupServerUrl = "https://safe.twonly.eu/";
|
||||
|
||||
if (backupServer != null) {
|
||||
backupServerUrl = backupServer.serverUrl;
|
||||
}
|
||||
|
||||
String backupIdHex = uint8ListToHex(backupId).toLowerCase();
|
||||
|
||||
return "${backupServerUrl}backups/$backupIdHex";
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import 'package:twonly/src/database/twonly_database.dart';
|
|||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/model/protobuf/backup/backup.pb.dart';
|
||||
import 'package:twonly/src/services/api/media_upload.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/common.service.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/settings/backup/backup.view.dart';
|
||||
|
|
|
|||
213
lib/src/services/twonly_safe/create_backup.twonly_safe.dart
Normal file
213
lib/src/services/twonly_safe/create_backup.twonly_safe.dart
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/model/protobuf/backup/backup.pb.dart';
|
||||
import 'package:twonly/src/services/api/media_upload.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/settings/backup/backup.view.dart';
|
||||
|
||||
Future performTwonlySafeBackup({bool force = false}) async {
|
||||
final user = await getUser();
|
||||
|
||||
if (user == null || user.twonlySafeBackup == null || user.isDemoUser) {
|
||||
// Log.warn("perform twonly safe backup was called while it is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.twonlySafeBackup!.backupUploadState ==
|
||||
LastBackupUploadState.pending) {
|
||||
Log.warn("Backup upload is already pending.");
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime? lastUpdateTime = user.twonlySafeBackup!.lastBackupDone;
|
||||
if (!force && lastUpdateTime != null) {
|
||||
if (lastUpdateTime.isAfter(DateTime.now().subtract(Duration(days: 1)))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Log.info("Starting new twonly Safe-Backup.");
|
||||
|
||||
final baseDir = (await getApplicationSupportDirectory()).path;
|
||||
|
||||
final backupDir = Directory(join(baseDir, "backup_twonly_safe/"));
|
||||
await backupDir.create(recursive: true);
|
||||
|
||||
final backupDatabaseFile =
|
||||
File(join(backupDir.path, "twonly_database.backup.sqlite"));
|
||||
|
||||
// copy database
|
||||
final originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
|
||||
await originalDatabase.copy(backupDatabaseFile.path);
|
||||
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
final backupDB = TwonlyDatabase(
|
||||
driftDatabase(
|
||||
name: "twonly_database.backup",
|
||||
native: DriftNativeOptions(
|
||||
databaseDirectory: () async {
|
||||
return backupDir;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await backupDB.deleteDataForTwonlySafe();
|
||||
|
||||
var secureStorageBackup = {};
|
||||
final storage = FlutterSecureStorage();
|
||||
secureStorageBackup[SecureStorageKeys.signalIdentity] =
|
||||
await storage.read(key: SecureStorageKeys.signalIdentity);
|
||||
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
|
||||
await storage.read(key: SecureStorageKeys.signalSignedPreKey);
|
||||
|
||||
var userBackup = await getUser();
|
||||
if (userBackup == null) return;
|
||||
// FILTER settings which should not be in the backup
|
||||
userBackup.twonlySafeBackup = null;
|
||||
userBackup.lastImageSend = null;
|
||||
userBackup.todaysImageCounter = null;
|
||||
userBackup.lastPlanBallance = "";
|
||||
userBackup.additionalUserInvites = "";
|
||||
userBackup.signalLastSignedPreKeyUpdated = null;
|
||||
|
||||
secureStorageBackup[SecureStorageKeys.userData] = jsonEncode(userBackup);
|
||||
|
||||
// Compress and convert backup data
|
||||
|
||||
final twonlyDatabaseBytes = await backupDatabaseFile.readAsBytes();
|
||||
await backupDatabaseFile.delete();
|
||||
|
||||
final backupProto = TwonlySafeBackupContent(
|
||||
secureStorageJson: jsonEncode(secureStorageBackup),
|
||||
twonlyDatabase: twonlyDatabaseBytes,
|
||||
);
|
||||
|
||||
final backupBytes = gzip.encode(backupProto.writeToBuffer());
|
||||
|
||||
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
|
||||
|
||||
if (user.twonlySafeBackup!.lastBackupDone == null ||
|
||||
user.twonlySafeBackup!.lastBackupDone!
|
||||
.isAfter(DateTime.now().subtract(Duration(days: 90)))) {
|
||||
force = true;
|
||||
}
|
||||
|
||||
final lastHash =
|
||||
await storage.read(key: SecureStorageKeys.twonlySafeLastBackupHash);
|
||||
|
||||
if (lastHash != null && !force) {
|
||||
if (backupHash == lastHash) {
|
||||
Log.info("Since last backup nothing has changed.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.twonlySafeLastBackupHash,
|
||||
value: backupHash,
|
||||
);
|
||||
|
||||
// Encrypt backup data
|
||||
|
||||
final xchacha20 = Xchacha20.poly1305Aead();
|
||||
final nonce = xchacha20.newNonce();
|
||||
|
||||
final secretBox = await xchacha20.encrypt(
|
||||
backupBytes,
|
||||
secretKey: SecretKey(user.twonlySafeBackup!.encryptionKey),
|
||||
nonce: nonce,
|
||||
);
|
||||
|
||||
final encryptedBackupBytes = (TwonlySafeBackupEncrypted(
|
||||
mac: secretBox.mac.bytes,
|
||||
nonce: nonce,
|
||||
cipherText: secretBox.cipherText,
|
||||
)).writeToBuffer();
|
||||
|
||||
Log.info("Backup files created.");
|
||||
|
||||
var encryptedBackupBytesFile =
|
||||
File(join(backupDir.path, "twonly_safe.backup"));
|
||||
|
||||
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
|
||||
|
||||
Log.info(
|
||||
"Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.");
|
||||
|
||||
if (user.backupServer != null) {
|
||||
if (encryptedBackupBytes.length > user.backupServer!.maxBackupBytes) {
|
||||
Log.error("Backup is to big for the alternative backup server.");
|
||||
await updateUserdata((user) {
|
||||
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
|
||||
return user;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final task = UploadTask.fromFile(
|
||||
taskId: "backup",
|
||||
file: encryptedBackupBytesFile,
|
||||
httpRequestMethod: "PUT",
|
||||
url: (await getTwonlySafeBackupUrl())!,
|
||||
requiresWiFi: true,
|
||||
post: 'binary',
|
||||
priority: 5,
|
||||
retries: 2,
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
);
|
||||
if (await FileDownloader().enqueue(task)) {
|
||||
Log.info("Starting upload from twonly Safe backup.");
|
||||
await updateUserdata((user) {
|
||||
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending;
|
||||
user.twonlySafeBackup!.lastBackupDone = DateTime.now();
|
||||
user.twonlySafeBackup!.lastBackupSize = encryptedBackupBytes.length;
|
||||
return user;
|
||||
});
|
||||
gUpdateBackupView();
|
||||
} else {
|
||||
Log.error("Error starting UploadTask for twonly Safe.");
|
||||
}
|
||||
}
|
||||
|
||||
Future handleBackupStatusUpdate(TaskStatusUpdate update) async {
|
||||
if (update.status == TaskStatus.failed ||
|
||||
update.status == TaskStatus.canceled) {
|
||||
Log.error(
|
||||
"twonly Safe upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}");
|
||||
await updateUserdata((user) {
|
||||
if (user.twonlySafeBackup != null) {
|
||||
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
|
||||
}
|
||||
return user;
|
||||
});
|
||||
} else if (update.status == TaskStatus.complete) {
|
||||
Log.error(
|
||||
"twonly Safe uploaded with status code ${update.responseStatusCode}");
|
||||
await updateUserdata((user) {
|
||||
if (user.twonlySafeBackup != null) {
|
||||
user.twonlySafeBackup!.backupUploadState =
|
||||
LastBackupUploadState.success;
|
||||
}
|
||||
return user;
|
||||
});
|
||||
} else {
|
||||
Log.info("Backup is in state: ${update.status}");
|
||||
return;
|
||||
}
|
||||
gUpdateBackupView();
|
||||
}
|
||||
101
lib/src/services/twonly_safe/restore.twonly_safe.dart
Normal file
101
lib/src/services/twonly_safe/restore.twonly_safe.dart
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/model/protobuf/backup/backup.pb.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future recoverTwonlySafe(
|
||||
String username,
|
||||
String password,
|
||||
BackupServer? server,
|
||||
) async {
|
||||
final (backupId, encryptionKey) = await getMasterKey(password, username);
|
||||
|
||||
String? backupServerUrl =
|
||||
await 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 await handleBackupData(encryptionKey, backupData);
|
||||
}
|
||||
|
||||
Future handleBackupData(
|
||||
Uint8List encryptionKey,
|
||||
Uint8List backupData,
|
||||
) async {
|
||||
TwonlySafeBackupEncrypted encryptedBackup =
|
||||
TwonlySafeBackupEncrypted.fromBuffer(
|
||||
backupData,
|
||||
);
|
||||
|
||||
SecretBox secretBox = SecretBox(
|
||||
encryptedBackup.cipherText,
|
||||
nonce: encryptedBackup.nonce,
|
||||
mac: Mac(encryptedBackup.mac),
|
||||
);
|
||||
|
||||
final compressedBytes = await Xchacha20.poly1305Aead().decrypt(
|
||||
secretBox,
|
||||
secretKey: SecretKeyData(encryptionKey),
|
||||
);
|
||||
|
||||
final plaintextBytes = gzip.decode(compressedBytes);
|
||||
|
||||
TwonlySafeBackupContent backupContent = TwonlySafeBackupContent.fromBuffer(
|
||||
plaintextBytes,
|
||||
);
|
||||
|
||||
final baseDir = (await getApplicationSupportDirectory()).path;
|
||||
final originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
|
||||
await originalDatabase.writeAsBytes(backupContent.twonlyDatabase);
|
||||
|
||||
final storage = FlutterSecureStorage();
|
||||
|
||||
final secureStorage = jsonDecode(backupContent.secureStorageJson);
|
||||
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.signalIdentity,
|
||||
value: secureStorage[SecureStorageKeys.signalIdentity]);
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.signalSignedPreKey,
|
||||
value: secureStorage[SecureStorageKeys.signalSignedPreKey]);
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.userData,
|
||||
value: secureStorage[SecureStorageKeys.userData]);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/app.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
|
||||
|
|
|
|||
164
lib/src/views/onboarding/recover.view.dart
Normal file
164
lib/src/views/onboarding/recover.view.dart
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
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.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/restore.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/settings/backup/twonly_safe_server.view.dart';
|
||||
|
||||
class BackupRecoveryView extends StatefulWidget {
|
||||
const BackupRecoveryView({super.key});
|
||||
|
||||
@override
|
||||
State<BackupRecoveryView> createState() => _BackupRecoveryViewState();
|
||||
}
|
||||
|
||||
class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
||||
bool obscureText = true;
|
||||
bool isLoading = false;
|
||||
BackupServer? backupServer;
|
||||
final TextEditingController usernameCtrl = TextEditingController();
|
||||
final TextEditingController passwordCtrl = TextEditingController();
|
||||
|
||||
Future _recoverTwonlySafe() async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
await recoverTwonlySafe(
|
||||
usernameCtrl.text,
|
||||
passwordCtrl.text,
|
||||
backupServer,
|
||||
);
|
||||
|
||||
Restart.restartApp(
|
||||
notificationTitle: 'Backup successfully recovered.',
|
||||
notificationBody: 'Click here to open the app again',
|
||||
);
|
||||
} 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: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("twonly Safe ${context.lang.twonlySafeRecoverTitle}"),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showAlertDialog(
|
||||
context,
|
||||
"twonly Safe",
|
||||
context.lang.backupTwonlySafeLongDesc,
|
||||
);
|
||||
},
|
||||
icon: FaIcon(FontAwesomeIcons.circleInfo),
|
||||
iconSize: 18,
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: EdgeInsetsGeometry.symmetric(vertical: 40, horizontal: 40),
|
||||
child: ListView(
|
||||
children: [
|
||||
Text(
|
||||
context.lang.twonlySafeRecoverDesc,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
TextField(
|
||||
controller: usernameCtrl,
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
},
|
||||
style: TextStyle(fontSize: 17),
|
||||
decoration: getInputDecoration(
|
||||
context,
|
||||
context.lang.registerUsernameDecoration,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Stack(
|
||||
children: [
|
||||
TextField(
|
||||
controller: passwordCtrl,
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
},
|
||||
style: TextStyle(fontSize: 17),
|
||||
obscureText: obscureText,
|
||||
decoration: getInputDecoration(
|
||||
context,
|
||||
context.lang.password,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
obscureText = !obscureText;
|
||||
});
|
||||
},
|
||||
icon: FaIcon(
|
||||
obscureText
|
||||
? FontAwesomeIcons.eye
|
||||
: FontAwesomeIcons.eyeSlash,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
Center(
|
||||
child: OutlinedButton(
|
||||
onPressed: () async {
|
||||
backupServer = await Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return TwonlySafeServerView();
|
||||
}));
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(context.lang.backupExpertSettings),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Center(
|
||||
child: FilledButton.icon(
|
||||
onPressed: (!isLoading) ? _recoverTwonlySafe : null,
|
||||
icon: isLoading
|
||||
? SizedBox(
|
||||
height: 12,
|
||||
width: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 1),
|
||||
)
|
||||
: Icon(Icons.lock_clock_rounded),
|
||||
label: Text(context.lang.twonlySafeRecoverBtn),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import 'package:twonly/src/utils/storage.dart';
|
|||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/onboarding/recover.view.dart';
|
||||
|
||||
class RegisterView extends StatefulWidget {
|
||||
const RegisterView({super.key, required this.callbackOnSuccess});
|
||||
|
|
@ -26,6 +27,7 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
final TextEditingController inviteCodeController = TextEditingController();
|
||||
|
||||
bool _isTryingToRegister = false;
|
||||
bool _isValidUserName = false;
|
||||
|
||||
Future createNewUser({bool isDemoAccount = false}) async {
|
||||
String username = (isDemoAccount) ? "<demo>" : usernameController.text;
|
||||
|
|
@ -134,6 +136,9 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
usernameController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: usernameController.text.length),
|
||||
);
|
||||
setState(() {
|
||||
_isValidUserName = usernameController.text.length >= 3;
|
||||
});
|
||||
},
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(12),
|
||||
|
|
@ -181,9 +186,7 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
),
|
||||
)
|
||||
: Icon(Icons.group),
|
||||
onPressed: () async {
|
||||
createNewUser();
|
||||
},
|
||||
onPressed: _isValidUserName ? createNewUser : null,
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||
|
|
@ -209,8 +212,11 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
showAlertDialog(context, "Coming soon",
|
||||
"This feature is not yet implemented! Just create a new account :/");
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return BackupRecoveryView();
|
||||
},
|
||||
));
|
||||
},
|
||||
label: Text("Restore identity"),
|
||||
),
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/common.service.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/create_backup.service.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'package:flutter/services.dart' show rootBundle;
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/common.service.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/settings/backup/twonly_safe_server.view.dart';
|
||||
|
|
@ -60,8 +60,11 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
|
|||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showAlertDialog(context, "twonly Safe",
|
||||
context.lang.backupTwonlySafeLongDesc);
|
||||
showAlertDialog(
|
||||
context,
|
||||
"twonly Safe",
|
||||
context.lang.backupTwonlySafeLongDesc,
|
||||
);
|
||||
},
|
||||
icon: FaIcon(FontAwesomeIcons.circleInfo),
|
||||
iconSize: 18,
|
||||
|
|
|
|||
|
|
@ -72,14 +72,15 @@ class _TwonlySafeServerViewState extends State<TwonlySafeServerView> {
|
|||
final data = jsonDecode(response.body);
|
||||
|
||||
final backupServer = BackupServer(
|
||||
serverUrl: serverUrl,
|
||||
retentionDays: data["retentionDays"]!,
|
||||
maxBackupBytes: data["maxBackupBytes"]!);
|
||||
serverUrl: serverUrl,
|
||||
retentionDays: data["retentionDays"]!,
|
||||
maxBackupBytes: data["maxBackupBytes"]!,
|
||||
);
|
||||
await updateUserdata((user) {
|
||||
user.backupServer = backupServer;
|
||||
return user;
|
||||
});
|
||||
if (mounted) Navigator.pop(context);
|
||||
if (mounted) Navigator.pop(context, backupServer);
|
||||
} else {
|
||||
// If the server did not return a 200 OK response, throw an exception.
|
||||
throw Exception(
|
||||
|
|
|
|||
Loading…
Reference in a new issue