diff --git a/lib/android/oath/state.dart b/lib/android/oath/state.dart index 5fe0c5dd..57f0071a 100755 --- a/lib/android/oath/state.dart +++ b/lib/android/oath/state.dart @@ -26,8 +26,8 @@ import '../../app/logging.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../app/views/user_interaction.dart'; -import '../../cancellation_exception.dart'; import '../../core/models.dart'; +import '../../exception/platform_exception_decoder.dart'; import '../../oath/models.dart'; import '../../oath/state.dart'; @@ -136,11 +136,8 @@ final addCredentialToAnyProvider = var result = jsonDecode(resultString); return OathCredential.fromJson(result['credential']); } on PlatformException catch (pe) { - if (CancellationException.isCancellation(pe)) { - throw CancellationException(); - } _log.error('Failed to add account.', pe); - rethrow; + throw pe.decode(); } }); @@ -216,10 +213,7 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier { _log.debug('Calculate', resultJson); return OathCode.fromJson(jsonDecode(resultJson)); } on PlatformException catch (pe) { - if (CancellationException.isCancellation(pe)) { - throw CancellationException(); - } - rethrow; + throw pe.decode(); } finally { touchTimer?.cancel(); controller?.close(); @@ -236,11 +230,8 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier { var result = jsonDecode(resultString); return OathCredential.fromJson(result['credential']); } on PlatformException catch (pe) { - if (CancellationException.isCancellation(pe)) { - throw CancellationException(); - } _log.error('Failed to add account.', pe); - rethrow; + throw pe.decode(); } } @@ -258,10 +249,7 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier { return OathCredential.fromJson(responseJson); } on PlatformException catch (pe) { _log.debug('Failed to execute renameOathCredential: ${pe.message}'); - if (CancellationException.isCancellation(pe)) { - throw CancellationException(); - } - rethrow; + throw pe.decode(); } } @@ -272,10 +260,7 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier { .invokeMethod('deleteAccount', {'credentialId': credential.id}); } on PlatformException catch (e) { _log.debug('Received exception: $e'); - if (CancellationException.isCancellation(e)) { - throw CancellationException(); - } - rethrow; + throw e.decode(); } } } diff --git a/lib/android/qr_scanner/qr_scanner_provider.dart b/lib/android/qr_scanner/qr_scanner_provider.dart index aed15c30..ddc32bf8 100644 --- a/lib/android/qr_scanner/qr_scanner_provider.dart +++ b/lib/android/qr_scanner/qr_scanner_provider.dart @@ -16,7 +16,7 @@ import 'package:flutter/material.dart'; import 'package:yubico_authenticator/app/state.dart'; -import 'package:yubico_authenticator/cancellation_exception.dart'; +import 'package:yubico_authenticator/exception/cancellation_exception.dart'; import 'package:yubico_authenticator/theme.dart'; import 'qr_scanner_view.dart'; diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 4efc7c5c..bcb2685c 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -17,7 +17,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../cancellation_exception.dart'; +import '../../exception/cancellation_exception.dart'; import '../../core/state.dart'; import '../../fido/views/fido_screen.dart'; import '../../oath/models.dart'; diff --git a/lib/exception/apdu_exception.dart b/lib/exception/apdu_exception.dart new file mode 100644 index 00000000..ba211949 --- /dev/null +++ b/lib/exception/apdu_exception.dart @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 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. + */ + +class ApduException implements Exception { + final int sw; + final String message; + final String? details; + + ApduException(this.sw, this.message, this.details); +} + diff --git a/lib/cancellation_exception.dart b/lib/exception/cancellation_exception.dart similarity index 80% rename from lib/cancellation_exception.dart rename to lib/exception/cancellation_exception.dart index ac7f209c..803a4338 100644 --- a/lib/cancellation_exception.dart +++ b/lib/exception/cancellation_exception.dart @@ -14,12 +14,6 @@ * limitations under the License. */ -import 'package:flutter/services.dart'; - class CancellationException implements Exception { - CancellationException(); - - static isCancellation(PlatformException pe) => - pe.code == 'CancellationException'; } diff --git a/lib/exception/platform_exception_decoder.dart b/lib/exception/platform_exception_decoder.dart new file mode 100644 index 00000000..f45f56da --- /dev/null +++ b/lib/exception/platform_exception_decoder.dart @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 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 'package:flutter/services.dart'; + +import 'apdu_exception.dart'; +import 'cancellation_exception.dart'; + +extension Decoder on PlatformException { + bool _isCancellation() => code == 'CancellationException'; + + bool _isApduException() => code == 'ApduException'; + + Exception decode() { + if (_isCancellation()) { + return CancellationException(); + } + + if (message != null && _isApduException()) { + final regExp = RegExp( + r'^com.yubico.yubikit.core.smartcard.ApduException: APDU error: 0x(.*)$'); + final firstMatch = regExp.firstMatch(message!); + if (firstMatch != null) { + final hexSw = firstMatch.group(1); + final sw = int.tryParse(hexSw!, radix: 16); + if (sw != null) { + return ApduException(sw, 'SW: 0x$hexSw', details); + } + } + } + + // original exception + return this; + } +} diff --git a/lib/oath/views/account_mixin.dart b/lib/oath/views/account_mixin.dart index a0f91d93..a48bfc0b 100755 --- a/lib/oath/views/account_mixin.dart +++ b/lib/oath/views/account_mixin.dart @@ -25,7 +25,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; -import '../../cancellation_exception.dart'; +import '../../exception/cancellation_exception.dart'; import '../../widgets/circle_timer.dart'; import '../../widgets/custom_icons.dart'; import '../models.dart'; diff --git a/lib/oath/views/account_view.dart b/lib/oath/views/account_view.dart index 5114e868..b3eeafc4 100755 --- a/lib/oath/views/account_view.dart +++ b/lib/oath/views/account_view.dart @@ -20,7 +20,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../app/shortcuts.dart'; import '../../app/state.dart'; -import '../../cancellation_exception.dart'; +import '../../exception/cancellation_exception.dart'; import '../../widgets/menu_list_tile.dart'; import '../models.dart'; import '../state.dart'; diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index b67c3adf..949d5dfa 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -31,7 +31,8 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; import '../../app/views/user_interaction.dart'; -import '../../cancellation_exception.dart'; +import '../../exception/apdu_exception.dart'; +import '../../exception/cancellation_exception.dart'; import '../../core/state.dart'; import '../../desktop/models.dart'; import '../../management/models.dart'; @@ -195,6 +196,8 @@ class _OathAddAccountPageState extends ConsumerState { // TODO: Make this cleaner than importing desktop specific RpcError. if (e is RpcError) { errorMessage = e.message; + } else if (e is ApduException) { + errorMessage = e.message; } else { errorMessage = e.toString(); } diff --git a/lib/oath/views/delete_account_dialog.dart b/lib/oath/views/delete_account_dialog.dart index 88d5a691..ef08f9f7 100755 --- a/lib/oath/views/delete_account_dialog.dart +++ b/lib/oath/views/delete_account_dialog.dart @@ -17,11 +17,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:yubico_authenticator/cancellation_exception.dart'; import '../../app/message.dart'; import '../../app/models.dart'; import '../../app/state.dart'; +import '../../exception/cancellation_exception.dart'; import '../../widgets/responsive_dialog.dart'; import '../models.dart'; import '../state.dart'; diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index f79d8f67..bc8993b6 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -22,7 +22,7 @@ import 'package:logging/logging.dart'; import '../../app/logging.dart'; import '../../app/message.dart'; import '../../app/models.dart'; -import '../../cancellation_exception.dart'; +import '../../exception/cancellation_exception.dart'; import '../../desktop/models.dart'; import '../../widgets/responsive_dialog.dart'; import '../../widgets/utf8_utils.dart'; diff --git a/test/exception/platform_exception_decoder_test.dart b/test/exception/platform_exception_decoder_test.dart new file mode 100644 index 00000000..881e1baf --- /dev/null +++ b/test/exception/platform_exception_decoder_test.dart @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 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 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:yubico_authenticator/exception/apdu_exception.dart'; +import 'package:yubico_authenticator/exception/cancellation_exception.dart'; +import 'package:yubico_authenticator/exception/platform_exception_decoder.dart'; + +PlatformException mockApdu(String message) => + PlatformException(code: 'ApduException', message: message); + +void main() { + test('Recognize cancellation exception', () { + final pe = PlatformException( + code: 'CancellationException', + message: null, + details: null, + stacktrace: null); + + expect(pe.decode(), isA()); + }); + + test('Recognize apdu exception', () { + var pe = mockApdu( + 'com.yubico.yubikit.core.smartcard.ApduException: APDU error: 0x6f00'); + + expect( + pe.decode(), + const TypeMatcher() + .having((ae) => ae.sw, 'SW', 28416) + .having((ae) => ae.message, 'message', 'SW: 0x6f00')); + + pe = mockApdu( + 'com.yubico.yubikit.core.smartcard.ApduException: APDU error: 0xIJKLMNO'); + + expect(pe.decode(), isNot(const TypeMatcher())); + + pe = mockApdu( + 'com.yubico.yubikit.core.smartcard.ApduException: APDU error: 6f00'); + + expect(pe.decode(), isNot(const TypeMatcher())); + + pe = mockApdu( + 'com.yubico.yubikit.core.smartcard.ApduException: APDU error:'); + + expect(pe.decode(), isNot(const TypeMatcher())); + + pe = mockApdu(''); + + expect(pe.decode(), isNot(const TypeMatcher())); + }); + + test('Rethrow', () { + var pe = PlatformException( + code: 'some code', + message: 'some message', + details: 'some details', + stacktrace: 'and stacktrace'); + + expect( + pe.decode(), + const TypeMatcher() + .having((pe) => pe.code, 'code', 'some code') + .having((pe) => pe.message, 'message', 'some message') + .having((pe) => pe.details, 'details', 'some details') + .having((pe) => pe.stacktrace, 'stacktrace', 'and stacktrace')); + }); +}