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/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModelTest.kt index c0eae8785..a76ac8682 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModelTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModelTest.kt @@ -429,8 +429,10 @@ class NFCViewModelTest { null, ) + advanceUntilIdle() job.cancel() + assertTrue(values.isNotEmpty()) assertNull(values.last()) val signStatusObserver: Observer = mock() diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/AdvancedSettingsScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/AdvancedSettingsScreen.kt index 3f56a435f..7a090b720 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/AdvancedSettingsScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/AdvancedSettingsScreen.kt @@ -45,7 +45,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi 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 c0289976d..4e4162073 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 @@ -341,6 +341,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 6e50aebc8..bbc4c4341 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 @@ -103,9 +103,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) } @@ -332,6 +333,7 @@ fun MyEidScreen( ), isPinBlocked = isPin1Blocked, isPukBlocked = isPukBlocked, + isNotActivated = isCourierCard, forgotPinText = if (isPin1Blocked) { stringResource( @@ -388,6 +390,12 @@ fun MyEidScreen( style = MaterialTheme.typography.bodySmall, ) } + if (isCourierCard) { + CourierCardWarningText( + modifier = modifier, + testTag = "myEidCourierCardPin1DescriptionText", + ) + } } } item { @@ -413,6 +421,7 @@ fun MyEidScreen( ), isPinBlocked = isPin2Blocked, isPukBlocked = isPukBlocked, + isNotActivated = isCourierCard, forgotPinText = if (isPin2Blocked) { stringResource( @@ -438,7 +447,12 @@ fun MyEidScreen( }, ) - if (!isPin2Activated) { + if (isCourierCard) { + CourierCardWarningText( + modifier = modifier, + testTag = "myEidCourierCardPin2DescriptionText", + ) + } else if (!isPin2Activated) { Text( modifier = modifier @@ -624,4 +638,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.8f 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 09d8e72cb..46f77d298 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 @@ -97,6 +97,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.shared.dialog.WrongCanDialog import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver import ee.ria.DigiDoc.ui.theme.Dimensions.MSPadding @@ -110,6 +111,7 @@ import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.removeInvi import ee.ria.DigiDoc.utils.extensions.notAccessible import ee.ria.DigiDoc.utils.pin.PinCodeUtil.shouldShowPINCodeError import ee.ria.DigiDoc.utils.snackbar.SnackBarManager.showMessage +import ee.ria.DigiDoc.utilsLib.extensions.clearSensitive import ee.ria.DigiDoc.viewmodel.NFCViewModel import ee.ria.DigiDoc.viewmodel.WebEidViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedContainerViewModel @@ -144,6 +146,7 @@ fun NFCView( isValidToSign: (Boolean) -> Unit = {}, isValidToDecrypt: (Boolean) -> Unit = {}, isValidToWebEidAuthenticate: (Boolean) -> Unit = {}, + onCourierCardDialogDismissed: () -> Unit = {}, showPinField: Boolean = true, isValidToAuthenticate: (Boolean) -> Unit = {}, signAction: (() -> Unit) -> Unit = {}, @@ -168,7 +171,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) @@ -206,6 +209,7 @@ fun NFCView( var doNotShowWrongCanDialogAgain by remember { mutableStateOf(nfcViewModel.getDoNotShowWrongCanDialog()) } + val showCourierCardDialog = rememberSaveable { mutableStateOf(false) } val focusManager = LocalFocusManager.current val saveFormParams = { val currentCan = canNumber.text @@ -224,6 +228,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()) } @@ -290,11 +303,15 @@ fun NFCView( } } + LaunchedEffect(Unit) { + nfcViewModel.resetIdCardUserData() + } + LaunchedEffect(nfcViewModel.shouldResetPIN) { nfcViewModel.shouldResetPIN.asFlow().collect { bool -> bool.let { if (bool) { - pinCode.value.fill(0) + pinCode.value.clearSensitive() nfcViewModel.resetShouldResetPIN() pinCode.value = byteArrayOf() } @@ -346,7 +363,7 @@ fun NFCView( LaunchedEffect(nfcViewModel.signedContainer) { nfcViewModel.signedContainer.asFlow().collect { signedContainer -> signedContainer?.let { - pinCode.value.fill(0) + pinCode.value.clearSensitive() sharedContainerViewModel.setSignedContainer(it) nfcViewModel.resetSignedContainer() onSuccess() @@ -400,8 +417,22 @@ fun NFCView( } } + LaunchedEffect(nfcViewModel.courierCardDetected) { + nfcViewModel.courierCardDetected + .asFlow() + .filterNotNull() + .collect { + withContext(Main) { + pinCode.value.clearSensitive() + isDecryptingWithCourierCard = true + showCourierCardDialog.value = true + nfcViewModel.resetCourierCardDetected() + } + } + } + LaunchedEffect(nfcViewModel.dialogError) { - pinCode.value.fill(0) + pinCode.value.clearSensitive() nfcViewModel.dialogError .asFlow() .filterNotNull() @@ -429,9 +460,9 @@ 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() } @@ -462,21 +493,59 @@ fun NFCView( } } + 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()) { if (!isAuthPin2UnchangedDialog) { 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 0c0019ad6..c919b7daa 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt @@ -119,6 +119,9 @@ class NFCViewModel private val _certMismatch = MutableLiveData(false) val certMismatch: LiveData = _certMismatch + private val _courierCardDetected = MutableLiveData(null) + val courierCardDetected: LiveData = _courierCardDetected + private val dialogMessages: ImmutableMap = ImmutableMap .builder() @@ -272,6 +275,11 @@ class NFCViewModel 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, @@ -364,6 +372,12 @@ class NFCViewModel 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( @@ -381,6 +395,7 @@ class NFCViewModel cdoc2Settings, configurationRepository, ) + pin1Code.clearSensitive() _shouldResetPIN.postValue(true) @@ -717,6 +732,10 @@ class NFCViewModel _dialogError.postValue(0) } + fun resetCourierCardDetected() { + _courierCardDetected.postValue(null) + } + fun resetIdCardUserData() { _userData.postValue(null) } diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index b2a920da2..f848cf24d 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -648,6 +648,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 ff4796927..8c8cd3a91 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -648,6 +648,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 9ef760a73..3a7cdfe5e 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 @@ -53,6 +53,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) @@ -66,6 +67,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..5b8c26191 --- /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_returnTrueWhenFlagIsOne() { + val data = buildIdCardData(pin1CodeChanged = true, pin2CodeChanged = true) + assertTrue(data.pin1CodeChanged) + } + + @Test + fun idCardData_pin1CodeChanged_returnFalseWhenFlagIsZero() { + val data = buildIdCardData(pin1CodeChanged = false, pin2CodeChanged = false) + assertFalse(data.pin1CodeChanged) + } + + @Test + fun idCardData_pin2CodeChanged_returnTrueWhenFlagIsOne() { + val data = buildIdCardData(pin1CodeChanged = true, pin2CodeChanged = true) + assertTrue(data.pin2CodeChanged) + } + + @Test + fun idCardData_pin2CodeChanged_returnFalseWhenFlagIsZero() { + val data = buildIdCardData(pin1CodeChanged = true, pin2CodeChanged = false) + assertFalse(data.pin2CodeChanged) + } + + @Test + fun idCardData_courierCard_returnTrueForCourierCardWhenPin1NotChanged() { + val data = buildIdCardData(pin1CodeChanged = false, pin2CodeChanged = false) + val isCourierCard = !data.pin1CodeChanged + assertTrue(isCourierCard) + } + + @Test + fun idCardData_activatedCard_returnFalseForCourierCardWhenBothPINsChanged() { + val data = buildIdCardData(pin1CodeChanged = true, pin2CodeChanged = true) + val isCourierCard = !data.pin1CodeChanged + assertFalse(isCourierCard) + } + + @Test + fun idCardData_regularThalesCard_returnFalseForCourierCardWhenOnlyPin2Changed() { + // 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 f8e59dd0d..3556d51eb 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 @@ -531,11 +531,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 }