@avieldr/react-native-rsa 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/LICENSE +20 -0
- package/README.md +453 -0
- package/Rsa.podspec +23 -0
- package/android/build.gradle +69 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/rsa/RsaModule.kt +129 -0
- package/android/src/main/java/com/rsa/RsaPackage.kt +33 -0
- package/android/src/main/java/com/rsa/core/ASN1Utils.kt +201 -0
- package/android/src/main/java/com/rsa/core/Algorithms.kt +126 -0
- package/android/src/main/java/com/rsa/core/KeyUtils.kt +83 -0
- package/android/src/main/java/com/rsa/core/RSACipher.kt +71 -0
- package/android/src/main/java/com/rsa/core/RSAKeyGenerator.kt +125 -0
- package/android/src/main/java/com/rsa/core/RSASigner.kt +70 -0
- package/ios/ASN1Utils.swift +225 -0
- package/ios/Algorithms.swift +89 -0
- package/ios/KeyUtils.swift +125 -0
- package/ios/RSACipher.swift +77 -0
- package/ios/RSAKeyGenerator.swift +164 -0
- package/ios/RSASigner.swift +101 -0
- package/ios/Rsa.h +61 -0
- package/ios/Rsa.mm +216 -0
- package/lib/module/NativeRsa.js +16 -0
- package/lib/module/NativeRsa.js.map +1 -0
- package/lib/module/constants.js +24 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/encoding.js +116 -0
- package/lib/module/encoding.js.map +1 -0
- package/lib/module/errors.js +135 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +232 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/keyInfo.js +286 -0
- package/lib/module/keyInfo.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeRsa.d.ts +32 -0
- package/lib/typescript/src/NativeRsa.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +21 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/encoding.d.ts +30 -0
- package/lib/typescript/src/encoding.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +47 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +122 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/keyInfo.d.ts +7 -0
- package/lib/typescript/src/keyInfo.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +63 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +133 -0
- package/src/NativeRsa.ts +59 -0
- package/src/constants.ts +25 -0
- package/src/encoding.ts +139 -0
- package/src/errors.ts +206 -0
- package/src/index.ts +305 -0
- package/src/keyInfo.ts +334 -0
- package/src/types.ts +85 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.rsa
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class RsaPackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == RsaModule.NAME) {
|
|
13
|
+
RsaModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
20
|
+
return ReactModuleInfoProvider {
|
|
21
|
+
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
|
|
22
|
+
moduleInfos[RsaModule.NAME] = ReactModuleInfo(
|
|
23
|
+
RsaModule.NAME,
|
|
24
|
+
RsaModule.NAME,
|
|
25
|
+
false, // canOverrideExistingModule
|
|
26
|
+
false, // needsEagerInit
|
|
27
|
+
false, // isCxxModule
|
|
28
|
+
true // isTurboModule
|
|
29
|
+
)
|
|
30
|
+
moduleInfos
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
package com.rsa.core
|
|
2
|
+
|
|
3
|
+
import java.io.ByteArrayOutputStream
|
|
4
|
+
import java.math.BigInteger
|
|
5
|
+
import java.security.interfaces.RSAPrivateCrtKey
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ASN.1 DER encoding/decoding utilities for RSA key format conversions.
|
|
9
|
+
*
|
|
10
|
+
* Handles the low-level binary encoding needed to convert between
|
|
11
|
+
* PKCS#1 and PKCS#8 key formats without external dependencies.
|
|
12
|
+
*
|
|
13
|
+
* ASN.1 (Abstract Syntax Notation One) uses TLV (Tag-Length-Value) encoding:
|
|
14
|
+
* - Tag: identifies the data type (e.g. 0x02 = INTEGER, 0x30 = SEQUENCE)
|
|
15
|
+
* - Length: number of bytes in the value
|
|
16
|
+
* - Value: the actual data bytes
|
|
17
|
+
*/
|
|
18
|
+
object ASN1Utils {
|
|
19
|
+
|
|
20
|
+
// RSA algorithm OID: 1.2.840.113549.1.1.1
|
|
21
|
+
// This identifies the key as RSA in AlgorithmIdentifier structures
|
|
22
|
+
val RSA_OID = byteArrayOf(
|
|
23
|
+
0x06, 0x09, 0x2A.toByte(), 0x86.toByte(), 0x48, 0x86.toByte(),
|
|
24
|
+
0xF7.toByte(), 0x0D, 0x01, 0x01, 0x01
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// ASN.1 NULL value — used as the parameter in AlgorithmIdentifier (RSA has no params)
|
|
28
|
+
val ASN1_NULL = byteArrayOf(0x05, 0x00)
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Encode a BigInteger as an ASN.1 INTEGER (tag 0x02 + length + value).
|
|
32
|
+
* BigInteger.toByteArray() returns a two's-complement representation,
|
|
33
|
+
* which is exactly what DER INTEGER expects.
|
|
34
|
+
*/
|
|
35
|
+
fun encodeAsn1Integer(value: BigInteger): ByteArray {
|
|
36
|
+
val output = ByteArrayOutputStream()
|
|
37
|
+
val bytes = value.toByteArray()
|
|
38
|
+
output.write(0x02) // INTEGER tag
|
|
39
|
+
output.write(encodeAsn1Length(bytes.size))
|
|
40
|
+
output.write(bytes)
|
|
41
|
+
return output.toByteArray()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Encode a length value in ASN.1 DER format.
|
|
46
|
+
*
|
|
47
|
+
* DER length encoding:
|
|
48
|
+
* - 0–127: single byte (short form)
|
|
49
|
+
* - 128–255: 0x81 followed by one byte
|
|
50
|
+
* - 256–65535: 0x82 followed by two bytes (big-endian)
|
|
51
|
+
*/
|
|
52
|
+
fun encodeAsn1Length(length: Int): ByteArray {
|
|
53
|
+
return when {
|
|
54
|
+
length < 128 -> byteArrayOf(length.toByte())
|
|
55
|
+
length < 256 -> byteArrayOf(0x81.toByte(), length.toByte())
|
|
56
|
+
length < 65536 -> byteArrayOf(
|
|
57
|
+
0x82.toByte(),
|
|
58
|
+
(length shr 8).toByte(),
|
|
59
|
+
(length and 0xFF).toByte()
|
|
60
|
+
)
|
|
61
|
+
else -> throw IllegalArgumentException("Length too large: $length")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decode an ASN.1 DER length field starting at [offset] in [bytes].
|
|
67
|
+
*
|
|
68
|
+
* @return Pair of (decoded length or null on error, number of bytes consumed)
|
|
69
|
+
*/
|
|
70
|
+
fun decodeAsn1Length(bytes: ByteArray, offset: Int): Pair<Int?, Int> {
|
|
71
|
+
if (offset >= bytes.size) return Pair(null, 0)
|
|
72
|
+
val first = bytes[offset].toInt() and 0xFF
|
|
73
|
+
if (first < 128) {
|
|
74
|
+
return Pair(first, 1)
|
|
75
|
+
}
|
|
76
|
+
val numBytes = first and 0x7F
|
|
77
|
+
if (numBytes == 0 || offset + numBytes >= bytes.size) return Pair(null, 0)
|
|
78
|
+
var length = 0
|
|
79
|
+
for (i in 0 until numBytes) {
|
|
80
|
+
length = (length shl 8) or (bytes[offset + 1 + i].toInt() and 0xFF)
|
|
81
|
+
}
|
|
82
|
+
return Pair(length, 1 + numBytes)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Encode an RSA private key as PKCS#1 DER.
|
|
87
|
+
*
|
|
88
|
+
* PKCS#1 RSAPrivateKey layout:
|
|
89
|
+
* SEQUENCE {
|
|
90
|
+
* version INTEGER (0),
|
|
91
|
+
* modulus INTEGER, -- n
|
|
92
|
+
* publicExp INTEGER, -- e
|
|
93
|
+
* privateExp INTEGER, -- d
|
|
94
|
+
* prime1 INTEGER, -- p
|
|
95
|
+
* prime2 INTEGER, -- q
|
|
96
|
+
* exp1 INTEGER, -- d mod (p-1)
|
|
97
|
+
* exp2 INTEGER, -- d mod (q-1)
|
|
98
|
+
* coeff INTEGER -- (inverse of q) mod p
|
|
99
|
+
* }
|
|
100
|
+
*/
|
|
101
|
+
fun encodePkcs1RsaPrivateKey(key: RSAPrivateCrtKey): ByteArray {
|
|
102
|
+
val output = ByteArrayOutputStream()
|
|
103
|
+
val sequenceContent = ByteArrayOutputStream()
|
|
104
|
+
|
|
105
|
+
sequenceContent.write(encodeAsn1Integer(BigInteger.ZERO)) // version = 0
|
|
106
|
+
sequenceContent.write(encodeAsn1Integer(key.modulus)) // n
|
|
107
|
+
sequenceContent.write(encodeAsn1Integer(key.publicExponent)) // e
|
|
108
|
+
sequenceContent.write(encodeAsn1Integer(key.privateExponent)) // d
|
|
109
|
+
sequenceContent.write(encodeAsn1Integer(key.primeP)) // p
|
|
110
|
+
sequenceContent.write(encodeAsn1Integer(key.primeQ)) // q
|
|
111
|
+
sequenceContent.write(encodeAsn1Integer(key.primeExponentP)) // d mod (p-1)
|
|
112
|
+
sequenceContent.write(encodeAsn1Integer(key.primeExponentQ)) // d mod (q-1)
|
|
113
|
+
sequenceContent.write(encodeAsn1Integer(key.crtCoefficient)) // (inverse of q) mod p
|
|
114
|
+
|
|
115
|
+
val contentBytes = sequenceContent.toByteArray()
|
|
116
|
+
output.write(0x30) // SEQUENCE tag
|
|
117
|
+
output.write(encodeAsn1Length(contentBytes.size))
|
|
118
|
+
output.write(contentBytes)
|
|
119
|
+
|
|
120
|
+
return output.toByteArray()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Wrap PKCS#1 private key bytes inside a PKCS#8 PrivateKeyInfo structure.
|
|
125
|
+
*
|
|
126
|
+
* PKCS#8 PrivateKeyInfo layout:
|
|
127
|
+
* SEQUENCE {
|
|
128
|
+
* version INTEGER (0),
|
|
129
|
+
* algorithm AlgorithmIdentifier { OID, NULL },
|
|
130
|
+
* privateKey OCTET STRING containing PKCS#1 bytes
|
|
131
|
+
* }
|
|
132
|
+
*/
|
|
133
|
+
fun wrapPkcs1InPkcs8(pkcs1Bytes: ByteArray): ByteArray {
|
|
134
|
+
val output = ByteArrayOutputStream()
|
|
135
|
+
val sequenceContent = ByteArrayOutputStream()
|
|
136
|
+
|
|
137
|
+
// version INTEGER 0
|
|
138
|
+
sequenceContent.write(encodeAsn1Integer(BigInteger.ZERO))
|
|
139
|
+
|
|
140
|
+
// AlgorithmIdentifier SEQUENCE { rsaOID, NULL }
|
|
141
|
+
val algIdContent = ByteArrayOutputStream()
|
|
142
|
+
algIdContent.write(RSA_OID)
|
|
143
|
+
algIdContent.write(ASN1_NULL)
|
|
144
|
+
val algIdBytes = algIdContent.toByteArray()
|
|
145
|
+
sequenceContent.write(0x30) // SEQUENCE tag
|
|
146
|
+
sequenceContent.write(encodeAsn1Length(algIdBytes.size))
|
|
147
|
+
sequenceContent.write(algIdBytes)
|
|
148
|
+
|
|
149
|
+
// privateKey OCTET STRING wrapping the raw PKCS#1 bytes
|
|
150
|
+
sequenceContent.write(0x04) // OCTET STRING tag
|
|
151
|
+
sequenceContent.write(encodeAsn1Length(pkcs1Bytes.size))
|
|
152
|
+
sequenceContent.write(pkcs1Bytes)
|
|
153
|
+
|
|
154
|
+
val contentBytes = sequenceContent.toByteArray()
|
|
155
|
+
output.write(0x30) // outer SEQUENCE tag
|
|
156
|
+
output.write(encodeAsn1Length(contentBytes.size))
|
|
157
|
+
output.write(contentBytes)
|
|
158
|
+
|
|
159
|
+
return output.toByteArray()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Extract the inner PKCS#1 private key bytes from a PKCS#8 wrapper.
|
|
164
|
+
*
|
|
165
|
+
* Walks the PKCS#8 structure:
|
|
166
|
+
* SEQUENCE → skip version INTEGER → skip AlgorithmIdentifier SEQUENCE → read OCTET STRING
|
|
167
|
+
*
|
|
168
|
+
* Returns the original bytes unchanged if parsing fails.
|
|
169
|
+
*/
|
|
170
|
+
fun extractPkcs1FromPkcs8(pkcs8Bytes: ByteArray): ByteArray {
|
|
171
|
+
var offset = 0
|
|
172
|
+
|
|
173
|
+
// Outer SEQUENCE
|
|
174
|
+
if (offset >= pkcs8Bytes.size || pkcs8Bytes[offset] != 0x30.toByte()) return pkcs8Bytes
|
|
175
|
+
offset++
|
|
176
|
+
val (_, seqLenBytes) = decodeAsn1Length(pkcs8Bytes, offset)
|
|
177
|
+
offset += seqLenBytes
|
|
178
|
+
|
|
179
|
+
// version INTEGER — skip over it
|
|
180
|
+
if (offset >= pkcs8Bytes.size || pkcs8Bytes[offset] != 0x02.toByte()) return pkcs8Bytes
|
|
181
|
+
offset++
|
|
182
|
+
val (verLen, verLenBytes) = decodeAsn1Length(pkcs8Bytes, offset)
|
|
183
|
+
offset += verLenBytes + (verLen ?: 0)
|
|
184
|
+
|
|
185
|
+
// AlgorithmIdentifier SEQUENCE — skip over it
|
|
186
|
+
if (offset >= pkcs8Bytes.size || pkcs8Bytes[offset] != 0x30.toByte()) return pkcs8Bytes
|
|
187
|
+
offset++
|
|
188
|
+
val (algLen, algLenBytes) = decodeAsn1Length(pkcs8Bytes, offset)
|
|
189
|
+
offset += algLenBytes + (algLen ?: 0)
|
|
190
|
+
|
|
191
|
+
// OCTET STRING containing the PKCS#1 private key
|
|
192
|
+
if (offset >= pkcs8Bytes.size || pkcs8Bytes[offset] != 0x04.toByte()) return pkcs8Bytes
|
|
193
|
+
offset++
|
|
194
|
+
val (octetLen, octetLenBytes) = decodeAsn1Length(pkcs8Bytes, offset)
|
|
195
|
+
offset += octetLenBytes
|
|
196
|
+
|
|
197
|
+
val len = octetLen ?: return pkcs8Bytes
|
|
198
|
+
if (offset + len > pkcs8Bytes.size) return pkcs8Bytes
|
|
199
|
+
return pkcs8Bytes.copyOfRange(offset, offset + len)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
package com.rsa.core
|
|
2
|
+
|
|
3
|
+
import java.security.spec.AlgorithmParameterSpec
|
|
4
|
+
import java.security.spec.MGF1ParameterSpec
|
|
5
|
+
import java.security.spec.PSSParameterSpec
|
|
6
|
+
import javax.crypto.spec.OAEPParameterSpec
|
|
7
|
+
import javax.crypto.spec.PSource
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Maps (padding, hash) option strings to Java Security / Cipher algorithm configurations.
|
|
11
|
+
*
|
|
12
|
+
* This provides a single lookup point so that RSACipher and RSASigner don't
|
|
13
|
+
* need to duplicate algorithm selection logic.
|
|
14
|
+
*
|
|
15
|
+
* Supported combinations:
|
|
16
|
+
* Cipher (encrypt/decrypt):
|
|
17
|
+
* - "pkcs1" → RSA/ECB/PKCS1Padding (hash is ignored)
|
|
18
|
+
* - "oaep" → RSA/ECB/OAEPPadding with explicit OAEPParameterSpec
|
|
19
|
+
*
|
|
20
|
+
* Signature (sign/verify):
|
|
21
|
+
* - "pkcs1" → SHA{N}withRSA
|
|
22
|
+
* - "pss" → SHA{N}withRSA/PSS with explicit PSSParameterSpec
|
|
23
|
+
*/
|
|
24
|
+
object Algorithms {
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolved cipher algorithm: the Java transformation string plus optional params.
|
|
28
|
+
*/
|
|
29
|
+
data class CipherAlgorithm(
|
|
30
|
+
val transformation: String,
|
|
31
|
+
val params: AlgorithmParameterSpec?
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolved signature algorithm: the Java algorithm name plus optional params.
|
|
36
|
+
*/
|
|
37
|
+
data class SignatureAlgorithm(
|
|
38
|
+
val name: String,
|
|
39
|
+
val params: AlgorithmParameterSpec?
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Look up the Java Cipher transformation and parameters for an encrypt/decrypt operation.
|
|
44
|
+
*
|
|
45
|
+
* @param padding "pkcs1" or "oaep"
|
|
46
|
+
* @param hash "sha1", "sha256", "sha384", or "sha512" (only used with OAEP)
|
|
47
|
+
*/
|
|
48
|
+
fun getCipherAlgorithm(padding: String, hash: String): CipherAlgorithm {
|
|
49
|
+
return when (padding) {
|
|
50
|
+
"pkcs1" -> CipherAlgorithm("RSA/ECB/PKCS1Padding", null)
|
|
51
|
+
"oaep" -> {
|
|
52
|
+
val (hashName, mgf1Spec) = getHashParams(hash)
|
|
53
|
+
// Explicit OAEPParameterSpec is needed to support all hash algorithms.
|
|
54
|
+
// The default "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" always uses SHA-1 for MGF1.
|
|
55
|
+
CipherAlgorithm(
|
|
56
|
+
"RSA/ECB/OAEPPadding",
|
|
57
|
+
OAEPParameterSpec(hashName, "MGF1", mgf1Spec, PSource.PSpecified.DEFAULT)
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
else -> throw IllegalArgumentException("Unsupported encryption padding: $padding")
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Look up the Java Signature algorithm name and parameters for a sign/verify operation.
|
|
66
|
+
*
|
|
67
|
+
* @param padding "pkcs1" or "pss"
|
|
68
|
+
* @param hash "sha1", "sha256", "sha384", or "sha512"
|
|
69
|
+
*/
|
|
70
|
+
fun getSignatureAlgorithm(padding: String, hash: String): SignatureAlgorithm {
|
|
71
|
+
return when (padding) {
|
|
72
|
+
"pkcs1" -> {
|
|
73
|
+
// Deterministic PKCS#1 v1.5 signatures — no additional params needed
|
|
74
|
+
val algName = when (hash) {
|
|
75
|
+
"sha1" -> "SHA1withRSA"
|
|
76
|
+
"sha256" -> "SHA256withRSA"
|
|
77
|
+
"sha384" -> "SHA384withRSA"
|
|
78
|
+
"sha512" -> "SHA512withRSA"
|
|
79
|
+
else -> throw IllegalArgumentException("Unsupported hash: $hash")
|
|
80
|
+
}
|
|
81
|
+
SignatureAlgorithm(algName, null)
|
|
82
|
+
}
|
|
83
|
+
"pss" -> {
|
|
84
|
+
// PSS signatures require explicit parameter spec (salt length = hash output length)
|
|
85
|
+
val (hashName, mgf1Spec, saltLen) = getHashParamsWithSalt(hash)
|
|
86
|
+
val algName = when (hash) {
|
|
87
|
+
"sha1" -> "SHA1withRSA/PSS"
|
|
88
|
+
"sha256" -> "SHA256withRSA/PSS"
|
|
89
|
+
"sha384" -> "SHA384withRSA/PSS"
|
|
90
|
+
"sha512" -> "SHA512withRSA/PSS"
|
|
91
|
+
else -> throw IllegalArgumentException("Unsupported hash: $hash")
|
|
92
|
+
}
|
|
93
|
+
SignatureAlgorithm(
|
|
94
|
+
algName,
|
|
95
|
+
PSSParameterSpec(hashName, "MGF1", mgf1Spec, saltLen, 1)
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
else -> throw IllegalArgumentException("Unsupported signature padding: $padding")
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- Internal helpers ---
|
|
103
|
+
|
|
104
|
+
/** Hash name and MGF1 parameter spec for a given hash algorithm. */
|
|
105
|
+
private fun getHashParams(hash: String): Pair<String, MGF1ParameterSpec> {
|
|
106
|
+
return when (hash) {
|
|
107
|
+
"sha1" -> Pair("SHA-1", MGF1ParameterSpec.SHA1)
|
|
108
|
+
"sha256" -> Pair("SHA-256", MGF1ParameterSpec.SHA256)
|
|
109
|
+
"sha384" -> Pair("SHA-384", MGF1ParameterSpec.SHA384)
|
|
110
|
+
"sha512" -> Pair("SHA-512", MGF1ParameterSpec.SHA512)
|
|
111
|
+
else -> throw IllegalArgumentException("Unsupported hash: $hash")
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Hash name, MGF1 spec, and salt length (= hash output size in bytes) for PSS. */
|
|
116
|
+
private data class HashParamsWithSalt(val hashName: String, val mgf1Spec: MGF1ParameterSpec, val saltLen: Int)
|
|
117
|
+
private fun getHashParamsWithSalt(hash: String): HashParamsWithSalt {
|
|
118
|
+
return when (hash) {
|
|
119
|
+
"sha1" -> HashParamsWithSalt("SHA-1", MGF1ParameterSpec.SHA1, 20)
|
|
120
|
+
"sha256" -> HashParamsWithSalt("SHA-256", MGF1ParameterSpec.SHA256, 32)
|
|
121
|
+
"sha384" -> HashParamsWithSalt("SHA-384", MGF1ParameterSpec.SHA384, 48)
|
|
122
|
+
"sha512" -> HashParamsWithSalt("SHA-512", MGF1ParameterSpec.SHA512, 64)
|
|
123
|
+
else -> throw IllegalArgumentException("Unsupported hash: $hash")
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package com.rsa.core
|
|
2
|
+
|
|
3
|
+
import java.security.KeyFactory
|
|
4
|
+
import java.security.PublicKey
|
|
5
|
+
import java.security.interfaces.RSAPrivateCrtKey
|
|
6
|
+
import java.security.spec.PKCS8EncodedKeySpec
|
|
7
|
+
import java.security.spec.X509EncodedKeySpec
|
|
8
|
+
import java.util.Base64
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* PEM parsing and key loading utilities for Android.
|
|
12
|
+
*
|
|
13
|
+
* Handles conversion between PEM-encoded key strings and Java security key objects.
|
|
14
|
+
* Supports both PKCS#1 ("BEGIN RSA PRIVATE KEY") and PKCS#8 ("BEGIN PRIVATE KEY")
|
|
15
|
+
* private key formats, and SPKI ("BEGIN PUBLIC KEY") public key format.
|
|
16
|
+
*/
|
|
17
|
+
object KeyUtils {
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract the base64-encoded content from a PEM string,
|
|
21
|
+
* stripping the header/footer lines and all whitespace.
|
|
22
|
+
*/
|
|
23
|
+
fun extractBase64(pem: String): String {
|
|
24
|
+
return pem.lines()
|
|
25
|
+
.filter { !it.startsWith("-----") }
|
|
26
|
+
.joinToString("")
|
|
27
|
+
.replace("\\s".toRegex(), "")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format raw DER bytes as a PEM string with the given header.
|
|
32
|
+
*
|
|
33
|
+
* @param derBytes The raw DER-encoded key bytes
|
|
34
|
+
* @param header The PEM header label (e.g. "RSA PRIVATE KEY", "PRIVATE KEY", "PUBLIC KEY")
|
|
35
|
+
* @return A properly formatted PEM string with 64-char line wrapping
|
|
36
|
+
*/
|
|
37
|
+
fun toPEM(derBytes: ByteArray, header: String): String {
|
|
38
|
+
val base64Key = Base64.getEncoder().encodeToString(derBytes)
|
|
39
|
+
return "-----BEGIN $header-----\n" +
|
|
40
|
+
base64Key.chunked(64).joinToString("\n") +
|
|
41
|
+
"\n-----END $header-----"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load an RSA private key from a PEM string.
|
|
46
|
+
*
|
|
47
|
+
* Accepts both PKCS#1 and PKCS#8 formats:
|
|
48
|
+
* - PKCS#1 ("BEGIN RSA PRIVATE KEY"): wraps in PKCS#8 first, since Java's
|
|
49
|
+
* KeyFactory only accepts PKCS#8 (PKCS8EncodedKeySpec)
|
|
50
|
+
* - PKCS#8 ("BEGIN PRIVATE KEY"): used directly
|
|
51
|
+
*
|
|
52
|
+
* @return The private key as RSAPrivateCrtKey (gives access to CRT components)
|
|
53
|
+
*/
|
|
54
|
+
fun loadPrivateKey(pem: String): RSAPrivateCrtKey {
|
|
55
|
+
val keyFactory = KeyFactory.getInstance("RSA")
|
|
56
|
+
val base64 = extractBase64(pem)
|
|
57
|
+
val derBytes = Base64.getDecoder().decode(base64)
|
|
58
|
+
|
|
59
|
+
// Java only accepts PKCS#8, so wrap PKCS#1 if needed
|
|
60
|
+
val pkcs8Bytes = if (pem.contains("BEGIN RSA PRIVATE KEY")) {
|
|
61
|
+
ASN1Utils.wrapPkcs1InPkcs8(derBytes)
|
|
62
|
+
} else {
|
|
63
|
+
derBytes
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
val spec = PKCS8EncodedKeySpec(pkcs8Bytes)
|
|
67
|
+
return keyFactory.generatePrivate(spec) as RSAPrivateCrtKey
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Load an RSA public key from a PEM string (SPKI/X.509 format).
|
|
72
|
+
*
|
|
73
|
+
* Expects "BEGIN PUBLIC KEY" (SPKI) format, which is what Java's
|
|
74
|
+
* X509EncodedKeySpec accepts directly.
|
|
75
|
+
*/
|
|
76
|
+
fun loadPublicKey(pem: String): PublicKey {
|
|
77
|
+
val keyFactory = KeyFactory.getInstance("RSA")
|
|
78
|
+
val base64 = extractBase64(pem)
|
|
79
|
+
val derBytes = Base64.getDecoder().decode(base64)
|
|
80
|
+
val spec = X509EncodedKeySpec(derBytes)
|
|
81
|
+
return keyFactory.generatePublic(spec)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package com.rsa.core
|
|
2
|
+
|
|
3
|
+
import java.util.Base64
|
|
4
|
+
import javax.crypto.Cipher
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* RSA encryption and decryption operations for Android.
|
|
8
|
+
*
|
|
9
|
+
* Uses `javax.crypto.Cipher` with algorithm configurations from [Algorithms].
|
|
10
|
+
* All data crosses the bridge as base64 strings to avoid binary encoding issues.
|
|
11
|
+
*
|
|
12
|
+
* Supported padding modes:
|
|
13
|
+
* - "oaep" — OAEP (Optimal Asymmetric Encryption Padding) with configurable hash.
|
|
14
|
+
* Uses explicit OAEPParameterSpec to ensure the correct hash is used
|
|
15
|
+
* for both the message digest and MGF1 mask generation.
|
|
16
|
+
* - "pkcs1" — PKCS#1 v1.5 padding (legacy, not recommended for new applications)
|
|
17
|
+
*/
|
|
18
|
+
object RSACipher {
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Encrypt data with an RSA public key.
|
|
22
|
+
*
|
|
23
|
+
* @param dataBase64 The plaintext data encoded as base64
|
|
24
|
+
* @param publicKeyPEM The public key in SPKI PEM format ("BEGIN PUBLIC KEY")
|
|
25
|
+
* @param padding "oaep" or "pkcs1"
|
|
26
|
+
* @param hash "sha256", "sha1", "sha384", or "sha512" (used with OAEP)
|
|
27
|
+
* @return The ciphertext encoded as base64
|
|
28
|
+
*/
|
|
29
|
+
fun encrypt(dataBase64: String, publicKeyPEM: String, padding: String, hash: String): String {
|
|
30
|
+
val publicKey = KeyUtils.loadPublicKey(publicKeyPEM)
|
|
31
|
+
val data = Base64.getDecoder().decode(dataBase64)
|
|
32
|
+
val algorithm = Algorithms.getCipherAlgorithm(padding, hash)
|
|
33
|
+
|
|
34
|
+
val cipher = Cipher.getInstance(algorithm.transformation)
|
|
35
|
+
if (algorithm.params != null) {
|
|
36
|
+
// OAEP requires explicit parameter spec for correct hash configuration
|
|
37
|
+
cipher.init(Cipher.ENCRYPT_MODE, publicKey, algorithm.params)
|
|
38
|
+
} else {
|
|
39
|
+
// PKCS#1 v1.5 — no additional params needed
|
|
40
|
+
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
val encrypted = cipher.doFinal(data)
|
|
44
|
+
return Base64.getEncoder().encodeToString(encrypted)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Decrypt data with an RSA private key.
|
|
49
|
+
*
|
|
50
|
+
* @param dataBase64 The ciphertext encoded as base64
|
|
51
|
+
* @param privateKeyPEM The private key in PEM format (PKCS#1 or PKCS#8)
|
|
52
|
+
* @param padding "oaep" or "pkcs1" — must match the padding used during encryption
|
|
53
|
+
* @param hash "sha256", "sha1", "sha384", or "sha512" — must match encryption hash
|
|
54
|
+
* @return The decrypted plaintext encoded as base64
|
|
55
|
+
*/
|
|
56
|
+
fun decrypt(dataBase64: String, privateKeyPEM: String, padding: String, hash: String): String {
|
|
57
|
+
val privateKey = KeyUtils.loadPrivateKey(privateKeyPEM)
|
|
58
|
+
val data = Base64.getDecoder().decode(dataBase64)
|
|
59
|
+
val algorithm = Algorithms.getCipherAlgorithm(padding, hash)
|
|
60
|
+
|
|
61
|
+
val cipher = Cipher.getInstance(algorithm.transformation)
|
|
62
|
+
if (algorithm.params != null) {
|
|
63
|
+
cipher.init(Cipher.DECRYPT_MODE, privateKey, algorithm.params)
|
|
64
|
+
} else {
|
|
65
|
+
cipher.init(Cipher.DECRYPT_MODE, privateKey)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
val decrypted = cipher.doFinal(data)
|
|
69
|
+
return Base64.getEncoder().encodeToString(decrypted)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
package com.rsa.core
|
|
2
|
+
|
|
3
|
+
import java.security.KeyFactory
|
|
4
|
+
import java.security.KeyPair
|
|
5
|
+
import java.security.KeyPairGenerator
|
|
6
|
+
import java.security.interfaces.RSAPrivateCrtKey
|
|
7
|
+
import java.security.interfaces.RSAPublicKey
|
|
8
|
+
import java.security.spec.RSAPublicKeySpec
|
|
9
|
+
import java.util.Base64
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Result of RSA key pair generation, containing both keys as PEM strings.
|
|
13
|
+
*/
|
|
14
|
+
data class RSAKeyPairResult(
|
|
15
|
+
val publicKey: String,
|
|
16
|
+
val privateKey: String
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* RSA key generation and conversion operations.
|
|
21
|
+
*
|
|
22
|
+
* Thread-safe singleton that provides:
|
|
23
|
+
* - Key pair generation (PKCS#1 or PKCS#8 private key format)
|
|
24
|
+
* - Public key extraction from a private key
|
|
25
|
+
* - Private key format conversion between PKCS#1 and PKCS#8
|
|
26
|
+
*
|
|
27
|
+
* Delegates ASN.1 encoding to [ASN1Utils] and PEM/key-loading to [KeyUtils].
|
|
28
|
+
*/
|
|
29
|
+
class RSAKeyGenerator {
|
|
30
|
+
|
|
31
|
+
companion object {
|
|
32
|
+
private const val DEFAULT_KEY_SIZE = 2048
|
|
33
|
+
|
|
34
|
+
@Volatile
|
|
35
|
+
private var INSTANCE: RSAKeyGenerator? = null
|
|
36
|
+
|
|
37
|
+
fun getInstance(): RSAKeyGenerator {
|
|
38
|
+
return INSTANCE ?: synchronized(this) {
|
|
39
|
+
INSTANCE ?: RSAKeyGenerator().also { INSTANCE = it }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generate an RSA key pair.
|
|
46
|
+
*
|
|
47
|
+
* @param keySize RSA key size in bits (default: 2048). Supported: 1024, 2048, 4096.
|
|
48
|
+
* @param format "pkcs1" or "pkcs8" — controls the private key output format.
|
|
49
|
+
* Public key is always SPKI/X.509 ("BEGIN PUBLIC KEY").
|
|
50
|
+
* @return RSAKeyPairResult with public and private keys in PEM format
|
|
51
|
+
*/
|
|
52
|
+
fun generateKeyPair(keySize: Int = DEFAULT_KEY_SIZE, format: String = "pkcs1"): RSAKeyPairResult {
|
|
53
|
+
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
|
|
54
|
+
keyPairGenerator.initialize(keySize)
|
|
55
|
+
val keyPair: KeyPair = keyPairGenerator.generateKeyPair()
|
|
56
|
+
|
|
57
|
+
val privateKey = keyPair.private as RSAPrivateCrtKey
|
|
58
|
+
val publicKey = keyPair.public as RSAPublicKey
|
|
59
|
+
|
|
60
|
+
// Encode private key in the requested format
|
|
61
|
+
val privateKeyPEM = when (format) {
|
|
62
|
+
"pkcs8" -> KeyUtils.toPEM(privateKey.encoded, "PRIVATE KEY")
|
|
63
|
+
else -> {
|
|
64
|
+
// Manually encode PKCS#1 since Java only gives us PKCS#8 natively
|
|
65
|
+
val pkcs1Bytes = ASN1Utils.encodePkcs1RsaPrivateKey(privateKey)
|
|
66
|
+
KeyUtils.toPEM(pkcs1Bytes, "RSA PRIVATE KEY")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Public key: Java's key.encoded returns SPKI/X.509 DER directly
|
|
71
|
+
val publicKeyPEM = KeyUtils.toPEM(publicKey.encoded, "PUBLIC KEY")
|
|
72
|
+
|
|
73
|
+
return RSAKeyPairResult(publicKey = publicKeyPEM, privateKey = privateKeyPEM)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract the public key from an RSA private key PEM string.
|
|
78
|
+
*
|
|
79
|
+
* Works with both PKCS#1 and PKCS#8 input formats.
|
|
80
|
+
* The returned public key is always in SPKI/X.509 PEM format.
|
|
81
|
+
*/
|
|
82
|
+
fun getPublicKeyFromPrivate(privateKeyPEM: String): String {
|
|
83
|
+
val keyFactory = KeyFactory.getInstance("RSA")
|
|
84
|
+
|
|
85
|
+
// Load the private key (handles both PKCS#1 and PKCS#8 automatically)
|
|
86
|
+
val privateKey = KeyUtils.loadPrivateKey(privateKeyPEM)
|
|
87
|
+
|
|
88
|
+
// Build public key from the private key's modulus and public exponent
|
|
89
|
+
val publicSpec = RSAPublicKeySpec(privateKey.modulus, privateKey.publicExponent)
|
|
90
|
+
val publicKey = keyFactory.generatePublic(publicSpec) as RSAPublicKey
|
|
91
|
+
|
|
92
|
+
return KeyUtils.toPEM(publicKey.encoded, "PUBLIC KEY")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Convert a private key PEM between PKCS#1 and PKCS#8 formats.
|
|
97
|
+
*
|
|
98
|
+
* @param pem The private key in PEM format
|
|
99
|
+
* @param targetFormat "pkcs1" or "pkcs8"
|
|
100
|
+
* @return The private key re-encoded in the target format
|
|
101
|
+
* @throws IllegalArgumentException if the PEM format is unrecognized
|
|
102
|
+
*/
|
|
103
|
+
fun convertPrivateKey(pem: String, targetFormat: String): String {
|
|
104
|
+
return when {
|
|
105
|
+
// PKCS#1 → PKCS#8: wrap the raw bytes in a PKCS#8 structure
|
|
106
|
+
pem.contains("BEGIN RSA PRIVATE KEY") && targetFormat == "pkcs8" -> {
|
|
107
|
+
val base64 = KeyUtils.extractBase64(pem)
|
|
108
|
+
val pkcs1Bytes = Base64.getDecoder().decode(base64)
|
|
109
|
+
val pkcs8Bytes = ASN1Utils.wrapPkcs1InPkcs8(pkcs1Bytes)
|
|
110
|
+
KeyUtils.toPEM(pkcs8Bytes, "PRIVATE KEY")
|
|
111
|
+
}
|
|
112
|
+
// PKCS#8 → PKCS#1: extract the inner PKCS#1 bytes
|
|
113
|
+
pem.contains("BEGIN PRIVATE KEY") && targetFormat == "pkcs1" -> {
|
|
114
|
+
val base64 = KeyUtils.extractBase64(pem)
|
|
115
|
+
val pkcs8Bytes = Base64.getDecoder().decode(base64)
|
|
116
|
+
val pkcs1Bytes = ASN1Utils.extractPkcs1FromPkcs8(pkcs8Bytes)
|
|
117
|
+
KeyUtils.toPEM(pkcs1Bytes, "RSA PRIVATE KEY")
|
|
118
|
+
}
|
|
119
|
+
// Already in the target format — return unchanged
|
|
120
|
+
pem.contains("BEGIN RSA PRIVATE KEY") && targetFormat == "pkcs1" -> pem
|
|
121
|
+
pem.contains("BEGIN PRIVATE KEY") && targetFormat == "pkcs8" -> pem
|
|
122
|
+
else -> throw IllegalArgumentException("Unrecognized PEM format or unsupported target format")
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|