@avieldr/react-native-rsa 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +453 -0
- package/Rsa.podspec +23 -0
- package/android/build.gradle +69 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/rsa/RsaModule.kt +129 -0
- package/android/src/main/java/com/rsa/RsaPackage.kt +33 -0
- package/android/src/main/java/com/rsa/core/ASN1Utils.kt +201 -0
- package/android/src/main/java/com/rsa/core/Algorithms.kt +126 -0
- package/android/src/main/java/com/rsa/core/KeyUtils.kt +83 -0
- package/android/src/main/java/com/rsa/core/RSACipher.kt +71 -0
- package/android/src/main/java/com/rsa/core/RSAKeyGenerator.kt +125 -0
- package/android/src/main/java/com/rsa/core/RSASigner.kt +70 -0
- package/ios/ASN1Utils.swift +225 -0
- package/ios/Algorithms.swift +89 -0
- package/ios/KeyUtils.swift +125 -0
- package/ios/RSACipher.swift +77 -0
- package/ios/RSAKeyGenerator.swift +164 -0
- package/ios/RSASigner.swift +101 -0
- package/ios/Rsa.h +61 -0
- package/ios/Rsa.mm +216 -0
- package/lib/module/NativeRsa.js +16 -0
- package/lib/module/NativeRsa.js.map +1 -0
- package/lib/module/constants.js +24 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/encoding.js +116 -0
- package/lib/module/encoding.js.map +1 -0
- package/lib/module/errors.js +135 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +232 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/keyInfo.js +286 -0
- package/lib/module/keyInfo.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeRsa.d.ts +32 -0
- package/lib/typescript/src/NativeRsa.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +21 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/encoding.d.ts +30 -0
- package/lib/typescript/src/encoding.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +47 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +122 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/keyInfo.d.ts +7 -0
- package/lib/typescript/src/keyInfo.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +63 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +133 -0
- package/src/NativeRsa.ts +59 -0
- package/src/constants.ts +25 -0
- package/src/encoding.ts +139 -0
- package/src/errors.ts +206 -0
- package/src/index.ts +305 -0
- package/src/keyInfo.ts +334 -0
- package/src/types.ts +85 -0
|
@@ -0,0 +1,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
|
+
}
|
package/src/NativeRsa.ts
ADDED
|
@@ -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');
|
package/src/constants.ts
ADDED
|
@@ -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;
|
package/src/encoding.ts
ADDED
|
@@ -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
|
+
}
|