@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
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
|
+
}
|