react-native-dpop 0.2.0 → 0.4.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,13 +24,13 @@ 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';
@@ -38,26 +39,18 @@ 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',
42
43
  });
43
44
 
44
45
  const proof = dpop.proof;
45
46
  const thumbprint = await dpop.calculateThumbprint();
46
47
  const publicJwk = await dpop.getPublicKey('JWK');
47
- const isBound = await dpop.isBoundToAlias();
48
+ const keyInfo = await DPoP.getKeyInfo();
48
49
  ```
49
50
 
50
51
  ## API
51
52
 
52
- ### Types
53
-
54
- - `GenerateProofInput`
55
- - `DPoPProofContext`
56
- - `DPoPKeyInfo`
57
- - `PublicJwk`
58
- - `PublicKeyFormat = 'JWK' | 'DER' | 'RAW'`
59
-
60
- ### `DPoP` static methods
53
+ ### Static methods
61
54
 
62
55
  - `DPoP.generateProof(input): Promise<DPoP>`
63
56
  - `DPoP.assertHardwareBacked(alias?): Promise<void>`
@@ -66,22 +59,97 @@ const isBound = await dpop.isBoundToAlias();
66
59
  - `DPoP.hasKeyPair(alias?): Promise<boolean>`
67
60
  - `DPoP.rotateKeyPair(alias?): Promise<void>`
68
61
 
69
- ### `DPoP` instance fields
62
+ ### Instance members
70
63
 
71
64
  - `proof: string`
72
65
  - `proofContext: DPoPProofContext`
73
66
  - `alias?: string`
74
-
75
- ### `DPoP` instance methods
76
-
77
67
  - `calculateThumbprint(): Promise<string>`
78
68
  - `getPublicKey(format): Promise<PublicJwk | string>`
79
69
  - `signWithDpopPrivateKey(payload): Promise<string>`
80
70
  - `isBoundToAlias(alias?): Promise<boolean>`
81
71
 
82
- ## Error Codes
72
+ ### Main types
83
73
 
84
- Native errors are rejected with codes such as:
74
+ - `GenerateProofInput`
75
+ - `DPoPProofContext`
76
+ - `DPoPKeyInfo`
77
+ - `PublicJwk`
78
+ - `PublicKeyFormat = 'JWK' | 'DER' | 'RAW'`
79
+ - `SecureHardwareFallbackReason = 'UNAVAILABLE' | 'PROVIDER_ERROR' | 'POLICY_REJECTED' | 'UNKNOWN'`
80
+ - `AndroidSecurityLevelName = 'SOFTWARE' | 'TRUSTED_ENVIRONMENT' | 'STRONGBOX'`
81
+ - `IOSSecurityLevelName = 'SOFTWARE' | 'SECURE_ENCLAVE'`
82
+
83
+ ## `getKeyInfo()`
84
+
85
+ `getKeyInfo()` returns shared fields plus platform-specific hardware metadata.
86
+
87
+ ```ts
88
+ type DPoPKeyInfo = {
89
+ alias: string;
90
+ hasKeyPair: boolean;
91
+ algorithm?: string;
92
+ curve?: string;
93
+ insideSecureHardware?: boolean;
94
+ hardware?: {
95
+ android?: {
96
+ strongBoxAvailable: boolean;
97
+ strongBoxBacked: boolean;
98
+ securityLevel?: number;
99
+ securityLevelName?: 'SOFTWARE' | 'TRUSTED_ENVIRONMENT' | 'STRONGBOX';
100
+ strongBoxFallbackReason?: 'UNAVAILABLE' | 'PROVIDER_ERROR' | 'POLICY_REJECTED' | 'UNKNOWN' | null;
101
+ };
102
+ ios?: {
103
+ secureEnclaveAvailable: boolean;
104
+ secureEnclaveBacked: boolean;
105
+ securityLevel?: number | null;
106
+ securityLevelName?: 'SOFTWARE' | 'SECURE_ENCLAVE';
107
+ secureEnclaveFallbackReason?: 'UNAVAILABLE' | 'PROVIDER_ERROR' | 'POLICY_REJECTED' | 'UNKNOWN' | null;
108
+ };
109
+ };
110
+ };
111
+ ```
112
+
113
+ ### Security level semantics
114
+
115
+ - `securityLevel = 1`
116
+ Software-backed key material
117
+ - `securityLevel = 2`
118
+ Hardware-backed key material
119
+ On Android this usually means TEE
120
+ On iOS this means Secure Enclave
121
+ - `securityLevel = 3`
122
+ Android StrongBox-backed key
123
+ - `securityLevel = null`
124
+ No key material available, or the native platform did not report a numeric level
125
+
126
+ ### Fallback semantics
127
+
128
+ - On Android, the library tries StrongBox first when available
129
+ - On iOS, the library tries Secure Enclave first when available
130
+ - Fallback reasons are sanitized enums rather than raw native errors
131
+ - On iOS Simulator, `secureEnclaveFallbackReason` is expected to be `UNAVAILABLE`
132
+
133
+ ## Notes
134
+
135
+ - Default alias: `react-native-dpop`
136
+ - `htm` is normalized to uppercase
137
+ - `ath` is derived from `accessToken` when provided
138
+ - `jti` and `iat` are auto-generated when omitted
139
+ - 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`
140
+
141
+ ## Example apps
142
+
143
+ This repository includes two example apps:
144
+
145
+ - `examples/v0.75`
146
+ - `examples/v0.83`
147
+
148
+ The root `example` script points to `examples/v0.83`.
149
+
150
+ ## Errors
151
+
152
+ Native rejections use codes such as:
85
153
 
86
154
  - `ERR_DPOP_GENERATE_PROOF`
87
155
  - `ERR_DPOP_CALCULATE_THUMBPRINT`
@@ -94,13 +162,6 @@ Native errors are rejected with codes such as:
94
162
  - `ERR_DPOP_ASSERT_HARDWARE_BACKED`
95
163
  - `ERR_DPOP_IS_BOUND_TO_ALIAS`
96
164
 
97
- ## Notes
98
-
99
- - If no alias is provided, the default alias is `react-native-dpop`.
100
- - `htm` is normalized to uppercase in proof generation.
101
- - `ath` is derived from `accessToken` (`SHA-256`, base64url) when provided.
102
- - `jti` and `iat` are auto-generated when omitted.
103
-
104
165
  ## Contributing
105
166
 
106
167
  - [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
  )
@@ -36,6 +37,14 @@ internal class DPoPKeyStore(private val context: Context) {
36
37
  companion object {
37
38
  private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
38
39
  private const val EC_CURVE = "secp256r1"
40
+ private const val META_PREFS = "react_native_dpop_keystore_meta"
41
+ private const val META_STRONGBOX_FALLBACK_PREFIX = "strongbox_fallback_reason_"
42
+ private const val REASON_UNAVAILABLE = "UNAVAILABLE"
43
+ private const val REASON_PROVIDER_ERROR = "PROVIDER_ERROR"
44
+ }
45
+
46
+ private val metadataPrefs by lazy {
47
+ context.getSharedPreferences(META_PREFS, Context.MODE_PRIVATE)
39
48
  }
40
49
 
41
50
  private val keyStore: KeyStore by lazy {
@@ -46,6 +55,7 @@ internal class DPoPKeyStore(private val context: Context) {
46
55
  if (keyStore.containsAlias(alias)) {
47
56
  keyStore.deleteEntry(alias)
48
57
  }
58
+ clearStrongBoxFallbackReason(alias)
49
59
  }
50
60
 
51
61
  fun generateKeyPair(alias: String): Boolean {
@@ -56,16 +66,20 @@ internal class DPoPKeyStore(private val context: Context) {
56
66
  if (keyStore.containsAlias(alias)) {
57
67
  keyStore.deleteEntry(alias)
58
68
  }
69
+ clearStrongBoxFallbackReason(alias)
59
70
 
60
71
  val generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, KEYSTORE_PROVIDER)
61
72
  if (isStrongBoxEnabled()) {
62
73
  try {
63
74
  generator.initialize(buildSpec(alias, useStrongBox = true))
64
75
  generator.generateKeyPair()
76
+ clearStrongBoxFallbackReason(alias)
65
77
  return true
66
78
  } catch (_: StrongBoxUnavailableException) {
79
+ storeStrongBoxFallbackReason(alias, REASON_UNAVAILABLE)
67
80
  // Fallback to hardware-backed keystore when StrongBox is unavailable.
68
81
  } catch (_: ProviderException) {
82
+ storeStrongBoxFallbackReason(alias, REASON_PROVIDER_ERROR)
69
83
  // Some devices expose StrongBox but fail during generation.
70
84
  }
71
85
  }
@@ -89,6 +103,12 @@ internal class DPoPKeyStore(private val context: Context) {
89
103
  return privateKey != null && publicKey != null
90
104
  }
91
105
 
106
+ fun isStrongBoxAvailable(): Boolean = isStrongBoxEnabled()
107
+
108
+ fun getStrongBoxFallbackReason(alias: String): String? {
109
+ return metadataPrefs.getString(strongBoxFallbackKey(alias), null)
110
+ }
111
+
92
112
  fun getKeyInfo(alias: String): KeyStoreKeyInfo {
93
113
  val keyPair = getKeyPair(alias)
94
114
  val keyFactory = KeyFactory.getInstance(keyPair.privateKey.algorithm, KEYSTORE_PROVIDER)
@@ -100,6 +120,7 @@ internal class DPoPKeyStore(private val context: Context) {
100
120
  curve = "P-256",
101
121
  insideSecureHardware = keyInfo.isInsideSecureHardware,
102
122
  securityLevel = readSecurityLevel(keyInfo),
123
+ securityLevelName = readSecurityLevelName(keyInfo),
103
124
  strongBoxAvailable = isStrongBoxEnabled(),
104
125
  strongBoxBacked = readStrongBoxBacked(keyInfo)
105
126
  )
@@ -153,6 +174,24 @@ internal class DPoPKeyStore(private val context: Context) {
153
174
  }
154
175
  }
155
176
 
177
+ private fun readSecurityLevelName(keyInfo: KeyInfo): String {
178
+ val strongBoxBacked = readStrongBoxBacked(keyInfo)
179
+ val securityLevel = readSecurityLevel(keyInfo)
180
+
181
+ return when (securityLevel) {
182
+ 1 -> "SOFTWARE"
183
+ 2 -> "TRUSTED_ENVIRONMENT"
184
+ 3 -> "STRONGBOX"
185
+ else -> {
186
+ when {
187
+ strongBoxBacked -> "STRONGBOX"
188
+ keyInfo.isInsideSecureHardware -> "TRUSTED_ENVIRONMENT"
189
+ else -> "SOFTWARE"
190
+ }
191
+ }
192
+ }
193
+ }
194
+
156
195
  private fun readStrongBoxBacked(keyInfo: KeyInfo): Boolean {
157
196
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
158
197
  return false
@@ -165,4 +204,14 @@ internal class DPoPKeyStore(private val context: Context) {
165
204
  false
166
205
  }
167
206
  }
207
+
208
+ private fun storeStrongBoxFallbackReason(alias: String, reason: String) {
209
+ metadataPrefs.edit().putString(strongBoxFallbackKey(alias), reason).apply()
210
+ }
211
+
212
+ private fun clearStrongBoxFallbackReason(alias: String) {
213
+ metadataPrefs.edit().remove(strongBoxFallbackKey(alias)).apply()
214
+ }
215
+
216
+ private fun strongBoxFallbackKey(alias: String): String = "$META_STRONGBOX_FALLBACK_PREFIX$alias"
168
217
  }
@@ -12,10 +12,32 @@ 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
+ }
20
+
15
21
  private fun resolveAlias(alias: String?): String {
16
22
  return alias ?: DEFAULT_ALIAS
17
23
  }
18
24
 
25
+ private fun resolveStrongBoxFallbackReason(
26
+ strongBoxAvailable: Boolean,
27
+ strongBoxBacked: Boolean,
28
+ fallbackReason: String?
29
+ ): String? {
30
+ if (fallbackReason != null) {
31
+ return fallbackReason
32
+ }
33
+
34
+ return if (strongBoxAvailable && !strongBoxBacked) {
35
+ UNKNOWN_STRONGBOX_FALLBACK_REASON
36
+ } else {
37
+ null
38
+ }
39
+ }
40
+
19
41
  override fun assertHardwareBacked(alias: String?, promise: Promise) {
20
42
  try {
21
43
  val effectiveAlias = resolveAlias(alias)
@@ -70,26 +92,62 @@ class DPoPModule(reactContext: ReactApplicationContext) :
70
92
  try {
71
93
  val effectiveAlias = resolveAlias(alias)
72
94
  if (!keyStore.hasKeyPair(effectiveAlias)) {
95
+ val strongBoxAvailable = keyStore.isStrongBoxAvailable()
96
+ val fallbackReason = resolveStrongBoxFallbackReason(
97
+ strongBoxAvailable = strongBoxAvailable,
98
+ strongBoxBacked = false,
99
+ fallbackReason = keyStore.getStrongBoxFallbackReason(effectiveAlias)
100
+ )
101
+ val hardwareAndroid = Arguments.createMap().apply {
102
+ putBoolean("strongBoxAvailable", strongBoxAvailable)
103
+ putBoolean("strongBoxBacked", false)
104
+ if (fallbackReason != null) {
105
+ putString("strongBoxFallbackReason", fallbackReason)
106
+ } else {
107
+ putNull("strongBoxFallbackReason")
108
+ }
109
+ }
110
+ val hardware = Arguments.createMap().apply {
111
+ putMap("android", hardwareAndroid)
112
+ }
73
113
  val result = Arguments.createMap().apply {
74
114
  putString("alias", effectiveAlias)
75
115
  putBoolean("hasKeyPair", false)
116
+ putMap("hardware", hardware)
76
117
  }
77
118
  promise.resolve(result)
78
119
  return
79
120
  }
80
121
 
81
122
  val keyInfo = keyStore.getKeyInfo(effectiveAlias)
123
+ val fallbackReason = resolveStrongBoxFallbackReason(
124
+ strongBoxAvailable = keyInfo.strongBoxAvailable,
125
+ strongBoxBacked = keyInfo.strongBoxBacked,
126
+ fallbackReason = keyStore.getStrongBoxFallbackReason(effectiveAlias)
127
+ )
128
+ val hardwareAndroid = Arguments.createMap().apply {
129
+ putBoolean("strongBoxAvailable", keyInfo.strongBoxAvailable)
130
+ putBoolean("strongBoxBacked", keyInfo.strongBoxBacked)
131
+ if (keyInfo.securityLevel != null) {
132
+ putInt("securityLevel", keyInfo.securityLevel)
133
+ }
134
+ putString("securityLevelName", keyInfo.securityLevelName)
135
+ if (fallbackReason != null) {
136
+ putString("strongBoxFallbackReason", fallbackReason)
137
+ } else {
138
+ putNull("strongBoxFallbackReason")
139
+ }
140
+ }
141
+ val hardware = Arguments.createMap().apply {
142
+ putMap("android", hardwareAndroid)
143
+ }
82
144
  val result = Arguments.createMap().apply {
83
145
  putString("alias", keyInfo.alias)
84
146
  putString("algorithm", keyInfo.algorithm)
85
147
  putString("curve", keyInfo.curve)
86
148
  putBoolean("hasKeyPair", true)
87
149
  putBoolean("insideSecureHardware", keyInfo.insideSecureHardware)
88
- putBoolean("strongBoxAvailable", keyInfo.strongBoxAvailable)
89
- putBoolean("strongBoxBacked", keyInfo.strongBoxBacked)
90
- if (keyInfo.securityLevel != null) {
91
- putInt("securityLevel", keyInfo.securityLevel)
92
- }
150
+ putMap("hardware", hardware)
93
151
  }
94
152
  promise.resolve(result)
95
153
  } catch (e: Exception) {
@@ -294,9 +352,4 @@ class DPoPModule(reactContext: ReactApplicationContext) :
294
352
  promise.reject("ERR_DPOP_GENERATE_PROOF", e.message, e)
295
353
  }
296
354
  }
297
-
298
- companion object {
299
- private const val DEFAULT_ALIAS = "react-native-dpop"
300
- const val NAME = NativeReactNativeDPoPSpec.NAME
301
- }
302
355
  }
@@ -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
  }
@@ -12,20 +12,24 @@ struct DPoPKeyInfo {
12
12
  let curve: String
13
13
  let hasKeyPair: Bool
14
14
  let insideSecureHardware: Bool
15
- let strongBoxAvailable: Bool
16
- let strongBoxBacked: Bool
17
15
  }
18
16
 
19
17
  final class DPoPKeyStore {
20
18
  private let secureEnclave = SecureEnclaveKeyStore()
21
19
  private let keychain = KeychainKeyStore()
20
+ private let fallbackReasonDefaults = UserDefaults.standard
21
+ private let fallbackReasonPrefix = "react_native_dpop_secure_enclave_fallback_reason_"
22
+ private let unavailableFallbackReason = "UNAVAILABLE"
23
+ private lazy var secureEnclaveAvailable = secureEnclave.isAvailable()
22
24
 
23
25
  func generateKeyPair(alias: String) throws {
24
26
  try deleteKeyPair(alias: alias)
25
27
 
26
28
  do {
27
29
  try secureEnclave.generateKeyPair(alias: alias)
30
+ clearSecureEnclaveFallbackReason(alias: alias)
28
31
  } catch {
32
+ storeSecureEnclaveFallbackReason(alias: alias, reason: mapSecureEnclaveFallbackReason(error))
29
33
  try keychain.generateKeyPair(alias: alias)
30
34
  }
31
35
  }
@@ -33,6 +37,7 @@ final class DPoPKeyStore {
33
37
  func deleteKeyPair(alias: String) throws {
34
38
  try secureEnclave.deleteKeyPair(alias: alias)
35
39
  try keychain.deleteKeyPair(alias: alias)
40
+ clearSecureEnclaveFallbackReason(alias: alias)
36
41
  }
37
42
 
38
43
  func hasKeyPair(alias: String) -> Bool {
@@ -64,9 +69,7 @@ final class DPoPKeyStore {
64
69
  algorithm: "EC",
65
70
  curve: "P-256",
66
71
  hasKeyPair: true,
67
- insideSecureHardware: true,
68
- strongBoxAvailable: false,
69
- strongBoxBacked: false
72
+ insideSecureHardware: true
70
73
  )
71
74
  }
72
75
 
@@ -76,9 +79,7 @@ final class DPoPKeyStore {
76
79
  algorithm: "EC",
77
80
  curve: "P-256",
78
81
  hasKeyPair: true,
79
- insideSecureHardware: false,
80
- strongBoxAvailable: false,
81
- strongBoxBacked: false
82
+ insideSecureHardware: false
82
83
  )
83
84
  }
84
85
 
@@ -87,13 +88,56 @@ final class DPoPKeyStore {
87
88
  algorithm: "EC",
88
89
  curve: "P-256",
89
90
  hasKeyPair: false,
90
- insideSecureHardware: false,
91
- strongBoxAvailable: false,
92
- strongBoxBacked: false
91
+ insideSecureHardware: false
93
92
  )
94
93
  }
95
94
 
96
95
  func isHardwareBacked(alias: String) -> Bool {
97
96
  secureEnclave.isHardwareBacked(alias: alias)
98
97
  }
98
+
99
+ func isSecureEnclaveAvailable() -> Bool {
100
+ secureEnclaveAvailable
101
+ }
102
+
103
+ func getSecureEnclaveFallbackReason(alias: String) -> String? {
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
113
+ }
114
+
115
+ private func storeSecureEnclaveFallbackReason(alias: String, reason: String) {
116
+ fallbackReasonDefaults.set(reason, forKey: fallbackReasonKey(alias: alias))
117
+ }
118
+
119
+ private func clearSecureEnclaveFallbackReason(alias: String) {
120
+ fallbackReasonDefaults.removeObject(forKey: fallbackReasonKey(alias: alias))
121
+ }
122
+
123
+ private func fallbackReasonKey(alias: String) -> String {
124
+ "\(fallbackReasonPrefix)\(alias)"
125
+ }
126
+
127
+ private func mapSecureEnclaveFallbackReason(_ error: Error) -> String {
128
+ let nsError = error as NSError
129
+
130
+ if nsError.domain == NSOSStatusErrorDomain {
131
+ switch nsError.code {
132
+ case Int(errSecNotAvailable), Int(errSecUnimplemented):
133
+ return "UNAVAILABLE"
134
+ case Int(errSecAuthFailed), Int(errSecInteractionNotAllowed), Int(errSecUserCanceled):
135
+ return "POLICY_REJECTED"
136
+ default:
137
+ return "PROVIDER_ERROR"
138
+ }
139
+ }
140
+
141
+ return "UNKNOWN"
142
+ }
99
143
  }
@@ -7,6 +7,8 @@ final class DPoPModule {
7
7
 
8
8
  private let keyStore = DPoPKeyStore()
9
9
  private let defaultAlias = "react-native-dpop"
10
+ private let unknownSecureEnclaveFallbackReason = "UNKNOWN"
11
+ private let unavailableSecureEnclaveFallbackReason = "UNAVAILABLE"
10
12
 
11
13
  private init() {}
12
14
 
@@ -44,26 +46,75 @@ final class DPoPModule {
44
46
 
45
47
  func getKeyInfo(alias: String?) -> [String: Any] {
46
48
  let effectiveAlias = resolveAlias(alias)
49
+ let secureEnclaveAvailable = keyStore.isSecureEnclaveAvailable()
47
50
  let keyInfo = keyStore.getKeyInfo(alias: effectiveAlias)
51
+ let secureEnclaveBacked = secureEnclaveAvailable && keyInfo.insideSecureHardware
52
+ let fallbackReason = resolveSecureEnclaveFallbackReason(
53
+ secureEnclaveAvailable: secureEnclaveAvailable,
54
+ secureEnclaveBacked: secureEnclaveBacked,
55
+ hasKeyPair: keyInfo.hasKeyPair,
56
+ fallbackReason: keyStore.getSecureEnclaveFallbackReason(alias: effectiveAlias)
57
+ )
58
+ let secureEnclaveFallbackReason: Any = fallbackReason ?? NSNull()
48
59
 
49
60
  if !keyInfo.hasKeyPair {
50
61
  return [
51
62
  "alias": effectiveAlias,
52
- "hasKeyPair": false
63
+ "hasKeyPair": false,
64
+ "hardware": [
65
+ "ios": [
66
+ "secureEnclaveAvailable": secureEnclaveAvailable,
67
+ "secureEnclaveBacked": false,
68
+ "securityLevel": NSNull(),
69
+ "securityLevelName": "SOFTWARE",
70
+ "secureEnclaveFallbackReason": secureEnclaveFallbackReason
71
+ ]
72
+ ]
53
73
  ]
54
74
  }
55
75
 
76
+ let securityLevel = secureEnclaveBacked ? 2 : 1
77
+ let securityLevelName = secureEnclaveBacked ? "SECURE_ENCLAVE" : "SOFTWARE"
78
+
56
79
  return [
57
80
  "alias": keyInfo.alias,
58
81
  "algorithm": keyInfo.algorithm,
59
82
  "curve": keyInfo.curve,
60
83
  "hasKeyPair": true,
61
- "insideSecureHardware": keyInfo.insideSecureHardware,
62
- "strongBoxAvailable": keyInfo.strongBoxAvailable,
63
- "strongBoxBacked": keyInfo.strongBoxBacked
84
+ "insideSecureHardware": secureEnclaveBacked,
85
+ "hardware": [
86
+ "ios": [
87
+ "secureEnclaveAvailable": secureEnclaveAvailable,
88
+ "secureEnclaveBacked": secureEnclaveBacked,
89
+ "securityLevel": securityLevel,
90
+ "securityLevelName": securityLevelName,
91
+ "secureEnclaveFallbackReason": secureEnclaveFallbackReason
92
+ ]
93
+ ]
64
94
  ]
65
95
  }
66
96
 
97
+ private func resolveSecureEnclaveFallbackReason(
98
+ secureEnclaveAvailable: Bool,
99
+ secureEnclaveBacked: Bool,
100
+ hasKeyPair: Bool,
101
+ fallbackReason: String?
102
+ ) -> String? {
103
+ if let fallbackReason {
104
+ return fallbackReason
105
+ }
106
+
107
+ if hasKeyPair && !secureEnclaveAvailable {
108
+ return unavailableSecureEnclaveFallbackReason
109
+ }
110
+
111
+ if hasKeyPair && !secureEnclaveBacked {
112
+ return unknownSecureEnclaveFallbackReason
113
+ }
114
+
115
+ return nil
116
+ }
117
+
67
118
  func getPublicKeyDer(alias: String?) throws -> String {
68
119
  let effectiveAlias = resolveAlias(alias)
69
120
  if !keyStore.hasKeyPair(alias: effectiveAlias) {
@@ -327,12 +378,19 @@ final class DPoPModule {
327
378
  additional: [String: Any]?,
328
379
  kid: String?,
329
380
  jti: String?,
330
- iat: NSNumber?,
381
+ iat: Any?,
331
382
  alias: String?,
332
383
  resolve: @escaping RCTPromiseResolveBlock,
333
384
  reject: @escaping RCTPromiseRejectBlock
334
385
  ) {
335
386
  do {
387
+ let normalizedIat: NSNumber?
388
+ if iat is NSNull {
389
+ normalizedIat = nil
390
+ } else {
391
+ normalizedIat = iat as? NSNumber
392
+ }
393
+
336
394
  resolve(
337
395
  try DPoPModule.shared.generateProof(
338
396
  htu: htu,
@@ -342,7 +400,7 @@ final class DPoPModule {
342
400
  additional: additional,
343
401
  kid: kid,
344
402
  jti: jti,
345
- iat: iat,
403
+ iat: normalizedIat,
346
404
  alias: alias
347
405
  )
348
406
  )
@@ -70,7 +70,7 @@ RCT_EXTERN_METHOD(generateProof:(NSString *)htu
70
70
  additional:(NSDictionary * _Nullable)additional
71
71
  kid:(NSString * _Nullable)kid
72
72
  jti:(NSString * _Nullable)jti
73
- iat:(NSNumber * _Nullable)iat
73
+ iat:(id _Nullable)iat
74
74
  alias:(NSString * _Nullable)alias
75
75
  resolve:(RCTPromiseResolveBlock)resolve
76
76
  reject:(RCTPromiseRejectBlock)reject)
@@ -4,6 +4,21 @@ import Security
4
4
  final class SecureEnclaveKeyStore {
5
5
  private let service = "com.dpop.secureenclave"
6
6
 
7
+ func isAvailable() -> Bool {
8
+ #if targetEnvironment(simulator)
9
+ return false
10
+ #else
11
+ let probeAlias = "__secure_enclave_probe_\(UUID().uuidString)"
12
+ do {
13
+ try generateKeyPair(alias: probeAlias)
14
+ try deleteKeyPair(alias: probeAlias)
15
+ return true
16
+ } catch {
17
+ return false
18
+ }
19
+ #endif
20
+ }
21
+
7
22
  func generateKeyPair(alias: String) throws {
8
23
  try deleteKeyPair(alias: alias)
9
24
 
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  import { NativeModules, TurboModuleRegistry } from 'react-native';
4
- const nativeDpopModule = TurboModuleRegistry.get('ReactNativeDPoP') ?? NativeModules.ReactNativeDPoP;
4
+ const nativeDpopModule =
5
+ // eslint-disable-next-line dot-notation -- required by noPropertyAccessFromIndexSignature from @tsconfig/strictest
6
+ TurboModuleRegistry.get('ReactNativeDPoP') ?? NativeModules['ReactNativeDPoP'];
5
7
  export default nativeDpopModule;
6
8
  //# sourceMappingURL=NativeReactNativeDPoP.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["NativeModules","TurboModuleRegistry","nativeDpopModule","get","ReactNativeDPoP"],"sourceRoot":"../../src","sources":["NativeReactNativeDPoP.ts"],"mappings":";;AACA,SAASA,aAAa,EAAEC,mBAAmB,QAAQ,cAAc;AA4BjE,MAAMC,gBAAgB,GACpBD,mBAAmB,CAACE,GAAG,CAAO,iBAAiB,CAAC,IAAKH,aAAa,CAACI,eAAoC;AAEzG,eAAeF,gBAAgB","ignoreList":[]}
1
+ {"version":3,"names":["NativeModules","TurboModuleRegistry","nativeDpopModule","get"],"sourceRoot":"../../src","sources":["NativeReactNativeDPoP.ts"],"mappings":";;AACA,SAASA,aAAa,EAAEC,mBAAmB,QAAQ,cAAc;AA4BjE,MAAMC,gBAAgB;AACpB;AACAD,mBAAmB,CAACE,GAAG,CAAO,iBAAiB,CAAC,IAAKH,aAAa,CAAC,iBAAiB,CAAsB;AAE5G,eAAeE,gBAAgB","ignoreList":[]}
@@ -26,7 +26,9 @@ export class DPoP {
26
26
  return NativeReactNativeDPoP.isBoundToAlias(this.proof, alias ?? this.alias ?? null);
27
27
  }
28
28
  static async generateProof(input) {
29
- const result = await NativeReactNativeDPoP.generateProof(input.htu, input.htm, input.nonce ?? null, input.accessToken ?? null, input.additional ?? null, input.kid ?? null, input.jti ?? null, input.iat ?? null, input.alias ?? null);
29
+ const result = await NativeReactNativeDPoP.generateProof(input.htu, input.htm, input.nonce ?? null, input.accessToken ?? null, input.additional ?? null, input.kid ?? null, input.jti ?? null,
30
+ // RN 0.75 Android bridge can crash when a nullable Double arrives as null.
31
+ input.iat ?? Math.floor(Date.now() / 1000), input.alias ?? null);
30
32
  return new DPoP(result.proof, result.proofContext, input.alias);
31
33
  }
32
34
  static async assertHardwareBacked(alias) {
@@ -1 +1 @@
1
- {"version":3,"names":["NativeReactNativeDPoP","DPoP","constructor","proof","proofContext","alias","calculateThumbprint","getPublicKey","format","getPublicKeyDer","getPublicKeyRaw","getPublicKeyJwk","signWithDpopPrivateKey","payload","isBoundToAlias","generateProof","input","result","htu","htm","nonce","accessToken","additional","kid","jti","iat","assertHardwareBacked","deleteKeyPair","getKeyInfo","hasKeyPair","rotateKeyPair"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,OAAOA,qBAAqB,MAAM,4BAAyB;AAoD3D,OAAO,MAAMC,IAAI,CAAC;EAKRC,WAAWA,CAACC,KAAa,EAAEC,YAA8B,EAAEC,KAAc,EAAE;IACjF,IAAI,CAACF,KAAK,GAAGA,KAAK;IAClB,IAAI,CAACC,YAAY,GAAGA,YAAY;IAChC,IAAI,CAACC,KAAK,GAAGA,KAAK;EACpB;EAEA,MAAaC,mBAAmBA,CAAA,EAAoB;IAClD,OAAON,qBAAqB,CAACM,mBAAmB,CAAC,IAAI,CAACD,KAAK,IAAI,IAAI,CAAC;EACtE;EAEA,MAAaE,YAAYA,CAACC,MAAuB,EAA+B;IAC9E,IAAIA,MAAM,KAAK,KAAK,EAAE;MACpB,OAAOR,qBAAqB,CAACS,eAAe,CAAC,IAAI,CAACJ,KAAK,IAAI,IAAI,CAAC;IAClE;IACA,IAAIG,MAAM,KAAK,KAAK,EAAE;MACpB,OAAOR,qBAAqB,CAACU,eAAe,CAAC,IAAI,CAACL,KAAK,IAAI,IAAI,CAAC;IAClE;IAEA,OAAOL,qBAAqB,CAACW,eAAe,CAAC,IAAI,CAACN,KAAK,IAAI,IAAI,CAAC;EAClE;EAEA,MAAaO,sBAAsBA,CAACC,OAAe,EAAmB;IACpE,OAAOb,qBAAqB,CAACY,sBAAsB,CAACC,OAAO,EAAE,IAAI,CAACR,KAAK,IAAI,IAAI,CAAC;EAClF;EAEA,MAAaS,cAAcA,CAACT,KAAc,EAAoB;IAC5D,OAAOL,qBAAqB,CAACc,cAAc,CAAC,IAAI,CAACX,KAAK,EAAEE,KAAK,IAAI,IAAI,CAACA,KAAK,IAAI,IAAI,CAAC;EACtF;EAEA,aAAoBU,aAAaA,CAACC,KAAyB,EAAiB;IAC1E,MAAMC,MAAM,GAAI,MAAMjB,qBAAqB,CAACe,aAAa,CACvDC,KAAK,CAACE,GAAG,EACTF,KAAK,CAACG,GAAG,EACTH,KAAK,CAACI,KAAK,IAAI,IAAI,EACnBJ,KAAK,CAACK,WAAW,IAAI,IAAI,EACzBL,KAAK,CAACM,UAAU,IAAI,IAAI,EACxBN,KAAK,CAACO,GAAG,IAAI,IAAI,EACjBP,KAAK,CAACQ,GAAG,IAAI,IAAI,EACjBR,KAAK,CAACS,GAAG,IAAI,IAAI,EACjBT,KAAK,CAACX,KAAK,IAAI,IACjB,CAAyB;IAEzB,OAAO,IAAIJ,IAAI,CAACgB,MAAM,CAACd,KAAK,EAAEc,MAAM,CAACb,YAAY,EAAEY,KAAK,CAACX,KAAK,CAAC;EACjE;EAEA,aAAoBqB,oBAAoBA,CAACrB,KAAc,EAAiB;IACtE,MAAML,qBAAqB,CAAC0B,oBAAoB,CAACrB,KAAK,IAAI,IAAI,CAAC;EACjE;EAEA,aAAoBsB,aAAaA,CAACtB,KAAc,EAAiB;IAC/D,MAAML,qBAAqB,CAAC2B,aAAa,CAACtB,KAAK,IAAI,IAAI,CAAC;EAC1D;EAEA,aAAoBuB,UAAUA,CAACvB,KAAc,EAAwB;IACnE,OAAOL,qBAAqB,CAAC4B,UAAU,CAACvB,KAAK,IAAI,IAAI,CAAC;EACxD;EAEA,aAAoBwB,UAAUA,CAACxB,KAAc,EAAoB;IAC/D,OAAOL,qBAAqB,CAAC6B,UAAU,CAACxB,KAAK,IAAI,IAAI,CAAC;EACxD;EAEA,aAAoByB,aAAaA,CAACzB,KAAc,EAAiB;IAC/D,MAAML,qBAAqB,CAAC8B,aAAa,CAACzB,KAAK,IAAI,IAAI,CAAC;EAC1D;AACF","ignoreList":[]}
1
+ {"version":3,"names":["NativeReactNativeDPoP","DPoP","constructor","proof","proofContext","alias","calculateThumbprint","getPublicKey","format","getPublicKeyDer","getPublicKeyRaw","getPublicKeyJwk","signWithDpopPrivateKey","payload","isBoundToAlias","generateProof","input","result","htu","htm","nonce","accessToken","additional","kid","jti","iat","Math","floor","Date","now","assertHardwareBacked","deleteKeyPair","getKeyInfo","hasKeyPair","rotateKeyPair"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,OAAOA,qBAAqB,MAAM,4BAAyB;AAqE3D,OAAO,MAAMC,IAAI,CAAC;EAKRC,WAAWA,CAACC,KAAa,EAAEC,YAA8B,EAAEC,KAAc,EAAE;IACjF,IAAI,CAACF,KAAK,GAAGA,KAAK;IAClB,IAAI,CAACC,YAAY,GAAGA,YAAY;IAChC,IAAI,CAACC,KAAK,GAAGA,KAAK;EACpB;EAEA,MAAaC,mBAAmBA,CAAA,EAAoB;IAClD,OAAON,qBAAqB,CAACM,mBAAmB,CAAC,IAAI,CAACD,KAAK,IAAI,IAAI,CAAC;EACtE;EAEA,MAAaE,YAAYA,CAACC,MAAuB,EAA+B;IAC9E,IAAIA,MAAM,KAAK,KAAK,EAAE;MACpB,OAAOR,qBAAqB,CAACS,eAAe,CAAC,IAAI,CAACJ,KAAK,IAAI,IAAI,CAAC;IAClE;IACA,IAAIG,MAAM,KAAK,KAAK,EAAE;MACpB,OAAOR,qBAAqB,CAACU,eAAe,CAAC,IAAI,CAACL,KAAK,IAAI,IAAI,CAAC;IAClE;IAEA,OAAOL,qBAAqB,CAACW,eAAe,CAAC,IAAI,CAACN,KAAK,IAAI,IAAI,CAAC;EAClE;EAEA,MAAaO,sBAAsBA,CAACC,OAAe,EAAmB;IACpE,OAAOb,qBAAqB,CAACY,sBAAsB,CAACC,OAAO,EAAE,IAAI,CAACR,KAAK,IAAI,IAAI,CAAC;EAClF;EAEA,MAAaS,cAAcA,CAACT,KAAc,EAAoB;IAC5D,OAAOL,qBAAqB,CAACc,cAAc,CAAC,IAAI,CAACX,KAAK,EAAEE,KAAK,IAAI,IAAI,CAACA,KAAK,IAAI,IAAI,CAAC;EACtF;EAEA,aAAoBU,aAAaA,CAACC,KAAyB,EAAiB;IAC1E,MAAMC,MAAM,GAAI,MAAMjB,qBAAqB,CAACe,aAAa,CACvDC,KAAK,CAACE,GAAG,EACTF,KAAK,CAACG,GAAG,EACTH,KAAK,CAACI,KAAK,IAAI,IAAI,EACnBJ,KAAK,CAACK,WAAW,IAAI,IAAI,EACzBL,KAAK,CAACM,UAAU,IAAI,IAAI,EACxBN,KAAK,CAACO,GAAG,IAAI,IAAI,EACjBP,KAAK,CAACQ,GAAG,IAAI,IAAI;IACjB;IACAR,KAAK,CAACS,GAAG,IAAIC,IAAI,CAACC,KAAK,CAACC,IAAI,CAACC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,EAC1Cb,KAAK,CAACX,KAAK,IAAI,IACjB,CAAyB;IAEzB,OAAO,IAAIJ,IAAI,CAACgB,MAAM,CAACd,KAAK,EAAEc,MAAM,CAACb,YAAY,EAAEY,KAAK,CAACX,KAAK,CAAC;EACjE;EAEA,aAAoByB,oBAAoBA,CAACzB,KAAc,EAAiB;IACtE,MAAML,qBAAqB,CAAC8B,oBAAoB,CAACzB,KAAK,IAAI,IAAI,CAAC;EACjE;EAEA,aAAoB0B,aAAaA,CAAC1B,KAAc,EAAiB;IAC/D,MAAML,qBAAqB,CAAC+B,aAAa,CAAC1B,KAAK,IAAI,IAAI,CAAC;EAC1D;EAEA,aAAoB2B,UAAUA,CAAC3B,KAAc,EAAwB;IACnE,OAAOL,qBAAqB,CAACgC,UAAU,CAAC3B,KAAK,IAAI,IAAI,CAAC;EACxD;EAEA,aAAoB4B,UAAUA,CAAC5B,KAAc,EAAoB;IAC/D,OAAOL,qBAAqB,CAACiC,UAAU,CAAC5B,KAAK,IAAI,IAAI,CAAC;EACxD;EAEA,aAAoB6B,aAAaA,CAAC7B,KAAc,EAAiB;IAC/D,MAAML,qBAAqB,CAACkC,aAAa,CAAC7B,KAAK,IAAI,IAAI,CAAC;EAC1D;AACF","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ export default function DPoPExampleContent(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=DPoPExampleContent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DPoPExampleContent.d.ts","sourceRoot":"","sources":["../../../../examples/shared/DPoPExampleContent.tsx"],"names":[],"mappings":"AAIA,MAAM,CAAC,OAAO,UAAU,kBAAkB,4CA0BzC"}
@@ -0,0 +1,2 @@
1
+ export default function App(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=App.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../../examples/v0.75/App.tsx"],"names":[],"mappings":"AAIA,MAAM,CAAC,OAAO,UAAU,GAAG,4CAM1B"}
@@ -0,0 +1,2 @@
1
+ export default function App(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=App.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../../examples/v0.83/App.tsx"],"names":[],"mappings":"AAKA,MAAM,CAAC,OAAO,UAAU,GAAG,4CAM1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"NativeReactNativeDPoP.d.ts","sourceRoot":"","sources":["../../../src/NativeReactNativeDPoP.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAE9E,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACxD,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC7D,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtE,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/E,aAAa,CACX,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,UAAU,EAAE,YAAY,GAAG,IAAI,EAC/B,GAAG,EAAE,MAAM,GAAG,IAAI,EAClB,GAAG,EAAE,MAAM,GAAG,IAAI,EAClB,GAAG,EAAE,MAAM,GAAG,IAAI,EAClB,KAAK,EAAE,MAAM,GAAG,IAAI,GACnB,OAAO,CAAC,YAAY,CAAC,CAAC;CAC1B;wBAKkC,IAAI;AAAvC,wBAAwC"}
1
+ {"version":3,"file":"NativeReactNativeDPoP.d.ts","sourceRoot":"","sources":["../../../src/NativeReactNativeDPoP.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAE9E,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACxD,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC7D,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtE,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/E,aAAa,CACX,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,UAAU,EAAE,YAAY,GAAG,IAAI,EAC/B,GAAG,EAAE,MAAM,GAAG,IAAI,EAClB,GAAG,EAAE,MAAM,GAAG,IAAI,EAClB,GAAG,EAAE,MAAM,GAAG,IAAI,EAClB,KAAK,EAAE,MAAM,GAAG,IAAI,GACnB,OAAO,CAAC,YAAY,CAAC,CAAC;CAC1B;wBAMkC,IAAI;AAAvC,wBAAwC"}
@@ -6,15 +6,31 @@ export type PublicJwk = {
6
6
  y: string;
7
7
  };
8
8
  export type PublicKeyFormat = 'JWK' | 'DER' | 'RAW';
9
+ export type SecureHardwareFallbackReason = 'UNAVAILABLE' | 'PROVIDER_ERROR' | 'POLICY_REJECTED' | 'UNKNOWN';
10
+ export type AndroidSecurityLevelName = 'SOFTWARE' | 'TRUSTED_ENVIRONMENT' | 'STRONGBOX';
11
+ export type IOSSecurityLevelName = 'SOFTWARE' | 'SECURE_ENCLAVE';
9
12
  export type DPoPKeyInfo = {
10
13
  alias: string;
11
14
  hasKeyPair: boolean;
12
15
  algorithm?: string;
13
16
  curve?: string;
14
17
  insideSecureHardware?: boolean;
15
- securityLevel?: number;
16
- strongBoxAvailable?: boolean;
17
- strongBoxBacked?: boolean;
18
+ hardware?: {
19
+ android?: {
20
+ strongBoxAvailable: boolean;
21
+ strongBoxBacked: boolean;
22
+ securityLevel?: number;
23
+ securityLevelName?: AndroidSecurityLevelName;
24
+ strongBoxFallbackReason?: SecureHardwareFallbackReason | null;
25
+ };
26
+ ios?: {
27
+ secureEnclaveAvailable: boolean;
28
+ secureEnclaveBacked: boolean;
29
+ securityLevel?: number | null;
30
+ securityLevelName?: IOSSecurityLevelName;
31
+ secureEnclaveFallbackReason?: SecureHardwareFallbackReason | null;
32
+ };
33
+ };
18
34
  };
19
35
  export type GenerateProofInput = {
20
36
  htu: string;
@@ -39,7 +55,7 @@ export type DPoPProofContext = {
39
55
  };
40
56
  export declare class DPoP {
41
57
  readonly proof: string;
42
- readonly alias?: string;
58
+ readonly alias: string | undefined;
43
59
  readonly proofContext: DPoPProofContext;
44
60
  private constructor();
45
61
  calculateThumbprint(): Promise<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAEA,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEhD,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,EAAE,IAAI,CAAC;IACV,GAAG,EAAE,OAAO,CAAC;IACb,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAEpD,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACpC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAOF,qBAAa,IAAI;IACf,SAAgB,KAAK,EAAE,MAAM,CAAC;IAC9B,SAAgB,KAAK,CAAC,EAAE,MAAM,CAAC;IAC/B,SAAgB,YAAY,EAAE,gBAAgB,CAAC;IAE/C,OAAO;IAMM,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC;IAItC,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC;IAWlE,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIxD,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;WAIzC,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;WAgBvD,oBAAoB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;WAInD,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;WAI5C,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;WAIhD,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;WAI5C,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGjE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-dpop",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "React Native library for DPoP proof generation and key management.",
5
5
  "keywords": [
6
6
  "android",
@@ -18,6 +18,7 @@
18
18
  "license": "MIT",
19
19
  "author": "Pedro Cirilo <phscirilo123@gmail.com> (https://github.com/Cirilord)",
20
20
  "main": "./lib/module/index.js",
21
+ "react-native": "./src/index.tsx",
21
22
  "types": "./lib/typescript/src/index.d.ts",
22
23
  "exports": {
23
24
  ".": {
@@ -47,11 +48,12 @@
47
48
  "!**/.*"
48
49
  ],
49
50
  "scripts": {
50
- "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
51
- "example": "yarn workspace react-native-dpop-example",
51
+ "clean": "del-cli android/build examples/v0.83/android/build examples/v0.83/android/app/build examples/v0.83/ios/build lib",
52
+ "example": "yarn workspace react-native-dpop-example-v0.83",
52
53
  "lint": "eslint \"**/*.{js,ts,tsx}\"",
53
54
  "prepare": "bob build",
54
- "release": "release-it --only-version",
55
+ "release": "release-it",
56
+ "release:version": "release-it --only-version",
55
57
  "test": "jest",
56
58
  "typecheck": "tsc"
57
59
  },
@@ -70,12 +72,14 @@
70
72
  "@react-native/babel-preset": "0.83.0",
71
73
  "@react-native/eslint-config": "0.83.0",
72
74
  "@release-it/conventional-changelog": "^10.0.1",
75
+ "@tsconfig/strictest": "^2.0.8",
73
76
  "@types/jest": "^29.5.14",
74
77
  "@types/react": "^19.2.0",
75
78
  "commitlint": "^19.8.1",
76
79
  "del-cli": "^6.0.0",
77
80
  "eslint": "^9.35.0",
78
81
  "eslint-config-prettier": "^10.1.8",
82
+ "eslint-plugin-import": "^2.32.0",
79
83
  "eslint-plugin-prettier": "^5.5.4",
80
84
  "jest": "^29.7.0",
81
85
  "lefthook": "^2.0.3",
@@ -88,7 +92,8 @@
88
92
  "typescript": "^5.9.2"
89
93
  },
90
94
  "workspaces": [
91
- "example"
95
+ "examples/v0.75",
96
+ "examples/v0.83"
92
97
  ],
93
98
  "packageManager": "yarn@4.11.0",
94
99
  "react-native-builder-bob": {
@@ -28,6 +28,7 @@ export interface Spec extends TurboModule {
28
28
  }
29
29
 
30
30
  const nativeDpopModule =
31
- TurboModuleRegistry.get<Spec>('ReactNativeDPoP') ?? (NativeModules.ReactNativeDPoP as Spec | undefined);
31
+ // eslint-disable-next-line dot-notation -- required by noPropertyAccessFromIndexSignature from @tsconfig/strictest
32
+ TurboModuleRegistry.get<Spec>('ReactNativeDPoP') ?? (NativeModules['ReactNativeDPoP'] as Spec | undefined);
32
33
 
33
34
  export default nativeDpopModule as Spec;
package/src/index.tsx CHANGED
@@ -11,15 +11,32 @@ export type PublicJwk = {
11
11
 
12
12
  export type PublicKeyFormat = 'JWK' | 'DER' | 'RAW';
13
13
 
14
+ export type SecureHardwareFallbackReason = 'UNAVAILABLE' | 'PROVIDER_ERROR' | 'POLICY_REJECTED' | 'UNKNOWN';
15
+ export type AndroidSecurityLevelName = 'SOFTWARE' | 'TRUSTED_ENVIRONMENT' | 'STRONGBOX';
16
+ export type IOSSecurityLevelName = 'SOFTWARE' | 'SECURE_ENCLAVE';
17
+
14
18
  export type DPoPKeyInfo = {
15
19
  alias: string;
16
20
  hasKeyPair: boolean;
17
21
  algorithm?: string;
18
22
  curve?: string;
19
23
  insideSecureHardware?: boolean;
20
- securityLevel?: number;
21
- strongBoxAvailable?: boolean;
22
- strongBoxBacked?: boolean;
24
+ hardware?: {
25
+ android?: {
26
+ strongBoxAvailable: boolean;
27
+ strongBoxBacked: boolean;
28
+ securityLevel?: number;
29
+ securityLevelName?: AndroidSecurityLevelName;
30
+ strongBoxFallbackReason?: SecureHardwareFallbackReason | null;
31
+ };
32
+ ios?: {
33
+ secureEnclaveAvailable: boolean;
34
+ secureEnclaveBacked: boolean;
35
+ securityLevel?: number | null;
36
+ securityLevelName?: IOSSecurityLevelName;
37
+ secureEnclaveFallbackReason?: SecureHardwareFallbackReason | null;
38
+ };
39
+ };
23
40
  };
24
41
 
25
42
  export type GenerateProofInput = {
@@ -52,7 +69,7 @@ type GenerateProofResult = {
52
69
 
53
70
  export class DPoP {
54
71
  public readonly proof: string;
55
- public readonly alias?: string;
72
+ public readonly alias: string | undefined;
56
73
  public readonly proofContext: DPoPProofContext;
57
74
 
58
75
  private constructor(proof: string, proofContext: DPoPProofContext, alias?: string) {
@@ -93,7 +110,8 @@ export class DPoP {
93
110
  input.additional ?? null,
94
111
  input.kid ?? null,
95
112
  input.jti ?? null,
96
- input.iat ?? null,
113
+ // RN 0.75 Android bridge can crash when a nullable Double arrives as null.
114
+ input.iat ?? Math.floor(Date.now() / 1000),
97
115
  input.alias ?? null
98
116
  )) as GenerateProofResult;
99
117