From e668b02f0bb1403081d2d78d058ab39cc178c1bf Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Wed, 8 Jul 2026 17:28:26 +0300 Subject: [PATCH] Fix CDOC2 encryption cancellation and UI --- .../fragment/screen/EncryptRecipientScreen.kt | 35 ++- .../crypto/ColoredRecipientStatusText.kt | 22 +- .../ui/component/crypto/EncryptNavigation.kt | 83 +++--- .../ui/component/crypto/RecipientComponent.kt | 67 +++-- .../crypto/RecipientDecryptionStatus.kt | 29 ++ .../ui/component/shared/LoadingScreen.kt | 56 +++- .../DigiDoc/ui/component/signing/NFCView.kt | 10 +- .../viewmodel/EncryptRecipientViewModel.kt | 101 +++++-- .../ee/ria/DigiDoc/viewmodel/NFCViewModel.kt | 6 +- app/src/main/res/values-et/strings.xml | 6 +- app/src/main/res/values/strings.xml | 2 + crypto-lib/build.gradle.kts | 1 + .../DigiDoc/cryptolib/CryptoContainerTest.kt | 74 +++++ .../ee/ria/DigiDoc/cryptolib/CDOC2Settings.kt | 24 ++ .../ria/DigiDoc/cryptolib/CryptoContainer.kt | 272 ++++++++++-------- .../libdigidoclib/SignedContainerTest.kt | 3 + .../DigiDoc/libdigidoclib/SignedContainer.kt | 2 + 17 files changed, 535 insertions(+), 258 deletions(-) create mode 100644 app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/RecipientDecryptionStatus.kt diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptRecipientScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptRecipientScreen.kt index 5951de38b..521587283 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptRecipientScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptRecipientScreen.kt @@ -84,8 +84,8 @@ import ee.ria.DigiDoc.ui.component.crypto.bottombar.EncryptBottomBar import ee.ria.DigiDoc.ui.component.crypto.bottombar.EncryptButtonBottomBar import ee.ria.DigiDoc.ui.component.crypto.bottomsheet.RecipientBottomSheet import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet +import ee.ria.DigiDoc.ui.component.shared.ContentLoadingScreen import ee.ria.DigiDoc.ui.component.shared.InvisibleElement -import ee.ria.DigiDoc.ui.component.shared.LoadingScreen import ee.ria.DigiDoc.ui.component.shared.MessageDialog import ee.ria.DigiDoc.ui.component.shared.PreventResize import ee.ria.DigiDoc.ui.component.shared.Recipient @@ -132,7 +132,7 @@ fun EncryptRecipientScreen( val cryptoContainer by sharedContainerViewModel.cryptoContainer.collectAsState() - val showLoading = remember { mutableStateOf(false) } + val isEncrypting by encryptRecipientViewModel.isEncrypting.collectAsState() val isSettingsMenuBottomSheetVisible = rememberSaveable { mutableStateOf(false) } val showPasswordDialog = rememberSaveable { mutableStateOf(false) } @@ -242,6 +242,15 @@ fun EncryptRecipientScreen( } } + LaunchedEffect(encryptRecipientViewModel.encryptedContainer) { + encryptRecipientViewModel.encryptedContainer.asFlow().collect { encrypted -> + encrypted?.let { + sharedContainerViewModel.setCryptoContainer(it, true) + encryptRecipientViewModel.resetEncryptedContainer() + } + } + } + LaunchedEffect(encryptRecipientViewModel.errorState) { encryptRecipientViewModel.errorState.asFlow().collect { error -> error?.let { @@ -274,12 +283,17 @@ fun EncryptRecipientScreen( }.testTag("encryptRecipientsScreen"), snackbarHost = { StatusSnackbarHost() }, topBar = { - if (!expanded) { + if (!expanded || isEncrypting) { TopBar( modifier = modifier, sharedMenuViewModel = sharedMenuViewModel, title = null, + showRightSideIcons = !isEncrypting, onLeftButtonClick = { + if (isEncrypting) { + encryptRecipientViewModel.cancelEncryption() + encryptionButtonEnabled.value = true + } navController.navigateUp() }, onRightSecondaryButtonClick = { @@ -296,21 +310,18 @@ fun EncryptRecipientScreen( encryptButtonIcon = R.drawable.ic_m3_arrow_forward_48dp_wght400, encryptButtonName = R.string.next_button, encryptButtonContentDescription = R.string.next_button, - isEncryptButtonEnabled = true, + isEncryptButtonEnabled = !isEncrypting, onEncryptButtonClick = { showPasswordDialog.value = true }, ) } else { EncryptBottomBar( modifier = modifier, - isEncryptButtonEnabled = encryptionButtonEnabled.value, + isEncryptButtonEnabled = encryptionButtonEnabled.value && !isEncrypting, onEncryptClick = { if (encryptionButtonEnabled.value) { encryptionButtonEnabled.value = false - showLoading.value = true - scope.launch(Main) { - encryptRecipientViewModel.encryptContainer(sharedContainerViewModel) - showLoading.value = false - } + expanded = false + encryptRecipientViewModel.encrypt(cryptoContainer) } }, ) @@ -429,8 +440,8 @@ fun EncryptRecipientScreen( ) } - if (showLoading.value) { - LoadingScreen(modifier = modifier) + if (isEncrypting) { + ContentLoadingScreen(modifier = modifier, contentPadding = paddingValues) } RecipientBottomSheet( diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/ColoredRecipientStatusText.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/ColoredRecipientStatusText.kt index f89237406..948e51da6 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/ColoredRecipientStatusText.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/ColoredRecipientStatusText.kt @@ -35,21 +35,25 @@ import ee.ria.DigiDoc.ui.theme.extendedColorScheme @Composable fun ColoredRecipientStatusText( text: String, + status: RecipientDecryptionStatus, modifier: Modifier = Modifier, - expired: Boolean = false, ) { val tagBackgroundColor = - if (!expired) { - MaterialTheme.extendedColorScheme.successContainer - } else { - MaterialTheme.colorScheme.errorContainer + when (status) { + RecipientDecryptionStatus.NOT_ENCRYPTED -> MaterialTheme.colorScheme.surfaceVariant + RecipientDecryptionStatus.NOT_ENCRYPTED_EXPIRED, + RecipientDecryptionStatus.EXPIRED, + -> MaterialTheme.colorScheme.errorContainer + RecipientDecryptionStatus.VALID -> MaterialTheme.extendedColorScheme.successContainer } val tagContentColor = - if (!expired) { - MaterialTheme.extendedColorScheme.onSuccessContainer - } else { - MaterialTheme.colorScheme.onErrorContainer + when (status) { + RecipientDecryptionStatus.NOT_ENCRYPTED -> MaterialTheme.colorScheme.onSurface + RecipientDecryptionStatus.NOT_ENCRYPTED_EXPIRED, + RecipientDecryptionStatus.EXPIRED, + -> MaterialTheme.colorScheme.onErrorContainer + RecipientDecryptionStatus.VALID -> MaterialTheme.extendedColorScheme.onSuccessContainer } FlowRow( diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/EncryptNavigation.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/EncryptNavigation.kt index f65bc132c..dab97fe76 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/EncryptNavigation.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/EncryptNavigation.kt @@ -95,10 +95,10 @@ import ee.ria.DigiDoc.ui.component.crypto.bottomsheet.RecipientBottomSheet import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet import ee.ria.DigiDoc.ui.component.settings.EditValueDialog import ee.ria.DigiDoc.ui.component.shared.ContainerNameView +import ee.ria.DigiDoc.ui.component.shared.ContentLoadingScreen import ee.ria.DigiDoc.ui.component.shared.CryptoDataFileItem import ee.ria.DigiDoc.ui.component.shared.CryptoDataFilesLocked import ee.ria.DigiDoc.ui.component.shared.InvisibleElement -import ee.ria.DigiDoc.ui.component.shared.LoadingScreen import ee.ria.DigiDoc.ui.component.shared.MessageDialog import ee.ria.DigiDoc.ui.component.shared.StatusSnackbarHost import ee.ria.DigiDoc.ui.component.shared.TabView @@ -163,6 +163,13 @@ fun EncryptNavigation( val isNestedContainer = sharedContainerViewModel.isNestedContainer(cryptoContainer) + val isUnencryptedCryptoContainer = + with(encryptViewModel) { + !isEncryptedContainer(cryptoContainer) && + !isDecryptedContainer(cryptoContainer) && + !isNestedContainer + } + val containerEncryptedSuccess = remember { mutableStateOf(false) } val containerEncryptedSuccessText = stringResource(id = R.string.crypto_create_success) val containerDecryptedSuccess = remember { mutableStateOf(false) } @@ -171,6 +178,7 @@ fun EncryptNavigation( val emptyFileInContainerText = stringResource(id = R.string.crypto_empty_file_message) val showLoadingScreen = remember { mutableStateOf(false) } + val isEncrypting by encryptRecipientViewModel.isEncrypting.collectAsState() val openRemoveFileDialog = rememberSaveable { mutableStateOf(false) } val fileRemoved = stringResource(id = R.string.file_removed) @@ -375,11 +383,7 @@ fun EncryptNavigation( val onEncryptClick = { if (encryptionButtonEnabled.value) { encryptionButtonEnabled.value = false - showLoadingScreen.value = true - scope.launch(Main) { - encryptRecipientViewModel.encryptContainer(sharedContainerViewModel) - showLoadingScreen.value = false - } + encryptRecipientViewModel.encrypt(cryptoContainer) } } @@ -410,8 +414,11 @@ fun EncryptNavigation( fileToSave.value = null } - BackHandler { - if (!isNestedContainer && encryptViewModel.isEncryptedContainer(cryptoContainer)) { + val handleClose = { + if (isEncrypting) { + encryptRecipientViewModel.cancelEncryption() + encryptionButtonEnabled.value = true + } else if (!isNestedContainer && encryptViewModel.isEncryptedContainer(cryptoContainer)) { showContainerCloseConfirmationDialog.value = true } else { handleBackButtonClick( @@ -422,6 +429,10 @@ fun EncryptNavigation( } } + BackHandler { + handleClose() + } + DisposableEffect(shouldResetContainer) { onDispose { if (shouldResetContainer == true) { @@ -456,11 +467,21 @@ fun EncryptNavigation( } } + LaunchedEffect(encryptRecipientViewModel.encryptedContainer) { + encryptRecipientViewModel.encryptedContainer.asFlow().collect { encrypted -> + encrypted?.let { + sharedContainerViewModel.setCryptoContainer(it, true) + encryptRecipientViewModel.resetEncryptedContainer() + } + } + } + LaunchedEffect(encryptRecipientViewModel.errorState) { encryptRecipientViewModel.errorState.asFlow().collect { error -> error?.let { showMessage(context, error) encryptionButtonEnabled.value = true + encryptRecipientViewModel.resetErrorState() } } } @@ -563,21 +584,18 @@ fun EncryptNavigation( ?.let { R.string.signing_container_documents_title }, leftIcon = when { + isEncrypting -> R.drawable.ic_m3_arrow_back_48dp_wght400 isNestedContainer -> R.drawable.ic_m3_arrow_back_48dp_wght400 else -> R.drawable.ic_m3_close_48dp_wght400 }, - leftIconContentDescription = R.string.crypto_close_container_title, - onLeftButtonClick = { - if (!isNestedContainer && encryptViewModel.isEncryptedContainer(cryptoContainer)) { - showContainerCloseConfirmationDialog.value = true + leftIconContentDescription = + if (isEncrypting) { + R.string.cancel_button } else { - handleBackButtonClick( - navController, - encryptViewModel, - sharedContainerViewModel, - ) - } - }, + R.string.crypto_close_container_title + }, + showRightSideIcons = !isEncrypting, + onLeftButtonClick = handleClose, onRightSecondaryButtonClick = { isSettingsMenuBottomSheetVisible.value = true }, @@ -617,7 +635,7 @@ fun EncryptNavigation( ), isShareButtonShown = encryptViewModel.isShareButtonShown(cryptoContainer), onEncryptClick = onEncryptClick, - encryptionButtonEnabled = encryptionButtonEnabled.value, + encryptionButtonEnabled = encryptionButtonEnabled.value && !isEncrypting, ) } }, @@ -674,15 +692,7 @@ fun EncryptNavigation( text = removeExtensionFromContainerFilename(cryptoContainerName), ) cryptoContainer?.let { - val isInitialCryptoContainer = - with(encryptViewModel) { - isContainerWithoutRecipients(cryptoContainer) && - !isEncryptedContainer(cryptoContainer) && - !isDecryptedContainer(cryptoContainer) && - !isNestedContainer - } - - if (isInitialCryptoContainer) { + if (isUnencryptedCryptoContainer) { Text( modifier = modifier @@ -843,7 +853,11 @@ fun EncryptNavigation( showRecipientsLoadingIndicator.value, recipientsLoading, onRecipientItemClick, - !encryptViewModel.isCDOC1Container(cryptoContainer), + isCDOC2Container = + !encryptViewModel.isCDOC1Container(cryptoContainer), + isEncryptedOrDecrypted = + encryptViewModel.isEncryptedContainer(cryptoContainer) || + encryptViewModel.isDecryptedContainer(cryptoContainer), ) }, ), @@ -1061,10 +1075,7 @@ fun EncryptNavigation( EncryptContainerBottomSheet( modifier = modifier, showSheet = showContainerBottomSheet, - isEditContainerButtonShown = - !isNestedContainer && - !encryptViewModel.isEncryptedContainer(cryptoContainer) && - !encryptViewModel.isDecryptedContainer(cryptoContainer), + isEditContainerButtonShown = isUnencryptedCryptoContainer, openEditContainerNameDialog = openEditContainerNameDialog, isSaveButtonShown = ( encryptViewModel.isEncryptedContainer(cryptoContainer) || @@ -1091,8 +1102,8 @@ fun EncryptNavigation( onRecipientRemove = { actionRecipient = it }, ) - if (showLoadingScreen.value) { - LoadingScreen(modifier = modifier) + if (showLoadingScreen.value || isEncrypting) { + ContentLoadingScreen(modifier = modifier) } if (showContainerCloseConfirmationDialog.value) { diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/RecipientComponent.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/RecipientComponent.kt index b59ef74b9..eae6ae3f3 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/RecipientComponent.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/RecipientComponent.kt @@ -82,7 +82,8 @@ fun RecipientComponent( showRecipientsLoadingIndicator: Boolean, recipientsLoadingContentDescription: String, onClick: (Addressee) -> Unit, - isCDOC2Container: Boolean = false, + isCDOC2Container: Boolean, + isEncryptedOrDecrypted: Boolean, ) { val recipientText = stringResource(R.string.crypto_recipient_title) val buttonName = stringResource(id = R.string.button_name) @@ -114,43 +115,39 @@ fun RecipientComponent( formatCompanyName(recipient.identifier, recipient.serialNumber) } val certTypeText = getRecipientCertTypeText(LocalContext.current, recipient.certType) - var expired = false - var certValidTo = - recipient.validTo - ?.let { - dateFormat.format( - it, - ) - }?.let { - stringResource( - R.string.crypto_cert_valid_to, - it, - ) - } ?: "" + val formattedValidTo = recipient.validTo?.let { dateFormat.format(it) } - val decryptionValidToText = - if (isCDOC2Container) { - certValidTo = "" - - recipient.validTo?.let { validToDate -> - val formattedDate = dateFormat.format(validToDate) - if (validToDate.before(Date())) { - expired = true - stringResource( - R.string.crypto_decryption_expired, - formattedDate, - ) - } else { - stringResource( - R.string.crypto_decryption_valid_to, - formattedDate, - ) - } - } ?: "" + val certValidTo = + if (!isCDOC2Container && formattedValidTo != null) { + stringResource(R.string.crypto_cert_valid_to, formattedValidTo) } else { "" } + val isExpired = recipient.validTo?.before(Date()) == true + + val decryptionStatus = + when { + !isCDOC2Container || formattedValidTo == null -> null + !isEncryptedOrDecrypted && isExpired -> RecipientDecryptionStatus.NOT_ENCRYPTED_EXPIRED + !isEncryptedOrDecrypted -> RecipientDecryptionStatus.NOT_ENCRYPTED + isExpired -> RecipientDecryptionStatus.EXPIRED + else -> RecipientDecryptionStatus.VALID + } + + val decryptionValidToText = + when (decryptionStatus) { + RecipientDecryptionStatus.NOT_ENCRYPTED -> + stringResource(R.string.crypto_recipient_expires_on, formattedValidTo.orEmpty()) + RecipientDecryptionStatus.NOT_ENCRYPTED_EXPIRED -> + stringResource(R.string.crypto_recipient_expired_on, formattedValidTo.orEmpty()) + RecipientDecryptionStatus.EXPIRED -> + stringResource(R.string.crypto_decryption_expired, formattedValidTo.orEmpty()) + RecipientDecryptionStatus.VALID -> + stringResource(R.string.crypto_decryption_valid_to, formattedValidTo.orEmpty()) + null -> "" + } + Card( modifier = modifier @@ -237,10 +234,10 @@ fun RecipientComponent( color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium, ) - if (decryptionValidToText.isNotEmpty()) { + if (decryptionStatus != null) { ColoredRecipientStatusText( text = decryptionValidToText, - expired = expired, + status = decryptionStatus, modifier = modifier .padding(vertical = SBorder) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/RecipientDecryptionStatus.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/RecipientDecryptionStatus.kt new file mode 100644 index 000000000..b89da4043 --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/RecipientDecryptionStatus.kt @@ -0,0 +1,29 @@ +/* + * 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.ui.component.crypto + +enum class RecipientDecryptionStatus { + NOT_ENCRYPTED, + NOT_ENCRYPTED_EXPIRED, + EXPIRED, + VALID, +} diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/LoadingScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/LoadingScreen.kt index 25ff79d03..07eda918a 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/LoadingScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/LoadingScreen.kt @@ -23,6 +23,7 @@ package ee.ria.DigiDoc.ui.component.shared import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -55,21 +56,46 @@ fun LoadingScreen(modifier: Modifier = Modifier) { testTagsAsResourceId = true }.testTag("loadingScreen"), ) { - Box( - modifier = - modifier - .fillMaxSize() - .padding(vertical = MPadding) - .testTag("activityOverlay"), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator( - modifier = - modifier - .size(loadingBarSize) - .testTag("activityIndicator"), - ) - } + LoadingIndicator(modifier = modifier) } } } + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun ContentLoadingScreen( + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(), +) { + Surface( + modifier = + modifier + .padding(contentPadding) + .fillMaxSize() + .focusGroup() + .semantics { + testTagsAsResourceId = true + }.testTag("loadingScreen"), + ) { + LoadingIndicator(modifier = modifier) + } +} + +@Composable +private fun LoadingIndicator(modifier: Modifier = Modifier) { + Box( + modifier = + modifier + .fillMaxSize() + .padding(vertical = MPadding) + .testTag("activityOverlay"), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + modifier = + modifier + .size(loadingBarSize) + .testTag("activityIndicator"), + ) + } +} 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 4133a38e7..ce0bf5516 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 @@ -283,7 +283,7 @@ fun NFCView( var isCanNumberReadOnly by remember { mutableStateOf(isCanNumberReadOnly) } BackHandler { - nfcViewModel.handleBackButton() + nfcViewModel.handleBackButton(activity) sharedSettingsViewModel.dataStore.clearTemporaryCanNumber() sharedSettingsViewModel.dataStore.setWebEidSessionActive(false) if (isSigning || isDecrypting || isAuthenticating) { @@ -817,23 +817,23 @@ fun NFCView( } } cancelAction { - nfcViewModel.handleBackButton() + nfcViewModel.handleBackButton(activity) scope.launch(IO) { signedContainer?.let { nfcViewModel.cancelNFCSignWorkRequest(it) } } } cancelDecryptAction { - nfcViewModel.handleBackButton() + nfcViewModel.handleBackButton(activity) nfcViewModel.cancelNfcOperation() } cancelWebEidAuthenticateAction { - nfcViewModel.handleBackButton() + nfcViewModel.handleBackButton(activity) nfcViewModel.cancelNfcOperation() } cancelWebEidSignAction { - nfcViewModel.handleBackButton() + nfcViewModel.handleBackButton(activity) nfcViewModel.cancelNfcOperation() } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptRecipientViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptRecipientViewModel.kt index 750fa0309..3901c3d8e 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptRecipientViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptRecipientViewModel.kt @@ -41,11 +41,14 @@ import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.DigiDoc.utilsLib.mimetype.MimeTypeResolver import ee.ria.DigiDoc.viewmodel.shared.SharedContainerViewModel +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import java.io.File import javax.inject.Inject @@ -82,6 +85,15 @@ class EncryptRecipientViewModel private val _hasSearched = MutableLiveData(false) val hasSearched: LiveData = _hasSearched + private var encryptionJob: Job? = null + private var encryptionGeneration = 0 + + private val _isEncrypting = MutableStateFlow(false) + val isEncrypting = _isEncrypting.asStateFlow() + + private val _encryptedContainer = MutableLiveData(null) + val encryptedContainer: LiveData = _encryptedContainer + fun handleIsRecipientAdded(isRecipientAdded: Boolean) { _isRecipientAdded.postValue(isRecipientAdded) } @@ -90,6 +102,68 @@ class EncryptRecipientViewModel _isContainerEncrypted.postValue(isContainerEncrypted) } + fun encrypt(cryptoContainer: CryptoContainer?) { + if (cryptoContainer == null) { + errorLog(logTag, "Unable to encrypt: crypto container is 'null'") + _errorState.postValue(R.string.crypto_encrypt_error) + return + } + + if (encryptionJob?.isActive == true) { + return + } + + // Cancelling cannot stop an encryption that is already running, so an old one can still be + // finishing while a new one starts. Only the newest one returns the screen to normal when it ends. + val generation = ++encryptionGeneration + _isEncrypting.value = true + encryptionJob = + viewModelScope.launch { + try { + debugLog(logTag, "Encrypting crypto container") + val encrypted = + CryptoContainer.encrypt( + context = context, + file = cryptoContainer.file, + dataFiles = cryptoContainer.dataFiles, + recipients = cryptoContainer.recipients, + cdoc2Settings = cdoc2Settings, + configurationRepository = configurationRepository, + ) + _encryptedContainer.postValue(encrypted) + handleIsContainerEncrypted(true) + debugLog(logTag, "Crypto container encrypted successfully") + } catch (ex: DataFilesEmptyException) { + errorLog(logTag, "Unable to encrypt: container has no data files", ex) + _errorState.postValue(R.string.crypto_encrypt_data_files_empty_error) + } catch (ex: RecipientsEmptyException) { + errorLog(logTag, "Unable to encrypt: container has no recipients", ex) + _errorState.postValue(R.string.crypto_encrypt_recipients_empty_error) + } catch (e: CancellationException) { + debugLog(logTag, "Encryption cancelled") + throw e + } catch (ex: Exception) { + errorLog(logTag, "Unable to encrypt crypto container", ex) + _errorState.postValue(R.string.crypto_encrypt_error) + } finally { + if (generation == encryptionGeneration) { + _isEncrypting.value = false + } + } + } + } + + fun cancelEncryption() { + debugLog(logTag, "Cancelling encryption") + encryptionJob?.cancel() + encryptionJob = null + _isEncrypting.value = false + } + + fun resetEncryptedContainer() { + _encryptedContainer.value = null + } + private fun filterRecipients() = queryText .combine(_recipientList) { text, _ -> @@ -150,33 +224,6 @@ class EncryptRecipientViewModel handleIsRecipientAdded(true) } - suspend fun encryptContainer(sharedContainerViewModel: SharedContainerViewModel) { - var cryptoContainer = sharedContainerViewModel.cryptoContainer.value - if (cryptoContainer != null) { - try { - cryptoContainer = - CryptoContainer.encrypt( - context = context, - file = cryptoContainer.file, - dataFiles = cryptoContainer.dataFiles, - recipients = cryptoContainer.recipients, - cdoc2Settings = cdoc2Settings, - configurationRepository = configurationRepository, - ) - sharedContainerViewModel.setCryptoContainer(cryptoContainer, true) - handleIsContainerEncrypted(true) - } catch (_: DataFilesEmptyException) { - _errorState.postValue(R.string.crypto_encrypt_data_files_empty_error) - } catch (_: RecipientsEmptyException) { - _errorState.postValue(R.string.crypto_encrypt_recipients_empty_error) - } catch (_: Exception) { - _errorState.postValue(R.string.crypto_encrypt_error) - } - } else { - _errorState.postValue(R.string.crypto_encrypt_error) - } - } - fun getMimetype(file: File): String? = mimeTypeResolver.mimeType(file) fun onSearchTextChange(text: String) { 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 42c34e5f9..b17a3184b 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt @@ -732,7 +732,11 @@ class NFCViewModel ) } - fun handleBackButton() { + fun handleBackButton(activity: Activity) { + debugLog(logTag, "Back pressed - stopping NFC reader mode and resetting state") + stopNFCDetectionTimeout() + nfcSmartCardReaderManager.disableNfcReaderMode() + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED _shouldResetPIN.postValue(true) resetValues() } diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 86fffb56a..e069e2a03 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -171,6 +171,8 @@ Adressaat Dekrüpteerimine võimalik kuni %1$s Dekrüpteerimise võimalus aegus %1$s + Aegub %1$s + Aegus %1$s (kehtiv kuni %1$s) Ümbriku failid Krüpteeritud failid @@ -609,8 +611,8 @@ Kasuta krüpteerimiseks CDOC2 failiformaati Server UUID - Fetch URL - Post URL + Vastuvõtmise URL + Saatmise URL Vali isiku tuvastamise meetod diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1f4b4eb0c..c5c37fcc9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -171,6 +171,8 @@ Recipient Decryption until %1$s Decryption expired %1$s + Expires on %1$s + Expired on %1$s (valid to %1$s) Container files Encrypted files diff --git a/crypto-lib/build.gradle.kts b/crypto-lib/build.gradle.kts index 42fcb3406..5f6553a2b 100644 --- a/crypto-lib/build.gradle.kts +++ b/crypto-lib/build.gradle.kts @@ -69,6 +69,7 @@ dependencies { implementation(libs.bouncy.castle) api(libs.guava) implementation(libs.unboundid.ldapsdk) + implementation(libs.okhttp3) implementation(libs.cdoc4j) implementation(libs.preferencex) implementation(libs.stax.api) diff --git a/crypto-lib/src/androidTest/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainerTest.kt b/crypto-lib/src/androidTest/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainerTest.kt index 08a422c71..b4d4f2c91 100644 --- a/crypto-lib/src/androidTest/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainerTest.kt +++ b/crypto-lib/src/androidTest/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainerTest.kt @@ -1305,6 +1305,80 @@ class CryptoContainerTest { assertFalse(cryptoContainer.isExistingContainer) } + @Test + fun cdoc2Settings_isManualKeyServerUrl_matchesWhenRequestUrlHasPath() { + setManualKeyServerUrls("https://keyserver.example.com") + + assertTrue(cdoc2Settings.isManualKeyServerUrl("https://keyserver.example.com/key-capsules")) + } + + @Test + fun cdoc2Settings_isManualKeyServerUrl_matchesSchemelessManualUrl() { + setManualKeyServerUrls("keyserver.example.com:8443") + + assertTrue(cdoc2Settings.isManualKeyServerUrl("https://keyserver.example.com:8443/key-capsules")) + } + + @Test + fun cdoc2Settings_isManualKeyServerUrl_matchesExplicitDefaultPortAgainstImplicitOne() { + setManualKeyServerUrls("https://keyserver.example.com:443") + + assertTrue(cdoc2Settings.isManualKeyServerUrl("https://keyserver.example.com/key-capsules")) + } + + @Test + fun cdoc2Settings_isManualKeyServerUrl_falseWhenPortDiffers() { + setManualKeyServerUrls("https://keyserver.example.com:8443") + + assertFalse(cdoc2Settings.isManualKeyServerUrl("https://keyserver.example.com/key-capsules")) + } + + @Test + fun cdoc2Settings_isManualKeyServerUrl_falseWhenSchemeDefaultPortDiffers() { + setManualKeyServerUrls("keyserver.example.com") + + assertFalse(cdoc2Settings.isManualKeyServerUrl("http://keyserver.example.com/key-capsules")) + } + + @Test + fun cdoc2Settings_isManualKeyServerUrl_matchesHostContainingUnderscore() { + setManualKeyServerUrls("https://key_server.example.com") + + assertTrue(cdoc2Settings.isManualKeyServerUrl("https://key_server.example.com/key-capsules")) + } + + @Test + fun cdoc2Settings_isManualKeyServerUrl_falseWhenNoManualServerConfigured() { + assertFalse(cdoc2Settings.isManualKeyServerUrl("https://keyserver.example.com/key-capsules")) + } + + @Test + fun cdoc2Settings_isManualKeyServerUrl_falseWhenUrlIsUnparseable() { + setManualKeyServerUrls("https://keyserver.example.com") + + assertFalse(cdoc2Settings.isManualKeyServerUrl("not a url")) + } + + @Test + fun cdoc2Settings_isManualKeyServerUrl_falseWhenUrlIsNull() { + setManualKeyServerUrls("https://keyserver.example.com") + + assertFalse(cdoc2Settings.isManualKeyServerUrl(null)) + } + + @Suppress("SameParameterValue") + private fun setManualKeyServerUrls(url: String) { + preferences + .edit() + .putString( + resources.getString(R.string.crypto_settings_use_cdoc2_post_url), + url, + ).putString( + resources.getString(R.string.crypto_settings_use_cdoc2_fetch_url), + url, + ).commit() + } + @Suppress("SameParameterValue") private fun createTempFileWithStringContent( filename: String, diff --git a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CDOC2Settings.kt b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CDOC2Settings.kt index 45c67b97d..71ead1c07 100644 --- a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CDOC2Settings.kt +++ b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CDOC2Settings.kt @@ -29,6 +29,8 @@ import ee.ria.DigiDoc.common.Constant.DIR_CRYPTO_CERT import ee.ria.DigiDoc.common.Constant.Defaults.DEFAULT_UUID_VALUE import ee.ria.DigiDoc.configuration.repository.ConfigurationRepository import ee.ria.DigiDoc.utilsLib.file.FileUtil +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import javax.inject.Inject class CDOC2Settings @@ -40,6 +42,7 @@ class CDOC2Settings private var preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) private var resources: Resources = context.resources + private val logTag = "CDOC2Settings" fun getUseEncryption(): Boolean { val defaultValue = configurationRepository.getConfiguration()?.cdoc2Default ?: false @@ -117,4 +120,25 @@ class CDOC2Settings } return null } + + fun isManualKeyServerUrl(url: String?): Boolean { + val target = url?.let(::hostAndPort) ?: return false + val manualUrls = + listOf( + preferences.getString(resources.getString(R.string.crypto_settings_use_cdoc2_post_url), "") ?: "", + preferences.getString(resources.getString(R.string.crypto_settings_use_cdoc2_fetch_url), "") ?: "", + ) + return manualUrls.any { manual -> + manual.isNotBlank() && hostAndPort(manual) == target + } + } + + private fun hostAndPort(url: String): String? { + val parsed = (if (url.contains("://")) url else "https://$url").toHttpUrlOrNull() + if (parsed == null) { + errorLog(logTag, "Unable to parse key server URL, manual certificate not applied") + return null + } + return "${parsed.host}:${parsed.port}" + } } diff --git a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainer.kt b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainer.kt index a53db026f..1baa3b2f3 100644 --- a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainer.kt +++ b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainer.kt @@ -57,6 +57,10 @@ import ee.ria.cdoc.Logger import ee.ria.cdoc.NetworkBackend import ee.ria.cdoc.Recipient import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.apache.commons.io.FilenameUtils import org.openeid.cdoc4j.CDOCParser @@ -64,11 +68,11 @@ import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException -import java.io.InputStream -import java.io.OutputStream import java.util.Base64 +import java.util.concurrent.locks.ReentrantLock import javax.inject.Inject import javax.inject.Singleton +import kotlin.concurrent.withLock private const val LOG_TAG = "CryptoContainer" @@ -166,6 +170,11 @@ class CryptoContainer val logger = JavaLogger() var loggingIsSet = false + // Encryption can pause and resume midway, so it needs the coroutine-friendly Mutex. + // Decryption runs start-to-finish in one go, so a plain lock (ReentrantLock) is enough. + private val encryptOperation = Mutex() + private val decryptOperation = ReentrantLock() + @Throws(CryptoException::class) private suspend fun open( context: Context, @@ -273,74 +282,78 @@ class CryptoContainer smartToken: Token, cdoc2Settings: CDOC2Settings, configurationRepository: ConfigurationRepository, - ): CryptoContainer { - val token = SmartCardTokenWrapper(pin, smartToken) - val conf = CryptoLibConf(cdoc2Settings) - val configurationProvider = configurationRepository.getConfiguration() + ): CryptoContainer = + decryptOperation.withLock { + val token = SmartCardTokenWrapper(pin, smartToken) + val conf = CryptoLibConf(cdoc2Settings) + val configurationProvider = configurationRepository.getConfiguration() - if (authCert == null || authCert.isEmpty()) { - throw CryptoException("Failed to get auth certificate") - } - val network = CryptoLibNetworkBackend(cdoc2Settings, configurationProvider, context, authCert, token) - val dataFiles = ArrayList() + if (authCert == null || authCert.isEmpty()) { + throw CryptoException("Failed to get auth certificate") + } + val network = + CryptoLibNetworkBackend(cdoc2Settings, configurationProvider, context, authCert, token) + val dataFiles = ArrayList() - val cdocReader = CDocReader.createReader(file.path, conf, token, network) - debugLog(LOG_TAG, "Reader created: (version ${cdocReader.version})") - val idx = cdocReader.getLockForCert(authCert) + val cdocReader = CDocReader.createReader(file.path, conf, token, network) + debugLog(LOG_TAG, "Reader created: (version ${cdocReader.version})") + try { + val idx = cdocReader.getLockForCert(authCert) - if (idx < 0) { - throw CryptoException("Failed to get lock for certificate") - } + if (idx < 0) { + throw CryptoException("Failed to get lock for certificate") + } - val fmk = cdocReader.getFMK(idx.toInt()) + val fmk = cdocReader.getFMK(idx.toInt()) - if (token.lastError != null) { - throw token.lastError as Throwable - } + if (token.lastError != null) { + throw token.lastError as Throwable + } - if (fmk.isEmpty()) { - throw CryptoException("Failed to get FMK") - } + if (fmk.isEmpty()) { + throw CryptoException("Failed to get FMK") + } - if (cdocReader.beginDecryption(fmk) != 0L) { - throw CryptoException("Failed to begin decryption") - } + if (cdocReader.beginDecryption(fmk) != 0L) { + throw CryptoException("Failed to begin decryption") + } - val fi = FileInfo() - var result: Long = cdocReader.nextFile(fi) - try { - while (result == CDoc.OK.toLong()) { - val ofile = File(fi.name) - val dir = - ContainerUtil.getContainerDataFilesDir( - context, - file, - ) - val tmp = sanitizeString(ofile.name, "") - val fileToSave = File(dir, tmp) - val ofs: OutputStream = FileOutputStream(fileToSave) - cdocReader.readFile(ofs) - dataFiles.add(fileToSave) - ofs.close() - result = cdocReader.nextFile(fi) - } - } catch (exc: IOException) { - throw CryptoException("IO Exception: ${exc.message}", exc) - } + val fi = FileInfo() + var result: Long = cdocReader.nextFile(fi) + while (result == CDoc.OK.toLong()) { + val ofile = File(fi.name) + val dir = + ContainerUtil.getContainerDataFilesDir( + context, + file, + ) + val tmp = sanitizeString(ofile.name, "") + val fileToSave = File(dir, tmp) + FileOutputStream(fileToSave).use { ofs -> + cdocReader.readFile(ofs) + } + dataFiles.add(fileToSave) + result = cdocReader.nextFile(fi) + } - if (cdocReader.finishDecryption() != 0L) { - throw CryptoException("Failed to finish decryption") - } + if (cdocReader.finishDecryption() != 0L) { + throw CryptoException("Failed to finish decryption") + } - return create( - context, - file, - dataFiles, - recipients, - decrypted = true, - encrypted = false, - ) - } + create( + context, + file, + dataFiles, + recipients, + decrypted = true, + encrypted = false, + ) + } catch (exc: IOException) { + throw CryptoException("IO Exception: ${exc.message}", exc) + } finally { + cdocReader.delete() + } + } @Throws(CryptoException::class) suspend fun encrypt( @@ -358,70 +371,94 @@ class CryptoContainer if (recipients.isEmpty()) { throw RecipientsEmptyException("Cannot create crypto container without recipients") } - val configurationProvider = configurationRepository.getConfiguration() - val conf = CryptoLibConf(cdoc2Settings) - - val network = Network(cdoc2Settings, configurationProvider, context) + return withContext(IO) { + encryptOperation.withLock { + val configurationProvider = configurationRepository.getConfiguration() + val conf = CryptoLibConf(cdoc2Settings) + val network = Network(cdoc2Settings, configurationProvider, context) + + val version = + if (file.extension == CDOC2_EXTENSION) { + 2 + } else { + 1 + } - val version = - if (file.extension == CDOC2_EXTENSION) { - 2 - } else { - 1 - } + debugLog( + LOG_TAG, + "Encrypting container (CDOC version $version, " + + "online key transfer: ${version == 2 && cdoc2Settings.getUseOnlineEncryption()})", + ) - val cdocWriter = CDocWriter.createWriter(version, file.path, conf, null, network) - try { - withContext(IO) { - if (version == 2 && cdoc2Settings.getUseOnlineEncryption()) { - val serverId = cdoc2Settings.getCDOC2UUID() - recipients.forEach { addressee -> - val recipient = Recipient.makeCertificate("", addressee.data, serverId) - if (cdocWriter.addRecipient(recipient) != 0L) { - throw CryptoException("Failed to add recipient") + val cdocWriter = CDocWriter.createWriter(version, file.path, conf, null, network) + var encryptionFinished = false + try { + if (version == 2 && cdoc2Settings.getUseOnlineEncryption()) { + val serverId = cdoc2Settings.getCDOC2UUID() + recipients.forEach { addressee -> + currentCoroutineContext().ensureActive() + val recipient = Recipient.makeCertificate("", addressee.data, serverId) + if (cdocWriter.addRecipient(recipient) != 0L) { + throw CryptoException("Failed to add recipient") + } } - } - } else { - recipients.forEach { addressee -> - val recipient = Recipient.makeCertificate("", addressee.data) - if (cdocWriter.addRecipient(recipient) != 0L) { - throw CryptoException("Failed to add recipient") + } else { + recipients.forEach { addressee -> + val recipient = Recipient.makeCertificate("", addressee.data) + if (cdocWriter.addRecipient(recipient) != 0L) { + throw CryptoException("Failed to add recipient") + } } } - } - } - if (cdocWriter.beginEncryption() != 0L) { - throw CryptoException("Failed to begin encryption") - } - withContext(IO) { - dataFiles.forEach { dataFile -> - val ifs: InputStream = FileInputStream(dataFile) - val bytes = ifs.readBytes() - if (cdocWriter.addFile(dataFile.name, bytes.size.toLong()) != 0L) { - throw CryptoException("Failed to add file") + + currentCoroutineContext().ensureActive() + + if (cdocWriter.beginEncryption() != 0L) { + throw CryptoException("Failed to begin encryption") + } + + dataFiles.forEach { dataFile -> + currentCoroutineContext().ensureActive() + val bytes = FileInputStream(dataFile).use { it.readBytes() } + if (cdocWriter.addFile(dataFile.name, bytes.size.toLong()) != 0L) { + throw CryptoException("Failed to add file") + } + + if (cdocWriter.writeData(bytes) != 0L) { + throw CryptoException("Failed to write data") + } } - if (cdocWriter.writeData(bytes) != 0L) { - throw CryptoException("Failed to write data") + currentCoroutineContext().ensureActive() + + if (cdocWriter.finishEncryption() != 0L) { + throw CryptoException("Failed to finish encryption") + } + encryptionFinished = true + debugLog(LOG_TAG, "Encryption finished successfully") + } catch (exc: IOException) { + errorLog(LOG_TAG, "IO Exception: ${exc.message}", exc) + throw CryptoException("IO Exception: ${exc.message}", exc) + } catch (exc: CDocException) { + errorLog(LOG_TAG, "CDoc Exception ${exc.code}: ${exc.message}", exc) + throw CryptoException("CDoc Exception ${exc.code}: ${exc.message}", exc) + } finally { + cdocWriter.delete() + if (!encryptionFinished) { + // A cancelled or failed run leaves a half-written container behind. + // Emptying it restores the placeholder the container started as, so it + // cannot show up as a broken entry in recent documents. + try { + FileOutputStream(file).use { } + } catch (exc: IOException) { + errorLog(LOG_TAG, "Unable to empty unfinished container file", exc) + } } - ifs.close() } - } - if (cdocWriter.finishEncryption() != 0L) { - throw CryptoException("Failed to finish encryption") + open(context, file) } - } catch (exc: IOException) { - errorLog(LOG_TAG, "IO Exception: ${exc.message}", exc) - throw CryptoException("IO Exception: ${exc.message}", exc) - } catch (exc: CDocException) { - errorLog(LOG_TAG, "CDoc Exception ${exc.code}: ${exc.message}", exc) - throw CryptoException("CDoc Exception ${exc.code}: ${exc.message}", exc) - } finally { - cdocWriter.delete() } - - return open(context, file) } @Throws(CryptoException::class) @@ -548,11 +585,14 @@ class CryptoContainer val certBytes = Base64.getDecoder().decode(cert) dst?.addCertificate(certBytes) } - val certFromSettings = cdoc2Settings.getCDOC2Cert() + val certFromSettings = + if (cdoc2Settings.isManualKeyServerUrl(url)) cdoc2Settings.getCDOC2Cert() else null if (certFromSettings != null) { - val certBytes = - Base64.getDecoder().decode(certFromSettings) - dst?.addCertificate(certBytes) + try { + dst?.addCertificate(Base64.getDecoder().decode(certFromSettings)) + } catch (e: IllegalArgumentException) { + errorLog(LOG_TAG, "Failed to decode manual key-server certificate", e) + } } return CDoc.OK.toLong() diff --git a/libdigidoc-lib/src/androidTest/kotlin/ee/ria/DigiDoc/libdigidoclib/SignedContainerTest.kt b/libdigidoc-lib/src/androidTest/kotlin/ee/ria/DigiDoc/libdigidoclib/SignedContainerTest.kt index f292dd7f7..9c0afadb9 100644 --- a/libdigidoc-lib/src/androidTest/kotlin/ee/ria/DigiDoc/libdigidoclib/SignedContainerTest.kt +++ b/libdigidoc-lib/src/androidTest/kotlin/ee/ria/DigiDoc/libdigidoclib/SignedContainerTest.kt @@ -720,6 +720,9 @@ class SignedContainerTest { @Test fun signedContainer_isExistingContainer_trueForSignedPdf() = runTest { + val isTestEnabled = System.getenv("WITH_EXTRA_DIGIDOC_TESTS")?.toBoolean() == true + assumeTrue("Is test enabled: $isTestEnabled", isTestEnabled) + val signedContainer = openOrCreate(context, signedPdfDocument, listOf(signedPdfDocument), true) assertTrue(signedContainer.isExistingContainer()) diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/SignedContainer.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/SignedContainer.kt index b91068b85..d7f9e8fd5 100644 --- a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/SignedContainer.kt +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/SignedContainer.kt @@ -394,10 +394,12 @@ class SignedContainer message.startsWith("Failed to connect to host") || message.startsWith("Failed to create proxy connection with host") || message.startsWith("Failed to create connection with host") -> { + errorLog(LOG_TAG, "Unable to open container, connection to host failed", e) throw NoInternetConnectionException(context) } message.startsWith("Failed to create ssl connection with host") -> { + errorLog(LOG_TAG, "Unable to open container, SSL connection to host failed", e) throw SSLHandshakeException(context) }