react-native-dpop 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,18 +4,19 @@ React Native library for DPoP proof generation and key management.
4
4
 
5
5
  ## Features
6
6
 
7
- - Generate DPoP proofs (`dpop+jwt`) signed with ES256.
8
- - Manage key pairs in the device keystore (create, rotate, delete).
9
- - Export public key in `JWK`, `DER`, or `RAW` format.
10
- - Calculate JWK thumbprint (`SHA-256`, base64url).
11
- - Verify if a proof is bound to a given key alias.
12
- - Retrieve non-sensitive key metadata (hardware-backed, StrongBox info, etc.).
13
- - iOS key storage uses Secure Enclave when available, with Keychain fallback.
7
+ - Generate DPoP proofs (`dpop+jwt`) signed with ES256
8
+ - Manage key pairs in the platform keystore
9
+ - Export the public key as `JWK`, `DER`, or `RAW`
10
+ - Calculate JWK thumbprints (`SHA-256`, base64url)
11
+ - Verify whether a proof is bound to a given key alias
12
+ - Retrieve non-sensitive key metadata, including secure hardware details
13
+ - Use Secure Enclave on iOS when available, with Keychain fallback
14
+ - Prefer StrongBox on Android when available, with hardware-backed fallback
14
15
 
15
- ## Platform Support
16
+ ## Platform support
16
17
 
17
- - Android: supported.
18
- - iOS: supported.
18
+ - Android
19
+ - iOS
19
20
 
20
21
  ## Installation
21
22
 
@@ -23,66 +24,172 @@ React Native library for DPoP proof generation and key management.
23
24
  npm install react-native-dpop
24
25
  ```
25
26
 
26
- For iOS, install pods in your app project:
27
+ For iOS:
27
28
 
28
29
  ```sh
29
30
  cd ios && pod install
30
31
  ```
31
32
 
32
- ## Quick Start
33
+ ## Quick start
33
34
 
34
35
  ```ts
35
36
  import { DPoP } from 'react-native-dpop';
36
37
 
37
- const dpop = await DPoP.generateProof({
38
+ const dPoP = await DPoP.generateProof({
38
39
  htu: 'https://api.example.com/token',
39
40
  htm: 'POST',
40
41
  accessToken: 'ACCESS_TOKEN',
41
- nonce: 'server-nonce',
42
+ nonce: 'SERVER_NONCE',
43
+ requireHardwareBacked: true,
42
44
  });
43
45
 
44
- const proof = dpop.proof;
45
- const thumbprint = await dpop.calculateThumbprint();
46
- const publicJwk = await dpop.getPublicKey('JWK');
47
- const isBound = await dpop.isBoundToAlias();
46
+ const proof = dPoP.proof;
47
+ const thumbprint = await dPoP.getPublicKeyThumbprint();
48
+ const publicJwk = await dPoP.getPublicKey('JWK');
49
+ const keyInfo = await DPoP.getKeyInfo();
48
50
  ```
49
51
 
50
52
  ## API
51
53
 
52
- ### Types
53
-
54
- - `GenerateProofInput`
55
- - `DPoPProofContext`
56
- - `DPoPKeyInfo`
57
- - `SecureHardwareFallbackReason = 'UNAVAILABLE' | 'PROVIDER_ERROR' | 'POLICY_REJECTED' | 'UNKNOWN'`
58
- - `PublicJwk`
59
- - `PublicKeyFormat = 'JWK' | 'DER' | 'RAW'`
60
-
61
- ### `DPoP` static methods
54
+ ### Static methods
62
55
 
63
56
  - `DPoP.generateProof(input): Promise<DPoP>`
57
+ - `DPoP.buildDPoPHeaders(input): Promise<DPoPHeaders>`
64
58
  - `DPoP.assertHardwareBacked(alias?): Promise<void>`
65
59
  - `DPoP.deleteKeyPair(alias?): Promise<void>`
66
60
  - `DPoP.getKeyInfo(alias?): Promise<DPoPKeyInfo>`
67
61
  - `DPoP.hasKeyPair(alias?): Promise<boolean>`
68
62
  - `DPoP.rotateKeyPair(alias?): Promise<void>`
69
63
 
70
- ### `DPoP` instance fields
64
+ ### Instance members
71
65
 
72
66
  - `proof: string`
73
67
  - `proofContext: DPoPProofContext`
74
68
  - `alias?: string`
75
-
76
- ### `DPoP` instance methods
77
-
78
- - `calculateThumbprint(): Promise<string>`
79
69
  - `getPublicKey(format): Promise<PublicJwk | string>`
80
- - `signWithDpopPrivateKey(payload): Promise<string>`
70
+ - `getPublicKeyThumbprint(): Promise<string>`
71
+ - `signWithDPoPPrivateKey(payload): Promise<string>`
81
72
  - `isBoundToAlias(alias?): Promise<boolean>`
82
73
 
83
- ## Error Codes
74
+ ### `signWithDPoPPrivateKey()`
75
+
76
+ `signWithDPoPPrivateKey()` reuses the same private key pair managed by the DPoP alias. It does not create or use a separate signing key.
77
+
78
+ This means:
79
+
80
+ - the signature is produced with the same key material used for DPoP proofs
81
+ - the active alias determines which private key is used
82
+ - if the alias points to a hardware-backed key, the same hardware-backed key is reused
83
+ - if the alias points to a fallback software-backed key, the same fallback key is reused
84
+
85
+ Recommended usage:
86
+
87
+ - use this only when you intentionally want to sign arbitrary payloads with the same DPoP key
88
+ - avoid treating it as a general-purpose application signing API
89
+ - if you need a different trust boundary or lifecycle, use a different alias or a different key management flow
90
+
91
+ ### Main types
92
+
93
+ - `GenerateProofInput`
94
+ - `DPoPHeaders`
95
+ - `DPoPProofContext`
96
+ - `DPoPKeyInfo`
97
+ - `PublicJwk`
98
+ - `PublicKeyFormat = 'JWK' | 'DER' | 'RAW'`
99
+ - `SecureHardwareFallbackReason = 'UNAVAILABLE' | 'PROVIDER_ERROR' | 'POLICY_REJECTED' | 'UNKNOWN'`
100
+ - `AndroidSecurityLevelName = 'SOFTWARE' | 'TRUSTED_ENVIRONMENT' | 'STRONGBOX'`
101
+ - `IOSSecurityLevelName = 'SOFTWARE' | 'SECURE_ENCLAVE'`
102
+
103
+ ## `getKeyInfo()`
104
+
105
+ `getKeyInfo()` returns shared fields plus platform-specific hardware metadata.
106
+
107
+ ```ts
108
+ type DPoPKeyInfo = {
109
+ alias: string;
110
+ hasKeyPair: boolean;
111
+ algorithm?: string;
112
+ curve?: string;
113
+ insideSecureHardware?: boolean;
114
+ hardware?: {
115
+ android?: {
116
+ strongBoxAvailable: boolean;
117
+ strongBoxBacked: boolean;
118
+ securityLevel?: number;
119
+ securityLevelName?: 'SOFTWARE' | 'TRUSTED_ENVIRONMENT' | 'STRONGBOX';
120
+ strongBoxFallbackReason?: 'UNAVAILABLE' | 'PROVIDER_ERROR' | 'POLICY_REJECTED' | 'UNKNOWN' | null;
121
+ };
122
+ ios?: {
123
+ secureEnclaveAvailable: boolean;
124
+ secureEnclaveBacked: boolean;
125
+ securityLevel?: number | null;
126
+ securityLevelName?: 'SOFTWARE' | 'SECURE_ENCLAVE';
127
+ secureEnclaveFallbackReason?: 'UNAVAILABLE' | 'PROVIDER_ERROR' | 'POLICY_REJECTED' | 'UNKNOWN' | null;
128
+ };
129
+ };
130
+ };
131
+ ```
132
+
133
+ ### Security level semantics
134
+
135
+ - `securityLevel = 1`
136
+ Software-backed key material
137
+ - `securityLevel = 2`
138
+ Hardware-backed key material
139
+ On Android this usually means TEE
140
+ On iOS this means Secure Enclave
141
+ - `securityLevel = 3`
142
+ Android StrongBox-backed key
143
+ - `securityLevel = null`
144
+ No key material available, or the native platform did not report a numeric level
145
+
146
+ ### Fallback semantics
147
+
148
+ - On Android, the library tries StrongBox first when available
149
+ - On iOS, the library tries Secure Enclave first when available
150
+ - Fallback reasons are sanitized enums rather than raw native errors
151
+ - On iOS Simulator, `secureEnclaveFallbackReason` is expected to be `UNAVAILABLE`
152
+
153
+ ## `buildDPoPHeaders()`
154
+
155
+ `buildDPoPHeaders()` generates a proof and returns request headers ready to use.
84
156
 
85
- Native errors are rejected with codes such as:
157
+ ```ts
158
+ const headers = await DPoP.buildDPoPHeaders({
159
+ htu: 'https://api.example.com/token',
160
+ htm: 'POST',
161
+ accessToken: 'ACCESS_TOKEN',
162
+ });
163
+
164
+ // {
165
+ // DPoP: '<proof>',
166
+ // Authorization: 'DPoP ACCESS_TOKEN',
167
+ // }
168
+ ```
169
+
170
+ If `accessToken` is omitted, only the `DPoP` header is returned.
171
+
172
+ ## Notes
173
+
174
+ - Default alias: `react-native-dpop`
175
+ - `htm` is normalized to uppercase
176
+ - `ath` is derived from `accessToken` when provided
177
+ - `jti` and `iat` are auto-generated when omitted
178
+ - `requireHardwareBacked` forces proof generation to fail instead of silently persisting a software-backed fallback key
179
+ - For React Native 0.75 on Android, the library ensures `iat` is sent as a number to avoid an older bridge nullability issue with `Double`
180
+
181
+ ## Example apps
182
+
183
+ This repository includes two example apps:
184
+
185
+ - `examples/v0.75`
186
+ - `examples/v0.83`
187
+
188
+ The root `example` script points to `examples/v0.83`.
189
+
190
+ ## Errors
191
+
192
+ Native rejections use codes such as:
86
193
 
87
194
  - `ERR_DPOP_GENERATE_PROOF`
88
195
  - `ERR_DPOP_CALCULATE_THUMBPRINT`
@@ -95,23 +202,6 @@ Native errors are rejected with codes such as:
95
202
  - `ERR_DPOP_ASSERT_HARDWARE_BACKED`
96
203
  - `ERR_DPOP_IS_BOUND_TO_ALIAS`
97
204
 
98
- ## Notes
99
-
100
- - If no alias is provided, the default alias is `react-native-dpop`.
101
- - `getKeyInfo` returns cross-platform fields and platform-specific details in `hardware`:
102
- - Android: `hardware.android.strongBoxAvailable`, `hardware.android.strongBoxBacked`, `hardware.android.securityLevel`, `hardware.android.strongBoxFallbackReason`
103
- - iOS: `hardware.ios.secureEnclaveAvailable`, `hardware.ios.secureEnclaveBacked`, `hardware.ios.securityLevel`, `hardware.ios.secureEnclaveFallbackReason`
104
- - Fallback reasons are sanitized enums (no raw native error): `UNAVAILABLE`, `PROVIDER_ERROR`, `POLICY_REJECTED`, `UNKNOWN`.
105
- - `securityLevel` semantics:
106
- - `null`: no key material available (or not reported)
107
- - `1`: not backed by secure enclave/strong dedicated hardware
108
- - `2`: hardware-backed (iOS Secure Enclave, Android typically TEE)
109
- - `3`: Android-only StrongBox (when reported by the device)
110
- - On iOS, `securityLevel` is normalized by this library (`2` for Secure Enclave-backed keys, `1` for Keychain fallback), not a native Apple numeric level API.
111
- - `htm` is normalized to uppercase in proof generation.
112
- - `ath` is derived from `accessToken` (`SHA-256`, base64url) when provided.
113
- - `jti` and `iat` are auto-generated when omitted.
114
-
115
205
  ## Contributing
116
206
 
117
207
  - [Development workflow](CONTRIBUTING.md#development-workflow)
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
11
11
  s.license = package["license"]
12
12
  s.authors = package["author"]
13
13
 
14
- s.platforms = { :ios => min_ios_version_supported }
14
+ s.platforms = { :ios => "14.0" }
15
15
  s.source = { :git => "https://github.com/Cirilord/react-native-dpop.git", :tag => "#{s.version}" }
16
16
 
17
17
  s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
@@ -28,6 +28,7 @@ internal data class KeyStoreKeyInfo(
28
28
  val curve: String,
29
29
  val insideSecureHardware: Boolean,
30
30
  val securityLevel: Int?,
31
+ val securityLevelName: String,
31
32
  val strongBoxAvailable: Boolean,
32
33
  val strongBoxBacked: Boolean
33
34
  )
@@ -112,6 +113,8 @@ internal class DPoPKeyStore(private val context: Context) {
112
113
  val keyPair = getKeyPair(alias)
113
114
  val keyFactory = KeyFactory.getInstance(keyPair.privateKey.algorithm, KEYSTORE_PROVIDER)
114
115
  val keyInfo = keyFactory.getKeySpec(keyPair.privateKey, KeyInfo::class.java)
116
+ val strongBoxAvailable = isStrongBoxEnabled()
117
+ val strongBoxBacked = strongBoxAvailable && readStrongBoxBacked(keyInfo)
115
118
 
116
119
  return KeyStoreKeyInfo(
117
120
  alias = alias,
@@ -119,8 +122,9 @@ internal class DPoPKeyStore(private val context: Context) {
119
122
  curve = "P-256",
120
123
  insideSecureHardware = keyInfo.isInsideSecureHardware,
121
124
  securityLevel = readSecurityLevel(keyInfo),
122
- strongBoxAvailable = isStrongBoxEnabled(),
123
- strongBoxBacked = readStrongBoxBacked(keyInfo)
125
+ securityLevelName = readSecurityLevelName(keyInfo, strongBoxBacked),
126
+ strongBoxAvailable = strongBoxAvailable,
127
+ strongBoxBacked = strongBoxBacked
124
128
  )
125
129
  }
126
130
 
@@ -172,6 +176,23 @@ internal class DPoPKeyStore(private val context: Context) {
172
176
  }
173
177
  }
174
178
 
179
+ private fun readSecurityLevelName(keyInfo: KeyInfo, strongBoxBacked: Boolean): String {
180
+ val securityLevel = readSecurityLevel(keyInfo)
181
+
182
+ return when (securityLevel) {
183
+ 1 -> "SOFTWARE"
184
+ 2 -> "TRUSTED_ENVIRONMENT"
185
+ 3 -> "STRONGBOX"
186
+ else -> {
187
+ when {
188
+ strongBoxBacked -> "STRONGBOX"
189
+ keyInfo.isInsideSecureHardware -> "TRUSTED_ENVIRONMENT"
190
+ else -> "SOFTWARE"
191
+ }
192
+ }
193
+ }
194
+ }
195
+
175
196
  private fun readStrongBoxBacked(keyInfo: KeyInfo): Boolean {
176
197
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
177
198
  return false
@@ -12,10 +12,49 @@ class DPoPModule(reactContext: ReactApplicationContext) :
12
12
  NativeReactNativeDPoPSpec(reactContext) {
13
13
  private val keyStore = DPoPKeyStore(reactContext)
14
14
 
15
+ companion object {
16
+ private const val DEFAULT_ALIAS = "react-native-dpop"
17
+ const val NAME = NativeReactNativeDPoPSpec.NAME
18
+ private const val UNKNOWN_STRONGBOX_FALLBACK_REASON = "UNKNOWN"
19
+ private val RESERVED_DPOP_CLAIMS = setOf("ath", "htm", "htu", "iat", "jti", "nonce")
20
+ }
21
+
15
22
  private fun resolveAlias(alias: String?): String {
16
23
  return alias ?: DEFAULT_ALIAS
17
24
  }
18
25
 
26
+ private fun ensureKeyPair(alias: String, requireHardwareBacked: Boolean): Unit {
27
+ var generatedInThisCall = false
28
+
29
+ if (!keyStore.hasKeyPair(alias)) {
30
+ keyStore.generateKeyPair(alias)
31
+ generatedInThisCall = true
32
+ }
33
+
34
+ if (requireHardwareBacked && !keyStore.isHardwareBacked(alias)) {
35
+ if (generatedInThisCall) {
36
+ keyStore.deleteKeyPair(alias)
37
+ }
38
+ throw IllegalStateException("Hardware-backed key required for alias: $alias")
39
+ }
40
+ }
41
+
42
+ private fun resolveStrongBoxFallbackReason(
43
+ strongBoxAvailable: Boolean,
44
+ strongBoxBacked: Boolean,
45
+ fallbackReason: String?
46
+ ): String? {
47
+ if (fallbackReason != null) {
48
+ return fallbackReason
49
+ }
50
+
51
+ return if (strongBoxAvailable && !strongBoxBacked) {
52
+ UNKNOWN_STRONGBOX_FALLBACK_REASON
53
+ } else {
54
+ null
55
+ }
56
+ }
57
+
19
58
  override fun assertHardwareBacked(alias: String?, promise: Promise) {
20
59
  try {
21
60
  val effectiveAlias = resolveAlias(alias)
@@ -35,27 +74,6 @@ class DPoPModule(reactContext: ReactApplicationContext) :
35
74
  }
36
75
  }
37
76
 
38
- override fun calculateThumbprint(alias: String?, promise: Promise) {
39
- try {
40
- val effectiveAlias = resolveAlias(alias)
41
- if (!keyStore.hasKeyPair(effectiveAlias)) {
42
- keyStore.generateKeyPair(effectiveAlias)
43
- }
44
-
45
- val keyPair = keyStore.getKeyPair(effectiveAlias)
46
- val coordinates = DPoPUtils.getPublicCoordinates(keyPair.publicKey)
47
- val thumbprint = DPoPUtils.calculateThumbprint(
48
- kty = "EC",
49
- crv = "P-256",
50
- x = coordinates.first,
51
- y = coordinates.second
52
- )
53
- promise.resolve(thumbprint)
54
- } catch (e: Exception) {
55
- promise.reject("ERR_DPOP_CALCULATE_THUMBPRINT", e.message, e)
56
- }
57
- }
58
-
59
77
  override fun deleteKeyPair(alias: String?, promise: Promise) {
60
78
  try {
61
79
  val effectiveAlias = resolveAlias(alias)
@@ -70,9 +88,14 @@ class DPoPModule(reactContext: ReactApplicationContext) :
70
88
  try {
71
89
  val effectiveAlias = resolveAlias(alias)
72
90
  if (!keyStore.hasKeyPair(effectiveAlias)) {
73
- val fallbackReason = keyStore.getStrongBoxFallbackReason(effectiveAlias)
91
+ val strongBoxAvailable = keyStore.isStrongBoxAvailable()
92
+ val fallbackReason = resolveStrongBoxFallbackReason(
93
+ strongBoxAvailable = strongBoxAvailable,
94
+ strongBoxBacked = false,
95
+ fallbackReason = keyStore.getStrongBoxFallbackReason(effectiveAlias)
96
+ )
74
97
  val hardwareAndroid = Arguments.createMap().apply {
75
- putBoolean("strongBoxAvailable", keyStore.isStrongBoxAvailable())
98
+ putBoolean("strongBoxAvailable", strongBoxAvailable)
76
99
  putBoolean("strongBoxBacked", false)
77
100
  if (fallbackReason != null) {
78
101
  putString("strongBoxFallbackReason", fallbackReason)
@@ -93,13 +116,18 @@ class DPoPModule(reactContext: ReactApplicationContext) :
93
116
  }
94
117
 
95
118
  val keyInfo = keyStore.getKeyInfo(effectiveAlias)
96
- val fallbackReason = keyStore.getStrongBoxFallbackReason(effectiveAlias)
119
+ val fallbackReason = resolveStrongBoxFallbackReason(
120
+ strongBoxAvailable = keyInfo.strongBoxAvailable,
121
+ strongBoxBacked = keyInfo.strongBoxBacked,
122
+ fallbackReason = keyStore.getStrongBoxFallbackReason(effectiveAlias)
123
+ )
97
124
  val hardwareAndroid = Arguments.createMap().apply {
98
125
  putBoolean("strongBoxAvailable", keyInfo.strongBoxAvailable)
99
126
  putBoolean("strongBoxBacked", keyInfo.strongBoxBacked)
100
127
  if (keyInfo.securityLevel != null) {
101
128
  putInt("securityLevel", keyInfo.securityLevel)
102
129
  }
130
+ putString("securityLevelName", keyInfo.securityLevelName)
103
131
  if (fallbackReason != null) {
104
132
  putString("strongBoxFallbackReason", fallbackReason)
105
133
  } else {
@@ -173,6 +201,27 @@ class DPoPModule(reactContext: ReactApplicationContext) :
173
201
  }
174
202
  }
175
203
 
204
+ override fun getPublicKeyThumbprint(alias: String?, promise: Promise) {
205
+ try {
206
+ val effectiveAlias = resolveAlias(alias)
207
+ if (!keyStore.hasKeyPair(effectiveAlias)) {
208
+ keyStore.generateKeyPair(effectiveAlias)
209
+ }
210
+
211
+ val keyPair = keyStore.getKeyPair(effectiveAlias)
212
+ val coordinates = DPoPUtils.getPublicCoordinates(keyPair.publicKey)
213
+ val thumbprint = DPoPUtils.getPublicKeyThumbprint(
214
+ kty = "EC",
215
+ crv = "P-256",
216
+ x = coordinates.first,
217
+ y = coordinates.second
218
+ )
219
+ promise.resolve(thumbprint)
220
+ } catch (e: Exception) {
221
+ promise.reject("ERR_DPOP_CALCULATE_THUMBPRINT", e.message, e)
222
+ }
223
+ }
224
+
176
225
  override fun hasKeyPair(alias: String?, promise: Promise) {
177
226
  try {
178
227
  val effectiveAlias = resolveAlias(alias)
@@ -206,7 +255,7 @@ class DPoPModule(reactContext: ReactApplicationContext) :
206
255
  }
207
256
  }
208
257
 
209
- override fun signWithDpopPrivateKey(payload: String, alias: String?, promise: Promise) {
258
+ override fun signWithDPoPPrivateKey(payload: String, alias: String?, promise: Promise) {
210
259
  try {
211
260
  val effectiveAlias = resolveAlias(alias)
212
261
  if (!keyStore.hasKeyPair(effectiveAlias)) {
@@ -235,13 +284,12 @@ class DPoPModule(reactContext: ReactApplicationContext) :
235
284
  jti: String?,
236
285
  iat: Double?,
237
286
  alias: String?,
287
+ requireHardwareBacked: Boolean,
238
288
  promise: Promise
239
289
  ) {
240
290
  try {
241
291
  val effectiveAlias = resolveAlias(alias)
242
- if (!keyStore.hasKeyPair(effectiveAlias)) {
243
- keyStore.generateKeyPair(effectiveAlias)
244
- }
292
+ ensureKeyPair(effectiveAlias, requireHardwareBacked)
245
293
 
246
294
  val keyPair = keyStore.getKeyPair(effectiveAlias)
247
295
  val coordinates = DPoPUtils.getPublicCoordinates(keyPair.publicKey)
@@ -281,6 +329,9 @@ class DPoPModule(reactContext: ReactApplicationContext) :
281
329
  val keys = additionalJson.keys()
282
330
  while (keys.hasNext()) {
283
331
  val key = keys.next()
332
+ if (RESERVED_DPOP_CLAIMS.contains(key)) {
333
+ throw IllegalArgumentException("additional must not override reserved DPoP claim: $key")
334
+ }
284
335
  payload.put(key, additionalJson.get(key))
285
336
  }
286
337
  }
@@ -320,9 +371,4 @@ class DPoPModule(reactContext: ReactApplicationContext) :
320
371
  promise.reject("ERR_DPOP_GENERATE_PROOF", e.message, e)
321
372
  }
322
373
  }
323
-
324
- companion object {
325
- private const val DEFAULT_ALIAS = "react-native-dpop"
326
- const val NAME = NativeReactNativeDPoPSpec.NAME
327
- }
328
374
  }
@@ -5,7 +5,6 @@ import com.facebook.react.bridge.NativeModule
5
5
  import com.facebook.react.bridge.ReactApplicationContext
6
6
  import com.facebook.react.module.model.ReactModuleInfo
7
7
  import com.facebook.react.module.model.ReactModuleInfoProvider
8
- import java.util.HashMap
9
8
 
10
9
  class DPoPPackage : BaseReactPackage() {
11
10
  override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
@@ -19,12 +18,13 @@ class DPoPPackage : BaseReactPackage() {
19
18
  override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
20
19
  mapOf(
21
20
  DPoPModule.NAME to ReactModuleInfo(
22
- name = DPoPModule.NAME,
23
- className = DPoPModule.NAME,
24
- canOverrideExistingModule = false,
25
- needsEagerInit = false,
26
- isCxxModule = false,
27
- isTurboModule = true
21
+ DPoPModule.NAME,
22
+ DPoPModule::class.java.name,
23
+ false,
24
+ false,
25
+ false,
26
+ false,
27
+ true
28
28
  )
29
29
  )
30
30
  }
@@ -18,12 +18,6 @@ internal object DPoPUtils {
18
18
  return Base64.getUrlEncoder().withoutPadding().encodeToString(input)
19
19
  }
20
20
 
21
- internal fun calculateThumbprint(kty: String, crv: String, x: String, y: String): String {
22
- val canonicalJwk = """{"crv":"$crv","kty":"$kty","x":"$x","y":"$y"}"""
23
- val hash = MessageDigest.getInstance("SHA-256").digest(canonicalJwk.toByteArray(Charsets.UTF_8))
24
- return base64UrlEncode(hash)
25
- }
26
-
27
21
  internal fun derToJose(derSignature: ByteArray, partLength: Int = 32): ByteArray {
28
22
  if (derSignature.isEmpty() || derSignature[0].toInt() != 0x30) {
29
23
  throw IllegalArgumentException("Invalid DER signature format")
@@ -65,6 +59,12 @@ internal object DPoPUtils {
65
59
  return Pair(x, y)
66
60
  }
67
61
 
62
+ internal fun getPublicKeyThumbprint(kty: String, crv: String, x: String, y: String): String {
63
+ val canonicalJwk = """{"crv":"$crv","kty":"$kty","x":"$x","y":"$y"}"""
64
+ val hash = MessageDigest.getInstance("SHA-256").digest(canonicalJwk.toByteArray(Charsets.UTF_8))
65
+ return base64UrlEncode(hash)
66
+ }
67
+
68
68
  internal fun hashAccessToken(accessToken: String): String {
69
69
  val hash = MessageDigest.getInstance("SHA-256").digest(accessToken.toByteArray(Charsets.UTF_8))
70
70
  return base64UrlEncode(hash)
@@ -19,6 +19,7 @@ final class DPoPKeyStore {
19
19
  private let keychain = KeychainKeyStore()
20
20
  private let fallbackReasonDefaults = UserDefaults.standard
21
21
  private let fallbackReasonPrefix = "react_native_dpop_secure_enclave_fallback_reason_"
22
+ private let unavailableFallbackReason = "UNAVAILABLE"
22
23
  private lazy var secureEnclaveAvailable = secureEnclave.isAvailable()
23
24
 
24
25
  func generateKeyPair(alias: String) throws {
@@ -92,7 +93,7 @@ final class DPoPKeyStore {
92
93
  }
93
94
 
94
95
  func isHardwareBacked(alias: String) -> Bool {
95
- secureEnclave.isHardwareBacked(alias: alias)
96
+ secureEnclaveAvailable && secureEnclave.isHardwareBacked(alias: alias)
96
97
  }
97
98
 
98
99
  func isSecureEnclaveAvailable() -> Bool {
@@ -100,7 +101,15 @@ final class DPoPKeyStore {
100
101
  }
101
102
 
102
103
  func getSecureEnclaveFallbackReason(alias: String) -> String? {
103
- fallbackReasonDefaults.string(forKey: fallbackReasonKey(alias: alias))
104
+ if !secureEnclaveAvailable && keychain.hasKeyPair(alias: alias) {
105
+ return unavailableFallbackReason
106
+ }
107
+
108
+ if let storedReason = fallbackReasonDefaults.string(forKey: fallbackReasonKey(alias: alias)) {
109
+ return storedReason
110
+ }
111
+
112
+ return nil
104
113
  }
105
114
 
106
115
  private func storeSecureEnclaveFallbackReason(alias: String, reason: String) {