Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions crypto-lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -444,6 +447,7 @@ class CryptoContainerTest {

assertNotNull(result)
assertEquals(containerRIACDOC1.name, result.name)
assertEquals(3, cryptoContainer.getRecipients().size)
}

@Test
Expand Down
15 changes: 11 additions & 4 deletions crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Addressee.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -148,7 +151,8 @@ class Addressee(
} else {
""
}
} catch (_: Exception) {
} catch (e: Exception) {
errorLog(LOG_TAG, "Unable to extract CN from certificate", e)
""
}

Expand All @@ -173,7 +177,8 @@ class Addressee(
} else {
""
}
} catch (_: Exception) {
} catch (e: Exception) {
errorLog(LOG_TAG, "Unable to extract serial number from certificate", e)
""
}

Expand Down Expand Up @@ -201,7 +206,8 @@ class Addressee(
}
}
CertType.UnknownType
} catch (_: Exception) {
} catch (e: Exception) {
errorLog(LOG_TAG, "Unable to extract certificate type", e)
CertType.UnknownType
}
}
Expand All @@ -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
}
}
Expand Down
81 changes: 81 additions & 0 deletions crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Cdoc1Parser.kt
Original file line number Diff line number Diff line change
@@ -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<String>,
val recipientCertificates: List<ByteArray>,
)

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<String>()
val recipientCertificates = mutableListOf<ByteArray>()
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() }
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})")
Expand All @@ -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 ->
Expand All @@ -204,20 +206,26 @@ 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,
isExistingContainer = true,
)
}

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) {
Expand All @@ -226,52 +234,31 @@ 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) {
serverId = lock.getString(Lock.Params.KEYSERVER_ID).takeIf { it.isNotBlank() }
transactionId = lock.getString(Lock.Params.TRANSACTION_ID).takeIf { it.isNotBlank() }
}
}
}

@Throws(CryptoException::class)
suspend fun openCDOC1(
context: Context,
file: File,
): CryptoContainer {
val dataFiles = ArrayList<File>()
val recipients = ArrayList<Addressee>()

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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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()
Expand Down
4 changes: 0 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down
Loading