mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 08:22:16 +03:00
support for FIDO, conditional messages
This commit is contained in:
parent
afaab491b8
commit
34f78d2518
@ -352,6 +352,9 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
contextManager?.let {
|
||||
try {
|
||||
if (device is NfcYubiKeyDevice) {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_STARTED)
|
||||
}
|
||||
it.processYubiKey(device)
|
||||
if (device is NfcYubiKeyDevice) {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_FINISHED)
|
||||
@ -389,11 +392,11 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
flutterLog = FlutterLog(messenger)
|
||||
deviceManager = DeviceManager(this, viewModel)
|
||||
appMethodChannel = AppMethodChannel(messenger)
|
||||
deviceManager = DeviceManager(this, viewModel,appMethodChannel)
|
||||
appContext = AppContext(messenger, this.lifecycleScope, viewModel)
|
||||
dialogManager = DialogManager(messenger, this.lifecycleScope)
|
||||
appPreferences = AppPreferences(this)
|
||||
appMethodChannel = AppMethodChannel(messenger)
|
||||
appLinkMethodChannel = AppLinkMethodChannel(messenger)
|
||||
managementHandler = ManagementHandler(messenger, deviceManager, dialogManager)
|
||||
|
||||
@ -441,7 +444,6 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
oathViewModel,
|
||||
dialogManager,
|
||||
appPreferences,
|
||||
appMethodChannel,
|
||||
nfcActivityListener
|
||||
)
|
||||
|
||||
|
@ -20,8 +20,10 @@ import androidx.collection.ArraySet
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import com.yubico.authenticator.MainActivity
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.OperationContext
|
||||
import com.yubico.authenticator.yubikit.NfcActivityState
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.core.smartcard.scp.ScpKeyParams
|
||||
@ -41,7 +43,8 @@ interface DeviceListener {
|
||||
|
||||
class DeviceManager(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val appViewModel: MainViewModel
|
||||
private val appViewModel: MainViewModel,
|
||||
private val appMethodChannel: MainActivity.AppMethodChannel
|
||||
) {
|
||||
var clearDeviceInfoOnDisconnect: Boolean = true
|
||||
|
||||
@ -179,8 +182,19 @@ class DeviceManager(
|
||||
onUsb(it)
|
||||
}
|
||||
|
||||
suspend fun <T> withKey(onNfc: suspend () -> T, onUsb: suspend (UsbYubiKeyDevice) -> T) =
|
||||
suspend fun <T> withKey(
|
||||
onNfc: suspend () -> com.yubico.yubikit.core.util.Result<T, Throwable>,
|
||||
onUsb: suspend (UsbYubiKeyDevice) -> T
|
||||
): T =
|
||||
appViewModel.connectedYubiKey.value?.let {
|
||||
onUsb(it)
|
||||
} ?: onNfc()
|
||||
} ?: try {
|
||||
onNfc().value.also {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_FINISHED)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_INTERRUPTED)
|
||||
throw e
|
||||
}
|
||||
|
||||
}
|
@ -60,7 +60,7 @@ class FidoConnectionHelper(
|
||||
block(YubiKitFidoSession(it))
|
||||
}
|
||||
|
||||
suspend fun <T> useSessionNfc(block: (YubiKitFidoSession) -> T): T {
|
||||
suspend fun <T> useSessionNfc(block: (YubiKitFidoSession) -> T): Result<T, Throwable> {
|
||||
try {
|
||||
val result = suspendCoroutine { outer ->
|
||||
pendingAction = {
|
||||
@ -74,11 +74,11 @@ class FidoConnectionHelper(
|
||||
pendingAction = null
|
||||
}
|
||||
}
|
||||
return result
|
||||
return Result.success(result!!)
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
return Result.failure(cancelled)
|
||||
} catch (error: Throwable) {
|
||||
throw error
|
||||
return Result.failure(error)
|
||||
} finally {
|
||||
dialogManager.closeDialog()
|
||||
}
|
||||
|
@ -35,12 +35,9 @@ class ManagementConnectionHelper(
|
||||
) {
|
||||
private var action: ManagementAction? = null
|
||||
|
||||
suspend fun <T> useSession(
|
||||
actionDescription: ManagementActionDescription,
|
||||
action: (YubiKitManagementSession) -> T
|
||||
): T {
|
||||
suspend fun <T> useSession(action: (YubiKitManagementSession) -> T): T {
|
||||
return deviceManager.withKey(
|
||||
onNfc = { useSessionNfc(actionDescription, action) },
|
||||
onNfc = { useSessionNfc(action) },
|
||||
onUsb = { useSessionUsb(it, action) })
|
||||
}
|
||||
|
||||
@ -51,28 +48,25 @@ class ManagementConnectionHelper(
|
||||
block(YubiKitManagementSession(it))
|
||||
}
|
||||
|
||||
private suspend fun <T> useSessionNfc(
|
||||
actionDescription: ManagementActionDescription,
|
||||
block: (YubiKitManagementSession) -> T
|
||||
): T {
|
||||
private suspend fun <T> useSessionNfc(block: (YubiKitManagementSession) -> T): Result<T, Throwable> {
|
||||
try {
|
||||
val result = suspendCoroutine { outer ->
|
||||
val result = suspendCoroutine<T> { outer ->
|
||||
action = {
|
||||
outer.resumeWith(runCatching {
|
||||
block.invoke(it.value)
|
||||
})
|
||||
}
|
||||
dialogManager.showDialog {
|
||||
logger.debug("Cancelled Dialog {}", actionDescription.name)
|
||||
logger.debug("Cancelled Dialog")
|
||||
action?.invoke(Result.failure(CancellationException()))
|
||||
action = null
|
||||
}
|
||||
}
|
||||
return result
|
||||
return Result.success(result!!)
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
return Result.failure(cancelled)
|
||||
} catch (error: Throwable) {
|
||||
throw error
|
||||
return Result.failure(error)
|
||||
} finally {
|
||||
dialogManager.closeDialog()
|
||||
}
|
||||
|
@ -27,15 +27,6 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
const val dialogDescriptionManagementIndex = 300
|
||||
|
||||
enum class ManagementActionDescription(private val value: Int) {
|
||||
DeviceReset(0), ActionFailure(1);
|
||||
|
||||
val id: Int
|
||||
get() = value + dialogDescriptionManagementIndex
|
||||
}
|
||||
|
||||
class ManagementHandler(
|
||||
messenger: BinaryMessenger,
|
||||
deviceManager: DeviceManager,
|
||||
@ -58,7 +49,7 @@ class ManagementHandler(
|
||||
}
|
||||
|
||||
private suspend fun deviceReset(): String =
|
||||
connectionHelper.useSession(ManagementActionDescription.DeviceReset) { managementSession ->
|
||||
connectionHelper.useSession { managementSession ->
|
||||
managementSession.deviceReset()
|
||||
NULL
|
||||
}
|
||||
|
@ -80,7 +80,6 @@ class OathManager(
|
||||
private val oathViewModel: OathViewModel,
|
||||
private val dialogManager: DialogManager,
|
||||
private val appPreferences: AppPreferences,
|
||||
private val appMethodChannel: MainActivity.AppMethodChannel,
|
||||
private val nfcActivityListener: NfcActivityListener
|
||||
) : AppContextManager(), DeviceListener {
|
||||
|
||||
@ -221,10 +220,6 @@ class OathManager(
|
||||
|
||||
override suspend fun processYubiKey(device: YubiKeyDevice) {
|
||||
try {
|
||||
if (device is NfcYubiKeyDevice) {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_STARTED)
|
||||
}
|
||||
|
||||
device.withConnection<SmartCardConnection, Unit> { connection ->
|
||||
val session = getOathSession(connection)
|
||||
val previousId = oathViewModel.currentSession()?.deviceId
|
||||
@ -310,7 +305,6 @@ class OathManager(
|
||||
deviceManager.setDeviceInfo(getDeviceInfo(device))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.PROCESSING_INTERRUPTED)
|
||||
// OATH not enabled/supported, try to get DeviceInfo over other USB interfaces
|
||||
logger.error("Failed to connect to CCID: ", e)
|
||||
// Clear any cached OATH state
|
||||
@ -346,7 +340,7 @@ class OathManager(
|
||||
|
||||
logger.debug("Added cred {}", credential)
|
||||
jsonSerializer.encodeToString(addedCred)
|
||||
}
|
||||
}.value
|
||||
}
|
||||
|
||||
private suspend fun addAccountsToAny(
|
||||
@ -725,7 +719,7 @@ class OathManager(
|
||||
|
||||
private suspend fun <T> useOathSessionNfc(
|
||||
block: (YubiKitOathSession) -> T
|
||||
): T {
|
||||
): Result<T, Throwable> {
|
||||
var firstShow = true
|
||||
while (true) { // loop until success or cancel
|
||||
try {
|
||||
@ -749,14 +743,12 @@ class OathManager(
|
||||
// here the coroutine is suspended and waits till pendingAction is
|
||||
// invoked - the pending action result will resume this coroutine
|
||||
}
|
||||
nfcActivityListener.onChange(NfcActivityState.PROCESSING_FINISHED)
|
||||
return result
|
||||
return Result.success(result!!)
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
return Result.failure(cancelled)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Exception during action: ", e)
|
||||
nfcActivityListener.onChange(NfcActivityState.PROCESSING_INTERRUPTED)
|
||||
throw e
|
||||
return Result.failure(e)
|
||||
}
|
||||
} // while
|
||||
}
|
||||
|
@ -109,8 +109,13 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
|
||||
try {
|
||||
await oath.setPassword(current, password);
|
||||
return true;
|
||||
} on PlatformException catch (e) {
|
||||
_log.debug('Calling set password failed with exception: $e');
|
||||
} on PlatformException catch (pe) {
|
||||
final decoded = pe.decode();
|
||||
if (decoded is CancellationException) {
|
||||
_log.debug('Set password cancelled');
|
||||
throw decoded;
|
||||
}
|
||||
_log.debug('Calling set password failed with exception: $pe');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -120,8 +125,13 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
|
||||
try {
|
||||
await oath.unsetPassword(current);
|
||||
return true;
|
||||
} on PlatformException catch (e) {
|
||||
_log.debug('Calling unset password failed with exception: $e');
|
||||
} on PlatformException catch (pe) {
|
||||
final decoded = pe.decode();
|
||||
if (decoded is CancellationException) {
|
||||
_log.debug('Unset password cancelled');
|
||||
throw decoded;
|
||||
}
|
||||
_log.debug('Calling unset password failed with exception: $pe');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../app/message.dart';
|
||||
import '../app/state.dart';
|
||||
import 'state.dart';
|
||||
import 'views/nfc/models.dart';
|
||||
@ -80,7 +81,9 @@ class _DialogProvider extends Notifier<int> {
|
||||
case NfcActivity.processingFinished:
|
||||
explicitAction = false; // next action might not be explicit
|
||||
processingTimer?.cancel();
|
||||
if (properties.showSuccess ?? false) {
|
||||
final showSuccess = properties.showSuccess ?? false;
|
||||
allowMessages = !showSuccess;
|
||||
if (showSuccess) {
|
||||
notifier.sendCommand(
|
||||
updateNfcView(NfcActivityClosingCountdownWidgetView(
|
||||
closeInSec: 5,
|
||||
|
@ -97,9 +97,7 @@ class _NfcActivityClosingCountdownWidgetViewState
|
||||
}
|
||||
|
||||
void hideNow() {
|
||||
debugPrint('XXX closing because have to!');
|
||||
ref.read(nfcEventCommandNotifier.notifier).sendCommand(
|
||||
NfcEventCommand(event: const NfcHideViewEvent(timeoutMs: 0)));
|
||||
ref.read(nfcEventCommandNotifier.notifier).sendCommand(hideNfcView);
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,10 +121,10 @@ class _NfcViewNotifier extends Notifier<NfcView> {
|
||||
bool? showSuccess,
|
||||
bool? showCloseButton}) {
|
||||
state = state.copyWith(
|
||||
operationSuccess: operationSuccess,
|
||||
operationFailure: operationFailure,
|
||||
showSuccess: showSuccess,
|
||||
showCloseButton: showCloseButton);
|
||||
operationSuccess: operationSuccess ?? state.operationSuccess,
|
||||
operationFailure: operationFailure ?? state.operationFailure,
|
||||
showSuccess: showSuccess ?? state.showSuccess,
|
||||
showCloseButton: showCloseButton ?? state.showCloseButton);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,12 +21,17 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../widgets/toast.dart';
|
||||
|
||||
var allowMessages = true;
|
||||
|
||||
void Function() showMessage(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
Duration duration = const Duration(seconds: 2),
|
||||
}) =>
|
||||
showToast(context, message, duration: duration);
|
||||
}) {
|
||||
return allowMessages
|
||||
? showToast(context, message, duration: duration)
|
||||
: () {};
|
||||
}
|
||||
|
||||
Future<T?> showBlurDialog<T>({
|
||||
required BuildContext context,
|
||||
|
@ -442,6 +442,12 @@
|
||||
"s_rename_account": "Konto umbenennen",
|
||||
"l_rename_account_desc": "Bearbeiten Sie den Aussteller/Namen des Kontos",
|
||||
"s_account_renamed": "Konto umbenannt",
|
||||
"l_rename_account_failed": null,
|
||||
"@l_rename_account_failed": {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"p_rename_will_change_account_displayed": "Das ändert die Anzeige dieses Kontos in der Liste.",
|
||||
"s_delete_account": "Konto löschen",
|
||||
"l_delete_account_desc": "Löschen Sie das Konto von Ihrem YubiKey",
|
||||
|
@ -442,6 +442,12 @@
|
||||
"s_rename_account": "Rename account",
|
||||
"l_rename_account_desc": "Edit the issuer/name of the account",
|
||||
"s_account_renamed": "Account renamed",
|
||||
"l_rename_account_failed": "Failed renaming account: {message}",
|
||||
"@l_rename_account_failed": {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"p_rename_will_change_account_displayed": "This will change how the account is displayed in the list.",
|
||||
"s_delete_account": "Delete account",
|
||||
"l_delete_account_desc": "Remove the account from your YubiKey",
|
||||
|
@ -442,6 +442,12 @@
|
||||
"s_rename_account": "Renommer compte",
|
||||
"l_rename_account_desc": "Modifier émetteur/nom du compte",
|
||||
"s_account_renamed": "Compte renommé",
|
||||
"l_rename_account_failed": null,
|
||||
"@l_rename_account_failed": {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"p_rename_will_change_account_displayed": "Cela modifiera l'affichage du compte dans la liste.",
|
||||
"s_delete_account": "Supprimer compte",
|
||||
"l_delete_account_desc": "Supprimer le compte de votre YubiKey",
|
||||
|
@ -442,6 +442,12 @@
|
||||
"s_rename_account": "アカウント名を変更",
|
||||
"l_rename_account_desc": "アカウントの発行者/名前を編集",
|
||||
"s_account_renamed": "アカウントの名前が変更されました",
|
||||
"l_rename_account_failed": null,
|
||||
"@l_rename_account_failed": {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"p_rename_will_change_account_displayed": "これにより、リスト内のアカウントの表示が変更されます。",
|
||||
"s_delete_account": "アカウントを削除",
|
||||
"l_delete_account_desc": "YubiKeyからアカウントを削除",
|
||||
|
@ -442,6 +442,12 @@
|
||||
"s_rename_account": "Zmień nazwę konta",
|
||||
"l_rename_account_desc": "Edytuj wydawcę/nazwę konta",
|
||||
"s_account_renamed": "Zmieniono nazwę konta",
|
||||
"l_rename_account_failed": null,
|
||||
"@l_rename_account_failed": {
|
||||
"placeholders": {
|
||||
"message": {}
|
||||
}
|
||||
},
|
||||
"p_rename_will_change_account_displayed": "Spowoduje to zmianę sposobu wyświetlania konta na liście.",
|
||||
"s_delete_account": "Usuń konto",
|
||||
"l_delete_account_desc": "Usuń konto z klucza YubiKey",
|
||||
|
@ -92,7 +92,7 @@ class RenameAccountDialog extends ConsumerStatefulWidget {
|
||||
} on CancellationException catch (_) {
|
||||
// ignored
|
||||
} catch (e) {
|
||||
_log.error('Failed to add account', e);
|
||||
_log.error('Failed to rename account', e);
|
||||
final String errorMessage;
|
||||
// TODO: Make this cleaner than importing desktop specific RpcError.
|
||||
if (e is RpcError) {
|
||||
@ -103,7 +103,7 @@ class RenameAccountDialog extends ConsumerStatefulWidget {
|
||||
await withContext((context) async => showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!
|
||||
.l_account_add_failed(errorMessage),
|
||||
.l_rename_account_failed(errorMessage),
|
||||
duration: const Duration(seconds: 4),
|
||||
));
|
||||
return null;
|
||||
|
Loading…
Reference in New Issue
Block a user