Skip to content

Commit 5c420d7

Browse files
authored
Use Provider for android API 21-23 to set spiImpl (#44)
1 parent 7ff81a2 commit 5c420d7

File tree

6 files changed

+236
-10
lines changed

6 files changed

+236
-10
lines changed

library/common/api/common.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ public abstract interface class org/kotlincrypto/core/Updatable {
2222
public abstract fun update ([BII)V
2323
}
2424

25+
public final class org/kotlincrypto/core/_AndroidSdkIntKt {
26+
}
27+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2023 Matthew Nelson
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+
* https://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+
package org.kotlincrypto.core
17+
18+
@InternalKotlinCryptoApi
19+
public val KC_ANDROID_SDK_INT: Int? by lazy {
20+
try {
21+
val clazz = Class.forName("android.os.Build\$VERSION")
22+
23+
try {
24+
clazz?.getField("SDK_INT")?.getInt(null)
25+
} catch (_: Throwable) {
26+
clazz?.getField("SDK")?.get(null)?.toString()?.toIntOrNull()
27+
}
28+
} catch (_: Throwable) {
29+
null
30+
}
31+
}

library/mac/src/jvmMain/kotlin/org/kotlincrypto/core/Mac.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
**/
1616
package org.kotlincrypto.core
1717

18+
import org.kotlincrypto.core.internal.AndroidApi21to23MacSpiProvider
1819
import org.kotlincrypto.core.internal.commonInit
1920
import org.kotlincrypto.core.internal.commonToString
2021
import java.nio.ByteBuffer
@@ -48,8 +49,11 @@ public actual abstract class Mac
4849
protected actual constructor(
4950
algorithm: String,
5051
private val engine: Engine,
51-
) : javax.crypto.Mac(engine, null, algorithm),
52-
Algorithm,
52+
) : javax.crypto.Mac(
53+
/* macSpi */ engine,
54+
/* provider */ AndroidApi21to23MacSpiProvider.createOrNull(engine, algorithm),
55+
/* algorithm */ algorithm
56+
), Algorithm,
5357
Copyable<Mac>,
5458
Resettable,
5559
Updatable
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright (c) 2023 Matthew Nelson
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+
* https://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+
package org.kotlincrypto.core.internal
17+
18+
import org.kotlincrypto.core.KC_ANDROID_SDK_INT
19+
import org.kotlincrypto.core.InternalKotlinCryptoApi
20+
import java.security.NoSuchAlgorithmException
21+
import java.security.Provider
22+
import javax.crypto.MacSpi
23+
24+
/**
25+
* Android API 21-23 requires that a Provider be set, otherwise
26+
* when [javax.crypto.Mac.init] is called it will not use the
27+
* provided [org.kotlincrypto.core.Mac.Engine] (i.e., [spi]).
28+
*
29+
* This simply wraps the [org.kotlincrypto.core.Mac.Engine]
30+
* such that initial [javax.crypto.Mac.init] call sets it
31+
* as the spiImpl, and does not look to system providers
32+
* for an instance that supports the [algorithm].
33+
*
34+
* See: https://github.com/KotlinCrypto/core/issues/37
35+
* See: https://github.com/KotlinCrypto/core/issues/41
36+
* See: https://github.com/KotlinCrypto/core/issues/42
37+
* */
38+
@Suppress("DEPRECATION")
39+
internal class AndroidApi21to23MacSpiProvider private constructor(
40+
@Volatile
41+
private var spi: MacSpi?,
42+
private val algorithm: String,
43+
): Provider("KC", 0.0, "") {
44+
45+
override fun getService(type: String?, algorithm: String?): Service = synchronized(this) {
46+
if (type == "Mac" && algorithm == this.algorithm && spi != null) {
47+
SpiProviderService()
48+
} else {
49+
throw NoSuchAlgorithmException("type[$type] and algorithm[$algorithm] not supported")
50+
}
51+
}
52+
53+
private inner class SpiProviderService: Service(
54+
/* provider */ this,
55+
/* type */ "Mac",
56+
/* algorithm */ algorithm,
57+
/* className */ "",
58+
/* aliases */ null,
59+
/* attributes */ null
60+
) {
61+
override fun newInstance(constructorParameter: Any?): Any = synchronized(this@AndroidApi21to23MacSpiProvider) {
62+
// simply return this if spi reference was dropped. b/c this is not
63+
// an instance of MacSpi, android's implementation of javax.crypto.Mac
64+
// will throw an exception which is what we want.
65+
val engine = spi ?: throw NoSuchAlgorithmException("algorithm[$algorithm] not supported")
66+
67+
// javax.crypto.Mac.init was called with a blanked key via org.kotlincrypto.Mac's
68+
// init block in order to set javax.crypto.Mac.initialized to true. return
69+
// the MacSpi (i.e. org.kotlincrypto.Mac.Engine), and null the reference as
70+
// we cannot provide a new instance if called again and do not want to return the
71+
// same, already initialized org.kotlincrypto.Mac.Engine
72+
spi = null
73+
74+
return engine
75+
}
76+
}
77+
78+
internal companion object {
79+
80+
@JvmStatic
81+
@JvmSynthetic
82+
internal fun createOrNull(engine: MacSpi, algorithm: String): AndroidApi21to23MacSpiProvider? {
83+
@OptIn(InternalKotlinCryptoApi::class)
84+
return KC_ANDROID_SDK_INT?.let { sdkInt ->
85+
if (sdkInt in 21..23) {
86+
AndroidApi21to23MacSpiProvider(engine, algorithm)
87+
} else {
88+
null
89+
}
90+
}
91+
}
92+
}
93+
}

library/mac/src/jvmTest/kotlin/org/kotlincrypto/core/JvmMacUnitTest.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,24 @@
1515
**/
1616
package org.kotlincrypto.core
1717

18+
import junit.framework.TestCase.assertEquals
1819
import java.security.InvalidKeyException
1920
import javax.crypto.spec.SecretKeySpec
2021
import kotlin.test.Test
22+
import kotlin.test.assertNull
2123
import kotlin.test.fail
2224

2325
class JvmMacUnitTest {
2426

2527
private val key = ByteArray(20) { it.toByte() }
2628

29+
@Test
30+
fun givenJvm_whenNotAndroid_providerIsNotSet() {
31+
val mac = TestMac(key, "My Algorithm", doFinal = { key })
32+
assertEquals(key, mac.doFinal())
33+
assertNull(mac.provider)
34+
}
35+
2736
@Test
2837
fun givenJvm_whenJavaxCryptoMacInitInvoked_thenThrowsException() {
2938
val mac = TestMac(key, "My Algorithm")

test-android/src/androidInstrumentedTest/kotlin/org/kotlincrypto/test/AndroidMacTest.kt

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,44 @@
1717

1818
package org.kotlincrypto.test
1919

20+
import android.os.Build
2021
import org.kotlincrypto.core.InternalKotlinCryptoApi
2122
import org.kotlincrypto.core.Mac
23+
import java.security.NoSuchAlgorithmException
24+
import java.security.NoSuchProviderException
25+
import java.security.Provider
2226
import javax.crypto.spec.SecretKeySpec
23-
import kotlin.test.Test
27+
import kotlin.test.*
2428

2529
class AndroidMacTest {
2630

31+
private val key = ByteArray(50) { it.toByte() }
32+
2733
@OptIn(InternalKotlinCryptoApi::class)
2834
class TestMac: Mac {
2935

30-
constructor(key: ByteArray): this(Engine("HmacSHA256", key))
36+
private val engine: Engine
37+
38+
fun updateCount(): Int = engine.count
3139

32-
private constructor(engine: Engine): super(engine.algorithm, engine)
40+
constructor(key: ByteArray): this(Engine("Anything????", key))
41+
42+
private constructor(engine: Engine): super(engine.algorithm, engine) {
43+
this.engine = engine
44+
}
3345

3446
private class Engine: Mac.Engine {
3547

48+
var count = 0
3649
val algorithm: String
3750
val delegate: javax.crypto.Mac
3851

3952
constructor(algorithm: String, key: ByteArray): super(key) {
4053
this.algorithm = algorithm
41-
this.delegate = getInstance(algorithm)
54+
55+
// Use HmacSHA256 for the tests such that we can get a non-static
56+
// result.
57+
this.delegate = getInstance("HmacSHA256")
4258
this.delegate.init(SecretKeySpec(key, "HmacSHA256"))
4359
}
4460

@@ -53,6 +69,7 @@ class AndroidMacTest {
5369

5470
override fun update(input: ByteArray, offset: Int, len: Int) {
5571
delegate.update(input, offset, len)
72+
count++
5673
}
5774

5875
override fun doFinal(): ByteArray = delegate.doFinal()
@@ -65,9 +82,78 @@ class AndroidMacTest {
6582
}
6683

6784
@Test
68-
fun givenAndroid_whenMacInstantiated_thenPasses() {
69-
// https://github.com/KotlinCrypto/core/issues/37
70-
val key = ByteArray(50) { it.toByte() }
71-
TestMac(key).doFinal()
85+
fun givenAndroid_whenMacInstantiated_thenUsesProvidedEngine() {
86+
val testMac = TestMac(key)
87+
testMac.apply { update(key) }.doFinal()
88+
89+
assertEquals(1, testMac.updateCount())
90+
}
91+
92+
@Test
93+
fun givenAndroid_whenApi23OrBelow_thenUsesProvider() {
94+
val provider = TestMac(key).provider
95+
if (Build.VERSION.SDK_INT in 21..23) {
96+
assertNotNull(provider)
97+
} else {
98+
assertNull(provider)
99+
}
100+
}
101+
102+
@Test
103+
fun givenAndroid_whenProvider_getServiceThrowsException() {
104+
val (mac, provider) = testMacAndProviderOrNull() ?: return
105+
106+
try {
107+
provider.getService("Mac", mac.algorithm)
108+
fail()
109+
} catch (_: NoSuchAlgorithmException) {
110+
// pass
111+
}
112+
}
113+
114+
@Test
115+
fun givenAndroid_whenMacGetInstanceForAlgorithm_thenIsNotCachedInMacSERVICE() {
116+
val (mac, _) = testMacAndProviderOrNull() ?: return
117+
118+
try {
119+
javax.crypto.Mac.getInstance(mac.algorithm)
120+
fail()
121+
} catch (_: NoSuchAlgorithmException) {
122+
// pass
123+
}
124+
}
125+
126+
@Test
127+
fun givenAndroid_whenMacGetInstanceForAlgorithmAndProviderName_thenIsNotCachedInMacSERVICE() {
128+
val (mac, provider) = testMacAndProviderOrNull() ?: return
129+
130+
try {
131+
javax.crypto.Mac.getInstance(mac.algorithm, provider.name)
132+
fail()
133+
} catch (_: NoSuchProviderException) {
134+
// pass
135+
}
136+
}
137+
138+
@Test
139+
fun givenAndroid_whenMacGetInstanceForAlgorithmAndProvider_thenIsNotCachedInMacSERVICE() {
140+
val (mac, provider) = testMacAndProviderOrNull() ?: return
141+
142+
try {
143+
// Even if the provider is used in getInstance, the spi should
144+
// be de-referenced which would return null and then throw
145+
// an exception here. We do NOT want any provider apis used
146+
// to obtain an instance of the spi.
147+
javax.crypto.Mac.getInstance(mac.algorithm, provider)
148+
fail()
149+
} catch (_: NoSuchAlgorithmException) {
150+
// pass
151+
}
152+
}
153+
154+
private fun testMacAndProviderOrNull(): Pair<Mac, Provider>? {
155+
val mac = TestMac(key)
156+
val provider = mac.provider ?: return null
157+
return Pair(mac, provider)
72158
}
73159
}

0 commit comments

Comments
 (0)