Skip to content

Feature/add has biometrics capabilities checking #52

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Flutter Locker🔒
# Flutter Locker🔒

<p align="center">
<img src="https://raw.githubusercontent.com/infinum/flutter-plugins-locker/master/Locker-github.jpg" width="700" max-width="50%" alt="Locker"/>
Expand Down Expand Up @@ -32,21 +32,26 @@ RetrieveSecretRequest(
iOsPrompt: IOsPrompt(touchIdText: 'description'),
)
```

## Usage

```dart
FlutterLocker.supportsBiometricAuthentication();
```
Checks if the devices has biometric capabilities no matter if they are configured and turned on.

```dart
FlutterLocker.canAuthenticate();
```
Checks if the devices has biometric features.
Checks if the devices has biometric capabilities that are configured and turned on.

```dart
await FlutterLocker.save(SaveSecretRequest(
key: 'key',
secret: 'secret',
androidPrompt: AndroidPrompt(title: 'Authenticate', cancelLabel: 'Cancel'),
));
```
```
Saves the secret. On Android prompt is shown, while on iOS there is no need for the prompt when saving.

```dart
Expand All @@ -65,16 +70,16 @@ Deletes the secret.

## Exceptions

For common exceptions, a `LockerException` is thrown.
For common exceptions, a `LockerException` is thrown.

Use `LockerException.reason` to find out what went wrong:
Use `LockerException.reason` to find out what went wrong:

- `secretNotFound` - Happens when you try to retrieve a secret that was never saved for that key
- `authenticationCanceled` - User canceled the authentication prompt
- `authenticationFailed` - User failed authentication, e.g. by too many wrong attempts

For other exception, a `PlatformException` is thrown. You can use `PlatformException.message` to get more info.

## Notes

- iOS only: app will not show authentication dialog when saving (authentication will always succeed)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v22.7.2), do not edit directly.
// Autogenerated from Pigeon (v22.7.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")

Expand Down Expand Up @@ -208,6 +208,7 @@ private open class FlutterLockerPigeonCodec : StandardMessageCodec() {

/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface FlutterLockerHostApi {
fun supportsBiometricAuthentication(callback: (Result<Boolean>) -> Unit)
fun canAuthenticate(callback: (Result<Boolean>) -> Unit)
fun save(request: SaveSecretRequest, callback: (Result<Unit>) -> Unit)
fun retrieve(request: RetrieveSecretRequest, callback: (Result<String>) -> Unit)
Expand All @@ -222,6 +223,24 @@ interface FlutterLockerHostApi {
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: FlutterLockerHostApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_locker.FlutterLockerHostApi.supportsBiometricAuthentication$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.supportsBiometricAuthentication{ result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_locker.FlutterLockerHostApi.canAuthenticate$separatedMessageChannelSuffix", codec)
if (api != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class FlutterLockerPlugin : FlutterPlugin, ActivityAware, FlutterLockerHostApi {
const val AUTHENTICATION_FAILED = "authenticationFailed"
}

override fun supportsBiometricAuthentication(callback: (Result<Boolean>) -> Unit) {
callback(Result.success(goldfinger.hasBiometricHardware(authenticators)))
}

override fun canAuthenticate(callback: (Result<Boolean>) -> Unit) {
callback(Result.success(goldfinger.canAuthenticate(authenticators)))
}
Expand Down
18 changes: 17 additions & 1 deletion ios/flutter_locker/Sources/flutter_locker/FlutterLocker.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v22.7.2), do not edit directly.
// Autogenerated from Pigeon (v22.7.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon

import Foundation
Expand Down Expand Up @@ -235,6 +235,7 @@ class FlutterLockerPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable

/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol FlutterLockerHostApi {
func supportsBiometricAuthentication(completion: @escaping (Result<Bool, Error>) -> Void)
func canAuthenticate(completion: @escaping (Result<Bool, Error>) -> Void)
func save(request: SaveSecretRequest, completion: @escaping (Result<Void, Error>) -> Void)
func retrieve(request: RetrieveSecretRequest, completion: @escaping (Result<String, Error>) -> Void)
Expand All @@ -247,6 +248,21 @@ class FlutterLockerHostApiSetup {
/// Sets up an instance of `FlutterLockerHostApi` to handle messages through the `binaryMessenger`.
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: FlutterLockerHostApi?, messageChannelSuffix: String = "") {
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
let supportsBiometricAuthenticationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_locker.FlutterLockerHostApi.supportsBiometricAuthentication\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
supportsBiometricAuthenticationChannel.setMessageHandler { _, reply in
api.supportsBiometricAuthentication { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
supportsBiometricAuthenticationChannel.setMessageHandler(nil)
}
let canAuthenticateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_locker.FlutterLockerHostApi.canAuthenticate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
canAuthenticateChannel.setMessageHandler { _, reply in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,27 @@ import Locker
extension FlutterError: Error {}

public class FlutterLockerPlugin: NSObject, FlutterLockerHostApi, FlutterPlugin {

func supportsBiometricAuthentication(completion: @escaping (Result<Bool, Error>) -> Void) {
let supportedBiometrics = Locker.supportedBiometricsAuthentication
completion(.success(supportedBiometrics != BiometricsType.none))
}

func canAuthenticate(completion: @escaping (Result<Bool, Error>) -> Void) {
let supportedBiometrics = Locker.configuredBiometricsAuthentication
completion(.success(supportedBiometrics != BiometricsType.none ? true : false))
let configuredBiometrics = Locker.configuredBiometricsAuthentication
completion(.success(configuredBiometrics != BiometricsType.none))
}

func save(request: SaveSecretRequest, completion: @escaping (Result<Void, Error>) -> Void) {
Locker.setSecret(request.secret, for: request.key)
// This can never fail
Locker.setShouldUseAuthenticationWithBiometrics(true, for: request.key)
completion(.success(Void()))
Locker.setSecret(request.secret, for: request.key) { error in
if let error {
completion(.failure(error))
} else {
completion(.success(()))
}
}
}

func retrieve(request: RetrieveSecretRequest, completion: @escaping (Result<String, Error>) -> Void) {
Expand Down
29 changes: 28 additions & 1 deletion lib/gen/locker_api.gen.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v22.7.2), do not edit directly.
// Autogenerated from Pigeon (v22.7.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers

Expand Down Expand Up @@ -193,6 +193,33 @@ class FlutterLockerHostApi {

final String pigeonVar_messageChannelSuffix;

Future<bool> supportsBiometricAuthentication() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_locker.FlutterLockerHostApi.supportsBiometricAuthentication$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_channel.send(null) as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}

Future<bool> canAuthenticate() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_locker.FlutterLockerHostApi.canAuthenticate$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
Expand Down
5 changes: 5 additions & 0 deletions lib/src/flutter_locker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ class FlutterLocker {

static final FlutterLockerHostApi _hostApi = FlutterLockerHostApi();

/// Checks if the devices has biometric features.
static Future<bool> supportsBiometricAuthentication() {
return _hostApi.supportsBiometricAuthentication();
}

/// Checks if the devices has biometric features.
static Future<bool> canAuthenticate() {
return _hostApi.canAuthenticate();
Expand Down
6 changes: 4 additions & 2 deletions pigeons/locker_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(PigeonOptions(
dartOut: 'lib/gen/locker_api.gen.dart',
swiftOut: 'ios/flutter_locker/Sources/flutter_locker/FlutterLocker.swift',
kotlinOut:
'android/src/main/kotlin/com/infinum/flutter_locker/FlutterLocker.kt',
kotlinOut: 'android/src/main/kotlin/com/infinum/flutter_locker/FlutterLocker.kt',
kotlinOptions: KotlinOptions(
package: 'com.infinum.flutter_locker',
),
Expand Down Expand Up @@ -54,6 +53,9 @@ class IOsPrompt {

@HostApi()
abstract class FlutterLockerHostApi {
@async
bool supportsBiometricAuthentication();

@async
bool canAuthenticate();

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: flutter_locker
description: Secures your secrets in keychain using biometric authentication (Fingerprint, Touch ID, Face ID...)
version: 2.1.6
version: 2.2.0
repository: https://github.com/infinum/flutter-plugins-locker
homepage: https://github.com/infinum/flutter-plugins-locker

Expand Down