@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
package/src/index.ts ADDED
@@ -0,0 +1,305 @@
1
+ import NativeRsa from './NativeRsa';
2
+ import { getKeyInfo } from './keyInfo';
3
+ import { utf8ToBase64 } from './encoding';
4
+ import { DEFAULTS } from './constants';
5
+ import {
6
+ requireString,
7
+ requirePrivateKey,
8
+ requirePublicKey,
9
+ validateKeySize,
10
+ validateEncryptionPadding,
11
+ validateSignaturePadding,
12
+ validateHash,
13
+ validateKeyFormat,
14
+ validateEncoding,
15
+ wrapNativeError,
16
+ } from './errors';
17
+ import type {
18
+ RSAKeyPair,
19
+ GenerateKeyPairOptions,
20
+ KeyFormat,
21
+ EncryptOptions,
22
+ DecryptOptions,
23
+ SignOptions,
24
+ VerifyOptions,
25
+ } from './types';
26
+
27
+ // Re-export all public types
28
+ export type {
29
+ RSAKeyPair,
30
+ GenerateKeyPairOptions,
31
+ RSAKeyInfo,
32
+ KeyFormat,
33
+ EncryptOptions,
34
+ DecryptOptions,
35
+ SignOptions,
36
+ VerifyOptions,
37
+ EncryptionPadding,
38
+ SignaturePadding,
39
+ HashAlgorithm,
40
+ InputEncoding,
41
+ } from './types';
42
+
43
+ // Re-export error class, error code type, and utilities that consumers may need
44
+ export { RsaError, type RsaErrorCode } from './errors';
45
+ export { getKeyInfo } from './keyInfo';
46
+ export { utf8ToBase64, base64ToUtf8 } from './encoding';
47
+
48
+ // --- Key Generation ---
49
+
50
+ /**
51
+ * Generate an RSA key pair (public + private) using native platform crypto.
52
+ *
53
+ * @param keySize RSA key size in bits. Default: 2048. Supported: 1024, 2048, 4096.
54
+ * @param options Optional configuration (format: 'pkcs1' | 'pkcs8')
55
+ * @returns Object with publicKey and privateKey in PEM format
56
+ * @throws {RsaError} INVALID_KEY_SIZE if keySize is not 1024, 2048, or 4096
57
+ * @throws {RsaError} INVALID_FORMAT if options.format is not 'pkcs1' or 'pkcs8'
58
+ * @throws {RsaError} KEY_GENERATION_FAILED if native key generation fails
59
+ */
60
+ async function generateKeyPair(
61
+ keySize: number = 2048,
62
+ options?: GenerateKeyPairOptions
63
+ ): Promise<RSAKeyPair> {
64
+ validateKeySize(keySize);
65
+ const format = options?.format ?? DEFAULTS.KEY_FORMAT;
66
+ validateKeyFormat(format);
67
+
68
+ try {
69
+ return await NativeRsa.generateKeyPair(keySize, format);
70
+ } catch (error) {
71
+ throw wrapNativeError(error, 'KEY_GENERATION_FAILED');
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Extract the public key from an RSA private key PEM string.
77
+ *
78
+ * @param privateKeyPEM RSA private key in PEM format (PKCS#1 or PKCS#8)
79
+ * @returns Public key in PEM format (SPKI/X.509)
80
+ * @throws {RsaError} INVALID_INPUT if privateKeyPEM is empty
81
+ * @throws {RsaError} INVALID_KEY if privateKeyPEM is not a valid private key
82
+ * @throws {RsaError} KEY_EXTRACTION_FAILED if native operation fails
83
+ */
84
+ async function getPublicKeyFromPrivate(
85
+ privateKeyPEM: string
86
+ ): Promise<string> {
87
+ requirePrivateKey(privateKeyPEM);
88
+
89
+ try {
90
+ return await NativeRsa.getPublicKeyFromPrivate(privateKeyPEM);
91
+ } catch (error) {
92
+ throw wrapNativeError(error, 'KEY_EXTRACTION_FAILED');
93
+ }
94
+ }
95
+
96
+ // --- Encrypt / Decrypt ---
97
+
98
+ /**
99
+ * Encrypt plaintext with an RSA public key.
100
+ *
101
+ * The input string is UTF-8 encoded to base64 by default before sending to native.
102
+ * Set `options.encoding = 'base64'` if the input is already base64-encoded binary.
103
+ *
104
+ * @param data The plaintext to encrypt (UTF-8 string or base64, depending on encoding option)
105
+ * @param publicKeyPEM The public key in SPKI PEM format
106
+ * @param options Padding, hash, and encoding options (defaults: oaep, sha256, utf8)
107
+ * @returns Base64-encoded ciphertext
108
+ * @throws {RsaError} INVALID_INPUT if data or publicKeyPEM is empty
109
+ * @throws {RsaError} INVALID_KEY if publicKeyPEM is not a valid public key
110
+ * @throws {RsaError} INVALID_PADDING if options.padding is invalid
111
+ * @throws {RsaError} INVALID_HASH if options.hash is invalid
112
+ * @throws {RsaError} INVALID_ENCODING if options.encoding is invalid
113
+ * @throws {RsaError} ENCRYPTION_FAILED if native encryption fails
114
+ */
115
+ async function encrypt(
116
+ data: string,
117
+ publicKeyPEM: string,
118
+ options?: EncryptOptions
119
+ ): Promise<string> {
120
+ requireString(data, 'data');
121
+ requirePublicKey(publicKeyPEM);
122
+
123
+ const padding = options?.padding ?? DEFAULTS.ENCRYPTION_PADDING;
124
+ const hash = options?.hash ?? DEFAULTS.HASH;
125
+ const encoding = options?.encoding ?? DEFAULTS.ENCODING;
126
+
127
+ validateEncryptionPadding(padding);
128
+ validateHash(hash);
129
+ validateEncoding(encoding);
130
+
131
+ // Convert UTF-8 text to base64 for the native bridge; pass through if already base64
132
+ const dataBase64 = encoding === 'utf8' ? utf8ToBase64(data) : data;
133
+
134
+ try {
135
+ return await NativeRsa.encrypt(dataBase64, publicKeyPEM, padding, hash);
136
+ } catch (error) {
137
+ throw wrapNativeError(error, 'ENCRYPTION_FAILED');
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Decrypt ciphertext with an RSA private key.
143
+ *
144
+ * Always returns base64-encoded plaintext. If the original data was UTF-8 text,
145
+ * use `base64ToUtf8()` to decode the result.
146
+ *
147
+ * @param encrypted Base64-encoded ciphertext (from encrypt())
148
+ * @param privateKeyPEM The private key in PEM format (PKCS#1 or PKCS#8)
149
+ * @param options Padding and hash options — must match what was used for encryption
150
+ * @returns Base64-encoded decrypted plaintext
151
+ * @throws {RsaError} INVALID_INPUT if encrypted or privateKeyPEM is empty
152
+ * @throws {RsaError} INVALID_KEY if privateKeyPEM is not a valid private key
153
+ * @throws {RsaError} INVALID_PADDING if options.padding is invalid
154
+ * @throws {RsaError} INVALID_HASH if options.hash is invalid
155
+ * @throws {RsaError} DECRYPTION_FAILED if native decryption fails
156
+ */
157
+ async function decrypt(
158
+ encrypted: string,
159
+ privateKeyPEM: string,
160
+ options?: DecryptOptions
161
+ ): Promise<string> {
162
+ requireString(encrypted, 'encrypted');
163
+ requirePrivateKey(privateKeyPEM);
164
+
165
+ const padding = options?.padding ?? DEFAULTS.ENCRYPTION_PADDING;
166
+ const hash = options?.hash ?? DEFAULTS.HASH;
167
+
168
+ validateEncryptionPadding(padding);
169
+ validateHash(hash);
170
+
171
+ try {
172
+ return await NativeRsa.decrypt(encrypted, privateKeyPEM, padding, hash);
173
+ } catch (error) {
174
+ throw wrapNativeError(error, 'DECRYPTION_FAILED');
175
+ }
176
+ }
177
+
178
+ // --- Sign / Verify ---
179
+
180
+ /**
181
+ * Sign data with an RSA private key.
182
+ *
183
+ * The input string is UTF-8 encoded to base64 by default before sending to native.
184
+ * Set `options.encoding = 'base64'` if the input is already base64-encoded binary.
185
+ *
186
+ * @param data The data to sign (UTF-8 string or base64, depending on encoding option)
187
+ * @param privateKeyPEM The private key in PEM format (PKCS#1 or PKCS#8)
188
+ * @param options Padding, hash, and encoding options (defaults: pss, sha256, utf8)
189
+ * @returns Base64-encoded signature
190
+ * @throws {RsaError} INVALID_INPUT if data or privateKeyPEM is empty
191
+ * @throws {RsaError} INVALID_KEY if privateKeyPEM is not a valid private key
192
+ * @throws {RsaError} INVALID_PADDING if options.padding is invalid
193
+ * @throws {RsaError} INVALID_HASH if options.hash is invalid
194
+ * @throws {RsaError} INVALID_ENCODING if options.encoding is invalid
195
+ * @throws {RsaError} SIGNING_FAILED if native signing fails
196
+ */
197
+ async function sign(
198
+ data: string,
199
+ privateKeyPEM: string,
200
+ options?: SignOptions
201
+ ): Promise<string> {
202
+ requireString(data, 'data');
203
+ requirePrivateKey(privateKeyPEM);
204
+
205
+ const padding = options?.padding ?? DEFAULTS.SIGNATURE_PADDING;
206
+ const hash = options?.hash ?? DEFAULTS.HASH;
207
+ const encoding = options?.encoding ?? DEFAULTS.ENCODING;
208
+
209
+ validateSignaturePadding(padding);
210
+ validateHash(hash);
211
+ validateEncoding(encoding);
212
+
213
+ const dataBase64 = encoding === 'utf8' ? utf8ToBase64(data) : data;
214
+
215
+ try {
216
+ return await NativeRsa.sign(dataBase64, privateKeyPEM, padding, hash);
217
+ } catch (error) {
218
+ throw wrapNativeError(error, 'SIGNING_FAILED');
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Verify a signature against data using an RSA public key.
224
+ *
225
+ * The input string encoding must match what was used during signing.
226
+ *
227
+ * @param data The original data that was signed
228
+ * @param signature Base64-encoded signature (from sign())
229
+ * @param publicKeyPEM The public key in SPKI PEM format
230
+ * @param options Padding, hash, and encoding options — must match signing options
231
+ * @returns true if the signature is valid, false otherwise
232
+ * @throws {RsaError} INVALID_INPUT if data, signature, or publicKeyPEM is empty
233
+ * @throws {RsaError} INVALID_KEY if publicKeyPEM is not a valid public key
234
+ * @throws {RsaError} INVALID_PADDING if options.padding is invalid
235
+ * @throws {RsaError} INVALID_HASH if options.hash is invalid
236
+ * @throws {RsaError} INVALID_ENCODING if options.encoding is invalid
237
+ * @throws {RsaError} VERIFICATION_FAILED if native verification fails
238
+ */
239
+ async function verify(
240
+ data: string,
241
+ signature: string,
242
+ publicKeyPEM: string,
243
+ options?: VerifyOptions
244
+ ): Promise<boolean> {
245
+ requireString(data, 'data');
246
+ requireString(signature, 'signature');
247
+ requirePublicKey(publicKeyPEM);
248
+
249
+ const padding = options?.padding ?? DEFAULTS.SIGNATURE_PADDING;
250
+ const hash = options?.hash ?? DEFAULTS.HASH;
251
+ const encoding = options?.encoding ?? DEFAULTS.ENCODING;
252
+
253
+ validateSignaturePadding(padding);
254
+ validateHash(hash);
255
+ validateEncoding(encoding);
256
+
257
+ const dataBase64 = encoding === 'utf8' ? utf8ToBase64(data) : data;
258
+
259
+ try {
260
+ return await NativeRsa.verify(dataBase64, signature, publicKeyPEM, padding, hash);
261
+ } catch (error) {
262
+ throw wrapNativeError(error, 'VERIFICATION_FAILED');
263
+ }
264
+ }
265
+
266
+ // --- Key Format Conversion ---
267
+
268
+ /**
269
+ * Convert a private key PEM between PKCS#1 and PKCS#8 formats.
270
+ *
271
+ * @param pem The private key in PEM format
272
+ * @param targetFormat 'pkcs1' or 'pkcs8'
273
+ * @returns The private key re-encoded in the target format
274
+ * @throws {RsaError} INVALID_INPUT if pem is empty
275
+ * @throws {RsaError} INVALID_KEY if pem is not a valid private key
276
+ * @throws {RsaError} INVALID_FORMAT if targetFormat is invalid
277
+ * @throws {RsaError} KEY_CONVERSION_FAILED if native conversion fails
278
+ */
279
+ async function convertPrivateKey(
280
+ pem: string,
281
+ targetFormat: KeyFormat
282
+ ): Promise<string> {
283
+ requirePrivateKey(pem);
284
+ validateKeyFormat(targetFormat);
285
+
286
+ try {
287
+ return await NativeRsa.convertPrivateKey(pem, targetFormat);
288
+ } catch (error) {
289
+ throw wrapNativeError(error, 'KEY_CONVERSION_FAILED');
290
+ }
291
+ }
292
+
293
+ // --- Default Export ---
294
+
295
+ const RSA = {
296
+ generateKeyPair,
297
+ getPublicKeyFromPrivate,
298
+ getKeyInfo,
299
+ encrypt,
300
+ decrypt,
301
+ sign,
302
+ verify,
303
+ convertPrivateKey,
304
+ };
305
+ export default RSA;
package/src/keyInfo.ts ADDED
@@ -0,0 +1,334 @@
1
+ import type { RSAKeyInfo } from './types';
2
+
3
+ const PEM_HEADERS = {
4
+ pkcs1Private: '-----BEGIN RSA PRIVATE KEY-----',
5
+ pkcs1PrivateEnd: '-----END RSA PRIVATE KEY-----',
6
+ pkcs8Private: '-----BEGIN PRIVATE KEY-----',
7
+ pkcs8PrivateEnd: '-----END PRIVATE KEY-----',
8
+ public: '-----BEGIN PUBLIC KEY-----',
9
+ publicEnd: '-----END PUBLIC KEY-----',
10
+ };
11
+
12
+ function parseASN1Length(
13
+ bytes: Uint8Array,
14
+ offset: number
15
+ ): { length: number | null; bytesRead: number } {
16
+ if (offset >= bytes.length) {
17
+ return { length: null, bytesRead: 0 };
18
+ }
19
+ const firstByte = bytes[offset]!;
20
+ if (firstByte < 128) {
21
+ return { length: firstByte, bytesRead: 1 };
22
+ }
23
+ const numLengthBytes = firstByte & 0x7f;
24
+ if (numLengthBytes === 0 || offset + numLengthBytes >= bytes.length) {
25
+ return { length: null, bytesRead: 0 };
26
+ }
27
+ let length = 0;
28
+ for (let i = 0; i < numLengthBytes; i++) {
29
+ length = (length << 8) | bytes[offset + 1 + i]!;
30
+ }
31
+ return { length, bytesRead: 1 + numLengthBytes };
32
+ }
33
+
34
+ function validatePKCS1Structure(derBytes: Uint8Array): string[] {
35
+ const errors: string[] = [];
36
+ let offset = 0;
37
+
38
+ if (derBytes[offset] !== 0x30) {
39
+ errors.push(
40
+ `Expected SEQUENCE tag (0x30), got 0x${derBytes[offset]?.toString(16)}`
41
+ );
42
+ return errors;
43
+ }
44
+ offset++;
45
+
46
+ const { length: seqLength, bytesRead } = parseASN1Length(derBytes, offset);
47
+ offset += bytesRead;
48
+
49
+ if (seqLength === null) {
50
+ errors.push('Invalid SEQUENCE length encoding');
51
+ return errors;
52
+ }
53
+
54
+ let integerCount = 0;
55
+ let tempOffset = offset;
56
+ const endOffset = offset + seqLength;
57
+
58
+ while (tempOffset < endOffset && tempOffset < derBytes.length) {
59
+ if (derBytes[tempOffset] !== 0x02) {
60
+ errors.push(
61
+ `Expected INTEGER tag (0x02) at position ${tempOffset}, got 0x${derBytes[tempOffset]?.toString(16)}`
62
+ );
63
+ break;
64
+ }
65
+ tempOffset++;
66
+ const { length: intLength, bytesRead: intBytesRead } = parseASN1Length(
67
+ derBytes,
68
+ tempOffset
69
+ );
70
+ if (intLength === null) {
71
+ errors.push('Invalid INTEGER length encoding');
72
+ break;
73
+ }
74
+ tempOffset += intBytesRead + intLength;
75
+ integerCount++;
76
+ }
77
+
78
+ if (integerCount !== 9) {
79
+ errors.push(
80
+ `Expected 9 INTEGER fields for PKCS#1, found ${integerCount}`
81
+ );
82
+ }
83
+ return errors;
84
+ }
85
+
86
+ function validatePKCS8Structure(derBytes: Uint8Array): string[] {
87
+ const errors: string[] = [];
88
+ let offset = 0;
89
+
90
+ // Outer SEQUENCE
91
+ if (derBytes[offset] !== 0x30) {
92
+ errors.push(
93
+ `Expected SEQUENCE tag (0x30), got 0x${derBytes[offset]?.toString(16)}`
94
+ );
95
+ return errors;
96
+ }
97
+ offset++;
98
+ const { length: seqLength, bytesRead } = parseASN1Length(derBytes, offset);
99
+ offset += bytesRead;
100
+ if (seqLength === null) {
101
+ errors.push('Invalid SEQUENCE length encoding');
102
+ return errors;
103
+ }
104
+
105
+ const endOffset = offset + seqLength;
106
+
107
+ // version INTEGER (should be 0)
108
+ if (offset >= endOffset || derBytes[offset] !== 0x02) {
109
+ errors.push(
110
+ `Expected INTEGER tag (0x02) for version, got 0x${derBytes[offset]?.toString(16)}`
111
+ );
112
+ return errors;
113
+ }
114
+ offset++;
115
+ const { length: verLen, bytesRead: verBytesRead } = parseASN1Length(
116
+ derBytes,
117
+ offset
118
+ );
119
+ if (verLen === null) {
120
+ errors.push('Invalid version INTEGER length');
121
+ return errors;
122
+ }
123
+ offset += verBytesRead + verLen;
124
+
125
+ // AlgorithmIdentifier SEQUENCE
126
+ if (offset >= endOffset || derBytes[offset] !== 0x30) {
127
+ errors.push(
128
+ `Expected SEQUENCE tag (0x30) for AlgorithmIdentifier, got 0x${derBytes[offset]?.toString(16)}`
129
+ );
130
+ return errors;
131
+ }
132
+ offset++;
133
+ const { length: algLen, bytesRead: algBytesRead } = parseASN1Length(
134
+ derBytes,
135
+ offset
136
+ );
137
+ if (algLen === null) {
138
+ errors.push('Invalid AlgorithmIdentifier length');
139
+ return errors;
140
+ }
141
+ offset += algBytesRead + algLen;
142
+
143
+ // privateKey OCTET STRING
144
+ if (offset >= endOffset || derBytes[offset] !== 0x04) {
145
+ errors.push(
146
+ `Expected OCTET STRING tag (0x04) for privateKey, got 0x${derBytes[offset]?.toString(16)}`
147
+ );
148
+ return errors;
149
+ }
150
+
151
+ return errors;
152
+ }
153
+
154
+ function validateSPKIStructure(derBytes: Uint8Array): string[] {
155
+ const errors: string[] = [];
156
+ let offset = 0;
157
+
158
+ // Outer SEQUENCE
159
+ if (derBytes[offset] !== 0x30) {
160
+ errors.push(
161
+ `Expected SEQUENCE tag (0x30), got 0x${derBytes[offset]?.toString(16)}`
162
+ );
163
+ return errors;
164
+ }
165
+ offset++;
166
+ const { length: seqLength, bytesRead } = parseASN1Length(derBytes, offset);
167
+ offset += bytesRead;
168
+ if (seqLength === null) {
169
+ errors.push('Invalid SEQUENCE length encoding');
170
+ return errors;
171
+ }
172
+
173
+ const endOffset = offset + seqLength;
174
+
175
+ // AlgorithmIdentifier SEQUENCE
176
+ if (offset >= endOffset || derBytes[offset] !== 0x30) {
177
+ errors.push(
178
+ `Expected SEQUENCE tag (0x30) for AlgorithmIdentifier, got 0x${derBytes[offset]?.toString(16)}`
179
+ );
180
+ return errors;
181
+ }
182
+ offset++;
183
+ const { length: algLen, bytesRead: algBytesRead } = parseASN1Length(
184
+ derBytes,
185
+ offset
186
+ );
187
+ if (algLen === null) {
188
+ errors.push('Invalid AlgorithmIdentifier length');
189
+ return errors;
190
+ }
191
+ offset += algBytesRead + algLen;
192
+
193
+ // subjectPublicKey BIT STRING
194
+ if (offset >= endOffset || derBytes[offset] !== 0x03) {
195
+ errors.push(
196
+ `Expected BIT STRING tag (0x03) for subjectPublicKey, got 0x${derBytes[offset]?.toString(16)}`
197
+ );
198
+ return errors;
199
+ }
200
+
201
+ return errors;
202
+ }
203
+
204
+ // Minimal base64 decoder (no external deps)
205
+ const BASE64_CHARS =
206
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
207
+
208
+ function base64Decode(input: string): Uint8Array {
209
+ const cleaned = input.replace(/[^A-Za-z0-9+/=]/g, '');
210
+ const len = cleaned.length;
211
+ const byteLen = (len * 3) / 4 - (cleaned.endsWith('==') ? 2 : cleaned.endsWith('=') ? 1 : 0);
212
+ const bytes = new Uint8Array(byteLen);
213
+ let p = 0;
214
+
215
+ for (let i = 0; i < len; i += 4) {
216
+ const a = BASE64_CHARS.indexOf(cleaned[i]!);
217
+ const b = BASE64_CHARS.indexOf(cleaned[i + 1]!);
218
+ const c = cleaned[i + 2] === '=' ? 0 : BASE64_CHARS.indexOf(cleaned[i + 2]!);
219
+ const d = cleaned[i + 3] === '=' ? 0 : BASE64_CHARS.indexOf(cleaned[i + 3]!);
220
+
221
+ const bits = (a << 18) | (b << 12) | (c << 6) | d;
222
+
223
+ if (p < byteLen) bytes[p++] = (bits >> 16) & 0xff;
224
+ if (p < byteLen) bytes[p++] = (bits >> 8) & 0xff;
225
+ if (p < byteLen) bytes[p++] = bits & 0xff;
226
+ }
227
+ return bytes;
228
+ }
229
+
230
+ /**
231
+ * Analyze an RSA key PEM string and return metadata about it.
232
+ * Runs entirely in JS — no native bridge call needed.
233
+ */
234
+ export function getKeyInfo(keyString: string): RSAKeyInfo {
235
+ const errors: string[] = [];
236
+ let format: RSAKeyInfo['format'] = 'unknown';
237
+ let keyType: RSAKeyInfo['keyType'] = 'unknown';
238
+ let base64Content = '';
239
+ let base64Lines: string[] = [];
240
+
241
+ if (
242
+ keyString.includes(PEM_HEADERS.pkcs1Private) &&
243
+ keyString.includes(PEM_HEADERS.pkcs1PrivateEnd)
244
+ ) {
245
+ format = 'pkcs1';
246
+ keyType = 'private';
247
+ const start =
248
+ keyString.indexOf(PEM_HEADERS.pkcs1Private) +
249
+ PEM_HEADERS.pkcs1Private.length;
250
+ const end = keyString.indexOf(PEM_HEADERS.pkcs1PrivateEnd);
251
+ const raw = keyString.substring(start, end).trim();
252
+ base64Lines = raw
253
+ .split('\n')
254
+ .map((l) => l.trim())
255
+ .filter((l) => l.length > 0);
256
+ base64Content = raw.replace(/\s/g, '');
257
+ } else if (
258
+ keyString.includes(PEM_HEADERS.pkcs8Private) &&
259
+ keyString.includes(PEM_HEADERS.pkcs8PrivateEnd)
260
+ ) {
261
+ format = 'pkcs8';
262
+ keyType = 'private';
263
+ const start =
264
+ keyString.indexOf(PEM_HEADERS.pkcs8Private) +
265
+ PEM_HEADERS.pkcs8Private.length;
266
+ const end = keyString.indexOf(PEM_HEADERS.pkcs8PrivateEnd);
267
+ const raw = keyString.substring(start, end).trim();
268
+ base64Lines = raw
269
+ .split('\n')
270
+ .map((l) => l.trim())
271
+ .filter((l) => l.length > 0);
272
+ base64Content = raw.replace(/\s/g, '');
273
+ } else if (
274
+ keyString.includes(PEM_HEADERS.public) &&
275
+ keyString.includes(PEM_HEADERS.publicEnd)
276
+ ) {
277
+ format = 'public';
278
+ keyType = 'public';
279
+ const start =
280
+ keyString.indexOf(PEM_HEADERS.public) + PEM_HEADERS.public.length;
281
+ const end = keyString.indexOf(PEM_HEADERS.publicEnd);
282
+ const raw = keyString.substring(start, end).trim();
283
+ base64Lines = raw
284
+ .split('\n')
285
+ .map((l) => l.trim())
286
+ .filter((l) => l.length > 0);
287
+ base64Content = raw.replace(/\s/g, '');
288
+ } else {
289
+ errors.push('Missing or invalid PEM headers');
290
+ }
291
+
292
+ // Validate line formatting (PEM standard: 64 chars per line)
293
+ if (base64Lines.length > 0) {
294
+ for (let i = 0; i < base64Lines.length - 1; i++) {
295
+ if (base64Lines[i]!.length !== 64) {
296
+ errors.push(
297
+ `Line ${i + 1} has ${base64Lines[i]!.length} chars, expected 64`
298
+ );
299
+ break;
300
+ }
301
+ }
302
+ const lastLine = base64Lines[base64Lines.length - 1]!;
303
+ if (lastLine.length > 64) {
304
+ errors.push(`Last line has ${lastLine.length} chars, expected <= 64`);
305
+ }
306
+ }
307
+
308
+ // Decode and validate structure
309
+ let derBytes: Uint8Array | null = null;
310
+ if (base64Content.length > 0) {
311
+ try {
312
+ derBytes = base64Decode(base64Content);
313
+ } catch {
314
+ errors.push('Invalid base64 encoding');
315
+ }
316
+ }
317
+
318
+ if (derBytes && format === 'pkcs1') {
319
+ errors.push(...validatePKCS1Structure(derBytes));
320
+ } else if (derBytes && format === 'pkcs8') {
321
+ errors.push(...validatePKCS8Structure(derBytes));
322
+ } else if (derBytes && format === 'public') {
323
+ errors.push(...validateSPKIStructure(derBytes));
324
+ }
325
+
326
+ return {
327
+ isValid: errors.length === 0,
328
+ format,
329
+ keyType,
330
+ pemLineCount: base64Lines.length,
331
+ derByteLength: derBytes?.length ?? 0,
332
+ errors,
333
+ };
334
+ }