Skip to content

Commit d961958

Browse files
Bump version to 0.1.2 and refactor package structure
Backport aes fix
1 parent 348a8c1 commit d961958

File tree

26 files changed

+231
-50
lines changed

26 files changed

+231
-50
lines changed

asn1/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ kotlin {
6060

6161
mavenPublishing {
6262
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
63-
signAllPublications()
63+
// signAllPublications()
6464

6565
coordinates(group.toString(), "asn1", version.toString())
6666

crypto/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ kotlin {
104104

105105
mavenPublishing {
106106
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
107-
signAllPublications()
107+
// signAllPublications()
108108

109109
coordinates(group.toString(), "crypto", version.toString())
110110

crypto/src/commonTest/kotlin/de/gematik/openhealth/crypto/cipher/AesTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class AesTest {
103103
16.bytes,
104104
"1234567890123456".encodeToByteArray(),
105105
byteArrayOf(),
106+
autoPadding = false
106107
).createCipher(
107108
SecretKey("1234567890123456".encodeToByteArray()),
108109
)
@@ -126,6 +127,7 @@ class AesTest {
126127
"0F 98 50 42 1A DA DC FF 64 5F 7E 79 79 E2 E6 8A".hexToByteArray(
127128
hexSpaceFormat,
128129
),
130+
autoPadding = false
129131
).createDecipher(
130132
SecretKey("1234567890123456".encodeToByteArray()),
131133
)

crypto/src/jvmMain/kotlin/de/gematik/openhealth/crypto/cipher/Aes.jvm.kt

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,20 @@ import javax.crypto.spec.IvParameterSpec
2727
import javax.crypto.spec.SecretKeySpec
2828

2929
@OptIn(UnsafeCryptoApi::class)
30-
private fun AesCipherSpec.algorithmName(): String =
30+
private fun AesCipherSpec.algorithmName(autoPadding: Boolean): String =
3131
when (this) {
32-
is AesEcbSpec -> if (autoPadding) "AES/ECB/PKCS5Padding" else "AES/ECB/NoPadding"
33-
is AesCbcSpec -> if (autoPadding) "AES/CBC/PKCS5Padding" else "AES/CBC/NoPadding"
34-
is AesGcmCipherSpec -> "AES/GCM/NoPadding"
35-
}
32+
is AesEcbSpec -> "AES/ECB/"
33+
is AesCbcSpec -> "AES/CBC/"
34+
is AesGcmCipherSpec -> "AES/GCM/"
35+
} + if (autoPadding) "PKCS5Padding" else "NoPadding"
3636

3737
@OptIn(UnsafeCryptoApi::class)
38-
private fun AesDecipherSpec.algorithmName(): String =
38+
private fun AesDecipherSpec.algorithmName(autoPadding: Boolean): String =
3939
when (this) {
40-
is AesEcbSpec -> if (autoPadding) "AES/ECB/PKCS5Padding" else "AES/ECB/NoPadding"
41-
is AesCbcSpec -> if (autoPadding) "AES/CBC/PKCS5Padding" else "AES/CBC/NoPadding"
42-
is AesGcmDecipherSpec -> "AES/GCM/NoPadding"
43-
}
40+
is AesEcbSpec -> "AES/ECB/"
41+
is AesCbcSpec -> "AES/CBC/"
42+
is AesGcmDecipherSpec -> "AES/GCM/"
43+
} + if (autoPadding) "PKCS5Padding" else "NoPadding"
4444

4545
private class JvmAesCipher(
4646
override val spec: AesCipherSpec,
@@ -50,7 +50,7 @@ private class JvmAesCipher(
5050

5151
@OptIn(UnsafeCryptoApi::class)
5252
private val cipher: Cipher =
53-
Cipher.getInstance(spec.algorithmName()).apply {
53+
Cipher.getInstance(spec.algorithmName(spec.autoPadding)).apply {
5454
val secretKey = SecretKeySpec(key.data, "AES")
5555
when (spec) {
5656
is AesGcmCipherSpec -> {
@@ -59,7 +59,15 @@ private class JvmAesCipher(
5959
updateAAD(spec.aad)
6060
}
6161
is AesCbcSpec -> {
62-
val ivSpec = IvParameterSpec(spec.iv)
62+
val iv =
63+
if (spec.iv.isEmpty()) {
64+
ByteArray(
65+
spec.tagLength.bytes,
66+
) { 0x00 }
67+
} else {
68+
spec.iv
69+
}
70+
val ivSpec = IvParameterSpec(iv)
6371
init(Cipher.ENCRYPT_MODE, secretKey, ivSpec)
6472
}
6573
else -> init(Cipher.ENCRYPT_MODE, secretKey)
@@ -87,7 +95,7 @@ private class JvmAesDecipher(
8795
) : AesDecipher {
8896
@OptIn(UnsafeCryptoApi::class)
8997
private val cipher: Cipher =
90-
Cipher.getInstance(spec.algorithmName()).apply {
98+
Cipher.getInstance(spec.algorithmName(spec.autoPadding)).apply {
9199
val secretKey = SecretKeySpec(key.data, "AES")
92100
when (spec) {
93101
is AesGcmDecipherSpec -> {
@@ -96,7 +104,15 @@ private class JvmAesDecipher(
96104
updateAAD(spec.aad)
97105
}
98106
is AesCbcSpec -> {
99-
val ivSpec = IvParameterSpec(spec.iv)
107+
val iv =
108+
if (spec.iv.isEmpty()) {
109+
ByteArray(
110+
spec.tagLength.bytes,
111+
) { 0x00 }
112+
} else {
113+
spec.iv
114+
}
115+
val ivSpec = IvParameterSpec(iv)
100116
init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
101117
}
102118
else -> init(Cipher.DECRYPT_MODE, secretKey)

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ android.useAndroidX=true
2828
android.nonTransitiveRClass=true
2929

3030
gematik.baseGroup=de.gematik.openhealth
31-
gematik.version=0.1.1
31+
gematik.version=0.1.2

smartcard/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlu
9595

9696
mavenPublishing {
9797
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
98-
signAllPublications()
98+
// signAllPublications()
9999

100100
coordinates(group.toString(), "smartcard", version.toString())
101101

smartcard/src/commonMain/kotlin/de/gematik/openhealth/smartcard/card/HealthCardScope.kt

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,26 @@ import de.gematik.openhealth.smartcard.command.HealthCardResponseStatus
2222
import de.gematik.openhealth.smartcard.command.ResponseException
2323
import de.gematik.openhealth.smartcard.command.commandApdu
2424
import de.gematik.openhealth.smartcard.command.requireSuccess
25+
import kotlin.jvm.JvmSynthetic
2526

26-
interface HealthCardScope : SmartCard.CommunicationScope {
27+
/**
28+
* Scope for communicating with the health card.
29+
*/
30+
interface HealthCardScope : SmartCardCommunicationScope {
2731
/**
28-
* Transmits the command on the given [SmartCard.CommunicationScope] and throws a [ResponseException]
32+
* Transmits the command on the given [SmartCardCommunicationScope] and throws a [ResponseException]
2933
* if the command was not successful.
3034
*/
35+
@JvmSynthetic
3136
suspend fun HealthCardCommand.transmitSuccessfully(): HealthCardResponse =
3237
transmit().also {
3338
it.requireSuccess()
3439
}
3540

41+
/**
42+
* Transmits the command on the given [SmartCardCommunicationScope] and returns the response.
43+
*/
44+
@JvmSynthetic
3645
suspend fun HealthCardCommand.transmit(): HealthCardResponse {
3746
val commandApdu = this.commandApdu(supportsExtendedLength)
3847
return transmit(commandApdu).let {
@@ -45,13 +54,14 @@ interface HealthCardScope : SmartCard.CommunicationScope {
4554
}
4655

4756
private class HealthCardScopeImpl(
48-
scope: SmartCard.CommunicationScope,
57+
scope: SmartCardCommunicationScope,
4958
) : HealthCardScope,
50-
SmartCard.CommunicationScope by scope
51-
52-
suspend fun <R> SmartCard.CommunicationScope.useHealthCard(
53-
block: suspend HealthCardScope.() -> R,
54-
): R = block(HealthCardScopeImpl(this))
59+
SmartCardCommunicationScope by scope
5560

56-
fun <R> SmartCard.CommunicationScope.useHealthCardBlocking(block: HealthCardScope.() -> R): R =
57-
block(HealthCardScopeImpl(this))
61+
/**
62+
* Creates a new [HealthCardScope] for the given [SmartCardCommunicationScope].
63+
*
64+
* @param scope The [SmartCardCommunicationScope] to use for communication.
65+
*
66+
*/
67+
fun SmartCardCommunicationScope.healthCardScope(): HealthCardScope = HealthCardScopeImpl(this)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2025 gematik GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package de.gematik.openhealth.smartcard.card
18+
19+
import de.gematik.openhealth.smartcard.command.CardCommandApdu
20+
import de.gematik.openhealth.smartcard.command.CardResponseApdu
21+
import kotlin.coroutines.resume
22+
import kotlin.coroutines.suspendCoroutine
23+
import kotlin.js.JsExport
24+
25+
/**
26+
* Defines the communication scope for interacting with a specific smart card.
27+
*/
28+
interface SmartCardCommunicationScope {
29+
/**
30+
* Indicates whether the card supports extended length APDU commands.
31+
*/
32+
val supportsExtendedLength: Boolean
33+
34+
/**
35+
* Transmits a command APDU to the smart card and receives the corresponding response APDU.
36+
*/
37+
fun transmit(
38+
commandApdu: CardCommandApdu,
39+
response: (responseApdu: CardResponseApdu) -> Unit,
40+
)
41+
42+
/**
43+
* Ensures extensibility for specific APDU commands.
44+
*/
45+
@JsExport.Ignore
46+
companion object
47+
}
48+
49+
/**
50+
* Transmits a command APDU to the smart card and receives the corresponding response APDU.
51+
*/
52+
suspend fun SmartCardCommunicationScope.transmit(commandApdu: CardCommandApdu): CardResponseApdu =
53+
suspendCoroutine { cont -> transmit(commandApdu) { cont.resume(it) } }

smartcard/src/commonMain/kotlin/de/gematik/openhealth/smartcard/card/TrustedChannel.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,18 @@ internal class TrustedChannelScopeImpl(
6060
override val paceKey: PaceKey,
6161
) : TrustedChannelScope,
6262
HealthCardScope {
63-
override val cardIdentifier: String = scope.cardIdentifier
6463
override val supportsExtendedLength: Boolean = scope.supportsExtendedLength
6564

6665
private val secureMessagingSequenceCounter: ByteArray = ByteArray(CIPHER_BLOCK_SIZE_BYTES)
6766

68-
override suspend fun transmit(commandApdu: CardCommandApdu): CardResponseApdu {
67+
override fun transmit(
68+
commandApdu: CardCommandApdu,
69+
response: (responseApdu: CardResponseApdu) -> Unit,
70+
) {
6971
val encryptedCommandApdu = encrypt(commandApdu)
70-
val encryptedResponseApdu = scope.transmit(encryptedCommandApdu)
71-
return decrypt(encryptedResponseApdu)
72+
scope.transmit(encryptedCommandApdu) { encryptedResponseApdu ->
73+
response(decrypt(encryptedResponseApdu))
74+
}
7275
}
7376

7477
private fun incrementMsgSeqCounter() {

smartcard/src/commonTest/kotlin/de/gematik/openhealth/smartcard/HealthCardTestScope.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,19 @@ import de.gematik.openhealth.smartcard.command.CardResponseApdu
2222
import de.gematik.openhealth.smartcard.command.HealthCardCommand
2323

2424
class HealthCardTestScope(
25-
override val cardIdentifier: String = "",
2625
override val supportsExtendedLength: Boolean = true,
2726
) : HealthCardScope {
2827
private var lastCardCommandAPDU: CardCommandApdu? = null
2928

3029
val lastCommandAPDUBytes: ByteArray
3130
get() = lastCardCommandAPDU?.apdu ?: ByteArray(0)
3231

33-
override suspend fun transmit(command: CardCommandApdu): CardResponseApdu {
34-
lastCardCommandAPDU = command
35-
return CardResponseApdu(byteArrayOf(0x90.toByte(), 0x00))
32+
override fun transmit(
33+
commandApdu: CardCommandApdu,
34+
response: (CardResponseApdu) -> Unit,
35+
) {
36+
lastCardCommandAPDU = commandApdu
37+
response(CardResponseApdu(byteArrayOf(0x90.toByte(), 0x00)))
3638
}
3739

3840
suspend fun test(cmd: HealthCardCommand): ByteArray {

0 commit comments

Comments
 (0)