@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,70 @@
|
|
|
1
|
+
package com.rsa.core
|
|
2
|
+
|
|
3
|
+
import java.security.Signature
|
|
4
|
+
import java.util.Base64
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* RSA signing and verification operations for Android.
|
|
8
|
+
*
|
|
9
|
+
* Uses `java.security.Signature` 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
|
+
* - "pss" — PSS (Probabilistic Signature Scheme) with configurable hash.
|
|
14
|
+
* Salt length equals the hash output length. Available on API 24+.
|
|
15
|
+
* - "pkcs1" — PKCS#1 v1.5 deterministic signatures
|
|
16
|
+
*/
|
|
17
|
+
object RSASigner {
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sign data with an RSA private key.
|
|
21
|
+
*
|
|
22
|
+
* @param dataBase64 The data to sign, encoded as base64
|
|
23
|
+
* @param privateKeyPEM The private key in PEM format (PKCS#1 or PKCS#8)
|
|
24
|
+
* @param padding "pss" or "pkcs1"
|
|
25
|
+
* @param hash "sha256", "sha1", "sha384", or "sha512"
|
|
26
|
+
* @return The signature encoded as base64
|
|
27
|
+
*/
|
|
28
|
+
fun sign(dataBase64: String, privateKeyPEM: String, padding: String, hash: String): String {
|
|
29
|
+
val privateKey = KeyUtils.loadPrivateKey(privateKeyPEM)
|
|
30
|
+
val data = Base64.getDecoder().decode(dataBase64)
|
|
31
|
+
val algorithm = Algorithms.getSignatureAlgorithm(padding, hash)
|
|
32
|
+
|
|
33
|
+
val sig = Signature.getInstance(algorithm.name)
|
|
34
|
+
// PSS requires explicit parameter spec; PKCS#1 v1.5 has no params
|
|
35
|
+
if (algorithm.params != null) {
|
|
36
|
+
sig.setParameter(algorithm.params)
|
|
37
|
+
}
|
|
38
|
+
sig.initSign(privateKey)
|
|
39
|
+
sig.update(data)
|
|
40
|
+
|
|
41
|
+
val signature = sig.sign()
|
|
42
|
+
return Base64.getEncoder().encodeToString(signature)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Verify a signature against data using an RSA public key.
|
|
47
|
+
*
|
|
48
|
+
* @param dataBase64 The original data that was signed, encoded as base64
|
|
49
|
+
* @param signatureBase64 The signature to verify, encoded as base64
|
|
50
|
+
* @param publicKeyPEM The public key in SPKI PEM format ("BEGIN PUBLIC KEY")
|
|
51
|
+
* @param padding "pss" or "pkcs1" — must match the padding used during signing
|
|
52
|
+
* @param hash "sha256", "sha1", "sha384", or "sha512" — must match signing hash
|
|
53
|
+
* @return true if the signature is valid, false otherwise
|
|
54
|
+
*/
|
|
55
|
+
fun verify(dataBase64: String, signatureBase64: String, publicKeyPEM: String, padding: String, hash: String): Boolean {
|
|
56
|
+
val publicKey = KeyUtils.loadPublicKey(publicKeyPEM)
|
|
57
|
+
val data = Base64.getDecoder().decode(dataBase64)
|
|
58
|
+
val signatureBytes = Base64.getDecoder().decode(signatureBase64)
|
|
59
|
+
val algorithm = Algorithms.getSignatureAlgorithm(padding, hash)
|
|
60
|
+
|
|
61
|
+
val sig = Signature.getInstance(algorithm.name)
|
|
62
|
+
if (algorithm.params != null) {
|
|
63
|
+
sig.setParameter(algorithm.params)
|
|
64
|
+
}
|
|
65
|
+
sig.initVerify(publicKey)
|
|
66
|
+
sig.update(data)
|
|
67
|
+
|
|
68
|
+
return sig.verify(signatureBytes)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ASN.1 DER encoding/decoding utilities for RSA key format conversions.
|
|
5
|
+
*
|
|
6
|
+
* Handles the low-level binary encoding needed to convert between
|
|
7
|
+
* PKCS#1 and PKCS#8 key formats, and to wrap raw public keys in SPKI.
|
|
8
|
+
*
|
|
9
|
+
* ASN.1 (Abstract Syntax Notation One) uses TLV (Tag-Length-Value) encoding:
|
|
10
|
+
* - Tag: identifies the data type (e.g. 0x02 = INTEGER, 0x30 = SEQUENCE)
|
|
11
|
+
* - Length: number of bytes in the value
|
|
12
|
+
* - Value: the actual data bytes
|
|
13
|
+
*/
|
|
14
|
+
enum ASN1Utils {
|
|
15
|
+
|
|
16
|
+
// RSA algorithm OID: 1.2.840.113549.1.1.1
|
|
17
|
+
// This identifies the key as RSA in AlgorithmIdentifier structures
|
|
18
|
+
static let rsaOID: [UInt8] = [
|
|
19
|
+
0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
// ASN.1 NULL value — used as the parameter in AlgorithmIdentifier (RSA has no params)
|
|
23
|
+
static let asn1Null: [UInt8] = [0x05, 0x00]
|
|
24
|
+
|
|
25
|
+
// MARK: - Length Encoding / Decoding
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Encode a length value in ASN.1 DER format.
|
|
29
|
+
*
|
|
30
|
+
* DER length encoding:
|
|
31
|
+
* - 0–127: single byte (short form)
|
|
32
|
+
* - 128–255: 0x81 followed by one byte
|
|
33
|
+
* - 256–65535: 0x82 followed by two bytes (big-endian)
|
|
34
|
+
* - 65536+: 0x83 followed by three bytes
|
|
35
|
+
*/
|
|
36
|
+
static func encodeASN1Length(_ length: Int) -> Data {
|
|
37
|
+
if length < 128 {
|
|
38
|
+
return Data([UInt8(length)])
|
|
39
|
+
} else if length < 256 {
|
|
40
|
+
return Data([0x81, UInt8(length)])
|
|
41
|
+
} else if length < 65536 {
|
|
42
|
+
return Data([0x82, UInt8(length >> 8), UInt8(length & 0xFF)])
|
|
43
|
+
} else {
|
|
44
|
+
return Data([0x83, UInt8(length >> 16), UInt8((length >> 8) & 0xFF), UInt8(length & 0xFF)])
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Decode an ASN.1 DER length field starting at `offset` in `bytes`.
|
|
50
|
+
*
|
|
51
|
+
* - Returns: Tuple of (decoded length or nil on error, number of bytes consumed)
|
|
52
|
+
*/
|
|
53
|
+
static func decodeASN1Length(_ bytes: [UInt8], _ offset: Int) -> (Int?, Int) {
|
|
54
|
+
guard offset < bytes.count else { return (nil, 0) }
|
|
55
|
+
let first = bytes[offset]
|
|
56
|
+
if first < 128 {
|
|
57
|
+
return (Int(first), 1)
|
|
58
|
+
}
|
|
59
|
+
let numBytes = Int(first & 0x7F)
|
|
60
|
+
guard numBytes > 0, offset + numBytes < bytes.count else { return (nil, 0) }
|
|
61
|
+
var length = 0
|
|
62
|
+
for i in 0..<numBytes {
|
|
63
|
+
length = (length << 8) | Int(bytes[offset + 1 + i])
|
|
64
|
+
}
|
|
65
|
+
return (length, 1 + numBytes)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// MARK: - Structure Builders
|
|
69
|
+
|
|
70
|
+
/// Wrap arbitrary data in an ASN.1 SEQUENCE (tag 0x30 + length + data).
|
|
71
|
+
static func wrapInSequence(_ data: Data) -> Data {
|
|
72
|
+
var result = Data([0x30])
|
|
73
|
+
result.append(encodeASN1Length(data.count))
|
|
74
|
+
result.append(data)
|
|
75
|
+
return result
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Wrap a raw PKCS#1 public key in an SPKI (SubjectPublicKeyInfo) structure.
|
|
80
|
+
*
|
|
81
|
+
* iOS `SecKeyCopyExternalRepresentation` returns raw PKCS#1 for RSA public keys,
|
|
82
|
+
* but the standard PEM "PUBLIC KEY" format expects SPKI wrapping.
|
|
83
|
+
*
|
|
84
|
+
* SPKI layout:
|
|
85
|
+
* SEQUENCE {
|
|
86
|
+
* algorithm AlgorithmIdentifier { OID, NULL },
|
|
87
|
+
* publicKey BIT STRING (0x00 unused-bits prefix + raw key bytes)
|
|
88
|
+
* }
|
|
89
|
+
*/
|
|
90
|
+
static func wrapPublicKeyInSPKI(rawKeyData: Data) -> Data {
|
|
91
|
+
// AlgorithmIdentifier SEQUENCE { rsaOID, NULL }
|
|
92
|
+
var algId = Data()
|
|
93
|
+
algId.append(contentsOf: rsaOID)
|
|
94
|
+
algId.append(contentsOf: asn1Null)
|
|
95
|
+
let algIdSeq = wrapInSequence(algId)
|
|
96
|
+
|
|
97
|
+
// BIT STRING: tag + length + 0x00 (zero unused bits) + raw key data
|
|
98
|
+
var bitStringContent = Data([0x00])
|
|
99
|
+
bitStringContent.append(rawKeyData)
|
|
100
|
+
|
|
101
|
+
var bitString = Data([0x03]) // BIT STRING tag
|
|
102
|
+
bitString.append(encodeASN1Length(bitStringContent.count))
|
|
103
|
+
bitString.append(bitStringContent)
|
|
104
|
+
|
|
105
|
+
// Combine into outer SEQUENCE
|
|
106
|
+
var spkiContent = Data()
|
|
107
|
+
spkiContent.append(algIdSeq)
|
|
108
|
+
spkiContent.append(bitString)
|
|
109
|
+
|
|
110
|
+
return wrapInSequence(spkiContent)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Wrap PKCS#1 private key data inside a PKCS#8 PrivateKeyInfo structure.
|
|
115
|
+
*
|
|
116
|
+
* PKCS#8 PrivateKeyInfo layout:
|
|
117
|
+
* SEQUENCE {
|
|
118
|
+
* version INTEGER (0),
|
|
119
|
+
* algorithm AlgorithmIdentifier { OID, NULL },
|
|
120
|
+
* privateKey OCTET STRING containing PKCS#1 bytes
|
|
121
|
+
* }
|
|
122
|
+
*/
|
|
123
|
+
static func wrapPkcs1InPkcs8(pkcs1Data: Data) -> Data {
|
|
124
|
+
var content = Data()
|
|
125
|
+
|
|
126
|
+
// version INTEGER 0
|
|
127
|
+
content.append(contentsOf: [0x02, 0x01, 0x00] as [UInt8])
|
|
128
|
+
|
|
129
|
+
// AlgorithmIdentifier SEQUENCE { rsaOID, NULL }
|
|
130
|
+
var algId = Data()
|
|
131
|
+
algId.append(contentsOf: rsaOID)
|
|
132
|
+
algId.append(contentsOf: asn1Null)
|
|
133
|
+
content.append(wrapInSequence(algId))
|
|
134
|
+
|
|
135
|
+
// OCTET STRING wrapping the raw PKCS#1 bytes
|
|
136
|
+
content.append(Data([0x04]))
|
|
137
|
+
content.append(encodeASN1Length(pkcs1Data.count))
|
|
138
|
+
content.append(pkcs1Data)
|
|
139
|
+
|
|
140
|
+
return wrapInSequence(content)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extract the inner PKCS#1 private key data from a PKCS#8 wrapper.
|
|
145
|
+
*
|
|
146
|
+
* Walks the PKCS#8 structure:
|
|
147
|
+
* SEQUENCE → skip version INTEGER → skip AlgorithmIdentifier → read OCTET STRING
|
|
148
|
+
*
|
|
149
|
+
* Returns the original data unchanged if parsing fails.
|
|
150
|
+
*/
|
|
151
|
+
static func extractPkcs1FromPkcs8(pkcs8Data: Data) -> Data {
|
|
152
|
+
let bytes = [UInt8](pkcs8Data)
|
|
153
|
+
var offset = 0
|
|
154
|
+
|
|
155
|
+
// Outer SEQUENCE tag + length
|
|
156
|
+
guard offset < bytes.count, bytes[offset] == 0x30 else { return pkcs8Data }
|
|
157
|
+
offset += 1
|
|
158
|
+
let (_, seqLenBytes) = decodeASN1Length(bytes, offset)
|
|
159
|
+
offset += seqLenBytes
|
|
160
|
+
|
|
161
|
+
// version INTEGER — skip over it
|
|
162
|
+
guard offset < bytes.count, bytes[offset] == 0x02 else { return pkcs8Data }
|
|
163
|
+
offset += 1
|
|
164
|
+
let (verLen, verLenBytes) = decodeASN1Length(bytes, offset)
|
|
165
|
+
offset += verLenBytes + (verLen ?? 0)
|
|
166
|
+
|
|
167
|
+
// AlgorithmIdentifier SEQUENCE — skip over it
|
|
168
|
+
guard offset < bytes.count, bytes[offset] == 0x30 else { return pkcs8Data }
|
|
169
|
+
offset += 1
|
|
170
|
+
let (algLen, algLenBytes) = decodeASN1Length(bytes, offset)
|
|
171
|
+
offset += algLenBytes + (algLen ?? 0)
|
|
172
|
+
|
|
173
|
+
// OCTET STRING containing the PKCS#1 private key
|
|
174
|
+
guard offset < bytes.count, bytes[offset] == 0x04 else { return pkcs8Data }
|
|
175
|
+
offset += 1
|
|
176
|
+
let (octetLen, octetLenBytes) = decodeASN1Length(bytes, offset)
|
|
177
|
+
offset += octetLenBytes
|
|
178
|
+
|
|
179
|
+
guard let len = octetLen, offset + len <= bytes.count else { return pkcs8Data }
|
|
180
|
+
return Data(bytes[offset..<(offset + len)])
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Strip the SPKI (SubjectPublicKeyInfo) wrapper to get the raw PKCS#1 public key.
|
|
185
|
+
*
|
|
186
|
+
* iOS `SecKeyCreateWithData` expects raw PKCS#1 public key data, not SPKI.
|
|
187
|
+
* Standard PEM "PUBLIC KEY" files contain SPKI, so we need to unwrap.
|
|
188
|
+
*
|
|
189
|
+
* SPKI layout:
|
|
190
|
+
* SEQUENCE {
|
|
191
|
+
* AlgorithmIdentifier SEQUENCE { ... },
|
|
192
|
+
* BIT STRING (0x00 prefix + raw key bytes)
|
|
193
|
+
* }
|
|
194
|
+
*/
|
|
195
|
+
static func stripSPKIHeader(spkiData: Data) -> Data {
|
|
196
|
+
let bytes = [UInt8](spkiData)
|
|
197
|
+
var offset = 0
|
|
198
|
+
|
|
199
|
+
// Outer SEQUENCE
|
|
200
|
+
guard offset < bytes.count, bytes[offset] == 0x30 else { return spkiData }
|
|
201
|
+
offset += 1
|
|
202
|
+
let (_, seqLenBytes) = decodeASN1Length(bytes, offset)
|
|
203
|
+
offset += seqLenBytes
|
|
204
|
+
|
|
205
|
+
// AlgorithmIdentifier SEQUENCE — skip over it
|
|
206
|
+
guard offset < bytes.count, bytes[offset] == 0x30 else { return spkiData }
|
|
207
|
+
offset += 1
|
|
208
|
+
let (algLen, algLenBytes) = decodeASN1Length(bytes, offset)
|
|
209
|
+
offset += algLenBytes + (algLen ?? 0)
|
|
210
|
+
|
|
211
|
+
// BIT STRING
|
|
212
|
+
guard offset < bytes.count, bytes[offset] == 0x03 else { return spkiData }
|
|
213
|
+
offset += 1
|
|
214
|
+
let (bitLen, bitLenBytes) = decodeASN1Length(bytes, offset)
|
|
215
|
+
offset += bitLenBytes
|
|
216
|
+
|
|
217
|
+
// Skip the "unused bits" byte (always 0x00 for RSA keys)
|
|
218
|
+
guard offset < bytes.count, bytes[offset] == 0x00 else { return spkiData }
|
|
219
|
+
offset += 1
|
|
220
|
+
|
|
221
|
+
// The remaining bytes are the raw PKCS#1 public key
|
|
222
|
+
guard let len = bitLen, offset + len - 1 <= bytes.count else { return spkiData }
|
|
223
|
+
return Data(bytes[offset..<(offset + len - 1)])
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Maps (padding, hash) option strings to `SecKeyAlgorithm` values.
|
|
6
|
+
*
|
|
7
|
+
* This provides a single lookup point so that RSACipher and RSASigner don't
|
|
8
|
+
* need to duplicate algorithm selection logic.
|
|
9
|
+
*
|
|
10
|
+
* Supported combinations:
|
|
11
|
+
* Encryption (encrypt/decrypt):
|
|
12
|
+
* - "pkcs1" → rsaEncryptionPKCS1
|
|
13
|
+
* - "oaep" → rsaEncryptionOAEPSHA{N}
|
|
14
|
+
*
|
|
15
|
+
* Signature (sign/verify):
|
|
16
|
+
* - "pkcs1" → rsaSignatureMessagePKCS1v15SHA{N}
|
|
17
|
+
* - "pss" → rsaSignatureMessagePSSSHA{N}
|
|
18
|
+
*
|
|
19
|
+
* The "Message" variants (rsaSignatureMessage…) hash the input data internally,
|
|
20
|
+
* so callers pass the raw message — not a pre-computed digest.
|
|
21
|
+
*/
|
|
22
|
+
enum Algorithms {
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Look up the SecKeyAlgorithm for an encrypt/decrypt operation.
|
|
26
|
+
*
|
|
27
|
+
* - Parameters:
|
|
28
|
+
* - padding: "pkcs1" or "oaep"
|
|
29
|
+
* - hash: "sha1", "sha256", "sha384", or "sha512" (only used with OAEP)
|
|
30
|
+
* - Throws: If the padding or hash is unsupported
|
|
31
|
+
*/
|
|
32
|
+
static func encryptionAlgorithm(padding: String, hash: String) throws -> SecKeyAlgorithm {
|
|
33
|
+
switch padding {
|
|
34
|
+
case "pkcs1":
|
|
35
|
+
return .rsaEncryptionPKCS1
|
|
36
|
+
case "oaep":
|
|
37
|
+
switch hash {
|
|
38
|
+
case "sha1": return .rsaEncryptionOAEPSHA1
|
|
39
|
+
case "sha256": return .rsaEncryptionOAEPSHA256
|
|
40
|
+
case "sha384": return .rsaEncryptionOAEPSHA384
|
|
41
|
+
case "sha512": return .rsaEncryptionOAEPSHA512
|
|
42
|
+
default:
|
|
43
|
+
throw NSError(domain: "Algorithms", code: 1,
|
|
44
|
+
userInfo: [NSLocalizedDescriptionKey: "Unsupported hash for OAEP: \(hash)"])
|
|
45
|
+
}
|
|
46
|
+
default:
|
|
47
|
+
throw NSError(domain: "Algorithms", code: 2,
|
|
48
|
+
userInfo: [NSLocalizedDescriptionKey: "Unsupported encryption padding: \(padding)"])
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Look up the SecKeyAlgorithm for a sign/verify operation.
|
|
54
|
+
*
|
|
55
|
+
* - Parameters:
|
|
56
|
+
* - padding: "pkcs1" or "pss"
|
|
57
|
+
* - hash: "sha1", "sha256", "sha384", or "sha512"
|
|
58
|
+
* - Throws: If the padding or hash is unsupported
|
|
59
|
+
*/
|
|
60
|
+
static func signatureAlgorithm(padding: String, hash: String) throws -> SecKeyAlgorithm {
|
|
61
|
+
switch padding {
|
|
62
|
+
case "pkcs1":
|
|
63
|
+
// PKCS#1 v1.5 deterministic signatures
|
|
64
|
+
switch hash {
|
|
65
|
+
case "sha1": return .rsaSignatureMessagePKCS1v15SHA1
|
|
66
|
+
case "sha256": return .rsaSignatureMessagePKCS1v15SHA256
|
|
67
|
+
case "sha384": return .rsaSignatureMessagePKCS1v15SHA384
|
|
68
|
+
case "sha512": return .rsaSignatureMessagePKCS1v15SHA512
|
|
69
|
+
default:
|
|
70
|
+
throw NSError(domain: "Algorithms", code: 3,
|
|
71
|
+
userInfo: [NSLocalizedDescriptionKey: "Unsupported hash for PKCS#1 signature: \(hash)"])
|
|
72
|
+
}
|
|
73
|
+
case "pss":
|
|
74
|
+
// PSS probabilistic signatures
|
|
75
|
+
switch hash {
|
|
76
|
+
case "sha1": return .rsaSignatureMessagePSSSHA1
|
|
77
|
+
case "sha256": return .rsaSignatureMessagePSSSHA256
|
|
78
|
+
case "sha384": return .rsaSignatureMessagePSSSHA384
|
|
79
|
+
case "sha512": return .rsaSignatureMessagePSSSHA512
|
|
80
|
+
default:
|
|
81
|
+
throw NSError(domain: "Algorithms", code: 4,
|
|
82
|
+
userInfo: [NSLocalizedDescriptionKey: "Unsupported hash for PSS signature: \(hash)"])
|
|
83
|
+
}
|
|
84
|
+
default:
|
|
85
|
+
throw NSError(domain: "Algorithms", code: 5,
|
|
86
|
+
userInfo: [NSLocalizedDescriptionKey: "Unsupported signature padding: \(padding)"])
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PEM parsing and key loading utilities for iOS.
|
|
6
|
+
*
|
|
7
|
+
* Handles conversion between PEM-encoded key strings and SecKey objects.
|
|
8
|
+
* Supports both PKCS#1 ("BEGIN RSA PRIVATE KEY") and PKCS#8 ("BEGIN PRIVATE KEY")
|
|
9
|
+
* private key formats, and SPKI ("BEGIN PUBLIC KEY") public key format.
|
|
10
|
+
*/
|
|
11
|
+
enum KeyUtils {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract the base64-encoded content from a PEM string,
|
|
15
|
+
* stripping the header/footer lines.
|
|
16
|
+
*/
|
|
17
|
+
static func extractBase64(from pem: String) -> String {
|
|
18
|
+
return pem.components(separatedBy: "\n")
|
|
19
|
+
.filter { !$0.hasPrefix("-----") }
|
|
20
|
+
.joined()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Format raw DER data as a PEM string with the given header.
|
|
25
|
+
*
|
|
26
|
+
* - Parameters:
|
|
27
|
+
* - data: The raw DER-encoded key data
|
|
28
|
+
* - header: The PEM header label (e.g. "RSA PRIVATE KEY", "PRIVATE KEY", "PUBLIC KEY")
|
|
29
|
+
* - Returns: A properly formatted PEM string with 64-char line wrapping
|
|
30
|
+
*/
|
|
31
|
+
static func toPEM(data: Data, header: String) -> String {
|
|
32
|
+
let base64 = data.base64EncodedString()
|
|
33
|
+
let lines = base64.chunked(into: 64).joined(separator: "\n")
|
|
34
|
+
return "-----BEGIN \(header)-----\n\(lines)\n-----END \(header)-----"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load an RSA private key from a PEM string as a SecKey.
|
|
39
|
+
*
|
|
40
|
+
* Accepts both PKCS#1 and PKCS#8 formats:
|
|
41
|
+
* - PKCS#1 ("BEGIN RSA PRIVATE KEY"): used directly, since SecKeyCreateWithData
|
|
42
|
+
* expects raw PKCS#1 for RSA private keys
|
|
43
|
+
* - PKCS#8 ("BEGIN PRIVATE KEY"): extracts the inner PKCS#1 data first
|
|
44
|
+
*
|
|
45
|
+
* - Throws: If base64 decoding fails or the key cannot be imported
|
|
46
|
+
*/
|
|
47
|
+
static func loadPrivateKey(pem: String) throws -> SecKey {
|
|
48
|
+
let base64 = extractBase64(from: pem)
|
|
49
|
+
guard let derData = Data(base64Encoded: base64) else {
|
|
50
|
+
throw NSError(domain: "KeyUtils", code: 1,
|
|
51
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid base64 encoding in private key PEM"])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// SecKeyCreateWithData expects raw PKCS#1 for RSA keys
|
|
55
|
+
let pkcs1Data: Data
|
|
56
|
+
if pem.contains("BEGIN RSA PRIVATE KEY") {
|
|
57
|
+
pkcs1Data = derData
|
|
58
|
+
} else if pem.contains("BEGIN PRIVATE KEY") {
|
|
59
|
+
// PKCS#8 wraps PKCS#1 — extract the inner key
|
|
60
|
+
pkcs1Data = ASN1Utils.extractPkcs1FromPkcs8(pkcs8Data: derData)
|
|
61
|
+
} else {
|
|
62
|
+
throw NSError(domain: "KeyUtils", code: 2,
|
|
63
|
+
userInfo: [NSLocalizedDescriptionKey: "Unrecognized private key PEM format"])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let attributes: [String: Any] = [
|
|
67
|
+
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
|
|
68
|
+
kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
var error: Unmanaged<CFError>?
|
|
72
|
+
guard let key = SecKeyCreateWithData(pkcs1Data as CFData, attributes as CFDictionary, &error) else {
|
|
73
|
+
let errorMessage = error?.takeRetainedValue().localizedDescription ?? "Unknown error"
|
|
74
|
+
throw NSError(domain: "KeyUtils", code: 3,
|
|
75
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to import private key: \(errorMessage)"])
|
|
76
|
+
}
|
|
77
|
+
return key
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Load an RSA public key from a PEM string as a SecKey.
|
|
82
|
+
*
|
|
83
|
+
* Expects SPKI ("BEGIN PUBLIC KEY") format. Since SecKeyCreateWithData
|
|
84
|
+
* expects raw PKCS#1 public key data (not SPKI), the SPKI header is
|
|
85
|
+
* stripped automatically.
|
|
86
|
+
*
|
|
87
|
+
* - Throws: If base64 decoding fails or the key cannot be imported
|
|
88
|
+
*/
|
|
89
|
+
static func loadPublicKey(pem: String) throws -> SecKey {
|
|
90
|
+
let base64 = extractBase64(from: pem)
|
|
91
|
+
guard let derData = Data(base64Encoded: base64) else {
|
|
92
|
+
throw NSError(domain: "KeyUtils", code: 4,
|
|
93
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid base64 encoding in public key PEM"])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// SecKeyCreateWithData expects raw PKCS#1 public key, not SPKI
|
|
97
|
+
let rawKeyData = ASN1Utils.stripSPKIHeader(spkiData: derData)
|
|
98
|
+
|
|
99
|
+
let attributes: [String: Any] = [
|
|
100
|
+
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
|
|
101
|
+
kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
var error: Unmanaged<CFError>?
|
|
105
|
+
guard let key = SecKeyCreateWithData(rawKeyData as CFData, attributes as CFDictionary, &error) else {
|
|
106
|
+
let errorMessage = error?.takeRetainedValue().localizedDescription ?? "Unknown error"
|
|
107
|
+
throw NSError(domain: "KeyUtils", code: 5,
|
|
108
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to import public key: \(errorMessage)"])
|
|
109
|
+
}
|
|
110
|
+
return key
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// MARK: - String Extension for PEM Line Wrapping
|
|
115
|
+
|
|
116
|
+
extension String {
|
|
117
|
+
/// Split a string into chunks of the given size (used for 64-char PEM line wrapping).
|
|
118
|
+
func chunked(into size: Int) -> [String] {
|
|
119
|
+
return stride(from: 0, to: count, by: size).map {
|
|
120
|
+
let start = index(startIndex, offsetBy: $0)
|
|
121
|
+
let end = index(start, offsetBy: min(size, count - $0))
|
|
122
|
+
return String(self[start..<end])
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* RSA encryption and decryption operations for iOS.
|
|
6
|
+
*
|
|
7
|
+
* Uses the Security framework's `SecKeyCreateEncryptedData` / `SecKeyCreateDecryptedData`.
|
|
8
|
+
* All data crosses the bridge as base64 strings to avoid binary encoding issues.
|
|
9
|
+
*
|
|
10
|
+
* Supported padding modes:
|
|
11
|
+
* - "oaep" — OAEP (Optimal Asymmetric Encryption Padding) with configurable hash
|
|
12
|
+
* - "pkcs1" — PKCS#1 v1.5 padding (legacy, not recommended for new applications)
|
|
13
|
+
*
|
|
14
|
+
* Exposed to Objective-C via `@objc` for the React Native bridge.
|
|
15
|
+
*/
|
|
16
|
+
@objc public class RSACipher: NSObject {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Encrypt data with an RSA public key.
|
|
20
|
+
*
|
|
21
|
+
* - Parameters:
|
|
22
|
+
* - dataBase64: The plaintext data encoded as base64
|
|
23
|
+
* - publicKeyPEM: The public key in SPKI PEM format ("BEGIN PUBLIC KEY")
|
|
24
|
+
* - padding: "oaep" or "pkcs1"
|
|
25
|
+
* - hash: "sha256", "sha1", "sha384", or "sha512" (used with OAEP)
|
|
26
|
+
* - Throws: If the input is invalid, key loading fails, or encryption fails
|
|
27
|
+
* - Returns: The ciphertext encoded as base64
|
|
28
|
+
*/
|
|
29
|
+
@objc public static func encrypt(dataBase64: String, publicKeyPEM: String, padding: String, hash: String) throws -> String {
|
|
30
|
+
guard let data = Data(base64Encoded: dataBase64) else {
|
|
31
|
+
throw NSError(domain: "RSACipher", code: 1,
|
|
32
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid base64 input data"])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let publicKey = try KeyUtils.loadPublicKey(pem: publicKeyPEM)
|
|
36
|
+
let algorithm = try Algorithms.encryptionAlgorithm(padding: padding, hash: hash)
|
|
37
|
+
|
|
38
|
+
var error: Unmanaged<CFError>?
|
|
39
|
+
guard let encryptedData = SecKeyCreateEncryptedData(publicKey, algorithm, data as CFData, &error) as Data? else {
|
|
40
|
+
let errorMessage = error?.takeRetainedValue().localizedDescription ?? "Unknown error"
|
|
41
|
+
throw NSError(domain: "RSACipher", code: 2,
|
|
42
|
+
userInfo: [NSLocalizedDescriptionKey: "Encryption failed: \(errorMessage)"])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return encryptedData.base64EncodedString()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Decrypt data with an RSA private key.
|
|
50
|
+
*
|
|
51
|
+
* - Parameters:
|
|
52
|
+
* - dataBase64: The ciphertext encoded as base64
|
|
53
|
+
* - privateKeyPEM: The private key in PEM format (PKCS#1 or PKCS#8)
|
|
54
|
+
* - padding: "oaep" or "pkcs1" — must match the padding used during encryption
|
|
55
|
+
* - hash: "sha256", "sha1", "sha384", or "sha512" — must match encryption hash
|
|
56
|
+
* - Throws: If the input is invalid, key loading fails, or decryption fails
|
|
57
|
+
* - Returns: The decrypted plaintext encoded as base64
|
|
58
|
+
*/
|
|
59
|
+
@objc public static func decrypt(dataBase64: String, privateKeyPEM: String, padding: String, hash: String) throws -> String {
|
|
60
|
+
guard let data = Data(base64Encoded: dataBase64) else {
|
|
61
|
+
throw NSError(domain: "RSACipher", code: 3,
|
|
62
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid base64 input data"])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let privateKey = try KeyUtils.loadPrivateKey(pem: privateKeyPEM)
|
|
66
|
+
let algorithm = try Algorithms.encryptionAlgorithm(padding: padding, hash: hash)
|
|
67
|
+
|
|
68
|
+
var error: Unmanaged<CFError>?
|
|
69
|
+
guard let decryptedData = SecKeyCreateDecryptedData(privateKey, algorithm, data as CFData, &error) as Data? else {
|
|
70
|
+
let errorMessage = error?.takeRetainedValue().localizedDescription ?? "Unknown error"
|
|
71
|
+
throw NSError(domain: "RSACipher", code: 4,
|
|
72
|
+
userInfo: [NSLocalizedDescriptionKey: "Decryption failed: \(errorMessage)"])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return decryptedData.base64EncodedString()
|
|
76
|
+
}
|
|
77
|
+
}
|