Skip to content

Commit 07f7961

Browse files
authored
Support for CMAC (#69)
Supported by JDK BouncyCastle and OpenSSL3 providers
1 parent 89f5de4 commit 07f7961

File tree

15 files changed

+398
-10
lines changed

15 files changed

+398
-10
lines changed

build-logic/src/main/kotlin/ckbuild/tests/GenerateProviderTestsTask.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ abstract class GenerateProviderTestsTask : DefaultTask() {
9292

9393
"AesCbcTest",
9494
"AesCbcCompatibilityTest",
95+
"AesCmacTest",
96+
"AesCmacCompatibilityTest",
97+
"AesCmacTestvectorsTest",
9598
"AesCtrTest",
9699
"AesCtrCompatibilityTest",
97100
"AesEcbCompatibilityTest",

cryptography-core/api/cryptography-core.api

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,19 @@ public abstract interface class dev/whyoleg/cryptography/algorithms/AES$CBC$Key
9494
public static synthetic fun cipher$default (Ldev/whyoleg/cryptography/algorithms/AES$CBC$Key;ZILjava/lang/Object;)Ldev/whyoleg/cryptography/algorithms/AES$IvCipher;
9595
}
9696

97+
public abstract interface class dev/whyoleg/cryptography/algorithms/AES$CMAC : dev/whyoleg/cryptography/algorithms/AES {
98+
public static final field Companion Ldev/whyoleg/cryptography/algorithms/AES$CMAC$Companion;
99+
public fun getId ()Ldev/whyoleg/cryptography/CryptographyAlgorithmId;
100+
}
101+
102+
public final class dev/whyoleg/cryptography/algorithms/AES$CMAC$Companion : dev/whyoleg/cryptography/CryptographyAlgorithmId {
103+
}
104+
105+
public abstract interface class dev/whyoleg/cryptography/algorithms/AES$CMAC$Key : dev/whyoleg/cryptography/algorithms/AES$Key {
106+
public abstract fun signatureGenerator ()Ldev/whyoleg/cryptography/operations/SignatureGenerator;
107+
public abstract fun signatureVerifier ()Ldev/whyoleg/cryptography/operations/SignatureVerifier;
108+
}
109+
97110
public abstract interface class dev/whyoleg/cryptography/algorithms/AES$CTR : dev/whyoleg/cryptography/algorithms/AES {
98111
public static final field Companion Ldev/whyoleg/cryptography/algorithms/AES$CTR$Companion;
99112
public fun getId ()Ldev/whyoleg/cryptography/CryptographyAlgorithmId;

cryptography-core/api/cryptography-core.klib.api

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ abstract interface <#A: dev.whyoleg.cryptography.algorithms/AES.Key> dev.whyoleg
3030
final object Companion : dev.whyoleg.cryptography/CryptographyAlgorithmId<dev.whyoleg.cryptography.algorithms/AES.CBC> // dev.whyoleg.cryptography.algorithms/AES.CBC.Companion|null[0]
3131
}
3232

33+
abstract interface CMAC : dev.whyoleg.cryptography.algorithms/AES<dev.whyoleg.cryptography.algorithms/AES.CMAC.Key> { // dev.whyoleg.cryptography.algorithms/AES.CMAC|null[0]
34+
open val id // dev.whyoleg.cryptography.algorithms/AES.CMAC.id|{}id[0]
35+
open fun <get-id>(): dev.whyoleg.cryptography/CryptographyAlgorithmId<dev.whyoleg.cryptography.algorithms/AES.CMAC> // dev.whyoleg.cryptography.algorithms/AES.CMAC.id.<get-id>|<get-id>(){}[0]
36+
37+
abstract interface Key : dev.whyoleg.cryptography.algorithms/AES.Key { // dev.whyoleg.cryptography.algorithms/AES.CMAC.Key|null[0]
38+
abstract fun signatureGenerator(): dev.whyoleg.cryptography.operations/SignatureGenerator // dev.whyoleg.cryptography.algorithms/AES.CMAC.Key.signatureGenerator|signatureGenerator(){}[0]
39+
abstract fun signatureVerifier(): dev.whyoleg.cryptography.operations/SignatureVerifier // dev.whyoleg.cryptography.algorithms/AES.CMAC.Key.signatureVerifier|signatureVerifier(){}[0]
40+
}
41+
42+
final object Companion : dev.whyoleg.cryptography/CryptographyAlgorithmId<dev.whyoleg.cryptography.algorithms/AES.CMAC> // dev.whyoleg.cryptography.algorithms/AES.CMAC.Companion|null[0]
43+
}
44+
3345
abstract interface CTR : dev.whyoleg.cryptography.algorithms/AES<dev.whyoleg.cryptography.algorithms/AES.CTR.Key> { // dev.whyoleg.cryptography.algorithms/AES.CTR|null[0]
3446
open val id // dev.whyoleg.cryptography.algorithms/AES.CTR.id|{}id[0]
3547
open fun <get-id>(): dev.whyoleg.cryptography/CryptographyAlgorithmId<dev.whyoleg.cryptography.algorithms/AES.CTR> // dev.whyoleg.cryptography.algorithms/AES.CTR.id.<get-id>|<get-id>(){}[0]

cryptography-core/src/commonMain/kotlin/algorithms/AES.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ public interface AES<K : AES.Key> : CryptographyAlgorithm {
6060
}
6161
}
6262

63+
@SubclassOptInRequired(CryptographyProviderApi::class)
64+
public interface CMAC : AES<CMAC.Key> {
65+
override val id: CryptographyAlgorithmId<CMAC> get() = Companion
66+
67+
public companion object : CryptographyAlgorithmId<CMAC>("AES-CMAC")
68+
69+
@SubclassOptInRequired(CryptographyProviderApi::class)
70+
public interface Key : AES.Key {
71+
public fun signatureGenerator(): SignatureGenerator
72+
public fun signatureVerifier(): SignatureVerifier
73+
}
74+
}
75+
6376
@SubclassOptInRequired(CryptographyProviderApi::class)
6477
public interface CTR : AES<CTR.Key> {
6578
override val id: CryptographyAlgorithmId<CTR> get() = Companion

cryptography-providers-tests-api/src/commonMain/kotlin/support.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ fun AlgorithmTestScope<out EC<*, *, *>>.supportsPrivateKeyDecoding(
127127

128128
fun ProviderTestScope.supports(algorithmId: CryptographyAlgorithmId<*>): Boolean = validate {
129129
when {
130+
algorithmId == AES.CMAC && provider.isJdkDefault -> "Default JDK provider doesn't support AES-CMAC, only supported with BouncyCastle"
130131
algorithmId == RSA.PSS &&
131132
provider.isJdkDefault &&
132133
platform.isAndroid -> "JDK provider on Android doesn't support RSASSA-PSS"

cryptography-providers-tests/src/commonMain/kotlin/SupportedAlgorithmsTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ abstract class SupportedAlgorithmsTest(provider: CryptographyProvider) : Provide
2424

2525
@Test
2626
fun testSupported() = testWithProvider {
27+
2728
assertSupports(AES.ECB, !context.provider.isWebCrypto)
2829
assertSupports(AES.CBC)
30+
assertSupports(AES.CMAC, !context.provider.isApple && !context.provider.isWebCrypto)
2931
assertSupports(AES.CTR)
3032
assertSupports(AES.GCM, !context.provider.isApple)
3133

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package dev.whyoleg.cryptography.providers.tests.compatibility
2+
3+
import dev.whyoleg.cryptography.*
4+
import dev.whyoleg.cryptography.algorithms.*
5+
import dev.whyoleg.cryptography.providers.tests.api.assertVerifySignature
6+
import dev.whyoleg.cryptography.providers.tests.api.compatibility.*
7+
import dev.whyoleg.cryptography.random.*
8+
import kotlinx.io.bytestring.*
9+
import kotlinx.serialization.*
10+
import kotlin.test.*
11+
12+
private const val maxPlaintextSize = 10000
13+
14+
abstract class AesCmacCompatibilityTest(provider: CryptographyProvider) :
15+
AesBasedCompatibilityTest<AES.CMAC.Key, AES.CMAC>(AES.CMAC, provider) {
16+
17+
override suspend fun CompatibilityTestScope<AES.CMAC>.generate(isStressTest: Boolean) {
18+
val dataIterations = when {
19+
isStressTest -> 10
20+
else -> 5
21+
}
22+
23+
val signatureParametersId = api.signatures.saveParameters(TestParameters.Empty)
24+
generateKeys(isStressTest) { key, keyReference, _ ->
25+
val signer = key.signatureGenerator()
26+
val verifier = key.signatureVerifier()
27+
repeat(dataIterations) {
28+
val dataSize = CryptographyRandom.nextInt(maxPlaintextSize)
29+
logger.log { "dataSize = $dataSize" }
30+
31+
val data = ByteString(CryptographyRandom.nextBytes(dataSize))
32+
val signature = signer.generateSignatureBlocking(data)
33+
logger.log { "signature.size = ${signature.size}" }
34+
35+
verifier.assertVerifySignature(data, signature, "Initial Verify")
36+
api.ciphers.saveData(signatureParametersId, SignatureData(keyReference, data, signature))
37+
}
38+
}
39+
}
40+
41+
override suspend fun CompatibilityTestScope<AES.CMAC>.validate() {
42+
val keys = validateKeys()
43+
api.ciphers.getParameters<TestParameters.Empty> { _, parametersId, _ ->
44+
api.ciphers.getData<SignatureData>(parametersId) { (keyReference, data, signature), _, _ ->
45+
keys[keyReference]?.forEach { key ->
46+
val verifier = key.signatureVerifier()
47+
val generator = key.signatureGenerator()
48+
verifier.assertVerifySignature(data, signature, "Verify")
49+
verifier.assertVerifySignature(data, generator.generateSignature(data), "Sign-Verify")
50+
}
51+
}
52+
}
53+
}
54+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package dev.whyoleg.cryptography.providers.tests.default
6+
7+
import dev.whyoleg.cryptography.*
8+
import dev.whyoleg.cryptography.algorithms.*
9+
import dev.whyoleg.cryptography.random.*
10+
import kotlin.test.*
11+
12+
abstract class AesCmacTest(provider: CryptographyProvider) : AesBasedTest<AES.CMAC>(AES.CMAC, provider) {
13+
14+
@Test
15+
fun verifyResult() = runTestForEachKeySize {
16+
val key = algorithm.keyGenerator(keySize).generateKey()
17+
val data = CryptographyRandom.nextBytes(100)
18+
val signature = key.signatureGenerator().generateSignature(data)
19+
assertTrue(key.signatureVerifier().tryVerifySignature(data, signature))
20+
}
21+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package dev.whyoleg.cryptography.providers.tests.testvectors
2+
3+
import dev.whyoleg.cryptography.*
4+
import dev.whyoleg.cryptography.algorithms.*
5+
import dev.whyoleg.cryptography.algorithms.AES.*
6+
7+
import dev.whyoleg.cryptography.providers.tests.api.*
8+
import kotlin.test.*
9+
10+
/**
11+
* Vector test for AES-CMAC algorithm found in:
12+
* https://datatracker.ietf.org/doc/html/rfc5297
13+
* https://github.com/aead/cmac/blob/master/vectors_test.go
14+
* https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_Core_All.pdf
15+
*/
16+
abstract class AesCmacTestvectorsTest(provider: CryptographyProvider) : AlgorithmTest<CMAC>(CMAC, provider) {
17+
18+
private fun testCase(key: String, salt: String, expected: String) {
19+
testWithAlgorithm {
20+
val key = algorithm.keyDecoder().decodeFromByteArrayBlocking(format = Key.Format.RAW, bytes = key.hexToByteArray())
21+
val result = key.signatureGenerator().createSignFunction()
22+
.apply { update(salt.hexToByteArray()) }
23+
.signToByteArray()
24+
assertEquals(16, result.size)
25+
assertEquals(result.toHexString(), expected)
26+
}
27+
}
28+
29+
@Test
30+
fun testDiversifyKeyCase1() {
31+
val key = "2b7e151628aed2a6abf7158809cf4f3c"
32+
val salt = "6bc1bee22e409f96e93d7e117393172a"
33+
val result = "070a16b46b4d4144f79bdd9dd04a287c"
34+
testCase(key, salt, result)
35+
}
36+
37+
@Test
38+
fun testDiversifyKeyCase2() {
39+
val key = "2b7e151628aed2a6abf7158809cf4f3c"
40+
val salt = "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411"
41+
val result = "dfa66747de9ae63030ca32611497c827"
42+
testCase(key, salt, result)
43+
}
44+
45+
@Test
46+
fun testDiversifyKeyCase3() {
47+
val key = "2b7e151628aed2a6abf7158809cf4f3c"
48+
val salt = "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411"
49+
val result = "dfa66747de9ae63030ca32611497c827"
50+
testCase(key, salt, result)
51+
}
52+
53+
@Test
54+
fun testDiversifyKeyCase4() {
55+
val key = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"
56+
val salt = ""
57+
val result = "028962f61b7bf89efc6b551f4667d983"
58+
testCase(key, salt, result)
59+
}
60+
}

cryptography-providers/jdk/src/jvmMain/kotlin/JdkCryptographyProvider.kt

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,20 @@ internal class JdkCryptographyProvider(
7878
SHA3_384 -> JdkDigest(state, "SHA3-384", SHA3_384)
7979
SHA3_512 -> JdkDigest(state, "SHA3-512", SHA3_512)
8080
RIPEMD160 -> JdkDigest(state, "RIPEMD160", RIPEMD160)
81-
HMAC -> JdkHmac(state)
82-
AES.CBC -> JdkAesCbc(state)
83-
AES.CTR -> JdkAesCtr(state)
84-
AES.ECB -> JdkAesEcb(state)
85-
AES.GCM -> JdkAesGcm(state)
86-
RSA.OAEP -> JdkRsaOaep(state)
81+
HMAC -> JdkHmac(state)
82+
AES.CBC -> JdkAesCbc(state)
83+
AES.CMAC -> JdkAesCmac(state)
84+
AES.CTR -> JdkAesCtr(state)
85+
AES.ECB -> JdkAesEcb(state)
86+
AES.GCM -> JdkAesGcm(state)
87+
RSA.OAEP -> JdkRsaOaep(state)
8788
RSA.PSS -> JdkRsaPss(state)
8889
RSA.PKCS1 -> JdkRsaPkcs1(state)
89-
RSA.RAW -> JdkRsaRaw(state)
90+
RSA.RAW -> JdkRsaRaw(state)
9091
ECDSA -> JdkEcdsa(state)
91-
ECDH -> JdkEcdh(state)
92-
PBKDF2 -> JdkPbkdf2(state)
93-
HKDF -> JdkHkdf(state, this)
92+
ECDH -> JdkEcdh(state)
93+
PBKDF2 -> JdkPbkdf2(state)
94+
HKDF -> JdkHkdf(state, this)
9495
else -> null
9596
}
9697
} as A?
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package dev.whyoleg.cryptography.providers.jdk.algorithms
2+
3+
import dev.whyoleg.cryptography.*
4+
import dev.whyoleg.cryptography.algorithms.*
5+
import dev.whyoleg.cryptography.materials.key.*
6+
import dev.whyoleg.cryptography.operations.*
7+
import dev.whyoleg.cryptography.providers.jdk.*
8+
import dev.whyoleg.cryptography.providers.jdk.materials.*
9+
import dev.whyoleg.cryptography.providers.jdk.operations.*
10+
11+
internal class JdkAesCmac(
12+
private val state: JdkCryptographyState,
13+
) : AES.CMAC {
14+
private val algorithm = "AESCMAC"
15+
private val keyWrapper: (JSecretKey) -> AES.CMAC.Key = { key -> JdkAesCmacKey(state, key) }
16+
private val keyDecoder = JdkSecretKeyDecoder<AES.Key.Format, _>(algorithm, keyWrapper)
17+
18+
override fun keyDecoder(): KeyDecoder<AES.Key.Format, AES.CMAC.Key> = keyDecoder
19+
override fun keyGenerator(keySize: BinarySize): KeyGenerator<AES.CMAC.Key> = JdkSecretKeyGenerator(state, "AES", keyWrapper) {
20+
init(keySize.inBits, state.secureRandom)
21+
}
22+
}
23+
24+
private class JdkAesCmacKey(
25+
state: JdkCryptographyState,
26+
key: JSecretKey,
27+
) : AES.CMAC.Key, JdkEncodableKey<AES.Key.Format>(key) {
28+
private val algorithm = "AESCMAC"
29+
private val signature = JdkMacSignature(state, key, algorithm)
30+
31+
override fun signatureGenerator(): SignatureGenerator = signature
32+
override fun signatureVerifier(): SignatureVerifier = signature
33+
34+
override fun encodeToByteArrayBlocking(format: AES.Key.Format): ByteArray = when (format) {
35+
AES.Key.Format.JWK -> error("$format is not supported")
36+
AES.Key.Format.RAW -> encodeToRaw()
37+
}
38+
}

cryptography-providers/openssl3/api/src/commonMain/kotlin/Openssl3CryptographyProvider.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ internal object Openssl3CryptographyProvider : CryptographyProvider() {
3333
RIPEMD160 -> Openssl3Digest("RIPEMD160", RIPEMD160)
3434
HMAC -> Openssl3Hmac
3535
AES.CBC -> Openssl3AesCbc
36+
AES.CMAC -> Openssl3AesCmac
3637
AES.CTR -> Openssl3AesCtr
3738
AES.ECB -> Openssl3AesEcb
3839
AES.GCM -> Openssl3AesGcm

0 commit comments

Comments
 (0)