cake_wallet/lib/view_model/set_up_2fa_viewmodel.dart
Adegoke David 4120394121
CW-266 verbose access controls for TOTP 2FA (#967)
* chore: Setup

* feat: Verbose controls for TOTP 2FA WIP [skip-ci]

* feat: Implement verbose controls for sends to contact, non contacts and internal wallets

* feat: Implement verbose 2FA control for exchanges to internal wallets [skip-ci]

* Implement verbose controls

* chore: PR cleanup

* fix: Implement fixes and recommendations on verbose controls

* feat: Localization for verbose controls settings

* fix: disable pin when 2fa is not activated

* fix: Naming error

* chore: Reformat code with linelength of 100

* fix: Wallet type page and type bug when creating wallet

* fix: add new values to be stored in local storage to both reload function and import/export functions in back_service.dart

* fix: White spaces with localization files

* fix: Switch observers in modify_2fa page to individual observer

* chore: Switch custom tab widget to reusable SettingsChoicesCell widget

* chore: Remove unneeded argument in create wallet entrypoint

* fix: Switch type for selectedCakePreference when importing preferences from backup file

* fix: Await all values being saved to local storage

---------

Co-authored-by: David Adegoke <blazebrain@Davids-MacBook-Pro.local>
2023-08-04 16:49:26 +03:00

421 lines
14 KiB
Dart

// ignore_for_file: prefer_final_fields
import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/totp_utils.dart' as Utils;
import 'package:cake_wallet/view_model/auth_state.dart';
import 'package:flutter/widgets.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../core/auth_service.dart';
import '../core/execution_state.dart';
import '../generated/i18n.dart';
part 'set_up_2fa_viewmodel.g.dart';
class Setup2FAViewModel = Setup2FAViewModelBase with _$Setup2FAViewModel;
abstract class Setup2FAViewModelBase with Store {
final SettingsStore _settingsStore;
final AuthService _authService;
final SharedPreferences _sharedPreferences;
Setup2FAViewModelBase(this._settingsStore, this._sharedPreferences, this._authService)
: _failureCounter = 0,
enteredOTPCode = '',
unhighlightTabs = false,
selected2FASettings = ObservableList<VerboseControlSettings>(),
state = InitialExecutionState() {
_getRandomBase32SecretKey();
selectCakePreset(selectedCake2FAPreset);
reaction((_) => state, _saveLastAuthTime);
}
static const maxFailedTrials = 3;
static const banTimeout = 180; // 3 minutes
final banTimeoutKey = S.current.auth_store_ban_timeout;
String get secretKey => _settingsStore.totpSecretKey;
String get deviceName => _settingsStore.deviceName;
String get totpVersionOneLink => _settingsStore.totpVersionOneLink;
@observable
ExecutionState state;
@observable
int _failureCounter;
@observable
String enteredOTPCode;
@computed
bool get useTOTP2FA => _settingsStore.useTOTP2FA;
@computed
bool get shouldRequireTOTP2FAForAccessingWallet =>
_settingsStore.shouldRequireTOTP2FAForAccessingWallet;
@computed
bool get shouldRequireTOTP2FAForSendsToContact =>
_settingsStore.shouldRequireTOTP2FAForSendsToContact;
@computed
bool get shouldRequireTOTP2FAForSendsToNonContact =>
_settingsStore.shouldRequireTOTP2FAForSendsToNonContact;
@computed
bool get shouldRequireTOTP2FAForSendsToInternalWallets =>
_settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;
@computed
bool get shouldRequireTOTP2FAForExchangesToInternalWallets =>
_settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets;
@computed
bool get shouldRequireTOTP2FAForAddingContacts =>
_settingsStore.shouldRequireTOTP2FAForAddingContacts;
@computed
bool get shouldRequireTOTP2FAForCreatingNewWallets =>
_settingsStore.shouldRequireTOTP2FAForCreatingNewWallets;
@computed
bool get shouldRequireTOTP2FAForAllSecurityAndBackupSettings =>
_settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings;
void _getRandomBase32SecretKey() {
final randomBase32Key = Utils.generateRandomBase32SecretKey(16);
_setBase32SecretKey(randomBase32Key);
}
@action
void setUseTOTP2FA(bool value) {
_settingsStore.useTOTP2FA = value;
}
@action
void _setBase32SecretKey(String value) {
if (_settingsStore.totpSecretKey == '') {
_settingsStore.totpSecretKey = value;
}
}
@action
void clearBase32SecretKey() {
_settingsStore.totpSecretKey = '';
}
Duration? banDuration() {
final unbanTimestamp = _sharedPreferences.getInt(banTimeoutKey);
if (unbanTimestamp == null) {
return null;
}
final unbanTime = DateTime.fromMillisecondsSinceEpoch(unbanTimestamp);
final now = DateTime.now();
if (now.isAfter(unbanTime)) {
return null;
}
return Duration(milliseconds: unbanTimestamp - now.millisecondsSinceEpoch);
}
Future<Duration> ban() async {
final multiplier = _failureCounter - maxFailedTrials;
final timeout = (multiplier * banTimeout) * 1000;
final unbanTimestamp = DateTime.now().millisecondsSinceEpoch + timeout;
await _sharedPreferences.setInt(banTimeoutKey, unbanTimestamp);
return Duration(milliseconds: timeout);
}
@action
Future<bool> totp2FAAuth(String otpText, bool isForSetup) async {
state = InitialExecutionState();
_failureCounter = _settingsStore.numberOfFailedTokenTrials;
final _banDuration = banDuration();
if (_banDuration != null) {
state = AuthenticationBanned(
error: S.current.auth_store_banned_for +
'${_banDuration.inMinutes}' +
S.current.auth_store_banned_minutes);
return false;
}
final result = Utils.verify(
secretKey: secretKey,
otp: otpText,
);
isForSetup ? setUseTOTP2FA(result) : null;
if (result) {
return true;
} else {
final value = _settingsStore.numberOfFailedTokenTrials + 1;
adjustTokenTrialNumber(value);
print(value);
if (_failureCounter >= maxFailedTrials) {
final banDuration = await ban();
state = AuthenticationBanned(
error: S.current.auth_store_banned_for +
'${banDuration.inMinutes}' +
S.current.auth_store_banned_minutes);
return false;
}
state = FailureState('Incorrect code');
return false;
}
}
@action
void success() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
state = ExecutedSuccessfullyState();
adjustTokenTrialNumber(0);
});
}
@action
void adjustTokenTrialNumber(int value) {
_failureCounter = value;
_settingsStore.numberOfFailedTokenTrials = value;
}
void _saveLastAuthTime(ExecutionState state) {
if (state is ExecutedSuccessfullyState) {
_authService.saveLastAuthTime();
}
}
@computed
Cake2FAPresetsOptions get selectedCake2FAPreset => _settingsStore.selectedCake2FAPreset;
@observable
bool unhighlightTabs = false;
@observable
ObservableList<VerboseControlSettings> selected2FASettings;
//! The code here works, but can be improved
//! Still trying out various ways to improve it
@action
void selectCakePreset(Cake2FAPresetsOptions cake2FAPreset) {
// The tabs are ordered in the format [Narrow || Normal || Verbose]
// Where Narrow = 0, Normal = 1 and Verbose = 2
switch (cake2FAPreset) {
case Cake2FAPresetsOptions.narrow:
activateCake2FANarrowPreset();
break;
case Cake2FAPresetsOptions.normal:
activateCake2FANormalPreset();
break;
case Cake2FAPresetsOptions.aggressive:
activateCake2FAAggressivePreset();
break;
default:
activateCake2FANormalPreset();
}
}
@action
void checkIfTheCurrentSettingMatchesAnyOfThePresets() {
final hasNormalPreset = checkIfTheNormalPresetIsPresent();
final hasNarrowPreset = checkIfTheNarrowPresetIsPresent();
final hasVerbosePreset = checkIfTheVerbosePresetIsPresent();
if (hasNormalPreset || hasNarrowPreset || hasVerbosePreset) {
unhighlightTabs = false;
} else {
unhighlightTabs = true;
}
}
@action
bool checkIfTheNormalPresetIsPresent() {
final hasContacts = selected2FASettings.contains(VerboseControlSettings.sendsToContacts);
final hasNonContacts = selected2FASettings.contains(VerboseControlSettings.sendsToNonContacts);
final hasSecurityAndBackup =
selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings);
final hasSendToInternalWallet =
selected2FASettings.contains(VerboseControlSettings.sendsToInternalWallets);
final hasExchangesToInternalWallet =
selected2FASettings.contains(VerboseControlSettings.exchangesToInternalWallets);
bool isOnlyNormalPresetControlsPresent = selected2FASettings.length == 5;
return (hasContacts &&
hasNonContacts &&
hasSecurityAndBackup &&
hasSendToInternalWallet &&
hasExchangesToInternalWallet &&
isOnlyNormalPresetControlsPresent);
}
@action
bool checkIfTheVerbosePresetIsPresent() {
final hasAccessWallets = selected2FASettings.contains(VerboseControlSettings.accessWallet);
final hasSecurityAndBackup =
selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings);
bool isOnlyVerbosePresetControlsPresent = selected2FASettings.length == 2;
return (hasAccessWallets && hasSecurityAndBackup && isOnlyVerbosePresetControlsPresent);
}
@action
bool checkIfTheNarrowPresetIsPresent() {
final hasNonContacts = selected2FASettings.contains(VerboseControlSettings.sendsToNonContacts);
final hasAddContacts = selected2FASettings.contains(VerboseControlSettings.addingContacts);
final hasCreateNewWallet =
selected2FASettings.contains(VerboseControlSettings.creatingNewWallets);
final hasSecurityAndBackup =
selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings);
bool isOnlyNarrowPresetControlsPresent = selected2FASettings.length == 4;
return (hasNonContacts &&
hasAddContacts &&
hasCreateNewWallet &&
hasSecurityAndBackup &&
isOnlyNarrowPresetControlsPresent);
}
@action
void activateCake2FANormalPreset() {
_settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.normal;
setAllControlsToFalse();
switchShouldRequireTOTP2FAForSendsToNonContact(true);
switchShouldRequireTOTP2FAForSendsToContact(true);
switchShouldRequireTOTP2FAForSendsToInternalWallets(true);
switchShouldRequireTOTP2FAForExchangesToInternalWallets(true);
switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true);
}
@action
void activateCake2FANarrowPreset() {
_settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.narrow;
setAllControlsToFalse();
switchShouldRequireTOTP2FAForSendsToNonContact(true);
switchShouldRequireTOTP2FAForAddingContacts(true);
switchShouldRequireTOTP2FAForCreatingNewWallet(true);
switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true);
}
@action
void activateCake2FAAggressivePreset() {
_settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.aggressive;
setAllControlsToFalse();
switchShouldRequireTOTP2FAForAccessingWallet(true);
switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true);
}
@action
void setAllControlsToFalse() {
switchShouldRequireTOTP2FAForAccessingWallet(false);
switchShouldRequireTOTP2FAForSendsToContact(false);
switchShouldRequireTOTP2FAForSendsToNonContact(false);
switchShouldRequireTOTP2FAForAddingContacts(false);
switchShouldRequireTOTP2FAForCreatingNewWallet(false);
switchShouldRequireTOTP2FAForExchangesToInternalWallets(false);
switchShouldRequireTOTP2FAForSendsToInternalWallets(false);
switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(false);
selected2FASettings.clear();
unhighlightTabs = false;
}
@action
void switchShouldRequireTOTP2FAForAccessingWallet(bool value) {
_settingsStore.shouldRequireTOTP2FAForAccessingWallet = value;
if (value) {
selected2FASettings.add(VerboseControlSettings.accessWallet);
} else {
selected2FASettings.remove(VerboseControlSettings.accessWallet);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForSendsToContact(bool value) {
_settingsStore.shouldRequireTOTP2FAForSendsToContact = value;
if (value) {
selected2FASettings.add(VerboseControlSettings.sendsToContacts);
} else {
selected2FASettings.remove(VerboseControlSettings.sendsToContacts);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForSendsToNonContact(bool value) {
_settingsStore.shouldRequireTOTP2FAForSendsToNonContact = value;
if (value) {
selected2FASettings.add(VerboseControlSettings.sendsToNonContacts);
} else {
selected2FASettings.remove(VerboseControlSettings.sendsToNonContacts);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForSendsToInternalWallets(bool value) {
_settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets = value;
if (value) {
selected2FASettings.add(VerboseControlSettings.sendsToInternalWallets);
} else {
selected2FASettings.remove(VerboseControlSettings.sendsToInternalWallets);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForExchangesToInternalWallets(bool value) {
_settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets = value;
if (value) {
selected2FASettings.add(VerboseControlSettings.exchangesToInternalWallets);
} else {
selected2FASettings.remove(VerboseControlSettings.exchangesToInternalWallets);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForAddingContacts(bool value) {
_settingsStore.shouldRequireTOTP2FAForAddingContacts = value;
if (value)
selected2FASettings.add(VerboseControlSettings.addingContacts);
else {
selected2FASettings.remove(VerboseControlSettings.addingContacts);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForCreatingNewWallet(bool value) {
_settingsStore.shouldRequireTOTP2FAForCreatingNewWallets = value;
if (value) {
selected2FASettings.add(VerboseControlSettings.creatingNewWallets);
} else {
selected2FASettings.remove(VerboseControlSettings.creatingNewWallets);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
@action
void switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(bool value) {
_settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings = value;
if (value)
selected2FASettings.add(VerboseControlSettings.securityAndBackupSettings);
else {
selected2FASettings.remove(VerboseControlSettings.securityAndBackupSettings);
}
checkIfTheCurrentSettingMatchesAnyOfThePresets();
}
}