diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d290213..b676128d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,12 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - third_party/**/example/** + - third_party/**/test/** + - third_party/**/analysis_options.yaml + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` diff --git a/pubspec.lock b/pubspec.lock index b5431110..e2d1cb28 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -265,12 +265,11 @@ packages: source: hosted version: "1.2.1" flutter_secure_storage_windows: - dependency: transitive + dependency: "direct overridden" description: - name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 - url: "https://pub.dev" - source: hosted + path: "third_party/flutter_secure_storage_windows" + relative: true + source: path version: "3.1.2" flutter_test: dependency: "direct dev" diff --git a/pubspec.yaml b/pubspec.yaml index df24eb7d..bd67fd27 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,10 @@ dependencies: web_socket_channel: ^3.0.3 yaml: ^3.1.3 +dependency_overrides: + flutter_secure_storage_windows: + path: third_party/flutter_secure_storage_windows + dev_dependencies: flutter_test: sdk: flutter diff --git a/third_party/flutter_secure_storage_windows/CHANGELOG.md b/third_party/flutter_secure_storage_windows/CHANGELOG.md new file mode 100644 index 00000000..399347a6 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/CHANGELOG.md @@ -0,0 +1,43 @@ +## 3.1.2 +Reverts onCupertinoProtectedDataAvailabilityChanged and isCupertinoProtectedDataAvailable. + +## 3.1.1 +Updated flutter_secure_storage_platform_interface to latest version. + +## 3.1.0 +Fixed CompanyName and CompanyProduct on Windows are ignored when the lang-charset in the Runner.rc file is not 040904e4 + +## 3.0.0 +- Migrated to win32 package replacing C. +- Changed PathNotFoundException to FileSystemException to be backwards compatible with Flutter SDK 2.12.0 +- Applied lint suggestions + +## 2.1.1 +Revert changes made in version 2.1.0 due to breaking changes. +These changes will be republished under a new major version number 3.0.0. + +## 2.1.0 +- Changed PathNotFoundException to FileSystemException to be backwards compatible with Flutter SDK 2.12.0 +- Applied lint suggestions + +## 2.0.0 +Write encrypted data to files instead of the windows credential system. + +## 1.1.3 +Updated flutter_secure_storage_platform_interface to latest version. + +## 1.1.2 +- Silently ignore errors when deleting keys that don't exist + +## 1.1.1 +- Fix application crash when key doesn't exists. + +## 1.1.0 +Features +- Add readAll, deleteAll and containsKey functions. + +Bugfixes +- Fix implementation of delete operation to allow null value. + +## 1.0.0 +- Initial Windows implementation diff --git a/third_party/flutter_secure_storage_windows/LICENSE b/third_party/flutter_secure_storage_windows/LICENSE new file mode 100644 index 00000000..defe7c85 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright 2017 German Saprykin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/third_party/flutter_secure_storage_windows/README.md b/third_party/flutter_secure_storage_windows/README.md new file mode 100644 index 00000000..de6482a6 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/README.md @@ -0,0 +1,12 @@ +# flutter_secure_storage_windows + +The windows implementation of [`flutter_secure_storage`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `flutter_secure_storage` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/flutter_secure_storage +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin + diff --git a/third_party/flutter_secure_storage_windows/analysis_options.yaml b/third_party/flutter_secure_storage_windows/analysis_options.yaml new file mode 100644 index 00000000..dbda697a --- /dev/null +++ b/third_party/flutter_secure_storage_windows/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lint/package.yaml \ No newline at end of file diff --git a/third_party/flutter_secure_storage_windows/example/README.md b/third_party/flutter_secure_storage_windows/example/README.md new file mode 100644 index 00000000..b37f4c62 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/README.md @@ -0,0 +1,16 @@ +# flutter_secure_storage_windows_example + +Demonstrates how to use the flutter_secure_storage_windows plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/third_party/flutter_secure_storage_windows/example/analysis_options.yaml b/third_party/flutter_secure_storage_windows/example/analysis_options.yaml new file mode 100644 index 00000000..61b6c4de --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/third_party/flutter_secure_storage_windows/example/integration_test/app_test.dart b/third_party/flutter_secure_storage_windows/example/integration_test/app_test.dart new file mode 100644 index 00000000..6e961389 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/integration_test/app_test.dart @@ -0,0 +1,1117 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:flutter_secure_storage_windows_example/main.dart' as app; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +// For legacy behavior investigation, change bellow line. +// Note that "Special charactors handling" cases can cause app crash +// or file creation outside of app support directory. + +const useMethodChannelOnly = false; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + Future cleanUpFiles() async { + // Clean up current & legacy files. + final directory = await getApplicationSupportDirectory(); + if (directory.existsSync()) { + directory + .listSync(recursive: false, followLinks: false) + .whereType() + .where((f) => + path.basename(f.path) == 'flutter_secure_storage.dat' || + f.path.endsWith('.secure')) + .forEach((f) => f.deleteSync()); + } + + // Check parent directory, too. + if (directory.parent.existsSync()) { + directory.parent + .listSync(recursive: false, followLinks: false) + .whereType() + .where((f) => f.path.endsWith('.secure')) + .forEach((f) => f.deleteSync()); + } + } + + setUpAll(() async { + await cleanUpFiles(); + }); + + tearDown(() async { + await cleanUpFiles(); + }); + + app.MyAppState getState( + WidgetTester tester, { + required bool useBackwardCompatibility, + }) { + final state = tester.state(find.byType(app.MyApp)); + + state.useBackwardCompatibilityKey.currentState!.value = + useBackwardCompatibility; + state.useMethodChannelOnlyKey.currentState!.value = useMethodChannelOnly; + + return state; + } + + Future checkSuccess( + WidgetTester tester, + app.MyAppState state, { + String? expectedDetail, + }) async { + printOnFailure('checkSuccess'); + await tester.pumpAndSettle(); + + expect( + state.resultSummaryFieldController.text, + 'SUCCESS', + reason: 'Failed: ${state.resultDetailFieldController}', + ); + + if (expectedDetail != null) { + expect(state.resultDetailFieldController.text, expectedDetail); + } + + return state.resultDetailFieldController.text; + } + + Future doTestSuite( + WidgetTester tester, { + required String key1, + String? key2, + String? writingValue1, + String? writingValue2, + bool useBackwardCompatibility = true, + }) async { + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: useBackwardCompatibility, + ); + + // write 1 + state.keyFieldController.text = key1; + state.valueFieldController.text = writingValue1 ?? ''; + + await tester.tap(find.text('Write')); + final value1 = await checkSuccess(tester, state); + + late final String value2; + if (key2 != null) { + // write 2 + state.keyFieldController.text = key2; + state.valueFieldController.text = writingValue2 ?? ''; + await tester.tap(find.text('Write')); + value2 = await checkSuccess(tester, state); + } + + state.valueFieldController.text = ''; + + // read for write 1 + state.keyFieldController.text = key1; + await tester.tap(find.text('Read')); + await checkSuccess(tester, state, expectedDetail: value1); + + if (key2 != null) { + // read for write 2 + state.keyFieldController.text = key2; + await tester.tap(find.text('Read')); + await checkSuccess(tester, state, expectedDetail: value2); + } + + // containsKey for write 1 + state.keyFieldController.text = key1; + await tester.tap(find.text('ContainsKey')); + await checkSuccess(tester, state, expectedDetail: 'true'); + + if (key2 != null) { + // containsKey for write 2 + state.keyFieldController.text = key2; + await tester.tap(find.text('ContainsKey')); + await checkSuccess(tester, state, expectedDetail: 'true'); + } + + // readAll + await tester.tap(find.text('ReadAll')); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: + (key2 != null ? {key1: value1, key2: value2} : {key1: value1}) + .toString(), + ); + + // delete for write 1 + state.keyFieldController.text = key1; + await tester.tap(find.text('Delete')); + await checkSuccess(tester, state); + + // read for delete + await tester.tap(find.text('Read')); + await checkSuccess(tester, state, expectedDetail: ''); + + // containsKey for delete + await tester.tap(find.text('ContainsKey')); + await checkSuccess(tester, state, expectedDetail: 'false'); + + // readAll for delete + await tester.tap(find.text('ReadAll')); + await checkSuccess( + tester, state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: (key2 == null ? {} : {key2: value2}).toString(), + ); + + if (key2 == null) { + // re-write for deleteAll + state.keyFieldController.text = key1; + state.valueFieldController.text = writingValue1 ?? ''; + await tester.tap(find.text('Write')); + await checkSuccess(tester, state); + // clear + state.valueFieldController.text = ''; + } + + // deleteAll + await tester.tap(find.text('DeleteAll')); + await checkSuccess(tester, state); + + // read for delete 2 + state.keyFieldController.text = key2 ?? key1; + await tester.tap(find.text('Read')); + await checkSuccess(tester, state, expectedDetail: ''); + + // containsKey for delete 2 + await tester.tap(find.text('ContainsKey')); + await checkSuccess(tester, state, expectedDetail: 'false'); + + // readAll for delete + await tester.tap(find.text('ReadAll')); + await checkSuccess( + tester, state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {}.toString(), + ); + } + + group( + 'Basic test', + () { + testWidgets('Smoke test', (tester) async { + await doTestSuite(tester, key1: 'key1', key2: 'key2'); + }); + }, + skip: kIsWeb || !Platform.isWindows + ? 'These tests only work on Windows' + : null, + ); + + group( + 'Backwards compatibilty cases', + () { + Future checkMigration( + WidgetTester tester, + app.MyAppState state, + ) async { + printOnFailure('checkMigration'); + await tester.tap(find.text('LegacyReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {}.toString(), + ); + } + + testWidgets('readAll - empty, empty', (tester) async { + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + await tester.tap(find.text('ReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {}.toString(), + ); + await checkMigration(tester, state); + }); + + testWidgets('readAll - 1 entry, 1 entry, different keys', (tester) async { + const key1 = 'key1'; + const key2 = 'key2'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key1; + await tester.tap(find.text('Write')); + final value1 = await checkSuccess(tester, state); + + state.keyFieldController.text = key2; + await tester.tap(find.text('LegacyWrite')); + final value2 = await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('ReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {key1: value1, key2: value2}.toString(), + ); + await checkMigration(tester, state); + }); + + testWidgets('readAll - 1 entry, 1 entry, same keys', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('Write')); + final value = await checkSuccess(tester, state); + + await tester.tap(find.text('LegacyWrite')); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('ReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {key: value}.toString(), + ); + await checkMigration(tester, state); + }); + + testWidgets('readAll - empty, 1entry', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('LegacyWrite')); + final value = await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('ReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {key: value}.toString(), + ); + await checkMigration(tester, state); + }); + + testWidgets('readAll - 1entry, empty', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('Write')); + final value = await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('ReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {key: value}.toString(), + ); + await checkMigration(tester, state); + }); + + testWidgets('readAll - 2 entries, 2 entries, same keys and diffrent keys', + (tester) async { + const key1 = 'key1'; + const key2 = 'key2'; + const key3 = 'key3'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key1; + await tester.tap(find.text('Write')); + final value1 = await checkSuccess(tester, state); + + state.keyFieldController.text = key2; + await tester.tap(find.text('Write')); + final value2 = await checkSuccess(tester, state); + + state.keyFieldController.text = key3; + await tester.tap(find.text('LegacyWrite')); + final value3 = await checkSuccess(tester, state); + + state.keyFieldController.text = key1; + await tester.tap(find.text('LegacyWrite')); + final value4 = await checkSuccess(tester, state); + + assert(value1 != value4); + + // Do test + await tester.tap(find.text('ReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {key1: value1, key2: value2, key3: value3}.toString(), + ); + await checkMigration(tester, state); + }); + + testWidgets('read - exists, exists', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('Write')); + final value = await checkSuccess(tester, state); + + await tester.tap(find.text('LegacyWrite')); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('Read')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state, expectedDetail: value); + await checkMigration(tester, state); + }); + + testWidgets('read - does not exist, exists', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('LegacyWrite')); + final value = await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('Read')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state, expectedDetail: value); + await checkMigration(tester, state); + }); + + testWidgets('read - exists, does not exist', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('Write')); + final value = await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('Read')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state, expectedDetail: value); + await checkMigration(tester, state); + }); + + testWidgets('read - does not exist, does not exist', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Do test + state.keyFieldController.text = key; + await tester.tap(find.text('Read')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state, expectedDetail: ''); + await checkMigration(tester, state); + }); + + testWidgets('containsKey - exists, exists', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('Write')); + await checkSuccess(tester, state); + + await tester.tap(find.text('LegacyWrite')); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('ContainsKey')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state, expectedDetail: 'true'); + // containsKey does not execute auto-migration + }); + + testWidgets('containsKey - does not exist, exists', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('LegacyWrite')); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('ContainsKey')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state, expectedDetail: 'true'); + // containsKey does not execute auto-migration + }); + + testWidgets('containsKey - exists, does not exist', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('Write')); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('ContainsKey')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state, expectedDetail: 'true'); + // containsKey does not execute auto-migration + }); + + testWidgets('containsKey - does not exist, does not exist', + (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Do test + state.keyFieldController.text = key; + await tester.tap(find.text('ContainsKey')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state, expectedDetail: 'false'); + // containsKey does not execute auto-migration + }); + + testWidgets('write - new', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Do test + state.keyFieldController.text = key; + await tester.tap(find.text('Write')); + await tester.pumpAndSettle(); + final writtenValue = await checkSuccess(tester, state); + await checkMigration(tester, state); + + await tester.tap(find.text('Read')); + await tester.pumpAndSettle(); + final readValue = await checkSuccess(tester, state); + expect(readValue, writtenValue); + }); + + testWidgets('write - overwrite', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Do test + state.keyFieldController.text = key; + await tester.tap(find.text('Write')); + await tester.pumpAndSettle(); + final writtenValue1 = await checkSuccess(tester, state); + await checkMigration(tester, state); + + await tester.tap(find.text('Write')); + await tester.pumpAndSettle(); + final writtenValue2 = await checkSuccess(tester, state); + await checkMigration(tester, state); + + assert(writtenValue1 != writtenValue2); + + await tester.tap(find.text('Read')); + await tester.pumpAndSettle(); + final readValue = await checkSuccess(tester, state); + expect(readValue, writtenValue2); + }); + + testWidgets('write - legacy value exists', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('LegacyWrite')); + await tester.pumpAndSettle(); + final legacyValue = await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('Write')); + await tester.pumpAndSettle(); + final writtenValue = await checkSuccess(tester, state); + await checkMigration(tester, state); + + assert(writtenValue != legacyValue); + + await tester.tap(find.text('Read')); + await tester.pumpAndSettle(); + final readValue = await checkSuccess(tester, state); + expect(readValue, writtenValue); + }); + + testWidgets('delete - exists, exists', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('Write')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state); + await tester.tap(find.text('LegacyWrite')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state); + await checkMigration(tester, state); + + await tester.tap(find.text('ContainsKey')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state, expectedDetail: 'false'); + }); + + testWidgets('delete - exists, does not exist', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('Write')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state); + await checkMigration(tester, state); + + await tester.tap(find.text('ContainsKey')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state, expectedDetail: 'false'); + }); + + testWidgets('delete - does not exist, exists', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('LegacyWrite')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state); + await checkMigration(tester, state); + + await tester.tap(find.text('ContainsKey')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state, expectedDetail: 'false'); + }); + + testWidgets('delete - does not exist, does not exist', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Do test + state.keyFieldController.text = key; + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state); + await checkMigration(tester, state); + + await tester.tap(find.text('ContainsKey')); + await tester.pumpAndSettle(); + await checkSuccess(tester, state, expectedDetail: 'false'); + }); + + testWidgets('deleteAll - empty, empty', (tester) async { + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + await tester.tap(find.text('DeleteAll')); + await checkSuccess(tester, state); + await checkMigration(tester, state); + + await tester.tap(find.text('ReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {}.toString(), + ); + }); + + testWidgets('deleteAll - 1 entry, 1 entry, different keys', + (tester) async { + const key1 = 'key1'; + const key2 = 'key2'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key1; + await tester.tap(find.text('Write')); + await checkSuccess(tester, state); + + state.keyFieldController.text = key2; + await tester.tap(find.text('LegacyWrite')); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('DeleteAll')); + await checkSuccess(tester, state); + await checkMigration(tester, state); + + await tester.tap(find.text('ReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {}.toString(), + ); + }); + + testWidgets('deleteAll - 1 entry, 1 entry, same keys', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('Write')); + await checkSuccess(tester, state); + + await tester.tap(find.text('LegacyWrite')); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('DeleteAll')); + await checkSuccess(tester, state); + await checkMigration(tester, state); + + await tester.tap(find.text('ReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {}.toString(), + ); + }); + + testWidgets('deleteAll - empty, 1entry', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('LegacyWrite')); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('DeleteAll')); + await checkSuccess(tester, state); + await checkMigration(tester, state); + + await tester.tap(find.text('ReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {}.toString(), + ); + }); + + testWidgets('deleteAll - 1entry, empty', (tester) async { + const key = 'key'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key; + await tester.tap(find.text('Write')); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('DeleteAll')); + await checkSuccess(tester, state); + await checkMigration(tester, state); + + await tester.tap(find.text('ReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {}.toString(), + ); + }); + + testWidgets( + 'deleteAll - 2 entries, 2 entries, same keys and diffrent keys', + (tester) async { + const key1 = 'key1'; + const key2 = 'key2'; + const key3 = 'key3'; + + app.main(); + await tester.pumpAndSettle(); + + final state = getState( + tester, + useBackwardCompatibility: true, + ); + + // Prepare + state.keyFieldController.text = key1; + await tester.tap(find.text('Write')); + await checkSuccess(tester, state); + + state.keyFieldController.text = key2; + await tester.tap(find.text('Write')); + await checkSuccess(tester, state); + + state.keyFieldController.text = key3; + await tester.tap(find.text('LegacyWrite')); + await checkSuccess(tester, state); + + state.keyFieldController.text = key1; + await tester.tap(find.text('LegacyWrite')); + await checkSuccess(tester, state); + + // Do test + await tester.tap(find.text('DeleteAll')); + await checkSuccess(tester, state); + await checkMigration(tester, state); + + await tester.tap(find.text('ReadAll')); + await tester.pumpAndSettle(); + await checkSuccess( + tester, + state, + // Standard map's order and toString() result should be stable + // even if there are no guarantee nand backward compatibility. + expectedDetail: {}.toString(), + ); + }); + }, + skip: kIsWeb || !Platform.isWindows + ? 'These tests only work on Windows' + : null, + ); + + group( + 'Special charactors handling', + () { + testWidgets('URL', (tester) async { + await doTestSuite( + tester, + key1: 'http://example.com', + useBackwardCompatibility: false, + ); + }); + + testWidgets('Double dot', (tester) async { + await doTestSuite( + tester, + key1: '/../a', + ); + }); + + testWidgets('Long key', (tester) async { + await doTestSuite( + tester, + key1: + String.fromCharCodes(Iterable.generate(256, (_) => 65 /* 'A' */)), + useBackwardCompatibility: false, + ); + }); + + testWidgets('Empty key & value', (tester) async { + await doTestSuite( + tester, + key1: '', + writingValue1: '', + ); + }); + + for (final char in { + 'ASCII whitespace': 0x20, + 'Nbsp': 0xA0, + 'Full-width space': 0x3000, + }.entries) { + testWidgets( + 'Space key & value - ${char.key} (U+${char.value.toRadixString(16).padLeft(4, '0')})', + (tester) async { + await doTestSuite( + tester, + key1: String.fromCharCode(char.value), + writingValue1: String.fromCharCode(char.value), + ); + }); + } + + for (final char in { + 'Horizontal tab': 0x09, + }.entries) { + testWidgets( + 'Space key & value - ${char.key} (U+${char.value.toRadixString(16).padLeft(4, '0')})', + (tester) async { + await doTestSuite( + tester, + key1: String.fromCharCode(char.value), + writingValue1: String.fromCharCode(char.value), + useBackwardCompatibility: false, + ); + }); + } + + for (final char in { + 'Latin-1 (French)': 'cl\u00E9', + 'CJK (Japanese)': '\u30AD\u30FC', + 'Surrogate Pair (Emoji)': '\uD83D\uDD11', + }.entries) { + testWidgets('Non ASCII key & value - ${char.key}', (tester) async { + await doTestSuite( + tester, + key1: char.value, + writingValue1: char.value, + ); + }); + } + + testWidgets('Only casing is differ', (tester) async { + await doTestSuite( + tester, + key1: 'key', + key2: 'KEY', + ); + }); + }, + skip: kIsWeb || !Platform.isWindows + ? 'These tests only work on Windows' + : null, + ); +} diff --git a/third_party/flutter_secure_storage_windows/example/lib/main.dart b/third_party/flutter_secure_storage_windows/example/lib/main.dart new file mode 100644 index 00000000..9424a018 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/lib/main.dart @@ -0,0 +1,383 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter_secure_storage_platform_interface/flutter_secure_storage_platform_interface.dart'; +import 'package:flutter_secure_storage_windows/flutter_secure_storage_windows.dart'; + +// testing application. +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => MyAppState(); +} + +class MyAppState extends State { + final TextEditingController keyFieldController = TextEditingController(); + final TextEditingController valueFieldController = TextEditingController(); + final TextEditingController resultSummaryFieldController = + TextEditingController(); + final TextEditingController resultDetailFieldController = + TextEditingController(); + final GlobalKey useMethodChannelOnlyKey = GlobalKey(); + final GlobalKey useBackwardCompatibilityKey = + GlobalKey(); + + Future? _future; + + FlutterSecureStoragePlatform _flutterSecureStorageWindowsPlugin = + FlutterSecureStorageWindows(); + final Map _options = {'useBackwardCompatibility': 'false'}; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: Column(children: [ + TextField( + controller: keyFieldController, + decoration: const InputDecoration(label: Text('Key')), + ), + TextField( + controller: valueFieldController, + decoration: const InputDecoration(label: Text('Value')), + ), + LabeledCheckbox( + key: useMethodChannelOnlyKey, + initialValue: false, + label: 'UseMethodChannelOnly', + onChanged: (useMethodChannelOnly) { + setState(() { + _flutterSecureStorageWindowsPlugin = useMethodChannelOnly + ? MethodChannelFlutterSecureStorage() + : FlutterSecureStorageWindows(); + }); + }, + ), + LabeledCheckbox( + key: useBackwardCompatibilityKey, + initialValue: false, + label: 'UseBackwardCompatibility', + onChanged: (useBackwardCompatibility) { + setState(() { + _options['useBackwardCompatibility'] = + useBackwardCompatibility.toString(); + }); + }, + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: ElevatedButton( + onPressed: doRead, + child: const Text('Read'), + ), + ), + Padding( + padding: const EdgeInsets.all(4), + child: ElevatedButton( + onPressed: doReadAll, + child: const Text('ReadAll'), + ), + ), + Padding( + padding: const EdgeInsets.all(4), + child: ElevatedButton( + onPressed: doContainsKey, + child: const Text('ContainsKey'), + ), + ), + ], + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: ElevatedButton( + onPressed: doWrite, + child: const Text('Write'), + ), + ), + Padding( + padding: const EdgeInsets.all(4), + child: ElevatedButton( + onPressed: doDelete, + child: const Text('Delete'), + ), + ), + Padding( + padding: const EdgeInsets.all(4), + child: ElevatedButton( + onPressed: doDeleteAll, + child: const Text('DeleteAll'), + ), + ), + ], + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: ElevatedButton( + onPressed: doLegacyWrite, + child: const Text('LegacyWrite'), + ), + ), + Padding( + padding: const EdgeInsets.all(4), + child: ElevatedButton( + onPressed: doLegacyReadAll, + child: const Text('LegacyReadAll'), + ), + ), + ], + ), + if (_future != null) + FutureBuilder( + builder: (context, snapshot) { + if (!snapshot.hasData && !snapshot.hasError) { + return const CircularProgressIndicator(); + } + + resultSummaryFieldController.text = + (snapshot.data?.success ?? false) ? 'SUCCESS' : 'FAIL'; + + return TextField( + controller: resultSummaryFieldController, + decoration: const InputDecoration(label: Text('Result')), + ); + }, + future: _future, + ), + + if (_future != null) + FutureBuilder( + builder: (context, snapshot) { + if (!snapshot.hasData && !snapshot.hasError) { + return const CircularProgressIndicator(); + } + + resultDetailFieldController.text = + snapshot.error?.toString() ?? + snapshot.data!.detail ?? + ''; + + return Column( + children: [ + TextField( + controller: resultSummaryFieldController, + decoration: + const InputDecoration(label: Text('Result')), + ), + TextField( + controller: resultDetailFieldController, + decoration: + const InputDecoration(label: Text('Detail')), + ), + ], + ); + }, + future: _future, + ), + // const Expanded(child: SizedBox()), + ]), + ), + ), + ); + } + + Future doTestCore(FutureOr Function() test) async { + late final TestResult result; + try { + result = await test(); + } catch (e, s) { + debugPrint(e.toString()); + debugPrintStack(stackTrace: s); + result = TestResult(success: false, detail: e.toString()); + } + + return result; + } + + void doTest(FutureOr Function() test) { + setState(() { + _future = doTestCore(test); + }); + } + + void doRead() => doTest(() async { + final key = keyFieldController.text; + return TestResult( + success: true, + detail: await _flutterSecureStorageWindowsPlugin.read( + key: key, + options: _options, + ), + ); + }); + + void doReadAll() => doTest(() async { + return TestResult( + success: true, + detail: (await _flutterSecureStorageWindowsPlugin.readAll( + options: _options, + )) + .toString(), + ); + }); + + void doContainsKey() => doTest(() async { + final key = keyFieldController.text; + return TestResult( + success: true, + detail: (await _flutterSecureStorageWindowsPlugin.containsKey( + key: key, + options: _options, + )) + .toString(), + ); + }); + + void doWrite() => doTest(() async { + final key = keyFieldController.text; + final value = valueFieldController.text.isNotEmpty + ? valueFieldController.text + : DateTime.now().toIso8601String(); + await _flutterSecureStorageWindowsPlugin.write( + key: key, + value: value, + options: _options, + ); + return TestResult(success: true, detail: value); + }); + + void doDelete() => doTest(() async { + final key = keyFieldController.text; + await _flutterSecureStorageWindowsPlugin.delete( + key: key, + options: _options, + ); + return TestResult( + success: true, + detail: null, + ); + }); + + void doDeleteAll() => doTest(() async { + await _flutterSecureStorageWindowsPlugin.deleteAll( + options: _options, + ); + return TestResult( + success: true, + detail: null, + ); + }); + + void doLegacyWrite() => doTest(() async { + final key = keyFieldController.text; + final value = valueFieldController.text.isNotEmpty + ? valueFieldController.text + : DateTime.now().toIso8601String(); + // call MethodChannelFlutterSecureStorage directly + final legacyStorage = MethodChannelFlutterSecureStorage(); + await legacyStorage.write( + key: key, + value: value, + options: _options, + ); + return TestResult(success: true, detail: value); + }); + + void doLegacyReadAll() => doTest(() async { + // call MethodChannelFlutterSecureStorage directly + final legacyStorage = MethodChannelFlutterSecureStorage(); + return TestResult( + success: true, + detail: (await legacyStorage.readAll( + options: _options, + )) + .toString()); + }); +} + +class TestResult { + final bool success; + final String? detail; + TestResult({ + required this.success, + required this.detail, + }); +} + +class LabeledCheckbox extends StatefulWidget { + final String label; + final EdgeInsetsGeometry padding; + final bool initialValue; + final ValueChanged? onChanged; + const LabeledCheckbox({ + Key? key, + required this.label, + this.padding = const EdgeInsets.all(4), + this.initialValue = false, + this.onChanged, + }) : super(key: key); + + @override + State createState() => LabeledCheckboxState._(); +} + +class LabeledCheckboxState extends State { + late bool _value; + + bool get value => _value; + set value(bool v) { + setState(() { + _value = v; + }); + + widget.onChanged?.call(v); + } + + LabeledCheckboxState._(); + + @override + void initState() { + super.initState(); + _value = widget.initialValue; + } + + @override + Widget build(BuildContext context) => InkWell( + onTap: () { + value = !value; + }, + child: Padding( + padding: widget.padding, + child: Row(children: [ + Expanded(child: Text(widget.label)), + Checkbox( + value: value, + onChanged: (newValue) { + if (newValue != null) { + value = newValue; + } + }) + ]), + ), + ); +} diff --git a/third_party/flutter_secure_storage_windows/example/pubspec.yaml b/third_party/flutter_secure_storage_windows/example/pubspec.yaml new file mode 100644 index 00000000..9d3d0a5c --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/pubspec.yaml @@ -0,0 +1,30 @@ +name: flutter_secure_storage_windows_example +description: Demonstrates how to use the flutter_secure_storage_windows plugin. +publish_to: 'none' + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: ">=2.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_secure_storage_platform_interface: + flutter_secure_storage_windows: + path: ../ + path: ^1.8.0 + path_provider: ^2.0.0 + +dev_dependencies: + flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +dependency_overrides: + flutter_secure_storage_platform_interface: + path: ../../flutter_secure_storage_platform_interface + +flutter: + uses-material-design: true diff --git a/third_party/flutter_secure_storage_windows/example/windows/CMakeLists.txt b/third_party/flutter_secure_storage_windows/example/windows/CMakeLists.txt new file mode 100644 index 00000000..6ff7ad0e --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(flutter_secure_storage_windows_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_secure_storage_windows_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/third_party/flutter_secure_storage_windows/example/windows/flutter/CMakeLists.txt b/third_party/flutter_secure_storage_windows/example/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..930d2071 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/third_party/flutter_secure_storage_windows/example/windows/flutter/generated_plugin_registrant.cc b/third_party/flutter_secure_storage_windows/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..0c507538 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); +} diff --git a/third_party/flutter_secure_storage_windows/example/windows/flutter/generated_plugin_registrant.h b/third_party/flutter_secure_storage_windows/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/third_party/flutter_secure_storage_windows/example/windows/flutter/generated_plugins.cmake b/third_party/flutter_secure_storage_windows/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..4fc759c4 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/third_party/flutter_secure_storage_windows/example/windows/runner/CMakeLists.txt b/third_party/flutter_secure_storage_windows/example/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/third_party/flutter_secure_storage_windows/example/windows/runner/Runner.rc b/third_party/flutter_secure_storage_windows/example/windows/runner/Runner.rc new file mode 100644 index 00000000..4a244a5a --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "flutter_secure_storage_windows_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "flutter_secure_storage_windows_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "flutter_secure_storage_windows_example.exe" "\0" + VALUE "ProductName", "flutter_secure_storage_windows_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/third_party/flutter_secure_storage_windows/example/windows/runner/flutter_window.cpp b/third_party/flutter_secure_storage_windows/example/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b25e363e --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/runner/flutter_window.cpp @@ -0,0 +1,66 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/third_party/flutter_secure_storage_windows/example/windows/runner/flutter_window.h b/third_party/flutter_secure_storage_windows/example/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/third_party/flutter_secure_storage_windows/example/windows/runner/main.cpp b/third_party/flutter_secure_storage_windows/example/windows/runner/main.cpp new file mode 100644 index 00000000..6e0f99d4 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"flutter_secure_storage_windows_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/third_party/flutter_secure_storage_windows/example/windows/runner/resource.h b/third_party/flutter_secure_storage_windows/example/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/third_party/flutter_secure_storage_windows/example/windows/runner/resources/app_icon.ico b/third_party/flutter_secure_storage_windows/example/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/third_party/flutter_secure_storage_windows/example/windows/runner/resources/app_icon.ico differ diff --git a/third_party/flutter_secure_storage_windows/example/windows/runner/runner.exe.manifest b/third_party/flutter_secure_storage_windows/example/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..a42ea768 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/third_party/flutter_secure_storage_windows/example/windows/runner/utils.cpp b/third_party/flutter_secure_storage_windows/example/windows/runner/utils.cpp new file mode 100644 index 00000000..f5bf9fa0 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/third_party/flutter_secure_storage_windows/example/windows/runner/utils.h b/third_party/flutter_secure_storage_windows/example/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/third_party/flutter_secure_storage_windows/example/windows/runner/win32_window.cpp b/third_party/flutter_secure_storage_windows/example/windows/runner/win32_window.cpp new file mode 100644 index 00000000..041a3855 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/third_party/flutter_secure_storage_windows/example/windows/runner/win32_window.h b/third_party/flutter_secure_storage_windows/example/windows/runner/win32_window.h new file mode 100644 index 00000000..c86632d8 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/example/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/third_party/flutter_secure_storage_windows/lib/flutter_secure_storage_windows.dart b/third_party/flutter_secure_storage_windows/lib/flutter_secure_storage_windows.dart new file mode 100644 index 00000000..7b561098 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/lib/flutter_secure_storage_windows.dart @@ -0,0 +1,3 @@ +export 'src/flutter_secure_storage_windows_stub.dart' + if (dart.library.ffi) 'src/flutter_secure_storage_windows_ffi.dart' + show FlutterSecureStorageWindows; diff --git a/third_party/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_ffi.dart b/third_party/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_ffi.dart new file mode 100644 index 00000000..b5cea2a9 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_ffi.dart @@ -0,0 +1,404 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; + +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart' show debugPrint, visibleForTesting; +import 'package:flutter/services.dart'; +import 'package:flutter_secure_storage_platform_interface/flutter_secure_storage_platform_interface.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:win32/win32.dart'; + +@visibleForTesting +extension OptionsExtension on Map { + bool get useBackwardCompatibility => + this['useBackwardCompatibility'] != 'false'; +} + +class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform { + final FlutterSecureStoragePlatform _backwardCompatible; + final MapStorage _storage; + + FlutterSecureStorageWindows() + : this._( + MethodChannelFlutterSecureStorage(), + DpapiJsonFileMapStorage(), + ); + + FlutterSecureStorageWindows._( + this._backwardCompatible, + this._storage, + ); + + /// Registers this plugin. + static void registerWith() { + FlutterSecureStoragePlatform.instance = FlutterSecureStorageWindows(); + } + + @override + Future containsKey({ + required String key, + required Map options, + }) async { + final map = await _storage.load(options); + if (map.containsKey(key)) { + return true; + } + + if (options.useBackwardCompatibility) { + return _backwardCompatible.containsKey(key: key, options: options); + } + + return false; + } + + @override + Future delete({ + required String key, + required Map options, + }) async { + final map = await _storage.load(options); + final initialSize = map.length; + map.remove(key); + if (map.length != initialSize) { + await _storage.save(map, options); + } + + if (options.useBackwardCompatibility) { + await _backwardCompatible.delete(key: key, options: options); + } + } + + @override + Future deleteAll({required Map options}) async { + await _storage.clear(options); + + if (options.useBackwardCompatibility) { + await _backwardCompatible.deleteAll(options: options); + } + } + + @override + Future read({ + required String key, + required Map options, + }) async { + final map = await _storage.load(options); + + var result = map[key]; + if (options.useBackwardCompatibility) { + if (result == null) { + final compatible = + await _backwardCompatible.read(key: key, options: options); + if (compatible != null) { + // Write back now, so the value should be retrieved from JSON file next. + result = map[key] = compatible; + await _storage.save(map, options); + } + } + + // Clear old entry. + await _backwardCompatible.delete(key: key, options: options); + } + + return result; + } + + @override + Future> readAll({ + required Map options, + }) async { + final map = await _storage.load(options); + if (!options.useBackwardCompatibility) { + // Just return a map. + return map; + } + + final compatible = await _backwardCompatible.readAll(options: options); + + if (compatible.isEmpty) { + return map; + } + + for (final entry in compatible.entries) { + map.putIfAbsent(entry.key, () => entry.value); + } + + // Write back now, so the value should be retrieved from JSON file next. + await _storage.save(map, options); + + // Clear old entries. + await _backwardCompatible.deleteAll(options: options); + + return map; + } + + @override + Future write({ + required String key, + required String value, + required Map options, + }) async { + final map = await _storage.load(options); + map[key] = value; + await _storage.save(map, options); + + if (options.useBackwardCompatibility) { + // Clear old entry. + _backwardCompatible.delete(key: key, options: options); + } + } + + // @override + // Future isCupertinoProtectedDataAvailable() => Future.value(true); + // + // @override + // Stream get onCupertinoProtectedDataAvailabilityChanged => + // Stream.value(true); +} + +@visibleForTesting +FlutterSecureStorageWindows createFlutterSecureStorageWindows( + FlutterSecureStoragePlatform backwardCompatible, + MapStorage mapStorage, +) => + FlutterSecureStorageWindows._(backwardCompatible, mapStorage); + +@visibleForTesting +abstract class MapStorage { + FutureOr> load(Map options); + FutureOr save(Map data, Map options); + FutureOr clear(Map options); +} + +@visibleForTesting +const String encryptedJsonFileName = 'flutter_secure_storage.dat'; + +@visibleForTesting +class DpapiJsonFileMapStorage extends MapStorage { + DpapiJsonFileMapStorage(); + + FutureOr _getJsonFilePath() async { + final appDataDirectory = await getApplicationSupportDirectory(); + + return path.canonicalize( + path.join( + appDataDirectory.path, + encryptedJsonFileName, + ), + ); + } + + @override + FutureOr> load(Map options) async { + final file = File(await _getJsonFilePath()); + if (!(await file.exists())) { + return {}; + } + + late final Uint8List encryptedText; + try { + encryptedText = await file.readAsBytes(); + } on FileSystemException catch (e) { + // Another process has been deleted a file or parent directory + // since previous File.exists() call. + // We can ignore it. + debugPrint( + 'Reading file has been deleted by another process. $e', + ); + return {}; + } + + late final String plainText; + try { + plainText = using((alloc) { + final Pointer pEncryptedText = alloc(encryptedText.length); + pEncryptedText + .asTypedList(encryptedText.length) + .setAll(0, encryptedText); + + // Specify size of the struct explicitly. + final Pointer encryptedTextBlob = + alloc.allocate(sizeOf()); + encryptedTextBlob.ref.cbData = encryptedText.length; + encryptedTextBlob.ref.pbData = pEncryptedText; + + // Specify size of the struct explicitly. + final Pointer plainTextBlob = + alloc.allocate(sizeOf()); + if (CryptUnprotectData( + encryptedTextBlob, + nullptr, + nullptr, + nullptr, + nullptr, + 0, + plainTextBlob, + ) == + 0) { + throw WindowsException( + GetLastError(), + message: 'Failure on CryptUnprotectData()', + ); + } + + if (plainTextBlob.ref.pbData.address == NULL) { + throw WindowsException( + ERROR_OUTOFMEMORY, + message: 'Failure on CryptUnprotectData()', + ); + } + + try { + return utf8.decoder.convert( + plainTextBlob.ref.pbData.asTypedList(plainTextBlob.ref.cbData), + ); + } finally { + if (plainTextBlob.ref.pbData.address != NULL) { + if (LocalFree(plainTextBlob.ref.pbData).address != NULL) { + debugPrint( + 'load: Failed to LocalFree with: 0x${GetLastError().toHexString(32)}', + ); + } + } + } + }); + } on FormatException catch (e) { + // A file content should be malformed. + debugPrint( + 'Failed to decrypt data: $e Delete corrupt file: ${file.path}', + ); + await file.delete(); + rethrow; + } on WindowsException catch (e) { + // A file content should be malformed. + debugPrint( + 'Failed to decrypt data: $e Delete corrupt file: ${file.path}', + ); + await file.delete(); + rethrow; + } + + final dynamic decoded; + try { + decoded = jsonDecode(plainText); + } on FormatException catch (e) { + // A file content should be malformed. + debugPrint( + 'Failed to parse JSON: $e Delete corrupt file: ${file.path}', + ); + await file.delete(); + rethrow; + } + + if (decoded is! Map) { + debugPrint( + 'Failed to parse JSON: Not an object. Delete corrupt file: ${file.path}', + ); + await file.delete(); + throw const FormatException('JSON is not an object.'); + } + + return { + for (final e + in decoded.entries.where((x) => x.key is String && x.value is String)) + e.key as String: e.value as String, + }; + } + + @override + FutureOr save( + Map data, + Map options, + ) async { + final file = File(await _getJsonFilePath()); + final json = jsonEncode(data); + final plainText = utf8.encode(json); + + await using>((alloc) async { + final Pointer pPlainText = alloc(plainText.length); + pPlainText.asTypedList(plainText.length).setAll(0, plainText); + + // Specify size of the struct explicitly. + final Pointer plainTextBlob = + alloc.allocate(sizeOf()); + plainTextBlob.ref.cbData = plainText.length; + plainTextBlob.ref.pbData = pPlainText; + + // Specify size of the struct explicitly. + final Pointer encryptedTextBlob = + alloc.allocate(sizeOf()); + if (CryptProtectData( + plainTextBlob, + nullptr, + nullptr, + nullptr, + nullptr, + 0, + encryptedTextBlob, + ) == + 0) { + throw WindowsException( + GetLastError(), + message: 'Failure on CryptProtectData()', + ); + } + + if (encryptedTextBlob.ref.pbData.address == NULL) { + throw WindowsException( + ERROR_OUTOFMEMORY, + message: 'Failure on CryptProtectData()', + ); + } + + try { + final encryptedText = encryptedTextBlob.ref.pbData + .asTypedList(encryptedTextBlob.ref.cbData); + + // Loop to handle race condition. + while (true) { + try { + await (await file.create(recursive: true)) + .writeAsBytes(encryptedText, flush: true); + // If success, finish loop. + break; + } on FileSystemException catch (e) { + // Another process has been deleted a file or parent directory + // since previous File.create() call. + // We will retry writing. + debugPrint( + 'Reading file has been deleted by another process. $e', + ); + } + } + } finally { + if (encryptedTextBlob.ref.pbData.address != NULL) { + if (LocalFree(encryptedTextBlob.ref.pbData).address != NULL) { + debugPrint( + 'save: Failed to LocalFree with: 0x${GetLastError().toHexString(32)}', + ); + } + } + } + }); + } + + @override + FutureOr clear(Map options) async { + final file = File(await _getJsonFilePath()); + if (await file.exists()) { + try { + await file.delete(); + } on FileSystemException catch (e) { + // Another process has been deleted a file or parent directory + // since previous File.exists() call. + // We can ignore it. + debugPrint( + 'Deleting file has been deleted by another process. $e', + ); + } + } + } +} diff --git a/third_party/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_stub.dart b/third_party/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_stub.dart new file mode 100644 index 00000000..b6647f0a --- /dev/null +++ b/third_party/flutter_secure_storage_windows/lib/src/flutter_secure_storage_windows_stub.dart @@ -0,0 +1,58 @@ +import 'package:flutter_secure_storage_platform_interface/flutter_secure_storage_platform_interface.dart'; + +/// A stub implementation to avoid extra transitive dependencies +/// on non-Windows platforms including web. +class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform { + /// Cannot be instantiated. + FlutterSecureStorageWindows() + : assert(false, 'Cannot instantiate this class.'); + + /// Registers this plugin. + static void registerWith() { + FlutterSecureStoragePlatform.instance = FlutterSecureStorageWindows(); + } + + @override + Future containsKey({ + required String key, + required Map options, + }) => + Future.value(false); + + @override + Future delete({ + required String key, + required Map options, + }) => + Future.value(); + + @override + Future deleteAll({required Map options}) => + Future.value(); + + @override + Future read({ + required String key, + required Map options, + }) => + Future.value(); + + @override + Future> readAll({required Map options}) => + Future.value({}); + + @override + Future write({ + required String key, + required String value, + required Map options, + }) => + Future.value(); + + // @override + // Future isCupertinoProtectedDataAvailable() => Future.value(true); + // + // @override + // Stream get onCupertinoProtectedDataAvailabilityChanged => + // Stream.value(true); +} diff --git a/third_party/flutter_secure_storage_windows/pubspec.yaml b/third_party/flutter_secure_storage_windows/pubspec.yaml new file mode 100644 index 00000000..0f479608 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/pubspec.yaml @@ -0,0 +1,30 @@ +name: flutter_secure_storage_windows +description: Windows implementation of flutter_secure_storage. Please use flutter_secure_storage instead of this package. +repository: https://github.com/mogol/flutter_secure_storage +version: 3.1.2 + +environment: + sdk: '>=2.12.0 <4.0.0' + flutter: ">=2.0.0" + +dependencies: + ffi: ^2.0.0 + flutter: + sdk: flutter + flutter_secure_storage_platform_interface: ^1.1.1 + path: ^1.8.0 + path_provider: ^2.0.0 + win32: ^5.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + lint: ^2.0.0 + +flutter: + plugin: + implements: flutter_secure_storage + platforms: + windows: + pluginClass: FlutterSecureStorageWindowsPlugin + dartPluginClass: FlutterSecureStorageWindows diff --git a/third_party/flutter_secure_storage_windows/test/unit_test.dart b/third_party/flutter_secure_storage_windows/test/unit_test.dart new file mode 100644 index 00000000..363db5d6 --- /dev/null +++ b/third_party/flutter_secure_storage_windows/test/unit_test.dart @@ -0,0 +1,1157 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:flutter_secure_storage_platform_interface/flutter_secure_storage_platform_interface.dart'; +import 'package:flutter_secure_storage_windows/src/flutter_secure_storage_windows_ffi.dart' + as ffi; +import 'package:flutter_secure_storage_windows/src/flutter_secure_storage_windows_ffi.dart'; +import 'package:flutter_secure_storage_windows/src/flutter_secure_storage_windows_stub.dart' + as stub; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + FutureOr cleanUpFiles() async { + // Clean up current & legacy files. + final directory = await getApplicationSupportDirectory(); + if (directory.existsSync()) { + directory + .listSync(followLinks: false) + .whereType() + .where((f) => + path.basename(f.path) == encryptedJsonFileName || + f.path.endsWith('.secure'),) + .forEach((f) => f.deleteSync()); + } + } + + setUpAll(() async { + await cleanUpFiles(); + }); + + tearDown(() async { + await cleanUpFiles(); + }); + + group('Basic test cases', () { + FlutterSecureStoragePlatform createTarget() { + TestWidgetsFlutterBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('plugins.it_nomads.com/flutter_secure_storage'), + (methodCall) async { + assert(false, 'MethodChanel is called.'); + return null; + }, + ); + return ffi.FlutterSecureStorageWindows(); + } + + Map createOptions() => + {'useBackwardCompatibility': 'false'}; + + test( + 'readAll - empty', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + final result = await target.readAll(options: options); + expect(result, isEmpty); + }), + ); + + test( + 'readAll - 1 entries', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + const key = 'KEY'; + const value = 'VALUE'; + await target.write(key: key, value: value, options: options); + final result = await target.readAll(options: options); + expect(result.length, 1); + expect(result[key], value); + }), + ); + + test( + 'readAll - 2 entries', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + const key1 = 'KEY1'; + const value1 = 'VALUE1'; + const key2 = 'KEY2'; + const value2 = 'VALUE2'; + await target.write(key: key1, value: value1, options: options); + await target.write(key: key2, value: value2, options: options); + final result = await target.readAll(options: options); + expect(result.length, 2); + expect(result[key1], value1); + expect(result[key2], value2); + }), + ); + + test( + 'read - exists', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + const key = 'KEY'; + const value = 'VALUE'; + await target.write(key: key, value: value, options: options); + final result = await target.read(key: key, options: options); + expect(result, isNotNull); + expect(result, value); + }), + ); + + test( + 'read - does not exist', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + const key = 'KEY'; + final result = await target.read(key: key, options: options); + expect(result, isNull); + }), + ); + + test( + 'containsKey - exists', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + const key = 'KEY'; + const value = 'VALUE'; + await target.write(key: key, value: value, options: options); + expect( + await target.containsKey(key: key, options: options), + isTrue, + ); + }), + ); + + test( + 'containsKey - does not exist', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + const key = 'KEY'; + expect( + await target.containsKey(key: key, options: options), + isFalse, + ); + }), + ); + + test( + 'write - new', + () => withFfi(() async { + // Just checking file was created. Its contents should be tested via "read" test. + + final target = createTarget(); + final options = createOptions(); + const key = 'KEY'; + const value = 'VALUE'; + await target.write(key: key, value: value, options: options); + + final directory = await getApplicationSupportDirectory(); + final file = File(path.join(directory.path, encryptedJsonFileName)); + expect(file.existsSync(), isTrue); + expect(file.statSync().size, greaterThan(0)); + // May be encrypted + final content = file.readAsBytesSync(); + expect( + content, + isNot( + Uint8List.fromList( + utf8.encode('{"$key":"$value"}'), + ), + ), + ); + try { + final map = jsonDecode(utf8.decode(content)); + if (map is! Map || map[key] != value) { + throw const FormatException('might be encrypted'); + } + + fail('might not be encrypted'); + } on FormatException catch (_) { + // OK + } + }), + ); + + test( + 'write - overwrite', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + const key = 'KEY'; + const value1 = 'VALUE1'; + const value2 = 'VALUE2'; + await target.write(key: key, value: value1, options: options); + await target.write(key: key, value: value2, options: options); + + final result = await target.read(key: key, options: options); + expect(result, isNotNull); + expect(result, value2); + + final results = await target.readAll(options: options); + expect(results.length, 1); + expect(results[key], value2); + }), + ); + + test( + 'delete - exists', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + const key = 'KEY'; + const value = 'VALUE'; + await target.write(key: key, value: value, options: options); + expect( + await target.containsKey(key: key, options: options), + isTrue, + ); + + await target.delete(key: key, options: options); + expect( + await target.containsKey(key: key, options: options), + isFalse, + ); + }), + ); + + test( + 'delete - does not exist', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + const key = 'KEY'; + expect( + await target.containsKey(key: key, options: options), + isFalse, + ); + + await target.delete(key: key, options: options); + + expect( + await target.containsKey(key: key, options: options), + isFalse, + ); + }), + ); + + test( + 'deleteAll - empty', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + await target.deleteAll(options: options); + expect( + await target.readAll(options: options), + isEmpty, + ); + }), + ); + + test( + 'deleteAll - 1 entries', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + const key = 'KEY'; + const value = 'VALUE'; + await target.write(key: key, value: value, options: options); + await target.deleteAll(options: options); + expect( + await target.readAll(options: options), + isEmpty, + ); + }), + ); + + test( + 'deleteAll - 2 entries', + () => withFfi(() async { + final target = createTarget(); + final options = createOptions(); + const key1 = 'KEY1'; + const value1 = 'VALUE1'; + const key2 = 'KEY2'; + const value2 = 'VALUE2'; + await target.write(key: key1, value: value1, options: options); + await target.write(key: key2, value: value2, options: options); + await target.deleteAll(options: options); + expect( + await target.readAll(options: options), + isEmpty, + ); + }), + ); + }); + + // These cases depend on 'Basic cases' are passed corrrectly. + // Just test backward compatibility logics. + group('Backwards compatibilty cases', () { + FlutterSecureStoragePlatform createTarget( + Future Function(MethodCall) handler, + ) { + TestWidgetsFlutterBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('plugins.it_nomads.com/flutter_secure_storage'), + handler, + ); + return ffi.createFlutterSecureStorageWindows( + MethodChannelFlutterSecureStorage(), + ffi.DpapiJsonFileMapStorage(), + ); + } + + Map createOptions() => {'useBackwardCompatibility': 'true'}; + + test( + 'readAll - empty, empty', + () => withFfi(() async { + var readAllCalled = 0; + var deleteAllCalled = 0; + final target = createTarget((call) async { + switch (call.method) { + case 'readAll': + readAllCalled++; + return {}; + case 'deleteAll': + deleteAllCalled++; + return null; + default: + fail('Unexpected method call: ${call.method}'); + } + }); + final options = createOptions(); + final result = await target.readAll(options: options); + expect(result, isEmpty); + expect(readAllCalled, 1); + expect(deleteAllCalled, 0); + }), + ); + test( + 'readAll - 1 entry, 1 entry, different keys', + () => withFfi(() async { + const newKey = 'KEY1'; + const newValue = 'VALUE1'; + const oldKey = 'KEY2'; + const oldValue = 'VALUE2'; + + var readAllCalled = 0; + var deleteAllCalled = 0; + var onInit = true; + final target = createTarget((call) async { + switch (call.method) { + case 'readAll': + readAllCalled++; + return deleteAllCalled > 0 ? {} : {oldKey: oldValue}; + case 'deleteAll': + deleteAllCalled++; + return null; + case 'delete': + if (onInit) { + return null; + } + } + fail('Unexpected method call: ${call.method}'); + }); + final options = createOptions(); + await target.write(key: newKey, value: newValue, options: options); + onInit = false; + final result1 = await target.readAll(options: options); + expect(result1.length, 2); + expect(result1[oldKey], oldValue); + expect(result1[newKey], newValue); + expect(readAllCalled, 1); + expect(deleteAllCalled, 1); + + final result2 = await target.readAll(options: options); + expect(result2.length, 2); + expect(result2[oldKey], oldValue); + expect(result2[newKey], newValue); + expect(readAllCalled, 2); + expect(deleteAllCalled, 1); + }), + ); + + test( + 'readAll - 1 entry, 1 entry, same keys', + () => withFfi(() async { + const newKey = 'KEY'; + const newValue = 'VALUE1'; + const oldKey = newKey; + const oldValue = 'VALUE2'; + + var readAllCalled = 0; + var deleteAllCalled = 0; + var onInit = true; + final target = createTarget((call) async { + switch (call.method) { + case 'readAll': + readAllCalled++; + return deleteAllCalled > 0 ? {} : {oldKey: oldValue}; + case 'deleteAll': + deleteAllCalled++; + return null; + case 'delete': + if (onInit) { + return null; + } + } + fail('Unexpected method call: ${call.method}'); + }); + final options = createOptions(); + await target.write(key: newKey, value: newValue, options: options); + onInit = false; + final result1 = await target.readAll(options: options); + expect(result1.length, 1); + expect(result1[oldKey], newValue); + expect(result1[newKey], newValue); + expect(readAllCalled, 1); + expect(deleteAllCalled, 1); + + final result2 = await target.readAll(options: options); + expect(result2.length, 1); + expect(result1[oldKey], newValue); + expect(result1[newKey], newValue); + expect(readAllCalled, 2); + expect(deleteAllCalled, 1); + }), + ); + + test( + 'readAll - empty, 1entry', + () => withFfi(() async { + const oldKey = 'KEY'; + const oldValue = 'VALUE2'; + + var readCalled = 0; + var readAllCalled = 0; + var deleteAllCalled = 0; + var deleteCalled = 0; + final target = createTarget((call) async { + switch (call.method) { + case 'read': + readCalled++; + return deleteAllCalled > 0 + ? null + : (call.arguments as Map)['key'] == oldKey + ? oldValue + : null; + case 'readAll': + readAllCalled++; + return deleteAllCalled > 0 ? {} : {oldKey: oldValue}; + case 'deleteAll': + deleteAllCalled++; + return null; + case 'delete': + deleteCalled++; + return null; + default: + fail('Unexpected method call: ${call.method}'); + } + }); + + final options = createOptions(); + final result1 = await target.readAll(options: options); + expect(result1.length, 1); + expect(result1[oldKey], oldValue); + expect(readCalled, 0); + expect(readAllCalled, 1); + expect(deleteAllCalled, 1); + expect(deleteCalled, 0); + + final result2 = await target.readAll(options: options); + expect(result2.length, 1); + expect(result2[oldKey], oldValue); + expect(readCalled, 0); + expect(readAllCalled, 2); + expect(deleteAllCalled, 1); + expect(deleteCalled, 0); + + final result3 = await target.read(key: oldKey, options: options); + expect(result3, oldValue); + // auto-migrated + expect(readCalled, 0); + expect(readAllCalled, 2); + expect(deleteAllCalled, 1); + expect(deleteCalled, 1); + }), + ); + + test( + 'readAll - 1entry, empty', + () => withFfi(() async { + const newKey = 'KEY'; + const newValue = 'VALUE1'; + + var readAllCalled = 0; + var deleteAllCalled = 0; + var onInit = true; + final target = createTarget((call) async { + switch (call.method) { + case 'readAll': + readAllCalled++; + return {}; + case 'deleteAll': + deleteAllCalled++; + return null; + case 'delete': + if (onInit) { + return null; + } + } + fail('Unexpected method call: ${call.method}'); + }); + final options = createOptions(); + await target.write(key: newKey, value: newValue, options: options); + onInit = false; + final result1 = await target.readAll(options: options); + expect(result1.length, 1); + expect(result1[newKey], newValue); + expect(readAllCalled, 1); + expect(deleteAllCalled, 0); + + final result2 = await target.readAll(options: options); + expect(result2.length, 1); + expect(result1[newKey], newValue); + expect(readAllCalled, 2); + expect(deleteAllCalled, 0); + }), + ); + + test( + 'readAll - 2 entries, 2 entries, same keys and diffrent keys', + () => withFfi(() async { + const newKey1 = 'KEY1'; + const newValue1 = 'VALUE1'; + const newKey2 = 'KEY2'; + const newValue2 = 'VALUE2'; + const oldKey1 = 'KEY3'; + const oldValue1 = 'VALUE3'; + const oldKey2 = newKey1; + const oldValue2 = 'VALUE4'; + + var readAllCalled = 0; + var deleteAllCalled = 0; + var onInit = true; + final target = createTarget((call) async { + switch (call.method) { + case 'readAll': + readAllCalled++; + return deleteAllCalled > 0 + ? {} + : {oldKey1: oldValue1, oldKey2: oldValue2}; + case 'deleteAll': + deleteAllCalled++; + return null; + case 'delete': + if (onInit) { + return null; + } + } + fail('Unexpected method call: ${call.method}'); + }); + final options = createOptions(); + await target.write(key: newKey1, value: newValue1, options: options); + await target.write(key: newKey2, value: newValue2, options: options); + onInit = false; + final result1 = await target.readAll(options: options); + expect(result1.length, 3); + expect(result1[newKey1], newValue1); + expect(result1[newKey2], newValue2); + expect(result1[oldKey1], oldValue1); + expect(result1[oldKey2], newValue1); + expect(readAllCalled, 1); + expect(deleteAllCalled, 1); + + final result2 = await target.readAll(options: options); + expect(result2.length, 3); + expect(result1[newKey1], newValue1); + expect(result1[newKey2], newValue2); + expect(result1[oldKey1], oldValue1); + expect(result1[oldKey2], newValue1); + expect(readAllCalled, 2); + expect(deleteAllCalled, 1); + }), + ); + + test( + 'read - exists, exists', + () => withFfi(() async { + const key = 'KEY'; + const newValue = 'VALUE1'; + const oldValue = 'VALUE2'; + + var readCalled = 0; + var deleteCalled = 0; + final target = createTarget((call) async { + switch (call.method) { + case 'read': + readCalled++; + return deleteCalled > 0 ? null : {key: oldValue}; + case 'delete': + deleteCalled++; + return null; + default: + fail('Unexpected method call: ${call.method}'); + } + }); + final options = createOptions(); + await target.write(key: key, value: newValue, options: options); + expect(deleteCalled, 1); + final result1 = await target.read(key: key, options: options); + expect(result1, newValue); + expect(readCalled, 0); + expect(deleteCalled, 2); + + final result2 = await target.read(key: key, options: options); + expect(result2, newValue); + expect(readCalled, 0); + expect(deleteCalled, 3); + }), + ); + + test( + 'read - does not exist, exists', + () => withFfi(() async { + const key = 'KEY'; + const value = 'VALUE'; + + var readCalled = 0; + var deleteCalled = 0; + final target = createTarget((call) async { + switch (call.method) { + case 'read': + readCalled++; + return deleteCalled > 0 ? null : value; + case 'delete': + deleteCalled++; + return null; + default: + fail('Unexpected method call: ${call.method}'); + } + }); + final options = createOptions(); + final result1 = await target.read(key: key, options: options); + expect(result1, value); + expect(readCalled, 1); + expect(deleteCalled, 1); + + final result2 = await target.read(key: key, options: options); + expect(result2, value); + expect(readCalled, 1); + expect(deleteCalled, 2); + }), + ); + + test( + 'read - exists, does not exist', + () => withFfi(() async { + const key = 'KEY'; + const value = 'VALUE'; + + var readCalled = 0; + var deleteCalled = 0; + final target = createTarget((call) async { + switch (call.method) { + case 'read': + readCalled++; + return null; + case 'delete': + deleteCalled++; + return null; + default: + fail('Unexpected method call: ${call.method}'); + } + }); + final options = createOptions(); + await target.write(key: key, value: value, options: options); + expect(deleteCalled, 1); + + final result1 = await target.read(key: key, options: options); + expect(result1, value); + expect(readCalled, 0); + expect(deleteCalled, 2); + + final result2 = await target.read(key: key, options: options); + expect(result2, value); + expect(readCalled, 0); + expect(deleteCalled, 3); + }), + ); + + test( + 'read - does not exist, does not exist', + () => withFfi(() async { + const key = 'KEY'; + + var readCalled = 0; + var deleteCalled = 0; + final target = createTarget((call) async { + switch (call.method) { + case 'read': + readCalled++; + return null; + case 'delete': + deleteCalled++; + return null; + default: + fail('Unexpected method call: ${call.method}'); + } + }); + final options = createOptions(); + + final result1 = await target.read(key: key, options: options); + expect(result1, isNull); + expect(readCalled, 1); + expect(deleteCalled, 1); + + final result2 = await target.read(key: key, options: options); + expect(result2, isNull); + expect(readCalled, 2); + expect(deleteCalled, 2); + }), + ); + + test( + 'containsKey - exists, exists', + () => withFfi(() async { + const key = 'KEY'; + const newValue = 'VALUE1'; + + var containsKeyCalled = 0; + var deleteCalled = 0; + final target = createTarget((call) async { + switch (call.method) { + case 'containsKey': + containsKeyCalled++; + return deleteCalled > 0 && (call.arguments as Map)['key'] == key; + case 'delete': + deleteCalled++; + return null; + default: + fail('Unexpected method call: ${call.method}'); + } + }); + final options = createOptions(); + await target.write(key: key, value: newValue, options: options); + expect(deleteCalled, 1); + expect( + await target.containsKey(key: key, options: options), + isTrue, + ); + expect(containsKeyCalled, 0); + expect(deleteCalled, 1); + + expect( + await target.containsKey(key: key, options: options), + isTrue, + ); + expect(containsKeyCalled, 0); + expect(deleteCalled, 1); + }), + ); + + test( + 'containsKey - does not exist, exists', + () => withFfi(() async { + const key = 'KEY'; + + var containsKeyCalled = 0; + final target = createTarget((call) async { + switch (call.method) { + case 'containsKey': + containsKeyCalled++; + return (call.arguments as Map)['key'] == key; + default: + fail('Unexpected method call: ${call.method}'); + } + }); + final options = createOptions(); + expect( + await target.containsKey(key: key, options: options), + isTrue, + ); + expect(containsKeyCalled, 1); + + expect( + await target.containsKey(key: key, options: options), + isTrue, + ); + expect(containsKeyCalled, 2); + }), + ); + + test( + 'containsKey - exists, does not exist', + () => withFfi(() async { + const key = 'KEY'; + const newValue = 'VALUE1'; + + var containsKeyCalled = 0; + var onInit = true; + final target = createTarget((call) async { + switch (call.method) { + case 'containsKey': + containsKeyCalled++; + return false; + case 'delete': + if (onInit) { + return null; + } + break; + } + fail('Unexpected method call: ${call.method}'); + }); + final options = createOptions(); + await target.write(key: key, value: newValue, options: options); + onInit = false; + expect( + await target.containsKey(key: key, options: options), + isTrue, + ); + expect(containsKeyCalled, 0); + + expect( + await target.containsKey(key: key, options: options), + isTrue, + ); + expect(containsKeyCalled, 0); + }), + ); + + test( + 'containsKey - does not exist, does not exist', + () => withFfi(() async { + const key = 'KEY'; + + var containsKeyCalled = 0; + final target = createTarget((call) async { + switch (call.method) { + case 'containsKey': + containsKeyCalled++; + return false; + default: + fail('Unexpected method call: ${call.method}'); + } + }); + final options = createOptions(); + expect( + await target.containsKey(key: key, options: options), + isFalse, + ); + expect(containsKeyCalled, 1); + + expect( + await target.containsKey(key: key, options: options), + isFalse, + ); + expect(containsKeyCalled, 2); + }), + ); + + test( + 'write - new', + () async { + const key = 'KEY'; + const value = 'VALUE'; + + var deleteCalled = 0; + final target = createTarget((call) async { + if (call.method == 'delete') { + deleteCalled++; + return null; + } + + fail('Unexpected method call: ${call.method}'); + }); + final options = createOptions(); + await target.write(key: key, value: value, options: options); + expect(deleteCalled, 1); + + final result = await target.read(key: key, options: options); + expect(result, value); + expect(deleteCalled, 2); + }, + ); + + test( + 'write - overwrite', + () async { + const key = 'KEY'; + const value1 = 'VALUE1'; + const value2 = 'VALUE2'; + + var deleteCalled = 0; + final target = createTarget((call) async { + if (call.method == 'delete') { + deleteCalled++; + return null; + } + + fail('Unexpected method call: ${call.method}'); + }); + final options = createOptions(); + await target.write(key: key, value: value1, options: options); + expect(deleteCalled, 1); + await target.write(key: key, value: value2, options: options); + expect(deleteCalled, 2); + + final result = await target.read(key: key, options: options); + expect(result, value2); + expect(deleteCalled, 3); + }, + ); + + test( + 'delete - exists, any', + () => withFfi(() async { + const key = 'KEY'; + const newValue = 'VALUE1'; + + var deleteCalled = 0; + final target = createTarget((call) async { + switch (call.method) { + case 'delete': + deleteCalled++; + return null; + default: + fail('Unexpected method call: ${call.method}'); + } + }); + final options = createOptions(); + await target.write(key: key, value: newValue, options: options); + expect(deleteCalled, 1); + await target.delete(key: key, options: options); + expect(deleteCalled, 2); + }), + ); + + test( + 'delete - does not exist, any', + () => withFfi(() async { + var deleteCalled = 0; + final target = createTarget((call) async { + switch (call.method) { + case 'delete': + deleteCalled++; + return null; + default: + fail('Unexpected method call: ${call.method}'); + } + }); + final options = createOptions(); + await target.delete(key: 'KEY', options: options); + expect(deleteCalled, 1); + }), + ); + + test( + 'deleteAll - empty, any', + () => withFfi(() async { + var deleteAllCalled = 0; + final target = createTarget((call) async { + switch (call.method) { + case 'deleteAll': + deleteAllCalled++; + return null; + default: + fail('Unexpected method call: ${call.method}'); + } + }); + final options = createOptions(); + await target.deleteAll(options: options); + expect(deleteAllCalled, 1); + }), + ); + test( + 'deleteAll - 1 entry, any', + () => withFfi(() async { + const key = 'KEY'; + const newValue = 'VALUE1'; + + var deleteAllCalled = 0; + var onInit = true; + final target = createTarget((call) async { + switch (call.method) { + case 'deleteAll': + deleteAllCalled++; + return null; + case 'delete': + if (onInit) { + return null; + } + break; + } + fail('Unexpected method call: ${call.method}'); + }); + final options = createOptions(); + await target.write(key: key, value: newValue, options: options); + onInit = false; + await target.deleteAll(options: options); + expect(deleteAllCalled, 1); + }), + ); + }); + + group('Stub does not work at all', () { + test( + 'constructor', + () async { + expect( + () => stub.FlutterSecureStorageWindows(), + throwsAssertionError, + ); + }, + ); + }); + + group('Special charactors handling', () { + FlutterSecureStoragePlatform createTarget() { + TestWidgetsFlutterBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('plugins.it_nomads.com/flutter_secure_storage'), + (methodCall) async { + switch (methodCall.method) { + case 'read': + return null; + case 'readAll': + return {}; + case 'containsKey': + return false; + case 'write': + fail('write on MethodChanel causes error for special chars.'); + case 'delete': + case 'deleteAll': + return null; + default: + fail('Unexpected method call: $methodCall'); + } + }, + ); + return ffi.FlutterSecureStorageWindows(); + } + + Map createOptions() => {'useBackwardCompatibility': 'true'}; + + Future testSpecialCharactor( + String key, { + String? value, + }) async { + final target = createTarget(); + final options = createOptions(); + + final realValue = value ?? DateTime.now().toIso8601String(); + + await target.write(key: key, value: realValue, options: options); + + expect(await target.containsKey(key: key, options: options), isTrue); + expect(await target.read(key: key, options: options), realValue); + expect(await target.readAll(options: options), {key: realValue}); + await target.delete(key: key, options: options); + expect(await target.containsKey(key: key, options: options), isFalse); + expect(await target.read(key: key, options: options), isNull); + expect(await target.readAll(options: options), isEmpty); + + await target.write(key: '$key#1', value: realValue, options: options); + await target.write(key: '$key#2', value: realValue, options: options); + + expect( + await target.containsKey(key: '$key#1', options: options), + isTrue, + ); + expect( + await target.containsKey(key: '$key#2', options: options), + isTrue, + ); + await target.deleteAll(options: options); + + expect( + await target.containsKey(key: '$key#1', options: options), + isFalse, + ); + expect( + await target.containsKey(key: '$key#2', options: options), + isFalse, + ); + } + + test('URL', () => testSpecialCharactor('http://example.com')); + test( + 'Long key', + () => testSpecialCharactor( + String.fromCharCodes(Iterable.generate(256, (_) => 65 /* 'A' */)), + ), + ); + test( + 'Empty key & value', + () => testSpecialCharactor('', value: ''), + ); + + test('Only casing is differ', () async { + final target = createTarget(); + final options = createOptions(); + const key1 = 'KEY'; + const key2 = 'key'; + const value1 = 'Value1'; + const value2 = 'Value2'; + + await target.write(key: key1, value: value1, options: options); + await target.write(key: key2, value: value2, options: options); + final results = await target.readAll(options: options); + expect(results.length, 2); + expect(results[key1], value1); + expect(results[key2], value2); + + expect(await target.read(key: key1, options: options), value1); + expect(await target.read(key: key2, options: options), value2); + expect(await target.containsKey(key: key1, options: options), isTrue); + expect(await target.containsKey(key: key2, options: options), isTrue); + + await target.delete(key: key1, options: options); + expect(await target.read(key: key1, options: options), isNull); + expect(await target.read(key: key2, options: options), value2); + expect(await target.containsKey(key: key1, options: options), isFalse); + expect(await target.containsKey(key: key2, options: options), isTrue); + + await target.write(key: key2, value: value2, options: options); + await target.deleteAll(options: options); + expect(await target.read(key: key1, options: options), isNull); + expect(await target.read(key: key2, options: options), isNull); + expect(await target.containsKey(key: key1, options: options), isFalse); + expect(await target.containsKey(key: key2, options: options), isFalse); + }); + }); +} + +bool canTest() { + if (!Platform.isWindows) { + markTestSkipped('This test must be run on Windows.'); + return false; + } + + return true; +} + +FutureOr withFfi( + FutureOr Function() test, +) async { + if (!canTest()) { + return; + } + + await test(); +} diff --git a/third_party/flutter_secure_storage_windows/windows/CMakeLists.txt b/third_party/flutter_secure_storage_windows/windows/CMakeLists.txt new file mode 100644 index 00000000..61bd19da --- /dev/null +++ b/third_party/flutter_secure_storage_windows/windows/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.15) +set(PROJECT_NAME "flutter_secure_storage_windows") +project(${PROJECT_NAME} LANGUAGES CXX) + +# This value is used when generating builds using this plugin, so it must +# not be changed +set(PLUGIN_NAME "flutter_secure_storage_windows_plugin") + +add_library(${PLUGIN_NAME} SHARED + "flutter_secure_storage_windows_plugin.cpp" +) +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +# List of absolute paths to libraries that should be bundled with the plugin +set(flutter_secure_storage_bundled_libraries + "" + PARENT_SCOPE +) +if(NOT DEFINED STORAGE_PREFIX) + add_compile_definitions(SECURE_STORAGE_KEY_PREFIX="${BINARY_NAME}_VGhpcyBpcyB0aGUgcHJlZml4IGZv_") +else() + add_compile_definitions(SECURE_STORAGE_KEY_PREFIX="${STORAGE_PREFIX}_VGhpcyBpcyB0aGUgcHJlZml4IGZv_") +endif() \ No newline at end of file diff --git a/third_party/flutter_secure_storage_windows/windows/flutter_secure_storage_windows_plugin.cpp b/third_party/flutter_secure_storage_windows/windows/flutter_secure_storage_windows_plugin.cpp new file mode 100644 index 00000000..ccdb0f6f --- /dev/null +++ b/third_party/flutter_secure_storage_windows/windows/flutter_secure_storage_windows_plugin.cpp @@ -0,0 +1,941 @@ +#include "include/flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h" + +// This must be included before many other Windows headers. +#include +#include +#include +#include +#include +#include +#include + +// For getPlatformVersion; remove unless needed for your plugin implementation. +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "version.lib") +#pragma comment(lib, "bcrypt.lib") + +namespace +{ + + class FlutterSecureStorageWindowsPlugin : public flutter::Plugin + { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); + + FlutterSecureStorageWindowsPlugin(); + + virtual ~FlutterSecureStorageWindowsPlugin(); + + private: + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result); + + // Retrieves the value passed to the given param. + std::optional GetStringArg( + const std::string ¶m, + const flutter::EncodableMap *args); + + // Derive the key for a value given a method argument map. + std::optional FlutterSecureStorageWindowsPlugin::GetValueKey(const flutter::EncodableMap *args); + + // Removes prefix of the given storage key. + // + // The prefix (defined by ELEMENT_PREFERENCES_KEY_PREFIX) is added automatically when writing to storage, + // to distinguish values that are written by this plugin from values that are not. + std::string RemoveKeyPrefix(const std::string &key); + + // Gets the string name for the given int error code + std::string GetErrorString(const DWORD &error_code); + // Get string name of ntstatus + std::string NtStatusToString(const CHAR* operation, NTSTATUS status); + + DWORD GetApplicationSupportPath(std::wstring& path); + + std::wstring SanitizeDirString(std::wstring string); + + bool PathExists(const std::wstring& path); + + bool MakePath(const std::wstring& path); + + PBYTE GetEncryptionKey(); + + // Stores the given value under the given key. + void Write(const std::string &key, const std::string &val); + + std::optional Read(const std::string &key); + + flutter::EncodableMap ReadAll(); + + void Delete(const std::string &key); + + void DeleteAll(); + + bool ContainsKey(const std::string &key); + }; + + const std::string ELEMENT_PREFERENCES_KEY_PREFIX = SECURE_STORAGE_KEY_PREFIX; + const int ELEMENT_PREFERENCES_KEY_PREFIX_LENGTH = (sizeof SECURE_STORAGE_KEY_PREFIX) - 1; + + std::wstring Utf8ToWide(const std::string& value) { + if (value.empty()) { + return std::wstring(); + } + + const int size = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0); + if (size <= 0) { + throw GetLastError(); + } + + std::wstring wide(static_cast(size), L'\0'); + const int converted = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, wide.data(), size); + if (converted <= 0) { + throw GetLastError(); + } + wide.resize(static_cast(converted - 1)); + return wide; + } + + std::string WideToUtf8(const wchar_t* value) { + if (value == nullptr || value[0] == L'\0') { + return std::string(); + } + + const int size = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr); + if (size <= 0) { + throw GetLastError(); + } + + std::string utf8(static_cast(size), '\0'); + const int converted = WideCharToMultiByte(CP_UTF8, 0, value, -1, utf8.data(), size, nullptr, nullptr); + if (converted <= 0) { + throw GetLastError(); + } + utf8.resize(static_cast(converted - 1)); + return utf8; + } + + std::wstring CredentialFilter() { + return Utf8ToWide(ELEMENT_PREFERENCES_KEY_PREFIX + '*'); + } + + static inline void rtrim(std::wstring& s) { + s.erase(std::find_if(s.rbegin(), s.rend(), [](wchar_t ch) { + return !std::isspace(ch); + }).base(), s.end()); + } + + // static + void FlutterSecureStorageWindowsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows *registrar) + { + auto channel = + std::make_unique>( + registrar->messenger(), "plugins.it_nomads.com/flutter_secure_storage", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique(); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto &call, auto result) + { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); + } + + FlutterSecureStorageWindowsPlugin::FlutterSecureStorageWindowsPlugin() {} + + FlutterSecureStorageWindowsPlugin::~FlutterSecureStorageWindowsPlugin() {} + + std::optional FlutterSecureStorageWindowsPlugin::GetValueKey(const flutter::EncodableMap *args) + { + auto key = this->GetStringArg("key", args); + if (key.has_value()) + return ELEMENT_PREFERENCES_KEY_PREFIX + key.value(); + return std::nullopt; + } + + std::string FlutterSecureStorageWindowsPlugin::RemoveKeyPrefix(const std::string& key) + { + return key.substr(ELEMENT_PREFERENCES_KEY_PREFIX_LENGTH); + } + + std::optional FlutterSecureStorageWindowsPlugin::GetStringArg( + const std::string ¶m, + const flutter::EncodableMap *args) + { + auto p = args->find(param); + if (p == args->end()) + return std::nullopt; + return std::get(p->second); + } + + std::string FlutterSecureStorageWindowsPlugin::GetErrorString(const DWORD &error_code) + { + switch (error_code) + { + case ERROR_NO_SUCH_LOGON_SESSION: + return "ERROR_NO_SUCH_LOGIN_SESSION"; + case ERROR_INVALID_FLAGS: + return "ERROR_INVALID_FLAGS"; + case ERROR_BAD_USERNAME: + return "ERROR_BAD_USERNAME"; + case SCARD_E_NO_READERS_AVAILABLE: + return "SCARD_E_NO_READERS_AVAILABLE"; + case SCARD_E_NO_SMARTCARD: + return "SCARD_E_NO_SMARTCARD"; + case SCARD_W_REMOVED_CARD: + return "SCARD_W_REMOVED_CARD"; + case SCARD_W_WRONG_CHV: + return "SCARD_W_WRONG_CHV"; + case ERROR_INVALID_PARAMETER: + return "ERROR_INVALID_PARAMETER"; + default: + return "UNKNOWN_ERROR"; + } + } + + std::string FlutterSecureStorageWindowsPlugin::NtStatusToString(const CHAR* operation, NTSTATUS status) + { + std::ostringstream oss; + oss << operation << ", 0x" << std::hex << status; + + switch (status) + { + case 0xc0000000: + oss << " (STATUS_SUCCESS)"; + break; + case 0xC0000008: + oss << " (STATUS_INVALID_HANDLE)"; + break; + case 0xc000000d: + oss << " (STATUS_INVALID_PARAMETER)"; + break; + case 0xc00000bb: + oss << " (STATUS_NOT_SUPPORTED)"; + break; + case 0xC0000225: + oss << " (STATUS_NOT_FOUND)"; + break; + } + return oss.str(); + } + + DWORD FlutterSecureStorageWindowsPlugin::GetApplicationSupportPath(std::wstring &path) + { + std::wstring companyName; + std::wstring productName; + TCHAR nameBuffer[MAX_PATH + 1]{}; + char* infoBuffer; + DWORD versionInfoSize; + DWORD resVal; + UINT queryLen; + LPVOID queryVal; + LPWSTR appdataPath; + std::wostringstream stream; + + SHGetKnownFolderPath(FOLDERID_RoamingAppData,KF_FLAG_DEFAULT,NULL,&appdataPath); + + if (nameBuffer == NULL) { + return ERROR_OUTOFMEMORY; + } + + resVal = GetModuleFileName(NULL,nameBuffer,MAX_PATH); + if (resVal == 0) { + return GetLastError(); + } + + versionInfoSize = GetFileVersionInfoSize(nameBuffer, NULL); + if (versionInfoSize != 0) { + infoBuffer = (char*) calloc(versionInfoSize,sizeof(char)); + if (infoBuffer == NULL) { + return ERROR_OUTOFMEMORY; + } + if (GetFileVersionInfo(nameBuffer, 0, versionInfoSize, infoBuffer) == 0) { + free(infoBuffer); + infoBuffer = NULL; + } + else { + + if (VerQueryValue(infoBuffer, TEXT("\\StringFileInfo\\040904e4\\CompanyName"), &queryVal, &queryLen) != 0) { + companyName = SanitizeDirString(std::wstring((const TCHAR*)queryVal)); + } + else if (VerQueryValue(infoBuffer, TEXT("\\StringFileInfo\\040904b0\\CompanyName"), &queryVal, &queryLen) != 0) { + companyName = SanitizeDirString(std::wstring((const TCHAR*)queryVal)); + } + else { + companyName = L"placeholder_company"; + } + if (VerQueryValue(infoBuffer, TEXT("\\StringFileInfo\\040904e4\\ProductName"), &queryVal, &queryLen) != 0) { + productName = SanitizeDirString(std::wstring((const TCHAR*)queryVal)); + } + else if (VerQueryValue(infoBuffer, TEXT("\\StringFileInfo\\040904b0\\ProductName"), &queryVal, &queryLen) != 0) { + productName = SanitizeDirString(std::wstring((const TCHAR*)queryVal)); + } + else { + productName = L"placeholder_product"; + } + } + stream << appdataPath << "\\" << companyName << "\\" << productName; + path = stream.str(); + } + else { + return GetLastError(); + } + return ERROR_SUCCESS; + + } + + std::wstring FlutterSecureStorageWindowsPlugin::SanitizeDirString(std::wstring string) + { + std::wstring illegalChars = L"\\/:?\"<>|"; + for (auto it = string.begin(); it < string.end(); ++it) { + if (illegalChars.find(*it) != std::wstring::npos) { + *it = L'_'; + } + } + rtrim(string); + return string; + } + + bool FlutterSecureStorageWindowsPlugin::PathExists(const std::wstring& path) + { + struct _stat info; + if (_wstat(path.c_str(), &info) != 0) { + return false; + } + return (info.st_mode & _S_IFDIR) != 0; + } + + bool FlutterSecureStorageWindowsPlugin::MakePath(const std::wstring& path) + { + int ret = _wmkdir(path.c_str()); + if (ret == 0) { + return true; + } + switch (errno) { + case ENOENT: + { + size_t pos = path.find_last_of('/'); + if (pos == std::wstring::npos) + pos = path.find_last_of('\\'); + if (pos == std::wstring::npos) + return false; + if (!MakePath(path.substr(0, pos))) + return false; + } + return 0 == _wmkdir(path.c_str()); + case EEXIST: + return PathExists(path); + default: + return false; + } + } + + PBYTE FlutterSecureStorageWindowsPlugin::GetEncryptionKey() + { + const size_t KEY_SIZE = 16; + DWORD credError = 0; + PBYTE AesKey; + PCREDENTIALW pcred; + std::wstring target_name = Utf8ToWide("key_" + ELEMENT_PREFERENCES_KEY_PREFIX); + + AesKey = (PBYTE)HeapAlloc(GetProcessHeap(), 0, KEY_SIZE); + if (NULL == AesKey) { + return NULL; + } + + bool ok = CredReadW(target_name.c_str(), CRED_TYPE_GENERIC, 0, &pcred); + if (ok) { + if (pcred->CredentialBlobSize != KEY_SIZE) { + CredFree(pcred); + CredDeleteW(target_name.c_str(), CRED_TYPE_GENERIC, 0); + goto NewKey; + } + memcpy(AesKey, pcred->CredentialBlob, KEY_SIZE); + CredFree(pcred); + return AesKey; + } + credError = GetLastError(); + if (credError != ERROR_NOT_FOUND) { + return NULL; + } + NewKey: + if (BCryptGenRandom(NULL, AesKey, KEY_SIZE, BCRYPT_USE_SYSTEM_PREFERRED_RNG) != ERROR_SUCCESS) { + return NULL; + } + CREDENTIALW cred = { 0 }; + cred.Type = CRED_TYPE_GENERIC; + cred.TargetName = const_cast(target_name.c_str()); + cred.CredentialBlobSize = KEY_SIZE; + cred.CredentialBlob = AesKey; + cred.Persist = CRED_PERSIST_LOCAL_MACHINE; + + ok = CredWriteW(&cred, 0); + if (!ok) { + std::cerr << "Failed to write encryption key" << std::endl; + return NULL; + } + return AesKey; + } + + void FlutterSecureStorageWindowsPlugin::HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result) + { + auto method = method_call.method_name(); + const auto *args = std::get_if(method_call.arguments()); + std::wstring path; + if (GetApplicationSupportPath(path) != ERROR_SUCCESS) { + result->Error("Exception occurred", "GetApplicationSupportPath"); + return; + } + try + { + if (method == "write") + { + auto key = this->GetValueKey(args); + auto val = this->GetStringArg("value", args); + if (key.has_value()) + { + if (val.has_value()) + this->Write(key.value(), val.value()); + else + this->Delete(key.value()); + result->Success(); + } + else + { + result->Error("Exception occurred", "write"); + } + } + else if (method == "read") + { + auto key = this->GetValueKey(args); + if (key.has_value()) + { + auto val = this->Read(key.value()); + if (val.has_value()) + result->Success(flutter::EncodableValue(val.value())); + else + result->Success(); + } + else + { + result->Error("Exception occurred", "read"); + } + } + else if (method == "readAll") + { + auto creds = this->ReadAll(); + result->Success(flutter::EncodableValue(creds)); + } + else if (method == "delete") + { + auto key = this->GetValueKey(args); + if (key.has_value()) + { + this->Delete(key.value()); + result->Success(); + } + else + { + result->Error("Exception occurred", "delete"); + } + } + else if (method == "deleteAll") + { + this->DeleteAll(); + result->Success(); + } + else if (method == "containsKey") + { + auto key = this->GetValueKey(args); + if (key.has_value()) + { + auto contains_key = this->ContainsKey(key.value()); + result->Success(flutter::EncodableValue(contains_key)); + } + else + { + result->Error("Exception occurred", "containsKey"); + } + } + else + { + result->NotImplemented(); + } + } + catch (DWORD e) + { + auto str_code = this->GetErrorString(e); + result->Error("Exception encountered: " + str_code, method); + } + } + + void FlutterSecureStorageWindowsPlugin::Write(const std::string &key, const std::string &val) + { + //The recommended size for AES-GCM IV is 12 bytes + const DWORD NONCE_SIZE = 12; + const DWORD KEY_SIZE = 16; + + NTSTATUS status; + BCRYPT_ALG_HANDLE algo = NULL; + BCRYPT_KEY_HANDLE keyHandle = NULL; + DWORD bytesWritten = 0, + ciphertextSize = 0; + PBYTE ciphertext = NULL, + iv = (PBYTE)HeapAlloc(GetProcessHeap(), 0, NONCE_SIZE), + encryptionKey = GetEncryptionKey(); + BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo{}; + BCRYPT_AUTH_TAG_LENGTHS_STRUCT authTagLengths{}; + std::basic_ofstream fs; + std::wstring appSupportPath; + std::string error; + + if (iv == NULL) { + error = "IV HeapAlloc Failed"; + goto err; + } + if (encryptionKey == NULL) { + error = "encryptionKey is NULL"; + goto err; + } + status = BCryptOpenAlgorithmProvider(&algo, BCRYPT_AES_ALGORITHM, NULL, 0); + if (!BCRYPT_SUCCESS(status)) { + error = NtStatusToString("BCryptOpenAlgorithmProvider", status); + goto err; + } + status = BCryptSetProperty(algo, BCRYPT_CHAINING_MODE, (PUCHAR)BCRYPT_CHAIN_MODE_GCM, sizeof(BCRYPT_CHAIN_MODE_GCM), 0); + if (!BCRYPT_SUCCESS(status)) { + error = NtStatusToString("BCryptSetProperty", status); + goto err; + } + status = BCryptGetProperty(algo, BCRYPT_AUTH_TAG_LENGTH, (PBYTE)&authTagLengths, sizeof(BCRYPT_AUTH_TAG_LENGTHS_STRUCT), &bytesWritten, 0); + if (!BCRYPT_SUCCESS(status)) { + error = NtStatusToString("BCryptGetProperty", status); + goto err; + } + BCRYPT_INIT_AUTH_MODE_INFO(authInfo); + authInfo.pbNonce = (PUCHAR)HeapAlloc(GetProcessHeap(), 0, NONCE_SIZE); + if (authInfo.pbNonce == NULL) { + error = "pbNonce HeapAlloc Failed"; + goto err; + } + authInfo.cbNonce = NONCE_SIZE; + status = BCryptGenRandom(NULL, iv, authInfo.cbNonce, BCRYPT_USE_SYSTEM_PREFERRED_RNG); + if (!BCRYPT_SUCCESS(status)) { + error = NtStatusToString("BCryptGenRandom", status); + goto err; + } + //copy the original IV into the authInfo, we can't write the IV directly into the authInfo because it will change after calling BCryptEncrypt and we still need to write the IV to file + memcpy(authInfo.pbNonce, iv, authInfo.cbNonce); + //We do not use additional authenticated data + authInfo.pbAuthData = NULL; + authInfo.cbAuthData = 0; + //Make space for the authentication tag + authInfo.pbTag = (PUCHAR)HeapAlloc(GetProcessHeap(), 0, authTagLengths.dwMaxLength); + if (authInfo.pbTag == NULL) { + error = "pbTag HeapAlloc Failed"; + goto err; + } + authInfo.cbTag = authTagLengths.dwMaxLength; + status = BCryptGenerateSymmetricKey(algo, &keyHandle, NULL, 0, encryptionKey, KEY_SIZE, 0); + if (!BCRYPT_SUCCESS(status)) { + error = NtStatusToString("BCryptGenerateSymmetricKey", status); + goto err; + } + //First call to BCryptEncrypt to get size of ciphertext + status = BCryptEncrypt(keyHandle, (PUCHAR)val.c_str(), (ULONG)val.length() + 1, (PVOID)&authInfo, NULL, 0, NULL, 0, &bytesWritten, 0); + if (!BCRYPT_SUCCESS(status)) { + error = NtStatusToString("BCryptEncrypt1", status); + goto err; + } + ciphertextSize = bytesWritten; + ciphertext = (PBYTE)HeapAlloc(GetProcessHeap(), 0, ciphertextSize); + if (ciphertext == NULL) { + error = "CipherText HeapAlloc failed"; + goto err; + } + //Actual encryption + status = BCryptEncrypt(keyHandle, (PUCHAR)val.c_str(), (ULONG)val.length() + 1, (PVOID)&authInfo, NULL, 0, ciphertext, ciphertextSize, &bytesWritten, 0); + if (!BCRYPT_SUCCESS(status)) { + error = NtStatusToString("BCryptEncrypt2", status); + goto err; + } + GetApplicationSupportPath(appSupportPath); + if (!PathExists(appSupportPath)) { + MakePath(appSupportPath); + } + fs = std::basic_ofstream(appSupportPath + L"\\" + std::wstring(key.begin(), key.end()) + L".secure", std::ios::binary | std::ios::trunc); + if (!fs) { + error = "Failed to open output stream"; + goto err; + } + fs.write(iv, NONCE_SIZE); + fs.write(authInfo.pbTag, authInfo.cbTag); + fs.write(ciphertext, ciphertextSize); + fs.close(); + HeapFree(GetProcessHeap(), 0, iv); + HeapFree(GetProcessHeap(), 0, encryptionKey); + HeapFree(GetProcessHeap(), 0, authInfo.pbNonce); + HeapFree(GetProcessHeap(), 0, authInfo.pbTag); + HeapFree(GetProcessHeap(), 0, ciphertext); + return; + err: + if (iv) { + HeapFree(GetProcessHeap(), 0, iv); + } + if (encryptionKey) { + HeapFree(GetProcessHeap(), 0, encryptionKey); + } + if (authInfo.pbNonce) { + HeapFree(GetProcessHeap(), 0, authInfo.pbNonce); + } + if (authInfo.pbTag) { + HeapFree(GetProcessHeap(), 0, authInfo.pbTag); + } + if (ciphertext) { + HeapFree(GetProcessHeap(), 0, ciphertext); + } + throw std::runtime_error(error); + } + + std::optional FlutterSecureStorageWindowsPlugin::Read(const std::string &key) + { + const DWORD NONCE_SIZE = 12; + const DWORD KEY_SIZE = 16; + + NTSTATUS status; + BCRYPT_ALG_HANDLE algo = NULL; + BCRYPT_KEY_HANDLE keyHandle = NULL; + BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo{}; + BCRYPT_AUTH_TAG_LENGTHS_STRUCT authTagLengths{}; + + PBYTE encryptionKey = GetEncryptionKey(), + ciphertext = NULL, + fileBuffer = NULL, + plaintext = NULL; + DWORD plaintextSize = 0, + bytesWritten = 0, + ciphertextSize = 0; + std::wstring appSupportPath; + std::basic_ifstream fs; + std::streampos fileSize; + std::optional returnVal = std::nullopt; + + if (encryptionKey == NULL) { + std::cerr << "encryptionKey is NULL" << std::endl; + goto cleanup; + } + GetApplicationSupportPath(appSupportPath); + if (!PathExists(appSupportPath)) { + MakePath(appSupportPath); + } + //Read full file into a buffer + fs = std::basic_ifstream(appSupportPath + L"\\" + std::wstring(key.begin(), key.end()) + L".secure", std::ios::binary); + if (!fs.good()) { + //Backwards comp. + PCREDENTIALW pcred; + std::wstring target_name = Utf8ToWide(key); + bool ok = CredReadW(target_name.c_str(), CRED_TYPE_GENERIC, 0, &pcred); + if (ok) + { + auto val = std::string((char*)pcred->CredentialBlob); + CredFree(pcred); + returnVal = val; + } + goto cleanup; + } + fs.unsetf(std::ios::skipws); + fs.seekg(0, std::ios::end); + fileSize = fs.tellg(); + fs.seekg(0, std::ios::beg); + fileBuffer = (PBYTE)HeapAlloc(GetProcessHeap(), 0, fileSize); + if (NULL == fileBuffer) { + std::cerr << "fileBuffer HeapAlloc failed" << std::endl; + goto cleanup; + } + fs.read(fileBuffer, fileSize); + fs.close(); + + status = BCryptOpenAlgorithmProvider(&algo, BCRYPT_AES_ALGORITHM, NULL, 0); + if (!BCRYPT_SUCCESS(status)) { + std::cerr << NtStatusToString("BCryptOpenAlgorithmProvider", status) << std::endl; + goto cleanup; + } + status = BCryptSetProperty(algo, BCRYPT_CHAINING_MODE, (PUCHAR)BCRYPT_CHAIN_MODE_GCM, sizeof(BCRYPT_CHAIN_MODE_GCM), 0); + if (!BCRYPT_SUCCESS(status)) { + std::cerr << NtStatusToString("BCryptOpenAlgorithmProvider", status) << std::endl; + goto cleanup; + } + status = BCryptGetProperty(algo, BCRYPT_AUTH_TAG_LENGTH, (PBYTE)&authTagLengths, sizeof(BCRYPT_AUTH_TAG_LENGTHS_STRUCT), &bytesWritten, 0); + if (!BCRYPT_SUCCESS(status)) { + std::cerr << NtStatusToString("BCryptGetProperty", status) << std::endl; + goto cleanup; + } + + BCRYPT_INIT_AUTH_MODE_INFO(authInfo); + authInfo.pbNonce = (PUCHAR)HeapAlloc(GetProcessHeap(), 0, NONCE_SIZE); + if (authInfo.pbNonce == NULL) { + std::cerr << "pbNonce HeapAlloc Failed" << std::endl; + goto cleanup; + } + authInfo.cbNonce = NONCE_SIZE; + //Check if file is at least long enough for iv and authentication tag + if (fileSize <= static_cast(NONCE_SIZE) + authTagLengths.dwMaxLength) { + std::cerr << "File is too small" << std::endl; + goto cleanup; + } + authInfo.pbTag = (PUCHAR)HeapAlloc(GetProcessHeap(), 0, authTagLengths.dwMaxLength); + if (authInfo.pbTag == NULL) { + std::cerr << "pbTag HeapAlloc Failed" << std::endl; + goto cleanup; + } + ciphertextSize = (DWORD)fileSize - NONCE_SIZE - authTagLengths.dwMaxLength; + ciphertext = (PBYTE)HeapAlloc(GetProcessHeap(), 0, ciphertextSize); + if (ciphertext == NULL) { + std::cerr << "ciphertext HeapAlloc failed" << std::endl; + goto cleanup; + } + //Copy different parts needed for decryption from filebuffer +#pragma warning(push) +#pragma warning(disable:6385) + memcpy(authInfo.pbNonce, fileBuffer, NONCE_SIZE); +#pragma warning(pop) + memcpy(authInfo.pbTag, &fileBuffer[NONCE_SIZE], authTagLengths.dwMaxLength); + memcpy(ciphertext, &fileBuffer[NONCE_SIZE + authTagLengths.dwMaxLength], ciphertextSize); + authInfo.cbTag = authTagLengths.dwMaxLength; + + status = BCryptGenerateSymmetricKey(algo, &keyHandle, NULL, 0, encryptionKey, KEY_SIZE, 0); + if (!BCRYPT_SUCCESS(status)) { + std::cerr << NtStatusToString("BCryptGenerateSymmetricKey", status) << std::endl; + goto cleanup; + } + //First call is to determine size of plaintext + status = BCryptDecrypt(keyHandle, ciphertext, ciphertextSize, (PVOID)&authInfo, NULL, 0, NULL, 0, &bytesWritten, 0); + if (!BCRYPT_SUCCESS(status)) { + std::cerr << NtStatusToString("BCryptDecrypt1", status) << std::endl; + goto cleanup; + } + plaintextSize = bytesWritten; + plaintext = (PBYTE)HeapAlloc(GetProcessHeap(), 0, plaintextSize); + if (NULL == plaintext) { + std::cerr << "plaintext HeapAlloc failed" << std::endl; + goto cleanup; + } + //Actuual decryption + status = BCryptDecrypt(keyHandle, ciphertext, ciphertextSize, (PVOID)&authInfo, NULL, 0, plaintext, plaintextSize, &bytesWritten, 0); + if (!BCRYPT_SUCCESS(status)) { + std::cerr << NtStatusToString("BCryptDecrypt2", status) << std::endl; + goto cleanup; + } + returnVal = (char*)plaintext; + cleanup: + if (encryptionKey) { + HeapFree(GetProcessHeap(), 0, encryptionKey); + } + if (ciphertext) { + HeapFree(GetProcessHeap(), 0, ciphertext); + } + if (plaintext) { + HeapFree(GetProcessHeap(), 0, plaintext); + } + if (fileBuffer) { + HeapFree(GetProcessHeap(), 0, fileBuffer); + } + if (authInfo.pbNonce) { + HeapFree(GetProcessHeap(), 0, authInfo.pbNonce); + } + if (authInfo.pbTag) { + HeapFree(GetProcessHeap(), 0, authInfo.pbTag); + } + return returnVal; + } + + flutter::EncodableMap FlutterSecureStorageWindowsPlugin::ReadAll() + { + WIN32_FIND_DATA searchRes; + HANDLE hFile; + std::wstring appSupportPath; + + GetApplicationSupportPath(appSupportPath); + if (!PathExists(appSupportPath)) { + MakePath(appSupportPath); + } + hFile = FindFirstFile((appSupportPath + L"\\*.secure").c_str(), &searchRes); + if (hFile == INVALID_HANDLE_VALUE) { + return flutter::EncodableMap(); + } + + flutter::EncodableMap creds; + + do { + std::wstring fileName(searchRes.cFileName); + size_t pos = fileName.find(L".secure"); + fileName.erase(pos, 7); + char* out = new char[fileName.length() + 1]; + size_t charsConverted = 0; + wcstombs_s(&charsConverted, out, fileName.length() + 1, fileName.c_str(), fileName.length() + 1); + std::optional val = this->Read(out); + auto key = this->RemoveKeyPrefix(out); + if (val.has_value()) { + creds[key] = val.value(); + continue; + } + } while (FindNextFile(hFile, &searchRes) != 0); + + //Backwards comp. + PCREDENTIALW* pcreds; + DWORD cred_count = 0; + + std::wstring credential_filter = CredentialFilter(); + bool ok = CredEnumerateW(credential_filter.c_str(), 0, &cred_count, &pcreds); + if (!ok) + { + return creds; + } + for (DWORD i = 0; i < cred_count; i++) + { + auto pcred = pcreds[i]; + std::string target_name = WideToUtf8(pcred->TargetName); + auto val = std::string((char*)pcred->CredentialBlob); + auto key = this->RemoveKeyPrefix(target_name); + //If the key exists then data was already read from a file, which implies that the data read from the credential system is outdated + if (creds.find(key) == creds.end()) { + creds[key] = val; + } + } + + CredFree(pcreds); + + return creds; + } + + void FlutterSecureStorageWindowsPlugin::Delete(const std::string &key) + { + std::wstring appSupportPath; + GetApplicationSupportPath(appSupportPath); + auto wstr = std::wstring(key.begin(), key.end()); + BOOL ok = DeleteFile((appSupportPath + L"\\" + wstr + L".secure").c_str()); + if (!ok) { + DWORD error = GetLastError(); + if (error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) { + throw error; + } + } + + //Backwards comp. + ok = CredDeleteW(wstr.c_str(), CRED_TYPE_GENERIC, 0); + if (!ok) + { + auto error = GetLastError(); + + // Silently ignore if we try to delete a key that doesn't exist + if (error == ERROR_NOT_FOUND) + return; + + throw error; + } + } + + void FlutterSecureStorageWindowsPlugin::DeleteAll() + { + + WIN32_FIND_DATA searchRes; + HANDLE hFile; + std::wstring appSupportPath; + + GetApplicationSupportPath(appSupportPath); + if (!PathExists(appSupportPath)) { + MakePath(appSupportPath); + } + hFile = FindFirstFile((appSupportPath + L"\\*.secure").c_str(), &searchRes); + if (hFile == INVALID_HANDLE_VALUE) { + return; + } + do { + std::wstring fileName(searchRes.cFileName); + BOOL ok = DeleteFile((appSupportPath + L"\\" + fileName).c_str()); + if (!ok) { + DWORD error = GetLastError(); + if (error != ERROR_FILE_NOT_FOUND) { + throw error; + } + } + } while (FindNextFile(hFile, &searchRes) != 0); + + //Backwards comp. + PCREDENTIALW* pcreds; + DWORD cred_count = 0; + + std::wstring credential_filter = CredentialFilter(); + bool read_ok = CredEnumerateW(credential_filter.c_str(), 0, &cred_count, &pcreds); + if (!read_ok) + { + auto error = GetLastError(); + if (error == ERROR_NOT_FOUND) + // No credentials to delete + return; + throw error; + } + + for (DWORD i = 0; i < cred_count; i++) + { + auto pcred = pcreds[i]; + auto target_name = pcred->TargetName; + + bool delete_ok = CredDeleteW(target_name, CRED_TYPE_GENERIC, 0); + if (!delete_ok) + { + throw GetLastError(); + } + } + + CredFree(pcreds); + } + + bool FlutterSecureStorageWindowsPlugin::ContainsKey(const std::string &key) + { + std::wstring appSupportPath; + GetApplicationSupportPath(appSupportPath); + std::wstring wstr = std::wstring(key.begin(), key.end()); + if (INVALID_FILE_ATTRIBUTES == GetFileAttributes((appSupportPath + L"\\" + wstr + L".secure").c_str())) { + //Backwards comp. + PCREDENTIALW pcred; + std::wstring target_name = Utf8ToWide(key); + bool ok = CredReadW(target_name.c_str(), CRED_TYPE_GENERIC, 0, &pcred); + if (ok) return true; + + auto error = GetLastError(); + if (error == ERROR_NOT_FOUND) + return false; + throw error; + } + return true; + } +} // namespace + +void FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) +{ + FlutterSecureStorageWindowsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/third_party/flutter_secure_storage_windows/windows/include/flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h b/third_party/flutter_secure_storage_windows/windows/include/flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h new file mode 100644 index 00000000..555971db --- /dev/null +++ b/third_party/flutter_secure_storage_windows/windows/include/flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_FLUTTER_SECURE_STORAGE_WINDOWS_PLUGIN_H_ +#define FLUTTER_PLUGIN_FLUTTER_SECURE_STORAGE_WINDOWS_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_FLUTTER_SECURE_STORAGE_WINDOWS_PLUGIN_H_