diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/FileOpeningViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/FileOpeningViewModelTest.kt index 742abd24a..c3882ea5b 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/FileOpeningViewModelTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/FileOpeningViewModelTest.kt @@ -56,6 +56,7 @@ import ee.ria.DigiDoc.domain.repository.fileopening.FileOpeningRepository import ee.ria.DigiDoc.domain.repository.siva.SivaRepository import ee.ria.DigiDoc.exceptions.EmptyFileException import ee.ria.DigiDoc.libdigidoclib.SignedContainer +import ee.ria.DigiDoc.libdigidoclib.exceptions.SSLHandshakeException import ee.ria.DigiDoc.libdigidoclib.init.Initialization import ee.ria.DigiDoc.libdigidoclib.init.LibdigidocLibraryLoader import ee.ria.DigiDoc.utilsLib.mimetype.MimeTypeResolver @@ -759,6 +760,30 @@ class FileOpeningViewModelTest { verify(errorStateObserver).onChanged(Pair(R.string.no_internet_connection, null)) } + @Test + fun fileOpeningViewModel_handleFiles_sslHandshakeException() = + runTest { + val uri: Uri = mock() + val uris = listOf(uri) + val exception = SSLHandshakeException(context) + + val isSivaConfirmed = true + + `when`(fileOpeningRepository.openOrCreateContainer(context, contentResolver, uris, isSivaConfirmed)) + .thenThrow(exception) + + viewModel.handleFiles( + context, + uris, + null, + isSivaConfirmed = isSivaConfirmed, + fileOpeningMethod = FileOpeningMethod.ALL, + ) + + verify(signedContainerObserver, atLeastOnce()).onChanged(null) + verify(errorStateObserver).onChanged(Pair(R.string.invalid_ssl_handshake, null)) + } + @Test fun fileOpeningViewModel_handleFiles_failWhenNullContainer() = runTest { diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModelTest.kt index 623db8d1d..b56ecfa0b 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModelTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModelTest.kt @@ -21,14 +21,14 @@ package ee.ria.DigiDoc.viewmodel.shared -import android.content.ContentResolver import android.content.Context import android.net.Uri import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.documentfile.provider.DocumentFile import androidx.test.platform.app.InstrumentationRegistry import com.google.gson.Gson import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.common.Constant.DIR_CRYPTO_CERT +import ee.ria.DigiDoc.common.Constant.DIR_SIVA_CERT import ee.ria.DigiDoc.common.Constant.DIR_TSA_CERT import ee.ria.DigiDoc.common.Constant.Defaults.DEFAULT_UUID_VALUE import ee.ria.DigiDoc.common.testfiles.asset.AssetFile @@ -52,7 +52,6 @@ import ee.ria.libdigidocpp.DigiDocConf import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer -import org.apache.commons.io.FileUtils import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -68,7 +67,6 @@ import org.mockito.MockitoAnnotations import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.mock import java.io.File -import java.io.InputStream import java.nio.charset.Charset import java.nio.file.Files import java.util.TimeZone @@ -81,9 +79,6 @@ class SharedSettingsViewModelTest { @get:Rule val mockWebServer = MockWebServer() - @Mock - lateinit var contentResolver: ContentResolver - @Mock lateinit var configurationRepository: ConfigurationRepository @@ -145,7 +140,7 @@ class SharedSettingsViewModelTest { viewModel = SharedSettingsViewModel( context = context, - contentResolver = contentResolver, + contentResolver = context.contentResolver, dataStore = dataStore, configurationRepository = configurationRepository, initialization = initialization, @@ -162,14 +157,9 @@ class SharedSettingsViewModelTest { @Test fun sharedSettingsViewModel_resetToDefaultSettings_success() { - val file = - AssetFile.getResourceFileAsFile( - context, - "siva.cer", - ee.ria.DigiDoc.common.R.raw.siva, - ) - val uri = Uri.fromFile(file) - saveTsaCert(uri) + val tsaCertFile = writeCertFile(DIR_TSA_CERT, "tsaCert") + dataStore.setTSACertName(tsaCertFile.name) + viewModel.resetToDefaultSettings() // resetSigningSettings @@ -180,6 +170,7 @@ class SharedSettingsViewModelTest { assertFalse(dataStore.getIsTsaCertificateViewVisible()) assertEquals("", dataStore.getTSACertName()) + assertFalse(tsaCertFile.exists()) // resetRightsSettings assertTrue(dataStore.getSettingsOpenAllFileTypes()) @@ -207,6 +198,39 @@ class SharedSettingsViewModelTest { assertFalse(dataStore.getSettingsDefaultLTA()) } + @Test + fun sharedSettingsViewModel_resetToDefaultSettings_deletesSivaCertificateFile() { + val certFile = writeCertFile(DIR_SIVA_CERT, "sivaCert") + dataStore.setSettingsSivaCertName(certFile.name) + + viewModel.resetToDefaultSettings() + + assertFalse(certFile.exists()) + assertEquals("", dataStore.getSettingsSivaCertName()) + } + + @Test + fun sharedSettingsViewModel_resetToDefaultSettings_deletesTsaCertificateFile() { + val certFile = writeCertFile(DIR_TSA_CERT, "tsaCert") + dataStore.setTSACertName(certFile.name) + + viewModel.resetToDefaultSettings() + + assertFalse(certFile.exists()) + assertEquals("", dataStore.getTSACertName()) + } + + @Test + fun sharedSettingsViewModel_resetToDefaultSettings_deletesCryptoCertificateFile() { + val certFile = writeCertFile(DIR_CRYPTO_CERT, "cryptoCert") + dataStore.setCryptoCertName(certFile.name) + + viewModel.resetToDefaultSettings() + + assertFalse(certFile.exists()) + assertEquals("", dataStore.getCryptoCertName()) + } + @Test fun sharedSettingsViewModel_saveProxySettings_savesManualProxySettings() { dataStore.setProxySetting(ProxySetting.MANUAL_PROXY) @@ -343,7 +367,9 @@ class SharedSettingsViewModelTest { val uri = Uri.fromFile(file) viewModel.handleSivaFile(uri) + assertEquals("sivaCert", dataStore.getSettingsSivaCertName()) + assertTrue(File(File(context.filesDir, DIR_SIVA_CERT), "sivaCert").exists()) } @Test(expected = Test.None::class) @@ -364,7 +390,9 @@ class SharedSettingsViewModelTest { val uri = Uri.fromFile(file) viewModel.handleTsaFile(uri) + assertEquals("tsaCert", dataStore.getTSACertName()) + assertTrue(File(File(context.filesDir, DIR_TSA_CERT), "tsaCert").exists()) } @Test(expected = Test.None::class) @@ -374,6 +402,60 @@ class SharedSettingsViewModelTest { viewModel.handleTsaFile(uri) } + @Test + fun sharedSettingsViewModel_handleSivaFile_deletesPreviouslyAddedCertificateFile() { + val previousCertFile = writeCertFile(DIR_SIVA_CERT, "previousSivaCert") + dataStore.setSettingsSivaCertName(previousCertFile.name) + val file = + AssetFile.getResourceFileAsFile( + context, + "siva.cer", + ee.ria.DigiDoc.common.R.raw.siva, + ) + val uri = Uri.fromFile(file) + viewModel.handleSivaFile(uri) + + assertFalse(previousCertFile.exists()) + assertEquals("sivaCert", dataStore.getSettingsSivaCertName()) + assertTrue(File(File(context.filesDir, DIR_SIVA_CERT), "sivaCert").exists()) + } + + @Test + fun sharedSettingsViewModel_handleTsaFile_deletesPreviouslyAddedCertificateFile() { + val previousCertFile = writeCertFile(DIR_TSA_CERT, "previousTsaCert") + dataStore.setTSACertName(previousCertFile.name) + val file = + AssetFile.getResourceFileAsFile( + context, + "siva.cer", + ee.ria.DigiDoc.common.R.raw.siva, + ) + val uri = Uri.fromFile(file) + viewModel.handleTsaFile(uri) + + assertFalse(previousCertFile.exists()) + assertEquals("tsaCert", dataStore.getTSACertName()) + assertTrue(File(File(context.filesDir, DIR_TSA_CERT), "tsaCert").exists()) + } + + @Test + fun sharedSettingsViewModel_handleCryptoCertFile_deletesPreviouslyAddedCertificateFile() { + val previousCertFile = writeCertFile(DIR_CRYPTO_CERT, "previousCryptoCert") + dataStore.setCryptoCertName(previousCertFile.name) + val file = + AssetFile.getResourceFileAsFile( + context, + "siva.cer", + ee.ria.DigiDoc.common.R.raw.siva, + ) + val uri = Uri.fromFile(file) + viewModel.handleCryptoCertFile(uri) + + assertFalse(previousCertFile.exists()) + assertEquals("cryptoCert", dataStore.getCryptoCertName()) + assertTrue(File(File(context.filesDir, DIR_CRYPTO_CERT), "cryptoCert").exists()) + } + @Test fun sharedSettingsViewModel_checkConnection_savesManualProxySettings() { mockWebServer.enqueue(MockResponse().setResponseCode(407)) @@ -458,28 +540,16 @@ class SharedSettingsViewModelTest { return tempFile } - private fun saveTsaCert(uri: Uri) { - try { - val initialStream: InputStream? = contentResolver.openInputStream(uri) - val documentFile = DocumentFile.fromSingleUri(context, uri) - if (documentFile != null) { - val tsaCertFolder = File(context.filesDir, DIR_TSA_CERT) - if (!tsaCertFolder.exists()) { - tsaCertFolder.mkdirs() - } - - var fileName = documentFile.name - if (fileName.isNullOrEmpty()) { - fileName = "tsaCert" - } - val tsaFile = File(tsaCertFolder, fileName) - - FileUtils.copyInputStreamToFile(initialStream, tsaFile) - - dataStore.setTSACertName(tsaFile.name) - } - } catch (_: Exception) { - // Do nothing + private fun writeCertFile( + certFolder: String, + fileName: String, + ): File { + val folder = File(context.filesDir, certFolder) + if (!folder.exists()) { + folder.mkdirs() } + val certFile = File(folder, fileName) + Files.write(certFile.toPath(), "cert".toByteArray(Charset.defaultCharset())) + return certFile } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/CryptoFileOpeningViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/CryptoFileOpeningViewModel.kt index ac606a730..88710aca2 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/CryptoFileOpeningViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/CryptoFileOpeningViewModel.kt @@ -38,6 +38,7 @@ import ee.ria.DigiDoc.cryptolib.CryptoContainer import ee.ria.DigiDoc.domain.repository.fileopening.FileOpeningRepository import ee.ria.DigiDoc.exceptions.EmptyFileException import ee.ria.DigiDoc.exceptions.FileAlreadyExistsException +import ee.ria.DigiDoc.libdigidoclib.exceptions.SSLHandshakeException import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.DigiDoc.viewmodel.shared.SharedContainerViewModel @@ -195,6 +196,9 @@ class CryptoFileOpeningViewModel is NoInternetConnectionException -> { _errorState.postValue(Pair(R.string.no_internet_connection, null)) } + is SSLHandshakeException -> { + _errorState.postValue(Pair(R.string.invalid_ssl_handshake, null)) + } is IOException -> { val message = e.message ?: "" if (message.startsWith("Failed to create connection with host") || diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/FileOpeningViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/FileOpeningViewModel.kt index 3b6de437b..cba4e1d54 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/FileOpeningViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/FileOpeningViewModel.kt @@ -43,6 +43,7 @@ import ee.ria.DigiDoc.domain.repository.siva.SivaRepository import ee.ria.DigiDoc.exceptions.EmptyFileException import ee.ria.DigiDoc.exceptions.FileAlreadyExistsException import ee.ria.DigiDoc.libdigidoclib.SignedContainer +import ee.ria.DigiDoc.libdigidoclib.exceptions.SSLHandshakeException import ee.ria.DigiDoc.utilsLib.extensions.isCryptoContainer import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog @@ -272,6 +273,9 @@ class FileOpeningViewModel is NoInternetConnectionException -> { _errorState.postValue(Pair(R.string.no_internet_connection, null)) } + is SSLHandshakeException -> { + _errorState.postValue(Pair(R.string.invalid_ssl_handshake, null)) + } is IOException -> { val message = e.message ?: "" if (message.startsWith("Failed to create connection with host") || diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/RecentDocumentsViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/RecentDocumentsViewModel.kt index 6bdd46dd2..e4dddd943 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/RecentDocumentsViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/RecentDocumentsViewModel.kt @@ -38,6 +38,7 @@ import ee.ria.DigiDoc.cryptolib.CDOC2Settings import ee.ria.DigiDoc.cryptolib.CryptoContainer import ee.ria.DigiDoc.domain.repository.siva.SivaRepository import ee.ria.DigiDoc.libdigidoclib.SignedContainer +import ee.ria.DigiDoc.libdigidoclib.exceptions.SSLHandshakeException import ee.ria.DigiDoc.utilsLib.container.ContainerUtil import ee.ria.DigiDoc.utilsLib.extensions.isCades import ee.ria.DigiDoc.utilsLib.extensions.isCryptoContainer @@ -172,6 +173,10 @@ class RecentDocumentsViewModel errorMessage = R.string.no_internet_connection } + if (ex is SSLHandshakeException) { + errorMessage = R.string.invalid_ssl_handshake + } + _errorState.postValue(errorMessage) } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt index 03d8861e5..d30933ed7 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt @@ -69,7 +69,6 @@ import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder import java.io.File import java.io.IOException -import java.io.InputStream import java.security.cert.CertificateException import java.security.cert.X509Certificate import java.text.SimpleDateFormat @@ -219,7 +218,6 @@ class SharedSettingsViewModel private fun resetSivaSettings() { dataStore.setSivaSetting(SivaSetting.DEFAULT) dataStore.setSettingsSivaUrl("") - dataStore.setSettingsSivaCertName(null) removeSivaCert() } @@ -236,16 +234,7 @@ class SharedSettingsViewModel setSettingsAskRoleAndAddress(false) dataStore.setSettingsDefaultLTA(false) dataStore.setIsTsaCertificateViewVisible(false) - val certFile = - FileUtil.getCertFile(context, dataStore.getTSACertName(), DIR_TSA_CERT) - removeCertificate(certFile) - } - - private fun removeCertificate(tsaFile: File?) { - if (tsaFile != null) { - FileUtil.removeFile(tsaFile.path) - } - dataStore.setTSACertName(null) + removeTsaCert() } private fun removeSivaCert() { @@ -430,71 +419,74 @@ class SharedSettingsViewModel } fun handleSivaFile(uri: Uri) { - try { - val initialStream: InputStream? = - contentResolver.openInputStream(uri) - ?: throw IllegalStateException("Unable to open input stream for SiVa certificate URI") - val sivaCertFolder = File(context.filesDir, DIR_SIVA_CERT) - if (!sivaCertFolder.exists()) { - val isFolderCreated = sivaCertFolder.mkdirs() - debugLog(logTag, String.format("SiVa cert folder created: %s", isFolderCreated)) - } - val fileName = - DocumentFile - .fromSingleUri(context, uri) - ?.name - .takeUnless { it.isNullOrEmpty() } ?: "sivaCert" - val sivaFile = File(sivaCertFolder, fileName) - FileUtils.copyInputStreamToFile(initialStream, sivaFile) - dataStore.setSettingsSivaCertName(sivaFile.name) - } catch (e: Exception) { - errorLog(logTag, "Unable to read SiVa certificate file data", e) - } + saveCertFile( + uri = uri, + certFolder = DIR_SIVA_CERT, + defaultFileName = "sivaCert", + label = "SiVa", + currentCertName = dataStore.getSettingsSivaCertName(), + setCertName = dataStore::setSettingsSivaCertName, + ) } fun handleTsaFile(uri: Uri) { - try { - val initialStream: InputStream? = - contentResolver.openInputStream(uri) - ?: throw IllegalStateException("Unable to open input stream for TSA certificate URI") - val tsaCertFolder = File(context.filesDir, DIR_TSA_CERT) - if (!tsaCertFolder.exists()) { - val isFolderCreated = tsaCertFolder.mkdirs() - debugLog(logTag, String.format("TSA cert folder created: %s", isFolderCreated)) - } - val fileName = - DocumentFile - .fromSingleUri(context, uri) - ?.name - .takeUnless { it.isNullOrEmpty() } ?: "tsaCert" - val tsaFile = File(tsaCertFolder, fileName) - FileUtils.copyInputStreamToFile(initialStream, tsaFile) - dataStore.setTSACertName(tsaFile.name) - } catch (e: Exception) { - errorLog(logTag, "Unable to read TSA certificate file data", e) - } + saveCertFile( + uri = uri, + certFolder = DIR_TSA_CERT, + defaultFileName = "tsaCert", + label = "TSA", + currentCertName = dataStore.getTSACertName(), + setCertName = dataStore::setTSACertName, + ) } fun handleCryptoCertFile(uri: Uri) { + saveCertFile( + uri = uri, + certFolder = DIR_CRYPTO_CERT, + defaultFileName = "cryptoCert", + label = "Crypto", + currentCertName = dataStore.getCryptoCertName(), + setCertName = dataStore::setCryptoCertName, + ) + } + + private fun saveCertFile( + uri: Uri, + certFolder: String, + defaultFileName: String, + label: String, + currentCertName: String, + setCertName: (String?) -> Unit, + ) { try { - val initialStream: InputStream? = - contentResolver.openInputStream(uri) - ?: throw IllegalStateException("Unable to open input stream for crypto certificate URI") - val cryptoCertFolder = File(context.filesDir, DIR_CRYPTO_CERT) - if (!cryptoCertFolder.exists()) { - val isFolderCreated = cryptoCertFolder.mkdirs() - debugLog(logTag, String.format("Crypto cert folder created: %s", isFolderCreated)) + val stream = contentResolver.openInputStream(uri) + if (stream == null) { + errorLog(logTag, "Unable to open input stream for the $label certificate URI") + return + } + stream.use { input -> + val certDir = File(context.filesDir, certFolder) + if (!certDir.exists()) { + val isFolderCreated = certDir.mkdirs() + debugLog(logTag, "$label cert folder created: $isFolderCreated") + } + val fileName = + DocumentFile + .fromSingleUri(context, uri) + ?.name + ?.takeIf(String::isNotEmpty) ?: defaultFileName + val certFile = File(certDir, fileName) + FileUtils.copyToFile(input, certFile) + setCertName(certFile.name) + if (currentCertName != certFile.name) { + FileUtil.getCertFile(context, currentCertName, certFolder)?.let { + FileUtil.removeFile(it.path) + } + } } - val fileName = - DocumentFile - .fromSingleUri(context, uri) - ?.name - .takeUnless { it.isNullOrEmpty() } ?: "cryptoCert" - val cryptoCertFile = File(cryptoCertFolder, fileName) - FileUtils.copyInputStreamToFile(initialStream, cryptoCertFile) - dataStore.setCryptoCertName(cryptoCertFile.name) } catch (e: Exception) { - errorLog(logTag, "Unable to read Crypto certificate file data", e) + errorLog(logTag, "Unable to read $label certificate file data", e) } }