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 +143 -53
- package/ReactNativeDPoP.podspec +1 -1
- package/android/src/main/java/com/reactnativedpop/DPoPKeyStore.kt +23 -2
- package/android/src/main/java/com/reactnativedpop/DPoPModule.kt +79 -33
- package/android/src/main/java/com/reactnativedpop/DPoPPackage.kt +7 -7
- package/android/src/main/java/com/reactnativedpop/DPoPUtils.kt +6 -6
- package/ios/DPoPKeyStore.swift +11 -2
- package/ios/DPoPModule.swift +96 -31
- package/ios/DPoPModuleBridge.mm +7 -6
- package/ios/DPoPUtils.swift +5 -5
- package/lib/module/NativeReactNativeDPoP.js +4 -2
- package/lib/module/NativeReactNativeDPoP.js.map +1 -1
- package/lib/module/index.js +33 -22
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/examples/shared/DPoPExampleContent.d.ts +2 -0
- package/lib/typescript/examples/shared/DPoPExampleContent.d.ts.map +1 -0
- package/lib/typescript/examples/v0.75/App.d.ts +2 -0
- package/lib/typescript/examples/v0.75/App.d.ts.map +1 -0
- package/lib/typescript/examples/v0.83/App.d.ts +2 -0
- package/lib/typescript/examples/v0.83/App.d.ts.map +1 -0
- package/lib/typescript/src/NativeReactNativeDPoP.d.ts +3 -3
- package/lib/typescript/src/NativeReactNativeDPoP.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +34 -24
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +8 -4
- package/src/NativeReactNativeDPoP.ts +15 -13
- package/src/index.tsx +66 -45
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
|
|
9
|
-
- Export public key
|
|
10
|
-
- Calculate JWK
|
|
11
|
-
- Verify
|
|
12
|
-
- Retrieve non-sensitive key metadata
|
|
13
|
-
-
|
|
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
|
|
16
|
+
## Platform support
|
|
16
17
|
|
|
17
|
-
- Android
|
|
18
|
-
- iOS
|
|
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
|
|
27
|
+
For iOS:
|
|
27
28
|
|
|
28
29
|
```sh
|
|
29
30
|
cd ios && pod install
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
## Quick
|
|
33
|
+
## Quick start
|
|
33
34
|
|
|
34
35
|
```ts
|
|
35
36
|
import { DPoP } from 'react-native-dpop';
|
|
36
37
|
|
|
37
|
-
const
|
|
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: '
|
|
42
|
+
nonce: 'SERVER_NONCE',
|
|
43
|
+
requireHardwareBacked: true,
|
|
42
44
|
});
|
|
43
45
|
|
|
44
|
-
const proof =
|
|
45
|
-
const thumbprint = await
|
|
46
|
-
const publicJwk = await
|
|
47
|
-
const
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
- `
|
|
70
|
+
- `getPublicKeyThumbprint(): Promise<string>`
|
|
71
|
+
- `signWithDPoPPrivateKey(payload): Promise<string>`
|
|
81
72
|
- `isBoundToAlias(alias?): Promise<boolean>`
|
|
82
73
|
|
|
83
|
-
|
|
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
|
-
|
|
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)
|
package/ReactNativeDPoP.podspec
CHANGED
|
@@ -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 =>
|
|
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
|
-
|
|
123
|
-
|
|
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
|
|
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",
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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)
|
package/ios/DPoPKeyStore.swift
CHANGED
|
@@ -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
|
-
|
|
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) {
|