mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +03:00
453 lines
15 KiB
Dart
453 lines
15 KiB
Dart
/*
|
|
* Copyright (C) 2024 Yubico.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:logging/logging.dart';
|
|
|
|
import '../../app/logging.dart';
|
|
import '../../app/message.dart';
|
|
import '../../app/models.dart';
|
|
import '../../app/state.dart';
|
|
import '../../desktop/models.dart';
|
|
import '../../exception/cancellation_exception.dart';
|
|
import '../../exception/no_data_exception.dart';
|
|
import '../../exception/platform_exception_decoder.dart';
|
|
import '../../fido/models.dart';
|
|
import '../../fido/state.dart';
|
|
import '../method_channel_notifier.dart';
|
|
|
|
final _log = Logger('android.fido.state');
|
|
|
|
final androidFidoStateProvider = AsyncNotifierProvider.autoDispose
|
|
.family<FidoStateNotifier, FidoState, DevicePath>(_FidoStateNotifier.new);
|
|
|
|
class _FidoStateNotifier extends FidoStateNotifier {
|
|
final _events = const EventChannel('android.fido.sessionState');
|
|
late StreamSubscription _sub;
|
|
late final _FidoMethodChannelNotifier fido =
|
|
ref.read(_fidoMethodsProvider.notifier);
|
|
|
|
@override
|
|
FutureOr<FidoState> build(DevicePath devicePath) async {
|
|
_sub = _events.receiveBroadcastStream().listen((event) {
|
|
final json = jsonDecode(event);
|
|
if (json == null) {
|
|
state = AsyncValue.error(const NoDataException(), StackTrace.current);
|
|
} else if (json == 'loading') {
|
|
state = const AsyncValue.loading();
|
|
} else {
|
|
final fidoState = FidoState.fromJson(json);
|
|
state = AsyncValue.data(fidoState);
|
|
}
|
|
}, onError: (err, stackTrace) {
|
|
state = AsyncValue.error(err, stackTrace);
|
|
});
|
|
|
|
ref.onDispose(_sub.cancel);
|
|
|
|
return Completer<FidoState>().future;
|
|
}
|
|
|
|
@override
|
|
Stream<InteractionEvent> reset() {
|
|
final controller = StreamController<InteractionEvent>();
|
|
const resetEvents = EventChannel('android.fido.reset');
|
|
|
|
final subscription =
|
|
resetEvents.receiveBroadcastStream().skip(1).listen((event) {
|
|
if (event is String && event.isNotEmpty) {
|
|
controller.sink.add(
|
|
InteractionEvent.values.firstWhere((e) => '"${e.name}"' == event));
|
|
}
|
|
});
|
|
|
|
controller.onCancel = () async {
|
|
await fido.cancelReset();
|
|
if (!controller.isClosed) {
|
|
await subscription.cancel();
|
|
}
|
|
};
|
|
|
|
controller.onListen = () async {
|
|
try {
|
|
await fido.reset();
|
|
await controller.sink.close();
|
|
ref.invalidateSelf();
|
|
} catch (e) {
|
|
_log.debug('Error during reset: \'$e\'');
|
|
controller.sink.addError(e);
|
|
}
|
|
};
|
|
|
|
return controller.stream;
|
|
}
|
|
|
|
@override
|
|
Future<PinResult> setPin(String newPin, {String? oldPin}) async {
|
|
try {
|
|
final response = jsonDecode(await fido.setPin(newPin, oldPin: oldPin));
|
|
if (response['success'] == true) {
|
|
_log.debug('FIDO PIN set/change successful');
|
|
return PinResult.success();
|
|
}
|
|
|
|
if (response['pinViolation'] == true) {
|
|
_log.debug('FIDO PIN violation');
|
|
return PinResult.failed(const FidoPinFailureReason.weakPin());
|
|
}
|
|
|
|
_log.debug('FIDO PIN set/change failed');
|
|
return PinResult.failed(FidoPinFailureReason.invalidPin(
|
|
response['pinRetries'], response['authBlocked']));
|
|
} on PlatformException catch (pe) {
|
|
var decodedException = pe.decode();
|
|
if (decodedException is CancellationException) {
|
|
_log.debug('User cancelled set/change FIDO PIN operation');
|
|
}
|
|
throw decodedException;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<PinResult> unlock(String pin) async {
|
|
try {
|
|
final response = jsonDecode(await fido.unlock(pin));
|
|
|
|
if (response['success'] == true) {
|
|
_log.debug('FIDO applet unlocked');
|
|
return PinResult.success();
|
|
}
|
|
|
|
_log.debug('FIDO applet unlock failed');
|
|
return PinResult.failed(FidoPinFailureReason.invalidPin(
|
|
response['pinRetries'], response['authBlocked']));
|
|
} on PlatformException catch (pe) {
|
|
var decodedException = pe.decode();
|
|
if (decodedException is! CancellationException) {
|
|
// non pin failure
|
|
// simulate cancellation but show an error
|
|
await ref.read(withContextProvider)((context) async => showMessage(
|
|
context, ref.watch(l10nProvider).p_operation_failed_try_again));
|
|
throw CancellationException();
|
|
}
|
|
|
|
_log.debug('User cancelled unlock FIDO operation');
|
|
throw decodedException;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> enableEnterpriseAttestation() async {
|
|
try {
|
|
final response = jsonDecode(await fido.enableEnterpriseAttestation());
|
|
|
|
if (response['success'] == true) {
|
|
_log.debug('Enterprise attestation enabled');
|
|
}
|
|
} on PlatformException catch (pe) {
|
|
var decodedException = pe.decode();
|
|
if (decodedException is CancellationException) {
|
|
_log.debug('User cancelled unlock FIDO operation');
|
|
throw decodedException;
|
|
}
|
|
|
|
_log.debug(
|
|
'Platform exception during enable enterprise attestation: $pe');
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|
|
|
|
final androidFingerprintProvider = AsyncNotifierProvider.autoDispose
|
|
.family<FidoFingerprintsNotifier, List<Fingerprint>, DevicePath>(
|
|
_FidoFingerprintsNotifier.new);
|
|
|
|
class _FidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
|
final _events = const EventChannel('android.fido.fingerprints');
|
|
late StreamSubscription _sub;
|
|
late final _FidoMethodChannelNotifier fido =
|
|
ref.read(_fidoMethodsProvider.notifier);
|
|
|
|
@override
|
|
FutureOr<List<Fingerprint>> build(DevicePath devicePath) async {
|
|
_sub = _events.receiveBroadcastStream().listen((event) {
|
|
final json = jsonDecode(event);
|
|
if (json == null) {
|
|
state = const AsyncValue.loading();
|
|
} else {
|
|
List<Fingerprint> newState = List.from((json as List)
|
|
.map((e) => Fingerprint.fromJson(e))
|
|
.sortedBy<String>((f) => f.label.toLowerCase())
|
|
.toList());
|
|
state = AsyncValue.data(newState);
|
|
}
|
|
}, onError: (err, stackTrace) {
|
|
state = AsyncValue.error(err, stackTrace);
|
|
});
|
|
|
|
ref.onDispose(_sub.cancel);
|
|
return Completer<List<Fingerprint>>().future;
|
|
}
|
|
|
|
@override
|
|
Stream<FingerprintEvent> registerFingerprint({String? name}) {
|
|
final controller = StreamController<FingerprintEvent>();
|
|
const registerEvents = EventChannel('android.fido.registerFp');
|
|
|
|
final registerFpSub =
|
|
registerEvents.receiveBroadcastStream().skip(1).listen((event) {
|
|
if (controller.isClosed) {
|
|
_log.debug('Controller already closed, ignoring: $event');
|
|
}
|
|
_log.debug('Received register fingerprint event: $event');
|
|
if (event is String && event.isNotEmpty) {
|
|
final e = jsonDecode(event);
|
|
_log.debug('Received register fingerprint event: $e');
|
|
|
|
final status = e['status'];
|
|
|
|
controller.sink.add(switch (status) {
|
|
'capture' => FingerprintEvent.capture(e['remaining']),
|
|
'capture-error' => FingerprintEvent.error(e['code']),
|
|
final other => throw UnimplementedError(other)
|
|
});
|
|
}
|
|
});
|
|
|
|
controller.onCancel = () async {
|
|
if (!controller.isClosed) {
|
|
_log.debug('Cancelling fingerprint registration');
|
|
await fido.cancelFingerprintRegistration();
|
|
await registerFpSub.cancel();
|
|
}
|
|
};
|
|
|
|
controller.onListen = () async {
|
|
try {
|
|
final registerFpResult = await fido.registerFingerprint(name);
|
|
|
|
_log.debug('Finished registerFingerprint with: $registerFpResult');
|
|
|
|
final resultJson = jsonDecode(registerFpResult);
|
|
|
|
if (resultJson['success'] == true) {
|
|
controller.sink
|
|
.add(FingerprintEvent.complete(Fingerprint.fromJson(resultJson)));
|
|
} else {
|
|
// TODO abstract platform errors
|
|
final errorStatus = resultJson['status'];
|
|
if (errorStatus != 'user-cancelled') {
|
|
throw RpcError(errorStatus, 'Platform error: $errorStatus', {});
|
|
}
|
|
}
|
|
} on PlatformException catch (pe) {
|
|
_log.debug('Received platform exception: \'$pe\'');
|
|
final decoded = pe.decode();
|
|
controller.sink.addError(decoded);
|
|
} catch (e) {
|
|
_log.debug('Received error: \'$e\'');
|
|
controller.sink.addError(e);
|
|
} finally {
|
|
await controller.sink.close();
|
|
}
|
|
};
|
|
|
|
return controller.stream;
|
|
}
|
|
|
|
@override
|
|
Future<Fingerprint> renameFingerprint(
|
|
Fingerprint fingerprint, String name) async {
|
|
try {
|
|
final renameFingerprintResponse =
|
|
jsonDecode(await fido.renameFingerprint(fingerprint, name));
|
|
|
|
if (renameFingerprintResponse['success'] == true) {
|
|
_log.debug('FIDO rename fingerprint succeeded');
|
|
return Fingerprint(fingerprint.templateId, name);
|
|
} else {
|
|
_log.debug('FIDO rename fingerprint failed');
|
|
return fingerprint;
|
|
}
|
|
} on PlatformException catch (pe) {
|
|
var decodedException = pe.decode();
|
|
if (decodedException is CancellationException) {
|
|
_log.debug('User cancelled rename fingerprint FIDO operation');
|
|
} else {
|
|
_log.error('Rename fingerprint FIDO operation failed.', pe);
|
|
}
|
|
|
|
throw decodedException;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> deleteFingerprint(Fingerprint fingerprint) async {
|
|
try {
|
|
final deleteFingerprintResponse =
|
|
jsonDecode(await fido.deleteFingerprint(fingerprint));
|
|
|
|
if (deleteFingerprintResponse['success'] == true) {
|
|
_log.debug('FIDO delete fingerprint succeeded');
|
|
} else {
|
|
_log.debug('FIDO delete fingerprint failed');
|
|
}
|
|
} on PlatformException catch (pe) {
|
|
var decodedException = pe.decode();
|
|
if (decodedException is CancellationException) {
|
|
_log.debug('User cancelled delete fingerprint FIDO operation');
|
|
} else {
|
|
_log.error('Delete fingerprint FIDO operation failed.', pe);
|
|
}
|
|
|
|
throw decodedException;
|
|
}
|
|
}
|
|
}
|
|
|
|
final androidCredentialProvider = AsyncNotifierProvider.autoDispose
|
|
.family<FidoCredentialsNotifier, List<FidoCredential>, DevicePath>(
|
|
_FidoCredentialsNotifier.new);
|
|
|
|
class _FidoCredentialsNotifier extends FidoCredentialsNotifier {
|
|
final _events = const EventChannel('android.fido.credentials');
|
|
late StreamSubscription _sub;
|
|
late final _FidoMethodChannelNotifier fido =
|
|
ref.read(_fidoMethodsProvider.notifier);
|
|
|
|
@override
|
|
FutureOr<List<FidoCredential>> build(DevicePath devicePath) async {
|
|
_sub = _events.receiveBroadcastStream().listen((event) {
|
|
final json = jsonDecode(event);
|
|
if (json == null) {
|
|
state = const AsyncValue.loading();
|
|
} else {
|
|
List<FidoCredential> newState = List.from(
|
|
(json as List).map((e) => FidoCredential.fromJson(e)).toList());
|
|
state = AsyncValue.data(newState);
|
|
}
|
|
}, onError: (err, stackTrace) {
|
|
state = AsyncValue.error(err, stackTrace);
|
|
});
|
|
|
|
ref.onDispose(_sub.cancel);
|
|
return Completer<List<FidoCredential>>().future;
|
|
}
|
|
|
|
@override
|
|
Future<void> deleteCredential(FidoCredential credential) async {
|
|
try {
|
|
await fido.deleteCredential(credential);
|
|
} on PlatformException catch (pe) {
|
|
var decodedException = pe.decode();
|
|
if (decodedException is CancellationException) {
|
|
_log.debug('User cancelled delete credential FIDO operation');
|
|
} else {
|
|
throw decodedException;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final _fidoMethodsProvider = NotifierProvider<_FidoMethodChannelNotifier, void>(
|
|
() => _FidoMethodChannelNotifier());
|
|
|
|
class _FidoMethodChannelNotifier extends MethodChannelNotifier {
|
|
_FidoMethodChannelNotifier()
|
|
: super(const MethodChannel('android.fido.methods'));
|
|
late final l10n = ref.read(l10nProvider);
|
|
|
|
@override
|
|
void build() {}
|
|
|
|
Future<dynamic> deleteCredential(FidoCredential credential) async =>
|
|
invoke('deleteCredential', {
|
|
'callArgs': {
|
|
'rpId': credential.rpId,
|
|
'credentialId': credential.credentialId
|
|
},
|
|
'operationName': l10n.c_nfc_fido_delete_passkey,
|
|
'operationProcessing': l10n.s_nfc_fido_delete_passkey_processing,
|
|
'operationSuccess': l10n.s_passkey_deleted,
|
|
'operationFailure': l10n.s_nfc_fido_delete_passkey_failure,
|
|
'showSuccess': true
|
|
});
|
|
|
|
Future<dynamic> cancelReset() async => invoke('cancelReset');
|
|
|
|
Future<dynamic> reset() async => invoke('reset', {
|
|
'operationName': l10n.c_nfc_fido_reset,
|
|
'operationProcessing': l10n.s_nfc_fido_reset_processing,
|
|
'operationSuccess': l10n.s_nfc_fido_reset_success,
|
|
'operationFailure': l10n.s_nfc_fido_reset_failure,
|
|
'showSuccess': true
|
|
});
|
|
|
|
Future<dynamic> setPin(String newPin, {String? oldPin}) async =>
|
|
invoke('setPin', {
|
|
'callArgs': {'pin': oldPin, 'newPin': newPin},
|
|
'operationName': oldPin != null
|
|
? l10n.c_nfc_fido_change_pin
|
|
: l10n.c_nfc_fido_set_pin,
|
|
'operationProcessing': oldPin != null
|
|
? l10n.s_nfc_fido_change_pin_processing
|
|
: l10n.s_nfc_fido_set_pin_processing,
|
|
'operationSuccess': oldPin != null
|
|
? l10n.s_nfc_fido_change_pin_success
|
|
: l10n.s_pin_set,
|
|
'operationFailure': oldPin != null
|
|
? l10n.s_nfc_fido_change_pin_failure
|
|
: l10n.s_nfc_fido_set_pin_failure,
|
|
'showSuccess': true
|
|
});
|
|
|
|
Future<dynamic> unlock(String pin) async => invoke('unlock', {
|
|
'callArgs': {'pin': pin},
|
|
'operationName': l10n.s_unlock,
|
|
'operationProcessing': l10n.s_nfc_unlock_processing,
|
|
'operationSuccess': l10n.s_nfc_unlock_success,
|
|
'operationFailure': l10n.s_nfc_unlock_failure,
|
|
'showSuccess': true
|
|
});
|
|
|
|
Future<dynamic> enableEnterpriseAttestation() async =>
|
|
invoke('enableEnterpriseAttestation');
|
|
|
|
Future<dynamic> registerFingerprint(String? name) async =>
|
|
invoke('registerFingerprint', {
|
|
'callArgs': {'name': name}
|
|
});
|
|
|
|
Future<dynamic> cancelFingerprintRegistration() async =>
|
|
invoke('cancelRegisterFingerprint');
|
|
|
|
Future<dynamic> renameFingerprint(
|
|
Fingerprint fingerprint, String name) async =>
|
|
invoke('renameFingerprint', {
|
|
'callArgs': {'templateId': fingerprint.templateId, 'name': name},
|
|
});
|
|
|
|
Future<dynamic> deleteFingerprint(Fingerprint fingerprint) async =>
|
|
invoke('deleteFingerprint', {
|
|
'callArgs': {'templateId': fingerprint.templateId},
|
|
});
|
|
}
|