dev
dart 405 lines 13.5 KB
Raw
1 // ignore_for_file: prefer_final_fields
2
3 import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
4 import 'package:cake_wallet/store/settings_store.dart';
5 import 'package:cake_wallet/utils/totp_utils.dart' as Utils;
6 import 'package:cake_wallet/view_model/auth_state.dart';
7 import 'package:flutter/widgets.dart';
8 import 'package:mobx/mobx.dart';
9 import 'package:shared_preferences/shared_preferences.dart';
10
11 import '../core/auth_service.dart';
12 import '../core/execution_state.dart';
13 import '../generated/i18n.dart';
14
15 part 'set_up_2fa_viewmodel.g.dart';
16
17 class Setup2FAViewModel = Setup2FAViewModelBase with _$Setup2FAViewModel;
18
19 abstract class Setup2FAViewModelBase with Store {
20 final SettingsStore _settingsStore;
21 final AuthService _authService;
22 final SharedPreferences _sharedPreferences;
23
24 Setup2FAViewModelBase(this._settingsStore, this._sharedPreferences, this._authService)
25 : _failureCounter = 0,
26 enteredOTPCode = '',
27 unhighlightTabs = false,
28 selected2FASettings = ObservableList<VerboseControlSettings>(),
29 state = InitialExecutionState() {
30 if (selectedCake2FAPreset != Cake2FAPresetsOptions.none) {
31 selectCakePreset(selectedCake2FAPreset);
32 }
33 reaction((_) => state, _saveLastAuthTime);
34 }
35
36 static const maxFailedTrials = 3;
37 static const banTimeout = 180; // 3 minutes
38 final banTimeoutKey = S.current.auth_store_ban_timeout;
39
40 String get deviceName => _settingsStore.deviceName;
41
42 @computed
43 String get totpSecretKey => _settingsStore.totpSecretKey;
44
45 String totpVersionOneLink = '';
46
47 @observable
48 ExecutionState state;
49
50 @observable
51 int _failureCounter;
52
53 @observable
54 String enteredOTPCode;
55
56 @computed
57 bool get useTOTP2FA => _settingsStore.useTOTP2FA;
58
59 @computed
60 bool get shouldRequireTOTP2FAForAccessingWallet =>
61 _settingsStore.shouldRequireTOTP2FAForAccessingWallet;
62
63 @computed
64 bool get shouldRequireTOTP2FAForSendsToContact =>
65 _settingsStore.shouldRequireTOTP2FAForSendsToContact;
66
67 @computed
68 bool get shouldRequireTOTP2FAForSendsToNonContact =>
69 _settingsStore.shouldRequireTOTP2FAForSendsToNonContact;
70
71 @computed
72 bool get shouldRequireTOTP2FAForSendsToInternalWallets =>
73 _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;
74
75 @computed
76 bool get shouldRequireTOTP2FAForExchangesToInternalWallets =>
77 _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets;
78
79 @computed
80 bool get shouldRequireTOTP2FAForExchangesToExternalWallets =>
81 _settingsStore.shouldRequireTOTP2FAForExchangesToExternalWallets;
82
83 @computed
84 bool get shouldRequireTOTP2FAForAddingContacts =>
85 _settingsStore.shouldRequireTOTP2FAForAddingContacts;
86
87 @computed
88 bool get shouldRequireTOTP2FAForCreatingNewWallets =>
89 _settingsStore.shouldRequireTOTP2FAForCreatingNewWallets;
90
91 @computed
92 bool get shouldRequireTOTP2FAForAllSecurityAndBackupSettings =>
93 _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings;
94
95 @action
96 void generateSecretKey() {
97 final _totpSecretKey = Utils.generateRandomBase32SecretKey(16);
98
99 totpVersionOneLink =
100 'otpauth://totp/Cake%20Wallet:$deviceName?secret=$_totpSecretKey&issuer=Cake%20Wallet&algorithm=SHA512&digits=8&period=30';
101
102 setTOTPSecretKey(_totpSecretKey);
103 }
104
105 @action
106 void setUseTOTP2FA(bool value) {
107 _settingsStore.useTOTP2FA = value;
108 }
109
110 @action
111 void setTOTPSecretKey(String value) {
112 _settingsStore.totpSecretKey = value;
113 }
114
115 Duration? banDuration() {
116 final unbanTimestamp = _sharedPreferences.getInt(banTimeoutKey);
117
118 if (unbanTimestamp == null) {
119 return null;
120 }
121
122 final unbanTime = DateTime.fromMillisecondsSinceEpoch(unbanTimestamp);
123 final now = DateTime.now();
124
125 if (now.isAfter(unbanTime)) {
126 return null;
127 }
128
129 return Duration(milliseconds: unbanTimestamp - now.millisecondsSinceEpoch);
130 }
131
132 Future<Duration> ban() async {
133 final multiplier = _failureCounter - maxFailedTrials;
134 final timeout = (multiplier * banTimeout) * 1000;
135 final unbanTimestamp = DateTime.now().millisecondsSinceEpoch + timeout;
136 await _sharedPreferences.setInt(banTimeoutKey, unbanTimestamp);
137
138 return Duration(milliseconds: timeout);
139 }
140
141 @action
142 Future<bool> totp2FAAuth(String otpText, bool isForSetup) async {
143 state = InitialExecutionState();
144 _failureCounter = _settingsStore.numberOfFailedTokenTrials;
145 final _banDuration = banDuration();
146
147 if (_banDuration != null) {
148 state = AuthenticationBanned(
149 error: S.current.auth_store_banned_for +
150 '${_banDuration.inMinutes}' +
151 S.current.auth_store_banned_minutes);
152 return false;
153 }
154
155 final result = Utils.verify(
156 secretKey: totpSecretKey,
157 otp: otpText,
158 );
159
160 isForSetup ? setUseTOTP2FA(result) : null;
161
162 if (result) {
163 return true;
164 } else {
165 final value = _settingsStore.numberOfFailedTokenTrials + 1;
166 adjustTokenTrialNumber(value);
167 if (_failureCounter >= maxFailedTrials) {
168 final banDuration = await ban();
169 state = AuthenticationBanned(
170 error: S.current.auth_store_banned_for +
171 '${banDuration.inMinutes}' +
172 S.current.auth_store_banned_minutes);
173 return false;
174 }
175
176 state = FailureState('Incorrect code');
177 return false;
178 }
179 }
180
181 @action
182 void success() {
183 WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
184 state = ExecutedSuccessfullyState();
185 adjustTokenTrialNumber(0);
186 });
187 }
188
189 @action
190 void adjustTokenTrialNumber(int value) {
191 _failureCounter = value;
192 _settingsStore.numberOfFailedTokenTrials = value;
193 }
194
195 void _saveLastAuthTime(ExecutionState state) {
196 if (state is ExecutedSuccessfullyState) {
197 _authService.saveLastAuthTime();
198 }
199 }
200
201 @computed
202 Cake2FAPresetsOptions get selectedCake2FAPreset => _settingsStore.selectedCake2FAPreset;
203
204 @observable
205 bool unhighlightTabs = false;
206
207 @observable
208 ObservableList<VerboseControlSettings> selected2FASettings;
209
210 @action
211 void checkIfTheCurrentSettingMatchesAnyOfThePresets() {
212 final hasNormalPreset = checkIfTheNormalPresetIsPresent();
213 final hasNarrowPreset = checkIfTheNarrowPresetIsPresent();
214 final hasVerbosePreset = checkIfTheVerbosePresetIsPresent();
215
216 if (hasNormalPreset || hasNarrowPreset || hasVerbosePreset) return;
217
218 noCake2FAPresetSelected();
219 }
220
221 @action
222 bool checkIfTheNormalPresetIsPresent() {
223 final hasContacts = selected2FASettings.contains(VerboseControlSettings.sendsToContacts);
224 final hasNonContacts = selected2FASettings.contains(VerboseControlSettings.sendsToNonContacts);
225 final hasSecurityAndBackup =
226 selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings);
227
228 final hasSendToInternalWallet =
229 selected2FASettings.contains(VerboseControlSettings.sendsToInternalWallets);
230
231 final hasExchangesToInternalWallet =
232 selected2FASettings.contains(VerboseControlSettings.exchangesToInternalWallets);
233
234 bool isOnlyNormalPresetControlsPresent = selected2FASettings.length == 5;
235
236 return (hasContacts &&
237 hasNonContacts &&
238 hasSecurityAndBackup &&
239 hasSendToInternalWallet &&
240 hasExchangesToInternalWallet &&
241 isOnlyNormalPresetControlsPresent);
242 }
243
244 @action
245 bool checkIfTheVerbosePresetIsPresent() {
246 final hasAccessWallets = selected2FASettings.contains(VerboseControlSettings.accessWallet);
247 final hasSecurityAndBackup =
248 selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings);
249
250 bool isOnlyVerbosePresetControlsPresent = selected2FASettings.length == 2;
251
252 return (hasAccessWallets && hasSecurityAndBackup && isOnlyVerbosePresetControlsPresent);
253 }
254
255 @action
256 bool checkIfTheNarrowPresetIsPresent() {
257 final hasNonContacts = selected2FASettings.contains(VerboseControlSettings.sendsToNonContacts);
258 final hasAddContacts = selected2FASettings.contains(VerboseControlSettings.addingContacts);
259 final hasCreateNewWallet =
260 selected2FASettings.contains(VerboseControlSettings.creatingNewWallets);
261 final hasSecurityAndBackup =
262 selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings);
263
264 bool isOnlyNarrowPresetControlsPresent = selected2FASettings.length == 4;
265
266 return (hasNonContacts &&
267 hasAddContacts &&
268 hasCreateNewWallet &&
269 hasSecurityAndBackup &&
270 isOnlyNarrowPresetControlsPresent);
271 }
272
273 @action
274 void noCake2FAPresetSelected() {
275 _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.none;
276 }
277
278 @action
279 void setAllControlsToFalse() {
280 switchShouldRequireTOTP2FAForAccessingWallet(false);
281 switchShouldRequireTOTP2FAForSendsToContact(false);
282 switchShouldRequireTOTP2FAForSendsToNonContact(false);
283 switchShouldRequireTOTP2FAForAddingContacts(false);
284 switchShouldRequireTOTP2FAForCreatingNewWallet(false);
285 switchShouldRequireTOTP2FAForExchangesToInternalWallets(false);
286 switchShouldRequireTOTP2FAForExchangesToExternalWallets(false);
287 switchShouldRequireTOTP2FAForSendsToInternalWallets(false);
288 switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(false);
289 selected2FASettings.clear();
290 unhighlightTabs = false;
291 }
292
293 final Map<Cake2FAPresetsOptions, List<VerboseControlSettings>> presetsMap = {
294 Cake2FAPresetsOptions.normal: [
295 VerboseControlSettings.sendsToContacts,
296 VerboseControlSettings.sendsToNonContacts,
297 VerboseControlSettings.sendsToInternalWallets,
298 VerboseControlSettings.securityAndBackupSettings,
299 VerboseControlSettings.exchangesToInternalWallets
300 ],
301 Cake2FAPresetsOptions.narrow: [
302 VerboseControlSettings.addingContacts,
303 VerboseControlSettings.sendsToNonContacts,
304 VerboseControlSettings.creatingNewWallets,
305 VerboseControlSettings.securityAndBackupSettings,
306 ],
307 Cake2FAPresetsOptions.aggressive: [
308 VerboseControlSettings.accessWallet,
309 VerboseControlSettings.securityAndBackupSettings,
310 ],
311 Cake2FAPresetsOptions.none: [],
312 };
313
314 @action
315 void selectCakePreset(Cake2FAPresetsOptions preset) {
316 setAllControlsToFalse();
317 presetsMap[preset]?.forEach(toggleControl);
318 _settingsStore.selectedCake2FAPreset = preset;
319 }
320
321 @action
322 void toggleControl(VerboseControlSettings control, [bool value = true]) {
323 final methodsMap = {
324 VerboseControlSettings.sendsToContacts: switchShouldRequireTOTP2FAForSendsToContact,
325 VerboseControlSettings.accessWallet: switchShouldRequireTOTP2FAForAccessingWallet,
326 VerboseControlSettings.addingContacts: switchShouldRequireTOTP2FAForAddingContacts,
327 VerboseControlSettings.creatingNewWallets: switchShouldRequireTOTP2FAForCreatingNewWallet,
328 VerboseControlSettings.sendsToNonContacts: switchShouldRequireTOTP2FAForSendsToNonContact,
329 VerboseControlSettings.sendsToInternalWallets:
330 switchShouldRequireTOTP2FAForSendsToInternalWallets,
331 VerboseControlSettings.securityAndBackupSettings:
332 switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
333 VerboseControlSettings.exchangesToInternalWallets:
334 switchShouldRequireTOTP2FAForExchangesToInternalWallets,
335 VerboseControlSettings.exchangesToExternalWallets:
336 switchShouldRequireTOTP2FAForExchangesToExternalWallets,
337 };
338
339 methodsMap[control]?.call(value);
340 }
341
342 @action
343 void switchShouldRequireTOTP2FAForSendsToContact(bool value) {
344 _settingsStore.shouldRequireTOTP2FAForSendsToContact = value;
345 updateSelectedSettings(VerboseControlSettings.sendsToContacts, value);
346 }
347
348 @action
349 void switchShouldRequireTOTP2FAForAccessingWallet(bool value) {
350 _settingsStore.shouldRequireTOTP2FAForAccessingWallet = value;
351 updateSelectedSettings(VerboseControlSettings.accessWallet, value);
352 }
353
354 @action
355 void switchShouldRequireTOTP2FAForSendsToNonContact(bool value) {
356 _settingsStore.shouldRequireTOTP2FAForSendsToNonContact = value;
357 updateSelectedSettings(VerboseControlSettings.sendsToNonContacts, value);
358 }
359
360 @action
361 void switchShouldRequireTOTP2FAForSendsToInternalWallets(bool value) {
362 _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets = value;
363 updateSelectedSettings(VerboseControlSettings.sendsToInternalWallets, value);
364 }
365
366 @action
367 void switchShouldRequireTOTP2FAForExchangesToInternalWallets(bool value) {
368 _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets = value;
369 updateSelectedSettings(VerboseControlSettings.exchangesToInternalWallets, value);
370 }
371
372 @action
373 void switchShouldRequireTOTP2FAForExchangesToExternalWallets(bool value) {
374 _settingsStore.shouldRequireTOTP2FAForExchangesToExternalWallets = value;
375 updateSelectedSettings(VerboseControlSettings.exchangesToExternalWallets, value);
376 }
377
378 @action
379 void switchShouldRequireTOTP2FAForAddingContacts(bool value) {
380 _settingsStore.shouldRequireTOTP2FAForAddingContacts = value;
381 updateSelectedSettings(VerboseControlSettings.addingContacts, value);
382 }
383
384 @action
385 void switchShouldRequireTOTP2FAForCreatingNewWallet(bool value) {
386 _settingsStore.shouldRequireTOTP2FAForCreatingNewWallets = value;
387 updateSelectedSettings(VerboseControlSettings.creatingNewWallets, value);
388 }
389
390 @action
391 void switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(bool value) {
392 _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings = value;
393 updateSelectedSettings(VerboseControlSettings.securityAndBackupSettings, value);
394 }
395
396 @action
397 void updateSelectedSettings(VerboseControlSettings control, bool value) {
398 if (value) {
399 selected2FASettings.add(control);
400 } else {
401 selected2FASettings.remove(control);
402 }
403 checkIfTheCurrentSettingMatchesAnyOfThePresets();
404 }
405 }