react-native-dpop 0.1.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/Dpop.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +105 -0
- package/android/build.gradle +67 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/dpop/DPoPKeyStore.kt +168 -0
- package/android/src/main/java/com/dpop/DPoPUtils.kt +208 -0
- package/android/src/main/java/com/dpop/DpopModule.kt +302 -0
- package/android/src/main/java/com/dpop/DpopPackage.kt +31 -0
- package/ios/Dpop.h +5 -0
- package/ios/Dpop.mm +21 -0
- package/lib/module/NativeDpop.js +5 -0
- package/lib/module/NativeDpop.js.map +1 -0
- package/lib/module/index.js +68 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeDpop.d.ts +19 -0
- package/lib/typescript/src/NativeDpop.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +57 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +120 -0
- package/src/NativeDpop.ts +30 -0
- package/src/index.tsx +145 -0
package/Dpop.podspec
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "Dpop"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/Cirilord/react-native-dpop.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
|
|
19
|
+
install_modules_dependencies(s)
|
|
20
|
+
end
|
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pedro Cirilo
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# react-native-dpop
|
|
2
|
+
|
|
3
|
+
React Native library for DPoP proof generation and key management.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
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
|
+
|
|
14
|
+
## Platform Support
|
|
15
|
+
|
|
16
|
+
- Android: supported.
|
|
17
|
+
- iOS: planned.
|
|
18
|
+
|
|
19
|
+
Current implementation throws on non-Android platforms.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install react-native-dpop
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { DPoP } from 'react-native-dpop';
|
|
31
|
+
|
|
32
|
+
const dpop = await DPoP.generateProof({
|
|
33
|
+
htu: 'https://api.example.com/token',
|
|
34
|
+
htm: 'POST',
|
|
35
|
+
accessToken: 'ACCESS_TOKEN',
|
|
36
|
+
nonce: 'server-nonce',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const proof = dpop.proof;
|
|
40
|
+
const thumbprint = await dpop.calculateThumbprint();
|
|
41
|
+
const publicJwk = await dpop.getPublicKey('JWK');
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
### Types
|
|
47
|
+
|
|
48
|
+
- `GenerateProofInput`
|
|
49
|
+
- `DPoPProofContext`
|
|
50
|
+
- `DPoPKeyInfo`
|
|
51
|
+
- `PublicJwk`
|
|
52
|
+
- `PublicKeyFormat = 'JWK' | 'DER' | 'RAW'`
|
|
53
|
+
|
|
54
|
+
### `DPoP` static methods
|
|
55
|
+
|
|
56
|
+
- `DPoP.generateProof(input): Promise<DPoP>`
|
|
57
|
+
- `DPoP.assertHardwareBacked(alias?): Promise<void>`
|
|
58
|
+
- `DPoP.deleteKeyPair(alias?): Promise<void>`
|
|
59
|
+
- `DPoP.getKeyInfo(alias?): Promise<DPoPKeyInfo>`
|
|
60
|
+
- `DPoP.hasKeyPair(alias?): Promise<boolean>`
|
|
61
|
+
- `DPoP.rotateKeyPair(alias?): Promise<void>`
|
|
62
|
+
|
|
63
|
+
### `DPoP` instance fields
|
|
64
|
+
|
|
65
|
+
- `proof: string`
|
|
66
|
+
- `proofContext: DPoPProofContext`
|
|
67
|
+
- `alias?: string`
|
|
68
|
+
|
|
69
|
+
### `DPoP` instance methods
|
|
70
|
+
|
|
71
|
+
- `calculateThumbprint(): Promise<string>`
|
|
72
|
+
- `getPublicKey(format): Promise<PublicJwk | string>`
|
|
73
|
+
- `signWithDpopPrivateKey(payload): Promise<string>`
|
|
74
|
+
- `isBoundToAlias(alias?): Promise<boolean>`
|
|
75
|
+
|
|
76
|
+
## Error Codes
|
|
77
|
+
|
|
78
|
+
Native errors are rejected with codes such as:
|
|
79
|
+
|
|
80
|
+
- `ERR_DPOP_GENERATE_PROOF`
|
|
81
|
+
- `ERR_DPOP_PUBLIC_KEY`
|
|
82
|
+
- `ERR_DPOP_SIGN_WITH_PRIVATE_KEY`
|
|
83
|
+
- `ERR_DPOP_HAS_KEY_PAIR`
|
|
84
|
+
- `ERR_DPOP_GET_KEY_INFO`
|
|
85
|
+
- `ERR_DPOP_ROTATE_KEY_PAIR`
|
|
86
|
+
- `ERR_DPOP_DELETE_KEY_PAIR`
|
|
87
|
+
- `ERR_DPOP_ASSERT_HARDWARE_BACKED`
|
|
88
|
+
- `ERR_DPOP_IS_BOUND_TO_ALIAS`
|
|
89
|
+
|
|
90
|
+
## Notes
|
|
91
|
+
|
|
92
|
+
- If no alias is provided, the default alias is `react-native-dpop`.
|
|
93
|
+
- `htm` is normalized to uppercase in proof generation.
|
|
94
|
+
- `ath` is derived from `accessToken` (`SHA-256`, base64url) when provided.
|
|
95
|
+
- `jti` and `iat` are auto-generated when omitted.
|
|
96
|
+
|
|
97
|
+
## Contributing
|
|
98
|
+
|
|
99
|
+
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
100
|
+
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
101
|
+
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.Dpop = [
|
|
3
|
+
kotlinVersion: "2.0.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36,
|
|
6
|
+
targetSdkVersion: 36
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
ext.getExtOrDefault = { prop ->
|
|
10
|
+
if (rootProject.ext.has(prop)) {
|
|
11
|
+
return rootProject.ext.get(prop)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return Dpop[prop]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
repositories {
|
|
18
|
+
google()
|
|
19
|
+
mavenCentral()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dependencies {
|
|
23
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
24
|
+
// noinspection DifferentKotlinGradleVersion
|
|
25
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
apply plugin: "com.android.library"
|
|
31
|
+
apply plugin: "kotlin-android"
|
|
32
|
+
|
|
33
|
+
apply plugin: "com.facebook.react"
|
|
34
|
+
|
|
35
|
+
android {
|
|
36
|
+
namespace "com.dpop"
|
|
37
|
+
|
|
38
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
39
|
+
|
|
40
|
+
defaultConfig {
|
|
41
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
42
|
+
targetSdkVersion getExtOrDefault("targetSdkVersion")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildFeatures {
|
|
46
|
+
buildConfig true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
buildTypes {
|
|
50
|
+
release {
|
|
51
|
+
minifyEnabled false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lint {
|
|
56
|
+
disable "GradleCompatible"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
compileOptions {
|
|
60
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
61
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dependencies {
|
|
66
|
+
implementation "com.facebook.react:react-android"
|
|
67
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
package com.dpop
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import android.security.keystore.KeyGenParameterSpec
|
|
7
|
+
import android.security.keystore.KeyInfo
|
|
8
|
+
import android.security.keystore.KeyProperties
|
|
9
|
+
import android.security.keystore.StrongBoxUnavailableException
|
|
10
|
+
import java.security.KeyFactory
|
|
11
|
+
import java.security.KeyPairGenerator
|
|
12
|
+
import java.security.KeyStore
|
|
13
|
+
import java.security.PrivateKey
|
|
14
|
+
import java.security.ProviderException
|
|
15
|
+
import java.security.interfaces.ECPublicKey
|
|
16
|
+
import java.security.spec.ECGenParameterSpec
|
|
17
|
+
import java.util.Calendar
|
|
18
|
+
import javax.security.auth.x500.X500Principal
|
|
19
|
+
|
|
20
|
+
internal data class KeyPairReference(
|
|
21
|
+
val privateKey: PrivateKey,
|
|
22
|
+
val publicKey: ECPublicKey
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
internal data class KeyStoreKeyInfo(
|
|
26
|
+
val alias: String,
|
|
27
|
+
val algorithm: String,
|
|
28
|
+
val curve: String,
|
|
29
|
+
val insideSecureHardware: Boolean,
|
|
30
|
+
val securityLevel: Int?,
|
|
31
|
+
val strongBoxAvailable: Boolean,
|
|
32
|
+
val strongBoxBacked: Boolean
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
internal class DPoPKeyStore(private val context: Context) {
|
|
36
|
+
companion object {
|
|
37
|
+
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
|
|
38
|
+
private const val EC_CURVE = "secp256r1"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private val keyStore: KeyStore by lazy {
|
|
42
|
+
KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fun deleteKeyPair(alias: String) {
|
|
46
|
+
if (keyStore.containsAlias(alias)) {
|
|
47
|
+
keyStore.deleteEntry(alias)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fun generateKeyPair(alias: String): Boolean {
|
|
52
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
53
|
+
throw IllegalStateException("Key pair generation is not supported on API < 23")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (keyStore.containsAlias(alias)) {
|
|
57
|
+
keyStore.deleteEntry(alias)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
val generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, KEYSTORE_PROVIDER)
|
|
61
|
+
if (isStrongBoxEnabled()) {
|
|
62
|
+
try {
|
|
63
|
+
generator.initialize(buildSpec(alias, useStrongBox = true))
|
|
64
|
+
generator.generateKeyPair()
|
|
65
|
+
return true
|
|
66
|
+
} catch (_: StrongBoxUnavailableException) {
|
|
67
|
+
// Fallback to hardware-backed keystore when StrongBox is unavailable.
|
|
68
|
+
} catch (_: ProviderException) {
|
|
69
|
+
// Some devices expose StrongBox but fail during generation.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
generator.initialize(buildSpec(alias, useStrongBox = false))
|
|
74
|
+
generator.generateKeyPair()
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fun getKeyPair(alias: String): KeyPairReference {
|
|
79
|
+
val privateKey = keyStore.getKey(alias, null) as? PrivateKey
|
|
80
|
+
?: throw IllegalStateException("Private key not found for alias: $alias")
|
|
81
|
+
val publicKey = keyStore.getCertificate(alias)?.publicKey as? ECPublicKey
|
|
82
|
+
?: throw IllegalStateException("Key pair not found for alias: $alias")
|
|
83
|
+
return KeyPairReference(privateKey = privateKey, publicKey = publicKey)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fun hasKeyPair(alias: String): Boolean {
|
|
87
|
+
val privateKey = keyStore.getKey(alias, null) as? PrivateKey
|
|
88
|
+
val publicKey = keyStore.getCertificate(alias)?.publicKey as? ECPublicKey
|
|
89
|
+
return privateKey != null && publicKey != null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fun getKeyInfo(alias: String): KeyStoreKeyInfo {
|
|
93
|
+
val keyPair = getKeyPair(alias)
|
|
94
|
+
val keyFactory = KeyFactory.getInstance(keyPair.privateKey.algorithm, KEYSTORE_PROVIDER)
|
|
95
|
+
val keyInfo = keyFactory.getKeySpec(keyPair.privateKey, KeyInfo::class.java)
|
|
96
|
+
|
|
97
|
+
return KeyStoreKeyInfo(
|
|
98
|
+
alias = alias,
|
|
99
|
+
algorithm = keyPair.privateKey.algorithm,
|
|
100
|
+
curve = "P-256",
|
|
101
|
+
insideSecureHardware = keyInfo.isInsideSecureHardware,
|
|
102
|
+
securityLevel = readSecurityLevel(keyInfo),
|
|
103
|
+
strongBoxAvailable = isStrongBoxEnabled(),
|
|
104
|
+
strongBoxBacked = readStrongBoxBacked(keyInfo)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fun isHardwareBacked(alias: String): Boolean {
|
|
109
|
+
val keyPair = getKeyPair(alias)
|
|
110
|
+
val keyFactory = KeyFactory.getInstance(keyPair.privateKey.algorithm, KEYSTORE_PROVIDER)
|
|
111
|
+
val keyInfo = keyFactory.getKeySpec(keyPair.privateKey, KeyInfo::class.java)
|
|
112
|
+
return keyInfo.isInsideSecureHardware
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private fun buildSpec(alias: String, useStrongBox: Boolean): KeyGenParameterSpec {
|
|
116
|
+
val principal = X500Principal("CN=$alias")
|
|
117
|
+
val start = Calendar.getInstance()
|
|
118
|
+
val end = Calendar.getInstance().apply { add(Calendar.YEAR, 25) }
|
|
119
|
+
|
|
120
|
+
val builder = KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_SIGN)
|
|
121
|
+
.setAlgorithmParameterSpec(ECGenParameterSpec(EC_CURVE))
|
|
122
|
+
.setDigests(KeyProperties.DIGEST_SHA256)
|
|
123
|
+
.setCertificateSubject(principal)
|
|
124
|
+
.setCertificateNotBefore(start.time)
|
|
125
|
+
.setCertificateNotAfter(end.time)
|
|
126
|
+
.setUserAuthenticationRequired(false)
|
|
127
|
+
|
|
128
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
129
|
+
builder.setUnlockedDeviceRequired(true)
|
|
130
|
+
if (useStrongBox) {
|
|
131
|
+
builder.setIsStrongBoxBacked(true)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return builder.build()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private fun isStrongBoxEnabled(): Boolean {
|
|
139
|
+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
|
140
|
+
context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private fun readSecurityLevel(keyInfo: KeyInfo): Int? {
|
|
144
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return try {
|
|
149
|
+
val method = KeyInfo::class.java.getMethod("getSecurityLevel")
|
|
150
|
+
method.invoke(keyInfo) as? Int
|
|
151
|
+
} catch (_: Exception) {
|
|
152
|
+
null
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private fun readStrongBoxBacked(keyInfo: KeyInfo): Boolean {
|
|
157
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return try {
|
|
162
|
+
val method = KeyInfo::class.java.getMethod("isStrongBoxBacked")
|
|
163
|
+
method.invoke(keyInfo) as? Boolean ?: false
|
|
164
|
+
} catch (_: Exception) {
|
|
165
|
+
false
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
package com.dpop
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.ReadableArray
|
|
5
|
+
import com.facebook.react.bridge.ReadableMap
|
|
6
|
+
import com.facebook.react.bridge.ReadableType
|
|
7
|
+
import com.facebook.react.bridge.WritableArray
|
|
8
|
+
import com.facebook.react.bridge.WritableMap
|
|
9
|
+
import java.math.BigInteger
|
|
10
|
+
import java.security.MessageDigest
|
|
11
|
+
import java.security.interfaces.ECPublicKey
|
|
12
|
+
import java.util.Base64
|
|
13
|
+
import org.json.JSONArray
|
|
14
|
+
import org.json.JSONObject
|
|
15
|
+
|
|
16
|
+
internal object DPoPUtils {
|
|
17
|
+
internal fun base64UrlEncode(input: ByteArray): String {
|
|
18
|
+
return Base64.getUrlEncoder().withoutPadding().encodeToString(input)
|
|
19
|
+
}
|
|
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
|
+
internal fun derToJose(derSignature: ByteArray, partLength: Int = 32): ByteArray {
|
|
28
|
+
if (derSignature.isEmpty() || derSignature[0].toInt() != 0x30) {
|
|
29
|
+
throw IllegalArgumentException("Invalid DER signature format")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var index = 1
|
|
33
|
+
val sequenceLength = readDerLength(derSignature, index)
|
|
34
|
+
index += sequenceLength.second
|
|
35
|
+
|
|
36
|
+
if (sequenceLength.first <= 0 || index + sequenceLength.first > derSignature.size) {
|
|
37
|
+
throw IllegalArgumentException("Invalid DER sequence length")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (derSignature[index].toInt() != 0x02) {
|
|
41
|
+
throw IllegalArgumentException("Invalid DER integer marker for R")
|
|
42
|
+
}
|
|
43
|
+
index += 1
|
|
44
|
+
val rLength = readDerLength(derSignature, index)
|
|
45
|
+
index += rLength.second
|
|
46
|
+
val rBytes = derSignature.copyOfRange(index, index + rLength.first)
|
|
47
|
+
index += rLength.first
|
|
48
|
+
|
|
49
|
+
if (derSignature[index].toInt() != 0x02) {
|
|
50
|
+
throw IllegalArgumentException("Invalid DER integer marker for S")
|
|
51
|
+
}
|
|
52
|
+
index += 1
|
|
53
|
+
val sLength = readDerLength(derSignature, index)
|
|
54
|
+
index += sLength.second
|
|
55
|
+
val sBytes = derSignature.copyOfRange(index, index + sLength.first)
|
|
56
|
+
|
|
57
|
+
val r = toUnsignedFixedLength(BigInteger(1, rBytes), partLength)
|
|
58
|
+
val s = toUnsignedFixedLength(BigInteger(1, sBytes), partLength)
|
|
59
|
+
return r + s
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
internal fun getPublicCoordinates(publicKey: ECPublicKey): Pair<String, String> {
|
|
63
|
+
val x = base64UrlEncode(toUnsignedFixedLength(publicKey.w.affineX, 32))
|
|
64
|
+
val y = base64UrlEncode(toUnsignedFixedLength(publicKey.w.affineY, 32))
|
|
65
|
+
return Pair(x, y)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
internal fun hashAccessToken(accessToken: String): String {
|
|
69
|
+
val hash = MessageDigest.getInstance("SHA-256").digest(accessToken.toByteArray(Charsets.UTF_8))
|
|
70
|
+
return base64UrlEncode(hash)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
internal fun isProofBoundToPublicKey(proof: String, publicKey: ECPublicKey): Boolean {
|
|
74
|
+
val segments = proof.split(".")
|
|
75
|
+
if (segments.size != 3) {
|
|
76
|
+
throw IllegalArgumentException("Invalid DPoP proof format")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
val headerBytes = Base64.getUrlDecoder().decode(padBase64Url(segments[0]))
|
|
80
|
+
val header = JSONObject(String(headerBytes, Charsets.UTF_8))
|
|
81
|
+
val jwk = header.optJSONObject("jwk")
|
|
82
|
+
?: throw IllegalArgumentException("DPoP proof header does not contain jwk")
|
|
83
|
+
|
|
84
|
+
val coordinates = getPublicCoordinates(publicKey)
|
|
85
|
+
return (
|
|
86
|
+
jwk.optString("kty") == "EC" &&
|
|
87
|
+
jwk.optString("crv") == "P-256" &&
|
|
88
|
+
jwk.optString("x") == coordinates.first &&
|
|
89
|
+
jwk.optString("y") == coordinates.second
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
internal fun readableMapToWritableMap(map: ReadableMap): WritableMap {
|
|
94
|
+
val result = Arguments.createMap()
|
|
95
|
+
val iterator = map.keySetIterator()
|
|
96
|
+
while (iterator.hasNextKey()) {
|
|
97
|
+
val key = iterator.nextKey()
|
|
98
|
+
when (map.getType(key)) {
|
|
99
|
+
ReadableType.Array -> result.putArray(key, readableArrayToWritableArray(map.getArray(key)!!))
|
|
100
|
+
ReadableType.Boolean -> result.putBoolean(key, map.getBoolean(key))
|
|
101
|
+
ReadableType.Map -> result.putMap(key, readableMapToWritableMap(map.getMap(key)!!))
|
|
102
|
+
ReadableType.Null -> result.putNull(key)
|
|
103
|
+
ReadableType.Number -> result.putDouble(key, map.getDouble(key))
|
|
104
|
+
ReadableType.String -> result.putString(key, map.getString(key))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
internal fun toJsonObject(map: ReadableMap): JSONObject {
|
|
111
|
+
val result = JSONObject()
|
|
112
|
+
val iterator = map.keySetIterator()
|
|
113
|
+
while (iterator.hasNextKey()) {
|
|
114
|
+
val key = iterator.nextKey()
|
|
115
|
+
when (map.getType(key)) {
|
|
116
|
+
ReadableType.Array -> result.put(key, toJsonArray(map.getArray(key)!!))
|
|
117
|
+
ReadableType.Boolean -> result.put(key, map.getBoolean(key))
|
|
118
|
+
ReadableType.Map -> result.put(key, toJsonObject(map.getMap(key)!!))
|
|
119
|
+
ReadableType.Null -> result.put(key, JSONObject.NULL)
|
|
120
|
+
ReadableType.Number -> result.put(key, map.getDouble(key))
|
|
121
|
+
ReadableType.String -> result.put(key, map.getString(key))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
internal fun toRawPublicKey(publicKey: ECPublicKey): ByteArray {
|
|
128
|
+
val x = toUnsignedFixedLength(publicKey.w.affineX, 32)
|
|
129
|
+
val y = toUnsignedFixedLength(publicKey.w.affineY, 32)
|
|
130
|
+
return byteArrayOf(0x04) + x + y
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fun readableArrayToWritableArray(array: ReadableArray): WritableArray {
|
|
134
|
+
val result = Arguments.createArray()
|
|
135
|
+
for (index in 0 until array.size()) {
|
|
136
|
+
when (array.getType(index)) {
|
|
137
|
+
ReadableType.Array -> result.pushArray(readableArrayToWritableArray(array.getArray(index)!!))
|
|
138
|
+
ReadableType.Boolean -> result.pushBoolean(array.getBoolean(index))
|
|
139
|
+
ReadableType.Map -> result.pushMap(readableMapToWritableMap(array.getMap(index)!!))
|
|
140
|
+
ReadableType.Null -> result.pushNull()
|
|
141
|
+
ReadableType.Number -> result.pushDouble(array.getDouble(index))
|
|
142
|
+
ReadableType.String -> result.pushString(array.getString(index))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return result
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private fun toJsonArray(array: ReadableArray): JSONArray {
|
|
149
|
+
val result = JSONArray()
|
|
150
|
+
for (index in 0 until array.size()) {
|
|
151
|
+
when (array.getType(index)) {
|
|
152
|
+
ReadableType.Array -> result.put(toJsonArray(array.getArray(index)!!))
|
|
153
|
+
ReadableType.Boolean -> result.put(array.getBoolean(index))
|
|
154
|
+
ReadableType.Map -> result.put(toJsonObject(array.getMap(index)!!))
|
|
155
|
+
ReadableType.Null -> result.put(JSONObject.NULL)
|
|
156
|
+
ReadableType.Number -> result.put(array.getDouble(index))
|
|
157
|
+
ReadableType.String -> result.put(array.getString(index))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private fun padBase64Url(input: String): String {
|
|
164
|
+
val remainder = input.length % 4
|
|
165
|
+
return if (remainder == 0) {
|
|
166
|
+
input
|
|
167
|
+
} else {
|
|
168
|
+
input + "=".repeat(4 - remainder)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private fun readDerLength(input: ByteArray, startIndex: Int): Pair<Int, Int> {
|
|
173
|
+
val first = input[startIndex].toInt() and 0xFF
|
|
174
|
+
if ((first and 0x80) == 0) {
|
|
175
|
+
return Pair(first, 1)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
val lengthBytesCount = first and 0x7F
|
|
179
|
+
if (lengthBytesCount == 0 || lengthBytesCount > 4) {
|
|
180
|
+
throw IllegalArgumentException("Invalid DER length")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
var length = 0
|
|
184
|
+
for (i in 0 until lengthBytesCount) {
|
|
185
|
+
length = (length shl 8) or (input[startIndex + 1 + i].toInt() and 0xFF)
|
|
186
|
+
}
|
|
187
|
+
return Pair(length, 1 + lengthBytesCount)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private fun toUnsignedFixedLength(value: BigInteger, length: Int): ByteArray {
|
|
191
|
+
val signed = value.toByteArray()
|
|
192
|
+
if (signed.size == length) {
|
|
193
|
+
return signed
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (signed.size == length + 1 && signed[0].toInt() == 0) {
|
|
197
|
+
return signed.copyOfRange(1, signed.size)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (signed.size < length) {
|
|
201
|
+
val output = ByteArray(length)
|
|
202
|
+
System.arraycopy(signed, 0, output, length - signed.size, signed.size)
|
|
203
|
+
return output
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw IllegalArgumentException("Coordinate is larger than expected length")
|
|
207
|
+
}
|
|
208
|
+
}
|