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 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,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -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
+ }