@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.
Files changed (59) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +453 -0
  3. package/Rsa.podspec +23 -0
  4. package/android/build.gradle +69 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/rsa/RsaModule.kt +129 -0
  7. package/android/src/main/java/com/rsa/RsaPackage.kt +33 -0
  8. package/android/src/main/java/com/rsa/core/ASN1Utils.kt +201 -0
  9. package/android/src/main/java/com/rsa/core/Algorithms.kt +126 -0
  10. package/android/src/main/java/com/rsa/core/KeyUtils.kt +83 -0
  11. package/android/src/main/java/com/rsa/core/RSACipher.kt +71 -0
  12. package/android/src/main/java/com/rsa/core/RSAKeyGenerator.kt +125 -0
  13. package/android/src/main/java/com/rsa/core/RSASigner.kt +70 -0
  14. package/ios/ASN1Utils.swift +225 -0
  15. package/ios/Algorithms.swift +89 -0
  16. package/ios/KeyUtils.swift +125 -0
  17. package/ios/RSACipher.swift +77 -0
  18. package/ios/RSAKeyGenerator.swift +164 -0
  19. package/ios/RSASigner.swift +101 -0
  20. package/ios/Rsa.h +61 -0
  21. package/ios/Rsa.mm +216 -0
  22. package/lib/module/NativeRsa.js +16 -0
  23. package/lib/module/NativeRsa.js.map +1 -0
  24. package/lib/module/constants.js +24 -0
  25. package/lib/module/constants.js.map +1 -0
  26. package/lib/module/encoding.js +116 -0
  27. package/lib/module/encoding.js.map +1 -0
  28. package/lib/module/errors.js +135 -0
  29. package/lib/module/errors.js.map +1 -0
  30. package/lib/module/index.js +232 -0
  31. package/lib/module/index.js.map +1 -0
  32. package/lib/module/keyInfo.js +286 -0
  33. package/lib/module/keyInfo.js.map +1 -0
  34. package/lib/module/package.json +1 -0
  35. package/lib/module/types.js +2 -0
  36. package/lib/module/types.js.map +1 -0
  37. package/lib/typescript/package.json +1 -0
  38. package/lib/typescript/src/NativeRsa.d.ts +32 -0
  39. package/lib/typescript/src/NativeRsa.d.ts.map +1 -0
  40. package/lib/typescript/src/constants.d.ts +21 -0
  41. package/lib/typescript/src/constants.d.ts.map +1 -0
  42. package/lib/typescript/src/encoding.d.ts +30 -0
  43. package/lib/typescript/src/encoding.d.ts.map +1 -0
  44. package/lib/typescript/src/errors.d.ts +47 -0
  45. package/lib/typescript/src/errors.d.ts.map +1 -0
  46. package/lib/typescript/src/index.d.ts +122 -0
  47. package/lib/typescript/src/index.d.ts.map +1 -0
  48. package/lib/typescript/src/keyInfo.d.ts +7 -0
  49. package/lib/typescript/src/keyInfo.d.ts.map +1 -0
  50. package/lib/typescript/src/types.d.ts +63 -0
  51. package/lib/typescript/src/types.d.ts.map +1 -0
  52. package/package.json +133 -0
  53. package/src/NativeRsa.ts +59 -0
  54. package/src/constants.ts +25 -0
  55. package/src/encoding.ts +139 -0
  56. package/src/errors.ts +206 -0
  57. package/src/index.ts +305 -0
  58. package/src/keyInfo.ts +334 -0
  59. 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
+ }