From f1f7ac38ae1bf25d37a6d4af20b33db2193af991 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Thu, 14 May 2026 13:45:09 +0300 Subject: [PATCH] Show locked card message --- .../ee/ria/DigiDoc/IdCardDataCreator.kt | 2 + .../screen/ProxyServicesSettingsScreen.kt | 1 + .../fragment/screen/SignatureInputScreen.kt | 3 + .../DigiDoc/ui/component/myeid/MyEidScreen.kt | 52 +++++++- .../MyEidPinAndCertificateView.kt | 6 +- .../dialog/CourierCardActivationDialog.kt | 122 ++++++++++++++++++ .../DigiDoc/ui/component/signing/NFCView.kt | 104 ++++++++++++--- .../ui/component/signing/SigningNavigation.kt | 1 - .../ee/ria/DigiDoc/viewmodel/NFCViewModel.kt | 42 +++--- .../shared/SharedSettingsViewModel.kt | 4 +- app/src/main/res/values-et/strings.xml | 5 + app/src/main/res/values/strings.xml | 5 + .../ee/ria/DigiDoc/domain/model/IdCardData.kt | 1 + .../domain/service/IdCardServiceImpl.kt | 2 + .../DigiDoc/domain/model/IdCardDataTest.kt | 103 +++++++++++++++ .../libdigidoclib/init/Initialization.kt | 13 +- 16 files changed, 417 insertions(+), 49 deletions(-) create mode 100644 app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/dialog/CourierCardActivationDialog.kt create mode 100644 id-card-lib/src/test/kotlin/ee/ria/DigiDoc/domain/model/IdCardDataTest.kt diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/IdCardDataCreator.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/IdCardDataCreator.kt index 9f9c784c9..5cb77e581 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/IdCardDataCreator.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/IdCardDataCreator.kt @@ -39,6 +39,7 @@ class IdCardDataCreator { pin1RetryCount: Int = 3, pin2RetryCount: Int = 3, pukRetryCount: Int = 3, + pin1CodeChanged: Boolean = true, pin2CodeChanged: Boolean = true, ): IdCardData = IdCardData( @@ -49,6 +50,7 @@ class IdCardDataCreator { pin1RetryCount = pin1RetryCount, pin2RetryCount = pin2RetryCount, pukRetryCount = pukRetryCount, + pin1CodeChanged = pin1CodeChanged, pin2CodeChanged = pin2CodeChanged, ) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ProxyServicesSettingsScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ProxyServicesSettingsScreen.kt index e8419f094..b4db5a021 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ProxyServicesSettingsScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ProxyServicesSettingsScreen.kt @@ -210,6 +210,7 @@ fun ProxyServicesSettingsScreen( errorState?.let { withContext(Main) { showMessage(context, errorState) + sharedSettingsViewModel.resetErrorState() } } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SignatureInputScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SignatureInputScreen.kt index 41eaeb286..4200da420 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SignatureInputScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SignatureInputScreen.kt @@ -364,6 +364,9 @@ fun SignatureInputScreen( isValidToSign = { isValid -> isValidToSign = isValid }, + onCourierCardDialogDismissed = { + navController.navigateUp() + }, signAction = { action -> signAction = action }, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/MyEidScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/MyEidScreen.kt index a4060d57b..994b30180 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/MyEidScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/MyEidScreen.kt @@ -114,9 +114,10 @@ fun MyEidScreen( val isPin2Blocked = idCardData?.pin2RetryCount == 0 val isPukBlocked = idCardData?.pukRetryCount == 0 val isPin2Activated = idCardData?.pin2CodeChanged == true + val isCourierCard = idCardData?.personalData != null && idCardData?.pin1CodeChanged == false - val alphaForPin1BlockedState = getAlphaForBlockedState(isPin1Blocked && isPukBlocked) - val alphaForPin2BlockedState = getAlphaForBlockedState(isPin2Blocked && isPukBlocked) + val alphaForPin1BlockedState = getAlphaForBlockedState((isPin1Blocked && isPukBlocked) || isCourierCard) + val alphaForPin2BlockedState = getAlphaForBlockedState((isPin2Blocked && isPukBlocked) || isCourierCard) val alphaForPukBlockedState = getAlphaForBlockedState(isPukBlocked) val selectedMyEidTabIndex = rememberSaveable { mutableIntStateOf(0) } @@ -357,6 +358,7 @@ fun MyEidScreen( ), isPinBlocked = isPin1Blocked, isPukBlocked = isPukBlocked, + isNotActivated = isCourierCard, forgotPinText = if (isPin1Blocked) { stringResource( @@ -413,6 +415,12 @@ fun MyEidScreen( style = MaterialTheme.typography.bodySmall, ) } + if (isCourierCard) { + CourierCardWarningText( + modifier = modifier, + testTag = "myEidCourierCardPin1DescriptionText", + ) + } } } item { @@ -438,6 +446,7 @@ fun MyEidScreen( ), isPinBlocked = isPin2Blocked, isPukBlocked = isPukBlocked, + isNotActivated = isCourierCard, forgotPinText = if (isPin2Blocked) { stringResource( @@ -463,7 +472,12 @@ fun MyEidScreen( }, ) - if (!isPin2Activated) { + if (isCourierCard) { + CourierCardWarningText( + modifier = modifier, + testTag = "myEidCourierCardPin2DescriptionText", + ) + } else if (!isPin2Activated) { Text( modifier = modifier @@ -649,4 +663,36 @@ fun MyEidScreen( ) } +@Composable +private fun CourierCardWarningText( + modifier: Modifier, + testTag: String, +) { + val message = stringResource(R.string.id_card_courier_warning_message) + val linkText = stringResource(R.string.id_card_courier_activate_button) + val linkUrl = stringResource(R.string.id_card_courier_activate_url) + val linkWord = stringResource(R.string.link) + HrefDynamicText( + modifier = + modifier + .fillMaxWidth() + .focusable(true) + .testTag(testTag) + .semantics { + contentDescription = "$message $linkText, $linkWord, $linkUrl" + }, + text1 = message, + text2 = null, + linkText = linkText, + linkUrl = linkUrl, + newLineBeforeLink = true, + textStyle = + TextStyle( + color = MaterialTheme.colorScheme.error, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + textAlign = TextAlign.Start, + ), + ) +} + fun getAlphaForBlockedState(isBlocked: Boolean) = if (!isBlocked) 1f else 0.7f diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinAndCertificateView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinAndCertificateView.kt index 6fa75cf17..e41097802 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinAndCertificateView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinAndCertificateView.kt @@ -78,6 +78,7 @@ fun MyEidPinAndCertificateView( linkUrl: String = "", isPinBlocked: Boolean = false, isPukBlocked: Boolean = false, + isNotActivated: Boolean = false, showForgotPin: Boolean = true, forgotPinText: String = "", onForgotPinClick: (() -> Unit)? = null, @@ -130,6 +131,7 @@ fun MyEidPinAndCertificateView( modifier = modifier .weight(1f) + .padding(vertical = XSPadding) .focusable() .semantics(mergeDescendants = true) { this.contentDescription = "$title. $subtitle".lowercase() @@ -179,7 +181,7 @@ fun MyEidPinAndCertificateView( verticalAlignment = Alignment.CenterVertically, ) { OutlinedButton( - enabled = !isPukBlocked, + enabled = !isPukBlocked && !isNotActivated, onClick = onForgotPinClick, modifier = modifier @@ -209,7 +211,7 @@ fun MyEidPinAndCertificateView( } Button( - enabled = !isPinBlocked, + enabled = !isPinBlocked && !isNotActivated, onClick = onChangePinClick ?: {}, modifier = modifier diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/dialog/CourierCardActivationDialog.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/dialog/CourierCardActivationDialog.kt new file mode 100644 index 000000000..8926a9937 --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/dialog/CourierCardActivationDialog.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +@file:Suppress("PackageName", "FunctionName") + +package ee.ria.DigiDoc.ui.component.shared.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.style.TextAlign +import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.ui.component.shared.CancelAndOkButtonRow +import ee.ria.DigiDoc.ui.component.shared.InvisibleElement +import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding +import ee.ria.DigiDoc.ui.theme.buttonRoundCornerShape +import kotlinx.coroutines.delay + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +@Composable +fun CourierCardActivationDialog( + modifier: Modifier = Modifier, + message: Int = R.string.id_card_courier_must_activate_to_sign, + onDismiss: () -> Unit, +) { + val focusRequester = remember { FocusRequester() } + Box(modifier = modifier.fillMaxSize()) { + BasicAlertDialog( + modifier = + Modifier + .clip(buttonRoundCornerShape) + .background(MaterialTheme.colorScheme.surface), + onDismissRequest = onDismiss, + ) { + LaunchedEffect(Unit) { + delay(100) + focusRequester.requestFocus() + } + Surface( + modifier = + Modifier + .padding(SPadding) + .wrapContentHeight() + .wrapContentWidth() + .verticalScroll(rememberScrollState()), + ) { + Column( + modifier = + Modifier + .semantics { testTagsAsResourceId = true } + .testTag("courierCardActivationDialogContainer"), + ) { + Text( + modifier = + Modifier + .padding(SPadding) + .fillMaxWidth() + .focusRequester(focusRequester) + .focusable(), + text = stringResource(message), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Start, + ) + CancelAndOkButtonRow( + okButtonTestTag = "courierCardDialogOkButton", + cancelButtonTestTag = "courierCardDialogCancelButton", + cancelButtonClick = {}, + okButtonClick = onDismiss, + cancelButtonTitle = R.string.cancel_button, + okButtonTitle = R.string.ok_button, + cancelButtonContentDescription = stringResource(R.string.cancel_button).lowercase(), + okButtonContentDescription = stringResource(R.string.ok_button).lowercase(), + showCancelButton = false, + ) + } + } + } + InvisibleElement(modifier = Modifier) + } +} diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCView.kt index 837b820b6..f88aa1574 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCView.kt @@ -94,6 +94,7 @@ import ee.ria.DigiDoc.ui.component.shared.InvisibleElement import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.shared.RoleDataView import ee.ria.DigiDoc.ui.component.shared.SecurePinTextField +import ee.ria.DigiDoc.ui.component.shared.dialog.CourierCardActivationDialog import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding @@ -134,6 +135,7 @@ fun NFCView( isSupported: (Boolean) -> Unit = {}, isValidToSign: (Boolean) -> Unit = {}, isValidToDecrypt: (Boolean) -> Unit = {}, + onCourierCardDialogDismissed: () -> Unit = {}, showPinField: Boolean = true, isValidToAuthenticate: (Boolean) -> Unit = {}, signAction: (() -> Unit) -> Unit = {}, @@ -151,7 +153,7 @@ fun NFCView( val getSettingsAskRoleAndAddress = sharedSettingsViewModel.dataStore::getSettingsAskRoleAndAddress - val personalData by nfcViewModel.userData.asFlow().collectAsState(null) + val userData by nfcViewModel.userData.asFlow().collectAsState(null) val dialogError by nfcViewModel.dialogError.asFlow().collectAsState(0) @@ -170,6 +172,7 @@ fun NFCView( } var errorText by remember { mutableStateOf("") } val showErrorDialog = rememberSaveable { mutableStateOf(false) } + val showCourierCardDialog = rememberSaveable { mutableStateOf(false) } val focusManager = LocalFocusManager.current val saveFormParams = { if (shouldRememberMe) { @@ -184,6 +187,15 @@ fun NFCView( val canNumberFocusRequester = remember { FocusRequester() } val pinNumberFocusRequester = remember { FocusRequester() } + + val isCourierCard = userData?.personalData != null && userData?.pin1CodeChanged == false + var isDecryptingWithCourierCard by rememberSaveable { mutableStateOf(false) } + val courierDialogMessage = + if (identityAction == IdentityAction.DECRYPT) { + R.string.id_card_courier_must_activate_to_decrypt + } else { + R.string.id_card_courier_must_activate_to_sign + } val canNumberWithInvisibleSpaces = TextFieldValue(addInvisibleElement(canNumber.text)) val pinCode = remember { mutableStateOf(byteArrayOf()) } @@ -220,6 +232,10 @@ fun NFCView( } } + LaunchedEffect(Unit) { + nfcViewModel.resetIdCardUserData() + } + LaunchedEffect(nfcViewModel.shouldResetPIN) { nfcViewModel.shouldResetPIN.asFlow().collect { bool -> bool.let { @@ -299,6 +315,20 @@ fun NFCView( } } + LaunchedEffect(nfcViewModel.courierCardDetected) { + nfcViewModel.courierCardDetected + .asFlow() + .filterNotNull() + .collect { + withContext(Main) { + pinCode.value.fill(0) + isDecryptingWithCourierCard = true + showCourierCardDialog.value = true + nfcViewModel.resetCourierCardDetected() + } + } + } + LaunchedEffect(nfcViewModel.dialogError) { pinCode.value.fill(0) nfcViewModel.dialogError @@ -328,35 +358,66 @@ fun NFCView( } } - LaunchedEffect(Unit, personalData, isAuthenticating) { - if (personalData != null && isAuthenticating && !isSigning) { - personalData?.let { data -> + LaunchedEffect(Unit, userData, isAuthenticating) { + if (userData != null && isAuthenticating && !isSigning) { + userData?.let { data -> isAuthenticated(true, data) nfcViewModel.resetIdCardUserData() } } } + LaunchedEffect(isCourierCard) { + if (isCourierCard) { + isValidToSign(false) + isValidToDecrypt(false) + if (identityAction == IdentityAction.SIGN || + identityAction == IdentityAction.DECRYPT) { + showCourierCardDialog.value = true + } + } else { + showCourierCardDialog.value = false + } + } + if (errorText.isNotEmpty()) { showMessage(errorText) errorText = "" } + if (showCourierCardDialog.value) { + CourierCardActivationDialog( + message = courierDialogMessage, + onDismiss = { + showCourierCardDialog.value = false + if (isDecryptingWithCourierCard) { + isDecryptingWithCourierCard = false + onError() + } else { + onCourierCardDialogDismissed() + } + }) + } + if (showErrorDialog.value) { var text1Arg: Int? = null val text2 = null var linkText = 0 var linkUrl = 0 - if (dialogError == R.string.too_many_requests_message) { - text1Arg = R.string.id_card_conditional_speech - linkText = R.string.additional_information - linkUrl = R.string.too_many_requests_url - } else if (dialogError == R.string.invalid_time_slot_message) { - linkText = R.string.additional_information - linkUrl = R.string.invalid_time_slot_url - } else if (dialogError == R.string.sign_blocked_pin2_unchanged_message) { - linkText = R.string.additional_information - linkUrl = R.string.sign_blocked_pin2_unchanged_url + when (dialogError) { + R.string.too_many_requests_message -> { + text1Arg = R.string.id_card_conditional_speech + linkText = R.string.additional_information + linkUrl = R.string.too_many_requests_url + } + R.string.invalid_time_slot_message -> { + linkText = R.string.additional_information + linkUrl = R.string.invalid_time_slot_url + } + R.string.sign_blocked_pin2_unchanged_message -> { + linkText = R.string.additional_information + linkUrl = R.string.sign_blocked_pin2_unchanged_url + } } Box(modifier = modifier.fillMaxSize()) { onError() @@ -383,7 +444,8 @@ fun NFCView( modifier .semantics { testTagsAsResourceId = true - }.testTag("smartIdErrorContainer"), + } + .testTag("smartIdErrorContainer"), ) { HrefMessageDialog( modifier = modifier, @@ -425,9 +487,11 @@ fun NFCView( detectTapGestures(onTap = { focusManager.clearFocus() }) - }.semantics { + } + .semantics { testTagsAsResourceId = true - }.testTag("signatureUpdateNFC"), + } + .testTag("signatureUpdateNFC"), ) { if (isAddingRoleAndAddress) { RoleDataView(modifier, sharedSettingsViewModel) @@ -470,7 +534,8 @@ fun NFCView( .semantics { heading() testTagsAsResourceId = true - }.testTag("signatureUpdateNFCNotFoundMessage"), + } + .testTag("signatureUpdateNFCNotFoundMessage"), textAlign = TextAlign.Center, ) } else { @@ -582,7 +647,8 @@ fun NFCView( modifier .semantics { testTagsAsResourceId = true - }.testTag("nfcViewContainer"), + } + .testTag("nfcViewContainer"), ) { PrimaryTextField( modifier = diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt index 7a958d7f0..1d7ea0967 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt @@ -26,7 +26,6 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.content.res.Configuration -import android.util.Log import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt index ca4f9f520..b34d678fb 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt @@ -57,7 +57,6 @@ import ee.ria.DigiDoc.utils.pin.PinCodeUtil.isPINLengthValid import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.libdigidocpp.ExternalSigner -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -102,6 +101,8 @@ class NFCViewModel val userData: LiveData = _userData private val _dialogError = MutableLiveData(0) val dialogError: LiveData = _dialogError + private val _courierCardDetected = MutableLiveData(null) + val courierCardDetected: LiveData = _courierCardDetected private val dialogMessages: ImmutableMap = ImmutableMap @@ -229,11 +230,14 @@ class NFCViewModel nfcSmartCardReaderManager.startDiscovery(activity) { nfcReader, exc -> if ((nfcReader != null) && (exc == null)) { try { - CoroutineScope(Main).launch { - _message.postValue(R.string.signature_update_nfc_detected) - } + _message.postValue(R.string.signature_update_nfc_detected) val card = TokenWithPace.create(nfcReader) card.tunnel(canNumber) + val pin1ChangedFlagValue = card.pinChangedFlag(CodeType.PIN1) + if (pin1ChangedFlagValue != 1) { + _courierCardDetected.postValue(true) + return@startDiscovery + } val signerCert = card.certificate(CertificateType.SIGNING) debugLog( logTag, @@ -260,11 +264,9 @@ class NFCViewModel signatureArray, ) - CoroutineScope(Main).launch { - _shouldResetPIN.postValue(true) - _signStatus.postValue(true) - _signedContainer.postValue(container) - } + _shouldResetPIN.postValue(true) + _signStatus.postValue(true) + _signedContainer.postValue(container) } catch (ex: SmartCardReaderException) { _signStatus.postValue(false) @@ -388,12 +390,16 @@ class NFCViewModel nfcSmartCardReaderManager.startDiscovery(activity) { nfcReader, exc -> if ((nfcReader != null) && (exc == null)) { try { - CoroutineScope(Main).launch { - _message.postValue(R.string.signature_update_nfc_detected) - } + _message.postValue(R.string.signature_update_nfc_detected) val card = TokenWithPace.create(nfcReader) card.tunnel(canNumber) + val pin1ChangedFlagValue = card.pinChangedFlag(CodeType.PIN1) + if (pin1ChangedFlagValue != 1) { + _courierCardDetected.postValue(true) + return@startDiscovery + } + val authCert = card.certificate(CertificateType.AUTHENTICATION) debugLog( @@ -414,11 +420,9 @@ class NFCViewModel if (pin1Code.isNotEmpty()) { Arrays.fill(pin1Code, 0.toByte()) } - CoroutineScope(Main).launch { - _shouldResetPIN.postValue(true) - _decryptStatus.postValue(true) - _cryptoContainer.postValue(decryptedContainer) - } + _shouldResetPIN.postValue(true) + _decryptStatus.postValue(true) + _cryptoContainer.postValue(decryptedContainer) } catch (ex: SmartCardReaderException) { _decryptStatus.postValue(false) @@ -597,6 +601,10 @@ class NFCViewModel _dialogError.postValue(0) } + fun resetCourierCardDetected() { + _courierCardDetected.postValue(null) + } + fun resetIdCardUserData() { _userData.postValue(null) } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt index 6c21e2f2c..7c4c08b6a 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt @@ -144,7 +144,7 @@ class SharedSettingsViewModel resetCryptoSettings() resetCertificateInfo() - resetErrors() + resetErrorState() } private fun resetProxySettings() { @@ -507,7 +507,7 @@ class SharedSettingsViewModel _cryptoCertificate.value = null } - private fun resetErrors() { + fun resetErrorState() { _errorState.value = null } diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index fe8481bab..eece44a3b 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -645,6 +645,11 @@ %1$s on blokeeritud, kuna %1$s-koodi on sisestatud 3 korda valesti. Selle ID-kaardiga allkirjastamine ei ole veel võimalik. Allkirjastamiseks tuleb %1$s-koodi muuta. + ID-kaardiga isikutuvastamine ja allkirjastamine ei ole veel võimalik. ID-kaardi kasutamiseks tuleb see aktiveerida Politsei- ja Piirivalveameti iseteeninduses. + Aktiveeri ID-kaart + https://www.politsei.ee/et/iseteenindus/ + Allkirjastamiseks tuleb ID-kaart aktiveerida. + Dekrüpteerimiseks tuleb ID-kaart aktiveerida. %1$s on blokeeritud, kuna %1$s-koodi on sisestatud 3 korda valesti. Tühista blokeering, et %1$s taas kasutada. PUK on blokeeritud, kuna PUK-koodi on sisestatud 3 korda valesti. PUK-koodi ei saa ise lahti blokeerida.\n\nKuigi PUK-kood on blokeeritud, saab kõiki eID võimalusi kasutada, välja arvatud PUK-koodi vajavaid.\n\nUue PUK-koodi saamiseks külasta klienditeeninduspunkti, kust saad uue koodiümbriku uute koodidega. https://www.id.ee/artikkel/id-kaardi-pin-ja-puk-koodide-muutmine/ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 802e6abc5..458e79d7b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -645,6 +645,11 @@ %1$s is blocked, because %1$s has been inserted 3 times incorrectly. Signing with the ID-card isn\'t possible yet. %1$s code must be changed in order to sign. + Authentication and signing with the ID-card isn\'t possible yet. ID-card must be activated in the Police and Border Guard Board\'s self-service portal in order to use it. + Activate ID-card + https://www.politsei.ee/en/self-service-portal/ + The ID-card must be activated in order to sign. + The ID-card must be activated in order to decrypt. %1$s has been blocked because %1$s code has been entered incorrectly 3 times. Unblock to reuse %1$s. PUK has been blocked because PUK code has been entered incorrectly 3 times. You can not unblock the PUK code yourself.\n\nAs long as the PUK code is blocked, all eID options can be used, except transactions that need PUK code.\n\nPlease visit the service center to obtain new codes. https://www.id.ee/en/article/changing-id-card-pin-codes-and-puk-code/ diff --git a/id-card-lib/src/main/kotlin/ee/ria/DigiDoc/domain/model/IdCardData.kt b/id-card-lib/src/main/kotlin/ee/ria/DigiDoc/domain/model/IdCardData.kt index 5c86a7bb3..1dcd0f2fd 100644 --- a/id-card-lib/src/main/kotlin/ee/ria/DigiDoc/domain/model/IdCardData.kt +++ b/id-card-lib/src/main/kotlin/ee/ria/DigiDoc/domain/model/IdCardData.kt @@ -33,5 +33,6 @@ data class IdCardData( val pin1RetryCount: Int, val pin2RetryCount: Int, val pukRetryCount: Int, + val pin1CodeChanged: Boolean, val pin2CodeChanged: Boolean, ) diff --git a/id-card-lib/src/main/kotlin/ee/ria/DigiDoc/domain/service/IdCardServiceImpl.kt b/id-card-lib/src/main/kotlin/ee/ria/DigiDoc/domain/service/IdCardServiceImpl.kt index fd22cc473..587e4e2c9 100644 --- a/id-card-lib/src/main/kotlin/ee/ria/DigiDoc/domain/service/IdCardServiceImpl.kt +++ b/id-card-lib/src/main/kotlin/ee/ria/DigiDoc/domain/service/IdCardServiceImpl.kt @@ -50,6 +50,7 @@ class IdCardServiceImpl val pin1RetryCounter = token.codeRetryCounter(CodeType.PIN1) val pin2RetryCounter = token.codeRetryCounter(CodeType.PIN2) val pukRetryCounter = token.codeRetryCounter(CodeType.PUK) + val pin1CodeChanged = token.pinChangedFlag(CodeType.PIN1) val pin2CodeChanged = token.pinChangedFlag(CodeType.PIN2) val authCertificate = ExtendedCertificate.create(authenticationCertificateData, certificateService) @@ -63,6 +64,7 @@ class IdCardServiceImpl pin1RetryCount = pin1RetryCounter, pin2RetryCount = pin2RetryCounter, pukRetryCount = pukRetryCounter, + pin1CodeChanged = pin1CodeChanged == 1, pin2CodeChanged = pin2CodeChanged == 1, ) } diff --git a/id-card-lib/src/test/kotlin/ee/ria/DigiDoc/domain/model/IdCardDataTest.kt b/id-card-lib/src/test/kotlin/ee/ria/DigiDoc/domain/model/IdCardDataTest.kt new file mode 100644 index 000000000..e33b9719e --- /dev/null +++ b/id-card-lib/src/test/kotlin/ee/ria/DigiDoc/domain/model/IdCardDataTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +@file:Suppress("PackageName") + +package ee.ria.DigiDoc.domain.model + +import ee.ria.DigiDoc.common.model.EIDType +import ee.ria.DigiDoc.common.model.ExtendedCertificate +import ee.ria.DigiDoc.idcard.PersonalData +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` + +class IdCardDataTest { + private val personalData: PersonalData = mock(PersonalData::class.java) + private val authCertificate: ExtendedCertificate = mock(ExtendedCertificate::class.java) + private val signCertificate: ExtendedCertificate = mock(ExtendedCertificate::class.java) + + init { + `when`(authCertificate.type).thenReturn(EIDType.ID_CARD) + `when`(signCertificate.type).thenReturn(EIDType.ID_CARD) + } + + private fun buildIdCardData( + pin1CodeChanged: Boolean, + pin2CodeChanged: Boolean, + ) = IdCardData( + type = EIDType.ID_CARD, + personalData = personalData, + authCertificate = authCertificate, + signCertificate = signCertificate, + pin1RetryCount = 3, + pin2RetryCount = 3, + pukRetryCount = 3, + pin1CodeChanged = pin1CodeChanged, + pin2CodeChanged = pin2CodeChanged, + ) + + @Test + fun idCardData_pin1CodeChanged_whenFlagIsOne_isTrue() { + val data = buildIdCardData(pin1CodeChanged = true, pin2CodeChanged = true) + assertTrue(data.pin1CodeChanged) + } + + @Test + fun idCardData_pin1CodeChanged_whenFlagIsZero_isFalse() { + val data = buildIdCardData(pin1CodeChanged = false, pin2CodeChanged = false) + assertFalse(data.pin1CodeChanged) + } + + @Test + fun idCardData_pin2CodeChanged_whenFlagIsOne_isTrue() { + val data = buildIdCardData(pin1CodeChanged = true, pin2CodeChanged = true) + assertTrue(data.pin2CodeChanged) + } + + @Test + fun idCardData_pin2CodeChanged_whenFlagIsZero_isFalse() { + val data = buildIdCardData(pin1CodeChanged = true, pin2CodeChanged = false) + assertFalse(data.pin2CodeChanged) + } + + @Test + fun idCardData_courierCard_pin1NotChanged_isCourierCard() { + val data = buildIdCardData(pin1CodeChanged = false, pin2CodeChanged = false) + val isCourierCard = !data.pin1CodeChanged + assertTrue(isCourierCard) + } + + @Test + fun idCardData_activatedCard_pin1Changed_isNotCourierCard() { + val data = buildIdCardData(pin1CodeChanged = true, pin2CodeChanged = true) + val isCourierCard = !data.pin1CodeChanged + assertFalse(isCourierCard) + } + + @Test + fun idCardData_regularThalesCard_pin1ChangedPin2Not_isNotCourierCard() { + // Regular Thales card: PIN1 has been changed, only PIN2 is unused + val data = buildIdCardData(pin1CodeChanged = true, pin2CodeChanged = false) + val isCourierCard = !data.pin1CodeChanged + assertFalse(isCourierCard) + } +} diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/Initialization.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/Initialization.kt index d1b645168..bec4f24ca 100644 --- a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/Initialization.kt +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/Initialization.kt @@ -533,11 +533,14 @@ class Initialization ): String? { val certFile: File? = getCertFile(context, fileName, certFolder) if (certFile != null) { - val fileContents: String = readFileContent(certFile.path) - return fileContents - .replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replace("\\s".toRegex(), "") + return try { + readFileContent(certFile.path) + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace("\\s".toRegex(), "") + } catch (_: IllegalStateException) { + null + } } return null }