From 3dfc0ab06e2cbb736f4f0a949713d236504bc8a9 Mon Sep 17 00:00:00 2001 From: Sander Kondratjev Date: Fri, 6 Feb 2026 13:10:49 +0200 Subject: [PATCH 1/5] NFC-117 Fix Web eID signing flow: CAN handling, certificate checks, and consent wording --- .../DigiDoc/domain/preferences/DataStore.kt | 42 +++++++++++ .../DigiDoc/fragment/screen/WebEidScreen.kt | 56 +++++++++++++-- .../DigiDoc/ui/component/signing/NFCView.kt | 70 ++++++++++++++----- .../ee/ria/DigiDoc/viewmodel/NFCViewModel.kt | 32 +++++++++ app/src/main/res/values-et/strings.xml | 3 + app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 3 + .../webEid/domain/model/WebEidPersonalData.kt | 28 ++++++++ .../webEid/domain/model/WebEidSignRequest.kt | 1 + .../webEid/utils/WebEidRequestParser.kt | 32 +++++++++ 10 files changed, 245 insertions(+), 23 deletions(-) create mode 100644 web-eid-lib/src/main/java/ee/ria/DigiDoc/webEid/domain/model/WebEidPersonalData.kt diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt b/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt index f2fc84c2..c5f047a1 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt @@ -148,6 +148,48 @@ class DataStore if (cert.isNotEmpty()) editor.putString(key, cert).commit() } + fun getTemporaryCanNumber(): String { + val encryptedPrefs = getEncryptedPreferences(context) + return encryptedPrefs?.getString( + resources.getString(R.string.main_settings_temporary_can_key), + "", + ) ?: "" + } + + fun setTemporaryCanNumber(can: String) { + val encryptedPrefs = getEncryptedPreferences(context) + encryptedPrefs?.edit { + putString(resources.getString(R.string.main_settings_temporary_can_key), can) + } + } + + fun clearTemporaryCanNumber() { + val encryptedPrefs = getEncryptedPreferences(context) + encryptedPrefs?.edit { + remove(resources.getString(R.string.main_settings_temporary_can_key)) + } + } + + fun setWebEidRememberMe(value: Boolean) { + preferences.edit { + putBoolean("web_eid_remember_me", value) + } + } + + fun getWebEidRememberMe(): Boolean = preferences.getBoolean("web_eid_remember_me", true) + + fun isWebEidSessionActive(): Boolean { + val prefs = getEncryptedPreferences(context) + return prefs?.getBoolean("web_eid_session_active", false) ?: false + } + + fun setWebEidSessionActive(active: Boolean) { + val prefs = getEncryptedPreferences(context) + prefs?.edit { + putBoolean("web_eid_session_active", active) + } + } + fun getPhoneNo(): String = preferences.getString( resources.getString(R.string.main_settings_phone_no_key), diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/WebEidScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/WebEidScreen.kt index b0af9832..1408109d 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/WebEidScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/WebEidScreen.kt @@ -125,8 +125,12 @@ fun WebEidScreen( val snackBarScope = rememberCoroutineScope() val messages by SnackBarManager.messages.collectAsState(emptyList()) val dialogError by viewModel.dialogError.collectAsState() - var rememberMe by rememberSaveable { mutableStateOf(true) } - val hasStoredCanNumber = sharedSettingsViewModel.dataStore.getCanNumber().isNotEmpty() + var rememberMe by rememberSaveable { + mutableStateOf(sharedSettingsViewModel.dataStore.getWebEidRememberMe()) + } + val hasStoredCanNumber = + sharedSettingsViewModel.dataStore.getCanNumber().isNotEmpty() || + sharedSettingsViewModel.dataStore.getTemporaryCanNumber().isNotEmpty() LaunchedEffect(messages) { messages.forEach { message -> @@ -137,6 +141,15 @@ fun WebEidScreen( } } + LaunchedEffect(authRequest, certificateRequest) { + if (authRequest != null || certificateRequest != null) { + if (!sharedSettingsViewModel.dataStore.isWebEidSessionActive()) { + sharedSettingsViewModel.dataStore.clearTemporaryCanNumber() + } + sharedSettingsViewModel.dataStore.setWebEidSessionActive(true) + } + } + Scaffold( snackbarHost = { SnackbarHost( @@ -297,7 +310,10 @@ fun WebEidScreen( if (!isWebEidAuthenticating) { WebEidRememberMe( rememberMe = rememberMe, - onRememberMeChange = { rememberMe = it }, + onRememberMeChange = { + rememberMe = it + sharedSettingsViewModel.dataStore.setWebEidRememberMe(it) + }, ) } } else if (isCertificateFlow || signRequest != null) { @@ -308,9 +324,14 @@ fun WebEidScreen( signRequest != null -> signRequest.origin else -> "" } + val signingPersonInfo = + signRequest?.personalData?.let { + "${it.givenNames} ${it.surname}, ${it.personalCode}" + } WebEidSignOrCertificateInfo( origin = origin, isCertificateFlow = isCertificateFlow, + signingPersonInfo = signingPersonInfo, ) } @@ -347,7 +368,10 @@ fun WebEidScreen( if (!isWebEidAuthenticating) { WebEidRememberMe( rememberMe = rememberMe, - onRememberMeChange = { rememberMe = it }, + onRememberMeChange = { + rememberMe = it + sharedSettingsViewModel.dataStore.setWebEidRememberMe(it) + }, ) } } else { @@ -364,6 +388,8 @@ fun WebEidScreen( cancelWebEidSignAction() }, onSuccess = { + sharedSettingsViewModel.dataStore.clearTemporaryCanNumber() + sharedSettingsViewModel.dataStore.setWebEidSessionActive(false) isWebEidAuthenticating = false navController.navigateUp() }, @@ -500,6 +526,7 @@ private fun WebEidAuthInfo(authRequest: WebEidAuthRequest) { private fun WebEidSignOrCertificateInfo( origin: String, isCertificateFlow: Boolean, + signingPersonInfo: String? = null, ) { Column( modifier = Modifier.fillMaxWidth(), @@ -529,7 +556,12 @@ private fun WebEidSignOrCertificateInfo( Spacer(modifier = Modifier.height(16.dp)) Text( - text = stringResource(R.string.web_eid_details_forwarded), + text = + if (isCertificateFlow) { + stringResource(R.string.web_eid_details_forwarded) + } else { + stringResource(R.string.web_eid_details) + }, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Left, ) @@ -537,7 +569,12 @@ private fun WebEidSignOrCertificateInfo( Spacer(modifier = Modifier.height(2.dp)) Text( - text = stringResource(R.string.web_eid_name_personal_identification_code), + text = + if (!isCertificateFlow && !signingPersonInfo.isNullOrBlank()) { + signingPersonInfo + } else { + stringResource(R.string.web_eid_name_personal_identification_code) + }, style = MaterialTheme.typography.bodySmall, textAlign = TextAlign.Left, ) @@ -545,7 +582,12 @@ private fun WebEidSignOrCertificateInfo( Spacer(modifier = Modifier.height(16.dp)) Text( - text = stringResource(R.string.web_eid_certificate_consent_text), + text = + if (isCertificateFlow) { + stringResource(R.string.web_eid_certificate_consent_text) + } else { + stringResource(R.string.web_eid_signature_consent_text) + }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onBackground, textAlign = TextAlign.Left, 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 e5c96ed7..a84a8ad4 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 @@ -54,6 +54,7 @@ import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -187,10 +188,24 @@ fun NFCView( var shouldRememberMe by rememberSaveable { mutableStateOf(rememberMe) } var canNumber by rememberSaveable(stateSaver = textFieldValueSaver) { + val storedCan = sharedSettingsViewModel.dataStore.getCanNumber() + val tempCan = sharedSettingsViewModel.dataStore.getTemporaryCanNumber() + + val initialCan = + when { + identityAction == IdentityAction.CERTIFICATE && storedCan.isNotEmpty() -> storedCan + identityAction == IdentityAction.CERTIFICATE -> "" + identityAction == IdentityAction.SIGN && tempCan.isNotEmpty() -> tempCan + + storedCan.isNotEmpty() -> storedCan + tempCan.isNotEmpty() && isWebEidAuthenticating -> tempCan + else -> "" + } + mutableStateOf( TextFieldValue( - text = sharedSettingsViewModel.dataStore.getCanNumber(), - selection = TextRange(sharedSettingsViewModel.dataStore.getCanNumber().length), + text = initialCan, + selection = TextRange(initialCan.length), ), ) } @@ -201,20 +216,27 @@ fun NFCView( val showErrorDialog = rememberSaveable { mutableStateOf(false) } val focusManager = LocalFocusManager.current val saveFormParams = { - val previousCanNumber = sharedSettingsViewModel.dataStore.getCanNumber() - val currentCanNumber = canNumber.text + val currentCan = canNumber.text - if (shouldRememberMe) { - if (previousCanNumber != currentCanNumber) { - signingCert = "" - sharedSettingsViewModel.dataStore.setSigningCertificate("") + when { + ( + identityAction == IdentityAction.AUTH || + identityAction == IdentityAction.CERTIFICATE + ) && + shouldRememberMe -> { + sharedSettingsViewModel.dataStore.setCanNumber(currentCan) + sharedSettingsViewModel.dataStore.setSigningCertificate(signingCert) + sharedSettingsViewModel.dataStore.clearTemporaryCanNumber() } - sharedSettingsViewModel.dataStore.setCanNumber(currentCanNumber) - sharedSettingsViewModel.dataStore.setSigningCertificate(signingCert) - } else { - sharedSettingsViewModel.dataStore.setCanNumber("") - sharedSettingsViewModel.dataStore.setSigningCertificate("") + ( + identityAction == IdentityAction.AUTH || + identityAction == IdentityAction.CERTIFICATE + ) && + !shouldRememberMe -> { + sharedSettingsViewModel.dataStore.setCanNumber("") + sharedSettingsViewModel.dataStore.setTemporaryCanNumber(currentCan) + } } } @@ -261,6 +283,8 @@ fun NFCView( BackHandler { nfcViewModel.handleBackButton() + sharedSettingsViewModel.dataStore.clearTemporaryCanNumber() + sharedSettingsViewModel.dataStore.setWebEidSessionActive(false) if (isSigning || isDecrypting || isAuthenticating) { onError() } else { @@ -268,6 +292,16 @@ fun NFCView( } } + DisposableEffect(Unit) { + onDispose { + val webEidActive = sharedSettingsViewModel.dataStore.isWebEidSessionActive() + + if (!shouldRememberMe && !webEidActive) { + sharedSettingsViewModel.dataStore.clearTemporaryCanNumber() + } + } + } + LaunchedEffect(nfcViewModel.shouldResetPIN) { nfcViewModel.shouldResetPIN.asFlow().collect { bool -> bool.let { @@ -350,6 +384,7 @@ fun NFCView( LaunchedEffect(nfcViewModel.webEidAuthResult) { nfcViewModel.webEidAuthResult.asFlow().collect { result -> result?.let { (authCert, signingCert, signature) -> + nfcViewModel.setExpectedWebEidSigningCertificate(signingCert) val encodedCert = Base64.getEncoder().encodeToString(signingCert) sharedSettingsViewModel.dataStore.setSigningCertificate(encodedCert) webEidViewModel?.handleWebEidAuthResult(authCert, signingCert, signature) @@ -364,6 +399,7 @@ fun NFCView( result?.let { signCert -> sharedSettingsViewModel.dataStore.setSigningCertificate(signCert) val certBytes = Base64.getDecoder().decode(signCert) + nfcViewModel.setExpectedWebEidSigningCertificate(certBytes) webEidViewModel?.handleWebEidCertificateResult(certBytes) nfcViewModel.resetWebEidCertificateResult() onSuccess() @@ -667,9 +703,10 @@ fun NFCView( ) } } else { - if (sharedSettingsViewModel.dataStore.getCanNumber().isNotEmpty()) { - saveFormParams() - } + saveFormParams() + val expectedSigningCertBase64 = + sharedSettingsViewModel.dataStore + .getSigningCertificate() nfcViewModel.performNFCWebEidSignWorkRequest( activity = activity, context = context, @@ -677,6 +714,7 @@ fun NFCView( pin2Code = pinCode.value, responseUri = responseUriString, hash = hashString, + expectedSigningCertBase64 = expectedSigningCertBase64, ) } } 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 e4ea3028..a94ead41 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt @@ -106,6 +106,7 @@ class NFCViewModel val webEidSignResult: LiveData?> = _webEidSignResult private val _webEidCertificateResult = MutableLiveData() val webEidCertificateResult: LiveData = _webEidCertificateResult + private var expectedWebEidSigningCert: ByteArray? = null private val dialogMessages: ImmutableMap = ImmutableMap @@ -176,6 +177,10 @@ class NFCViewModel fun getNFCStatus(activity: Activity): NfcStatus = NfcStatus.NFC_ACTIVE + fun setExpectedWebEidSigningCertificate(cert: ByteArray) { + expectedWebEidSigningCert = cert + } + private fun resetValues() { _errorState.postValue(null) _message.postValue(null) @@ -605,6 +610,7 @@ class NFCViewModel pin2Code: ByteArray?, responseUri: String, hash: String, + expectedSigningCertBase64: String?, ) { val pinType = context.getString(R.string.signature_id_card_pin2) activity.requestedOrientation = activity.resources.configuration.orientation @@ -623,6 +629,15 @@ class NFCViewModel val card = TokenWithPace.create(nfcReader) card.tunnel(canNumber) val signerCert = card.certificate(CertificateType.SIGNING) + expectedSigningCertBase64 + ?.takeIf { it.isNotEmpty() } + ?.let { + val expectedCert = Base64.getDecoder().decode(it) + if (!expectedCert.contentEquals(signerCert)) { + throw IllegalStateException("Web eID signing certificate mismatch") + } + } + val signerCertB64 = Base64.getEncoder().encodeToString(signerCert) val hashBytes = Base64.getDecoder().decode(hash) val (_, signatureArray) = idCardService.sign(card, pin2Code, hashBytes) @@ -654,6 +669,7 @@ class NFCViewModel showTechnicalError(ex) } } finally { + expectedWebEidSigningCert = null pin2Code.clearSensitive() nfcSmartCardReaderManager.disableNfcReaderMode() activity.requestedOrientation = @@ -665,6 +681,7 @@ class NFCViewModel } fun handleBackButton() { + expectedWebEidSigningCert = null _shouldResetPIN.postValue(true) resetValues() } @@ -726,6 +743,15 @@ class NFCViewModel errorLog(logTag, "Unable to sign with NFC - Certificate status: unknown", e) } + private fun showWebEidSigningCertificateMismatchError(e: Exception) { + _errorState.postValue(Triple(R.string.signature_update_nfc_wrong_certificate, null, null)) + errorLog( + logTag, + "Web eID signing failed - signing certificate does not match previously used certificate", + e, + ) + } + private fun showTechnicalError(e: Exception) { _errorState.postValue(Triple(R.string.signature_update_nfc_technical_error, null, null)) errorLog(logTag, "Unable to perform with NFC: ${e.message}", e) @@ -811,6 +837,11 @@ class NFCViewModel true } + message.contains("Web eID signing certificate mismatch") -> { + showWebEidSigningCertificateMismatchError(ex) + true + } + else -> false }.also { errorLog(logTag, "Exception: ${ex.message}", ex) @@ -819,6 +850,7 @@ class NFCViewModel override fun onCleared() { super.onCleared() + expectedWebEidSigningCert = null nfcSmartCardReaderManager.disableNfcReaderMode() } } diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 33ef2756..72765ed5 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -289,6 +289,7 @@ https://www.id.ee/artikkel/id-kaardi-pin-ja-puk-koodide-muutmine/ Sertifikaat on kehtetu Sertifikaadi staatus on teadmata + Valitud ID-kaart ei vasta varem kasutatud sertifikaadile. Palun kasuta sama ID-kaarti, millega autentisid. ID-kaardi Mobiil-ID Smart-ID @@ -666,11 +667,13 @@ Autentides nõustun oma nime ja isikukoodi edastamisega teenusepakkujale. Autentimispäring: Edastatavad andmed: + Andmed: NIMI, ISIKUKOOD Järgmisel kasutamisel on andmeväljad eeltäidetud. Kinnita Vali sertifikaat Sertifikaati valides nõustun oma nime ja isikukoodi edastamisega teenusepakkujale. + PIN2 koodi sisestamisega annad omakäelise digiallkirja. Allkirjastamine Allkirjasta ID-kaardiga Sertifikaadipäring: diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 8e254fdf..c8a34638 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -67,6 +67,7 @@ mainSettingsUUID can signingCert + temporaryCanNumber mainSettingsMobileNr mainSettingsPersonalCode mainSettingsSmartIdPersonalCode diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d5fd3b09..591d2cec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -289,6 +289,7 @@ https://www.id.ee/en/article/changing-id-card-pin-codes-and-puk-code/ Certificate status revoked Certificate status unknown + The selected ID card does not match the previously used certificate. Please use the same ID card you authenticated with. ID-card\'s Mobile-ID Smart-ID @@ -666,11 +667,13 @@ By authenticating, I agree to the transfer of my name and personal identification code to the service provider. Authentication request from: Details forwarded: + Details: NAME, PERSONAL IDENTIFICATION CODE The entered data will be filled the next time you authenticate. Confirm Select a certificate By choosing the certificate, I agree to the transfer of my name and personal identification code to the service provider. + By entering your PIN2, you give a handwritten-equivalent digital signature. Sign Sign with ID-card Certificate request from: diff --git a/web-eid-lib/src/main/java/ee/ria/DigiDoc/webEid/domain/model/WebEidPersonalData.kt b/web-eid-lib/src/main/java/ee/ria/DigiDoc/webEid/domain/model/WebEidPersonalData.kt new file mode 100644 index 00000000..187f75bb --- /dev/null +++ b/web-eid-lib/src/main/java/ee/ria/DigiDoc/webEid/domain/model/WebEidPersonalData.kt @@ -0,0 +1,28 @@ +/* + * 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.webEid.domain.model + +data class WebEidPersonalData( + val givenNames: String, + val surname: String, + val personalCode: String, +) diff --git a/web-eid-lib/src/main/java/ee/ria/DigiDoc/webEid/domain/model/WebEidSignRequest.kt b/web-eid-lib/src/main/java/ee/ria/DigiDoc/webEid/domain/model/WebEidSignRequest.kt index 46b4aea9..5a4bab82 100644 --- a/web-eid-lib/src/main/java/ee/ria/DigiDoc/webEid/domain/model/WebEidSignRequest.kt +++ b/web-eid-lib/src/main/java/ee/ria/DigiDoc/webEid/domain/model/WebEidSignRequest.kt @@ -29,4 +29,5 @@ data class WebEidSignRequest( val signingCertificate: X509Certificate, val hash: String?, val hashFunction: String?, + val personalData: WebEidPersonalData?, ) diff --git a/web-eid-lib/src/main/java/ee/ria/DigiDoc/webEid/utils/WebEidRequestParser.kt b/web-eid-lib/src/main/java/ee/ria/DigiDoc/webEid/utils/WebEidRequestParser.kt index c576cbd4..7c748a9a 100644 --- a/web-eid-lib/src/main/java/ee/ria/DigiDoc/webEid/utils/WebEidRequestParser.kt +++ b/web-eid-lib/src/main/java/ee/ria/DigiDoc/webEid/utils/WebEidRequestParser.kt @@ -25,12 +25,17 @@ import android.net.Uri import ee.ria.DigiDoc.utilsLib.signing.CertificateUtil import ee.ria.DigiDoc.webEid.domain.model.WebEidAuthRequest import ee.ria.DigiDoc.webEid.domain.model.WebEidCertificateRequest +import ee.ria.DigiDoc.webEid.domain.model.WebEidPersonalData import ee.ria.DigiDoc.webEid.domain.model.WebEidSignRequest import ee.ria.DigiDoc.webEid.exception.WebEidErrorCode.ERR_WEBEID_MOBILE_INVALID_REQUEST import ee.ria.DigiDoc.webEid.exception.WebEidException +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x500.style.BCStyle +import org.bouncycastle.asn1.x500.style.IETFUtils import org.json.JSONObject import java.net.URI import java.net.URISyntaxException +import java.security.cert.X509Certificate import java.util.Base64 object WebEidRequestParser { @@ -109,6 +114,7 @@ object WebEidRequestParser { signingCertificate, hash = hash, hashFunction = hashFunction, + personalData = extractPersonalData(signingCertificate), ) } @@ -208,4 +214,30 @@ object WebEidRequestParser { return hashBytes } + + private fun extractPersonalData(cert: X509Certificate): WebEidPersonalData { + val x500Name = X500Name.getInstance(cert.subjectX500Principal.encoded) + val cnRDNs = x500Name.getRDNs(BCStyle.CN) + + require(cnRDNs.isNotEmpty()) { + "Signing certificate CN missing" + } + + val cn = + IETFUtils + .valueToString(cnRDNs.first().first.value) + .replace("\\,", ",") + .replace("\\ ", " ") + val parts = cn.split(",").map { it.trim() } + + require(parts.size >= 3) { + "Unexpected signing certificate CN format: $cn" + } + + return WebEidPersonalData( + surname = parts[0], + givenNames = parts[1], + personalCode = parts[2], + ) + } } From 909670f1760796e90dd0095ea4693ba9e9b7b5c7 Mon Sep 17 00:00:00 2001 From: Mart Aarma Date: Wed, 11 Feb 2026 12:02:52 +0200 Subject: [PATCH 2/5] NFC-117 Fix cancel and response flows --- .../domain/preferences/DataStoreTest.kt | 27 +++++++++++++++++++ .../DigiDoc/viewmodel/WebEidViewModelTest.kt | 6 ++++- app/src/main/AndroidManifest.xml | 5 ++++ .../kotlin/ee/ria/DigiDoc/MainActivity.kt | 8 +++++- .../DigiDoc/domain/preferences/DataStore.kt | 12 +++++++++ .../ee/ria/DigiDoc/fragment/WebEidFragment.kt | 4 +++ .../DigiDoc/fragment/screen/WebEidScreen.kt | 10 +++++++ .../ria/DigiDoc/viewmodel/WebEidViewModel.kt | 4 +++ 8 files changed, 74 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt index e3278e27..f14a2178 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt @@ -40,6 +40,7 @@ import ee.ria.DigiDoc.network.siva.SivaSetting import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.BeforeClass @@ -614,4 +615,30 @@ class DataStoreTest { assertFalse(result) } + + @Test + fun dataStore_getWebEidBrowserPackage_defaultNull() { + val result = dataStore.getWebEidBrowserPackage() + + assertNull(result) + } + + @Test + fun dataStore_setWebEidBrowserPackage_success() { + dataStore.setWebEidBrowserPackage("com.android.chrome") + + val result = dataStore.getWebEidBrowserPackage() + + assertEquals("com.android.chrome", result) + } + + @Test + fun dataStore_setWebEidBrowserPackage_nullClearsValue() { + dataStore.setWebEidBrowserPackage("com.android.chrome") + dataStore.setWebEidBrowserPackage(null) + + val result = dataStore.getWebEidBrowserPackage() + + assertNull(result) + } } diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModelTest.kt index d6bc8a89..15e08a02 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModelTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModelTest.kt @@ -26,6 +26,7 @@ import android.util.Base64.URL_SAFE import android.util.Base64.decode import androidx.arch.core.executor.testing.InstantTaskExecutorRule import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.domain.preferences.DataStore import ee.ria.DigiDoc.webEid.WebEidAuthService import ee.ria.DigiDoc.webEid.WebEidSignService import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -58,6 +59,9 @@ class WebEidViewModelTest { @Mock private lateinit var signService: WebEidSignService + @Mock + private lateinit var dataStore: DataStore + private lateinit var viewModel: WebEidViewModel private val signingCertBase64Raw = @@ -82,7 +86,7 @@ class WebEidViewModelTest { @Before fun setup() { MockitoAnnotations.openMocks(this) - viewModel = WebEidViewModel(authService, signService) + viewModel = WebEidViewModel(authService, signService, dataStore) } @Test diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bc844f5..03ca0154 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -156,6 +156,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt b/app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt index a83126e5..09931c70 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt @@ -121,7 +121,13 @@ class MainActivity : val componentClassName = this.javaClass.name val locale = dataStore.getLocale() ?: getLocale("en") - val webEidUri = intent?.data?.takeIf { it.scheme == "web-eid-mobile" } + val webEidUri = intent.data?.takeIf { it.scheme == "web-eid-mobile" } + + if (webEidUri != null) { + val browserPackage = intent.getStringExtra("com.android.browser.application_id") + ?.takeIf { it.isNotEmpty() } + dataStore.setWebEidBrowserPackage(browserPackage) + } val externalFileUris = if (webEidUri != null) { diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt b/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt index c5f047a1..c9146e52 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt @@ -178,6 +178,18 @@ class DataStore fun getWebEidRememberMe(): Boolean = preferences.getBoolean("web_eid_remember_me", true) + fun setWebEidBrowserPackage(packageName: String?) { + preferences.edit { + if (packageName.isNullOrEmpty()) { + remove("web_eid_browser_package") + } else { + putString("web_eid_browser_package", packageName) + } + } + } + + fun getWebEidBrowserPackage(): String? = preferences.getString("web_eid_browser_package", null) + fun isWebEidSessionActive(): Boolean { val prefs = getEncryptedPreferences(context) return prefs?.getBoolean("web_eid_session_active", false) ?: false diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/WebEidFragment.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/WebEidFragment.kt index eb5dcf0f..0f5a8454 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/WebEidFragment.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/WebEidFragment.kt @@ -63,9 +63,13 @@ fun WebEidFragment( LaunchedEffect(viewModel) { viewModel.relyingPartyResponseEvents.collect { responseUri -> + val browserPackage = viewModel.getWebEidBrowserPackage() val browserIntent = Intent(Intent.ACTION_VIEW, responseUri).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + if (!browserPackage.isNullOrEmpty()) { + setPackage(browserPackage) + } } activity.startActivity(browserIntent) activity.finishAndRemoveTask() diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/WebEidScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/WebEidScreen.kt index 1408109d..384622e1 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/WebEidScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/WebEidScreen.kt @@ -22,6 +22,7 @@ package ee.ria.DigiDoc.fragment.screen import android.app.Activity +import android.content.Intent import android.content.res.Configuration import androidx.activity.compose.LocalActivity import androidx.compose.foundation.background @@ -455,6 +456,15 @@ fun WebEidScreen( OutlinedButton( onClick = { isWebEidAuthenticating = false + val browserPackage = viewModel.getWebEidBrowserPackage() + if (!browserPackage.isNullOrEmpty()) { + val launchIntent = + activity.packageManager.getLaunchIntentForPackage(browserPackage) + if (launchIntent != null) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + activity.startActivity(launchIntent) + } + } activity.finishAndRemoveTask() }, modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModel.kt index b501f681..101e2017 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModel.kt @@ -25,6 +25,7 @@ import android.net.Uri import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.domain.preferences.DataStore import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.DigiDoc.webEid.WebEidAuthService import ee.ria.DigiDoc.webEid.WebEidSignService @@ -50,8 +51,11 @@ class WebEidViewModel constructor( private val authService: WebEidAuthService, private val signService: WebEidSignService, + private val dataStore: DataStore, ) : ViewModel() { private val logTag = javaClass.simpleName + + fun getWebEidBrowserPackage(): String? = dataStore.getWebEidBrowserPackage() private val _authRequest = MutableStateFlow(null) val authRequest: StateFlow = _authRequest.asStateFlow() private val _certificateRequest = MutableStateFlow(null) From 74a351e5a5b7a9e87f5219a306bbd7086f81d729 Mon Sep 17 00:00:00 2001 From: Sander Kondratjev Date: Thu, 12 Feb 2026 15:05:26 +0200 Subject: [PATCH 3/5] NFC-117 Add tests to datastore for temp can number, remember me and session --- .../domain/preferences/DataStoreTest.kt | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt index f14a2178..a75aeaac 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt @@ -641,4 +641,83 @@ class DataStoreTest { assertNull(result) } + + @Test + fun dataStore_getTemporaryCanNumber_defaultEmpty() { + val result = dataStore.getTemporaryCanNumber() + + assertEquals("", result) + } + + @Test + fun dataStore_setTemporaryCanNumber_success() { + dataStore.setTemporaryCanNumber("123456") + + val result = dataStore.getTemporaryCanNumber() + + assertEquals("123456", result) + } + + @Test + fun dataStore_clearTemporaryCanNumber_success() { + dataStore.setTemporaryCanNumber("123456") + dataStore.clearTemporaryCanNumber() + + val result = dataStore.getTemporaryCanNumber() + + assertEquals("", result) + } + + @Test + fun dataStore_getWebEidRememberMe_defaultTrue() { + val result = dataStore.getWebEidRememberMe() + + assertTrue(result) + } + + @Test + fun dataStore_setWebEidRememberMe_successWithFalse() { + dataStore.setWebEidRememberMe(false) + + val result = dataStore.getWebEidRememberMe() + + assertFalse(result) + } + + @Test + fun dataStore_setWebEidRememberMe_successWithTrue() { + dataStore.setWebEidRememberMe(true) + + val result = dataStore.getWebEidRememberMe() + + assertTrue(result) + } + + @Test + fun dataStore_isWebEidSessionActive_defaultFalse() { + val result = dataStore.isWebEidSessionActive() + + assertFalse(result) + } + + @Test + fun dataStore_setWebEidSessionActive_successWithTrue() { + dataStore.setWebEidSessionActive(true) + + val result = dataStore.isWebEidSessionActive() + + assertTrue(result) + } + + @Test + fun dataStore_setWebEidSessionActive_successWithFalse() { + dataStore.setWebEidSessionActive(true) + dataStore.setWebEidSessionActive(false) + + val result = dataStore.isWebEidSessionActive() + + assertFalse(result) + } + + } From 681e645c0a99be98ab6805103a92a47afe146a82 Mon Sep 17 00:00:00 2001 From: Sander Kondratjev Date: Thu, 12 Feb 2026 15:36:03 +0200 Subject: [PATCH 4/5] NFC-117 Fix added datastore in view model also in tests --- .../kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModelTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModelTest.kt index 15e08a02..952609e0 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModelTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModelTest.kt @@ -25,6 +25,7 @@ import android.net.Uri import android.util.Base64.URL_SAFE import android.util.Base64.decode import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.test.platform.app.InstrumentationRegistry import ee.ria.DigiDoc.R import ee.ria.DigiDoc.domain.preferences.DataStore import ee.ria.DigiDoc.webEid.WebEidAuthService @@ -59,7 +60,6 @@ class WebEidViewModelTest { @Mock private lateinit var signService: WebEidSignService - @Mock private lateinit var dataStore: DataStore private lateinit var viewModel: WebEidViewModel @@ -86,6 +86,8 @@ class WebEidViewModelTest { @Before fun setup() { MockitoAnnotations.openMocks(this) + val context = InstrumentationRegistry.getInstrumentation().targetContext + dataStore = DataStore(context) viewModel = WebEidViewModel(authService, signService, dataStore) } From 58f898d8e698e2fe606cc2de53ef4e9ca8dd51ae Mon Sep 17 00:00:00 2001 From: Sander Kondratjev Date: Thu, 12 Feb 2026 15:38:54 +0200 Subject: [PATCH 5/5] NFC-117 Klint check --- .../ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt | 2 -- app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt | 6 ++++-- .../main/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModel.kt | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt index a75aeaac..c0463347 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt @@ -718,6 +718,4 @@ class DataStoreTest { assertFalse(result) } - - } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt b/app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt index 09931c70..95b6cba8 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt @@ -124,8 +124,10 @@ class MainActivity : val webEidUri = intent.data?.takeIf { it.scheme == "web-eid-mobile" } if (webEidUri != null) { - val browserPackage = intent.getStringExtra("com.android.browser.application_id") - ?.takeIf { it.isNotEmpty() } + val browserPackage = + intent + .getStringExtra("com.android.browser.application_id") + ?.takeIf { it.isNotEmpty() } dataStore.setWebEidBrowserPackage(browserPackage) } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModel.kt index 101e2017..2d4218da 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModel.kt @@ -56,6 +56,7 @@ class WebEidViewModel private val logTag = javaClass.simpleName fun getWebEidBrowserPackage(): String? = dataStore.getWebEidBrowserPackage() + private val _authRequest = MutableStateFlow(null) val authRequest: StateFlow = _authRequest.asStateFlow() private val _certificateRequest = MutableStateFlow(null)