-
-
Notifications
You must be signed in to change notification settings - Fork 14
commit c8a6ff0
Commit: c8a6ff0ba90e3d6c0f2181d12484aeedf8ed8709
Author: Claude
Date: 2026年03月23日
The pub.dev flutter_secure_storage_windows plugin requires ATL (atlstr.h) which isn't installed by default with Visual Studio Build Tools, causing C1083 compile errors.
Solution: override flutter_secure_storage_windows with a local package (packages/flutter_secure_storage_windows/) that implements the same FlutterSecureStoragePlatform interface using the win32 Dart FFI package to call Windows Credential Manager APIs directly (CredWrite, CredRead, CredDelete, CredEnumerate). No C++ code, no ATL — pure Dart.
Other platforms (Android, iOS, macOS, Linux) are unaffected and continue to use the upstream flutter_secure_storage implementation.
https://claude.ai/code/session_015C9QnzTMDXurBU1chuhDVg
Why: Fixes a bug or regression in the existing codebase.
.../lib/flutter_secure_storage_windows.dart | 178 +++++++++++++++++++++
.../flutter_secure_storage_windows/pubspec.yaml | 25 +++
pubspec.yaml | 8 +
3 files changed, 211 insertions(+)
packages/flutter_secure_storage_windows/lib/flutter_secure_storage_windows.dartpackages/flutter_secure_storage_windows/pubspec.yamlpubspec.yaml
diff --git a/packages/flutter_secure_storage_windows/lib/flutter_secure_storage_windows.dart b/packages/flutter_secure_storage_windows/lib/flutter_secure_storage_windows.dart new file mode 100644 index 0000000..b2e67fc --- /dev/null +++ b/packages/flutter_secure_storage_windows/lib/flutter_secure_storage_windows.dart @@ -0,0 +1,178 @@ +import 'dart:ffi'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; +import 'package:win32/win32.dart'; +import 'package:flutter_secure_storage_platform_interface/flutter_secure_storage_platform_interface.dart'; + +/// Windows implementation of [FlutterSecureStoragePlatform]. +/// +/// Stores secrets in the Windows Credential Manager via pure Dart FFI +/// (win32 package). No C++ plugin, no ATL dependency. +class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform { + // Namespace all credentials so they don't clash with other apps. + static const String _prefix = 'freegosy/'; + + static void registerWith() { + FlutterSecureStoragePlatform.instance = FlutterSecureStorageWindows(); + } + + String _fullKey(String key) => '$_prefix$key'; + + // --------------------------------------------------------------------------- + // Read + // --------------------------------------------------------------------------- + + @override + Future<String?> read({ + required String key, + required Map<String, String> options, + }) async { + final arena = Arena(); + try { + final targetName = _fullKey(key).toNativeUtf16(allocator: arena); + final ppCred = arena<Pointer<CREDENTIAL>>(); + + final ok = CredRead(targetName, CRED_TYPE_GENERIC, 0, ppCred); + if (ok == 0) return null; // ERROR_NOT_FOUND or similar + + final pCred = ppCred.value; + try { + final size = pCred.ref.CredentialBlobSize; + if (size == 0) return ''; + final bytes = Uint8List.fromList( + pCred.ref.CredentialBlob.asTypedList(size), + ); + return utf8.decode(bytes); + } finally { + CredFree(pCred.cast()); + } + } finally { + arena.releaseAll(); + } + } + + @override + Future<bool> containsKey({ + required String key, + required Map<String, String> options, + }) async { + return await read(key: key, options: options) != null; + } + + // --------------------------------------------------------------------------- + // Write + // --------------------------------------------------------------------------- + + @override + Future<void> write({ + required String key, + required String value, + required Map<String, String> options, + }) async { + final bytes = utf8.encode(value); + final arena = Arena(); + try { + final targetName = _fullKey(key).toNativeUtf16(allocator: arena); + + Pointer<Uint8> blob = nullptr; + if (bytes.isNotEmpty) { + blob = arena<Uint8>(bytes.length); + blob.asTypedList(bytes.length).setAll(0, bytes); + } + + final pCred = arena<CREDENTIAL>(); + // calloc (used by Arena) zero-initialises, so only non-zero fields need + // to be set explicitly. + pCred.ref.Type = CRED_TYPE_GENERIC; + pCred.ref.TargetName = targetName; + pCred.ref.CredentialBlobSize = bytes.length; + pCred.ref.CredentialBlob = blob; + pCred.ref.Persist = CRED_PERSIST_LOCAL_MACHINE; + + final ok = CredWrite(pCred, 0); + if (ok == 0) { + throw Exception('CredWrite failed (Win32 error ${GetLastError()})'); + } + } finally { + arena.releaseAll(); + } + } + + // --------------------------------------------------------------------------- + // Delete + // --------------------------------------------------------------------------- + + @override + Future<void> delete({ + required String key, + required Map<String, String> options, + }) async { + final arena = Arena(); + try { + final targetName = _fullKey(key).toNativeUtf16(allocator: arena); + // Ignore return value — deleting a non-existent key is not an error. + CredDelete(targetName, CRED_TYPE_GENERIC, 0); + } finally { + arena.releaseAll(); + } + } + + // --------------------------------------------------------------------------- + // Bulk operations + // --------------------------------------------------------------------------- + + @override + Future<Map<String, String>> readAll({ + required Map<String, String> options, + }) async { + final arena = Arena(); + try { + // CredEnumerate filter: prefix followed by '*' matches all entries whose + // TargetName starts with the prefix. + final filter = '$_prefix*'.toNativeUtf16(allocator: arena); + final pCount = arena<Uint32>(); + final ppCreds = arena<Pointer<Pointer<CREDENTIAL>>>(); + + final ok = CredEnumerate(filter, 0, pCount, ppCreds); + if (ok == 0) return {}; // No matching credentials. + + final count = pCount.value; + final credArray = ppCreds.value; // Pointer<Pointer<CREDENTIAL>> + final result = <String, String>{}; + + try { + for (int i = 0; i < count; i++) { + final pCred = credArray[i]; + final targetName = pCred.ref.TargetName.toDartString(); + if (!targetName.startsWith(_prefix)) continue; + + final k = targetName.substring(_prefix.length); + final size = pCred.ref.CredentialBlobSize; + result[k] = size > 0 + ? utf8.decode( + Uint8List.fromList(pCred.ref.CredentialBlob.asTypedList(size)), + ) + : ''; + } + } finally { + CredFree(credArray.cast()); + } + + return result; + } finally { + arena.releaseAll(); + } + } + + @override + Future<void> deleteAll({ + required Map<String, String> options, + }) async { + final all = await readAll(options: options); + for (final key in all.keys) { + await delete(key: key, options: options); + } + } +} diff --git a/packages/flutter_secure_storage_windows/pubspec.yaml b/packages/flutter_secure_storage_windows/pubspec.yaml new file mode 100644 index 0000000..18ac5ed --- /dev/null +++ b/packages/flutter_secure_storage_windows/pubspec.yaml @@ -0,0 +1,25 @@ +name: flutter_secure_storage_windows +description: > + Windows implementation of flutter_secure_storage using Windows Credential + Manager via pure Dart FFI. No C++ / ATL required. +version: 1.0.0 +publish_to: none + +environment: + sdk: ^3.0.0 + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_secure_storage_platform_interface: ^1.0.2 + win32: ^5.5.4 + ffi: ^2.1.0 + +flutter: + plugin: + implements: flutter_secure_storage + platforms: + windows: + dartPluginClass: FlutterSecureStorageWindows + fileName: flutter_secure_storage_windows.dart diff --git a/pubspec.yaml b/pubspec.yaml index e44beb4..e2c3f90 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,14 @@ dependencies: file_picker: ^8.0.0+1 path: ^1.9.0 flutter_secure_storage: ^9.2.2 + win32: ^5.5.4 + ffi: ^2.1.0 + +dependency_overrides: + # Replace the pub.dev flutter_secure_storage_windows (which requires ATL) + # with a local pure-Dart FFI implementation using the win32 package. + flutter_secure_storage_windows: + path: packages/flutter_secure_storage_windows dev_dependencies: flutter_test: