@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,63 @@
1
+ /** Private key encoding format */
2
+ export type KeyFormat = 'pkcs1' | 'pkcs8';
3
+ /** Padding mode for encrypt/decrypt operations */
4
+ export type EncryptionPadding = 'oaep' | 'pkcs1';
5
+ /** Padding mode for sign/verify operations */
6
+ export type SignaturePadding = 'pss' | 'pkcs1';
7
+ /** Hash algorithm used for padding schemes */
8
+ export type HashAlgorithm = 'sha256' | 'sha1' | 'sha384' | 'sha512';
9
+ /**
10
+ * Controls how the JS input string is interpreted before sending to native.
11
+ * - 'utf8': the string is UTF-8 text (will be encoded to base64 before bridging)
12
+ * - 'base64': the string is already base64-encoded binary data
13
+ */
14
+ export type InputEncoding = 'utf8' | 'base64';
15
+ export interface GenerateKeyPairOptions {
16
+ /** Output format for the private key. Default: 'pkcs1' */
17
+ format?: KeyFormat;
18
+ }
19
+ export interface RSAKeyPair {
20
+ /** Public key in PEM format (SPKI/X.509) */
21
+ publicKey: string;
22
+ /** Private key in PEM format (PKCS#1 or PKCS#8 depending on options) */
23
+ privateKey: string;
24
+ }
25
+ export interface RSAKeyInfo {
26
+ isValid: boolean;
27
+ format: 'pkcs1' | 'pkcs8' | 'public' | 'unknown';
28
+ keyType: 'private' | 'public' | 'unknown';
29
+ pemLineCount: number;
30
+ derByteLength: number;
31
+ errors: string[];
32
+ }
33
+ export interface EncryptOptions {
34
+ /** Padding mode. Default: 'oaep' */
35
+ padding?: EncryptionPadding;
36
+ /** Hash algorithm (used with OAEP). Default: 'sha256' */
37
+ hash?: HashAlgorithm;
38
+ /** How to interpret the input string. Default: 'utf8' */
39
+ encoding?: InputEncoding;
40
+ }
41
+ export interface DecryptOptions {
42
+ /** Padding mode — must match encryption. Default: 'oaep' */
43
+ padding?: EncryptionPadding;
44
+ /** Hash algorithm — must match encryption. Default: 'sha256' */
45
+ hash?: HashAlgorithm;
46
+ }
47
+ export interface SignOptions {
48
+ /** Padding mode. Default: 'pss' */
49
+ padding?: SignaturePadding;
50
+ /** Hash algorithm. Default: 'sha256' */
51
+ hash?: HashAlgorithm;
52
+ /** How to interpret the input string. Default: 'utf8' */
53
+ encoding?: InputEncoding;
54
+ }
55
+ export interface VerifyOptions {
56
+ /** Padding mode — must match signing. Default: 'pss' */
57
+ padding?: SignaturePadding;
58
+ /** Hash algorithm — must match signing. Default: 'sha256' */
59
+ hash?: HashAlgorithm;
60
+ /** How to interpret the input string. Default: 'utf8' */
61
+ encoding?: InputEncoding;
62
+ }
63
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAEA,kCAAkC;AAClC,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAI1C,kDAAkD;AAClD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEjD,8CAA8C;AAC9C,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,CAAC;AAE/C,8CAA8C;AAC9C,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEpE;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,CAAC;AAI9C,MAAM,WAAW,sBAAsB;IACrC,0DAA0D;IAC1D,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;IACjD,OAAO,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC1C,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAID,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,yDAAyD;IACzD,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,gEAAgE;IAChE,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB;AAID,MAAM,WAAW,WAAW;IAC1B,mCAAmC;IACnC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,wCAAwC;IACxC,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,wDAAwD;IACxD,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,6DAA6D;IAC7D,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B"}
package/package.json ADDED
@@ -0,0 +1,133 @@
1
+ {
2
+ "name": "@avieldr/react-native-rsa",
3
+ "version": "1.0.0",
4
+ "description": "High-performance native RSA cryptography for React Native",
5
+ "main": "./lib/module/index.js",
6
+ "types": "./lib/typescript/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "source": "./src/index.ts",
10
+ "types": "./lib/typescript/src/index.d.ts",
11
+ "default": "./lib/module/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "lib",
18
+ "android",
19
+ "ios",
20
+ "*.podspec",
21
+ "!ios/build",
22
+ "!android/build",
23
+ "!android/gradle",
24
+ "!android/gradlew",
25
+ "!android/gradlew.bat",
26
+ "!android/local.properties",
27
+ "!**/__tests__",
28
+ "!**/__fixtures__",
29
+ "!**/__mocks__",
30
+ "!**/.*"
31
+ ],
32
+ "scripts": {
33
+ "example": "yarn workspace react-native-rsa-example",
34
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
35
+ "prepare": "bob build",
36
+ "typecheck": "tsc",
37
+ "lint": "eslint \"**/*.{js,ts,tsx}\""
38
+ },
39
+ "keywords": [
40
+ "react-native",
41
+ "ios",
42
+ "android",
43
+ "rsa",
44
+ "crypto",
45
+ "cryptography",
46
+ "encryption",
47
+ "signing",
48
+ "security",
49
+ "turbo-module",
50
+ "new-architecture"
51
+ ],
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/avieldr/react-native-rsa-turbo.git"
55
+ },
56
+ "author": "Aviel Drori <avielwebdesign@gmail.com>",
57
+ "license": "MIT",
58
+ "bugs": {
59
+ "url": "https://github.com/avieldr/react-native-rsa-turbo/issues"
60
+ },
61
+ "homepage": "https://github.com/avieldr/react-native-rsa-turbo#readme",
62
+ "publishConfig": {
63
+ "registry": "https://registry.npmjs.org/",
64
+ "access": "public"
65
+ },
66
+ "devDependencies": {
67
+ "@eslint/compat": "^1.3.2",
68
+ "@eslint/eslintrc": "^3.3.1",
69
+ "@eslint/js": "^9.35.0",
70
+ "@react-native/babel-preset": "0.83.0",
71
+ "@react-native/eslint-config": "0.83.0",
72
+ "@types/react": "^19.2.0",
73
+ "del-cli": "^6.0.0",
74
+ "eslint": "^9.35.0",
75
+ "eslint-config-prettier": "^10.1.8",
76
+ "eslint-plugin-prettier": "^5.5.4",
77
+ "prettier": "^2.8.8",
78
+ "react": "19.2.0",
79
+ "react-native": "0.83.0",
80
+ "react-native-builder-bob": "^0.40.13",
81
+ "turbo": "^2.5.6",
82
+ "typescript": "^5.9.2"
83
+ },
84
+ "peerDependencies": {
85
+ "react": "*",
86
+ "react-native": "*"
87
+ },
88
+ "workspaces": [
89
+ "example"
90
+ ],
91
+ "packageManager": "yarn@4.11.0",
92
+ "react-native-builder-bob": {
93
+ "source": "src",
94
+ "output": "lib",
95
+ "targets": [
96
+ [
97
+ "module",
98
+ {
99
+ "esm": true
100
+ }
101
+ ],
102
+ [
103
+ "typescript",
104
+ {
105
+ "project": "tsconfig.build.json"
106
+ }
107
+ ]
108
+ ]
109
+ },
110
+ "codegenConfig": {
111
+ "name": "RsaSpec",
112
+ "type": "modules",
113
+ "jsSrcsDir": "src",
114
+ "android": {
115
+ "javaPackageName": "com.rsa"
116
+ }
117
+ },
118
+ "prettier": {
119
+ "quoteProps": "consistent",
120
+ "singleQuote": true,
121
+ "tabWidth": 2,
122
+ "trailingComma": "es5",
123
+ "useTabs": false
124
+ },
125
+ "create-react-native-library": {
126
+ "type": "turbo-module",
127
+ "languages": "kotlin-objc",
128
+ "tools": [
129
+ "eslint"
130
+ ],
131
+ "version": "0.57.0"
132
+ }
133
+ }
@@ -0,0 +1,59 @@
1
+ import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
+
3
+ /**
4
+ * Native bridge specification for RSA operations.
5
+ *
6
+ * All methods use flat string parameters (no objects/arrays) because
7
+ * React Native's codegen works best with simple types across the bridge.
8
+ *
9
+ * Data is passed as base64 strings to avoid binary encoding issues.
10
+ * The JS layer (index.ts) handles defaults resolution and UTF-8→base64 encoding.
11
+ */
12
+ export interface Spec extends TurboModule {
13
+ /** Generate an RSA key pair → { publicKey, privateKey } in PEM format */
14
+ generateKeyPair(
15
+ keySize: number,
16
+ format: string
17
+ ): Promise<{ publicKey: string; privateKey: string }>;
18
+
19
+ /** Extract public key from a private key PEM → public key PEM */
20
+ getPublicKeyFromPrivate(privateKeyPEM: string): Promise<string>;
21
+
22
+ /** Encrypt base64 data with a public key → base64 ciphertext */
23
+ encrypt(
24
+ dataBase64: string,
25
+ publicKeyPEM: string,
26
+ padding: string,
27
+ hash: string
28
+ ): Promise<string>;
29
+
30
+ /** Decrypt base64 ciphertext with a private key → base64 plaintext */
31
+ decrypt(
32
+ dataBase64: string,
33
+ privateKeyPEM: string,
34
+ padding: string,
35
+ hash: string
36
+ ): Promise<string>;
37
+
38
+ /** Sign base64 data with a private key → base64 signature */
39
+ sign(
40
+ dataBase64: string,
41
+ privateKeyPEM: string,
42
+ padding: string,
43
+ hash: string
44
+ ): Promise<string>;
45
+
46
+ /** Verify a base64 signature against base64 data → boolean */
47
+ verify(
48
+ dataBase64: string,
49
+ signatureBase64: string,
50
+ publicKeyPEM: string,
51
+ padding: string,
52
+ hash: string
53
+ ): Promise<boolean>;
54
+
55
+ /** Convert a private key PEM between PKCS#1 and PKCS#8 formats */
56
+ convertPrivateKey(pem: string, targetFormat: string): Promise<string>;
57
+ }
58
+
59
+ export default TurboModuleRegistry.getEnforcing<Spec>('Rsa');
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Default values for all RSA operation options.
3
+ *
4
+ * These are applied in `index.ts` when the caller omits an option.
5
+ * Keeping them in one place ensures consistency across encrypt, decrypt, sign, verify.
6
+ */
7
+ export const DEFAULTS = {
8
+ /** Default padding for encrypt/decrypt — OAEP is recommended over PKCS#1 */
9
+ ENCRYPTION_PADDING: 'oaep' as const,
10
+
11
+ /** Default padding for sign/verify — PSS is recommended over PKCS#1 */
12
+ SIGNATURE_PADDING: 'pss' as const,
13
+
14
+ /** Default hash algorithm */
15
+ HASH: 'sha256' as const,
16
+
17
+ /** Default input string encoding — UTF-8 text is the common case */
18
+ ENCODING: 'utf8' as const,
19
+
20
+ /** Default private key format for key generation */
21
+ KEY_FORMAT: 'pkcs1' as const,
22
+ };
23
+
24
+ /** Supported RSA key sizes in bits */
25
+ export const VALID_KEY_SIZES = [1024, 2048, 4096] as const;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Pure-JS UTF-8 ↔ Base64 encoding utilities.
3
+ *
4
+ * These work in all React Native JS engines (Hermes, JSC) without
5
+ * external dependencies. They handle full Unicode including surrogate pairs.
6
+ *
7
+ * Used by the public API to convert UTF-8 input strings to base64
8
+ * before sending them across the native bridge.
9
+ */
10
+
11
+ const BASE64_CHARS =
12
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
13
+
14
+ /**
15
+ * Encode a UTF-8 string to base64.
16
+ *
17
+ * Steps:
18
+ * 1. Convert the JS string (UTF-16) to UTF-8 byte values
19
+ * 2. Encode those bytes as base64
20
+ *
21
+ * Handles multi-byte characters and surrogate pairs correctly.
22
+ */
23
+ export function utf8ToBase64(str: string): string {
24
+ // Step 1: UTF-16 string → UTF-8 byte array
25
+ const bytes: number[] = [];
26
+ for (let i = 0; i < str.length; i++) {
27
+ let codePoint = str.codePointAt(i)!;
28
+ // Skip the low surrogate of a surrogate pair (codePointAt already decoded it)
29
+ if (codePoint > 0xffff) {
30
+ i++;
31
+ }
32
+
33
+ if (codePoint < 0x80) {
34
+ // 1-byte: ASCII (0xxxxxxx)
35
+ bytes.push(codePoint);
36
+ } else if (codePoint < 0x800) {
37
+ // 2-byte: (110xxxxx 10xxxxxx)
38
+ bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f));
39
+ } else if (codePoint < 0x10000) {
40
+ // 3-byte: (1110xxxx 10xxxxxx 10xxxxxx)
41
+ bytes.push(
42
+ 0xe0 | (codePoint >> 12),
43
+ 0x80 | ((codePoint >> 6) & 0x3f),
44
+ 0x80 | (codePoint & 0x3f)
45
+ );
46
+ } else {
47
+ // 4-byte: (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx) — for emoji, etc.
48
+ bytes.push(
49
+ 0xf0 | (codePoint >> 18),
50
+ 0x80 | ((codePoint >> 12) & 0x3f),
51
+ 0x80 | ((codePoint >> 6) & 0x3f),
52
+ 0x80 | (codePoint & 0x3f)
53
+ );
54
+ }
55
+ }
56
+
57
+ // Step 2: Byte array → Base64 string (3 bytes per group → 4 base64 chars)
58
+ let result = '';
59
+ for (let i = 0; i < bytes.length; i += 3) {
60
+ const a = bytes[i]!;
61
+ const b = i + 1 < bytes.length ? bytes[i + 1]! : 0;
62
+ const c = i + 2 < bytes.length ? bytes[i + 2]! : 0;
63
+
64
+ result += BASE64_CHARS[a >> 2];
65
+ result += BASE64_CHARS[((a & 3) << 4) | (b >> 4)];
66
+ result +=
67
+ i + 1 < bytes.length ? BASE64_CHARS[((b & 0xf) << 2) | (c >> 6)] : '=';
68
+ result += i + 2 < bytes.length ? BASE64_CHARS[c & 0x3f] : '=';
69
+ }
70
+ return result;
71
+ }
72
+
73
+ /**
74
+ * Decode a base64 string to a UTF-8 string.
75
+ *
76
+ * Steps:
77
+ * 1. Decode base64 → raw byte values
78
+ * 2. Interpret those bytes as UTF-8 and build a JS string
79
+ *
80
+ * Useful for reading decrypted plaintext that was originally UTF-8 text.
81
+ */
82
+ export function base64ToUtf8(base64: string): string {
83
+ // Step 1: Base64 string → byte array
84
+ const cleaned = base64.replace(/[^A-Za-z0-9+/=]/g, '');
85
+ const bytes: number[] = [];
86
+ for (let i = 0; i < cleaned.length; i += 4) {
87
+ const a = BASE64_CHARS.indexOf(cleaned[i]!);
88
+ const b = BASE64_CHARS.indexOf(cleaned[i + 1]!);
89
+ const c =
90
+ cleaned[i + 2] === '=' ? 0 : BASE64_CHARS.indexOf(cleaned[i + 2]!);
91
+ const d =
92
+ cleaned[i + 3] === '=' ? 0 : BASE64_CHARS.indexOf(cleaned[i + 3]!);
93
+
94
+ const bits = (a << 18) | (b << 12) | (c << 6) | d;
95
+ bytes.push((bits >> 16) & 0xff);
96
+ if (cleaned[i + 2] !== '=') {
97
+ bytes.push((bits >> 8) & 0xff);
98
+ }
99
+ if (cleaned[i + 3] !== '=') {
100
+ bytes.push(bits & 0xff);
101
+ }
102
+ }
103
+
104
+ // Step 2: UTF-8 byte array → JS string
105
+ let str = '';
106
+ let idx = 0;
107
+ while (idx < bytes.length) {
108
+ const byte = bytes[idx]!;
109
+ if (byte < 0x80) {
110
+ // 1-byte: ASCII
111
+ str += String.fromCodePoint(byte);
112
+ idx++;
113
+ } else if ((byte & 0xe0) === 0xc0) {
114
+ // 2-byte character
115
+ str += String.fromCodePoint(
116
+ ((byte & 0x1f) << 6) | (bytes[idx + 1]! & 0x3f)
117
+ );
118
+ idx += 2;
119
+ } else if ((byte & 0xf0) === 0xe0) {
120
+ // 3-byte character
121
+ str += String.fromCodePoint(
122
+ ((byte & 0x0f) << 12) |
123
+ ((bytes[idx + 1]! & 0x3f) << 6) |
124
+ (bytes[idx + 2]! & 0x3f)
125
+ );
126
+ idx += 3;
127
+ } else {
128
+ // 4-byte character (emoji, etc.)
129
+ const codePoint =
130
+ ((byte & 0x07) << 18) |
131
+ ((bytes[idx + 1]! & 0x3f) << 12) |
132
+ ((bytes[idx + 2]! & 0x3f) << 6) |
133
+ (bytes[idx + 3]! & 0x3f);
134
+ str += String.fromCodePoint(codePoint);
135
+ idx += 4;
136
+ }
137
+ }
138
+ return str;
139
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,206 @@
1
+ import { VALID_KEY_SIZES } from './constants';
2
+
3
+ /**
4
+ * Error codes thrown by @avieldr/react-native-rsa.
5
+ *
6
+ * Validation errors (thrown before native call):
7
+ * - INVALID_INPUT: Required parameter is missing or empty
8
+ * - INVALID_KEY: Key format is wrong or key type mismatch
9
+ * - INVALID_KEY_SIZE: Unsupported key size
10
+ * - INVALID_PADDING: Unknown padding mode
11
+ * - INVALID_HASH: Unknown hash algorithm
12
+ * - INVALID_FORMAT: Unknown key format
13
+ * - INVALID_ENCODING: Unknown encoding type
14
+ *
15
+ * Native operation errors (thrown by platform crypto):
16
+ * - KEY_GENERATION_FAILED: Native key generation failed
17
+ * - KEY_EXTRACTION_FAILED: Failed to extract public key from private
18
+ * - KEY_CONVERSION_FAILED: Failed to convert key format
19
+ * - ENCRYPTION_FAILED: Native encryption operation failed
20
+ * - DECRYPTION_FAILED: Native decryption operation failed
21
+ * - SIGNING_FAILED: Native signing operation failed
22
+ * - VERIFICATION_FAILED: Native signature verification failed
23
+ */
24
+ export type RsaErrorCode =
25
+ // Validation errors
26
+ | 'INVALID_INPUT'
27
+ | 'INVALID_KEY'
28
+ | 'INVALID_KEY_SIZE'
29
+ | 'INVALID_PADDING'
30
+ | 'INVALID_HASH'
31
+ | 'INVALID_FORMAT'
32
+ | 'INVALID_ENCODING'
33
+ // Native operation errors
34
+ | 'KEY_GENERATION_FAILED'
35
+ | 'KEY_EXTRACTION_FAILED'
36
+ | 'KEY_CONVERSION_FAILED'
37
+ | 'ENCRYPTION_FAILED'
38
+ | 'DECRYPTION_FAILED'
39
+ | 'SIGNING_FAILED'
40
+ | 'VERIFICATION_FAILED';
41
+
42
+ /**
43
+ * Error thrown by @avieldr/react-native-rsa for invalid inputs or native failures.
44
+ * Extends Error with a `code` property for programmatic handling.
45
+ */
46
+ export class RsaError extends Error {
47
+ readonly code: RsaErrorCode;
48
+ /** The original error from the native layer, if any */
49
+ readonly cause?: Error;
50
+
51
+ constructor(code: RsaErrorCode, message: string, cause?: Error) {
52
+ super(message);
53
+ this.name = 'RsaError';
54
+ this.code = code;
55
+ this.cause = cause;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Map of native error codes to RsaErrorCode.
61
+ * Native modules reject with codes like "RSAEncryptError" — we normalize them.
62
+ */
63
+ const NATIVE_ERROR_CODE_MAP: Record<string, RsaErrorCode> = {
64
+ RSAKeyGenerationError: 'KEY_GENERATION_FAILED',
65
+ RSAKeyExtractionError: 'KEY_EXTRACTION_FAILED',
66
+ RSAConvertKeyError: 'KEY_CONVERSION_FAILED',
67
+ RSAEncryptError: 'ENCRYPTION_FAILED',
68
+ RSADecryptError: 'DECRYPTION_FAILED',
69
+ RSASignError: 'SIGNING_FAILED',
70
+ RSAVerifyError: 'VERIFICATION_FAILED',
71
+ };
72
+
73
+ /**
74
+ * Wrap a native error into an RsaError with a normalized code.
75
+ * If the error is already an RsaError, returns it unchanged.
76
+ */
77
+ export function wrapNativeError(error: unknown, fallbackCode: RsaErrorCode): RsaError {
78
+ if (error instanceof RsaError) {
79
+ return error;
80
+ }
81
+
82
+ if (error instanceof Error) {
83
+ // React Native errors from native modules have a `code` property
84
+ const nativeCode = (error as { code?: string }).code;
85
+ const code = (nativeCode && NATIVE_ERROR_CODE_MAP[nativeCode]) || fallbackCode;
86
+ return new RsaError(code, error.message, error);
87
+ }
88
+
89
+ // Unknown error type
90
+ return new RsaError(fallbackCode, String(error));
91
+ }
92
+
93
+ // --- Validation helpers ---
94
+
95
+ export function requireString(
96
+ value: unknown,
97
+ name: string
98
+ ): asserts value is string {
99
+ if (typeof value !== 'string' || value.length === 0) {
100
+ throw new RsaError('INVALID_INPUT', `'${name}' must be a non-empty string`);
101
+ }
102
+ }
103
+
104
+ export function requirePrivateKey(pem: string): void {
105
+ requireString(pem, 'privateKey');
106
+ if (
107
+ pem.includes('BEGIN PUBLIC KEY')
108
+ ) {
109
+ throw new RsaError(
110
+ 'INVALID_KEY',
111
+ 'Expected a private key (BEGIN RSA PRIVATE KEY or BEGIN PRIVATE KEY), but received a public key'
112
+ );
113
+ }
114
+ if (
115
+ !pem.includes('BEGIN RSA PRIVATE KEY') &&
116
+ !pem.includes('BEGIN PRIVATE KEY')
117
+ ) {
118
+ throw new RsaError(
119
+ 'INVALID_KEY',
120
+ 'Expected a private key (BEGIN RSA PRIVATE KEY or BEGIN PRIVATE KEY)'
121
+ );
122
+ }
123
+ }
124
+
125
+ export function requirePublicKey(pem: string): void {
126
+ requireString(pem, 'publicKey');
127
+ if (
128
+ pem.includes('BEGIN RSA PRIVATE KEY') ||
129
+ pem.includes('BEGIN PRIVATE KEY')
130
+ ) {
131
+ throw new RsaError(
132
+ 'INVALID_KEY',
133
+ 'Expected a public key (BEGIN PUBLIC KEY), but received a private key'
134
+ );
135
+ }
136
+ if (!pem.includes('BEGIN PUBLIC KEY')) {
137
+ throw new RsaError(
138
+ 'INVALID_KEY',
139
+ 'Expected a public key (BEGIN PUBLIC KEY)'
140
+ );
141
+ }
142
+ }
143
+
144
+ export function validateKeySize(keySize: number): void {
145
+ if (!(VALID_KEY_SIZES as readonly number[]).includes(keySize)) {
146
+ throw new RsaError(
147
+ 'INVALID_KEY_SIZE',
148
+ `Invalid key size ${keySize}. Supported sizes: ${VALID_KEY_SIZES.join(', ')}`
149
+ );
150
+ }
151
+ }
152
+
153
+ const VALID_ENCRYPTION_PADDINGS = ['oaep', 'pkcs1'];
154
+
155
+ export function validateEncryptionPadding(padding: string): void {
156
+ if (!VALID_ENCRYPTION_PADDINGS.includes(padding)) {
157
+ throw new RsaError(
158
+ 'INVALID_PADDING',
159
+ `Invalid encryption padding '${padding}'. Must be 'oaep' or 'pkcs1'`
160
+ );
161
+ }
162
+ }
163
+
164
+ const VALID_SIGNATURE_PADDINGS = ['pss', 'pkcs1'];
165
+
166
+ export function validateSignaturePadding(padding: string): void {
167
+ if (!VALID_SIGNATURE_PADDINGS.includes(padding)) {
168
+ throw new RsaError(
169
+ 'INVALID_PADDING',
170
+ `Invalid signature padding '${padding}'. Must be 'pss' or 'pkcs1'`
171
+ );
172
+ }
173
+ }
174
+
175
+ const VALID_HASHES = ['sha1', 'sha256', 'sha384', 'sha512'];
176
+
177
+ export function validateHash(hash: string): void {
178
+ if (!VALID_HASHES.includes(hash)) {
179
+ throw new RsaError(
180
+ 'INVALID_HASH',
181
+ `Invalid hash '${hash}'. Must be 'sha1', 'sha256', 'sha384', or 'sha512'`
182
+ );
183
+ }
184
+ }
185
+
186
+ const VALID_KEY_FORMATS = ['pkcs1', 'pkcs8'];
187
+
188
+ export function validateKeyFormat(format: string): void {
189
+ if (!VALID_KEY_FORMATS.includes(format)) {
190
+ throw new RsaError(
191
+ 'INVALID_FORMAT',
192
+ `Invalid key format '${format}'. Must be 'pkcs1' or 'pkcs8'`
193
+ );
194
+ }
195
+ }
196
+
197
+ const VALID_ENCODINGS = ['utf8', 'base64'];
198
+
199
+ export function validateEncoding(encoding: string): void {
200
+ if (!VALID_ENCODINGS.includes(encoding)) {
201
+ throw new RsaError(
202
+ 'INVALID_ENCODING',
203
+ `Invalid encoding '${encoding}'. Must be 'utf8' or 'base64'`
204
+ );
205
+ }
206
+ }