From a5be1785c0f14a3a70e64c1af56256225d54698b Mon Sep 17 00:00:00 2001 From: Dillon Nys <24740863+dnys1@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:25:26 -0700 Subject: [PATCH] fix(native_storage): Clearing of secure storage on macOS/iOS (#109) - The `kSecMatchLimit` flag must always be `kSecMatchLimitAll` to return a list, but this was previously not set in release mode. - Adds tests for all platforms to check that the keys to be cleared only include those owned by native_storage --- .github/workflows/native_storage.yaml | 8 +- packages/native/storage/CHANGELOG.md | 1 + .../celest/native_storage/NativeStorage.kt | 4 + .../integration_test/storage_shared.dart | 24 ++++- .../integration_test/storage_test.dart | 8 +- .../lib/src/local/local_storage.android.dart | 4 + .../lib/src/local/local_storage.linux.dart | 6 ++ .../lib/src/local/local_storage.windows.dart | 60 ++----------- .../lib/src/local/local_storage_darwin.dart | 24 +++-- .../src/local/local_storage_platform.vm.dart | 7 +- .../src/local/local_storage_platform.web.dart | 19 ++-- .../storage/lib/src/memory_storage.dart | 14 ++- .../src/native/android/jni_bindings.ffi.dart | 11 +++ .../lib/src/native/windows/windows.dart | 90 +++++++++++++++++++ .../lib/src/native_storage_extended.dart | 17 ++++ .../src/secure/secure_storage.android.dart | 4 + .../lib/src/secure/secure_storage.darwin.dart | 42 +++++---- .../lib/src/secure/secure_storage.linux.dart | 31 +++++++ .../src/secure/secure_storage.windows.dart | 48 ++-------- .../secure/secure_storage_platform.vm.dart | 7 +- .../storage/test/native_storage_test.dart | 8 +- 21 files changed, 300 insertions(+), 137 deletions(-) create mode 100644 packages/native/storage/lib/src/native_storage_extended.dart diff --git a/.github/workflows/native_storage.yaml b/.github/workflows/native_storage.yaml index 6f507cfa..b8a6f3ec 100644 --- a/.github/workflows/native_storage.yaml +++ b/.github/workflows/native_storage.yaml @@ -123,8 +123,6 @@ jobs: export DISPLAY=:99 sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & flutter test -d linux integration_test/storage_test.dart - # TODO: Re-enable - # Need to fix this: Git error. Command: `git clone --mirror https://github.com/dart-lang/native /c/Users/runneradmin/.pub-cache\git\cache\native-647c69ed8027da6d6def6bc40efa87cf1a2f76aa` test_windows: runs-on: windows-latest timeout-minutes: 15 @@ -138,9 +136,9 @@ jobs: - name: Get Packages working-directory: packages/native/storage run: dart pub get --no-example - # - name: Test - # working-directory: packages/native/storage - # run: dart test + - name: Test + working-directory: packages/native/storage + run: dart test - name: Get Packages (Example) working-directory: packages/native/storage/example run: flutter pub get diff --git a/packages/native/storage/CHANGELOG.md b/packages/native/storage/CHANGELOG.md index 4ba354ff..b8a9f3d7 100644 --- a/packages/native/storage/CHANGELOG.md +++ b/packages/native/storage/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.1.3 - chore: Migrate to jni 0.8.0 to enable isolated Android storage +- fix: Removal of secure storage values on macOS/iOS in release mode ## 0.1.2 diff --git a/packages/native/storage/android/src/main/kotlin/dev/celest/native_storage/NativeStorage.kt b/packages/native/storage/android/src/main/kotlin/dev/celest/native_storage/NativeStorage.kt index 4603383c..7c8a0439 100644 --- a/packages/native/storage/android/src/main/kotlin/dev/celest/native_storage/NativeStorage.kt +++ b/packages/native/storage/android/src/main/kotlin/dev/celest/native_storage/NativeStorage.kt @@ -27,6 +27,10 @@ sealed class NativeStorage( private val editor: SharedPreferences.Editor get() = sharedPreferences.edit() + val allKeys: List + get() = sharedPreferences.all.keys.filter { it.startsWith(prefix) } + .map { it.substring(prefix.length) }.toList() + fun write(key: String, value: String?) { println("Writing: $prefix$key") with(editor) { diff --git a/packages/native/storage/example/integration_test/storage_shared.dart b/packages/native/storage/example/integration_test/storage_shared.dart index caf5beac..bd614021 100644 --- a/packages/native/storage/example/integration_test/storage_shared.dart +++ b/packages/native/storage/example/integration_test/storage_shared.dart @@ -1,9 +1,10 @@ import 'dart:math'; import 'package:native_storage/native_storage.dart'; +import 'package:native_storage/src/native_storage_extended.dart'; import 'package:test/test.dart'; -void sharedTests(String name, NativeStorageFactory factory) { +void sharedTests(String name, NativeStorageExtendedFactory factory) { group(name, () { const allowedNamespaces = [null, 'com.domain.myapp']; const allowedScopes = [null, 'scope', 'scope1/scope2']; @@ -29,6 +30,8 @@ void sharedTests(String name, NativeStorageFactory factory) { test('writes a new key-value pair to storage', () { storage.write(key, 'value'); expect(storage.read(key), 'value'); + + expect(storage.allKeys, equals([key])); }); test('updates the value for an existing key', () { @@ -38,12 +41,15 @@ void sharedTests(String name, NativeStorageFactory factory) { storage.write(key, 'update'); expect(storage.read(key), 'update', reason: 'Value was updated'); + + expect(storage.allKeys, equals([key])); }); }); group('read', () { test('can read a non-existent key', () { expect(storage.read(key), isNull); + expect(storage.allKeys, isEmpty); }); }); @@ -52,9 +58,11 @@ void sharedTests(String name, NativeStorageFactory factory) { storage.write(key, 'delete'); expect(storage.read(key), 'delete', reason: 'Value was written'); + expect(storage.allKeys, equals([key])); storage.delete(key); expect(storage.read(key), isNull, reason: 'Value was deleted'); + expect(storage.allKeys, isEmpty); }); test('can delete a non-existent key', () { @@ -74,6 +82,7 @@ void sharedTests(String name, NativeStorageFactory factory) { storage.write(key2, value2); expect(storage.read(key1), value1); expect(storage.read(key2), value2); + expect(storage.allKeys, unorderedEquals([key1, key2])); storage.clear(); @@ -87,6 +96,7 @@ void sharedTests(String name, NativeStorageFactory factory) { isNull, reason: 'Storage was cleared', ); + expect(storage.allKeys, isEmpty); }); test('does not throw when no items present', () { @@ -233,10 +243,16 @@ void sharedTests(String name, NativeStorageFactory factory) { expect(parent.read('key'), 'parentValue'); expect(child.read('key'), 'childValue'); + expect(parent.allKeys, unorderedEquals(['key', 'child/key'])); + expect((child as NativeStorageExtended).allKeys, ['key']); + parent.clear(); expect(parent.read('key'), isNull); expect(child.read('key'), isNull); + + expect(parent.allKeys, isEmpty); + expect(child.allKeys, isEmpty); }); test('child does not clear parent', () { @@ -249,11 +265,17 @@ void sharedTests(String name, NativeStorageFactory factory) { expect(parent.read('key'), 'parentValue'); expect(child.read('key'), 'childValue'); + expect(parent.allKeys, unorderedEquals(['key', 'child/key'])); + expect((child as NativeStorageExtended).allKeys, ['key']); + child.clear(); expect(parent.read('key'), 'parentValue'); expect(child.read('key'), isNull); + expect(parent.allKeys, ['key']); + expect(child.allKeys, isEmpty); + parent.clear(); }); }); diff --git a/packages/native/storage/example/integration_test/storage_test.dart b/packages/native/storage/example/integration_test/storage_test.dart index c3eef128..472f6a04 100644 --- a/packages/native/storage/example/integration_test/storage_test.dart +++ b/packages/native/storage/example/integration_test/storage_test.dart @@ -1,11 +1,15 @@ import 'package:integration_test/integration_test.dart'; import 'package:native_storage/native_storage.dart'; +import 'package:native_storage/src/local/local_storage_platform.vm.dart' + if (dart.library.js_interop) 'package:native_storage/src/local/local_storage_platform.web.dart'; +import 'package:native_storage/src/secure/secure_storage_platform.vm.dart' + if (dart.library.js_interop) 'package:native_storage/src/secure/secure_storage_platform.web.dart'; import 'storage_shared.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); sharedTests('NativeMemoryStorage', NativeMemoryStorage.new); - sharedTests('NativeSecureStorage', NativeSecureStorage.new); - sharedTests('NativeLocalStorage', NativeLocalStorage.new); + sharedTests('NativeSecureStorage', NativeSecureStoragePlatform.new); + sharedTests('NativeLocalStorage', NativeLocalStoragePlatform.new); } diff --git a/packages/native/storage/lib/src/local/local_storage.android.dart b/packages/native/storage/lib/src/local/local_storage.android.dart index 38450eb4..0eb062d7 100644 --- a/packages/native/storage/lib/src/local/local_storage.android.dart +++ b/packages/native/storage/lib/src/local/local_storage.android.dart @@ -42,4 +42,8 @@ final class LocalStoragePlatformAndroid extends NativeLocalStoragePlatform { _storage.write(key.toJString(), value.toJString()); return value; } + + @override + List get allKeys => + _storage.getAllKeys().map((key) => key.toDartString()).toList(); } diff --git a/packages/native/storage/lib/src/local/local_storage.linux.dart b/packages/native/storage/lib/src/local/local_storage.linux.dart index ab0d9b9f..a44f5402 100644 --- a/packages/native/storage/lib/src/local/local_storage.linux.dart +++ b/packages/native/storage/lib/src/local/local_storage.linux.dart @@ -62,4 +62,10 @@ final class LocalStorageLinux extends NativeLocalStoragePlatform { _writeData(data); return value; } + + @override + List get allKeys => [ + for (final key in _readData().keys) + if (key.startsWith(_prefix)) key.substring(_prefix.length), + ]; } diff --git a/packages/native/storage/lib/src/local/local_storage.windows.dart b/packages/native/storage/lib/src/local/local_storage.windows.dart index f94ecc60..54f02829 100644 --- a/packages/native/storage/lib/src/local/local_storage.windows.dart +++ b/packages/native/storage/lib/src/local/local_storage.windows.dart @@ -1,72 +1,24 @@ import 'package:native_storage/src/local/local_storage_platform.vm.dart'; import 'package:native_storage/src/native/windows/windows.dart'; -import 'package:native_storage/src/util/functional.dart'; import 'package:win32_registry/win32_registry.dart'; -final class LocalStorageWindows extends NativeLocalStoragePlatform { +final class LocalStorageWindows extends NativeLocalStoragePlatform + with NativeStorageWindows { LocalStorageWindows({ String? namespace, super.scope, - }) : _namespace = namespace, + }) : namespaceOverride = namespace, super.base(); - final String? _namespace; - - @override - late final String namespace = lazy(() { - if (_namespace != null) { - return _namespace; - } - if (windows.applicationInfo case (:final companyName, :final productName)) { - return '$companyName\\$productName'; - } - return windows.applicationId; - }); - - late final _registry = lazy(() { - final hkcu = Registry.currentUser; - var key = hkcu - .createKey('SOFTWARE\\Classes\\Local Settings\\Software\\$namespace'); - if (scope case final scope?) { - for (final path in scope.split('/')) { - key = key.createKey(path); - } - } - return key; - }); - @override - String? delete(String key) { - final current = read(key); - if (current == null) { - return null; - } - _registry.deleteValue(key); - return current; - } + final String? namespaceOverride; @override - String? read(String key) => _registry.getValueAsString(key); + String? read(String key) => registry.getValueAsString(key); @override String write(String key, String value) { - _registry.createValue(RegistryValue(key, RegistryValueType.string, value)); + registry.createValue(RegistryValue(key, RegistryValueType.string, value)); return value; } - - @override - void clear() { - for (final value in List.of(_registry.values)) { - _registry.deleteValue(value.name); - } - for (final subkey in List.of(_registry.subkeyNames)) { - _registry.deleteKey(subkey, recursive: true); - } - } - - @override - void close() { - _registry.close(); - super.close(); - } } diff --git a/packages/native/storage/lib/src/local/local_storage_darwin.dart b/packages/native/storage/lib/src/local/local_storage_darwin.dart index 1e592259..3a71f219 100644 --- a/packages/native/storage/lib/src/local/local_storage_darwin.dart +++ b/packages/native/storage/lib/src/local/local_storage_darwin.dart @@ -50,17 +50,27 @@ final class LocalStoragePlatformDarwin extends NativeLocalStoragePlatform { @override void clear() { - final allValues = _userDefaults.persistentDomainForName_( + for (final key in allKeys) { + _userDefaults.removeObjectForKey_(darwin.nsString('$_prefix$key')); + } + } + + @override + List get allKeys { + final domain = _userDefaults.persistentDomainForName_( darwin.nsString(namespace), ); - if (allValues == null) { - return; + if (domain == null) { + return const []; } - for (var i = 0; i < allValues.allKeys.count; i++) { - final key = NSString.castFrom(allValues.allKeys.objectAtIndex_(i)); - if (scope == null || key.toString().startsWith(scope!)) { - _userDefaults.removeObjectForKey_(key); + final allKeys = []; + final domainKeys = domain.allKeys; + for (var i = 0; i < domainKeys.count; i++) { + final key = NSString.castFrom(domainKeys.objectAtIndex_(i)); + if (scope == null || key.toString().startsWith(_prefix)) { + allKeys.add(key.toString().substring(_prefix.length)); } } + return allKeys; } } diff --git a/packages/native/storage/lib/src/local/local_storage_platform.vm.dart b/packages/native/storage/lib/src/local/local_storage_platform.vm.dart index 97977c3f..e9f47040 100644 --- a/packages/native/storage/lib/src/local/local_storage_platform.vm.dart +++ b/packages/native/storage/lib/src/local/local_storage_platform.vm.dart @@ -6,9 +6,14 @@ import 'package:native_storage/src/local/local_storage.android.dart'; import 'package:native_storage/src/local/local_storage.linux.dart'; import 'package:native_storage/src/local/local_storage.windows.dart'; import 'package:native_storage/src/local/local_storage_darwin.dart'; +import 'package:native_storage/src/native_storage_extended.dart'; /// The VM implementation of [NativeLocalStorage]. -abstract base class NativeLocalStoragePlatform implements NativeLocalStorage { +abstract base class NativeLocalStoragePlatform + implements + NativeLocalStorage, + // ignore: invalid_use_of_visible_for_testing_member + NativeStorageExtended { factory NativeLocalStoragePlatform({ String? namespace, String? scope, diff --git a/packages/native/storage/lib/src/local/local_storage_platform.web.dart b/packages/native/storage/lib/src/local/local_storage_platform.web.dart index adda698b..2ed9a1c5 100644 --- a/packages/native/storage/lib/src/local/local_storage_platform.web.dart +++ b/packages/native/storage/lib/src/local/local_storage_platform.web.dart @@ -1,10 +1,15 @@ import 'package:native_storage/native_storage.dart'; import 'package:native_storage/src/isolated/isolated_storage_platform.unsupported.dart' as unsupported; +import 'package:native_storage/src/native_storage_extended.dart'; import 'package:web/web.dart' as web; /// The browser implementation of [NativeLocalStorage]. -final class NativeLocalStoragePlatform implements NativeLocalStorage { +final class NativeLocalStoragePlatform + implements + NativeLocalStorage, + // ignore: invalid_use_of_visible_for_testing_member + NativeStorageExtended { NativeLocalStoragePlatform({String? namespace, this.scope}) : namespace = namespace ?? web.window.location.hostname; @@ -20,10 +25,8 @@ final class NativeLocalStoragePlatform implements NativeLocalStorage { @override void clear() { - for (final key in _storage.keys) { - if (key.startsWith(_prefix)) { - _storage.removeItem(key); - } + for (final key in allKeys) { + _storage.removeItem('$_prefix$key'); } } @@ -45,6 +48,12 @@ final class NativeLocalStoragePlatform implements NativeLocalStorage { return value; } + @override + List get allKeys => [ + for (final key in _storage.keys) + if (key.startsWith(_prefix)) key.substring(_prefix.length), + ]; + @override void close() { _secure?.close(); diff --git a/packages/native/storage/lib/src/memory_storage.dart b/packages/native/storage/lib/src/memory_storage.dart index 15e8fc92..a0376921 100644 --- a/packages/native/storage/lib/src/memory_storage.dart +++ b/packages/native/storage/lib/src/memory_storage.dart @@ -1,9 +1,15 @@ import 'package:native_storage/src/isolated/isolated_storage.dart'; import 'package:native_storage/src/native_storage.dart'; +import 'package:native_storage/src/native_storage_extended.dart'; import 'package:native_storage/src/secure/secure_storage.dart'; /// An in-memory implementation of [NativeStorage] and [NativeSecureStorage]. -final class NativeMemoryStorage implements NativeStorage, NativeSecureStorage { +final class NativeMemoryStorage + implements + NativeStorage, + NativeSecureStorage, + // ignore: invalid_use_of_visible_for_testing_member + NativeStorageExtended { NativeMemoryStorage({ String? namespace, this.scope, @@ -38,6 +44,12 @@ final class NativeMemoryStorage implements NativeStorage, NativeSecureStorage { @override String write(String key, String value) => _storage['$_prefix$key'] = value; + @override + List get allKeys => [ + for (final key in _storage.keys) + if (key.startsWith(_prefix)) key.substring(_prefix.length), + ]; + @override void close() { clear(); diff --git a/packages/native/storage/lib/src/native/android/jni_bindings.ffi.dart b/packages/native/storage/lib/src/native/android/jni_bindings.ffi.dart index 579767a2..76c43de9 100644 --- a/packages/native/storage/lib/src/native/android/jni_bindings.ffi.dart +++ b/packages/native/storage/lib/src/native/android/jni_bindings.ffi.dart @@ -69,6 +69,17 @@ class NativeStorage extends jni.JObject { return _id_getSharedPreferences(this, const jni.JObjectType(), []); } + static final _id_getAllKeys = _class.instanceMethodId( + r"getAllKeys", + r"()Ljava/util/List;", + ); + + /// from: public final java.util.List getAllKeys() + /// The returned object must be released after use, by calling the [release] method. + jni.JList getAllKeys() { + return _id_getAllKeys(this, const jni.JListType(jni.JStringType()), []); + } + static final _id_write = _class.instanceMethodId( r"write", r"(Ljava/lang/String;Ljava/lang/String;)V", diff --git a/packages/native/storage/lib/src/native/windows/windows.dart b/packages/native/storage/lib/src/native/windows/windows.dart index b7112eb5..90bbe8ea 100644 --- a/packages/native/storage/lib/src/native/windows/windows.dart +++ b/packages/native/storage/lib/src/native/windows/windows.dart @@ -2,10 +2,14 @@ import 'dart:ffi'; import 'dart:io'; import 'package:ffi/ffi.dart'; +import 'package:meta/meta.dart'; +import 'package:native_storage/native_storage.dart'; import 'package:native_storage/src/native_storage_exception.dart'; +import 'package:native_storage/src/native_storage_extended.dart'; import 'package:native_storage/src/util/functional.dart'; import 'package:path/path.dart' as p; import 'package:win32/win32.dart'; +import 'package:win32_registry/win32_registry.dart'; import 'package:windows_applicationmodel/windows_applicationmodel.dart'; final windows = WindowsCommon._(); @@ -130,3 +134,89 @@ final class WindowsCommon { ); }); } + +// ignore: invalid_use_of_visible_for_testing_member +mixin NativeStorageWindows on NativeStorage implements NativeStorageExtended { + @protected + abstract final String? namespaceOverride; + + @override + late final String namespace = lazy(() { + if (namespaceOverride case final namespaceOverride?) { + return namespaceOverride; + } + if (windows.applicationInfo case (:final companyName, :final productName)) { + return '$companyName\\$productName'; + } + return windows.applicationId; + }); + + @protected + late final registry = lazy(() { + final hkcu = Registry.currentUser; + var key = hkcu + .createKey('SOFTWARE\\Classes\\Local Settings\\Software\\$namespace'); + if (scope case final scope?) { + for (final path in scope.split('/')) { + key = key.createKey(path); + } + } + return key; + }); + + @override + String? delete(String key) { + final current = read(key); + if (current == null) { + return null; + } + registry.deleteValue(key); + return current; + } + + @override + List get allKeys { + final allKeys = []; + try { + for (final value in List.of(registry.values)) { + allKeys.add(value.name); + } + } on WindowsException catch (e) { + if (e.toString().contains('marked for deletion')) { + // OK. Will throw if recently cleared/deleted. + return const []; + } + rethrow; + } + for (final subkey in List.of(registry.subkeyNames)) { + try { + allKeys.addAll( + registry.createKey(subkey).values.map((v) => '$subkey/${v.name}'), + ); + } on WindowsException catch (e) { + if (e.toString().contains('marked for deletion')) { + // OK. Will throw if recently cleared/deleted. + continue; + } + rethrow; + } + } + return allKeys; + } + + @override + void clear() { + for (final value in List.of(registry.values)) { + registry.deleteValue(value.name); + } + for (final subkey in List.of(registry.subkeyNames)) { + registry.deleteKey(subkey, recursive: true); + } + } + + @override + void close() { + registry.close(); + super.close(); + } +} diff --git a/packages/native/storage/lib/src/native_storage_extended.dart b/packages/native/storage/lib/src/native_storage_extended.dart new file mode 100644 index 00000000..a9486a76 --- /dev/null +++ b/packages/native/storage/lib/src/native_storage_extended.dart @@ -0,0 +1,17 @@ +@visibleForTesting +library; + +import 'package:meta/meta.dart'; +import 'package:native_storage/native_storage.dart'; + +typedef NativeStorageExtendedFactory = NativeStorageExtended Function({ + String? namespace, + String? scope, +}); + +/// Extended interface for [NativeStorage], used in tests. +@visibleForTesting +abstract interface class NativeStorageExtended implements NativeStorage { + @visibleForTesting + List get allKeys; +} diff --git a/packages/native/storage/lib/src/secure/secure_storage.android.dart b/packages/native/storage/lib/src/secure/secure_storage.android.dart index 35940d28..62a0f67d 100644 --- a/packages/native/storage/lib/src/secure/secure_storage.android.dart +++ b/packages/native/storage/lib/src/secure/secure_storage.android.dart @@ -42,4 +42,8 @@ final class SecureStorageAndroid extends NativeSecureStoragePlatform { _storage.write(key.toJString(), value.toJString()); return value; } + + @override + List get allKeys => + _storage.getAllKeys().map((key) => key.toDartString()).toList(); } diff --git a/packages/native/storage/lib/src/secure/secure_storage.darwin.dart b/packages/native/storage/lib/src/secure/secure_storage.darwin.dart index dd6fd737..b6de9117 100644 --- a/packages/native/storage/lib/src/secure/secure_storage.darwin.dart +++ b/packages/native/storage/lib/src/secure/secure_storage.darwin.dart @@ -141,42 +141,52 @@ final class SecureStorageDarwin extends NativeSecureStoragePlatform { } void _clear({required Arena arena}) { + for (final key in _allKeys(arena: arena)) { + // Use the item's primary key to avoid a bad lookup. + final primaryKey = { + kSecClass: kSecClassGenericPassword, + kSecAttrService: namespace.toCFString(arena), + kSecAttrAccount: _scoped(key).toCFString(arena), + }.toCFDictionary(arena); + _check(() => SecItemDelete(primaryKey), key: key); + } + } + + @override + List get allKeys => using((arena) => _allKeys(arena: arena)); + + List _allKeys({required Arena arena}) { final query = { ..._baseQuery(arena), kSecReturnAttributes: kCFBooleanTrue, - // Required when `useDataProtection` is disabled. - if (!darwin.useDataProtection) kSecMatchLimit: kSecMatchLimitAll, + kSecMatchLimit: kSecMatchLimitAll, }; - final result = arena(); try { - // Find all keys for the namespace. _check( - () => SecItemCopyMatching(query.toCFDictionary(arena), result.cast()), + () => SecItemCopyMatching( + query.toCFDictionary(arena), + result.cast(), + ), ); } on SecureStorageItemNotFoundException { // OK. Keychain will throw this error if there are no items. - return; + return const []; } - final items = result.value; assert(items != nullptr, 'items should not be null'); + final allKeys = []; for (var i = 0; i < CFArrayGetCount(items); i++) { final item = CFArrayGetValueAtIndex(items, i).cast(); - final itemKey = CFDictionaryGetValue( + final key = CFDictionaryGetValue( item, kSecAttrAccount.cast(), ).cast().toDartString()!; - if (scope == null || itemKey.startsWith(_prefix)) { - // Use the item's primary key to avoid a bad lookup. - final primaryKey = { - kSecClass: kSecClassGenericPassword, - kSecAttrService: namespace.toCFString(arena), - kSecAttrAccount: itemKey.toCFString(arena), - }.toCFDictionary(arena); - _check(() => SecItemDelete(primaryKey), key: itemKey); + if (scope == null || key.startsWith(_prefix)) { + allKeys.add(key.substring(_prefix.length)); } } + return allKeys; } void _check( diff --git a/packages/native/storage/lib/src/secure/secure_storage.linux.dart b/packages/native/storage/lib/src/secure/secure_storage.linux.dart index 1cca127d..5d25e6d0 100644 --- a/packages/native/storage/lib/src/secure/secure_storage.linux.dart +++ b/packages/native/storage/lib/src/secure/secure_storage.linux.dart @@ -169,6 +169,37 @@ final class SecureStorageLinux extends NativeSecureStoragePlatform { return value; } + @override + List get allKeys => using((arena) { + final schema = _schema(arena); + final attributes = _attributes(arena: arena); + final secrets = _check( + (err) => linux.libSecret.secret_password_searchv_sync( + schema, + attributes, + SecretSearchFlags.SECRET_SEARCH_ALL, + nullptr, + err, + ), + arena: arena, + ); + if (secrets == nullptr) { + return const []; + } + final count = linux.glib.g_list_length(secrets); + final allKeys = []; + for (var i = 0; i < count; i++) { + final secret = + linux.glib.g_list_nth_data(secrets, i).cast(); + final label = + linux.libSecret.secret_item_get_label(secret).toDartString(); + if (scope == null || label.startsWith(_prefix)) { + allKeys.add(label.substring(_prefix.length)); + } + } + return allKeys; + }); + R _check( R Function(Pointer> err) action, { required Arena arena, diff --git a/packages/native/storage/lib/src/secure/secure_storage.windows.dart b/packages/native/storage/lib/src/secure/secure_storage.windows.dart index 0f3976d6..0293b1aa 100644 --- a/packages/native/storage/lib/src/secure/secure_storage.windows.dart +++ b/packages/native/storage/lib/src/secure/secure_storage.windows.dart @@ -1,5 +1,3 @@ -// ignore_for_file: non_constant_identifier_names, constant_identifier_names - import 'dart:convert'; import 'dart:ffi'; import 'dart:typed_data'; @@ -8,41 +6,27 @@ import 'package:ffi/ffi.dart'; import 'package:native_storage/native_storage.dart'; import 'package:native_storage/src/native/windows/windows.dart'; import 'package:native_storage/src/secure/secure_storage_platform.vm.dart'; -import 'package:native_storage/src/util/functional.dart'; import 'package:win32/win32.dart'; import 'package:win32_registry/win32_registry.dart'; -final class SecureStorageWindows extends NativeSecureStoragePlatform { +final class SecureStorageWindows extends NativeSecureStoragePlatform + with NativeStorageWindows { SecureStorageWindows({ String? namespace, super.scope, - }) : _namespace = namespace, + }) : namespaceOverride = namespace, super.base(); - final String? _namespace; - @override - String get namespace => _namespace ?? windows.applicationId; + final String? namespaceOverride; WindowsException _windowsException(int hr) => WindowsException(HRESULT_FROM_WIN32(hr)); - late final _registry = lazy(() { - final hkcu = Registry.currentUser; - var key = hkcu - .createKey('SOFTWARE\\Classes\\Local Settings\\Software\\$namespace'); - if (scope case final scope?) { - for (final path in scope.split('/')) { - key = key.createKey(path); - } - } - return key; - }); - @override String? read(String key) { return using((arena) { - final value = _registry.getValueAsString(key); + final value = registry.getValueAsString(key); if (value == null) { return null; } @@ -54,33 +38,13 @@ final class SecureStorageWindows extends NativeSecureStoragePlatform { String write(String key, String value) { return using((arena) { final encrypted = _encrypt(value, arena); - _registry.createValue( + registry.createValue( RegistryValue(key, RegistryValueType.string, encrypted), ); return value; }); } - @override - String? delete(String key) { - final current = read(key); - if (current == null) { - return null; - } - _registry.deleteValue(key); - return current; - } - - @override - void clear() { - for (final value in List.of(_registry.values)) { - _registry.deleteValue(value.name); - } - for (final subkey in List.of(_registry.subkeyNames)) { - _registry.deleteKey(subkey, recursive: true); - } - } - /// A wrapper around [CryptProtectData] for encrypting [Uint8List]. String _encrypt(String value, Arena arena) { final bytes = utf8.encode(value); diff --git a/packages/native/storage/lib/src/secure/secure_storage_platform.vm.dart b/packages/native/storage/lib/src/secure/secure_storage_platform.vm.dart index b0c00cce..00a1dc4b 100644 --- a/packages/native/storage/lib/src/secure/secure_storage_platform.vm.dart +++ b/packages/native/storage/lib/src/secure/secure_storage_platform.vm.dart @@ -2,12 +2,17 @@ import 'dart:io'; import 'package:meta/meta.dart'; import 'package:native_storage/native_storage.dart'; +import 'package:native_storage/src/native_storage_extended.dart'; import 'package:native_storage/src/secure/secure_storage.android.dart'; import 'package:native_storage/src/secure/secure_storage.darwin.dart'; import 'package:native_storage/src/secure/secure_storage.linux.dart'; import 'package:native_storage/src/secure/secure_storage.windows.dart'; -abstract base class NativeSecureStoragePlatform implements NativeSecureStorage { +abstract base class NativeSecureStoragePlatform + implements + NativeSecureStorage, + // ignore: invalid_use_of_visible_for_testing_member + NativeStorageExtended { factory NativeSecureStoragePlatform({ String? namespace, String? scope, diff --git a/packages/native/storage/test/native_storage_test.dart b/packages/native/storage/test/native_storage_test.dart index 846c239c..71fef440 100644 --- a/packages/native/storage/test/native_storage_test.dart +++ b/packages/native/storage/test/native_storage_test.dart @@ -1,9 +1,13 @@ import 'package:native_storage/native_storage.dart'; +import 'package:native_storage/src/local/local_storage_platform.vm.dart' + if (dart.library.js_interop) 'package:native_storage/src/local/local_storage_platform.web.dart'; +import 'package:native_storage/src/secure/secure_storage_platform.vm.dart' + if (dart.library.js_interop) 'package:native_storage/src/secure/secure_storage_platform.web.dart'; import '../example/integration_test/storage_shared.dart'; void main() { sharedTests('NativeMemoryStorage', NativeMemoryStorage.new); - sharedTests('NativeSecureStorage', NativeSecureStorage.new); - sharedTests('NativeLocalStorage', NativeLocalStorage.new); + sharedTests('NativeSecureStorage', NativeSecureStoragePlatform.new); + sharedTests('NativeLocalStorage', NativeLocalStoragePlatform.new); }