Support for importing ente encrypted export (#171)

This commit is contained in:
Neeraj Gupta 2023-07-31 17:29:38 +05:30 committed by GitHub
commit 8016c11a2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 260 additions and 84 deletions

View file

@ -75,10 +75,14 @@
"importCodes": "Import codes", "importCodes": "Import codes",
"importTypePlainText": "Plain text", "importTypePlainText": "Plain text",
"importTypeEnteEncrypted": "ente Encrypted export", "importTypeEnteEncrypted": "ente Encrypted export",
"passwordForDecryptingExport" : "Password to decrypt export",
"passwordEmptyError": "Password can not be empty",
"importFromApp": "Import codes from {appName}", "importFromApp": "Import codes from {appName}",
"importSelectJsonFile": "Select JSON file", "importSelectJsonFile": "Select JSON file",
"importEnteEncGuide": "Select the encrypted JSON file exported from ente",
"importRaivoGuide": "Use the \"Export OTPs to Zip archive\" option in Raivo's Settings.\n\nExtract the zip file and import the 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", "exportCodes": "Export codes",
"importLabel": "Import",
"importInstruction": "Please select a file that contains a list of your codes in the following format", "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", "importCodeDelimiterInfo": "The codes can be separated by a comma or a new line",
"selectFile": "Select file", "selectFile": "Select file",

View file

@ -0,0 +1,166 @@
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/models/export/ente.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/crypto_util.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:file_picker/file_picker.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';
Future<void> showEncryptedImportInstruction(BuildContext context) async {
final l10n = context.l10n;
final result = await showDialogWidget(
context: context,
title: l10n.importFromApp("ente Auth"),
body: l10n.importEnteEncGuide,
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 _pickEnteJsonFile(context);
} else {}
}
}
Future<void> _decryptExportData(
BuildContext context,
EnteAuthExport enteAuthExport, {
String? password,
}) async {
final l10n = context.l10n;
bool isPasswordIncorrect = false;
int? importedCodeCount;
await showTextInputDialog(
context,
title: l10n.passwordForDecryptingExport,
submitButtonLabel: l10n.importLabel,
hintText: l10n.enterYourPasswordHint,
isPasswordInput: true,
alwaysShowSuccessState: false,
showOnlyLoadingState: true,
onSubmit: (String password) async {
if (password.isEmpty) {
showToast(context, l10n.passwordEmptyError);
Future.delayed(const Duration(seconds: 0), () {
_decryptExportData(context, enteAuthExport, password: password);
});
return;
}
if (password.isNotEmpty) {
final progressDialog = createProgressDialog(context, l10n.pleaseWait);
try {
await progressDialog.show();
final derivedKey = await CryptoUtil.deriveKey(
utf8.encode(password) as Uint8List,
Sodium.base642bin(enteAuthExport.kdfParams.salt),
enteAuthExport.kdfParams.memLimit,
enteAuthExport.kdfParams.opsLimit,
);
Uint8List? decryptedContent;
// Encrypt the key with this derived key
try {
decryptedContent = await CryptoUtil.decryptChaCha(
Sodium.base642bin(enteAuthExport.encryptedData),
derivedKey,
Sodium.base642bin(enteAuthExport.encryptionNonce),
);
} catch (e) {
showToast(context, l10n.incorrectPasswordTitle);
isPasswordIncorrect = true;
}
if (isPasswordIncorrect) {
await progressDialog.hide();
Future.delayed(const Duration(seconds: 0), () {
_decryptExportData(context, enteAuthExport, password: password);
});
return;
}
String content = utf8.decode(decryptedContent!);
List<String> splitCodes = content.split("\n");
final parsedCodes = [];
for (final code in splitCodes) {
try {
parsedCodes.add(Code.fromRawData(code));
} catch (e) {
Logger('EncryptedText').severe("Could not parse code", e);
}
}
for (final code in parsedCodes) {
await CodeStore.instance.addCode(code, shouldSync: false);
}
unawaited(AuthenticatorService.instance.sync());
importedCodeCount = parsedCodes.length;
await progressDialog.hide();
} catch (e, s) {
await progressDialog.hide();
Logger("ExportWidget").severe(e, s);
showToast(context, "Error while exporting codes.");
}
}
},
);
if (importedCodeCount != null) {
final DialogWidget dialog = choiceDialog(
title: context.l10n.importSuccessTitle,
body: context.l10n.importSuccessDesc(importedCodeCount!),
firstButtonLabel: l10n.ok,
firstButtonType: ButtonType.primary,
);
await showConfettiDialog(
context: context,
dialogBuilder: (BuildContext context) {
return dialog;
},
);
}
}
Future<void> _pickEnteJsonFile(BuildContext context) async {
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result == null) {
return;
}
try {
File file = File(result.files.single.path!);
final jsonString = await file.readAsString();
EnteAuthExport exportedData =
EnteAuthExport.fromJson(jsonDecode(jsonString));
await _decryptExportData(context, exportedData);
} catch (e) {
await showErrorDialog(
context,
context.l10n.sorry,
context.l10n.importFailureDesc,
);
}
}

View file

@ -1,8 +1,8 @@
import 'package:ente_auth/ui/settings/data/import/encrypted_ente_import.dart';
import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart'; 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/raivo_plain_text_import.dart';
import 'package:ente_auth/ui/settings/data/import_page.dart'; import 'package:ente_auth/ui/settings/data/import_page.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
class ImportService { class ImportService {
@ -17,7 +17,7 @@ class ImportService {
} else if(type == ImportType.ravio) { } else if(type == ImportType.ravio) {
showRaivoImportInstruction(context); showRaivoImportInstruction(context);
} else { } else {
showToast(context, 'Coming soon!'); showEncryptedImportInstruction(context);
} }
} }
} }

View file

@ -21,7 +21,7 @@ Future<void> showRaivoImportInstruction(BuildContext context) async {
title: l10n.importFromApp("Raivo OTP"), title: l10n.importFromApp("Raivo OTP"),
body: l10n.importRaivoGuide, body: l10n.importRaivoGuide,
buttons: [ buttons: [
ButtonWidget( ButtonWidget(
buttonType: ButtonType.primary, buttonType: ButtonType.primary,
labelText: l10n.importSelectJsonFile, labelText: l10n.importSelectJsonFile,
isInAlert: true, isInAlert: true,

View file

@ -38,68 +38,71 @@ class ImportCodePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Material(
body: CustomScrollView( color: Colors.transparent,
primary: false, child: Scaffold(
slivers: <Widget>[ body: CustomScrollView(
TitleBarWidget( primary: false,
flexibleSpaceTitle: TitleBarTitleWidget( slivers: <Widget>[
title: context.l10n.importCodes, TitleBarWidget(
), flexibleSpaceTitle: TitleBarTitleWidget(
flexibleSpaceCaption: "Import source", title: context.l10n.importCodes,
actionIcons: [
IconButtonWidget(
icon: Icons.close_outlined,
iconButtonType: IconButtonType.secondary,
onTap: () {
Navigator.pop(context);
Navigator.pop(context);
},
), ),
], flexibleSpaceCaption: "Import source",
), actionIcons: [
SliverList( IconButtonWidget(
delegate: SliverChildBuilderDelegate( icon: Icons.close_outlined,
(delegateBuildContext, index) { iconButtonType: IconButtonType.secondary,
final type = importOptions[index]; onTap: () {
return Padding( Navigator.pop(context);
padding: const EdgeInsets.symmetric(horizontal: 16.0), Navigator.pop(context);
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,
), ),
), 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

@ -310,26 +310,29 @@ Future<dynamic> showTextInputDialog(
builder: (context) { builder: (context) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom; final bottomInset = MediaQuery.of(context).viewInsets.bottom;
final isKeyboardUp = bottomInset > 100; final isKeyboardUp = bottomInset > 100;
return Center( return Material(
child: Padding( color: Colors.transparent,
padding: EdgeInsets.only(bottom: isKeyboardUp ? bottomInset : 0), child: Center(
child: TextInputDialog( child: Padding(
title: title, padding: EdgeInsets.only(bottom: isKeyboardUp ? bottomInset : 0),
message: message, child: TextInputDialog(
label: label, title: title,
body: body, message: message,
icon: icon, label: label,
submitButtonLabel: submitButtonLabel, body: body,
onSubmit: onSubmit, icon: icon,
hintText: hintText, submitButtonLabel: submitButtonLabel,
prefixIcon: prefixIcon, onSubmit: onSubmit,
initialValue: initialValue, hintText: hintText,
alignMessage: alignMessage, prefixIcon: prefixIcon,
maxLength: maxLength, initialValue: initialValue,
showOnlyLoadingState: showOnlyLoadingState, alignMessage: alignMessage,
textCapitalization: textCapitalization, maxLength: maxLength,
alwaysShowSuccessState: alwaysShowSuccessState, showOnlyLoadingState: showOnlyLoadingState,
isPasswordInput: isPasswordInput, textCapitalization: textCapitalization,
alwaysShowSuccessState: alwaysShowSuccessState,
isPasswordInput: isPasswordInput,
),
), ),
), ),
); );