From 4089761d25955eba939f72431c2b2a6ee5ac0304 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 --- 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 | 71 +++++++--------- gradle/libs.versions.toml | 4 - 6 files changed, 127 insertions(+), 50 deletions(-) create mode 100644 crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Cdoc1Parser.kt diff --git a/crypto-lib/build.gradle.kts b/crypto-lib/build.gradle.kts index 5f6553a2b..9431c11ff 100644 --- a/crypto-lib/build.gradle.kts +++ b/crypto-lib/build.gradle.kts @@ -70,9 +70,7 @@ dependencies { api(libs.guava) implementation(libs.unboundid.ldapsdk) implementation(libs.okhttp3) - 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 b4d4f2c91..341314b41 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 1baa3b2f3..f5218af22 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 @@ -63,7 +63,6 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.apache.commons.io.FilenameUtils -import org.openeid.cdoc4j.CDOCParser import java.io.File import java.io.FileInputStream import java.io.FileOutputStream @@ -180,7 +179,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})") @@ -193,7 +195,7 @@ class CryptoContainer } } - val cdoc1Recipients = cdoc1?.getRecipients().orEmpty() + val cdoc1Recipients = cdoc1?.recipientCertificates?.map { Addressee(it) }.orEmpty() val recipients = if (cdoc1Recipients.isNotEmpty()) { cdoc1Recipients.onEach { recipient -> @@ -204,11 +206,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, @@ -216,8 +219,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) { @@ -226,7 +234,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) { @@ -234,44 +245,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, @@ -317,11 +304,12 @@ class CryptoContainer 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) + val fileInfo = FileInfo() + var result: Long = cdocReader.nextFile(fileInfo) while (result == CDoc.OK.toLong()) { - val ofile = File(fi.name) + val ofile = File(fileInfo.name) val dir = ContainerUtil.getContainerDataFilesDir( context, @@ -333,12 +321,14 @@ class CryptoContainer cdocReader.readFile(ofs) } dataFiles.add(fileToSave) - result = cdocReader.nextFile(fi) + debugLog(LOG_TAG, "Decrypted data file: $tmp") + result = cdocReader.nextFile(fileInfo) } if (cdocReader.finishDecryption() != 0L) { throw CryptoException("Failed to finish decryption") } + debugLog(LOG_TAG, "Decryption finished, ${dataFiles.size} data file(s) extracted") create( context, @@ -349,6 +339,7 @@ class CryptoContainer encrypted = false, ) } catch (exc: IOException) { + errorLog(LOG_TAG, "IO Exception while decrypting container: ${exc.message}", exc) throw CryptoException("IO Exception: ${exc.message}", exc) } finally { cdocReader.delete() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 159dfe871..7eea27d59 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" }