react-native-dpop 0.1.0 → 0.3.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 +21 -3
- package/{Dpop.podspec → ReactNativeDPoP.podspec} +7 -3
- package/android/build.gradle +3 -3
- package/android/src/main/java/com/{dpop → reactnativedpop}/DPoPKeyStore.kt +30 -1
- package/android/src/main/java/com/{dpop/DpopModule.kt → reactnativedpop/DPoPModule.kt} +35 -9
- package/android/src/main/java/com/{dpop/DpopPackage.kt → reactnativedpop/DPoPPackage.kt} +7 -7
- package/android/src/main/java/com/{dpop → reactnativedpop}/DPoPUtils.kt +1 -1
- package/ios/DPoPKeyStore.swift +134 -0
- package/ios/DPoPModule.h +5 -0
- package/ios/DPoPModule.swift +373 -0
- package/ios/DPoPModuleBridge.mm +93 -0
- package/ios/DPoPUtils.swift +203 -0
- package/ios/KeychainKeyStore.swift +82 -0
- package/ios/SecureEnclaveKeyStore.swift +125 -0
- package/lib/module/NativeReactNativeDPoP.js +6 -0
- package/lib/module/NativeReactNativeDPoP.js.map +1 -0
- package/lib/module/index.js +13 -33
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/{NativeDpop.d.ts → NativeReactNativeDPoP.d.ts} +1 -1
- package/lib/typescript/src/NativeReactNativeDPoP.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +15 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/{NativeDpop.ts → NativeReactNativeDPoP.ts} +5 -2
- package/src/index.tsx +29 -39
- package/ios/Dpop.h +0 -5
- package/ios/Dpop.mm +0 -21
- package/lib/module/NativeDpop.js +0 -5
- package/lib/module/NativeDpop.js.map +0 -1
- package/lib/typescript/src/NativeDpop.d.ts.map +0 -1
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
import React
|
|
4
|
+
|
|
5
|
+
final class DPoPModule {
|
|
6
|
+
static let shared = DPoPModule()
|
|
7
|
+
|
|
8
|
+
private let keyStore = DPoPKeyStore()
|
|
9
|
+
private let defaultAlias = "react-native-dpop"
|
|
10
|
+
|
|
11
|
+
private init() {}
|
|
12
|
+
|
|
13
|
+
func resolveAlias(_ alias: String?) -> String {
|
|
14
|
+
guard let alias, !alias.isEmpty else {
|
|
15
|
+
return defaultAlias
|
|
16
|
+
}
|
|
17
|
+
return alias
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func assertHardwareBacked(alias: String?) throws {
|
|
21
|
+
let effectiveAlias = resolveAlias(alias)
|
|
22
|
+
guard keyStore.hasKeyPair(alias: effectiveAlias) else {
|
|
23
|
+
throw DPoPError.keyNotFound(alias: effectiveAlias)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
guard keyStore.isHardwareBacked(alias: effectiveAlias) else {
|
|
27
|
+
throw DPoPError.notHardwareBacked(alias: effectiveAlias)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func calculateThumbprint(alias: String?) throws -> String {
|
|
32
|
+
let effectiveAlias = resolveAlias(alias)
|
|
33
|
+
if !keyStore.hasKeyPair(alias: effectiveAlias) {
|
|
34
|
+
try keyStore.generateKeyPair(alias: effectiveAlias)
|
|
35
|
+
}
|
|
36
|
+
let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
|
|
37
|
+
let coordinates = try DPoPUtils.getPublicCoordinates(fromRawPublicKey: try DPoPUtils.toRawPublicKey(keyPair.publicKey))
|
|
38
|
+
return DPoPUtils.calculateThumbprint(kty: "EC", crv: "P-256", x: coordinates.x, y: coordinates.y)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func deleteKeyPair(alias: String?) throws {
|
|
42
|
+
try keyStore.deleteKeyPair(alias: resolveAlias(alias))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func getKeyInfo(alias: String?) -> [String: Any] {
|
|
46
|
+
let effectiveAlias = resolveAlias(alias)
|
|
47
|
+
let secureEnclaveAvailable = keyStore.isSecureEnclaveAvailable()
|
|
48
|
+
let fallbackReason = keyStore.getSecureEnclaveFallbackReason(alias: effectiveAlias)
|
|
49
|
+
let secureEnclaveFallbackReason: Any = fallbackReason ?? NSNull()
|
|
50
|
+
let keyInfo = keyStore.getKeyInfo(alias: effectiveAlias)
|
|
51
|
+
|
|
52
|
+
if !keyInfo.hasKeyPair {
|
|
53
|
+
return [
|
|
54
|
+
"alias": effectiveAlias,
|
|
55
|
+
"hasKeyPair": false,
|
|
56
|
+
"hardware": [
|
|
57
|
+
"ios": [
|
|
58
|
+
"secureEnclaveAvailable": secureEnclaveAvailable,
|
|
59
|
+
"secureEnclaveBacked": false,
|
|
60
|
+
"securityLevel": NSNull(),
|
|
61
|
+
"secureEnclaveFallbackReason": secureEnclaveFallbackReason
|
|
62
|
+
]
|
|
63
|
+
]
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let secureEnclaveBacked = secureEnclaveAvailable && keyInfo.insideSecureHardware
|
|
68
|
+
let securityLevel = secureEnclaveBacked ? 2 : 1
|
|
69
|
+
|
|
70
|
+
return [
|
|
71
|
+
"alias": keyInfo.alias,
|
|
72
|
+
"algorithm": keyInfo.algorithm,
|
|
73
|
+
"curve": keyInfo.curve,
|
|
74
|
+
"hasKeyPair": true,
|
|
75
|
+
"insideSecureHardware": secureEnclaveBacked,
|
|
76
|
+
"hardware": [
|
|
77
|
+
"ios": [
|
|
78
|
+
"secureEnclaveAvailable": secureEnclaveAvailable,
|
|
79
|
+
"secureEnclaveBacked": secureEnclaveBacked,
|
|
80
|
+
"securityLevel": securityLevel,
|
|
81
|
+
"secureEnclaveFallbackReason": secureEnclaveFallbackReason
|
|
82
|
+
]
|
|
83
|
+
]
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func getPublicKeyDer(alias: String?) throws -> String {
|
|
88
|
+
let effectiveAlias = resolveAlias(alias)
|
|
89
|
+
if !keyStore.hasKeyPair(alias: effectiveAlias) {
|
|
90
|
+
try keyStore.generateKeyPair(alias: effectiveAlias)
|
|
91
|
+
}
|
|
92
|
+
let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
|
|
93
|
+
return DPoPUtils.base64UrlEncode(try DPoPUtils.toDerPublicKey(keyPair.publicKey))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func getPublicKeyJwk(alias: String?) throws -> [String: Any] {
|
|
97
|
+
let effectiveAlias = resolveAlias(alias)
|
|
98
|
+
if !keyStore.hasKeyPair(alias: effectiveAlias) {
|
|
99
|
+
try keyStore.generateKeyPair(alias: effectiveAlias)
|
|
100
|
+
}
|
|
101
|
+
let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
|
|
102
|
+
let coordinates = try DPoPUtils.getPublicCoordinates(fromRawPublicKey: try DPoPUtils.toRawPublicKey(keyPair.publicKey))
|
|
103
|
+
return [
|
|
104
|
+
"kty": "EC",
|
|
105
|
+
"crv": "P-256",
|
|
106
|
+
"x": coordinates.x,
|
|
107
|
+
"y": coordinates.y
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func getPublicKeyRaw(alias: String?) throws -> String {
|
|
112
|
+
let effectiveAlias = resolveAlias(alias)
|
|
113
|
+
if !keyStore.hasKeyPair(alias: effectiveAlias) {
|
|
114
|
+
try keyStore.generateKeyPair(alias: effectiveAlias)
|
|
115
|
+
}
|
|
116
|
+
let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
|
|
117
|
+
return DPoPUtils.base64UrlEncode(try DPoPUtils.toRawPublicKey(keyPair.publicKey))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func hasKeyPair(alias: String?) -> Bool {
|
|
121
|
+
keyStore.hasKeyPair(alias: resolveAlias(alias))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func isBoundToAlias(proof: String, alias: String?) throws -> Bool {
|
|
125
|
+
let effectiveAlias = resolveAlias(alias)
|
|
126
|
+
if !keyStore.hasKeyPair(alias: effectiveAlias) {
|
|
127
|
+
try keyStore.generateKeyPair(alias: effectiveAlias)
|
|
128
|
+
}
|
|
129
|
+
let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
|
|
130
|
+
return try DPoPUtils.isProofBoundToPublicKey(proof, publicKey: keyPair.publicKey)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func rotateKeyPair(alias: String?) throws {
|
|
134
|
+
try keyStore.generateKeyPair(alias: resolveAlias(alias))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func signWithDpopPrivateKey(payload: String, alias: String?) throws -> String {
|
|
138
|
+
let effectiveAlias = resolveAlias(alias)
|
|
139
|
+
if !keyStore.hasKeyPair(alias: effectiveAlias) {
|
|
140
|
+
try keyStore.generateKeyPair(alias: effectiveAlias)
|
|
141
|
+
}
|
|
142
|
+
let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
|
|
143
|
+
var error: Unmanaged<CFError>?
|
|
144
|
+
guard let derSignature = SecKeyCreateSignature(
|
|
145
|
+
keyPair.privateKey,
|
|
146
|
+
.ecdsaSignatureMessageX962SHA256,
|
|
147
|
+
Data(payload.utf8) as CFData,
|
|
148
|
+
&error
|
|
149
|
+
) as Data? else {
|
|
150
|
+
throw DPoPError.securityError(error?.takeRetainedValue())
|
|
151
|
+
}
|
|
152
|
+
let joseSignature = try DPoPUtils.derToJose(derSignature, partLength: 32)
|
|
153
|
+
return DPoPUtils.base64UrlEncode(joseSignature)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func generateProof(
|
|
157
|
+
htu: String,
|
|
158
|
+
htm: String,
|
|
159
|
+
nonce: String?,
|
|
160
|
+
accessToken: String?,
|
|
161
|
+
additional: [String: Any]?,
|
|
162
|
+
kid: String?,
|
|
163
|
+
jti: String?,
|
|
164
|
+
iat: NSNumber?,
|
|
165
|
+
alias: String?
|
|
166
|
+
) throws -> [String: Any] {
|
|
167
|
+
let effectiveAlias = resolveAlias(alias)
|
|
168
|
+
if !keyStore.hasKeyPair(alias: effectiveAlias) {
|
|
169
|
+
try keyStore.generateKeyPair(alias: effectiveAlias)
|
|
170
|
+
}
|
|
171
|
+
let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
|
|
172
|
+
let coordinates = try DPoPUtils.getPublicCoordinates(fromRawPublicKey: try DPoPUtils.toRawPublicKey(keyPair.publicKey))
|
|
173
|
+
|
|
174
|
+
var jwk: [String: Any] = [
|
|
175
|
+
"kty": "EC",
|
|
176
|
+
"crv": "P-256",
|
|
177
|
+
"x": coordinates.x,
|
|
178
|
+
"y": coordinates.y
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
var header: [String: Any] = [
|
|
182
|
+
"typ": "dpop+jwt",
|
|
183
|
+
"alg": "ES256",
|
|
184
|
+
"jwk": jwk
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
if let kid, !kid.isEmpty {
|
|
188
|
+
header["kid"] = kid
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let issuedAt = iat?.int64Value ?? Int64(Date().timeIntervalSince1970)
|
|
192
|
+
let finalJti = (jti?.isEmpty == false) ? jti! : UUID().uuidString
|
|
193
|
+
|
|
194
|
+
var payload: [String: Any] = [
|
|
195
|
+
"jti": finalJti,
|
|
196
|
+
"htm": htm.uppercased(),
|
|
197
|
+
"htu": htu,
|
|
198
|
+
"iat": issuedAt
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
if let nonce, !nonce.isEmpty {
|
|
202
|
+
payload["nonce"] = nonce
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if let accessToken, !accessToken.isEmpty {
|
|
206
|
+
payload["ath"] = DPoPUtils.hashAccessToken(accessToken)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if let additional {
|
|
210
|
+
for (key, value) in additional {
|
|
211
|
+
payload[key] = value
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let headerSegment = DPoPUtils.base64UrlEncode(try DPoPUtils.jsonData(header))
|
|
216
|
+
let payloadSegment = DPoPUtils.base64UrlEncode(try DPoPUtils.jsonData(payload))
|
|
217
|
+
let signingInput = "\(headerSegment).\(payloadSegment)"
|
|
218
|
+
|
|
219
|
+
var error: Unmanaged<CFError>?
|
|
220
|
+
guard let derSignature = SecKeyCreateSignature(
|
|
221
|
+
keyPair.privateKey,
|
|
222
|
+
.ecdsaSignatureMessageX962SHA256,
|
|
223
|
+
Data(signingInput.utf8) as CFData,
|
|
224
|
+
&error
|
|
225
|
+
) as Data? else {
|
|
226
|
+
throw DPoPError.securityError(error?.takeRetainedValue())
|
|
227
|
+
}
|
|
228
|
+
let joseSignature = try DPoPUtils.derToJose(derSignature, partLength: 32)
|
|
229
|
+
let jwt = "\(signingInput).\(DPoPUtils.base64UrlEncode(joseSignature))"
|
|
230
|
+
|
|
231
|
+
let proofContext: [String: Any] = [
|
|
232
|
+
"htu": payload["htu"] as? String ?? htu,
|
|
233
|
+
"htm": payload["htm"] as? String ?? htm.uppercased(),
|
|
234
|
+
"nonce": payload["nonce"] ?? NSNull(),
|
|
235
|
+
"ath": payload["ath"] ?? NSNull(),
|
|
236
|
+
"kid": header["kid"] ?? NSNull(),
|
|
237
|
+
"jti": payload["jti"] as? String ?? finalJti,
|
|
238
|
+
"iat": Double(issuedAt),
|
|
239
|
+
"additional": additional ?? NSNull()
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
return [
|
|
243
|
+
"proof": jwt,
|
|
244
|
+
"proofContext": proofContext
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@objc extension ReactNativeDPoP {
|
|
251
|
+
static func moduleName() -> String! {
|
|
252
|
+
"ReactNativeDPoP"
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
static func requiresMainQueueSetup() -> Bool {
|
|
256
|
+
false
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func assertHardwareBacked(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
260
|
+
do {
|
|
261
|
+
try DPoPModule.shared.assertHardwareBacked(alias: alias)
|
|
262
|
+
resolve(nil)
|
|
263
|
+
} catch {
|
|
264
|
+
reject("ERR_DPOP_ASSERT_HARDWARE_BACKED", error.localizedDescription, error)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
func calculateThumbprint(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
269
|
+
do {
|
|
270
|
+
resolve(try DPoPModule.shared.calculateThumbprint(alias: alias))
|
|
271
|
+
} catch {
|
|
272
|
+
reject("ERR_DPOP_CALCULATE_THUMBPRINT", error.localizedDescription, error)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
func deleteKeyPair(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
277
|
+
do {
|
|
278
|
+
try DPoPModule.shared.deleteKeyPair(alias: alias)
|
|
279
|
+
resolve(nil)
|
|
280
|
+
} catch {
|
|
281
|
+
reject("ERR_DPOP_DELETE_KEY_PAIR", error.localizedDescription, error)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func getKeyInfo(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
286
|
+
resolve(DPoPModule.shared.getKeyInfo(alias: alias))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
func getPublicKeyDer(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
290
|
+
do {
|
|
291
|
+
resolve(try DPoPModule.shared.getPublicKeyDer(alias: alias))
|
|
292
|
+
} catch {
|
|
293
|
+
reject("ERR_DPOP_PUBLIC_KEY", error.localizedDescription, error)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
func getPublicKeyJwk(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
298
|
+
do {
|
|
299
|
+
resolve(try DPoPModule.shared.getPublicKeyJwk(alias: alias))
|
|
300
|
+
} catch {
|
|
301
|
+
reject("ERR_DPOP_PUBLIC_KEY", error.localizedDescription, error)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
func getPublicKeyRaw(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
306
|
+
do {
|
|
307
|
+
resolve(try DPoPModule.shared.getPublicKeyRaw(alias: alias))
|
|
308
|
+
} catch {
|
|
309
|
+
reject("ERR_DPOP_PUBLIC_KEY", error.localizedDescription, error)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
func hasKeyPair(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
314
|
+
resolve(DPoPModule.shared.hasKeyPair(alias: alias))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
func isBoundToAlias(_ proof: String, alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
318
|
+
do {
|
|
319
|
+
resolve(try DPoPModule.shared.isBoundToAlias(proof: proof, alias: alias))
|
|
320
|
+
} catch {
|
|
321
|
+
reject("ERR_DPOP_IS_BOUND_TO_ALIAS", error.localizedDescription, error)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
func rotateKeyPair(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
326
|
+
do {
|
|
327
|
+
try DPoPModule.shared.rotateKeyPair(alias: alias)
|
|
328
|
+
resolve(nil)
|
|
329
|
+
} catch {
|
|
330
|
+
reject("ERR_DPOP_ROTATE_KEY_PAIR", error.localizedDescription, error)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
func signWithDpopPrivateKey(_ payload: String, alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
335
|
+
do {
|
|
336
|
+
resolve(try DPoPModule.shared.signWithDpopPrivateKey(payload: payload, alias: alias))
|
|
337
|
+
} catch {
|
|
338
|
+
reject("ERR_DPOP_SIGN_WITH_PRIVATE_KEY", error.localizedDescription, error)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
func generateProof(
|
|
343
|
+
_ htu: String,
|
|
344
|
+
htm: String,
|
|
345
|
+
nonce: String?,
|
|
346
|
+
accessToken: String?,
|
|
347
|
+
additional: [String: Any]?,
|
|
348
|
+
kid: String?,
|
|
349
|
+
jti: String?,
|
|
350
|
+
iat: NSNumber?,
|
|
351
|
+
alias: String?,
|
|
352
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
353
|
+
reject: @escaping RCTPromiseRejectBlock
|
|
354
|
+
) {
|
|
355
|
+
do {
|
|
356
|
+
resolve(
|
|
357
|
+
try DPoPModule.shared.generateProof(
|
|
358
|
+
htu: htu,
|
|
359
|
+
htm: htm,
|
|
360
|
+
nonce: nonce,
|
|
361
|
+
accessToken: accessToken,
|
|
362
|
+
additional: additional,
|
|
363
|
+
kid: kid,
|
|
364
|
+
jti: jti,
|
|
365
|
+
iat: iat,
|
|
366
|
+
alias: alias
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
} catch {
|
|
370
|
+
reject("ERR_DPOP_GENERATE_PROOF", error.localizedDescription, error)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTUtils.h>
|
|
3
|
+
|
|
4
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
5
|
+
#if __has_include(<ReactNativeDPoPSpec/ReactNativeDPoPSpec.h>)
|
|
6
|
+
#import <ReactNativeDPoPSpec/ReactNativeDPoPSpec.h>
|
|
7
|
+
#else
|
|
8
|
+
#import "ReactNativeDPoPSpec.h"
|
|
9
|
+
#endif
|
|
10
|
+
#endif
|
|
11
|
+
|
|
12
|
+
#if __has_include(<ReactNativeDPoP/ReactNativeDPoP-Swift.h>)
|
|
13
|
+
#import <ReactNativeDPoP/ReactNativeDPoP-Swift.h>
|
|
14
|
+
#else
|
|
15
|
+
#import "ReactNativeDPoP-Swift.h"
|
|
16
|
+
#endif
|
|
17
|
+
|
|
18
|
+
@interface RCT_EXTERN_MODULE(ReactNativeDPoP, NSObject)
|
|
19
|
+
|
|
20
|
+
RCT_EXTERN_METHOD(assertHardwareBacked:(NSString * _Nullable)alias
|
|
21
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
22
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
23
|
+
|
|
24
|
+
RCT_EXTERN_METHOD(calculateThumbprint:(NSString * _Nullable)alias
|
|
25
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
26
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
27
|
+
|
|
28
|
+
RCT_EXTERN_METHOD(deleteKeyPair:(NSString * _Nullable)alias
|
|
29
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
30
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
31
|
+
|
|
32
|
+
RCT_EXTERN_METHOD(getKeyInfo:(NSString * _Nullable)alias
|
|
33
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
34
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
35
|
+
|
|
36
|
+
RCT_EXTERN_METHOD(getPublicKeyDer:(NSString * _Nullable)alias
|
|
37
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
38
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
39
|
+
|
|
40
|
+
RCT_EXTERN_METHOD(getPublicKeyJwk:(NSString * _Nullable)alias
|
|
41
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
42
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
43
|
+
|
|
44
|
+
RCT_EXTERN_METHOD(getPublicKeyRaw:(NSString * _Nullable)alias
|
|
45
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
46
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
47
|
+
|
|
48
|
+
RCT_EXTERN_METHOD(hasKeyPair:(NSString * _Nullable)alias
|
|
49
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
50
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
51
|
+
|
|
52
|
+
RCT_EXTERN_METHOD(isBoundToAlias:(NSString *)proof
|
|
53
|
+
alias:(NSString * _Nullable)alias
|
|
54
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
55
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
56
|
+
|
|
57
|
+
RCT_EXTERN_METHOD(rotateKeyPair:(NSString * _Nullable)alias
|
|
58
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
59
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
60
|
+
|
|
61
|
+
RCT_EXTERN_METHOD(signWithDpopPrivateKey:(NSString *)payload
|
|
62
|
+
alias:(NSString * _Nullable)alias
|
|
63
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
64
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
65
|
+
|
|
66
|
+
RCT_EXTERN_METHOD(generateProof:(NSString *)htu
|
|
67
|
+
htm:(NSString *)htm
|
|
68
|
+
nonce:(NSString * _Nullable)nonce
|
|
69
|
+
accessToken:(NSString * _Nullable)accessToken
|
|
70
|
+
additional:(NSDictionary * _Nullable)additional
|
|
71
|
+
kid:(NSString * _Nullable)kid
|
|
72
|
+
jti:(NSString * _Nullable)jti
|
|
73
|
+
iat:(NSNumber * _Nullable)iat
|
|
74
|
+
alias:(NSString * _Nullable)alias
|
|
75
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
76
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
77
|
+
|
|
78
|
+
@end
|
|
79
|
+
|
|
80
|
+
@implementation ReactNativeDPoP
|
|
81
|
+
@end
|
|
82
|
+
|
|
83
|
+
#if RCT_NEW_ARCH_ENABLED
|
|
84
|
+
@implementation ReactNativeDPoP (TurboModule)
|
|
85
|
+
|
|
86
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
87
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
88
|
+
{
|
|
89
|
+
return std::make_shared<facebook::react::NativeReactNativeDPoPSpecJSI>(params);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@end
|
|
93
|
+
#endif
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CryptoKit
|
|
3
|
+
import Security
|
|
4
|
+
|
|
5
|
+
enum DPoPUtils {
|
|
6
|
+
static func base64UrlEncode(_ data: Data) -> String {
|
|
7
|
+
data.base64EncodedString()
|
|
8
|
+
.replacingOccurrences(of: "+", with: "-")
|
|
9
|
+
.replacingOccurrences(of: "/", with: "_")
|
|
10
|
+
.replacingOccurrences(of: "=", with: "")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static func base64UrlDecode(_ value: String) -> Data? {
|
|
14
|
+
let remainder = value.count % 4
|
|
15
|
+
let padded = remainder == 0 ? value : value + String(repeating: "=", count: 4 - remainder)
|
|
16
|
+
let base64 = padded
|
|
17
|
+
.replacingOccurrences(of: "-", with: "+")
|
|
18
|
+
.replacingOccurrences(of: "_", with: "/")
|
|
19
|
+
return Data(base64Encoded: base64)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static func sha256(_ data: Data) -> Data {
|
|
23
|
+
Data(SHA256.hash(data: data))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static func hashAccessToken(_ accessToken: String) -> String {
|
|
27
|
+
let data = Data(accessToken.utf8)
|
|
28
|
+
return base64UrlEncode(sha256(data))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static func calculateThumbprint(kty: String, crv: String, x: String, y: String) -> String {
|
|
32
|
+
let canonical = "{\"crv\":\"\(crv)\",\"kty\":\"\(kty)\",\"x\":\"\(x)\",\"y\":\"\(y)\"}"
|
|
33
|
+
return base64UrlEncode(sha256(Data(canonical.utf8)))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static func jsonData(_ object: Any) throws -> Data {
|
|
37
|
+
if #available(iOS 11.0, *) {
|
|
38
|
+
return try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys])
|
|
39
|
+
}
|
|
40
|
+
return try JSONSerialization.data(withJSONObject: object, options: [])
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static func derToJose(_ derSignature: Data, partLength: Int = 32) throws -> Data {
|
|
44
|
+
let bytes = [UInt8](derSignature)
|
|
45
|
+
guard !bytes.isEmpty, bytes[0] == 0x30 else {
|
|
46
|
+
throw DPoPError.invalidDerSignature
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var index = 1
|
|
50
|
+
let (_, seqLengthBytes) = try readDerLength(bytes, startIndex: index)
|
|
51
|
+
index += seqLengthBytes
|
|
52
|
+
|
|
53
|
+
guard index < bytes.count, bytes[index] == 0x02 else {
|
|
54
|
+
throw DPoPError.invalidDerSignature
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
index += 1
|
|
58
|
+
let (rLength, rLengthBytes) = try readDerLength(bytes, startIndex: index)
|
|
59
|
+
index += rLengthBytes
|
|
60
|
+
let rEnd = index + rLength
|
|
61
|
+
guard rEnd <= bytes.count else {
|
|
62
|
+
throw DPoPError.invalidDerSignature
|
|
63
|
+
}
|
|
64
|
+
let r = Data(bytes[index..<rEnd])
|
|
65
|
+
index = rEnd
|
|
66
|
+
|
|
67
|
+
guard index < bytes.count, bytes[index] == 0x02 else {
|
|
68
|
+
throw DPoPError.invalidDerSignature
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
index += 1
|
|
72
|
+
let (sLength, sLengthBytes) = try readDerLength(bytes, startIndex: index)
|
|
73
|
+
index += sLengthBytes
|
|
74
|
+
let sEnd = index + sLength
|
|
75
|
+
guard sEnd <= bytes.count else {
|
|
76
|
+
throw DPoPError.invalidDerSignature
|
|
77
|
+
}
|
|
78
|
+
let s = Data(bytes[index..<sEnd])
|
|
79
|
+
|
|
80
|
+
let rFixed = try toUnsignedFixedLength(r, length: partLength)
|
|
81
|
+
let sFixed = try toUnsignedFixedLength(s, length: partLength)
|
|
82
|
+
return rFixed + sFixed
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static func getPublicCoordinates(fromRawPublicKey raw: Data) throws -> (x: String, y: String) {
|
|
86
|
+
guard raw.count == 65, raw.first == 0x04 else {
|
|
87
|
+
throw DPoPError.invalidPublicKey
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let x = raw.subdata(in: 1..<33)
|
|
91
|
+
let y = raw.subdata(in: 33..<65)
|
|
92
|
+
return (base64UrlEncode(x), base64UrlEncode(y))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static func toRawPublicKey(_ publicKey: SecKey) throws -> Data {
|
|
96
|
+
var error: Unmanaged<CFError>?
|
|
97
|
+
guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
|
|
98
|
+
throw DPoPError.securityError(error?.takeRetainedValue())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
guard publicKeyData.count == 65, publicKeyData.first == 0x04 else {
|
|
102
|
+
throw DPoPError.invalidPublicKey
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return publicKeyData
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static func toDerPublicKey(_ publicKey: SecKey) throws -> Data {
|
|
109
|
+
let raw = try toRawPublicKey(publicKey)
|
|
110
|
+
let cryptoKey = try P256.Signing.PublicKey(x963Representation: raw)
|
|
111
|
+
return cryptoKey.derRepresentation
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static func isProofBoundToPublicKey(_ proof: String, publicKey: SecKey) throws -> Bool {
|
|
115
|
+
let parts = proof.split(separator: ".", omittingEmptySubsequences: false)
|
|
116
|
+
guard parts.count == 3 else {
|
|
117
|
+
throw DPoPError.invalidProofFormat
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
guard let headerData = base64UrlDecode(String(parts[0])) else {
|
|
121
|
+
throw DPoPError.invalidProofFormat
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let json = try JSONSerialization.jsonObject(with: headerData, options: [])
|
|
125
|
+
guard let header = json as? [String: Any],
|
|
126
|
+
let jwk = header["jwk"] as? [String: Any],
|
|
127
|
+
let kty = jwk["kty"] as? String,
|
|
128
|
+
let crv = jwk["crv"] as? String,
|
|
129
|
+
let x = jwk["x"] as? String,
|
|
130
|
+
let y = jwk["y"] as? String else {
|
|
131
|
+
throw DPoPError.invalidProofFormat
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let coordinates = try getPublicCoordinates(fromRawPublicKey: try toRawPublicKey(publicKey))
|
|
135
|
+
return kty == "EC" && crv == "P-256" && x == coordinates.x && y == coordinates.y
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private static func readDerLength(_ input: [UInt8], startIndex: Int) throws -> (Int, Int) {
|
|
139
|
+
guard startIndex < input.count else {
|
|
140
|
+
throw DPoPError.invalidDerSignature
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let first = Int(input[startIndex])
|
|
144
|
+
if (first & 0x80) == 0 {
|
|
145
|
+
return (first, 1)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let lengthBytesCount = first & 0x7F
|
|
149
|
+
guard lengthBytesCount > 0, lengthBytesCount <= 4, startIndex + lengthBytesCount < input.count else {
|
|
150
|
+
throw DPoPError.invalidDerSignature
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
var length = 0
|
|
154
|
+
for index in 0..<lengthBytesCount {
|
|
155
|
+
length = (length << 8) | Int(input[startIndex + 1 + index])
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (length, 1 + lengthBytesCount)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private static func toUnsignedFixedLength(_ value: Data, length: Int) throws -> Data {
|
|
162
|
+
let bytes = [UInt8](value)
|
|
163
|
+
if bytes.count == length {
|
|
164
|
+
return value
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if bytes.count == length + 1, bytes.first == 0x00 {
|
|
168
|
+
return Data(bytes.dropFirst())
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if bytes.count < length {
|
|
172
|
+
return Data(repeating: 0, count: length - bytes.count) + value
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw DPoPError.invalidDerSignature
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
enum DPoPError: LocalizedError {
|
|
180
|
+
case invalidDerSignature
|
|
181
|
+
case invalidPublicKey
|
|
182
|
+
case invalidProofFormat
|
|
183
|
+
case keyNotFound(alias: String)
|
|
184
|
+
case notHardwareBacked(alias: String)
|
|
185
|
+
case securityError(CFError?)
|
|
186
|
+
|
|
187
|
+
var errorDescription: String? {
|
|
188
|
+
switch self {
|
|
189
|
+
case .invalidDerSignature:
|
|
190
|
+
return "Invalid DER signature format"
|
|
191
|
+
case .invalidPublicKey:
|
|
192
|
+
return "Invalid P-256 public key"
|
|
193
|
+
case .invalidProofFormat:
|
|
194
|
+
return "Invalid DPoP proof format"
|
|
195
|
+
case .keyNotFound(let alias):
|
|
196
|
+
return "Key pair not found for alias: \(alias)"
|
|
197
|
+
case .notHardwareBacked(let alias):
|
|
198
|
+
return "Key pair is not hardware-backed for alias: \(alias)"
|
|
199
|
+
case .securityError(let error):
|
|
200
|
+
return (error as Error?)?.localizedDescription ?? "Security framework error"
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|