backup creation and upload does work #121

This commit is contained in:
otsmr 2025-06-18 21:15:29 +02:00
parent fcc2c9fb9e
commit 9295de7b76
25 changed files with 705 additions and 153 deletions

View file

@ -2,6 +2,10 @@
# Set the source directory
protoc --proto_path="./lib/src/model/protobuf/backup/" --dart_out="./lib/src/model/protobuf/backup/" "backup.proto"
SRC_DIR="../twonly-server/twonly/src/"
DST_DIR="$(pwd)/lib/src/model/protobuf/"

View file

@ -4,4 +4,5 @@ class SecureStorageKeys {
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";
}

View file

@ -23,25 +23,39 @@ class UserData {
String displayName;
String? avatarSvg;
String? avatarJson;
int? avatarCounter;
@JsonKey(defaultValue: 0)
int avatarCounter = 0;
// --- SUBSCRIPTION DTA ---
@JsonKey(defaultValue: "Preview")
String subscriptionPlan;
DateTime? lastImageSend;
int? todaysImageCounter;
// --- SETTINGS ---
@JsonKey(defaultValue: ThemeMode.system)
ThemeMode themeMode = ThemeMode.system;
int? defaultShowTime;
@JsonKey(defaultValue: "Preview")
String subscriptionPlan;
bool? useHighQuality;
@JsonKey(defaultValue: true)
bool useHighQuality = true;
List<String>? preSelectedEmojies;
ThemeMode? themeMode;
Map<String, List<String>>? autoDownloadOptions;
bool? storeMediaFilesInGallery;
@JsonKey(defaultValue: false)
bool storeMediaFilesInGallery = false;
List<String>? lastUsedEditorEmojis;
String? lastPlanBallance;
String? additionalUserInvites;
DateTime? lastImageSend;
int? todaysImageCounter;
List<String>? tutorialDisplayed;
int? myBestFriendContactId;
@ -50,22 +64,35 @@ class UserData {
// --- BACKUP ---
@JsonKey(defaultValue: false)
bool identityBackupEnabled = false;
DateTime? identityBackupLastBackupTime;
@JsonKey(defaultValue: 0)
int identityBackupLastBackupSize = 0;
DateTime? nextTimeToShowBackupNotice;
BackupServer? backupServer;
List<int>? twonlySafeEncryptionKey;
List<int>? twonlySafeBackupId;
TwonlySafeBackup? twonlySafeBackup;
factory UserData.fromJson(Map<String, dynamic> json) =>
_$UserDataFromJson(json);
Map<String, dynamic> toJson() => _$UserDataToJson(this);
}
enum LastBackupUploadState { none, pending, failed, success }
@JsonSerializable()
class TwonlySafeBackup {
TwonlySafeBackup({
required this.backupId,
required this.encryptionKey,
});
int lastBackupSize = 0;
LastBackupUploadState backupUploadState = LastBackupUploadState.none;
DateTime? lastBackupDone;
List<int> backupId;
List<int> encryptionKey;
factory TwonlySafeBackup.fromJson(Map<String, dynamic> json) =>
_$TwonlySafeBackupFromJson(json);
Map<String, dynamic> toJson() => _$TwonlySafeBackupToJson(this);
}
@JsonSerializable()
class BackupServer {
BackupServer({

View file

@ -15,28 +15,31 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
)
..avatarSvg = json['avatarSvg'] as String?
..avatarJson = json['avatarJson'] as String?
..avatarCounter = (json['avatarCounter'] as num?)?.toInt()
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
..lastImageSend = json['lastImageSend'] == null
? null
: DateTime.parse(json['lastImageSend'] as String)
..todaysImageCounter = (json['todaysImageCounter'] as num?)?.toInt()
..themeMode =
$enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
ThemeMode.system
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
..useHighQuality = json['useHighQuality'] as bool?
..useHighQuality = json['useHighQuality'] as bool? ?? true
..preSelectedEmojies = (json['preSelectedEmojies'] as List<dynamic>?)
?.map((e) => e as String)
.toList()
..themeMode = $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode'])
..autoDownloadOptions =
(json['autoDownloadOptions'] as Map<String, dynamic>?)?.map(
(k, e) =>
MapEntry(k, (e as List<dynamic>).map((e) => e as String).toList()),
)
..storeMediaFilesInGallery = json['storeMediaFilesInGallery'] as bool?
..storeMediaFilesInGallery =
json['storeMediaFilesInGallery'] as bool? ?? false
..lastUsedEditorEmojis = (json['lastUsedEditorEmojis'] as List<dynamic>?)
?.map((e) => e as String)
.toList()
..lastPlanBallance = json['lastPlanBallance'] as String?
..additionalUserInvites = json['additionalUserInvites'] as String?
..lastImageSend = json['lastImageSend'] == null
? null
: DateTime.parse(json['lastImageSend'] as String)
..todaysImageCounter = (json['todaysImageCounter'] as num?)?.toInt()
..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
?.map((e) => e as String)
.toList()
@ -45,26 +48,16 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
json['signalLastSignedPreKeyUpdated'] == null
? null
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
..identityBackupEnabled = json['identityBackupEnabled'] as bool? ?? false
..identityBackupLastBackupTime =
json['identityBackupLastBackupTime'] == null
? null
: DateTime.parse(json['identityBackupLastBackupTime'] as String)
..identityBackupLastBackupSize =
(json['identityBackupLastBackupSize'] as num?)?.toInt() ?? 0
..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null
? null
: DateTime.parse(json['nextTimeToShowBackupNotice'] as String)
..backupServer = json['backupServer'] == null
? null
: BackupServer.fromJson(json['backupServer'] as Map<String, dynamic>)
..twonlySafeEncryptionKey =
(json['twonlySafeEncryptionKey'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList()
..twonlySafeBackupId = (json['twonlySafeBackupId'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList();
..twonlySafeBackup = json['twonlySafeBackup'] == null
? null
: TwonlySafeBackup.fromJson(
json['twonlySafeBackup'] as Map<String, dynamic>);
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'userId': instance.userId,
@ -74,31 +67,26 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'avatarSvg': instance.avatarSvg,
'avatarJson': instance.avatarJson,
'avatarCounter': instance.avatarCounter,
'defaultShowTime': instance.defaultShowTime,
'subscriptionPlan': instance.subscriptionPlan,
'lastImageSend': instance.lastImageSend?.toIso8601String(),
'todaysImageCounter': instance.todaysImageCounter,
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'defaultShowTime': instance.defaultShowTime,
'useHighQuality': instance.useHighQuality,
'preSelectedEmojies': instance.preSelectedEmojies,
'themeMode': _$ThemeModeEnumMap[instance.themeMode],
'autoDownloadOptions': instance.autoDownloadOptions,
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
'lastUsedEditorEmojis': instance.lastUsedEditorEmojis,
'lastPlanBallance': instance.lastPlanBallance,
'additionalUserInvites': instance.additionalUserInvites,
'lastImageSend': instance.lastImageSend?.toIso8601String(),
'todaysImageCounter': instance.todaysImageCounter,
'tutorialDisplayed': instance.tutorialDisplayed,
'myBestFriendContactId': instance.myBestFriendContactId,
'signalLastSignedPreKeyUpdated':
instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
'identityBackupEnabled': instance.identityBackupEnabled,
'identityBackupLastBackupTime':
instance.identityBackupLastBackupTime?.toIso8601String(),
'identityBackupLastBackupSize': instance.identityBackupLastBackupSize,
'nextTimeToShowBackupNotice':
instance.nextTimeToShowBackupNotice?.toIso8601String(),
'backupServer': instance.backupServer,
'twonlySafeEncryptionKey': instance.twonlySafeEncryptionKey,
'twonlySafeBackupId': instance.twonlySafeBackupId,
'twonlySafeBackup': instance.twonlySafeBackup,
};
const _$ThemeModeEnumMap = {
@ -107,6 +95,39 @@ const _$ThemeModeEnumMap = {
ThemeMode.dark: 'dark',
};
TwonlySafeBackup _$TwonlySafeBackupFromJson(Map<String, dynamic> json) =>
TwonlySafeBackup(
backupId: (json['backupId'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
encryptionKey: (json['encryptionKey'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
)
..lastBackupSize = (json['lastBackupSize'] as num).toInt()
..backupUploadState =
$enumDecode(_$LastBackupUploadStateEnumMap, json['backupUploadState'])
..lastBackupDone = json['lastBackupDone'] == null
? null
: DateTime.parse(json['lastBackupDone'] as String);
Map<String, dynamic> _$TwonlySafeBackupToJson(TwonlySafeBackup instance) =>
<String, dynamic>{
'lastBackupSize': instance.lastBackupSize,
'backupUploadState':
_$LastBackupUploadStateEnumMap[instance.backupUploadState]!,
'lastBackupDone': instance.lastBackupDone?.toIso8601String(),
'backupId': instance.backupId,
'encryptionKey': instance.encryptionKey,
};
const _$LastBackupUploadStateEnumMap = {
LastBackupUploadState.none: 'none',
LastBackupUploadState.pending: 'pending',
LastBackupUploadState.failed: 'failed',
LastBackupUploadState.success: 'success',
};
BackupServer _$BackupServerFromJson(Map<String, dynamic> json) => BackupServer(
serverUrl: json['serverUrl'] as String,
retentionDays: (json['retentionDays'] as num).toInt(),

View file

@ -45,6 +45,7 @@ class ErrorCode extends $pb.ProtobufEnum {
static const ErrorCode PlanUpgradeNotYearly = ErrorCode._(1026, _omitEnumNames ? '' : 'PlanUpgradeNotYearly');
static const ErrorCode InvalidSignedPreKey = ErrorCode._(1027, _omitEnumNames ? '' : 'InvalidSignedPreKey');
static const ErrorCode UserIdNotFound = ErrorCode._(1028, _omitEnumNames ? '' : 'UserIdNotFound');
static const ErrorCode UserIdAlreadyTaken = ErrorCode._(1029, _omitEnumNames ? '' : 'UserIdAlreadyTaken');
static const $core.List<ErrorCode> values = <ErrorCode> [
Unknown,
@ -78,6 +79,7 @@ class ErrorCode extends $pb.ProtobufEnum {
PlanUpgradeNotYearly,
InvalidSignedPreKey,
UserIdNotFound,
UserIdAlreadyTaken,
];
static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values);

View file

@ -48,6 +48,7 @@ const ErrorCode$json = {
{'1': 'PlanUpgradeNotYearly', '2': 1026},
{'1': 'InvalidSignedPreKey', '2': 1027},
{'1': 'UserIdNotFound', '2': 1028},
{'1': 'UserIdAlreadyTaken', '2': 1029},
],
};
@ -66,5 +67,6 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
'Cg5JbnZhbGlkUHJlS2V5cxD8BxITCg5Wb3VjaGVySW5WYWxpZBD9BxITCg5QbGFuTm90QWxsb3'
'dlZBD+BxIVChBQbGFuTGltaXRSZWFjaGVkEP8HEhQKD05vdEVub3VnaENyZWRpdBCACBISCg1Q'
'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW'
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAg=');
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
'EIUI');

View file

@ -0,0 +1,160 @@
//
// Generated code. Do not modify.
// source: backup.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class TwonlySafeBackupContent extends $pb.GeneratedMessage {
factory TwonlySafeBackupContent({
$core.String? secureStorageJson,
$core.List<$core.int>? twonlyDatabase,
}) {
final $result = create();
if (secureStorageJson != null) {
$result.secureStorageJson = secureStorageJson;
}
if (twonlyDatabase != null) {
$result.twonlyDatabase = twonlyDatabase;
}
return $result;
}
TwonlySafeBackupContent._() : super();
factory TwonlySafeBackupContent.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory TwonlySafeBackupContent.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TwonlySafeBackupContent', createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'secureStorageJson', protoName: 'secureStorageJson')
..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'twonlyDatabase', $pb.PbFieldType.OY, protoName: 'twonlyDatabase')
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
TwonlySafeBackupContent clone() => TwonlySafeBackupContent()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
TwonlySafeBackupContent copyWith(void Function(TwonlySafeBackupContent) updates) => super.copyWith((message) => updates(message as TwonlySafeBackupContent)) as TwonlySafeBackupContent;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static TwonlySafeBackupContent create() => TwonlySafeBackupContent._();
TwonlySafeBackupContent createEmptyInstance() => create();
static $pb.PbList<TwonlySafeBackupContent> createRepeated() => $pb.PbList<TwonlySafeBackupContent>();
@$core.pragma('dart2js:noInline')
static TwonlySafeBackupContent getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<TwonlySafeBackupContent>(create);
static TwonlySafeBackupContent? _defaultInstance;
@$pb.TagNumber(1)
$core.String get secureStorageJson => $_getSZ(0);
@$pb.TagNumber(1)
set secureStorageJson($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1)
$core.bool hasSecureStorageJson() => $_has(0);
@$pb.TagNumber(1)
void clearSecureStorageJson() => clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get twonlyDatabase => $_getN(1);
@$pb.TagNumber(2)
set twonlyDatabase($core.List<$core.int> v) { $_setBytes(1, v); }
@$pb.TagNumber(2)
$core.bool hasTwonlyDatabase() => $_has(1);
@$pb.TagNumber(2)
void clearTwonlyDatabase() => clearField(2);
}
class TwonlySafeBackupEncrypted extends $pb.GeneratedMessage {
factory TwonlySafeBackupEncrypted({
$core.List<$core.int>? mac,
$core.List<$core.int>? nonce,
$core.List<$core.int>? cipherText,
}) {
final $result = create();
if (mac != null) {
$result.mac = mac;
}
if (nonce != null) {
$result.nonce = nonce;
}
if (cipherText != null) {
$result.cipherText = cipherText;
}
return $result;
}
TwonlySafeBackupEncrypted._() : super();
factory TwonlySafeBackupEncrypted.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory TwonlySafeBackupEncrypted.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TwonlySafeBackupEncrypted', createEmptyInstance: create)
..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'mac', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'cipherText', $pb.PbFieldType.OY, protoName: 'cipherText')
..hasRequiredFields = false
;
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
TwonlySafeBackupEncrypted clone() => TwonlySafeBackupEncrypted()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
TwonlySafeBackupEncrypted copyWith(void Function(TwonlySafeBackupEncrypted) updates) => super.copyWith((message) => updates(message as TwonlySafeBackupEncrypted)) as TwonlySafeBackupEncrypted;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static TwonlySafeBackupEncrypted create() => TwonlySafeBackupEncrypted._();
TwonlySafeBackupEncrypted createEmptyInstance() => create();
static $pb.PbList<TwonlySafeBackupEncrypted> createRepeated() => $pb.PbList<TwonlySafeBackupEncrypted>();
@$core.pragma('dart2js:noInline')
static TwonlySafeBackupEncrypted getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<TwonlySafeBackupEncrypted>(create);
static TwonlySafeBackupEncrypted? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get mac => $_getN(0);
@$pb.TagNumber(1)
set mac($core.List<$core.int> v) { $_setBytes(0, v); }
@$pb.TagNumber(1)
$core.bool hasMac() => $_has(0);
@$pb.TagNumber(1)
void clearMac() => clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get nonce => $_getN(1);
@$pb.TagNumber(2)
set nonce($core.List<$core.int> v) { $_setBytes(1, v); }
@$pb.TagNumber(2)
$core.bool hasNonce() => $_has(1);
@$pb.TagNumber(2)
void clearNonce() => clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get cipherText => $_getN(2);
@$pb.TagNumber(3)
set cipherText($core.List<$core.int> v) { $_setBytes(2, v); }
@$pb.TagNumber(3)
$core.bool hasCipherText() => $_has(2);
@$pb.TagNumber(3)
void clearCipherText() => clearField(3);
}
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');

View file

@ -0,0 +1,11 @@
//
// Generated code. Do not modify.
// source: backup.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import

View file

@ -0,0 +1,44 @@
//
// Generated code. Do not modify.
// source: backup.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use twonlySafeBackupContentDescriptor instead')
const TwonlySafeBackupContent$json = {
'1': 'TwonlySafeBackupContent',
'2': [
{'1': 'secureStorageJson', '3': 1, '4': 1, '5': 9, '10': 'secureStorageJson'},
{'1': 'twonlyDatabase', '3': 2, '4': 1, '5': 12, '10': 'twonlyDatabase'},
],
};
/// Descriptor for `TwonlySafeBackupContent`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List twonlySafeBackupContentDescriptor = $convert.base64Decode(
'ChdUd29ubHlTYWZlQmFja3VwQ29udGVudBIsChFzZWN1cmVTdG9yYWdlSnNvbhgBIAEoCVIRc2'
'VjdXJlU3RvcmFnZUpzb24SJgoOdHdvbmx5RGF0YWJhc2UYAiABKAxSDnR3b25seURhdGFiYXNl');
@$core.Deprecated('Use twonlySafeBackupEncryptedDescriptor instead')
const TwonlySafeBackupEncrypted$json = {
'1': 'TwonlySafeBackupEncrypted',
'2': [
{'1': 'mac', '3': 1, '4': 1, '5': 12, '10': 'mac'},
{'1': 'nonce', '3': 2, '4': 1, '5': 12, '10': 'nonce'},
{'1': 'cipherText', '3': 3, '4': 1, '5': 12, '10': 'cipherText'},
],
};
/// Descriptor for `TwonlySafeBackupEncrypted`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List twonlySafeBackupEncryptedDescriptor = $convert.base64Decode(
'ChlUd29ubHlTYWZlQmFja3VwRW5jcnlwdGVkEhAKA21hYxgBIAEoDFIDbWFjEhQKBW5vbmNlGA'
'IgASgMUgVub25jZRIeCgpjaXBoZXJUZXh0GAMgASgMUgpjaXBoZXJUZXh0');

View file

@ -0,0 +1,14 @@
//
// Generated code. Do not modify.
// source: backup.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'backup.pb.dart';

View file

@ -0,0 +1,12 @@
syntax = "proto3";
message TwonlySafeBackupContent {
string secureStorageJson = 1;
bytes twonlyDatabase = 2;
}
message TwonlySafeBackupEncrypted {
bytes mac = 1;
bytes nonce = 2;
bytes cipherText = 3;
}

View file

@ -21,6 +21,7 @@ import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/services/api/media_received.dart';
import 'package:twonly/src/services/backup.identitiy.service.dart';
import 'package:twonly/src/services/notification.service.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/utils/log.dart';
@ -67,6 +68,9 @@ Future initFileDownloader() async {
if (update.task.taskId.contains("download_")) {
await handleDownloadStatusUpdate(update);
}
if (update.task.taskId.contains("backup")) {
await handleBackupStatusUpdate(update);
}
case TaskProgressUpdate():
Log.info(
'Progress update for ${update.task} with progress ${update.progress}');

View file

@ -230,13 +230,12 @@ Future notifyContactsAboutProfileChange() async {
UserData? user = await getUser();
if (user == null) return;
if (user.avatarCounter == null) return;
if (user.avatarSvg == null) return;
for (Contact contact in contacts) {
if (contact.myAvatarCounter < user.avatarCounter!) {
if (contact.myAvatarCounter < user.avatarCounter) {
twonlyDB.contactsDao.updateContact(contact.userId,
ContactsCompanion(myAvatarCounter: Value(user.avatarCounter!)));
ContactsCompanion(myAvatarCounter: Value(user.avatarCounter)));
await encryptAndSendMessageAsync(
null,
contact.userId,

View file

@ -1,34 +1,50 @@
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive_io.dart';
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/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hashlib/hashlib.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_send.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() async {
Future performTwonlySafeBackup({bool force = false}) async {
Log.info("Starting new backup creation.");
final user = await getUser();
if (user == null || user.twonlySafeBackup == null) {
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;
}
final baseDir = (await getApplicationSupportDirectory()).path;
var originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
var backupDir = Directory(join(baseDir, "backup_twonly_safe/"));
if (backupDir.existsSync()) {
await backupDir.delete(recursive: true);
}
final backupDir = Directory(join(baseDir, "backup_twonly_safe/"));
await backupDir.create(recursive: true);
var backupDatabaseFile =
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",
@ -48,30 +64,145 @@ Future performTwonlySafeBackup() async {
await storage.read(key: SecureStorageKeys.signalIdentity);
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
await storage.read(key: SecureStorageKeys.signalSignedPreKey);
secureStorageBackup[SecureStorageKeys.userData] =
await storage.read(key: SecureStorageKeys.userData);
var backupSecureStorage = File(join(backupDir.path, "secure_storage.json"));
var userBackup = await getUser();
if (userBackup == null) return;
// FILTER settings which should not be in the backup
userBackup.twonlySafeBackup = null;
await backupSecureStorage.writeAsString(jsonEncode(secureStorageBackup));
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 twonlySafeBackupZip = File(join(backupDir.path, "twonly_safe.zip"));
var encryptedBackupBytesFile =
File(join(backupDir.path, "twonly_safe.backup"));
await createZipArchive(
twonlySafeBackupZip.path, [backupSecureStorage, backupDatabaseFile]);
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
// await backupDir.delete(recursive: true);
Log.info(
"Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.");
String backupServerUrl = "https://safe.twonly.eu/";
if (user.backupServer != null) {
backupServerUrl = user.backupServer!.serverUrl;
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;
}
}
String backupId =
uint8ListToHex(user.twonlySafeBackup!.backupId).toLowerCase();
final task = UploadTask.fromFile(
taskId: "backup",
file: encryptedBackupBytesFile,
httpRequestMethod: "PUT",
url: "${backupServerUrl}backups/$backupId",
requiresWiFi: true,
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<void> createZipArchive(String zipFilePath, List<File> filesToZip) async {
final encoder = ZipFileEncoder();
encoder.create(zipFilePath);
for (var file in filesToZip) {
await encoder.addFile(file);
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;
}
await encoder.close();
gUpdateBackupView();
}
Future enableTwonlySafe(String password) async {
@ -81,9 +212,10 @@ Future enableTwonlySafe(String password) async {
final (backupId, encryptionKey) = await getMasterKey(password, user.username);
await updateUserdata((user) {
user.identityBackupEnabled = true;
user.twonlySafeBackupId = backupId.toList();
user.twonlySafeEncryptionKey = encryptionKey.toList();
user.twonlySafeBackup = TwonlySafeBackup(
encryptionKey: encryptionKey,
backupId: backupId,
);
return user;
});
startTwonlySafeBackup();
@ -92,11 +224,7 @@ Future enableTwonlySafe(String password) async {
Future disableTwonlySafe() async {
await updateUserdata((user) {
user.identityBackupEnabled = false;
user.twonlySafeBackupId = null;
user.twonlySafeEncryptionKey = null;
user.identityBackupLastBackupTime = null;
user.identityBackupLastBackupSize = 0;
user.twonlySafeBackup = null;
return user;
});
}

View file

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:gal/gal.dart';
import 'package:intl/intl.dart';
import 'package:local_auth/local_auth.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
@ -384,3 +385,31 @@ List<List<dynamic>> chatMessages = [
],
["Curabitur blandit tempus porttitor.", DateTime.now()],
];
String formatDateTime(BuildContext context, DateTime? dateTime) {
if (dateTime == null) {
return "Never";
}
final now = DateTime.now();
final difference = now.difference(dateTime);
final date = DateFormat.yMd(Localizations.localeOf(context).toLanguageTag())
.format(dateTime);
final time = DateFormat.Hm(Localizations.localeOf(context).toLanguageTag())
.format(dateTime);
if (difference.inDays == 0) {
return time;
} else {
return "$time $date";
}
}
String formatBytes(int bytes, {int decimalPlaces = 2}) {
if (bytes <= 0) return "0 Bytes";
const List<String> units = ["Bytes", "KB", "MB", "GB", "TB"];
final int unitIndex = (log(bytes) / log(1000)).floor();
final double formattedSize = bytes / pow(1000, unitIndex);
return "${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}";
}

View file

@ -63,8 +63,8 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
memoryPath = join(memoryPath, token);
}
final user = await getUser();
if (user != null && (user.storeMediaFilesInGallery ?? true)) {}
bool storeToGallery = user?.storeMediaFilesInGallery ?? true;
if (user != null) return;
bool storeToGallery = user!.storeMediaFilesInGallery;
if (widget.videoFilePath != null) {
memoryPath += ".mp4";

View file

@ -176,9 +176,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
void initAsync() async {
final user = await getUser();
if (user == null) return;
if (user.useHighQuality != null) {
useHighQuality = user.useHighQuality!;
}
useHighQuality = user.useHighQuality;
hasAudioPermission = await Permission.microphone.isGranted;
if (!mounted) return;
setState(() {});

View file

@ -26,7 +26,7 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
if (user != null &&
(user.nextTimeToShowBackupNotice == null ||
DateTime.now().isAfter(user.nextTimeToShowBackupNotice!))) {
if (!gIsDemoUser && (!user.identityBackupEnabled)) {
if (!gIsDemoUser && (user.twonlySafeBackup == null)) {
showBackupNotice = true;
}
}

View file

@ -320,7 +320,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
imageSaved = true;
});
final user = await getUser();
if (user != null && (user.storeMediaFilesInGallery ?? true)) {
if (user != null && (user.storeMediaFilesInGallery)) {
if (videoPath != null) {
await saveVideoToGallery(videoPath!);
} else {

View file

@ -5,8 +5,10 @@ import 'package:twonly/globals.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/utils/log.dart';
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';
@ -43,6 +45,11 @@ class _RegisterViewState extends State<RegisterView> {
Log.info("Got user_id ${res.value} from server");
userId = res.value.userid.toInt();
} else {
if (res.error == ErrorCode.UserIdAlreadyTaken) {
Log.error("User ID already token. Tying again.");
await deleteLocalUserData();
return createNewUser();
}
if (mounted) {
showAlertDialog(
context,

View file

@ -1,10 +1,14 @@
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/backup.identitiy.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart';
Function() gUpdateBackupView = () {};
class BackupView extends StatefulWidget {
const BackupView({super.key});
@ -12,25 +16,54 @@ class BackupView extends StatefulWidget {
State<BackupView> createState() => _BackupViewState();
}
BackupServer defaultBackupServer = BackupServer(
serverUrl: "Default",
retentionDays: 180,
maxBackupBytes: 2097152,
);
class _BackupViewState extends State<BackupView> {
bool _twonlyIdBackupEnabled = false;
DateTime? _twonlyIdLastBackup;
bool _dataBackupEnabled = false;
DateTime? _dataBackupLastBackup;
TwonlySafeBackup? twonlySafeBackup;
BackupServer backupServer = defaultBackupServer;
int activePageIdx = 0;
final PageController pageController =
PageController(keepPage: true, initialPage: 0);
@override
void initState() {
initAsync();
super.initState();
gUpdateBackupView = initAsync;
}
@override
void dispose() {
gUpdateBackupView = () {};
super.dispose();
}
Future initAsync() async {
final user = await getUser();
if (user != null) {
_twonlyIdBackupEnabled = user.identityBackupEnabled;
_twonlyIdLastBackup = user.identityBackupLastBackupTime;
_dataBackupEnabled = false;
setState(() {});
twonlySafeBackup = user?.twonlySafeBackup;
backupServer = defaultBackupServer;
if (user?.backupServer != null) {
backupServer = user!.backupServer!;
}
setState(() {});
}
String backupStatus(LastBackupUploadState status) {
switch (status) {
case LastBackupUploadState.none:
return '-';
case LastBackupUploadState.pending:
return 'Pending';
case LastBackupUploadState.failed:
return 'Failed';
case LastBackupUploadState.success:
return 'Success';
}
}
@ -40,16 +73,73 @@ class _BackupViewState extends State<BackupView> {
appBar: AppBar(
title: Text(context.lang.settingsBackup),
),
body: ListView(
body: PageView(
controller: pageController,
onPageChanged: (index) {
setState(() {
activePageIdx = index;
});
},
children: [
BackupOption(
title: 'twonly Safe',
description:
'Back up your twonly identity, as this is the only way to restore your account if you uninstall or lose your phone.',
lastBackup: _twonlyIdLastBackup,
autoBackupEnabled: _twonlyIdBackupEnabled,
autoBackupEnabled: twonlySafeBackup != null,
child: (twonlySafeBackup != null)
? Table(
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
...[
(
"Server",
(backupServer.serverUrl.contains("@"))
? backupServer.serverUrl.split("@")[1]
: backupServer.serverUrl
.replaceAll("https://", "")
),
(
"Max. Backup-Größe",
formatBytes(backupServer.maxBackupBytes)
),
("Speicherdauer", "${backupServer.retentionDays} Days"),
(
"Letztes Backup",
formatDateTime(
context, twonlySafeBackup!.lastBackupDone)
),
(
"Backup-Größe",
formatBytes(twonlySafeBackup!.lastBackupSize)
),
(
"Ergebnis",
backupStatus(twonlySafeBackup!.backupUploadState)
)
].map((pair) {
return TableRow(
children: [
TableCell(
// padding: EdgeInsets.all(4),
child: Text(pair.$1),
),
TableCell(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Text(
pair.$2,
textAlign: TextAlign.right,
),
),
),
],
);
}),
],
)
: null,
onTap: () async {
if (_twonlyIdBackupEnabled) {
if (twonlySafeBackup != null) {
bool disable = await showAlertDialog(context, "Are you sure?",
"Without an backup, you can not restore your user account.");
if (disable) {
@ -68,12 +158,41 @@ class _BackupViewState extends State<BackupView> {
title: 'Daten-Backup (Coming Soon)',
description:
'This backup contains besides of your twonly-Identity also all of your media files. This backup will also be encrypted using a password chosen by the user but stored locally on the smartphone. You then have to ensure to manually copy it onto your laptop or device of your choice.',
autoBackupEnabled: _dataBackupEnabled,
onTap: () {},
lastBackup: _dataBackupLastBackup,
autoBackupEnabled: false,
onTap: null,
),
],
),
bottomNavigationBar: BottomNavigationBar(
showSelectedLabels: true,
showUnselectedLabels: true,
unselectedIconTheme: IconThemeData(
color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150)),
selectedIconTheme:
IconThemeData(color: Theme.of(context).colorScheme.inverseSurface),
items: [
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.vault, size: 17),
label: "twonly Safe",
),
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.boxArchive, size: 17),
label: "Daten-Backup",
),
],
onTap: (int index) {
activePageIdx = index;
setState(() {
pageController.animateToPage(
index,
duration: const Duration(milliseconds: 100),
curve: Curves.bounceIn,
);
});
},
currentIndex: activePageIdx,
// ),
),
);
}
}
@ -83,41 +202,23 @@ class BackupOption extends StatelessWidget {
final String description;
final Widget? child;
final bool autoBackupEnabled;
final DateTime? lastBackup;
final Function() onTap;
final Function()? onTap;
const BackupOption({
super.key,
required this.title,
required this.description,
required this.autoBackupEnabled,
required this.lastBackup,
required this.onTap,
this.child,
});
String formatDateTime(DateTime? dateTime) {
if (dateTime == null) {
return "Never";
}
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inDays == 0) {
return 'Today';
} else if (difference.inDays == 1) {
return 'Yesterday';
} else {
return '${difference.inDays} Days ago';
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (autoBackupEnabled) ? null : onTap,
child: Card(
margin: EdgeInsets.all(8.0),
margin: EdgeInsets.all(16.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@ -131,21 +232,18 @@ class BackupOption extends StatelessWidget {
Text(description),
SizedBox(height: 8.0),
(child != null) ? child! : Container(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Last backup: ${formatDateTime(lastBackup)}'),
(autoBackupEnabled)
? OutlinedButton(
onPressed: onTap,
child: Text("Disable"),
)
: FilledButton(
onPressed: onTap,
child: Text("Enable"),
)
],
),
Expanded(child: Container()),
Center(
child: (autoBackupEnabled)
? OutlinedButton(
onPressed: onTap,
child: Text("Disable"),
)
: FilledButton(
onPressed: onTap,
child: Text("Enable"),
),
)
],
),
),

View file

@ -27,7 +27,7 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
setState(() {
autoDownloadOptions =
user.autoDownloadOptions ?? defaultAutoDownloadOptions;
storeMediaFilesInGallery = user.storeMediaFilesInGallery ?? true;
storeMediaFilesInGallery = user.storeMediaFilesInGallery;
});
}

View file

@ -14,11 +14,7 @@ class ModifyAvatar extends StatelessWidget {
await updateUserdata((user) {
user.avatarJson = json;
user.avatarSvg = svg;
if (user.avatarCounter == null) {
user.avatarCounter = 1;
} else {
user.avatarCounter = user.avatarCounter! + 1;
}
user.avatarCounter = user.avatarCounter + 1;
return user;
});
await notifyContactsAboutProfileChange();

View file

@ -32,11 +32,7 @@ class _ProfileViewState extends State<ProfileView> {
Future updateUserDisplayName(String displayName) async {
await updateUserdata((user) {
user.displayName = displayName;
if (user.avatarCounter == null) {
user.avatarCounter = 1;
} else {
user.avatarCounter = user.avatarCounter! + 1;
}
user.avatarCounter = user.avatarCounter + 1;
return user;
});

View file

@ -67,7 +67,6 @@ dependencies:
tutorial_coach_mark: ^1.3.0
background_downloader: ^9.2.2
hashlib: ^2.0.0
archive: ^4.0.7
dev_dependencies:
flutter_test: