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 611befac2..f1e61b0ac 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 @@ -555,7 +555,7 @@ class DataStoreTest { fun dataStore_getLibdigidocppVersion_success() { val result = dataStore.getLibdigidocppVersion() - assertEquals("4.3.0.40", result) + assertEquals("4.5.0.52", result) } @Test diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModelTest.kt index ea9a56124..4c3f9eef9 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModelTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModelTest.kt @@ -44,7 +44,6 @@ import ee.ria.DigiDoc.libdigidoclib.init.Initialization import ee.ria.DigiDoc.libdigidoclib.init.LibdigidocLibraryLoader import ee.ria.DigiDoc.smartcardreader.nfc.NfcSmartCardReaderManager import ee.ria.DigiDoc.smartcardreader.nfc.NfcSmartCardReaderManager.NfcStatus -import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch @@ -168,7 +167,7 @@ class NFCViewModelTest { viewModel.removePendingSignature(signedContainer) - assertEquals(1, signedContainer.getSignatures(Main).size) + assertEquals(1, signedContainer.getSignatures().size) } @Test @@ -477,7 +476,7 @@ class NFCViewModelTest { viewModel.cancelNFCSignWorkRequest(signedContainer) - assertEquals(1, signedContainer.getSignatures(Main).size) + assertEquals(1, signedContainer.getSignatures().size) } @Test diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/domain/service/siva/SivaServiceImpl.kt b/app/src/main/kotlin/ee/ria/DigiDoc/domain/service/siva/SivaServiceImpl.kt index ff0c85bd1..5ef4e65d7 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/domain/service/siva/SivaServiceImpl.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/domain/service/siva/SivaServiceImpl.kt @@ -31,7 +31,6 @@ import ee.ria.DigiDoc.utilsLib.extensions.isSignedPDF import ee.ria.DigiDoc.utilsLib.extensions.isXades import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.DigiDoc.utilsLib.mimetype.MimeTypeResolver -import kotlinx.coroutines.Dispatchers.Main import java.io.File import javax.inject.Inject import javax.inject.Singleton @@ -64,7 +63,7 @@ class SivaServiceImpl override suspend fun isTimestampedContainer(signedContainer: SignedContainer): Boolean = signedContainer.getDataFiles().size == 1 && signedContainer.containerMimetype().equals(ASICS_MIMETYPE) && - signedContainer.getSignatures(Main).first().profile == "TimeStampToken" + signedContainer.getSignatures().firstOrNull()?.profile == "TimeStampToken" override suspend fun getTimestampedContainer( context: Context, @@ -78,7 +77,7 @@ class SivaServiceImpl nestedContainer?.rawContainer(), parentContainer.getContainerFile(), parentContainer.isExistingContainer(), - parentContainer.getSignatures(Main), + parentContainer.getSignatures(), ) } catch (ex: Exception) { errorLog(logTag, "Unable to open timestamped container", ex) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/init/LibrarySetup.kt b/app/src/main/kotlin/ee/ria/DigiDoc/init/LibrarySetup.kt index ce964e73f..f9498ae3b 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/init/LibrarySetup.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/init/LibrarySetup.kt @@ -32,6 +32,7 @@ import ee.ria.DigiDoc.libdigidoclib.exceptions.AlreadyInitializedException import ee.ria.DigiDoc.libdigidoclib.init.Initialization import ee.ria.DigiDoc.libdigidoclib.init.LibdigidocLibraryLoader import ee.ria.DigiDoc.utils.snackbar.SnackBarManager +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main @@ -66,7 +67,11 @@ class LibrarySetup if (!isConfigured) { withContext(IO) { try { - TSLUtil.setupTSLFiles(context) + try { + TSLUtil.setupTSLFiles(context) + } catch (ex: Exception) { + errorLog(logTag, "Unable to set up the bundled TSL files", ex) + } configurationLoader.initConfiguration( context, dataStore.getProxySetting(), @@ -74,15 +79,19 @@ class LibrarySetup ) isConfigured = true } catch (ex: Exception) { - if (ex !is UnknownHostException && - ex !is SocketTimeoutException && - ex !is InterruptedIOException - ) { - errorLog( + errorLog(logTag, "Unable to initialize configuration", ex) + + val isNetworkFailure = + ex is UnknownHostException || + ex is SocketTimeoutException || + ex is InterruptedIOException + if (isNetworkFailure) { + debugLog( logTag, - "Unable to initialize configuration: ", - ex, + "Configuration was not refreshed because the network is unreachable; " + + "continuing with the currently cached configuration", ) + } else { withContext(Main) { SnackBarManager.showMessage( context, @@ -97,7 +106,9 @@ class LibrarySetup try { initialization.init(context, isLoggingEnabled) } catch (e: Exception) { - if (e !is AlreadyInitializedException) { + if (e is AlreadyInitializedException) { + debugLog(logTag, "libdigidocpp was already initialized") + } else { errorLog(logTag, "Unable to initialize libdigidocpp", e) withContext(Main) { Toast diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt index e62ddd9a3..755e72134 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt @@ -557,29 +557,25 @@ fun SigningNavigation( signedContainer?.let { val pastTime = System.currentTimeMillis() showSignaturesLoadingIndicator.value = true - signatures = it.getSignatures(Main) + val validatedSignatures = it.getSignatures() + val signatureCounts = it.getSignaturesStatusCount(validatedSignatures) + val timestampCounts = + if (it.containerMimetype() != ASICS_MIMETYPE) it.getTimestampStatusCount() else null + val hasEmptyFile = signingViewModel.isEmptyFileInList(it.getDataFiles()) + signatures = validatedSignatures showSignaturesLoadingIndicator.value = false withContext(Main) { - val signatureCounts = signedContainer?.getSignaturesStatusCount() - validSignaturesCount = signatureCounts?.get(ValidatorInterface.Status.Valid) ?: 0 - unknownSignaturesCount = - signatureCounts?.get(ValidatorInterface.Status.Unknown) ?: 0 - invalidSignaturesCount = - signatureCounts?.get(ValidatorInterface.Status.Invalid) ?: 0 - - if (signedContainer?.containerMimetype() != ASICS_MIMETYPE) { - val timestampCounts = signedContainer?.getTimestampStatusCount() - validTimestampsCount = timestampCounts?.get(ValidatorInterface.Status.Valid) ?: 0 - unknownTimestampsCount = - timestampCounts?.get(ValidatorInterface.Status.Unknown) ?: 0 - invalidTimestampsCount = - timestampCounts?.get(ValidatorInterface.Status.Invalid) ?: 0 + validSignaturesCount = signatureCounts[ValidatorInterface.Status.Valid] ?: 0 + unknownSignaturesCount = signatureCounts[ValidatorInterface.Status.Unknown] ?: 0 + invalidSignaturesCount = signatureCounts[ValidatorInterface.Status.Invalid] ?: 0 + + if (timestampCounts != null) { + validTimestampsCount = timestampCounts[ValidatorInterface.Status.Valid] ?: 0 + unknownTimestampsCount = timestampCounts[ValidatorInterface.Status.Unknown] ?: 0 + invalidTimestampsCount = timestampCounts[ValidatorInterface.Status.Invalid] ?: 0 } - isEmptyFileInContainer = - signingViewModel.isEmptyFileInList( - signedContainer?.getDataFiles() ?: listOf(), - ) + isEmptyFileInContainer = hasEmptyFile sharedContainerViewModel.setContainerNotifications( listOfNotNull( diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt index cb68e2184..15579dfbc 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt @@ -256,7 +256,7 @@ class MobileIdViewModel setErrorState(context, status) } else { CoroutineScope(Main).launch { - val containerSignatures = container?.getSignatures(Main) + val containerSignatures = container?.getSignatures() val signatureInterface = if (containerSignatures?.isEmpty() == true) { null 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 b17a3184b..d867c92c8 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt @@ -227,7 +227,7 @@ class NFCViewModel } suspend fun removePendingSignature(signedContainer: SignedContainer) { - val signatures = signedContainer.getSignatures(Main) + val signatures = signedContainer.getSignatures() if (signatures.isNotEmpty()) { val lastSignatureStatus = signatures.last().validator.status if (lastSignatureStatus == ValidatorInterface.Status.Invalid || @@ -300,18 +300,22 @@ class NFCViewModel signer.setUserAgent(UserAgentUtil.getAppInfo(context, SendDiagnostics.NFC)) val dataToSignBytes = - containerWrapper.prepareSignature(signer, container, signerCert, roleData) + runBlocking { + containerWrapper.prepareSignature(signer, container, signerCert, roleData) + } val signatureArray = card.calculateSignature(pin2Code, dataToSignBytes, true) pin2Code.clearSensitive() debugLog(logTag, "Signature: " + Hex.toHexString(signatureArray)) - containerWrapper.finalizeSignature( - signer, - container, - signatureArray, - ) + runBlocking { + containerWrapper.finalizeSignature( + signer, + container, + signatureArray, + ) + } _shouldResetPIN.postValue(true) _signStatus.postValue(true) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt index 8d951cefe..65ced52f5 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt @@ -288,7 +288,7 @@ class SmartIdViewModel setErrorState(context, status) } else { CoroutineScope(Main).launch { - val containerSignatures = container?.getSignatures(Main) + val containerSignatures = container?.getSignatures() val signatureInterface = if (containerSignatures?.isEmpty() == true) { null diff --git a/config-lib/src/androidTest/kotlin/ee/ria/DigiDoc/configuration/cache/ConfigurationCacheTest.kt b/config-lib/src/androidTest/kotlin/ee/ria/DigiDoc/configuration/cache/ConfigurationCacheTest.kt deleted file mode 100644 index 081d92a89..000000000 --- a/config-lib/src/androidTest/kotlin/ee/ria/DigiDoc/configuration/cache/ConfigurationCacheTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.configuration.cache - -import android.content.Context -import ee.ria.DigiDoc.configuration.utils.Constant.CACHE_CONFIG_FOLDER -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations -import org.mockito.junit.MockitoJUnitRunner -import java.io.File -import java.io.FileNotFoundException -import kotlin.io.path.createTempDirectory - -@RunWith(MockitoJUnitRunner::class) -class ConfigurationCacheTest { - @Mock - lateinit var context: Context - - @Mock - lateinit var configDir: File - - private val testStringData = "testData" - private val testByteArrayData = "testData".toByteArray() - - @Before - fun setUp() { - MockitoAnnotations.openMocks(this) - `when`(context.cacheDir).thenReturn(createTempDirectory("cacheDirectory").toFile()) - `when`(configDir.exists()).thenReturn(false) - `when`(configDir.mkdirs()).thenReturn(true) - } - - @Test - fun configurationCache_cacheConfigurationFiles_success() { - ConfigurationCache.cacheConfigurationFiles(context, testStringData, testStringData, testByteArrayData) - - assertEquals(3, File(context.cacheDir, CACHE_CONFIG_FOLDER).listFiles()?.size ?: 0) - assertTrue(File(File(context.cacheDir, CACHE_CONFIG_FOLDER), "active-config.json").exists()) - assertTrue(File(File(context.cacheDir, CACHE_CONFIG_FOLDER), "active-config.pub").exists()) - assertTrue(File(File(context.cacheDir, CACHE_CONFIG_FOLDER), "active-config.rsa").exists()) - } - - @Test - fun configurationCache_getCachedFile_success() { - val cacheConfigFolder = File(context.cacheDir, CACHE_CONFIG_FOLDER) - cacheConfigFolder.mkdirs() - val existingFile = File.createTempFile("config", ".json", cacheConfigFolder) - - val cachedFile = ConfigurationCache.getCachedFile(context, existingFile.name) - assertNotNull(cachedFile) - assertEquals(existingFile.name, cachedFile.name) - assertEquals(existingFile.length(), cachedFile.length()) - - existingFile.delete() - cacheConfigFolder.delete() - } - - @Test(expected = FileNotFoundException::class) - fun configurationCache_getCachedFile_throwsFileNotFoundException() { - ConfigurationCache.getCachedFile(context, "nonExistent.txt") - } -} diff --git a/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/cache/ConfigurationCache.kt b/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/cache/ConfigurationCache.kt index 033e3f848..8dc79bce4 100644 --- a/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/cache/ConfigurationCache.kt +++ b/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/cache/ConfigurationCache.kt @@ -26,69 +26,102 @@ import ee.ria.DigiDoc.configuration.utils.Constant.CACHED_CONFIG_JSON import ee.ria.DigiDoc.configuration.utils.Constant.CACHED_CONFIG_PUB import ee.ria.DigiDoc.configuration.utils.Constant.CACHED_CONFIG_RSA import ee.ria.DigiDoc.configuration.utils.Constant.CACHE_CONFIG_FOLDER -import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.infoLog import java.io.File import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.FileWriter import java.io.IOException +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.copyTo +import kotlin.io.path.createTempFile +import kotlin.io.path.deleteIfExists +import kotlin.io.path.exists +import kotlin.io.path.moveTo +import kotlin.io.path.name +import kotlin.io.path.writeBytes object ConfigurationCache { private const val LOG_TAG = "ConfigurationCache" + private const val TEMPORARY_SUFFIX = ".tmp" + private const val BACKUP_SUFFIX = ".bak" + @Throws(IOException::class) fun cacheConfigurationFiles( context: Context, confData: String, publicKey: String, signature: ByteArray, ) { - cacheFile(context, CACHED_CONFIG_JSON, confData) - cacheFile(context, CACHED_CONFIG_PUB, publicKey) - cacheFile(context, CACHED_CONFIG_RSA, signature) - } - - private fun cacheFile( - context: Context, - fileName: String, - data: String, - ) { - val configDir = File(context.cacheDir, CACHE_CONFIG_FOLDER) - if (!configDir.exists()) { - configDir.mkdirs() + val configDir = File(context.cacheDir, CACHE_CONFIG_FOLDER).toPath() + try { + Files.createDirectories(configDir) + } catch (e: IOException) { + errorLog(LOG_TAG, "Unable to create the configuration cache directory: $configDir", e) + throw e } - val configFile = File(configDir, fileName) + val files = + listOf( + CACHED_CONFIG_JSON to confData.toByteArray(StandardCharsets.UTF_8), + CACHED_CONFIG_PUB to publicKey.toByteArray(StandardCharsets.UTF_8), + CACHED_CONFIG_RSA to signature, + ) + val temporaryFiles = mutableListOf() + val backups = mutableMapOf() + val replaced = mutableListOf() + try { - FileWriter(configFile).use { writer -> - writer.write(data) + files.forEach { (fileName, data) -> + val temporaryFile = createTempFile(configDir, fileName, TEMPORARY_SUFFIX) + temporaryFiles.add(temporaryFile) + temporaryFile.writeBytes(data) } - } catch (ioe: IOException) { - LoggingUtil.errorLog( - LOG_TAG, - "Unable to cache file $fileName", - ioe, - ) + + files.forEach { (fileName, _) -> + val destination = configDir.resolve(fileName) + if (destination.exists()) { + val backup = createTempFile(configDir, fileName, BACKUP_SUFFIX) + destination.copyTo(backup, overwrite = true) + backups[destination] = backup + } + } + + files.forEachIndexed { index, (fileName, _) -> + val destination = configDir.resolve(fileName) + temporaryFiles[index].moveTo(destination, overwrite = true) + replaced.add(destination) + } + infoLog(LOG_TAG, "Cached the configuration, its public key and its signature") + } catch (e: Exception) { + errorLog(LOG_TAG, "Unable to cache the configuration files, restoring the previous ones", e) + restorePreviousFiles(replaced, backups) + throw e + } finally { + (temporaryFiles + backups.values).forEach(::deleteLeftover) } } - private fun cacheFile( - context: Context, - fileName: String, - data: ByteArray, + internal fun restorePreviousFiles( + replaced: List, + backups: Map, ) { - val configDir = File(context.cacheDir, CACHE_CONFIG_FOLDER) - if (!configDir.exists()) { - configDir.mkdirs() + replaced.forEach(::deleteLeftover) + backups.forEach { (destination, backup) -> + try { + backup.moveTo(destination, overwrite = true) + } catch (e: IOException) { + errorLog(LOG_TAG, "Unable to restore the previous configuration file: ${destination.name}", e) + } } + } - val configFile = File(configDir, fileName) - + private fun deleteLeftover(path: Path) { try { - FileOutputStream(configFile).use { fos -> - fos.write(data) - } + path.deleteIfExists() } catch (e: IOException) { - LoggingUtil.errorLog(LOG_TAG, "Unable to cache file $fileName", e) + errorLog(LOG_TAG, "Unable to delete the leftover file: ${path.name}", e) } } @@ -102,6 +135,7 @@ object ConfigurationCache { if (configFile.exists() && configFile.isFile) { return configFile } + errorLog(LOG_TAG, "Cached configuration file not found: $fileName") throw FileNotFoundException() } } diff --git a/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/loader/ConfigurationLoaderImpl.kt b/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/loader/ConfigurationLoaderImpl.kt index 625ec2c0b..9ded33725 100644 --- a/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/loader/ConfigurationLoaderImpl.kt +++ b/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/loader/ConfigurationLoaderImpl.kt @@ -42,6 +42,7 @@ import ee.ria.DigiDoc.network.proxy.ManualProxy import ee.ria.DigiDoc.network.proxy.ProxySetting import ee.ria.DigiDoc.utilsLib.date.DateUtil import ee.ria.DigiDoc.utilsLib.extensions.removeWhitespaces +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -107,6 +108,19 @@ class ConfigurationLoaderImpl private fun getConfigCacheDir(context: Context): File = File(context.cacheDir, CACHE_CONFIG_FOLDER) + private fun cacheConfiguration( + context: Context, + confData: String, + publicKey: String, + signature: ByteArray, + ) { + try { + ConfigurationCache.cacheConfigurationFiles(context, confData, publicKey, signature) + } catch (e: Exception) { + errorLog(logTag, "Unable to cache the configuration, continuing with the loaded one", e) + } + } + private fun decodeSignature(signatureBytes: ByteArray): ByteArray { val signatureText = String(signatureBytes, Charsets.UTF_8) return if (ConfigurationUtil.isBase64(signatureText)) { @@ -144,11 +158,16 @@ class ConfigurationLoaderImpl val configText = confFile.readText() val publicKey = publicKeyFile.readText() - val signature = decodeSignature(signatureFile.readBytes()) + val storedSignature = signatureFile.readBytes() + val signature = decodeSignature(storedSignature) configurationSignatureVerifier.verifyConfigurationSignature(configText, publicKey, signature) val configurationProvider = gson.fromJson(configText, ConfigurationProvider::class.java) - ConfigurationCache.cacheConfigurationFiles(context, configText, publicKey, signature) + + if (!storedSignature.contentEquals(signature)) { + debugLog(logTag, "Normalizing the cached configuration signature to its decoded form") + cacheConfiguration(context, configText, publicKey, signature) + } if (!afterCentralCheck) { configurationProperties.updateProperties( @@ -181,12 +200,7 @@ class ConfigurationLoaderImpl configurationSignatureVerifier.verifyConfigurationSignature(confData, publicKey, signature) - ConfigurationCache.cacheConfigurationFiles( - context, - confData, - publicKey, - signature, - ) + cacheConfiguration(context, confData, publicKey, signature) val configurationProvider = gson.fromJson(confData, ConfigurationProvider::class.java) configurationProperties.updateProperties( context, @@ -267,12 +281,7 @@ class ConfigurationLoaderImpl centralConfigurationProvider.metaInf.serial, ) ) { - ConfigurationCache.cacheConfigurationFiles( - context, - centralConfig, - centralPublicKey, - centralSignature, - ) + cacheConfiguration(context, centralConfig, centralPublicKey, centralSignature) configurationProperties.updateProperties( context, Date(), diff --git a/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/utils/TSLUtil.kt b/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/utils/TSLUtil.kt index 4e393acd0..7c7190d7e 100644 --- a/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/utils/TSLUtil.kt +++ b/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/utils/TSLUtil.kt @@ -66,9 +66,23 @@ object TSLUtil { if (!tslFiles.isNullOrEmpty()) { for (fileName in tslFiles) { if (isXMLFile(fileName) && shouldCopyTSL(context, assetsPath, fileName, destination)) { - copyTSLFromAssets(context, assetsPath, fileName, destination) val tslFile = File(destination, fileName) - setFileDateAttributes(tslFile) + try { + copyTSLFromAssets(context, assetsPath, fileName, destination) + } catch (ioe: IOException) { + errorLog(logTag, "Failed to copy TSL '$fileName' from assets into cache", ioe) + continue + } + try { + setFileDateAttributes(tslFile) + } catch (e: Exception) { + errorLog( + logTag, + "Unable to backdate TSL '$fileName'; libdigidocpp will only look for a newer " + + "list once the cached copy is a day old", + e, + ) + } removeExistingETag(tslFile.path) debugLog(logTag, "Copied TSL '$fileName' from assets into cache (${tslFile.length()} bytes)") } @@ -89,52 +103,48 @@ object TSLUtil { if (!FileUtil.fileExists(cachedFile.path)) { debugLog(logTag, "TSL '$fileName' is not in the cache yet; copying it from assets") return true - } else { + } + + val assetsTslVersion = try { - context.assets - .open(File(sourcePath, fileName).path) - .use { assetsTSLInputStream -> - FileInputStream(cachedFile) - .use { cachedTSLInputStream -> - val assetsTslVersion: Int = - readSequenceNumber(assetsTSLInputStream) - val cachedTslVersion: Int = - readSequenceNumber(cachedTSLInputStream) - val isAssetNewer = assetsTslVersion > cachedTslVersion - debugLog( - logTag, - "TSL '$fileName': assets version $assetsTslVersion, cached version " + - "$cachedTslVersion — ${if (isAssetNewer) "updating cache" else "cache is up to date"}", - ) - return isAssetNewer - } - } + context.assets.open(File(sourcePath, fileName).path).use { readSequenceNumber(it) } } catch (e: Exception) { - val message = "Error comparing sequence number between assets and cached TSLs" - errorLog(logTag, message, e) + errorLog(logTag, "Unable to read the bundled TSL '$fileName'; keeping the cached copy", e) return false } - } + + val cachedTslVersion = + try { + FileInputStream(cachedFile).use { readSequenceNumber(it) } + } catch (e: Exception) { + errorLog(logTag, "Cached TSL '$fileName' cannot be parsed; replacing it from assets", e) + return true + } + + val isAssetNewer = assetsTslVersion > cachedTslVersion + debugLog( + logTag, + "TSL '$fileName': assets version $assetsTslVersion, cached version $cachedTslVersion, " + + if (isAssetNewer) "updating cache" else "cache is up to date", + ) + return isAssetNewer } @Suppress("SameParameterValue") + @Throws(IOException::class) private fun copyTSLFromAssets( context: Context, sourcePath: String, fileName: String, destinationDir: String, ) { - try { - BufferedReader( - InputStreamReader( - context.assets.open(File(sourcePath, fileName).path), - StandardCharsets.UTF_8, - ), - ).use { reader -> - FileUtil.writeToFile(reader, destinationDir, fileName) - } - } catch (ioe: IOException) { - errorLog(logTag, "Failed to copy file: $fileName from assets", ioe) + BufferedReader( + InputStreamReader( + context.assets.open(File(sourcePath, fileName).path), + StandardCharsets.UTF_8, + ), + ).use { reader -> + FileUtil.writeToFile(reader, destinationDir, fileName) } } @@ -177,7 +187,8 @@ object TSLUtil { debugLog( logTag, - "Changed file ${file.name} modified date attribute ${currentFileAttrs.lastModifiedTime()} -> ${updatedFileAttrs.lastModifiedTime()}", + "Changed file ${file.name} modified date attribute " + + "${currentFileAttrs.lastModifiedTime()} -> ${updatedFileAttrs.lastModifiedTime()}", ) } } diff --git a/config-lib/src/test/kotlin/ee/ria/DigiDoc/configuration/cache/ConfigurationCacheTest.kt b/config-lib/src/test/kotlin/ee/ria/DigiDoc/configuration/cache/ConfigurationCacheTest.kt new file mode 100644 index 000000000..fd4ff9018 --- /dev/null +++ b/config-lib/src/test/kotlin/ee/ria/DigiDoc/configuration/cache/ConfigurationCacheTest.kt @@ -0,0 +1,186 @@ +/* + * 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.configuration.cache + +import android.content.Context +import ee.ria.DigiDoc.configuration.utils.Constant.CACHED_CONFIG_JSON +import ee.ria.DigiDoc.configuration.utils.Constant.CACHED_CONFIG_PUB +import ee.ria.DigiDoc.configuration.utils.Constant.CACHED_CONFIG_RSA +import ee.ria.DigiDoc.configuration.utils.Constant.CACHE_CONFIG_FOLDER +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnitRunner +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.nio.file.Files + +@RunWith(MockitoJUnitRunner::class) +class ConfigurationCacheTest { + @Mock + lateinit var context: Context + + private lateinit var cacheDir: File + + private val configDir: File + get() = File(cacheDir, CACHE_CONFIG_FOLDER) + + @Before + fun setUp() { + cacheDir = Files.createTempDirectory("configuration-cache-test").toFile() + cacheDir.deleteOnExit() + `when`(context.cacheDir).thenReturn(cacheDir) + } + + @Test + fun configurationCache_cacheConfigurationFiles_writesAllThreeFiles() { + ConfigurationCache.cacheConfigurationFiles(context, "{}", "public-key", byteArrayOf(1, 2, 3)) + + assertEquals(3, configDir.listFiles()?.size ?: 0) + assertEquals("{}", File(configDir, CACHED_CONFIG_JSON).readText()) + assertEquals("public-key", File(configDir, CACHED_CONFIG_PUB).readText()) + assertArrayEquals(byteArrayOf(1, 2, 3), File(configDir, CACHED_CONFIG_RSA).readBytes()) + } + + @Test + fun configurationCache_cacheConfigurationFiles_leavesNoTemporaryOrBackupFilesBehind() { + ConfigurationCache.cacheConfigurationFiles(context, "{}", "public-key", byteArrayOf(1)) + + assertTrue("Leftover files: ${leftoverFilesIn(configDir)}", leftoverFilesIn(configDir).isEmpty()) + } + + @Test + fun configurationCache_cacheConfigurationFiles_replacesThePreviousSetCompletely() { + ConfigurationCache.cacheConfigurationFiles(context, "{\"old\":1}", "old-key", byteArrayOf(9)) + + ConfigurationCache.cacheConfigurationFiles(context, "{\"new\":2}", "new-key", byteArrayOf(8)) + + assertEquals(3, configDir.listFiles()?.size ?: 0) + assertEquals("{\"new\":2}", File(configDir, CACHED_CONFIG_JSON).readText()) + assertEquals("new-key", File(configDir, CACHED_CONFIG_PUB).readText()) + assertArrayEquals(byteArrayOf(8), File(configDir, CACHED_CONFIG_RSA).readBytes()) + } + + @Test + fun configurationCache_cacheConfigurationFiles_keepsThePreviousSetWhenTheWriteFails() { + ConfigurationCache.cacheConfigurationFiles(context, "{\"old\":1}", "old-key", byteArrayOf(9)) + + assertTrue(configDir.setWritable(false)) + try { + assertThrows(IOException::class.java) { + ConfigurationCache.cacheConfigurationFiles(context, "{\"new\":2}", "new-key", byteArrayOf(8)) + } + } finally { + assertTrue(configDir.setWritable(true)) + } + + assertEquals("{\"old\":1}", File(configDir, CACHED_CONFIG_JSON).readText()) + assertEquals("old-key", File(configDir, CACHED_CONFIG_PUB).readText()) + assertArrayEquals(byteArrayOf(9), File(configDir, CACHED_CONFIG_RSA).readBytes()) + assertTrue("Leftover files: ${leftoverFilesIn(configDir)}", leftoverFilesIn(configDir).isEmpty()) + } + + @Test + fun configurationCache_restorePreviousFiles_bringsBackEveryBackedUpFile() { + assertTrue(configDir.mkdirs()) + val json = writeFile(CACHED_CONFIG_JSON, "{\"new\":2}") + val pub = writeFile(CACHED_CONFIG_PUB, "new-key") + val jsonBackup = writeFile("$CACHED_CONFIG_JSON.bak", "{\"old\":1}") + val pubBackup = writeFile("$CACHED_CONFIG_PUB.bak", "old-key") + + ConfigurationCache.restorePreviousFiles( + listOf(json.toPath(), pub.toPath()), + mapOf(json.toPath() to jsonBackup.toPath(), pub.toPath() to pubBackup.toPath()), + ) + + assertEquals("{\"old\":1}", json.readText()) + assertEquals("old-key", pub.readText()) + assertFalse(jsonBackup.exists()) + assertFalse(pubBackup.exists()) + assertEquals(2, configDir.listFiles()?.size ?: 0) + } + + @Test + fun configurationCache_restorePreviousFiles_removesFilesThatHadNoPreviousVersion() { + assertTrue(configDir.mkdirs()) + val json = writeFile(CACHED_CONFIG_JSON, "{\"new\":2}") + + ConfigurationCache.restorePreviousFiles(listOf(json.toPath()), emptyMap()) + + assertFalse(json.exists()) + assertEquals(0, configDir.listFiles()?.size ?: 0) + } + + @Test + fun configurationCache_getCachedFile_success() { + assertTrue(configDir.mkdirs()) + val existingFile = File.createTempFile("config", ".json", configDir) + + val cachedFile = ConfigurationCache.getCachedFile(context, existingFile.name) + + assertNotNull(cachedFile) + assertEquals(existingFile.name, cachedFile.name) + assertEquals(existingFile.length(), cachedFile.length()) + } + + @Test + fun configurationCache_getCachedFile_throwsFileNotFoundException() { + assertThrows(FileNotFoundException::class.java) { + ConfigurationCache.getCachedFile(context, "nonExistent.txt") + } + } + + @Test + fun configurationCache_cacheConfigurationFiles_throwsWhenTheCacheDirectoryCannotBeCreated() { + assertTrue(File(cacheDir, "config").createNewFile()) + + assertThrows(IOException::class.java) { + ConfigurationCache.cacheConfigurationFiles(context, "{}", "public-key", byteArrayOf(1)) + } + } + + @Test + fun configurationCache_getCachedFile_throwsWhenThePathIsADirectory() { + assertTrue(File(configDir, "a-directory").mkdirs()) + + assertThrows(FileNotFoundException::class.java) { + ConfigurationCache.getCachedFile(context, "a-directory") + } + } + + private fun writeFile( + fileName: String, + content: String, + ): File = File(configDir, fileName).apply { writeText(content) } + + private fun leftoverFilesIn(directory: File): List = + directory.listFiles()?.filter { it.name.endsWith(".tmp") || it.name.endsWith(".bak") } ?: emptyList() +} diff --git a/libdigidoc-lib/libs/libdigidocpp.jar b/libdigidoc-lib/libs/libdigidocpp.jar index a4771f02e..33bf8ca03 100644 Binary files a/libdigidoc-lib/libs/libdigidocpp.jar and b/libdigidoc-lib/libs/libdigidocpp.jar differ 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 9c0afadb9..2310373e1 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 @@ -657,7 +657,7 @@ class SignedContainerTest { runTest { val signedContainer = openOrCreate(context, container, listOf(container), true) - val signaturesStatuses = signedContainer.getSignaturesStatusCount() + val signaturesStatuses = signedContainer.getSignaturesStatusCount(signedContainer.getSignatures()) assertNotNull(signaturesStatuses) assertEquals(0, signaturesStatuses[ValidatorInterface.Status.Valid]) @@ -677,7 +677,7 @@ class SignedContainerTest { val signedContainer = openOrCreate(context, noSignaturesContainer, listOf(noSignaturesContainer), true) - val signaturesStatuses = signedContainer.getSignaturesStatusCount() + val signaturesStatuses = signedContainer.getSignaturesStatusCount(signedContainer.getSignatures()) assertNotNull(signaturesStatuses) assertEquals(0, signaturesStatuses[ValidatorInterface.Status.Valid]) diff --git a/libdigidoc-lib/src/androidTest/kotlin/ee/ria/DigiDoc/libdigidoclib/init/InitializationTest.kt b/libdigidoc-lib/src/androidTest/kotlin/ee/ria/DigiDoc/libdigidoclib/init/InitializationTest.kt index dc557510a..fff7b6553 100644 --- a/libdigidoc-lib/src/androidTest/kotlin/ee/ria/DigiDoc/libdigidoclib/init/InitializationTest.kt +++ b/libdigidoc-lib/src/androidTest/kotlin/ee/ria/DigiDoc/libdigidoclib/init/InitializationTest.kt @@ -26,11 +26,15 @@ import android.content.res.Resources import android.content.res.Resources.NotFoundException import androidx.test.platform.app.InstrumentationRegistry import ee.ria.DigiDoc.common.Constant.Defaults.DEFAULT_UUID_VALUE +import ee.ria.DigiDoc.common.testfiles.asset.AssetFile.Companion.getResourceFileAsFile import ee.ria.DigiDoc.configuration.provider.ConfigurationProvider import ee.ria.DigiDoc.configuration.repository.ConfigurationRepository +import ee.ria.DigiDoc.libdigidoclib.SignedContainer.Companion.openOrCreate import ee.ria.DigiDoc.libdigidoclib.exceptions.AlreadyInitializedException import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Before import org.junit.Test @@ -104,6 +108,18 @@ class InitializationTest { field.isAccessible = true field.setBoolean(initialization, false) } + + @Throws( + SecurityException::class, + NoSuchFieldException::class, + java.lang.IllegalArgumentException::class, + IllegalAccessException::class, + ) + private fun isInitializedFlag(): Boolean { + val field: Field = Initialization::class.java.getDeclaredField("isInitialized") + field.isAccessible = true + return field.getBoolean(initialization) + } } @Before @@ -116,14 +132,36 @@ class InitializationTest { } @Test - fun initialization_init_success() { + fun initialization_init_doesNotThrowWhenTslUnreachable() { try { - `when`(configurationRepository.getConfiguration()).thenReturn(configurationProvider) + val configuration = configurationProvider.copy(tslUrl = "http://127.0.0.1:1/eu-lotl.xml") + `when`(configurationRepository.getConfiguration()).thenReturn(configuration) runTest { initialization.init(context) } - } catch (_: Exception) { - fail("No exceptions should be thrown") + } catch (e: Exception) { + fail("Failing to download the list must not be fatal, but init threw: $e") + } + + assertTrue( + "The library should still be marked as initialized when the list cannot be downloaded", + isInitializedFlag(), + ) + } + + @Test + fun initialization_init_containerRemainsUsableWhenTslUnreachable() { + val configuration = configurationProvider.copy(tslUrl = "http://127.0.0.1:1/eu-lotl.xml") + `when`(configurationRepository.getConfiguration()).thenReturn(configuration) + + runTest { + initialization.init(context) + + val containerFile = + getResourceFileAsFile(context, "example.asice", ee.ria.DigiDoc.common.R.raw.example) + val signedContainer = openOrCreate(context, containerFile, listOf(containerFile), false) + + assertEquals(2, signedContainer.getSignatures().size) } } @@ -159,6 +197,9 @@ class InitializationTest { @Test fun initialization_init_throwsAlreadyInitializedExceptionWhenInitTwice() { + `when`(configurationRepository.getConfiguration()) + .thenReturn(configurationProvider.copy(tslUrl = "http://127.0.0.1:1/eu-lotl.xml")) + assertThrows(AlreadyInitializedException::class.java) { runTest { initialization.init(context) diff --git a/libdigidoc-lib/src/debug/jniLibs/arm64-v8a/libdigidoc_java.so b/libdigidoc-lib/src/debug/jniLibs/arm64-v8a/libdigidoc_java.so index a3cfbc1ba..30cf96c8c 100644 Binary files a/libdigidoc-lib/src/debug/jniLibs/arm64-v8a/libdigidoc_java.so and b/libdigidoc-lib/src/debug/jniLibs/arm64-v8a/libdigidoc_java.so differ diff --git a/libdigidoc-lib/src/debug/jniLibs/armeabi-v7a/libdigidoc_java.so b/libdigidoc-lib/src/debug/jniLibs/armeabi-v7a/libdigidoc_java.so index 5a7d455df..82730ee7f 100644 Binary files a/libdigidoc-lib/src/debug/jniLibs/armeabi-v7a/libdigidoc_java.so and b/libdigidoc-lib/src/debug/jniLibs/armeabi-v7a/libdigidoc_java.so differ diff --git a/libdigidoc-lib/src/debug/jniLibs/x86_64/libdigidoc_java.so b/libdigidoc-lib/src/debug/jniLibs/x86_64/libdigidoc_java.so index e30e23bd3..fdc8079a8 100644 Binary files a/libdigidoc-lib/src/debug/jniLibs/x86_64/libdigidoc_java.so and b/libdigidoc-lib/src/debug/jniLibs/x86_64/libdigidoc_java.so differ diff --git a/libdigidoc-lib/src/main/jniLibs/arm64-v8a/libdigidoc_java.so b/libdigidoc-lib/src/main/jniLibs/arm64-v8a/libdigidoc_java.so index a3cfbc1ba..30cf96c8c 100644 Binary files a/libdigidoc-lib/src/main/jniLibs/arm64-v8a/libdigidoc_java.so and b/libdigidoc-lib/src/main/jniLibs/arm64-v8a/libdigidoc_java.so differ diff --git a/libdigidoc-lib/src/main/jniLibs/armeabi-v7a/libdigidoc_java.so b/libdigidoc-lib/src/main/jniLibs/armeabi-v7a/libdigidoc_java.so index 5a7d455df..82730ee7f 100644 Binary files a/libdigidoc-lib/src/main/jniLibs/armeabi-v7a/libdigidoc_java.so and b/libdigidoc-lib/src/main/jniLibs/armeabi-v7a/libdigidoc_java.so differ diff --git a/libdigidoc-lib/src/main/jniLibs/x86_64/libdigidoc_java.so b/libdigidoc-lib/src/main/jniLibs/x86_64/libdigidoc_java.so index e30e23bd3..fdc8079a8 100644 Binary files a/libdigidoc-lib/src/main/jniLibs/x86_64/libdigidoc_java.so and b/libdigidoc-lib/src/main/jniLibs/x86_64/libdigidoc_java.so differ 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 d7f9e8fd5..c76f31b1c 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 @@ -37,6 +37,7 @@ import ee.ria.DigiDoc.libdigidoclib.domain.model.SignatureWrapper import ee.ria.DigiDoc.libdigidoclib.domain.model.ValidatorInterface import ee.ria.DigiDoc.libdigidoclib.exceptions.ContainerDataFilesEmptyException import ee.ria.DigiDoc.libdigidoclib.exceptions.SSLHandshakeException +import ee.ria.DigiDoc.libdigidoclib.init.libdigidocppDispatcher import ee.ria.DigiDoc.utilsLib.container.ContainerUtil import ee.ria.DigiDoc.utilsLib.extensions.isContainer import ee.ria.DigiDoc.utilsLib.extensions.isPDF @@ -48,17 +49,12 @@ import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.libdigidocpp.Container import ee.ria.libdigidocpp.ContainerOpenCB import ee.ria.libdigidocpp.Signature -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.async import kotlinx.coroutines.withContext import org.apache.commons.io.FilenameUtils import java.io.File import java.io.IOException import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.CoroutineContext private const val LOG_TAG = "SignedContainer" @@ -72,16 +68,13 @@ class SignedContainer private val isExistingContainer: Boolean = false, private val timestamps: List = emptyList(), ) : ee.ria.DigiDoc.common.container.Container { - suspend fun getDataFiles(): List { - return CoroutineScope(IO) - .async { - val wrappedDataFile = - container - ?.dataFiles() - ?.mapNotNull { DataFileWrapper(it) } ?: emptyList() - return@async wrappedDataFile - }.await() - } + suspend fun getDataFiles(): List = + withContext(libdigidocppDispatcher) { + container + ?.dataFiles() + ?.filterNotNull() + ?.map { DataFileWrapper(it) } ?: emptyList() + } @Throws(Exception::class) suspend fun getNestedTimestampedContainer(isSivaConfirmed: Boolean): SignedContainer? { @@ -112,8 +105,8 @@ class SignedContainer fun getTimestamps(): List = timestamps - suspend fun getSignatures(thread: CoroutineContext = IO): List = - withContext(thread) { + suspend fun getSignatures(): List = + withContext(libdigidocppDispatcher) { try { container ?.signatures() @@ -141,7 +134,7 @@ class SignedContainer val containerName = name.let { ContainerUtil.addExtensionToContainerFilename(it) } val newFile = File(containerFile?.parent, containerName) - withContext(IO) { + withContext(libdigidocppDispatcher) { containerFile?.renameTo(newFile) containerFile = newFile @@ -185,18 +178,21 @@ class SignedContainer } } } - throw IllegalArgumentException("Could not find file ${dataFile.id} in container ${containerFile?.name}") + val message = "Could not find file ${dataFile.id} in container ${containerFile?.name}" + errorLog(LOG_TAG, message) + throw IllegalArgumentException(message) } @Throws(Exception::class) - suspend fun removeDataFile(dataFile: DataFileInterface) { - if ((container?.dataFiles()?.size ?: 0) == 1) { - throw ContainerDataFilesEmptyException() - } + suspend fun removeDataFile(dataFile: DataFileInterface) = + withContext(libdigidocppDispatcher) { + if ((container?.dataFiles()?.size ?: 0) == 1) { + errorLog(LOG_TAG, "Refusing to remove the last data file from the container") + throw ContainerDataFilesEmptyException() + } - val dataFiles = container?.dataFiles() - if (dataFiles != null) { - withContext(IO) { + val dataFiles = container?.dataFiles() + if (dataFiles != null) { for (i in dataFiles.indices) { if (dataFile.id == dataFiles[i].id()) { container.removeDataFile(i.toLong()) @@ -207,7 +203,6 @@ class SignedContainer container.save() } } - } fun rawContainer(): Container? = container @@ -239,10 +234,8 @@ class SignedContainer containerMimetype().equals(DDOC_MIMETYPE, true) && containerFile?.extension == DDOC_EXTENSION - suspend fun getSignaturesStatusCount(): Map { - val signatures = getSignatures(Main) - return countStatuses(signatures) { it.validator.status } - } + fun getSignaturesStatusCount(signatures: List): Map = + countStatuses(signatures) { it.validator.status } fun getTimestampStatusCount(): Map { val timestamps = getTimestamps() @@ -327,43 +320,53 @@ class SignedContainer dataFiles: List?, ): SignedContainer { if (dataFiles.isNullOrEmpty()) { - throw NoSuchElementException("Cannot create an empty container") + val message = "Cannot create an empty container" + errorLog(LOG_TAG, message) + throw NoSuchElementException(message) } val container = - try { - withContext(IO) { - Container.create(file.path) + withContext(libdigidocppDispatcher) { + val created = + try { + Container.create(file.path) + } catch (e: Exception) { + handleContainerException(context, e) + } ?: run { + val message = "Container creation failed for ${file.name}" + errorLog(LOG_TAG, message) + throw IOException(message) + } + + dataFiles.forEachIndexed { index, dataFile -> + dataFile?.let { + debugLog( + LOG_TAG, + "Adding datafile '${dataFile.name}'. File ${index + 1} / ${dataFiles.size}", + ) + try { + created.addDataFile(it.absolutePath, it.mimeType(context)) + } catch (e: Exception) { + errorLog(LOG_TAG, "Unable to add file '${it.name}' to container", e) + } + } ?: run { + errorLog(LOG_TAG, "Unable to add file to container") + } } - } catch (e: Exception) { - handleContainerException(context, e) - } ?: throw IOException("Container creation failed") - - dataFiles.forEachIndexed { index, dataFile -> - dataFile?.let { - debugLog( - LOG_TAG, - "Adding datafile '${dataFile.name}'. File ${index + 1} / ${dataFiles.size}", - ) + + if (created.dataFiles().isEmpty()) { + val message = "No valid data files in the container" + errorLog(LOG_TAG, message) + throw NoSuchElementException(message) + } + try { - container.addDataFile(it.absolutePath, it.mimeType(context)) + created.save() } catch (e: Exception) { - errorLog(LOG_TAG, "Unable to add file to container. ${e.localizedMessage}") + handleContainerException(context, e) } - } ?: run { - errorLog(LOG_TAG, "Unable to add file to container") + created } - } - - if (container.dataFiles().isEmpty()) { - throw NoSuchElementException("No valid data files in the container") - } - - try { - container.save() - } catch (e: Exception) { - handleContainerException(context, e) - } return SignedContainer(context, container, file, false) } @@ -376,7 +379,7 @@ class SignedContainer ): SignedContainer = try { val openedContainer = - withContext(IO) { + withContext(libdigidocppDispatcher) { Container.open(file?.path ?: "", DigidocContainerOpenCB(isSivaConfirmed)) } SignedContainer(context, openedContainer, file, true) diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapper.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapper.kt index 9d2a63104..f2560f3d6 100644 --- a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapper.kt +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapper.kt @@ -22,24 +22,26 @@ package ee.ria.DigiDoc.libdigidoclib.domain.model import ee.ria.DigiDoc.libdigidoclib.SignedContainer +import ee.ria.DigiDoc.libdigidoclib.init.libdigidocppDispatcher import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.DigiDoc.utilsLib.text.TextUtil.removeEmptyStrings import ee.ria.libdigidocpp.ExternalSigner import ee.ria.libdigidocpp.Signature import ee.ria.libdigidocpp.StringVector +import kotlinx.coroutines.withContext import java.security.cert.CertificateException interface ContainerWrapper { @Throws(CertificateException::class) - fun prepareSignature( + suspend fun prepareSignature( signer: ExternalSigner, signedContainer: SignedContainer?, cert: ByteArray?, roleData: RoleData?, ): ByteArray - fun finalizeSignature( + suspend fun finalizeSignature( signer: ExternalSigner, signedContainer: SignedContainer?, signatureArray: ByteArray, @@ -51,44 +53,45 @@ class ContainerWrapperImpl : ContainerWrapper { private val logTag = "Libdigidoc-ContainerWrapper" @Throws(CertificateException::class) - override fun prepareSignature( + override suspend fun prepareSignature( signer: ExternalSigner, signedContainer: SignedContainer?, cert: ByteArray?, roleData: RoleData?, - ): ByteArray { - debugLog(logTag, "Preparing signature (with role data: ${roleData != null})") - signature = - when { - roleData != null && signedContainer != null -> { - signer.setSignerRoles(StringVector(removeEmptyStrings(roleData.roles))) - signer.setSignatureProductionPlace( - roleData.city, - roleData.state, - roleData.zip, - roleData.country, - ) - signedContainer.rawContainer()?.prepareSignature( - signer, - ) ?: throw IllegalStateException("Failed to prepare signature with role data") + ): ByteArray = + withContext(libdigidocppDispatcher) { + debugLog(logTag, "Preparing signature (with role data: ${roleData != null})") + signature = + when { + roleData != null && signedContainer != null -> { + signer.setSignerRoles(StringVector(removeEmptyStrings(roleData.roles))) + signer.setSignatureProductionPlace( + roleData.city, + roleData.state, + roleData.zip, + roleData.country, + ) + signedContainer.rawContainer()?.prepareSignature( + signer, + ) ?: throw IllegalStateException("Failed to prepare signature with role data") + } + signedContainer?.rawContainer() != null -> { + signedContainer.rawContainer()?.prepareSignature( + signer, + ) ?: throw IllegalStateException("Failed to prepare signature without role data") + } + else -> throw IllegalStateException("Unable to get container") } - signedContainer?.rawContainer() != null -> { - signedContainer.rawContainer()?.prepareSignature( - signer, - ) ?: throw IllegalStateException("Failed to prepare signature without role data") - } - else -> throw IllegalStateException("Unable to get container") - } - val dataToSign = signature.dataToSign() - debugLog(logTag, "Signature prepared (${dataToSign.size} bytes to sign)") - return dataToSign - } + val dataToSign = signature.dataToSign() + debugLog(logTag, "Signature prepared (${dataToSign.size} bytes to sign)") + dataToSign + } - override fun finalizeSignature( + override suspend fun finalizeSignature( signer: ExternalSigner, signedContainer: SignedContainer?, signatureArray: ByteArray, - ) { + ) = withContext(libdigidocppDispatcher) { signature.setSignatureValue(signatureArray) debugLog(logTag, "Extending signature profile (fetches OCSP confirmation and timestamp)") try { diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/Initialization.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/Initialization.kt index b2b7ed284..4e2ee3fe8 100644 --- a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/Initialization.kt +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/Initialization.kt @@ -56,7 +56,6 @@ import ee.ria.libdigidocpp.Conf import ee.ria.libdigidocpp.DigiDocConf import ee.ria.libdigidocpp.digidoc import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers.Default import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.SupervisorJob @@ -69,6 +68,8 @@ import java.util.Optional import javax.inject.Inject import javax.inject.Singleton +internal const val LIBDIGIDOC_INIT_LOG_TAG = "Libdigidoc-Initialization" + @Singleton class Initialization @Inject @@ -76,7 +77,6 @@ class Initialization private val configurationRepository: ConfigurationRepository, ) { private var isInitialized = false - private val libdigidocInitLogTag = "Libdigidoc-Initialization" private val libdigidocLogLevel = 4 // 4 - Debug messages private var tsaUrlChangeListener: OnSharedPreferenceChangeListener? = null @@ -84,8 +84,7 @@ class Initialization private var sivaUrlChangeListener: OnSharedPreferenceChangeListener? = null private var sivaCertChangeListener: OnSharedPreferenceChangeListener? = null - private val configApplyDispatcher = Default.limitedParallelism(1) - private val configScope = CoroutineScope(SupervisorJob() + configApplyDispatcher) + private val configScope = CoroutineScope(SupervisorJob() + IO) private var lastAppliedConfiguration: ConfigurationProvider? = null /** @@ -99,7 +98,7 @@ class Initialization isLoggingEnabled: Boolean = false, ) { if (isInitialized) { - debugLog(libdigidocInitLogTag, "libdigidocpp is already initialized; only refreshing the log level") + debugLog(LIBDIGIDOC_INIT_LOG_TAG, "libdigidocpp is already initialized; only refreshing the log level") setLibdigidocppLogLevel(isLoggingEnabled) throw AlreadyInitializedException("Libdigidocpp is already initialized") } @@ -108,18 +107,18 @@ class Initialization try { initSchema(context) } catch (ioe: IOException) { - errorLog(libdigidocInitLogTag, "Init schema failed: ${ioe.message}") + errorLog(LIBDIGIDOC_INIT_LOG_TAG, "Init schema failed: ${ioe.message}") throw ioe } catch (nfe: NotFoundException) { - errorLog(libdigidocInitLogTag, "Init schema failed: ${nfe.message}") + errorLog(LIBDIGIDOC_INIT_LOG_TAG, "Init schema failed: ${nfe.message}") throw nfe } catch (erre: ErrnoException) { - errorLog(libdigidocInitLogTag, "Init schema failed: ${erre.message}") + errorLog(LIBDIGIDOC_INIT_LOG_TAG, "Init schema failed: ${erre.message}") throw erre } debugLog( - libdigidocInitLogTag, + LIBDIGIDOC_INIT_LOG_TAG, "TSL cache directory contains: ${getSchemaDir(context).list()?.joinToString() ?: "(empty)"}", ) initLibDigiDocpp( @@ -134,14 +133,25 @@ class Initialization context: Context, path: String, isLoggingEnabled: Boolean, - ) { + ) = withContext(libdigidocppDispatcher) { initLibDigiDocConfiguration( context, isLoggingEnabled, ) - digidoc.initializeLib(UserAgentUtil.getAppInfo(context), path) + + val appInfo = UserAgentUtil.getAppInfo(context) + + try { + digidoc.initializeLib(appInfo, path) + } catch (e: RuntimeException) { + errorLog(LIBDIGIDOC_INIT_LOG_TAG, "Unable to initialize TSL: ${e.message}", e) + } catch (e: Exception) { + errorLog(LIBDIGIDOC_INIT_LOG_TAG, "Unable to initialize Libdigidocpp: ${e.message}", e) + throw e + } + UserAgentUtil.setLibdigidocppVersion(digidoc.version()) - debugLog(libdigidocInitLogTag, "Initialized libdigidocpp ${digidoc.version()} (TSL cache: $path)") + debugLog(LIBDIGIDOC_INIT_LOG_TAG, "Initialized libdigidocpp ${digidoc.version()} (TSL cache: $path)") isInitialized = true } @@ -154,6 +164,7 @@ class Initialization initLibDigiDocLogging(context, isLoggingEnabled) forcePKCS12Certificate() + initTSLSettings() val proxySetting: ProxySetting? = getProxySetting(context) val manualProxy: ManualProxy = getManualProxySettings(context) @@ -200,7 +211,7 @@ class Initialization private suspend fun overrideConfiguration( context: Context, configurationProvider: ConfigurationProvider, - ) = withContext(configApplyDispatcher) { + ) = withContext(libdigidocppDispatcher) { val newSerial = configurationProvider.metaInf.serial val normalizedConfiguration = configurationProvider.copy( @@ -210,36 +221,56 @@ class Initialization val lastSerial = lastAppliedConfiguration?.metaInf?.serial if (lastSerial != null && newSerial < lastSerial) { debugLog( - libdigidocInitLogTag, + LIBDIGIDOC_INIT_LOG_TAG, "Ignoring older configuration (serial $newSerial < applied $lastSerial)", ) return@withContext } if (normalizedConfiguration == lastAppliedConfiguration) { debugLog( - libdigidocInitLogTag, + LIBDIGIDOC_INIT_LOG_TAG, "Configuration unchanged (serial $newSerial); skipping re-apply", ) return@withContext } + + val tslUrl: String? = configurationProvider.tslUrl + val sivaUrl: String? = configurationProvider.sivaUrl + val tsaUrl: String? = configurationProvider.tsaUrl + val tslCerts: List? = configurationProvider.tslCerts + val trustBundle: List? = configurationProvider.certBundle + if (tslUrl.isNullOrBlank() || + sivaUrl.isNullOrBlank() || + tsaUrl.isNullOrBlank() || + tslCerts.isNullOrEmpty() || + trustBundle.isNullOrEmpty() + ) { + errorLog( + LIBDIGIDOC_INIT_LOG_TAG, + "Refusing to apply incomplete configuration, keeping the previously applied one — " + + "config serial: $newSerial, has TSL URL: ${!tslUrl.isNullOrBlank()}, " + + "has TSA URL: ${!tsaUrl.isNullOrBlank()}, has SiVa URL: ${!sivaUrl.isNullOrBlank()}, " + + "TSL signer certs: ${tslCerts?.size ?: 0}, trust bundle certs: ${trustBundle?.size ?: 0}", + ) + return@withContext + } + debugLog( - libdigidocInitLogTag, + LIBDIGIDOC_INIT_LOG_TAG, "Applying configuration to libdigidocpp — " + - "TSL URL: ${configurationProvider.tslUrl}, TSA URL: ${configurationProvider.tsaUrl}, " + - "SiVa URL: ${configurationProvider.sivaUrl}, " + - "TSL signer certs: ${configurationProvider.tslCerts.size}, " + - "trust bundle certs: ${configurationProvider.certBundle.size}, " + + "TSL URL: $tslUrl, TSA URL: $tsaUrl, SiVa URL: $sivaUrl, " + + "TSL signer certs: ${tslCerts.size}, trust bundle certs: ${trustBundle.size}, " + "config serial: $newSerial", ) - overrideTSLUrl(configurationProvider.tslUrl) - overrideTSLCert(configurationProvider.tslCerts) - overrideSivaUrl(configurationProvider.sivaUrl) - overrideTSCerts(configurationProvider.certBundle) - overrideTSUrl(configurationProvider.tsaUrl) - overrideVerifyServiceCert(configurationProvider.certBundle) + overrideTSLUrl(tslUrl) + overrideTSLCert(tslCerts) + overrideSivaUrl(sivaUrl) + overrideTSCerts(trustBundle) + overrideTSUrl(tsaUrl) + overrideVerifyServiceCert(trustBundle) - certBundle = configurationProvider.certBundle + certBundle = trustBundle val tsaUrlPreferenceKey = context.resources.getString(ee.ria.DigiDoc.network.R.string.main_settings_tsa_url_key) val tsaCertPreferenceKey = @@ -252,24 +283,24 @@ class Initialization initTsaUrl( context, tsaUrlPreferenceKey, - configurationProvider.tsaUrl, + tsaUrl, ) initTsCert( context, tsaCertPreferenceKey, "", tsaUrlPreferenceKey, - configurationProvider.tsaUrl, + tsaUrl, ) initSivaUrl( context, sivaUrlPreferenceKey, - configurationProvider.sivaUrl, + sivaUrl, ) initSivaCert( context, sivaCertPreferenceKey, - configurationProvider.certBundle, + trustBundle, ) lastAppliedConfiguration = normalizedConfiguration @@ -279,6 +310,12 @@ class Initialization DigiDocConf.instance().setPKCS12Cert("798.p12") } + private fun initTSLSettings() { + DigiDocConf.instance().setTSLOnlineDigest(true) + DigiDocConf.instance().setTSLTimeOut(10) + debugLog(LIBDIGIDOC_INIT_LOG_TAG, "TSL online digest: true, TSL timeout: 10s") + } + private fun initLibDigiDocLogging( context: Context, isLoggingEnabled: Boolean, @@ -287,7 +324,7 @@ class Initialization if (!logDirectory.exists()) { val isDirCreated = logDirectory.mkdir() if (isDirCreated) { - debugLog(libdigidocInitLogTag, "Directories created or already exist for ${logDirectory.path}") + debugLog(LIBDIGIDOC_INIT_LOG_TAG, "Directories created or already exist for ${logDirectory.path}") } } setLibdigidocppLogLevel(isLoggingEnabled) @@ -319,14 +356,26 @@ class Initialization sharedPreferences.getString(hostPreferenceKey, hostDefaultValue), sharedPreferences.getInt(portPreferenceKey, portDefaultValue), sharedPreferences.getString(usernamePreferenceKey, usernameDefaultValue), - EncryptedPreferences.getString(context, passwordPreferenceKey, passwordDefaultValue), + readProxyPassword(context, passwordPreferenceKey, passwordDefaultValue), ) } catch (e: IllegalStateException) { - errorLog(libdigidocInitLogTag, "Error initializing proxy", e) + errorLog(LIBDIGIDOC_INIT_LOG_TAG, "Error initializing proxy", e) throw RuntimeException(e) } } + private fun readProxyPassword( + context: Context, + passwordPreferenceKey: String, + passwordDefaultValue: String, + ): String = + try { + EncryptedPreferences.getString(context, passwordPreferenceKey, passwordDefaultValue) + } catch (e: Exception) { + errorLog(LIBDIGIDOC_INIT_LOG_TAG, "Unable to read the stored proxy password", e) + passwordDefaultValue + } + private fun initTsaUrl( context: Context, preferenceKey: String, @@ -440,7 +489,7 @@ class Initialization private fun overrideTSCerts(certBundle: List) { DigiDocConf.instance().setTSCert(ByteArray(0)) // Clear existing TS certificates list for (tsCert in certBundle) { - DigiDocConf.instance().addTSCert(Base64.getDecoder().decode(tsCert.removeWhitespaces().trim())) + decodeCert(tsCert)?.let { DigiDocConf.instance().addTSCert(it) } } } @@ -451,32 +500,46 @@ class Initialization private fun overrideTSLCert(tslCerts: List) { DigiDocConf.instance().setTSLCert(ByteArray(0)) // Clear existing TSL certificates list for (tslCert in tslCerts) { - DigiDocConf.instance().addTSLCert(Base64.getDecoder().decode(tslCert.removeWhitespaces().trim())) + decodeCert(tslCert)?.let { DigiDocConf.instance().addTSLCert(it) } } } private fun overrideVerifyServiceCert(certBundle: List) { DigiDocConf.instance().setVerifyServiceCert(ByteArray(0)) for (cert in certBundle) { - DigiDocConf.instance().addVerifyServiceCert(Base64.getDecoder().decode(cert.removeWhitespaces().trim())) + decodeCert(cert)?.let { DigiDocConf.instance().addVerifyServiceCert(it) } } } private suspend fun loadConfiguration(context: Context) { val current = configurationRepository.getConfiguration() - debugLog( - libdigidocInitLogTag, - if (current == null) { - "No cached configuration yet; will apply it once it is loaded" - } else { - "Applying cached configuration" - }, - ) - current?.let { overrideConfiguration(context, it) } + if (current == null) { + errorLog( + LIBDIGIDOC_INIT_LOG_TAG, + "No configuration to apply to libdigidocpp yet; the observer below applies one " + + "as soon as it loads", + ) + } else { + debugLog(LIBDIGIDOC_INIT_LOG_TAG, "Applying cached configuration") + overrideConfiguration(context, current) + } + configScope.launch { - configurationRepository.observeConfigurationUpdates { newConfig -> - debugLog(libdigidocInitLogTag, "Configuration updated; reapplying it to libdigidocpp") - overrideConfiguration(context, newConfig) + try { + configurationRepository.observeConfigurationUpdates { newConfig -> + debugLog(LIBDIGIDOC_INIT_LOG_TAG, "Configuration updated; reapplying it to libdigidocpp") + try { + overrideConfiguration(context, newConfig) + } catch (e: Exception) { + errorLog( + LIBDIGIDOC_INIT_LOG_TAG, + "Unable to apply the updated configuration to libdigidocpp", + e, + ) + } + } + } catch (e: Exception) { + errorLog(LIBDIGIDOC_INIT_LOG_TAG, "Stopped observing configuration updates", e) } } } @@ -490,8 +553,8 @@ class Initialization key: String?, ) { if (TextUtils.equals(key, preferenceKey)) { - val value = sharedPreferences.getString(key, defaultValue) - if (value != null) { + val value = sharedPreferences.getString(key, defaultValue) ?: return + applyToLibdigidocpp("TSA URL") { DigiDocConf.instance().setTSUrl(value.ifEmpty { defaultValue }) } } @@ -510,23 +573,18 @@ class Initialization key: String?, ) { if (TextUtils.equals(key, preferenceKey)) { - if (sharedPreferences.getString( - tsaUrlPreferenceKey, - defaultTsaUrl, - ) == defaultTsaUrl - ) { - overrideTSCerts(certBundle, null) - } else { - overrideTSCerts( - certBundle, - sharedPreferences.getString(key, defaultValue)?.let { - getCustomCertFile( - context, - it, - DIR_TSA_CERT, - ) - }, - ) + val isDefaultTsaUrl = + sharedPreferences.getString(tsaUrlPreferenceKey, defaultTsaUrl) == defaultTsaUrl + val customCertName = sharedPreferences.getString(key, defaultValue) + applyToLibdigidocpp("TSA certificates") { + if (isDefaultTsaUrl) { + overrideTSCerts(certBundle, null) + } else { + overrideTSCerts( + certBundle, + customCertName?.let { getCustomCertFile(context, it, DIR_TSA_CERT) }, + ) + } } } } @@ -541,8 +599,8 @@ class Initialization key: String?, ) { if (TextUtils.equals(key, preferenceKey)) { - val value = sharedPreferences.getString(key, defaultValue) - if (value != null) { + val value = sharedPreferences.getString(key, defaultValue) ?: return + applyToLibdigidocpp("SiVa URL") { DigiDocConf .instance() .setVerifyServiceUri(value.ifEmpty { defaultValue }) @@ -561,17 +619,14 @@ class Initialization key: String?, ) { if (TextUtils.equals(key, preferenceKey)) { - overrideVerifyServiceCert( - context, - defaultValues, - sharedPreferences.getString(key, "")?.let { - getCustomCertFile( - context, - it, - DIR_SIVA_CERT, - ) - }, - ) + val customCertName = sharedPreferences.getString(key, "") + applyToLibdigidocpp("SiVa certificates") { + overrideVerifyServiceCert( + context, + defaultValues, + customCertName?.let { getCustomCertFile(context, it, DIR_SIVA_CERT) }, + ) + } } } } @@ -579,23 +634,39 @@ class Initialization companion object { private var certBundle: List = ArrayList() + private fun decodeCert(cert: String): ByteArray? = + try { + Base64.getDecoder().decode(cert.removeWhitespaces().trim()) + } catch (e: IllegalArgumentException) { + errorLog(LIBDIGIDOC_INIT_LOG_TAG, "Skipping a certificate that is not valid Base64", e) + null + } + private fun getCustomCertFile( context: Context, fileName: String, certFolder: String, ): String? { val certFile: File? = getCertFile(context, fileName, certFolder) - if (certFile != null) { - return try { - readFileContent(certFile.path) - .replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replace("\\s".toRegex(), "") - } catch (_: IllegalStateException) { - null + if (certFile == null) { + if (fileName.isNotBlank()) { + errorLog(LIBDIGIDOC_INIT_LOG_TAG, "Custom certificate '$fileName' was not found in $certFolder") } + return null + } + return try { + readFileContent(certFile.path) + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace("\\s".toRegex(), "") + } catch (e: Exception) { + errorLog( + LIBDIGIDOC_INIT_LOG_TAG, + "Unable to read custom certificate '$fileName' from $certFolder", + e, + ) + null } - return null } private fun overrideTSCerts( @@ -604,15 +675,11 @@ class Initialization ) { DigiDocConf.instance().setTSCert(ByteArray(0)) // Clear existing TS certificates list for (tsCert in certBundle) { - DigiDocConf.instance().addTSCert( - Base64.getDecoder().decode(tsCert.removeWhitespaces().trim()), - ) + decodeCert(tsCert)?.let { DigiDocConf.instance().addTSCert(it) } } if (customTsCert != null) { - DigiDocConf.instance().addTSCert( - Base64.getDecoder().decode(customTsCert.removeWhitespaces().trim()), - ) + decodeCert(customTsCert)?.let { DigiDocConf.instance().addTSCert(it) } } } @@ -624,17 +691,13 @@ class Initialization try { DigiDocConf.instance().setVerifyServiceCert(ByteArray(0)) if (!customSivaCert.isNullOrEmpty()) { - DigiDocConf.instance().addVerifyServiceCert( - Base64.getDecoder().decode(customSivaCert.removeWhitespaces().trim()), - ) + decodeCert(customSivaCert)?.let { DigiDocConf.instance().addVerifyServiceCert(it) } } for (cert in certBundle) { - DigiDocConf.instance().addVerifyServiceCert( - Base64.getDecoder().decode(cert.removeWhitespaces().trim()), - ) + decodeCert(cert)?.let { DigiDocConf.instance().addVerifyServiceCert(it) } } } catch (e: Exception) { - errorLog("Libdigidoc-Initialization", "Error adding custom SiVa certificate", e) + errorLog(LIBDIGIDOC_INIT_LOG_TAG, "Error adding custom SiVa certificate", e) CoroutineScope(Main).launch { Toast .makeText( diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/LibdigidocppDispatcher.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/LibdigidocppDispatcher.kt new file mode 100644 index 000000000..63c2748fe --- /dev/null +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/LibdigidocppDispatcher.kt @@ -0,0 +1,50 @@ +/* + * 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.libdigidoclib.init + +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch + +// Libdigidocpp is not thread-safe and clears a trust list before refilling it, +// so every call is confined here +internal val libdigidocppDispatcher: CoroutineDispatcher = IO.limitedParallelism(1) + +private val libdigidocppScope = CoroutineScope(SupervisorJob() + libdigidocppDispatcher) + +internal fun applyToLibdigidocpp( + description: String, + block: suspend () -> Unit, +) { + libdigidocppScope.launch { + try { + block() + debugLog(LIBDIGIDOC_INIT_LOG_TAG, "Applied $description to libdigidocpp") + } catch (e: Exception) { + errorLog(LIBDIGIDOC_INIT_LOG_TAG, "Failed to apply $description to libdigidocpp", e) + } + } +} diff --git a/libdigidoc-lib/src/main/res/raw/schema.zip b/libdigidoc-lib/src/main/res/raw/schema.zip index 1df7b7fc2..8813774c4 100644 Binary files a/libdigidoc-lib/src/main/res/raw/schema.zip and b/libdigidoc-lib/src/main/res/raw/schema.zip differ diff --git a/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt b/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt index 2f3aa9df8..000748484 100644 --- a/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt +++ b/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt @@ -62,7 +62,6 @@ import ee.ria.DigiDoc.utilsLib.signing.UUIDUtil import ee.ria.DigiDoc.utilsLib.text.MessageUtil import ee.ria.libdigidocpp.Container import ee.ria.libdigidocpp.ExternalSigner -import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.delay import retrofit2.Call import retrofit2.Response @@ -357,7 +356,7 @@ class MobileSignServiceImpl String(dataToSignBytes, StandardCharsets.UTF_8) .removeWhitespaces() - val containerSignatures = signedContainer.getSignatures(Main) + val containerSignatures = signedContainer.getSignatures() signatureInterface = if (containerSignatures.isEmpty()) { diff --git a/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt b/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt index e6eb90da5..c3b1ac2ca 100644 --- a/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt +++ b/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt @@ -57,7 +57,6 @@ import ee.ria.DigiDoc.utilsLib.signing.CertificateUtil import ee.ria.DigiDoc.utilsLib.signing.UUIDUtil import ee.ria.DigiDoc.utilsLib.text.MessageUtil import ee.ria.libdigidocpp.ExternalSigner -import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.delay import retrofit2.Call import java.io.IOException @@ -267,7 +266,7 @@ class SmartSignServiceImpl String(dataToSignBytes, StandardCharsets.UTF_8) .removeWhitespaces() - val containerSignatures = signedContainer.getSignatures(Main) + val containerSignatures = signedContainer.getSignatures() signatureInterface = if (containerSignatures.isEmpty()) { diff --git a/utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/file/FileUtil.kt b/utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/file/FileUtil.kt index 5b923d642..bc5490544 100644 --- a/utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/file/FileUtil.kt +++ b/utils-lib/src/main/kotlin/ee/ria/DigiDoc/utilsLib/file/FileUtil.kt @@ -424,15 +424,19 @@ object FileUtil { directory.delete() } + @Throws(IOException::class) fun writeToFile( reader: BufferedReader, destinationPath: String, fileName: String, ) { val file = File(destinationPath, fileName) + var temporaryFile: File? = null try { - file.outputStream().use { outputStream -> + temporaryFile = File.createTempFile(fileName, ".tmp", File(destinationPath)) + + temporaryFile.outputStream().use { outputStream -> OutputStreamWriter(outputStream, StandardCharsets.UTF_8).use { writer -> reader.forEachLine { line -> writer.write(line) @@ -440,8 +444,20 @@ object FileUtil { } } } - } catch (e: IOException) { + + if (!temporaryFile.renameTo(file)) { + throw IOException("Unable to move the completed file into place: $fileName") + } + infoLog(LOG_TAG, "Wrote file $fileName (${file.length()} bytes)") + } catch (e: Exception) { errorLog(LOG_TAG, "Failed to write to file: $fileName", e) + throw e + } finally { + temporaryFile?.let { + if (it.exists() && !it.delete()) { + errorLog(LOG_TAG, "Unable to delete the temporary file: ${it.name}") + } + } } } diff --git a/utils-lib/src/test/kotlin/ee/ria/DigiDoc/utilsLib/file/FileUtilTest.kt b/utils-lib/src/test/kotlin/ee/ria/DigiDoc/utilsLib/file/FileUtilTest.kt index 57af028b3..55b26afb6 100644 --- a/utils-lib/src/test/kotlin/ee/ria/DigiDoc/utilsLib/file/FileUtilTest.kt +++ b/utils-lib/src/test/kotlin/ee/ria/DigiDoc/utilsLib/file/FileUtilTest.kt @@ -27,6 +27,8 @@ import android.net.Uri import kotlinx.coroutines.runBlocking import org.apache.commons.io.FilenameUtils import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.mockito.Mock @@ -35,6 +37,8 @@ import org.mockito.Mockito.mockStatic import org.mockito.Mockito.`when` import java.io.File import java.io.IOException +import java.io.Reader +import java.nio.file.Files class FileUtilTest { @Mock @@ -244,4 +248,47 @@ class FileUtilTest { assertEquals(0, externalFileUris.size) assertEquals(listOf(), externalFileUris) } + + @Test + fun fileUtil_writeToFile_writesContentAndLeavesNoTemporaryFile() { + val directory = createTempDirectory() + + FileUtil.writeToFile("content".reader().buffered(), directory.path, "eu-lotl.xml") + + val written = File(directory, "eu-lotl.xml") + assertTrue(written.exists()) + assertEquals("content", written.readText().trim()) + assertTrue("Temporary files were left behind", temporaryFilesIn(directory).isEmpty()) + } + + @Test + fun fileUtil_writeToFile_keepsPreviousContentWhenTheSourceFails() { + val directory = createTempDirectory() + val destination = File(directory, "eu-lotl.xml") + destination.writeText("previous") + + val failingReader = + object : Reader() { + override fun read( + buffer: CharArray, + offset: Int, + length: Int, + ): Int = throw IOException("source unavailable") + + override fun close() = Unit + }.buffered() + + assertThrows(IOException::class.java) { + FileUtil.writeToFile(failingReader, directory.path, "eu-lotl.xml") + } + + assertEquals("previous", destination.readText()) + assertTrue("Temporary files were left behind", temporaryFilesIn(directory).isEmpty()) + } + + private fun temporaryFilesIn(directory: File): List = + directory.listFiles()?.filter { it.name.endsWith(".tmp") } ?: emptyList() + + private fun createTempDirectory(): File = + Files.createTempDirectory("file-util-test").toFile().apply { deleteOnExit() } }