Skip to content

Commit 897a04d

Browse files
committed
refactor: Added a StoredSalt to add a distinction between a Salt object and the stored Salt object.
1 parent 22da7ce commit 897a04d

17 files changed

Lines changed: 134 additions & 103 deletions

File tree

lib/main.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import 'package:open_authenticator/i18n/translations.g.dart';
1010
import 'package:open_authenticator/model/app_links.dart';
1111
import 'package:open_authenticator/model/backend/authentication/providers/provider.dart';
1212
import 'package:open_authenticator/model/backend/authentication/session.dart';
13-
import 'package:open_authenticator/model/crypto.dart';
13+
import 'package:open_authenticator/model/crypto/store.dart';
1414
import 'package:open_authenticator/model/settings/show_intro.dart';
1515
import 'package:open_authenticator/model/settings/theme.dart';
1616
import 'package:open_authenticator/model/totp/limit.dart';

lib/model/app_unlock/methods/master_password.dart

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class MasterPasswordAppUnlockMethod extends AppUnlockMethod<String> {
3030
}
3131

3232
if (reason == .openApp) {
33-
Salt? salt = await Salt.readFromLocalStorage();
33+
Salt? salt = await _ref.read(saltProvider.future);
3434
_ref.read(cryptoStoreProvider.notifier).use(CryptoStore.fromPassword(result.value, salt!));
3535
}
3636

@@ -62,7 +62,8 @@ class MasterPasswordAppUnlockMethod extends AppUnlockMethod<String> {
6262
return null;
6363
}
6464
}
65-
if (!(await _ensureSaltAvailable())) {
65+
Salt? salt = await _ref.read(saltProvider.future);
66+
if (salt == null) {
6667
return MasterPasswordNoSalt();
6768
}
6869
List<PasswordVerificationMethod> passwordVerificationMethods = await _ref.read(passwordVerificationProvider.future);
@@ -72,28 +73,6 @@ class MasterPasswordAppUnlockMethod extends AppUnlockMethod<String> {
7273
return null;
7374
}
7475

75-
/// Ensures that the local salt exists, restoring it from the TOTP list when possible.
76-
Future<bool> _ensureSaltAvailable() async {
77-
if (await Salt.readFromLocalStorage() != null) {
78-
return true;
79-
}
80-
try {
81-
List<Totp> totps = await _ref.read(totpRepositoryProvider.future);
82-
Salt? salt = totps.firstOrNull?.encryptedData.encryptionSalt;
83-
if (salt == null) {
84-
return false;
85-
}
86-
await salt.saveToLocalStorage();
87-
_ref.invalidate(cryptoStoreProvider);
88-
_ref.invalidate(cryptoStoreVerificationMethodProvider);
89-
_ref.invalidate(passwordVerificationProvider);
90-
return true;
91-
} catch (ex, stackTrace) {
92-
handleException(ex, stackTrace);
93-
return false;
94-
}
95-
}
96-
9776
/// Prompts master password for unlock.
9877
Future<Result<String>> _promptMasterPasswordForUnlock(BuildContext context, String? message) async {
9978
String? password = await MasterPasswordInputDialog.prompt(

lib/model/app_unlock/methods/method.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@ import 'package:local_auth/local_auth.dart' hide LocalAuthentication;
44
import 'package:open_authenticator/i18n/localizable_exception.dart';
55
import 'package:open_authenticator/i18n/translations.g.dart';
66
import 'package:open_authenticator/model/app_unlock/reason.dart';
7-
import 'package:open_authenticator/model/crypto.dart';
8-
import 'package:open_authenticator/model/password_verification/methods/crypto_store.dart';
7+
import 'package:open_authenticator/model/crypto/salt.dart';
8+
import 'package:open_authenticator/model/crypto/store.dart';
99
import 'package:open_authenticator/model/password_verification/methods/method.dart';
1010
import 'package:open_authenticator/model/password_verification/methods/password_signature.dart';
1111
import 'package:open_authenticator/model/password_verification/password_verification.dart';
1212
import 'package:open_authenticator/model/totp/repository.dart';
1313
import 'package:open_authenticator/model/totp/totp.dart';
1414
import 'package:open_authenticator/utils/local_authentication/local_authentication.dart';
1515
import 'package:open_authenticator/utils/result.dart';
16-
import 'package:open_authenticator/utils/utils.dart';
1716
import 'package:open_authenticator/widgets/dialog/text_input_dialog.dart';
1817

1918
part 'local_auth.dart';

lib/model/backup.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import 'package:flutter_riverpod/misc.dart';
88
import 'package:open_authenticator/app.dart';
99
import 'package:open_authenticator/i18n/localizable_exception.dart';
1010
import 'package:open_authenticator/i18n/translations.g.dart';
11-
import 'package:open_authenticator/model/crypto.dart';
11+
import 'package:open_authenticator/model/crypto/salt.dart';
12+
import 'package:open_authenticator/model/crypto/store.dart';
1213
import 'package:open_authenticator/model/totp/decrypted.dart';
1314
import 'package:open_authenticator/model/totp/json.dart';
1415
import 'package:open_authenticator/model/totp/repository.dart';

lib/model/crypto/salt.dart

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
4+
import 'package:cipherlib/random.dart';
5+
import 'package:flutter/foundation.dart';
6+
import 'package:flutter_riverpod/flutter_riverpod.dart';
7+
import 'package:open_authenticator/model/database/database.dart';
8+
import 'package:open_authenticator/model/totp/totp.dart';
9+
import 'package:simple_secure_storage/simple_secure_storage.dart';
10+
11+
/// The salt provider instance.
12+
final saltProvider = AsyncNotifierProvider<StoredSalt, Salt?>(StoredSalt.new);
13+
14+
/// Allows to get and set the salt.
15+
class StoredSalt extends AsyncNotifier<Salt?> {
16+
/// The password derived key storage key.
17+
static const String _kPasswordDerivedKeySaltKey = 'passwordDerivedKeySalt';
18+
19+
@override
20+
Future<Salt?> build() async {
21+
String? value = await SimpleSecureStorage.read(_kPasswordDerivedKeySaltKey);
22+
if (value != null) {
23+
return Salt.fromRawValue(
24+
value: base64.decode(value),
25+
);
26+
}
27+
return await _tryRecoverSaltFromDatabase();
28+
}
29+
30+
/// Tries to recover the salt from the database.
31+
Future<Salt?> _tryRecoverSaltFromDatabase() async {
32+
List<Totp> totps = await ref.read(appDatabaseProvider).listTotps();
33+
Salt? salt = totps.firstOrNull?.encryptedData.encryptionSalt;
34+
if (salt != null) {
35+
await _saveToLocalStorage(salt);
36+
}
37+
return salt;
38+
}
39+
40+
/// Changes the current salt.
41+
Future<void> changeSalt(Salt salt) async {
42+
_saveToLocalStorage(salt);
43+
if (ref.mounted) {
44+
state = AsyncData(salt);
45+
}
46+
}
47+
48+
/// Saves the given [salt] to the local storage.
49+
Future<void> _saveToLocalStorage(Salt salt) async => await SimpleSecureStorage.write(_kPasswordDerivedKeySaltKey, base64.encode(salt.value));
50+
51+
/// Deletes the current salt from the local storage and resets the current state.
52+
Future<void> deleteFromLocalStorage() async {
53+
await SimpleSecureStorage.delete(_kPasswordDerivedKeySaltKey);
54+
if (ref.mounted) {
55+
state = const AsyncData(null);
56+
}
57+
}
58+
}
59+
60+
/// Represents a decoded salt.
61+
class Salt {
62+
/// The salt length.
63+
static const int _saltLength = 256 ~/ 8;
64+
65+
/// The salt value.
66+
final Uint8List value;
67+
68+
/// Creates a new salt instance.
69+
const Salt.fromRawValue({
70+
required this.value,
71+
});
72+
73+
/// Generates a random salt.
74+
static Salt generate() => Salt.fromRawValue(
75+
value: randomBytes(_saltLength),
76+
);
77+
78+
@override
79+
String toString() => base64.encode(value);
80+
}
Lines changed: 14 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:open_authenticator/app.dart';
1010
import 'package:open_authenticator/i18n/localizable_exception.dart';
1111
import 'package:open_authenticator/i18n/translations.g.dart';
1212
import 'package:open_authenticator/model/app_unlock/methods/method.dart';
13+
import 'package:open_authenticator/model/crypto/salt.dart';
1314
import 'package:open_authenticator/model/password_verification/methods/password_signature.dart';
1415
import 'package:open_authenticator/model/settings/app_unlock_method.dart';
1516
import 'package:open_authenticator/utils/utils.dart';
@@ -25,7 +26,7 @@ class StoredCryptoStore extends AsyncNotifier<CryptoStore?> {
2526

2627
@override
2728
FutureOr<CryptoStore?> build() async {
28-
Salt? salt = await Salt.readFromLocalStorage();
29+
Salt? salt = await ref.watch(saltProvider.future);
2930
if (salt == null) {
3031
return null;
3132
}
@@ -45,7 +46,7 @@ class StoredCryptoStore extends AsyncNotifier<CryptoStore?> {
4546
Future<void> deleteFromLocalStorage({bool deleteSalt = false}) async {
4647
await SimpleSecureStorage.delete(_kPasswordDerivedKeyKey);
4748
if (deleteSalt) {
48-
await Salt.deleteFromLocalStorage();
49+
await ref.read(saltProvider.notifier).deleteFromLocalStorage();
4950
}
5051
}
5152

@@ -57,7 +58,11 @@ class StoredCryptoStore extends AsyncNotifier<CryptoStore?> {
5758
}
5859

5960
/// Changes the current crypto store password, preserving the current salt if possible.
60-
Future<CryptoStore> changeCryptoStore(String newPassword, {CryptoStore? newCryptoStore, bool checkSettings = true}) async {
61+
Future<CryptoStore> changeCryptoStore(
62+
String newPassword, {
63+
CryptoStore? newCryptoStore,
64+
bool checkSettings = true,
65+
}) async {
6166
Salt? salt = newCryptoStore?.salt;
6267
if (salt == null) {
6368
CryptoStore? currentCryptoStore = await future;
@@ -71,7 +76,7 @@ class StoredCryptoStore extends AsyncNotifier<CryptoStore?> {
7176
}
7277
}
7378
Future<void> saveCryptoStoreOnLocalStorage() async => await SimpleSecureStorage.write(_kPasswordDerivedKeyKey, base64.encode(newCryptoStore!.key));
74-
await salt.saveToLocalStorage();
79+
await ref.read(saltProvider.notifier).changeSalt(salt);
7580
if (checkSettings) {
7681
String unlockMethod = await ref.read(appUnlockMethodSettingsEntryProvider.future);
7782
if (unlockMethod == MasterPasswordAppUnlockMethod.kMethodId) {
@@ -89,9 +94,6 @@ class StoredCryptoStore extends AsyncNotifier<CryptoStore?> {
8994

9095
/// Allows to encrypt some data according to a key.
9196
class CryptoStore {
92-
/// The key length.
93-
static const int _keyLength = 256 ~/ 8;
94-
9597
/// The initialization vector length.
9698
static const int _initializationVectorLength = 96 ~/ 8;
9799

@@ -109,10 +111,11 @@ class CryptoStore {
109111
});
110112

111113
/// Creates a [CryptoStoreWithPasswordSignature] from the given [password].
112-
CryptoStore.fromPassword(String password, Salt salt) : this._(
113-
key: _deriveKey(password, salt).bytes,
114-
salt: salt,
115-
);
114+
CryptoStore.fromPassword(String password, Salt salt)
115+
: this._(
116+
key: _deriveKey(password, salt).bytes,
117+
salt: salt,
118+
);
116119

117120
/// Generates a derived key from the given [password] and save it to the device secure storage.
118121
/// Also returns the salt that has been used.
@@ -164,48 +167,6 @@ class CryptoStore {
164167
MACHashBase get hmacSecretKey => sha256.hmac.by(key);
165168
}
166169

167-
/// Represents a decoded salt.
168-
class Salt {
169-
/// The salt length.
170-
static const int _saltLength = CryptoStore._keyLength;
171-
172-
/// The password derived key storage key.
173-
static const String _kPasswordDerivedKeySaltKey = 'passwordDerivedKeySalt';
174-
175-
/// The salt value.
176-
final Uint8List value;
177-
178-
/// Creates a new salt instance.
179-
const Salt.fromRawValue({
180-
required this.value,
181-
});
182-
183-
/// Reads the salt from local storage.
184-
static Future<Salt?> readFromLocalStorage() async {
185-
String? value = await SimpleSecureStorage.read(_kPasswordDerivedKeySaltKey);
186-
if (value == null) {
187-
return null;
188-
}
189-
return Salt.fromRawValue(
190-
value: base64.decode(value),
191-
);
192-
}
193-
194-
/// Generates a random salt.
195-
static Salt generate() => Salt.fromRawValue(
196-
value: randomBytes(_saltLength),
197-
);
198-
199-
/// Deletes the salt from local storage.
200-
static Future<void> deleteFromLocalStorage() async => await SimpleSecureStorage.delete(_kPasswordDerivedKeySaltKey);
201-
202-
/// Writes the salt to the secure storage.
203-
Future<void> saveToLocalStorage() async => await SimpleSecureStorage.write(_kPasswordDerivedKeySaltKey, base64.encode(value));
204-
205-
@override
206-
String toString() => base64.encode(value);
207-
}
208-
209170
/// Thrown when the password entered for a new crypto store is incorrect.
210171
class _PasswordMismatchException extends LocalizableException {
211172
/// Creates a new password mismatch exception instance.

lib/model/database/database.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
88
import 'package:open_authenticator/model/backend/request/response.dart';
99
import 'package:open_authenticator/model/backend/synchronization/push/operation.dart';
1010
import 'package:open_authenticator/model/backend/synchronization/push/result.dart';
11-
import 'package:open_authenticator/model/crypto.dart';
11+
import 'package:open_authenticator/model/crypto/salt.dart';
1212
import 'package:open_authenticator/model/database/database.steps.dart';
1313
import 'package:open_authenticator/model/totp/algorithm.dart';
1414
import 'package:open_authenticator/model/totp/json.dart';

lib/model/password_verification/methods/crypto_store.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'dart:async';
22

33
import 'package:flutter_riverpod/flutter_riverpod.dart';
4-
import 'package:open_authenticator/model/crypto.dart';
4+
import 'package:open_authenticator/model/crypto/store.dart';
55
import 'package:open_authenticator/model/password_verification/methods/method.dart';
66

77
/// The provider instance.

lib/model/password_verification/methods/password_signature.dart

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import 'dart:async';
22
import 'dart:convert';
33

44
import 'package:flutter_riverpod/flutter_riverpod.dart';
5-
import 'package:open_authenticator/model/crypto.dart';
5+
import 'package:open_authenticator/model/crypto/salt.dart';
6+
import 'package:open_authenticator/model/crypto/store.dart';
67
import 'package:open_authenticator/model/password_verification/methods/method.dart';
78
import 'package:simple_secure_storage/simple_secure_storage.dart';
89

@@ -19,18 +20,27 @@ class PasswordSignatureVerificationMethodNotifier extends AsyncNotifier<Password
1920
@override
2021
FutureOr<PasswordSignatureVerificationMethod> build() async => PasswordSignatureVerificationMethod(
2122
passwordSignature: await SimpleSecureStorage.read(_kPasswordSignatureKey),
23+
salt: await ref.watch(saltProvider.future),
2224
);
2325

2426
/// Enables the password signature verification method.
2527
Future<bool> enable(String? password) async {
26-
Salt? salt = await Salt.readFromLocalStorage();
27-
if (password == null || salt == null) {
28+
if (password == null) {
29+
return false;
30+
}
31+
Salt? salt = await ref.read(saltProvider.future);
32+
if (salt == null) {
2833
return false;
2934
}
3035
String passwordSignature = await _generatePasswordSignature(password, salt);
3136
await SimpleSecureStorage.write(_kPasswordSignatureKey, passwordSignature);
3237
if (ref.mounted) {
33-
state = AsyncData(PasswordSignatureVerificationMethod(passwordSignature: passwordSignature));
38+
state = AsyncData(
39+
PasswordSignatureVerificationMethod(
40+
passwordSignature: passwordSignature,
41+
salt: salt,
42+
),
43+
);
3444
}
3545
return true;
3646
}
@@ -39,7 +49,7 @@ class PasswordSignatureVerificationMethodNotifier extends AsyncNotifier<Password
3949
Future<void> disable() async {
4050
await SimpleSecureStorage.delete(_kPasswordSignatureKey);
4151
if (ref.mounted) {
42-
state = const AsyncData(PasswordSignatureVerificationMethod(passwordSignature: null));
52+
state = const AsyncData(PasswordSignatureVerificationMethod());
4353
}
4454
}
4555

@@ -56,24 +66,24 @@ class PasswordSignatureVerificationMethod with PasswordVerificationMethod {
5666
/// The password signature.
5767
final String? passwordSignature;
5868

69+
/// The salt instance.
70+
final Salt? salt;
71+
5972
/// Creates a new password signature verification method instance.
6073
const PasswordSignatureVerificationMethod({
6174
this.passwordSignature,
75+
this.salt,
6276
});
6377

6478
@override
65-
bool get enabled => passwordSignature != null;
79+
bool get enabled => passwordSignature != null && salt != null;
6680

6781
@override
6882
Future<bool> verify(String password) async {
6983
if (!(await super.verify(password))) {
7084
return false;
7185
}
72-
Salt? salt = await Salt.readFromLocalStorage();
73-
if (salt == null) {
74-
return false;
75-
}
76-
CryptoStore cryptoStore = CryptoStore.fromPassword(password, salt);
86+
CryptoStore cryptoStore = CryptoStore.fromPassword(password, salt!);
7787
return cryptoStore.hmacSecretKey.verify(base64.decode(passwordSignature!), utf8.encode(password));
7888
}
7989
}

lib/model/settings/storage_type.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'package:open_authenticator/model/backend/request/response.dart';
88
import 'package:open_authenticator/model/backend/synchronization/push/operation.dart';
99
import 'package:open_authenticator/model/backend/synchronization/queue.dart';
1010
import 'package:open_authenticator/model/backup.dart';
11-
import 'package:open_authenticator/model/crypto.dart';
11+
import 'package:open_authenticator/model/crypto/store.dart';
1212
import 'package:open_authenticator/model/database/database.dart';
1313
import 'package:open_authenticator/model/password_verification/password_verification.dart';
1414
import 'package:open_authenticator/model/settings/entry.dart';

0 commit comments

Comments
 (0)