Export in encrypted format + import from Ravio (#168)

This commit is contained in:
Neeraj Gupta 2023-07-31 16:06:51 +05:30 committed by GitHub
commit fb8aa013fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 685 additions and 281 deletions

View file

@ -73,6 +73,11 @@
"changePassword": "Change password",
"data" : "Data",
"importCodes": "Import codes",
"importTypePlainText": "Plain text",
"importTypeEnteEncrypted": "ente Encrypted export",
"importFromApp": "Import codes from {appName}",
"importSelectJsonFile": "Select JSON file",
"importRaivoGuide": "Use the \"Export OTPs to Zip archive\" option in Raivo's Settings.\n\nExtract the zip file and import the JSON file.",
"exportCodes": "Export codes",
"importInstruction": "Please select a file that contains a list of your codes in the following format",
"importCodeDelimiterInfo": "The codes can be separated by a comma or a new line",

View file

@ -0,0 +1,66 @@
/*
Version: 1.0
KDF Algo: ARGON2ID
Decrypted Data Format: It contains code.rawData [1] separated by new line.
[1] otpauth://totp/provider.com:you@email.com?secret=YOUR_SECRET
*/
class EnteAuthExport {
final int version;
final KDFParams kdfParams;
final String encryptedData;
final String encryptionNonce;
// Named constructor which can be used to specify each field individually
EnteAuthExport({
required this.version,
required this.kdfParams,
required this.encryptedData,
required this.encryptionNonce,
});
// Convert EnteExport object to JSON
Map<String, dynamic> toJson() => {
'version': version,
'kdfParams': kdfParams.toJson(),
'encryptedData': encryptedData,
'encryptionNonce': encryptionNonce,
};
// Convert JSON to EnteExport object
static EnteAuthExport fromJson(Map<String, dynamic> json) => EnteAuthExport(
version: json['version'],
kdfParams: KDFParams.fromJson(json['kdfParams']),
encryptedData: json['encryptedData'],
encryptionNonce: json['encryptionNonce'],
);
}
// KDFParams is a class that holds the parameters for the KDF function.
// It is used to derive a key from a password.
class KDFParams {
final int memLimit;
final int opsLimit;
final String salt;
// Named constructor which can be used to specify each field individually
KDFParams({
required this.memLimit,
required this.opsLimit,
required this.salt,
});
// Convert KDFParams object to JSON
Map<String, dynamic> toJson() => {
'memLimit': memLimit,
'opsLimit': opsLimit,
'salt': salt,
};
// Convert JSON to KDFParams object
static KDFParams fromJson(Map<String, dynamic> json) => KDFParams(
memLimit: json['memLimit'],
opsLimit: json['opsLimit'],
salt: json['salt'],
);
}

View file

@ -73,19 +73,22 @@ class DialogWidget extends StatelessWidget {
boxShadow: shadowFloatLight,
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ContentContainer(
title: title,
body: body,
icon: icon,
),
const SizedBox(height: 36),
Actions(buttons),
],
child: Material(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ContentContainer(
title: title,
body: body,
icon: icon,
),
const SizedBox(height: 36),
Actions(buttons),
],
),
),
),
);

View file

@ -0,0 +1,64 @@
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
import 'package:ente_auth/ui/components/menu_item_widget.dart';
import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:ente_auth/ui/settings/data/export_widget.dart';
import 'package:ente_auth/ui/settings/data/import_page.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
class DataSectionWidget extends StatelessWidget {
final _logger = Logger("AccountSectionWidget");
DataSectionWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return ExpandableMenuItemWidget(
title: l10n.data,
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Icons.key_outlined,
);
}
Column _getSectionOptions(BuildContext context) {
final l10n = context.l10n;
List<Widget> children = [];
children.addAll([
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: l10n.importCodes,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
routeToPage(context, ImportCodePage());
// _showImportInstructionDialog(context);
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: l10n.exportCodes,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
handleExportClick(context);
},
),
sectionOptionSpacing,
]);
return Column(
children: children,
);
}
}

View file

@ -0,0 +1,146 @@
import 'dart:convert';
import 'dart:io';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/export/ente.dart';
import 'package:ente_auth/services/local_authentication_service.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/dialog_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/utils/crypto_util.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
import 'package:share_plus/share_plus.dart';
Future<void> handleExportClick(BuildContext context) async {
final result = await showDialogWidget(
context: context,
title: "Select export format",
body: "Encrypted exports will be protected by a password of your choice.",
buttons: [
const ButtonWidget(
buttonType: ButtonType.primary,
labelText: "Encrypted",
isInAlert: true,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.first,
),
const ButtonWidget(
buttonType: ButtonType.secondary,
labelText: "Plain text",
buttonSize: ButtonSize.large,
isInAlert: true,
buttonAction: ButtonAction.second,
),
],
);
if (result?.action != null && result!.action != ButtonAction.cancel) {
if (result.action == ButtonAction.first) {
await _requestForEncryptionPassword(context);
} else {
await _showExportWarningDialog(context);
}
}
}
Future<void> _requestForEncryptionPassword(BuildContext context,
{String? password,}) async {
await showTextInputDialog(
context,
title: "Password to encrypt export",
submitButtonLabel: "Export",
hintText: "Enter password",
isPasswordInput: true,
alwaysShowSuccessState: false,
onSubmit: (String password) async {
if (password.isEmpty || password.length < 4) {
showToast(context, "Password must be at least 4 characters long.");
Future.delayed(const Duration(seconds: 0), () {
_requestForEncryptionPassword(context, password: password);
});
return;
}
if (password.isNotEmpty) {
try {
final kekSalt = CryptoUtil.getSaltToDeriveKey();
final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
utf8.encode(password) as Uint8List,
kekSalt,
);
String exportPlainText = await _getAuthDataForExport();
// Encrypt the key with this derived key
final encResult = await CryptoUtil.encryptChaCha(
utf8.encode(exportPlainText) as Uint8List, derivedKeyResult.key,);
final encContent = Sodium.bin2base64(encResult.encryptedData!);
final encNonce = Sodium.bin2base64(encResult.header!);
final EnteAuthExport data = EnteAuthExport(
version: 1,
encryptedData: encContent,
encryptionNonce: encNonce,
kdfParams: KDFParams(
memLimit: derivedKeyResult.memLimit,
opsLimit: derivedKeyResult.opsLimit,
salt: Sodium.bin2base64(kekSalt),),
);
// get json value of data
_exportCodes(context, jsonEncode(data.toJson()));
} catch(e,s) {
Logger("ExportWidget").severe(e, s);
showToast(context, "Error while exporting codes.");
}
}
},
);
}
Future<void> _showExportWarningDialog(BuildContext context) async {
await showChoiceActionSheet(
context,
title: context.l10n.warning,
body: context.l10n.exportWarningDesc,
isCritical: true,
firstButtonOnTap: () async {
final data = await _getAuthDataForExport();
await _exportCodes(context, data);
},
secondButtonLabel: context.l10n.cancel,
firstButtonLabel: context.l10n.iUnderStand,
);
}
Future<void> _exportCodes(BuildContext context, String fileContent) async {
final _codeFile = File(
Configuration.instance.getTempDirectory() + "ente-authenticator-codes.txt",
);
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthentication(context, context.l10n.authToExportCodes);
if (!hasAuthenticated) {
return;
}
if (_codeFile.existsSync()) {
await _codeFile.delete();
}
_codeFile.writeAsStringSync(fileContent);
await Share.shareFiles([_codeFile.path]);
Future.delayed(const Duration(seconds: 15), () async {
if (_codeFile.existsSync()) {
_codeFile.deleteSync();
}
});
}
Future<String> _getAuthDataForExport() async {
final codes = await CodeStore.instance.getAllCodes();
String data = "";
for (final code in codes) {
data += code.rawData + "\n";
}
return data;
}

View file

@ -0,0 +1,23 @@
import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart';
import 'package:ente_auth/ui/settings/data/import/ravio_plain_text_import.dart';
import 'package:ente_auth/ui/settings/data/import_page.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/cupertino.dart';
class ImportService {
static final ImportService _instance = ImportService._internal();
factory ImportService() => _instance;
ImportService._internal();
Future<void> initiateImport(BuildContext context,ImportType type) async {
if(type == ImportType.plainText) {
showImportInstructionDialog(context);
} else if(type == ImportType.ravio) {
showRaivoImportInstruction(context);
} else {
showToast(context, 'Coming soon!');
}
}
}

View file

@ -0,0 +1,148 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/services/authenticator_service.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/ui/components/dialog_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
class PlainTextImport extends StatelessWidget {
const PlainTextImport({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Column(
children: [
Text(
l10n.importInstruction,
),
const SizedBox(
height: 20,
),
Container(
color: Theme.of(context).colorScheme.gNavBackgroundColor,
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
"otpauth://totp/provider.com:you@email.com?secret=YOUR_SECRET",
style: TextStyle(
fontFeatures: const [FontFeature.tabularFigures()],
fontFamily: Platform.isIOS ? "Courier" : "monospace",
fontSize: 13,
),
),
),
),
const SizedBox(
height: 20,
),
Text(l10n.importCodeDelimiterInfo),
],
);
}
}
Future<void> showImportInstructionDialog(BuildContext context) async {
final l10n = context.l10n;
final AlertDialog alert = AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
title: Text(
l10n.importCodes,
style: Theme.of(context).textTheme.headline6,
),
content: const SingleChildScrollView(
child: PlainTextImport(),
),
actions: [
TextButton(
child: Text(
l10n.cancel,
style: const TextStyle(
color: Colors.red,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
),
TextButton(
child: Text(l10n.selectFile),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
_pickImportFile(context);
},
),
],
);
return showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
barrierColor: Colors.black12,
);
}
Future<void> _pickImportFile(BuildContext context) async {
final l10n = context.l10n;
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result == null) {
return;
}
final progressDialog = createProgressDialog(context, l10n.pleaseWait);
await progressDialog.show();
try {
File file = File(result.files.single.path!);
final codes = await file.readAsString();
List<String> splitCodes = codes.split(",");
if (splitCodes.length == 1) {
splitCodes = codes.split("\n");
}
final parsedCodes = [];
for (final code in splitCodes) {
try {
parsedCodes.add(Code.fromRawData(code));
} catch (e) {
Logger('PlainText').severe("Could not parse code", e);
}
}
for (final code in parsedCodes) {
await CodeStore.instance.addCode(code, shouldSync: false);
}
unawaited(AuthenticatorService.instance.sync());
await progressDialog.hide();
final DialogWidget dialog = choiceDialog(
title: context.l10n.importSuccessTitle,
body: context.l10n.importSuccessDesc(parsedCodes.length),
firstButtonLabel: l10n.ok,
firstButtonType: ButtonType.primary,
);
await showConfettiDialog(
context: context,
dialogBuilder: (BuildContext context) {
return dialog;
},
);
} catch (e) {
await progressDialog.hide();
await showErrorDialog(
context,
context.l10n.sorry,
context.l10n.importFailureDesc,
);
}
}

View file

@ -0,0 +1,110 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/services/authenticator_service.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/dialog_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
Future<void> showRaivoImportInstruction(BuildContext context) async {
final l10n = context.l10n;
final result = await showDialogWidget(
context: context,
title: l10n.importFromApp("Raivo OTP"),
body: l10n.importRaivoGuide,
buttons: [
ButtonWidget(
buttonType: ButtonType.primary,
labelText: l10n.importSelectJsonFile,
isInAlert: true,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.first,
),
ButtonWidget(
buttonType: ButtonType.secondary,
labelText: context.l10n.cancel,
buttonSize: ButtonSize.large,
isInAlert: true,
buttonAction: ButtonAction.second,
),
],
);
if (result?.action != null && result!.action != ButtonAction.cancel) {
if (result.action == ButtonAction.first) {
await _pickRaivoJsonFile(context);
} else {}
}
}
Future<void> _pickRaivoJsonFile(BuildContext context) async {
final l10n = context.l10n;
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result == null) {
return;
}
final progressDialog = createProgressDialog(context, l10n.pleaseWait);
await progressDialog.show();
try {
File file = File(result.files.single.path!);
final jsonString = await file.readAsString();
List<dynamic> jsonArray = jsonDecode(jsonString);
final parsedCodes = [];
for (var item in jsonArray) {
var kind = item['kind'];
var algorithm = item['algorithm'];
var timer = item['timer'];
var digits = item['digits'];
var issuer = item['issuer'];
var secret = item['secret'];
var account = item['account'];
var counter = item['counter'];
// Build the OTP URL
String otpUrl;
if (kind.toLowerCase() == 'totp') {
otpUrl =
'otpauth://$kind/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer';
} else if (kind.toLowerCase() == 'hotp') {
otpUrl =
'otpauth://$kind/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&counter=$counter';
} else {
throw Exception('Invalid OTP type');
}
parsedCodes.add(Code.fromRawData(otpUrl));
}
for (final code in parsedCodes) {
await CodeStore.instance.addCode(code, shouldSync: false);
}
unawaited(AuthenticatorService.instance.sync());
await progressDialog.hide();
final DialogWidget dialog = choiceDialog(
title: context.l10n.importSuccessTitle,
body: context.l10n.importSuccessDesc(parsedCodes.length),
firstButtonLabel: l10n.ok,
firstButtonType: ButtonType.primary,
);
await showConfettiDialog(
context: context,
dialogBuilder: (BuildContext context) {
return dialog;
},
);
} catch (e) {
await progressDialog.hide();
await showErrorDialog(
context,
context.l10n.sorry,
context.l10n.importFailureDesc,
);
}
}

View file

@ -0,0 +1,106 @@
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/components/buttons/icon_button_widget.dart';
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
import 'package:ente_auth/ui/components/divider_widget.dart';
import 'package:ente_auth/ui/components/menu_item_widget.dart';
import 'package:ente_auth/ui/components/title_bar_title_widget.dart';
import 'package:ente_auth/ui/components/title_bar_widget.dart';
import 'package:ente_auth/ui/settings/data/import/import_service.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
enum ImportType {
plainText,
encrypted,
ravio,
}
class ImportCodePage extends StatelessWidget {
late List<ImportType> importOptions = [
ImportType.plainText,
ImportType.encrypted,
ImportType.ravio,
];
ImportCodePage({super.key});
String getTitle(BuildContext context, ImportType type) {
switch (type) {
case ImportType.plainText:
return context.l10n.importTypePlainText;
case ImportType.encrypted:
return context.l10n.importTypeEnteEncrypted;
case ImportType.ravio:
return 'Ravio OTP';
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
TitleBarWidget(
flexibleSpaceTitle: TitleBarTitleWidget(
title: context.l10n.importCodes,
),
flexibleSpaceCaption: "Import source",
actionIcons: [
IconButtonWidget(
icon: Icons.close_outlined,
iconButtonType: IconButtonType.secondary,
onTap: () {
Navigator.pop(context);
Navigator.pop(context);
},
),
],
),
SliverList(
delegate: SliverChildBuilderDelegate(
(delegateBuildContext, index) {
final type = importOptions[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
if (index == 0)
const SizedBox(
height: 24,
),
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: getTitle(context, type),
),
alignCaptionedTextToLeft: true,
menuItemColor: getEnteColorScheme(context).fillFaint,
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
isBottomBorderRadiusRemoved:
index != importOptions.length - 1,
isTopBorderRadiusRemoved: index != 0,
onTap: () async {
ImportService().initiateImport(context, type);
// routeToPage(context, ImportCodePage());
// _showImportInstructionDialog(context);
},
),
if (index != importOptions.length - 1)
DividerWidget(
dividerType: DividerType.menu,
bgColor: getEnteColorScheme(context).fillFaint,
),
],
),
);
},
childCount: importOptions.length,
),
),
],
),
);
}
}

View file

@ -1,267 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/services/authenticator_service.dart';
import 'package:ente_auth/services/local_authentication_service.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
import 'package:ente_auth/ui/components/dialog_widget.dart';
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
import 'package:ente_auth/ui/components/menu_item_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:share_plus/share_plus.dart';
class DataSectionWidget extends StatelessWidget {
final _logger = Logger("AccountSectionWidget");
final _codeFile = File(
Configuration.instance.getTempDirectory() + "ente-authenticator-codes.txt",
);
DataSectionWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return ExpandableMenuItemWidget(
title: l10n.data,
selectionOptionsWidget: _getSectionOptions(context),
leadingIcon: Icons.key_outlined,
);
}
Column _getSectionOptions(BuildContext context) {
final l10n = context.l10n;
List<Widget> children = [];
children.addAll([
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: l10n.importCodes,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
_showImportInstructionDialog(context);
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: l10n.exportCodes,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
_showExportWarningDialog(context);
},
),
sectionOptionSpacing,
]);
return Column(
children: children,
);
}
Future<void> _showImportInstructionDialog(BuildContext context) async {
final l10n = context.l10n;
final AlertDialog alert = AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
title: Text(
l10n.importCodes,
style: Theme.of(context).textTheme.headline6,
),
content: SingleChildScrollView(
child: Column(
children: [
Text(
l10n.importInstruction,
),
const SizedBox(
height: 20,
),
Container(
color: Theme.of(context).colorScheme.gNavBackgroundColor,
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
"otpauth://totp/provider.com:you@email.com?secret=YOUR_SECRET",
style: TextStyle(
fontFeatures: const [FontFeature.tabularFigures()],
fontFamily: Platform.isIOS ? "Courier" : "monospace",
fontSize: 13,
),
),
),
),
const SizedBox(
height: 20,
),
Text(l10n.importCodeDelimiterInfo),
],
),
),
actions: [
TextButton(
child: Text(
l10n.cancel,
style: const TextStyle(
color: Colors.red,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
),
TextButton(
child: Text(l10n.selectFile),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
_pickImportFile(context);
},
),
],
);
return showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
barrierColor: Colors.black12,
);
}
Future<void> _showExportWarningDialog(BuildContext context) async {
final AlertDialog alert = AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
title: Text(
context.l10n.warning,
style: Theme.of(context).textTheme.headline6,
),
content: Text(
context.l10n.exportWarningDesc,
),
actions: [
TextButton(
child: Text(
context.l10n.iUnderStand,
style: const TextStyle(
color: Colors.red,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
_exportCodes(context);
},
),
TextButton(
child: Text(
context.l10n.cancel,
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
),
],
);
return showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
barrierColor: Colors.black12,
);
}
Future<void> _exportCodes(BuildContext context) async {
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthentication(context, context.l10n.authToExportCodes);
if (!hasAuthenticated) {
return;
}
if (_codeFile.existsSync()) {
await _codeFile.delete();
}
final codes = await CodeStore.instance.getAllCodes();
String data = "";
for (final code in codes) {
data += code.rawData + "\n";
}
_codeFile.writeAsStringSync(data);
await Share.shareFiles([_codeFile.path]);
Future.delayed(const Duration(seconds: 15), () async {
if (_codeFile.existsSync()) {
_codeFile.deleteSync();
}
});
}
Future<void> _pickImportFile(BuildContext context) async {
final l10n = context.l10n;
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result == null) {
return;
}
final dialog = createProgressDialog(context, l10n.pleaseWait);
await dialog.show();
try {
File file = File(result.files.single.path!);
final codes = await file.readAsString();
List<String> splitCodes = codes.split(",");
if (splitCodes.length == 1) {
splitCodes = codes.split("\n");
}
final parsedCodes = [];
for (final code in splitCodes) {
try {
parsedCodes.add(Code.fromRawData(code));
} catch (e) {
_logger.severe("Could not parse code", e);
}
}
for (final code in parsedCodes) {
await CodeStore.instance.addCode(code, shouldSync: false);
}
unawaited(AuthenticatorService.instance.sync());
final DialogWidget dialog = choiceDialog(
title: context.l10n.importSuccessTitle,
body: context.l10n.importSuccessDesc(parsedCodes.length),
// body: "You have imported " + parsedCodes.length.toString() + " codes!",
firstButtonLabel: l10n.ok,
firstButtonOnTap: () async {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
firstButtonType: ButtonType.primary,
);
await showConfettiDialog(
context: context,
dialogBuilder: (BuildContext context) {
return dialog;
},
);
} catch (e) {
await dialog.hide();
await showErrorDialog(
context,
context.l10n.sorry,
context.l10n.importFailureDesc,
);
}
}
}

View file

@ -6,7 +6,7 @@ import 'package:ente_auth/ui/settings/about_section_widget.dart';
import 'package:ente_auth/ui/settings/account_section_widget.dart';
import 'package:ente_auth/ui/settings/app_version_widget.dart';
import 'package:ente_auth/ui/settings/danger_section_widget.dart';
import 'package:ente_auth/ui/settings/data_section_widget.dart';
import 'package:ente_auth/ui/settings/data/data_section_widget.dart';
import 'package:ente_auth/ui/settings/security_section_widget.dart';
import 'package:ente_auth/ui/settings/social_section_widget.dart';
import 'package:ente_auth/ui/settings/support_dev_widget.dart';