From da1aee61de6b861f9f7efd096177827ec8b4a3c2 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Fri, 19 Jun 2026 23:18:26 +0300 Subject: [PATCH] Fix encrypting containers with e-Seal certificates --- .../viewmodel/EncryptRecipientViewModel.kt | 3 +- crypto-lib/build.gradle.kts | 2 - .../DigiDoc/cryptolib/CryptoContainerTest.kt | 4 + .../ee/ria/DigiDoc/cryptolib/Addressee.kt | 15 +- .../ee/ria/DigiDoc/cryptolib/Cdoc1Parser.kt | 81 +++++++++ .../ria/DigiDoc/cryptolib/CryptoContainer.kt | 157 +++++++++--------- gradle/libs.versions.toml | 4 - 7 files changed, 174 insertions(+), 92 deletions(-) create mode 100644 crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Cdoc1Parser.kt diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptRecipientViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptRecipientViewModel.kt index 750fa0309..b274797af 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptRecipientViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptRecipientViewModel.kt @@ -169,7 +169,8 @@ class EncryptRecipientViewModel _errorState.postValue(R.string.crypto_encrypt_data_files_empty_error) } catch (_: RecipientsEmptyException) { _errorState.postValue(R.string.crypto_encrypt_recipients_empty_error) - } catch (_: Exception) { + } catch (e: Exception) { + errorLog(logTag, "Unable to encrypt container", e) _errorState.postValue(R.string.crypto_encrypt_error) } } else { diff --git a/crypto-lib/build.gradle.kts b/crypto-lib/build.gradle.kts index 42fcb3406..b45aa8faa 100644 --- a/crypto-lib/build.gradle.kts +++ b/crypto-lib/build.gradle.kts @@ -69,9 +69,7 @@ dependencies { implementation(libs.bouncy.castle) api(libs.guava) implementation(libs.unboundid.ldapsdk) - implementation(libs.cdoc4j) implementation(libs.preferencex) - implementation(libs.stax.api) testImplementation(libs.junit) diff --git a/crypto-lib/src/androidTest/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainerTest.kt b/crypto-lib/src/androidTest/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainerTest.kt index 08a422c71..12b04736d 100644 --- a/crypto-lib/src/androidTest/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainerTest.kt +++ b/crypto-lib/src/androidTest/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainerTest.kt @@ -404,6 +404,9 @@ class CryptoContainerTest { assertNotNull(result) assertEquals(containerCDOC1.name, result.name) + assertEquals(1, cryptoContainer.getDataFiles().size) + assertEquals("soe_30-04-2025_uus-sadama-16-3.jpeg", cryptoContainer.getDataFiles().first().name) + assertEquals(1, cryptoContainer.getRecipients().size) } @Test @@ -444,6 +447,7 @@ class CryptoContainerTest { assertNotNull(result) assertEquals(containerRIACDOC1.name, result.name) + assertEquals(3, cryptoContainer.getRecipients().size) } @Test diff --git a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Addressee.kt b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Addressee.kt index 6ab998e46..924a18662 100644 --- a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Addressee.kt +++ b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Addressee.kt @@ -21,6 +21,7 @@ package ee.ria.DigiDoc.cryptolib +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.cdoc.Lock.parseLabel import org.bouncycastle.asn1.ASN1InputStream import org.bouncycastle.asn1.ASN1OctetString @@ -35,6 +36,8 @@ import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.util.Date +private const val LOG_TAG = "Addressee" + class Addressee( var data: ByteArray, var identifier: String, @@ -148,7 +151,8 @@ class Addressee( } else { "" } - } catch (_: Exception) { + } catch (e: Exception) { + errorLog(LOG_TAG, "Unable to extract CN from certificate", e) "" } @@ -173,7 +177,8 @@ class Addressee( } else { "" } - } catch (_: Exception) { + } catch (e: Exception) { + errorLog(LOG_TAG, "Unable to extract serial number from certificate", e) "" } @@ -201,7 +206,8 @@ class Addressee( } } CertType.UnknownType - } catch (_: Exception) { + } catch (e: Exception) { + errorLog(LOG_TAG, "Unable to extract certificate type", e) CertType.UnknownType } } @@ -213,7 +219,8 @@ class Addressee( .getInstance("X.509") .generateCertificate(cert.inputStream()) as X509Certificate certificate.notAfter - } catch (_: Exception) { + } catch (e: Exception) { + errorLog(LOG_TAG, "Unable to extract validTo from certificate", e) null } } diff --git a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Cdoc1Parser.kt b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Cdoc1Parser.kt new file mode 100644 index 000000000..b4bbe2161 --- /dev/null +++ b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Cdoc1Parser.kt @@ -0,0 +1,81 @@ +/* + * 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.cryptolib + +import android.util.Xml +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog +import org.xmlpull.v1.XmlPullParser +import java.io.InputStream +import java.util.Base64 + +private const val LOG_TAG = "Cdoc1Parser" +private const val X509_CERTIFICATE = "X509Certificate" +private const val ENCRYPTION_PROPERTY = "EncryptionProperty" +private const val NAME_ATTRIBUTE = "Name" +private const val ORIG_FILE = "orig_file" + +data class Cdoc1Content( + val dataFileNames: List, + val recipientCertificates: List, +) + +object Cdoc1Parser { + fun parse(inputStream: InputStream): Cdoc1Content { + debugLog(LOG_TAG, "Parsing CDOC1 XML stream") + val parser = Xml.newPullParser().apply { setInput(inputStream, null) } + val dataFileNames = mutableListOf() + val recipientCertificates = mutableListOf() + while (parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.eventType != XmlPullParser.START_TAG) { + continue + } + when (parser.localName) { + X509_CERTIFICATE -> certificateOf(parser.nextText())?.let(recipientCertificates::add) + ENCRYPTION_PROPERTY -> + if (parser.isOrigFile()) { + fileNameOf(parser.nextText())?.let(dataFileNames::add) + } + } + } + debugLog( + LOG_TAG, + "Parsed CDOC1: ${dataFileNames.size} data file name(s), " + + "${recipientCertificates.size} recipient certificate(s)", + ) + return Cdoc1Content(dataFileNames, recipientCertificates) + } +} + +private val XmlPullParser.localName: String + get() = name.substringAfterLast(':') + +private fun XmlPullParser.isOrigFile(): Boolean = getAttributeValue(null, NAME_ATTRIBUTE) == ORIG_FILE + +private fun fileNameOf(origFileProperty: String): String? = + origFileProperty.substringBefore('|').trim().ifEmpty { null } + +private fun certificateOf(base64: String): ByteArray? = + runCatching { Base64.getMimeDecoder().decode(base64) } + .onFailure { errorLog(LOG_TAG, "Unable to decode recipient certificate", it) } + .getOrNull() + ?.takeIf { it.isNotEmpty() } diff --git a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainer.kt b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainer.kt index a53db026f..6c749c5ba 100644 --- a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainer.kt +++ b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainer.kt @@ -59,7 +59,6 @@ import ee.ria.cdoc.Recipient import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext import org.apache.commons.io.FilenameUtils -import org.openeid.cdoc4j.CDOCParser import java.io.File import java.io.FileInputStream import java.io.FileOutputStream @@ -171,7 +170,10 @@ class CryptoContainer context: Context, file: File, ): CryptoContainer { - val cdoc1 = if (file.extension == CDOC1_EXTENSION) openCDOC1(context, file) else null + debugLog(LOG_TAG, "Opening crypto container: ${file.name} (extension ${file.extension})") + val cdoc1 = if (file.extension == CDOC1_EXTENSION) parseCdoc1(file) else null + val dataFiles = cdoc1?.dataFileNames?.map { File(it) }.orEmpty() + debugLog(LOG_TAG, "Parsed CDOC1 content: ${cdoc1 != null}, data file count: ${dataFiles.size}") val cdocReader = CDocReader.createReader(file.path, null, null, null) debugLog(LOG_TAG, "Reader created: (version ${cdocReader.version})") @@ -184,7 +186,7 @@ class CryptoContainer } } - val cdoc1Recipients = cdoc1?.getRecipients().orEmpty() + val cdoc1Recipients = cdoc1?.recipientCertificates?.map { Addressee(it) }.orEmpty() val recipients = if (cdoc1Recipients.isNotEmpty()) { cdoc1Recipients.onEach { recipient -> @@ -195,11 +197,12 @@ class CryptoContainer } else { lockAddressees } + debugLog(LOG_TAG, "Resolved ${recipients.size} recipient(s) for container ${file.name}") return create( context, file, - cdoc1?.getDataFiles().orEmpty(), + dataFiles, recipients, decrypted = false, encrypted = true, @@ -207,8 +210,13 @@ class CryptoContainer ) } - private fun addresseeOf(lock: Lock): Addressee = - when { + private fun addresseeOf(lock: Lock): Addressee { + debugLog( + LOG_TAG, + "Mapping lock to addressee with label ${lock.label}. " + + "Is CDOC1: ${lock.isCDoc1}, is PKI: ${lock.isPKI}, is symmetric: ${lock.isSymmetric}", + ) + return when { lock.isCDoc1 -> Addressee(lock.getBytes(Lock.Params.CERT)).apply { if (!lock.isRSA) { @@ -217,7 +225,10 @@ class CryptoContainer } lock.isPKI -> Addressee(lock.label, lock.getBytes(Lock.Params.RCPT_KEY), "") lock.isSymmetric -> Addressee(lock.label, "", CertType.UnknownType, null, ByteArray(0)) - else -> Addressee("Unknown capsule", ByteArray(0), "") + else -> { + debugLog(LOG_TAG, "Unknown lock type for label ${lock.label}, mapping to 'Unknown capsule'") + Addressee("Unknown capsule", ByteArray(0), "") + } }.apply { keyLabel = lock.label.takeIf { it.isNotBlank() } if (lock.type == Lock.Type.SERVER) { @@ -225,44 +236,20 @@ class CryptoContainer transactionId = lock.getString(Lock.Params.TRANSACTION_ID).takeIf { it.isNotBlank() } } } + } @Throws(CryptoException::class) - suspend fun openCDOC1( - context: Context, - file: File, - ): CryptoContainer { - val dataFiles = ArrayList() - val recipients = ArrayList() - + private suspend fun parseCdoc1(file: File): Cdoc1Content = withContext(IO) { + debugLog(LOG_TAG, "Parsing CDOC1 container: ${file.name}") try { - FileInputStream(file).use { dataFilesStream -> - CDOCParser.getDataFileNames(dataFilesStream).forEach { dataFileName -> - dataFiles.add(File(dataFileName)) - } - } - FileInputStream(file).use { recipientsStream -> - CDOCParser.getRecipients(recipientsStream).forEach { recipient -> - val addressee = Addressee(recipient.certificate.encoded) - recipients.add(addressee) - } - } + FileInputStream(file).use { Cdoc1Parser.parse(it) } } catch (e: Exception) { + errorLog(LOG_TAG, "Can't open crypto container: ${e.message}", e) throw CryptoException("Can't open crypto container", e) } } - return create( - context, - file, - dataFiles, - recipients, - decrypted = false, - encrypted = true, - isExistingContainer = true, - ) - } - @Throws(CryptoException::class, SmartCardReaderException::class) fun decrypt( context: Context, @@ -286,60 +273,68 @@ class CryptoContainer val cdocReader = CDocReader.createReader(file.path, conf, token, network) debugLog(LOG_TAG, "Reader created: (version ${cdocReader.version})") - val idx = cdocReader.getLockForCert(authCert) + try { + val idx = cdocReader.getLockForCert(authCert) - if (idx < 0) { - throw CryptoException("Failed to get lock for certificate") - } + if (idx < 0) { + throw CryptoException("Failed to get lock for certificate") + } - val fmk = cdocReader.getFMK(idx.toInt()) + val fmk = cdocReader.getFMK(idx.toInt()) - if (token.lastError != null) { - throw token.lastError as Throwable - } + if (token.lastError != null) { + throw token.lastError as Throwable + } - if (fmk.isEmpty()) { - throw CryptoException("Failed to get FMK") - } + if (fmk.isEmpty()) { + throw CryptoException("Failed to get FMK") + } - if (cdocReader.beginDecryption(fmk) != 0L) { - throw CryptoException("Failed to begin decryption") - } + if (cdocReader.beginDecryption(fmk) != 0L) { + throw CryptoException("Failed to begin decryption") + } + debugLog(LOG_TAG, "Decryption started for container ${file.name}") - val fi = FileInfo() - var result: Long = cdocReader.nextFile(fi) - try { - while (result == CDoc.OK.toLong()) { - val ofile = File(fi.name) - val dir = - ContainerUtil.getContainerDataFilesDir( - context, - file, - ) - val tmp = sanitizeString(ofile.name, "") - val fileToSave = File(dir, tmp) - val ofs: OutputStream = FileOutputStream(fileToSave) - cdocReader.readFile(ofs) - dataFiles.add(fileToSave) - ofs.close() - result = cdocReader.nextFile(fi) + val fileInfo = FileInfo() + var result: Long = cdocReader.nextFile(fileInfo) + try { + while (result == CDoc.OK.toLong()) { + val ofile = File(fileInfo.name) + val dir = + ContainerUtil.getContainerDataFilesDir( + context, + file, + ) + val tmp = sanitizeString(ofile.name, "") + val fileToSave = File(dir, tmp) + val ofs: OutputStream = FileOutputStream(fileToSave) + cdocReader.readFile(ofs) + dataFiles.add(fileToSave) + ofs.close() + debugLog(LOG_TAG, "Decrypted data file: $tmp") + result = cdocReader.nextFile(fileInfo) + } + } catch (exc: IOException) { + errorLog(LOG_TAG, "IO Exception while decrypting data files: ${exc.message}", exc) + throw CryptoException("IO Exception: ${exc.message}", exc) } - } catch (exc: IOException) { - throw CryptoException("IO Exception: ${exc.message}", exc) - } - if (cdocReader.finishDecryption() != 0L) { - throw CryptoException("Failed to finish decryption") - } + if (cdocReader.finishDecryption() != 0L) { + throw CryptoException("Failed to finish decryption") + } + debugLog(LOG_TAG, "Decryption finished, ${dataFiles.size} data file(s) extracted") - return create( - context, - file, - dataFiles, - recipients, - decrypted = true, - encrypted = false, - ) + return create( + context, + file, + dataFiles, + recipients, + decrypted = true, + encrypted = false, + ) + } finally { + cdocReader.delete() + } } @Throws(CryptoException::class) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4ba8db4c0..88837cd38 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,8 +52,6 @@ firebaseCrashlytics = "3.0.7" googleServices = "4.4.4" firebaseCrashlyticsKtx = "19.4.4" kotlinxCoroutinesRx3 = "1.11.0" -cdoc4j = "1.5" -stax-api = "1.0-2" unboundid-ldapsdk = "7.0.4" material-icons-core = "1.7.8" byte-buddy = "1.18.8" @@ -97,9 +95,7 @@ commons-text = { group = "org.apache.commons", name = "commons-text", version.re commons-codec = { module = "commons-codec:commons-codec", version.ref = "commonsCodec" } commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commons-compress" } bouncy-castle = { group = "org.bouncycastle", name = "bcpkix-jdk18on", version.ref = "bouncy-castle" } -cdoc4j = { group = "org.open-eid.cdoc4j", name = "cdoc4j", version.ref = "cdoc4j" } unboundid-ldapsdk = { group = "com.unboundid", name = "unboundid-ldapsdk", version.ref = "unboundid-ldapsdk" } -stax-api = { group = "javax.xml.stream", name = "stax-api", version.ref = "stax-api" } okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp3-tls= { group = "com.squareup.okhttp3", name = "okhttp-tls", version.ref = "okhttp" } okhttp3-mockwebserver= { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "okhttp" }