From bd3f9c1386c1cc1366a3460c0131a9db6f28ece4 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 | 212 ++++++++---------- gradle/libs.versions.toml | 4 - 7 files changed, 197 insertions(+), 124 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 59b2e4f3c..a7d4ca9b1 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 @@ -403,6 +403,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 @@ -443,6 +446,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 32ce1500c..afc86a62b 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.Recipient.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, @@ -144,7 +147,8 @@ class Addressee( } else { "" } - } catch (_: Exception) { + } catch (e: Exception) { + errorLog(LOG_TAG, "Unable to extract CN from certificate", e) "" } @@ -169,7 +173,8 @@ class Addressee( } else { "" } - } catch (_: Exception) { + } catch (e: Exception) { + errorLog(LOG_TAG, "Unable to extract serial number from certificate", e) "" } @@ -197,7 +202,8 @@ class Addressee( } } CertType.UnknownType - } catch (_: Exception) { + } catch (e: Exception) { + errorLog(LOG_TAG, "Unable to extract certificate type", e) CertType.UnknownType } } @@ -209,7 +215,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 e9e5fdbe1..7f4423413 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 @@ -58,7 +58,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 @@ -169,54 +168,35 @@ class CryptoContainer context: Context, file: File, ): CryptoContainer { - val dataFiles = ArrayList() - var recipients = ArrayList() - if (file.extension == CDOC1_EXTENSION) { - val cdoc1Container = openCDOC1(context, file) - dataFiles.addAll(cdoc1Container.getDataFiles()) - recipients.addAll(cdoc1Container.getRecipients()) - } + 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 addressees = ArrayList() + val lockRecipients = ArrayList() val cdocReader = CDocReader.createReader(file.path, null, null, null) debugLog(LOG_TAG, "Reader created: (version ${cdocReader.version})") - - withContext(IO) { - cdocReader.locks.forEach { lock -> - if (lock.isCertificate) { - var concatKDFAlgorithmURI = "" - if (!lock.isRSA) { - concatKDFAlgorithmURI = lock.getString(Lock.Params.CONCAT_DIGEST) - } - addressees.add( - Addressee(lock.label, lock.getBytes(Lock.Params.CERT), concatKDFAlgorithmURI), - ) - } else if (lock.isPKI) { - addressees.add( - Addressee(lock.label, lock.getBytes(Lock.Params.RCPT_KEY), ""), - ) - } else if (lock.isSymmetric) { - addressees.add( - Addressee(lock.label, "", CertType.UnknownType, null, ByteArray(0)), - ) - } else { - addressees.add(Addressee("Unknown capsule", ByteArray(0), "")) - } + try { + withContext(IO) { + cdocReader.locks.forEach { lockRecipients.add(addresseeOf(it)) } } + } finally { cdocReader.delete() } - if (!recipients.isEmpty()) { - addressees.forEach { addressee -> - recipients.forEach { recipient -> - if (addressee.data.contentEquals(recipient.data)) { - recipient.concatKDFAlgorithmURI = addressee.concatKDFAlgorithmURI + val recipients = + if (cdoc1 != null) { + cdoc1.recipientCertificates + .map { Addressee(it) } + .onEach { recipient -> + lockRecipients + .firstOrNull { it.data.contentEquals(recipient.data) } + ?.let { recipient.concatKDFAlgorithmURI = it.concatKDFAlgorithmURI } } - } + } else { + lockRecipients } - } else { - recipients = addressees - } + debugLog(LOG_TAG, "Resolved ${recipients.size} recipient(s) for container ${file.name}") return create( context, @@ -228,42 +208,40 @@ class CryptoContainer ) } - @Throws(CryptoException::class) - suspend fun openCDOC1( - context: Context, - file: File, - ): CryptoContainer { - val dataFiles = ArrayList() - val recipients = ArrayList() + private fun addresseeOf(lock: Lock): Addressee { + debugLog( + LOG_TAG, + "Mapping lock to addressee: label=${lock.label}, " + + "isCDoc1=${lock.isCDoc1}, isPKI=${lock.isPKI}, isSymmetric=${lock.isSymmetric}", + ) + return when { + lock.isCDoc1 -> + Addressee(lock.getBytes(Lock.Params.CERT)).apply { + if (!lock.isRSA) { + concatKDFAlgorithmURI = lock.getString(Lock.Params.CONCAT_DIGEST) + } + } + lock.isPKI -> Addressee(lock.label, lock.getBytes(Lock.Params.RCPT_KEY), "") + lock.isSymmetric -> Addressee(lock.label, "", CertType.UnknownType, null, ByteArray(0)) + else -> { + debugLog(LOG_TAG, "Unknown lock type for label=${lock.label}, mapping to 'Unknown capsule'") + Addressee("Unknown capsule", ByteArray(0), "") + } + } + } + @Throws(CryptoException::class) + 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, - ) - } - @Throws(CryptoException::class, SmartCardReaderException::class) fun decrypt( context: Context, @@ -287,60 +265,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" }