Skip to content

Commit

Permalink
Feature: Keyfile sign in 2.0.0 (#10)
Browse files Browse the repository at this point in the history
Previously, while signing in to an application using a keyfile, the console threw InvalidPasswordException every time the user entered a password. While fixing the bug, it was noticed that all control of the view is done directly in the widget. For this reason, the entire keyfile signing functionality was rebuilt, separating view management into cubit.

List of changes:
- created keyfile_dropzone_cubit.dart to manage parsing of uploaded file to Keyfile after drag & drop
- created sign_in_keyfile_drawer_page_cubit.dart to combine and manage the functionality of uploading a keyfile, entering a password and signing in to the application
- created new version of keyfile [2.0.0], which contains secret_data, public_key and version. In comparison with previous versions, it replaced "address" with "public_key", removed "public_address" and replaced camelCase names with snake_case.
- created keyfile_entity.dart representing single keyfile structure and keyfile_secret_data_entity.dart representing its private key
- replaced keyfile.dart with decrypted_keyfile_model.dart and encrypted_keyfile_model.dart to allow recognizing keyfile content before and after its decrypting
  • Loading branch information
nemoforte authored Mar 27, 2024
1 parent 797cfb9 commit 71b9cf3
Show file tree
Hide file tree
Showing 38 changed files with 1,332 additions and 690 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:miro/blocs/pages/drawer/sign_in_keyfile_drawer_page/sign_in_keyfile_drawer_page_state.dart';
import 'package:miro/blocs/widgets/keyfile_dropzone/keyfile_dropzone_cubit.dart';
import 'package:miro/blocs/widgets/keyfile_dropzone/keyfile_dropzone_state.dart';
import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception.dart';
import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart';
import 'package:miro/shared/models/keyfile/decrypted_keyfile_model.dart';
import 'package:miro/shared/models/keyfile/encrypted_keyfile_model.dart';

class SignInKeyfileDrawerPageCubit extends Cubit<SignInKeyfileDrawerPageState> {
final KeyfileDropzoneCubit keyfileDropzoneCubit;
final TextEditingController passwordTextEditingController;
late final StreamSubscription<KeyfileDropzoneState> _keyfileDropzoneStateSubscription;

SignInKeyfileDrawerPageCubit({
required this.keyfileDropzoneCubit,
required this.passwordTextEditingController,
}) : super(const SignInKeyfileDrawerPageState()) {
_keyfileDropzoneStateSubscription = keyfileDropzoneCubit.stream.listen(_listenKeyfileChange);
}

@override
Future<void> close() {
keyfileDropzoneCubit.close();
_keyfileDropzoneStateSubscription.cancel();
return super.close();
}

void notifyPasswordChanged() {
bool keyfileUploadedBool = keyfileDropzoneCubit.state.hasKeyfile;
if (keyfileUploadedBool) {
_decryptKeyfile();
}
}

void _listenKeyfileChange(KeyfileDropzoneState keyfileDropzoneState) {
bool keyfileValidBool = keyfileDropzoneState.keyfileExceptionType == null;
if (keyfileValidBool) {
_decryptKeyfile();
} else {
emit(SignInKeyfileDrawerPageState(keyfileExceptionType: keyfileDropzoneState.keyfileExceptionType));
}
}

void _decryptKeyfile() {
try {
String password = passwordTextEditingController.text;
EncryptedKeyfileModel encryptedKeyfileModel = keyfileDropzoneCubit.state.encryptedKeyfileModel!;
DecryptedKeyfileModel decryptedKeyfileModel = encryptedKeyfileModel.decrypt(password);

emit(SignInKeyfileDrawerPageState(decryptedKeyfileModel: decryptedKeyfileModel));
} on KeyfileException catch (e) {
emit(SignInKeyfileDrawerPageState(keyfileExceptionType: e.keyfileExceptionType));
} catch (e) {
emit(const SignInKeyfileDrawerPageState(keyfileExceptionType: KeyfileExceptionType.invalidKeyfile));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:equatable/equatable.dart';
import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart';
import 'package:miro/shared/models/keyfile/decrypted_keyfile_model.dart';

class SignInKeyfileDrawerPageState extends Equatable {
final KeyfileExceptionType? keyfileExceptionType;
final DecryptedKeyfileModel? decryptedKeyfileModel;

const SignInKeyfileDrawerPageState({
this.keyfileExceptionType,
this.decryptedKeyfileModel,
});

@override
List<Object?> get props => <Object?>[keyfileExceptionType, decryptedKeyfileModel];
}
52 changes: 52 additions & 0 deletions lib/blocs/widgets/keyfile_dropzone/keyfile_dropzone_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'dart:convert';
import 'dart:html' as html;

import 'package:file_picker/file_picker.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:miro/blocs/widgets/keyfile_dropzone/keyfile_dropzone_state.dart';
import 'package:miro/shared/entity/keyfile/keyfile_entity.dart';
import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception.dart';
import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart';
import 'package:miro/shared/models/generic/file_model.dart';
import 'package:miro/shared/models/keyfile/encrypted_keyfile_model.dart';
import 'package:miro/shared/utils/logger/app_logger.dart';

class KeyfileDropzoneCubit extends Cubit<KeyfileDropzoneState> {
KeyfileDropzoneCubit() : super(KeyfileDropzoneState.empty());

Future<void> uploadFileViaHtml(dynamic htmlFile) async {
if (htmlFile is html.File) {
updateSelectedFile(await FileModel.fromHtmlFile(htmlFile));
} else {
AppLogger().log(message: 'Unsupported file type ${htmlFile.runtimeType}');
}
}

Future<void> uploadFileManually() async {
FilePickerResult? filePickerResult = await FilePicker.platform.pickFiles(allowMultiple: false);
if (filePickerResult == null) {
return;
}
PlatformFile platformFile = filePickerResult.files.single;
if (platformFile.bytes == null) {
return;
}

updateSelectedFile(FileModel.fromPlatformFile(platformFile));
}

void updateSelectedFile(FileModel fileModel) {
try {
Map<String, dynamic> keyfileJson = jsonDecode(fileModel.content) as Map<String, dynamic>;
KeyfileEntity keyfileEntity = KeyfileEntity.fromJson(keyfileJson);
EncryptedKeyfileModel encryptedKeyfileModel = EncryptedKeyfileModel.fromEntity(keyfileEntity);
emit(KeyfileDropzoneState(encryptedKeyfileModel: encryptedKeyfileModel, fileModel: fileModel));
} on KeyfileException catch (keyfileException) {
AppLogger().log(message: keyfileException.keyfileExceptionType.toString());
emit(KeyfileDropzoneState(keyfileExceptionType: keyfileException.keyfileExceptionType, fileModel: fileModel));
} catch (e) {
AppLogger().log(message: 'Invalid keyfile: $e');
emit(KeyfileDropzoneState(keyfileExceptionType: KeyfileExceptionType.invalidKeyfile, fileModel: fileModel));
}
}
}
27 changes: 27 additions & 0 deletions lib/blocs/widgets/keyfile_dropzone/keyfile_dropzone_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:equatable/equatable.dart';
import 'package:miro/shared/exceptions/keyfile_exception/keyfile_exception_type.dart';
import 'package:miro/shared/models/generic/file_model.dart';
import 'package:miro/shared/models/keyfile/encrypted_keyfile_model.dart';

class KeyfileDropzoneState extends Equatable {
final EncryptedKeyfileModel? encryptedKeyfileModel;
final FileModel? fileModel;
final KeyfileExceptionType? keyfileExceptionType;

const KeyfileDropzoneState({
this.encryptedKeyfileModel,
this.fileModel,
this.keyfileExceptionType,
});

factory KeyfileDropzoneState.empty() {
return const KeyfileDropzoneState();
}

bool get hasFile => fileModel != null;

bool get hasKeyfile => encryptedKeyfileModel != null;

@override
List<Object?> get props => <Object?>[encryptedKeyfileModel, fileModel, keyfileExceptionType];
}
53 changes: 29 additions & 24 deletions lib/generated/intl/messages_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,31 @@ class MessageLookup extends MessageLookupByLibrary {

static String m3(amount) => "Tip must be greater or equal ${amount}";

static String m4(separator, networkName, parsedRemainingTime) =>
static String m4(version) => "Keyfile version ${version}";

static String m5(separator, networkName, parsedRemainingTime) =>
"Connecting to <${networkName}>${separator} Please wait... ${parsedRemainingTime}";

static String m5(errorsCount) => "Found ${errorsCount} problems with server";
static String m6(errorsCount) => "Found ${errorsCount} problems with server";

static String m6(latestBlockTime) =>
static String m7(latestBlockTime) =>
"The last available block on this interx was created long time ago ${latestBlockTime}. The displayed contents may be out of date.";

static String m7(seconds) => "Refresh in ${seconds} sec.";
static String m8(seconds) => "Refresh in ${seconds} sec.";

static String m8(availableAmountText, tokenDenominationModelName) =>
static String m9(availableAmountText, tokenDenominationModelName) =>
"Available: ${availableAmountText} ${tokenDenominationModelName}";

static String m9(hash) => "Transaction hash: 0x${hash}";
static String m10(hash) => "Transaction hash: 0x${hash}";

static String m10(amount) => "+ ${amount} more";
static String m11(amount) => "+ ${amount} more";

static String m11(widgetFeeTokenAmountModel) =>
static String m12(widgetFeeTokenAmountModel) =>
"Transaction fee ${widgetFeeTokenAmountModel}";

static String m12(txMsgType) => "Preview for ${txMsgType} unavailable";
static String m13(txMsgType) => "Preview for ${txMsgType} unavailable";

static String m13(selected) => "${selected} selected";
static String m14(selected) => "${selected} selected";

final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
Expand Down Expand Up @@ -233,13 +235,17 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Enter password"),
"keyfileErrorCannotBeEmpty":
MessageLookupByLibrary.simpleMessage("Keyfile cannot be empty"),
"keyfileErrorInvalid":
MessageLookupByLibrary.simpleMessage("Invalid Keyfile"),
"keyfileErrorPasswordsMatch":
MessageLookupByLibrary.simpleMessage("Passwords don\'t match"),
"keyfileErrorUnsupportedVersion":
MessageLookupByLibrary.simpleMessage("Unsupported version"),
"keyfileErrorWrongPassword":
MessageLookupByLibrary.simpleMessage("Wrong password"),
"keyfileHintPassword": MessageLookupByLibrary.simpleMessage("Password"),
"keyfileHintRepeatPassword":
MessageLookupByLibrary.simpleMessage("Repeat password"),
"keyfileInvalid":
MessageLookupByLibrary.simpleMessage("Invalid Keyfile"),
"keyfileSignIn":
MessageLookupByLibrary.simpleMessage("Sign in with Keyfile"),
"keyfileTip": MessageLookupByLibrary.simpleMessage(
Expand All @@ -252,10 +258,9 @@ class MessageLookup extends MessageLookupByLibrary {
"Drop Keyfile to the dropzone"),
"keyfileToastDownloaded":
MessageLookupByLibrary.simpleMessage("Keyfile downloaded"),
"keyfileVersion": m4,
"keyfileWarning": MessageLookupByLibrary.simpleMessage(
"You won’t be able to download it again"),
"keyfileWrongPassword":
MessageLookupByLibrary.simpleMessage("Wrong password"),
"kiraNetwork": MessageLookupByLibrary.simpleMessage("Kira Network"),
"mnemonic": MessageLookupByLibrary.simpleMessage("Mnemonic"),
"mnemonicEnter":
Expand Down Expand Up @@ -312,7 +317,7 @@ class MessageLookup extends MessageLookupByLibrary {
"networkCheckedConnection":
MessageLookupByLibrary.simpleMessage("Checked connection"),
"networkChoose": MessageLookupByLibrary.simpleMessage("Choose network"),
"networkConnectingTo": m4,
"networkConnectingTo": m5,
"networkConnectionCancelled":
MessageLookupByLibrary.simpleMessage("Connection cancelled"),
"networkConnectionEstablished":
Expand All @@ -327,7 +332,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("undefined"),
"networkHintCustomAddress":
MessageLookupByLibrary.simpleMessage("Custom address"),
"networkHowManyProblems": m5,
"networkHowManyProblems": m6,
"networkList": MessageLookupByLibrary.simpleMessage("List of networks"),
"networkNoAvailable":
MessageLookupByLibrary.simpleMessage("No available networks"),
Expand All @@ -349,7 +354,7 @@ class MessageLookup extends MessageLookupByLibrary {
"The application is incompatible with this server. Some views may not work correctly."),
"networkWarningMissingInfo": MessageLookupByLibrary.simpleMessage(
"Connecting a wallet unavailable due to missing essential data from network."),
"networkWarningWhenLastBlock": m6,
"networkWarningWhenLastBlock": m7,
"or": MessageLookupByLibrary.simpleMessage("or "),
"paginatedListPageSize":
MessageLookupByLibrary.simpleMessage("Page size"),
Expand All @@ -363,7 +368,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Successful"),
"proposalsVoters": MessageLookupByLibrary.simpleMessage("Voters"),
"refresh": MessageLookupByLibrary.simpleMessage("Refresh"),
"refreshInSeconds": m7,
"refreshInSeconds": m8,
"sec": MessageLookupByLibrary.simpleMessage("sec."),
"seeAll": MessageLookupByLibrary.simpleMessage("See all"),
"seeMore": MessageLookupByLibrary.simpleMessage("See more"),
Expand Down Expand Up @@ -424,7 +429,7 @@ class MessageLookup extends MessageLookupByLibrary {
"toastSuccessfullyCopied":
MessageLookupByLibrary.simpleMessage("Successfully copied"),
"tx": MessageLookupByLibrary.simpleMessage("Transactions"),
"txAvailableBalances": m8,
"txAvailableBalances": m9,
"txButtonBackToAccount":
MessageLookupByLibrary.simpleMessage("Back to account"),
"txButtonClaimAllRewards":
Expand Down Expand Up @@ -477,7 +482,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("See more on Explorer"),
"txFetchingRemoteData": MessageLookupByLibrary.simpleMessage(
"Fetching remote data. Please wait..."),
"txHash": m9,
"txHash": m10,
"txHintAmountToClaim":
MessageLookupByLibrary.simpleMessage("Amount to claim"),
"txHintClaim": MessageLookupByLibrary.simpleMessage("Claim"),
Expand All @@ -498,7 +503,7 @@ class MessageLookup extends MessageLookupByLibrary {
"txListAmountFeesOnly":
MessageLookupByLibrary.simpleMessage("Fees only"),
"txListAmountPlusFees": MessageLookupByLibrary.simpleMessage("+ fees"),
"txListAmountPlusMore": m10,
"txListAmountPlusMore": m11,
"txListDate": MessageLookupByLibrary.simpleMessage("Date"),
"txListDetails": MessageLookupByLibrary.simpleMessage("Details"),
"txListDirection": MessageLookupByLibrary.simpleMessage("Direction"),
Expand Down Expand Up @@ -536,10 +541,10 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Unknown transaction type"),
"txMsgUndelegate":
MessageLookupByLibrary.simpleMessage("Unstake Tokens"),
"txNoticeFee": m11,
"txNoticeFee": m12,
"txPleaseSelectToken":
MessageLookupByLibrary.simpleMessage("Please select a token"),
"txPreviewUnavailable": m12,
"txPreviewUnavailable": m13,
"txRecipientWillGet":
MessageLookupByLibrary.simpleMessage("Recipient will get"),
"txSearchTokens": MessageLookupByLibrary.simpleMessage("Search tokens"),
Expand Down Expand Up @@ -569,7 +574,7 @@ class MessageLookup extends MessageLookupByLibrary {
"validatorsAbout":
MessageLookupByLibrary.simpleMessage("About Validator"),
"validatorsActive": MessageLookupByLibrary.simpleMessage("Active"),
"validatorsButtonFilter": m13,
"validatorsButtonFilter": m14,
"validatorsDropdownAll": MessageLookupByLibrary.simpleMessage("All"),
"validatorsHintSearch":
MessageLookupByLibrary.simpleMessage("Search validators"),
Expand Down
Loading

0 comments on commit 71b9cf3

Please sign in to comment.