Skip to content

Commit 96f3b78

Browse files
committed
Added test cases for the DPoPKeyStore class
1 parent a96a2f9 commit 96f3b78

File tree

4 files changed

+247
-17
lines changed

4 files changed

+247
-17
lines changed

auth0/src/main/java/com/auth0/android/dpop/DPoPException.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import com.auth0.android.Auth0Exception
55
public class DPoPException : Auth0Exception {
66

77
internal enum class Code {
8+
UNSUPPORTED_ERROR,
89
KEY_GENERATION_ERROR,
910
KEY_STORE_ERROR,
10-
SIGNING_FAILURE,
11-
UNKNOWN,
11+
SIGNING_ERROR,
12+
UNKNOWN_ERROR,
1213
}
1314

1415
private var code: Code? = null
@@ -36,20 +37,22 @@ public class DPoPException : Auth0Exception {
3637

3738
public companion object {
3839

40+
public val UNSUPPORTED_ERROR :DPoPException = DPoPException(Code.UNSUPPORTED_ERROR)
3941
public val KEY_GENERATION_ERROR: DPoPException = DPoPException(Code.KEY_GENERATION_ERROR)
4042
public val KEY_STORE_ERROR: DPoPException = DPoPException(Code.KEY_STORE_ERROR)
41-
public val SIGNING_FAILURE: DPoPException = DPoPException(Code.SIGNING_FAILURE)
42-
public val UNKNOWN: DPoPException = DPoPException(Code.UNKNOWN)
43+
public val SIGNING_ERROR: DPoPException = DPoPException(Code.SIGNING_ERROR)
44+
public val UNKNOWN_ERROR: DPoPException = DPoPException(Code.UNKNOWN_ERROR)
4345

4446
private const val DEFAULT_MESSAGE =
4547
"An unknown error has occurred. Please check the error cause for more details."
4648

4749
private fun getMessage(code: Code): String {
4850
return when (code) {
51+
Code.UNSUPPORTED_ERROR -> "DPoP is not supported in versions below Android 9 (API level 28)."
4952
Code.KEY_GENERATION_ERROR -> "Error generating DPoP key pair."
5053
Code.KEY_STORE_ERROR -> "Error while accessing the key pair in the keystore."
51-
Code.SIGNING_FAILURE -> "Error while signing the DPoP proof."
52-
Code.UNKNOWN -> DEFAULT_MESSAGE
54+
Code.SIGNING_ERROR -> "Error while signing the DPoP proof."
55+
Code.UNKNOWN_ERROR -> DEFAULT_MESSAGE
5356
}
5457
}
5558
}

auth0/src/main/java/com/auth0/android/dpop/DPoPKeyStore.kt

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import android.os.Build
66
import android.security.keystore.KeyGenParameterSpec
77
import android.security.keystore.KeyProperties
88
import android.util.Log
9-
import androidx.annotation.RequiresApi
109
import java.security.InvalidAlgorithmParameterException
1110
import java.security.KeyPairGenerator
1211
import java.security.KeyStore
@@ -24,15 +23,15 @@ import javax.security.cert.CertificateException
2423
/**
2524
* Class to handle all DPoP related keystore operations
2625
*/
27-
internal class DPoPKeyStore {
28-
29-
private val keyStore: KeyStore by lazy {
30-
KeyStore.getInstance(ANDROID_KEYSTORE).apply {
31-
load(null)
32-
}
33-
}
26+
internal class DPoPKeyStore(
27+
private val keyStore: KeyStore =
28+
KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
29+
) {
3430

3531
fun generateKeyPair(context: Context) {
32+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
33+
throw DPoPException.UNSUPPORTED_ERROR
34+
}
3635
try {
3736
val keyPairGenerator = KeyPairGenerator.getInstance(
3837
KeyProperties.KEY_ALGORITHM_EC,
@@ -73,7 +72,7 @@ internal class DPoPKeyStore {
7372
throw DPoPException(DPoPException.Code.KEY_GENERATION_ERROR, e)
7473
}
7574

76-
else -> throw DPoPException(DPoPException.Code.UNKNOWN, e)
75+
else -> throw DPoPException(DPoPException.Code.UNKNOWN_ERROR, e)
7776
}
7877
}
7978
}
@@ -88,7 +87,7 @@ internal class DPoPKeyStore {
8887
} catch (e: KeyStoreException) {
8988
throw DPoPException(DPoPException.Code.KEY_STORE_ERROR, e)
9089
}
91-
Log.e(TAG, "Returning null key pair ")
90+
Log.d(TAG, "Returning null key pair ")
9291
return null
9392
}
9493

auth0/src/main/java/com/auth0/android/dpop/DPoPProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ public object DPoPProvider {
328328
return encodeBase64Url(convertDerToRawSignature(signatureBytes))
329329
} catch (e: Exception) {
330330
Log.e(TAG, "Error signing data: ${e.stackTraceToString()}")
331-
throw DPoPException(DPoPException.Code.SIGNING_FAILURE, e)
331+
throw DPoPException(DPoPException.Code.SIGNING_ERROR, e)
332332
}
333333
}
334334

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package com.auth0.android.dpop
2+
3+
import android.content.Context
4+
import android.content.pm.PackageManager
5+
import android.os.Build
6+
import android.security.keystore.KeyGenParameterSpec
7+
import android.security.keystore.KeyProperties
8+
import android.util.Log
9+
import com.nhaarman.mockitokotlin2.any
10+
import com.nhaarman.mockitokotlin2.anyOrNull
11+
import com.nhaarman.mockitokotlin2.mock
12+
import com.nhaarman.mockitokotlin2.never
13+
import com.nhaarman.mockitokotlin2.verify
14+
import com.nhaarman.mockitokotlin2.whenever
15+
import org.hamcrest.MatcherAssert.assertThat
16+
import org.hamcrest.Matchers.`is`
17+
import org.hamcrest.Matchers.notNullValue
18+
import org.hamcrest.Matchers.nullValue
19+
import org.junit.Assert.assertEquals
20+
import org.junit.Assert.assertThrows
21+
import org.junit.Before
22+
import org.junit.Test
23+
import org.junit.runner.RunWith
24+
import org.mockito.Mockito.doNothing
25+
import org.powermock.api.mockito.PowerMockito
26+
import org.powermock.core.classloader.annotations.PrepareForTest
27+
import org.powermock.modules.junit4.PowerMockRunner
28+
import org.powermock.reflect.Whitebox
29+
import java.security.InvalidAlgorithmParameterException
30+
import java.security.KeyPairGenerator
31+
import java.security.KeyStore
32+
import java.security.KeyStoreException
33+
import java.security.PrivateKey
34+
import java.security.PublicKey
35+
import java.security.cert.Certificate
36+
import javax.security.auth.x500.X500Principal
37+
38+
@RunWith(PowerMockRunner::class)
39+
@PrepareForTest(
40+
DPoPKeyStore::class,
41+
KeyStore::class,
42+
KeyPairGenerator::class,
43+
KeyGenParameterSpec.Builder::class,
44+
Build.VERSION::class,
45+
X500Principal::class,
46+
Log::class
47+
)
48+
public class DPoPKeyStoreTest {
49+
50+
private lateinit var mockKeyStore: KeyStore
51+
private lateinit var mockKeyPairGenerator: KeyPairGenerator
52+
private lateinit var mockContext: Context
53+
private lateinit var mockPackageManager: PackageManager
54+
private lateinit var mockSpecBuilder: KeyGenParameterSpec.Builder
55+
56+
private lateinit var dpopKeyStore: DPoPKeyStore
57+
58+
@Before
59+
public fun setUp() {
60+
61+
mockKeyStore = mock()
62+
mockKeyPairGenerator = mock()
63+
mockContext = mock()
64+
mockPackageManager = mock()
65+
mockSpecBuilder = mock()
66+
67+
PowerMockito.mockStatic(KeyStore::class.java)
68+
PowerMockito.mockStatic(KeyPairGenerator::class.java)
69+
PowerMockito.mockStatic(Log::class.java)
70+
PowerMockito.mockStatic(Build.VERSION::class.java)
71+
Whitebox.setInternalState(Build.VERSION::class.java, "SDK_INT", Build.VERSION_CODES.P)
72+
73+
PowerMockito.whenNew(KeyGenParameterSpec.Builder::class.java).withAnyArguments()
74+
.thenReturn(mockSpecBuilder)
75+
76+
// Configure mocks
77+
PowerMockito.`when`(KeyStore.getInstance("AndroidKeyStore")).thenReturn(mockKeyStore)
78+
doNothing().whenever(mockKeyStore).load(anyOrNull())
79+
PowerMockito.`when`(
80+
KeyPairGenerator.getInstance(
81+
KeyProperties.KEY_ALGORITHM_EC,
82+
"AndroidKeyStore"
83+
)
84+
).thenReturn(mockKeyPairGenerator)
85+
86+
whenever(mockSpecBuilder.setAlgorithmParameterSpec(any())).thenReturn(mockSpecBuilder)
87+
whenever(mockSpecBuilder.setDigests(any())).thenReturn(mockSpecBuilder)
88+
whenever(mockSpecBuilder.setCertificateSubject(any())).thenReturn(mockSpecBuilder)
89+
whenever(mockSpecBuilder.setCertificateNotBefore(any())).thenReturn(mockSpecBuilder)
90+
whenever(mockSpecBuilder.setCertificateNotAfter(any())).thenReturn(mockSpecBuilder)
91+
whenever(mockSpecBuilder.setIsStrongBoxBacked(any())).thenReturn(mockSpecBuilder)
92+
whenever(mockContext.packageManager).thenReturn(mockPackageManager)
93+
whenever(mockPackageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)).thenReturn(
94+
true
95+
)
96+
97+
dpopKeyStore = DPoPKeyStore(mockKeyStore)
98+
}
99+
100+
@Test
101+
public fun `generateKeyPair should generate a key pair successfully`() {
102+
whenever(mockPackageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)).thenReturn(
103+
false
104+
)
105+
dpopKeyStore.generateKeyPair(mockContext)
106+
107+
verify(mockKeyPairGenerator).initialize(mockSpecBuilder.build())
108+
verify(mockKeyPairGenerator).generateKeyPair()
109+
verify(mockSpecBuilder, never()).setIsStrongBoxBacked(true)
110+
}
111+
112+
@Test
113+
public fun `generateKeyPair should enable StrongBox when available`() {
114+
dpopKeyStore.generateKeyPair(mockContext)
115+
verify(mockSpecBuilder).setIsStrongBoxBacked(true)
116+
}
117+
118+
@Test
119+
public fun `generateKeyPair should throw KEY_GENERATION_ERROR when failed to generate key pair`() {
120+
val cause = InvalidAlgorithmParameterException("Exception")
121+
PowerMockito.`when`(
122+
mockKeyPairGenerator.initialize(mockSpecBuilder.build())
123+
).thenThrow(cause)
124+
125+
val exception = assertThrows(DPoPException::class.java) {
126+
dpopKeyStore.generateKeyPair(mockContext)
127+
}
128+
assertEquals(exception.message, DPoPException.KEY_GENERATION_ERROR.message)
129+
assertThat(exception.cause, `is`(cause))
130+
}
131+
132+
@Test
133+
public fun `generateKeyPair should throw UNKNOWN_ERROR when any unhandled exception occurs`() {
134+
val cause = RuntimeException("Exception")
135+
PowerMockito.`when`(
136+
mockKeyPairGenerator.initialize(mockSpecBuilder.build())
137+
).thenThrow(cause)
138+
139+
val exception = assertThrows(DPoPException::class.java) {
140+
dpopKeyStore.generateKeyPair(mockContext)
141+
}
142+
assertEquals(exception.message, DPoPException.UNKNOWN_ERROR.message)
143+
assertThat(exception.cause, `is`(cause))
144+
}
145+
146+
@Test
147+
public fun `getKeyPair should return key pair when it exists`() {
148+
val mockPrivateKey = mock<PrivateKey>()
149+
val mockPublicKey = mock<PublicKey>()
150+
val mockCertificate = mock<Certificate>()
151+
152+
whenever(mockKeyStore.getKey(any(), anyOrNull())).thenReturn(mockPrivateKey)
153+
whenever(mockKeyStore.getCertificate(any())).thenReturn(mockCertificate)
154+
whenever(mockCertificate.publicKey).thenReturn(mockPublicKey)
155+
156+
val keyPair = dpopKeyStore.getKeyPair()
157+
158+
assertThat(keyPair, `is`(notNullValue()))
159+
assertThat(keyPair!!.first, `is`(mockPrivateKey))
160+
assertThat(keyPair.second, `is`(mockPublicKey))
161+
}
162+
163+
@Test
164+
public fun `getKeyPair should return null when certificate is null`() {
165+
val mockPrivateKey = mock<PrivateKey>()
166+
whenever(mockKeyStore.getKey(any(), anyOrNull())).thenReturn(mockPrivateKey)
167+
whenever(mockKeyStore.getCertificate(any())).thenReturn(null)
168+
169+
val keyPair = dpopKeyStore.getKeyPair()
170+
assertThat(keyPair, `is`(nullValue()))
171+
}
172+
173+
@Test
174+
public fun `getKeyPair should throw KEY_STORE_ERROR on KeyStoreException`() {
175+
val cause = KeyStoreException("Test Exception")
176+
whenever(mockKeyStore.getKey(any(), anyOrNull())).thenThrow(cause)
177+
178+
val exception = assertThrows(DPoPException::class.java) {
179+
dpopKeyStore.getKeyPair()
180+
}
181+
assertEquals(exception.message, DPoPException.KEY_STORE_ERROR.message)
182+
assertThat(exception.cause, `is`(cause))
183+
}
184+
185+
@Test
186+
public fun `hasKeyPair should return true when alias exists`() {
187+
whenever(mockKeyStore.containsAlias(any())).thenReturn(true)
188+
val result = dpopKeyStore.hasKeyPair()
189+
assertThat(result, `is`(true))
190+
}
191+
192+
@Test
193+
public fun `hasKeyPair should return false when alias does not exist`() {
194+
whenever(mockKeyStore.containsAlias(any())).thenReturn(false)
195+
val result = dpopKeyStore.hasKeyPair()
196+
assertThat(result, `is`(false))
197+
}
198+
199+
@Test
200+
public fun `hasKeyPair should throw KEY_STORE_ERROR on KeyStoreException`() {
201+
val cause = KeyStoreException("Test Exception")
202+
whenever(mockKeyStore.containsAlias(any())).thenThrow(cause)
203+
204+
val exception = assertThrows(DPoPException::class.java) {
205+
dpopKeyStore.hasKeyPair()
206+
}
207+
assertEquals(exception.message, DPoPException.KEY_STORE_ERROR.message)
208+
assertThat(exception.cause, `is`(cause))
209+
}
210+
211+
@Test
212+
public fun `deleteKeyPair should call deleteEntry`() {
213+
dpopKeyStore.deleteKeyPair()
214+
verify(mockKeyStore).deleteEntry(any())
215+
}
216+
217+
@Test
218+
public fun `deleteKeyPair should throw KEY_STORE_ERROR on KeyStoreException`() {
219+
val cause = KeyStoreException("Test Exception")
220+
whenever(mockKeyStore.deleteEntry(any())).thenThrow(cause)
221+
222+
val exception = assertThrows(DPoPException::class.java) {
223+
dpopKeyStore.deleteKeyPair()
224+
}
225+
assertEquals(exception.message, DPoPException.KEY_STORE_ERROR.message)
226+
assertThat(exception.cause, `is`(cause))
227+
}
228+
}

0 commit comments

Comments
 (0)