Skip to content

Feature - Check availability of protected data on iOS and macOS #629

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ _Note_ KeyStore was introduced in Android 4.3 (API level 18). The plugin wouldn'
## Platform Implementation
Please note that this table represents the functions implemented in this repository and it is possible that changes haven't yet been released on pub.dev

| | read | write | delete | containsKey | readAll | deleteAll |
|---------|--------------------|--------------------|--------------------|--------------------|--------------------|--------------------|
| Android | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| iOS | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Windows | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Linux | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| macOS | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Web | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | read | write | delete | containsKey | readAll | deleteAll | isCupertinoProtectedDataAvailable | onCupertinoProtectedDataAvailabilityChanged |
| ------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | --------------------------------- | ------------------------------------------- |
| Android | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
| iOS | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Windows | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
| Linux | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
| macOS | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: (on macOS 12 and newer) |
| Web | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | |

## Getting Started

Expand Down
14 changes: 13 additions & 1 deletion flutter_secure_storage/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class ItemsWidget extends StatefulWidget {
ItemsWidgetState createState() => ItemsWidgetState();
}

enum _Actions { deleteAll }
enum _Actions { deleteAll, isProtectedDataAvailable }

enum _ItemActions { delete, edit, containsKey, read }

Expand Down Expand Up @@ -56,6 +56,10 @@ class ItemsWidgetState extends State<ItemsWidget> {
_readAll();
}

Future<void> _isProtectedDataAvailable() async {
await _storage.isCupertinoProtectedDataAvailable();
}

Future<void> _addNewItem() async {
final String key = _randomValue();
final String value = _randomValue();
Expand Down Expand Up @@ -99,6 +103,9 @@ class ItemsWidgetState extends State<ItemsWidget> {
case _Actions.deleteAll:
_deleteAll();
break;
case _Actions.isProtectedDataAvailable:
_isProtectedDataAvailable();
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<_Actions>>[
Expand All @@ -107,6 +114,11 @@ class ItemsWidgetState extends State<ItemsWidget> {
value: _Actions.deleteAll,
child: Text('Delete all'),
),
const PopupMenuItem(
key: Key('is_protected_data_available'),
value: _Actions.isProtectedDataAvailable,
child: Text('IsProtectedDataAvailable'),
),
],
),
],
Expand Down
11 changes: 11 additions & 0 deletions flutter_secure_storage/example/test_driver/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ void main() {
await pageObject.rowHasTitle('Row 0', 0);
await pageObject.deleteRow(0);
await pageObject.hasNoRow(0);

await Future.delayed(const Duration(seconds: 1));

await pageObject.isProtectedDataAvailable();
},
timeout: const Timeout(Duration(seconds: 120)),
);
Expand All @@ -61,6 +65,8 @@ class HomePageObject {
final _addRandomButtonFinder = find.byValueKey('add_random');
final _deleteAllButtonFinder = find.byValueKey('delete_all');
final _popUpMenuButtonFinder = find.byValueKey('popup_menu');
final _isProtectedDataAvailableButtonFinder =
find.byValueKey('is_protected_data_available');

Future deleteAll() async {
await driver.tap(_popUpMenuButtonFinder);
Expand Down Expand Up @@ -96,4 +102,9 @@ class HomePageObject {
Future hasNoRow(int index) async {
await driver.waitForAbsent(find.byValueKey('title_row_$index'));
}

Future<void> isProtectedDataAvailable() async {
await driver.tap(_popUpMenuButtonFinder);
await driver.tap(_isProtectedDataAvailableButtonFinder);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@
//

import Flutter
import UIKit

public class SwiftFlutterSecureStoragePlugin: NSObject, FlutterPlugin {
public class SwiftFlutterSecureStoragePlugin: NSObject, FlutterPlugin, FlutterStreamHandler {

private let flutterSecureStorageManager: FlutterSecureStorage = FlutterSecureStorage()

private var secStoreAvailabilitySink: FlutterEventSink?

public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "plugins.it_nomads.com/flutter_secure_storage", binaryMessenger: registrar.messenger())
let eventChannel = FlutterEventChannel(name: "plugins.it_nomads.com/flutter_secure_storage/events", binaryMessenger: registrar.messenger())
let instance = SwiftFlutterSecureStoragePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
registrar.addApplicationDelegate(instance)
eventChannel.setStreamHandler(instance)
}

public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
Expand All @@ -39,11 +44,43 @@ public class SwiftFlutterSecureStoragePlugin: NSObject, FlutterPlugin {
self.readAll(call, handleResult)
case "containsKey":
self.containsKey(call, handleResult)
case "isProtectedDataAvailable":
// UIApplication is not thread safe
DispatchQueue.main.async {
result(UIApplication.shared.isProtectedDataAvailable)
}
default:
handleResult(FlutterMethodNotImplemented)
}
}
}

public func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) {
guard let sink = secStoreAvailabilitySink else {
return
}

sink(true)
}

public func applicationProtectedDataWillBecomeUnavailable(_ application: UIApplication) {
guard let sink = secStoreAvailabilitySink else {
return
}

sink(false)
}

public func onListen(withArguments arguments: Any?,
eventSink: @escaping FlutterEventSink) -> FlutterError? {
self.secStoreAvailabilitySink = eventSink
return nil
}

public func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.secStoreAvailabilitySink = nil
return nil
}

private func read(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let values = parseCall(call)
Expand Down
16 changes: 16 additions & 0 deletions flutter_secure_storage/lib/flutter_secure_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,22 @@ class FlutterSecureStorage {
}
}

/// iOS only feature
///
/// On all unsupported platforms returns an stream emitting `true` once
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
_platform.onCupertinoProtectedDataAvailabilityChanged;

/// iOS and macOS only feature.
///
/// On macOS this is only avaible on macOS 12 or newer. On older versions always returns true.
/// On all unsupported platforms returns true
///
/// iOS: https://developer.apple.com/documentation/uikit/uiapplication/1622925-isprotecteddataavailable
/// macOS: https://developer.apple.com/documentation/appkit/nsapplication/3752992-isprotecteddataavailable
Future<bool> isCupertinoProtectedDataAvailable() =>
_platform.isCupertinoProtectedDataAvailable();

/// Initializes the shared preferences with mock values for testing.
@visibleForTesting
static void setMockInitialValues(Map<String, String> values) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,11 @@ class TestFlutterSecureStoragePlatform extends FlutterSecureStoragePlatform {
required Map<String, String> options,
}) async =>
data[key] = value;

@override
Future<bool> isCupertinoProtectedDataAvailable() => Future.value(true);

@override
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
Stream.value(true);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ public class FlutterSecureStoragePlugin: NSObject, FlutterPlugin {
readAll(call, result)
case "containsKey":
containsKey(call, result)
case "isProtectedDataAvailable":
// NSApplication is not thread safe
DispatchQueue.main.async {
if #available(macOS 12.0, *) {
result(NSApplication.shared.isProtectedDataAvailable)
} else {
result(true)
}
}
default:
result(FlutterMethodNotImplemented)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
library flutter_secure_storage_platform_interface;

import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
Expand Down Expand Up @@ -29,6 +31,10 @@ abstract class FlutterSecureStoragePlatform extends PlatformInterface {
_instance = instance;
}

Stream<bool> get onCupertinoProtectedDataAvailabilityChanged;

Future<bool> isCupertinoProtectedDataAvailable();

Future<void> write({
required String key,
required String value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,25 @@ part of '../flutter_secure_storage_platform_interface.dart';
const MethodChannel _channel =
MethodChannel('plugins.it_nomads.com/flutter_secure_storage');

const EventChannel _eventChannel =
EventChannel('plugins.it_nomads.com/flutter_secure_storage/events');

class MethodChannelFlutterSecureStorage extends FlutterSecureStoragePlatform {
@override
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged => _eventChannel
.receiveBroadcastStream()
.where((event) => event is bool)
.map((event) => event as bool);

@override
Future<bool> isCupertinoProtectedDataAvailable() async {
if (!kIsWeb && Platform.isIOS) {
(await _channel.invokeMethod<bool>('isProtectedDataAvailable'))!;
}

return Future.value(true);
}

@override
Future<bool> containsKey({
required String key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,11 @@ class ExtendsFlutterSecureStoragePlatform extends FlutterSecureStoragePlatform {
required Map<String, String> options,
}) =>
Future<void>.value();

@override
Future<bool> isCupertinoProtectedDataAvailable() => Future.value(true);

@override
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
Stream.value(true);
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ void main() {

if (methodCall.method == 'containsKey') {
return true;
} else if (methodCall.method == 'isProtectedDataAvailable') {
return true;
}

return null;
Expand Down Expand Up @@ -170,5 +172,11 @@ void main() {
],
);
});

test('isProtectedDataAvailable', () async {
final result = await storage.isCupertinoProtectedDataAvailable();

expect(result, true);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform {
_backwardCompatible.delete(key: key, options: options);
}
}

@override
Future<bool> isCupertinoProtectedDataAvailable() => Future.value(true);

@override
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
Stream.value(true);
}

@visibleForTesting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,46 @@ class FlutterSecureStorageWindows extends FlutterSecureStoragePlatform {
}

@override
Future<bool> containsKey(
{required String key, required Map<String, String> options,}) =>
Future<bool> containsKey({
required String key,
required Map<String, String> options,
}) =>
Future.value(false);

@override
Future<void> delete(
{required String key, required Map<String, String> options,}) =>
Future<void> delete({
required String key,
required Map<String, String> options,
}) =>
Future.value();

@override
Future<void> deleteAll({required Map<String, String> options}) =>
Future.value();

@override
Future<String?> read(
{required String key, required Map<String, String> options,}) =>
Future<String?> read({
required String key,
required Map<String, String> options,
}) =>
Future.value();

@override
Future<Map<String, String>> readAll({required Map<String, String> options}) =>
Future.value({});

@override
Future<void> write(
{required String key,
required String value,
required Map<String, String> options,}) =>
Future<void> write({
required String key,
required String value,
required Map<String, String> options,
}) =>
Future.value();

@override
Future<bool> isCupertinoProtectedDataAvailable() => Future.value(true);

@override
Stream<bool> get onCupertinoProtectedDataAvailabilityChanged =>
Stream.value(true);
}