Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
builder: (_) => StackOkDialog(
title: "Backup creation failed",
message: ex?.toString() ?? "Unexpected error",
desktopPopRootNavigator: true,
),
);
}
Expand Down Expand Up @@ -232,9 +233,9 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
builder: (child) {
return Background(
child: Scaffold(
backgroundColor: Theme.of(
context,
).extension<StackColors>()!.background,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
Expand Down Expand Up @@ -286,9 +287,9 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
"Choose file location",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textDark3,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
),
),
),
Expand Down Expand Up @@ -338,9 +339,9 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
const SizedBox(width: 16),
SvgPicture.asset(
Assets.svg.folder,
color: Theme.of(
context,
).extension<StackColors>()!.textDark3,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
Expand Down Expand Up @@ -374,9 +375,9 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
"Create a passphrase",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textDark3,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
),
textAlign: TextAlign.left,
),
Expand Down Expand Up @@ -419,9 +420,9 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(
context,
).extension<StackColors>()!.textDark3,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
Expand Down Expand Up @@ -494,19 +495,19 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
width: MediaQuery.of(context).size.width - 32 - 24,
height: 5,
fillColor: passwordStrength < 0.51
? Theme.of(
context,
).extension<StackColors>()!.accentColorRed
? Theme.of(context)
.extension<StackColors>()!
.accentColorRed
: passwordStrength < 1
? Theme.of(
context,
).extension<StackColors>()!.accentColorYellow
: Theme.of(
context,
).extension<StackColors>()!.accentColorGreen,
backgroundColor: Theme.of(
context,
).extension<StackColors>()!.buttonBackSecondary,
? Theme.of(context)
.extension<StackColors>()!
.accentColorYellow
: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
percent: passwordStrength < 0.25 ? 0.03 : passwordStrength,
),
),
Expand Down Expand Up @@ -549,9 +550,9 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(
context,
).extension<StackColors>()!.textDark3,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 16,
height: 16,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import '../../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface
import '../../../../../wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../../../wl_gen/interfaces/frost_interface.dart';
import 'swb_view_only_recovery_data.dart';

class PreRestoreState {
final Set<String> walletIds;
Expand Down Expand Up @@ -296,9 +297,27 @@ abstract class SWB {
backupWallet['isFavorite'] = wallet.info.isFavourite;
backupWallet['otherDataJsonString'] = wallet.info.otherDataJsonString;

if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
SWBViewOnlyRecoveryData? viewOnlyRecoveryData;
if (wallet is ViewOnlyOptionInterface) {
final rawViewOnlyData = await _secureStore.read(
key: Wallet.getViewOnlyWalletDataSecStoreKey(
walletId: wallet.walletId,
),
);
viewOnlyRecoveryData = normalizeSWBViewOnlyRecoveryData(
operation: "back up",
walletName: wallet.info.name,
walletId: wallet.walletId,
otherData: wallet.info.otherData,
encodedData: rawViewOnlyData,
);
}

if (viewOnlyRecoveryData != null) {
backupWallet['viewOnlyWalletDataKey'] =
(await wallet.getViewOnlyWalletData()).toJsonEncodedString();
viewOnlyRecoveryData.encodedData;
backupWallet['otherDataJsonString'] =
viewOnlyRecoveryData.otherDataJsonString;
} else if (wallet is MnemonicInterface) {
backupWallet['mnemonic'] = await wallet.getMnemonic();
backupWallet['mnemonicPassphrase'] = await wallet
Expand Down Expand Up @@ -399,14 +418,19 @@ abstract class SWB {
String? mnemonic, mnemonicPassphrase, privateKey;

ViewOnlyWalletData? viewOnlyData;
if (info.isViewOnly) {
if (walletbackup['viewOnlyWalletDataKey'] is String) {
final viewOnlyDataEncoded =
walletbackup['viewOnlyWalletDataKey'] as String;

viewOnlyData = ViewOnlyWalletData.fromJsonEncodedString(
viewOnlyDataEncoded,
walletId: info.walletId,
);
} else if (info.isViewOnly) {
throw FormatException(
'Cannot restore view-only wallet "${info.name}": '
'recovery data is missing',
);
} else if (walletbackup['mnemonic'] == null) {
// probably private key based
if (walletbackup['privateKey'] != null) {
Expand Down Expand Up @@ -700,8 +724,34 @@ abstract class SWB {
SecureStorageInterface secureStorageInterface,
ShopInBitService shopinbitService,
) async {
if (!Platform.isLinux) await WakelockPlus.enable();
if (Platform.isLinux) {
return _restoreStackWalletJSON(
jsonBackup,
uiState,
secureStorageInterface,
shopinbitService,
);
}

await WakelockPlus.enable();
try {
return await _restoreStackWalletJSON(
jsonBackup,
uiState,
secureStorageInterface,
shopinbitService,
);
} finally {
await WakelockPlus.disable();
}
}

static Future<bool?> _restoreStackWalletJSON(
String jsonBackup,
StackRestoringUIState? uiState,
SecureStorageInterface secureStorageInterface,
ShopInBitService shopinbitService,
) async {
Logging.instance.d("SWB creating temp backup");
final preRestoreJSON = await createStackWalletJSON(
secureStorage: secureStorageInterface,
Expand All @@ -724,6 +774,7 @@ abstract class SWB {
json.decode(jsonBackup) as Map<String, dynamic>;

final List<dynamic> wallets = validJSON["wallets"] as List;
normalizeSWBViewOnlyWalletBackups(wallets);

// check for duplicate walletIds and assign new ones if required
for (final wallet in wallets) {
Expand Down Expand Up @@ -912,7 +963,6 @@ abstract class SWB {
await status;
}

if (!Platform.isLinux) await WakelockPlus.disable();
// check if cancel was requested and restore previous state
if (_checkShouldCancel(
preRestoreState,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import 'dart:convert';

import '../../../../../models/keys/view_only_wallet_data.dart';
import '../../../../../wallets/isar/models/wallet_info.dart';

class SWBViewOnlyRecoveryData {
final String encodedData;
final Map<String, dynamic> otherData;

const SWBViewOnlyRecoveryData({
required this.encodedData,
required this.otherData,
});

String get otherDataJsonString => jsonEncode(otherData);
}

void normalizeSWBViewOnlyWalletBackups(List<dynamic> wallets) {
for (final entry in wallets) {
if (entry is! Map) {
throw const FormatException("Cannot restore invalid wallet record");
}
final wallet = Map<String, dynamic>.from(entry);
final walletName = wallet["name"];
final walletId = wallet["id"];
if (walletName is! String || walletId is! String) {
throw const FormatException("Cannot restore invalid wallet record");
}

Map<String, dynamic>? otherData;
if (wallet["otherDataJsonString"] is String) {
try {
final decoded = jsonDecode(wallet["otherDataJsonString"] as String);
otherData = Map<String, dynamic>.from(decoded as Map);
} catch (_) {
if (wallet["viewOnlyWalletDataKey"] == null) {
continue;
}
}
}

final normalized = normalizeSWBViewOnlyRecoveryData(
operation: "restore",
walletName: walletName,
walletId: walletId,
otherData: otherData,
encodedData: wallet["viewOnlyWalletDataKey"],
);
if (normalized != null) {
entry["otherDataJsonString"] = normalized.otherDataJsonString;
}
}
}

SWBViewOnlyRecoveryData? normalizeSWBViewOnlyRecoveryData({
required String operation,
required String walletName,
required String walletId,
required Map<String, dynamic>? otherData,
required Object? encodedData,
}) {
final declaresViewOnly = otherData?[WalletInfoKeys.isViewOnlyKey] == true;

if (encodedData == null) {
if (declaresViewOnly) {
throw FormatException(
'Cannot $operation view-only wallet "$walletName": '
'recovery data is missing',
);
}
return null;
}

if (encodedData is! String) {
throw FormatException(
'Cannot $operation view-only wallet "$walletName": '
'recovery data is invalid',
);
}

final ViewOnlyWalletData data;
try {
data = ViewOnlyWalletData.fromJsonEncodedString(
encodedData,
walletId: walletId,
);
} catch (_) {
throw FormatException(
'Cannot $operation view-only wallet "$walletName": '
'recovery data is invalid',
);
}

if (!_hasRequiredRecoveryData(data)) {
throw FormatException(
'Cannot $operation view-only wallet "$walletName": '
'recovery data is incomplete',
);
}

final storedType = otherData?[WalletInfoKeys.viewOnlyTypeIndexKey];
if (storedType != null &&
(storedType is! int || storedType != data.type.index)) {
throw FormatException(
'Cannot $operation view-only wallet "$walletName": '
'recovery metadata conflicts with its data',
);
}

final normalized = Map<String, dynamic>.from(otherData ?? const {})
..[WalletInfoKeys.isViewOnlyKey] = true
..[WalletInfoKeys.viewOnlyTypeIndexKey] = data.type.index;

return SWBViewOnlyRecoveryData(
encodedData: encodedData,
otherData: Map.unmodifiable(normalized),
);
}

bool _hasRequiredRecoveryData(ViewOnlyWalletData data) => switch (data) {
CryptonoteViewOnlyWalletData() =>
data.address.isNotEmpty && data.privateViewKey.isNotEmpty,
AddressViewOnlyWalletData() => data.address.isNotEmpty,
ExtendedKeysViewOnlyWalletData() =>
data.xPubs.isNotEmpty &&
data.xPubs.every((xPub) => xPub.encoded.isNotEmpty),
SparkViewOnlyWalletData() => data.viewKey.isNotEmpty,
};
Loading
Loading