@hituchhimpa/react-native-auth-vault 0.1.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 +129 -0
- package/ReactNativeAuthVault.podspec +20 -0
- package/android/build.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/hituchhimpa/reactnativeauthvault/CryptoEngine.kt +190 -0
- package/android/src/main/java/com/hituchhimpa/reactnativeauthvault/ReactNativeAuthVaultModule.kt +103 -0
- package/android/src/main/java/com/hituchhimpa/reactnativeauthvault/ReactNativeAuthVaultPackage.kt +31 -0
- package/android/src/main/java/com/hituchhimpa/reactnativeauthvault/SecurityEngine.kt +75 -0
- package/ios/CryptoEngine.swift +160 -0
- package/ios/ReactNativeAuthVault.h +5 -0
- package/ios/ReactNativeAuthVault.mm +89 -0
- package/ios/SecurityEngine.swift +85 -0
- package/lib/module/NativeReactNativeAuthVault.js +5 -0
- package/lib/module/NativeReactNativeAuthVault.js.map +1 -0
- package/lib/module/index.js +12 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeReactNativeAuthVault.d.ts +12 -0
- package/lib/typescript/src/NativeReactNativeAuthVault.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +9 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +137 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hituchhimpa7
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# ๐ก๏ธ React Native Auth Vault
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@hituchhimpa/react-native-auth-vault)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](#)
|
|
6
|
+
|
|
7
|
+
A premium, native-first security and authentication library for React Native. Provides secure, hardware-backed cryptographic storage and runtime security auditing for enterprise-grade mobile applications.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## โจ Features
|
|
12
|
+
|
|
13
|
+
* ๐ **Hardware-Backed Encryption**: Private keys are generated and stored securely inside the **Android Keystore (TEE/StrongBox)** and **iOS Secure Enclave**.
|
|
14
|
+
* ๐ค **Biometric Protection**: FaceID, TouchID, and Android BiometricPrompt integration with customizable user prompts.
|
|
15
|
+
* โก **Optional Biometrics**: Supports both biometric-authenticated operations and high-performance silent hardware encryption (no prompts).
|
|
16
|
+
* ๐ก๏ธ **Security Auditing**: Run real-time checks to inspect device integrity (e.g. root/jailbreak detection, device passcode setup, biometrics status).
|
|
17
|
+
* ๐งต **Thread Safe & Native**: Constructed using React Native's modern architecture, executing complex cryptographic tasks on native threads.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ๐ฆ Installation
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install @hituchhimpa/react-native-auth-vault
|
|
25
|
+
# or
|
|
26
|
+
yarn add @hituchhimpa/react-native-auth-vault
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For iOS, install the pods:
|
|
30
|
+
```sh
|
|
31
|
+
cd ios && pod install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## ๐ Usage
|
|
37
|
+
|
|
38
|
+
### 1. Store & Retrieve with Biometric Protection
|
|
39
|
+
Prompt the user for biometrics (Face ID/Touch ID/Fingerprint/Passcode) to unlock access to the stored key:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { AuthVault } from '@hituchhimpa/react-native-auth-vault';
|
|
43
|
+
|
|
44
|
+
// Store a value securely (triggers biometric prompt)
|
|
45
|
+
const saveSecureToken = async (token: string) => {
|
|
46
|
+
try {
|
|
47
|
+
const success = await AuthVault.setItem(
|
|
48
|
+
'user_token',
|
|
49
|
+
token,
|
|
50
|
+
'Scan fingerprint to secure your credentials'
|
|
51
|
+
);
|
|
52
|
+
if (success) console.log('Stored securely!');
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Storage failed:', error);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Retrieve a value (triggers biometric prompt)
|
|
59
|
+
const getSecureToken = async () => {
|
|
60
|
+
try {
|
|
61
|
+
const token = await AuthVault.getItem(
|
|
62
|
+
'user_token',
|
|
63
|
+
'Scan fingerprint to access your account'
|
|
64
|
+
);
|
|
65
|
+
console.log('Retrieved Token:', token);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Failed to unlock token:', error);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Silent Hardware-Backed Storage (Optional Biometrics)
|
|
73
|
+
Encrypt and store keys using hardware cryptoprocessors (Secure Enclave / TEE) **silently** without prompting the user. Perfect for API request signing, background session tokens, or caching:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Pass an empty string `""` as the prompt to bypass the biometric UI
|
|
77
|
+
const saveSilentToken = async (token: string) => {
|
|
78
|
+
await AuthVault.setItem('api_key', token, '');
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const getSilentToken = async () => {
|
|
82
|
+
const token = await AuthVault.getItem('api_key', '');
|
|
83
|
+
return token; // Returns the token silently
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3. Device Security Auditing
|
|
88
|
+
Get security metrics to decide whether your app should run or restrict sensitive actions:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const checkDeviceSecurity = () => {
|
|
92
|
+
const audit = AuthVault.audit();
|
|
93
|
+
console.log(audit);
|
|
94
|
+
/*
|
|
95
|
+
Output:
|
|
96
|
+
{
|
|
97
|
+
isRooted: false, // Root/Jailbreak status
|
|
98
|
+
hasPin: true, // Device lock (PIN/Password) setup
|
|
99
|
+
biometricsEnabled: true, // Biometrics enrollment status
|
|
100
|
+
hardwareBacked: true // Key storage hardware backing status
|
|
101
|
+
}
|
|
102
|
+
*/
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## ๐ ๏ธ API Reference
|
|
109
|
+
|
|
110
|
+
| Method | Type | Description |
|
|
111
|
+
| :--- | :--- | :--- |
|
|
112
|
+
| `setItem(key, value, prompt)` | `Promise<boolean>` | Encrypts and saves `value` locally. Pass non-empty `prompt` for biometrics, or `""` for silent encryption. |
|
|
113
|
+
| `getItem(key, prompt)` | `Promise<string \| null>` | Decrypts and retrieves `value`. Pass non-empty `prompt` for biometrics, or `""` for silent decryption. |
|
|
114
|
+
| `removeItem(key)` | `Promise<boolean>` | Deletes the stored key and encrypted value from device. |
|
|
115
|
+
| `encrypt(plainText, prompt)` | `Promise<string>` | Encrypts raw text and returns a base64 cipher payload. |
|
|
116
|
+
| `decrypt(base64Text, prompt)` | `Promise<string>` | Decrypts a base64 cipher payload back to plain text. |
|
|
117
|
+
| `audit()` | `Object` | Runs security checks on the device hardware and environment. |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## ๐ค Author
|
|
122
|
+
|
|
123
|
+
Developed and maintained by [Hitesh Chhimpa](https://github.com/hituchhimpa7).
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## ๐ License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "ReactNativeAuthVault"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/hituchhimpa7/react-native-auth-vault.git.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
|
|
19
|
+
install_modules_dependencies(s)
|
|
20
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.ReactNativeAuthVault = [
|
|
3
|
+
kotlinVersion: "2.0.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36,
|
|
6
|
+
targetSdkVersion: 36
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
ext.getExtOrDefault = { prop ->
|
|
10
|
+
if (rootProject.ext.has(prop)) {
|
|
11
|
+
return rootProject.ext.get(prop)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return ReactNativeAuthVault[prop]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
repositories {
|
|
18
|
+
google()
|
|
19
|
+
mavenCentral()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dependencies {
|
|
23
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
24
|
+
// noinspection DifferentKotlinGradleVersion
|
|
25
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
apply plugin: "com.android.library"
|
|
31
|
+
apply plugin: "kotlin-android"
|
|
32
|
+
|
|
33
|
+
apply plugin: "com.facebook.react"
|
|
34
|
+
|
|
35
|
+
android {
|
|
36
|
+
namespace "com.hituchhimpa.reactnativeauthvault"
|
|
37
|
+
|
|
38
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
39
|
+
|
|
40
|
+
defaultConfig {
|
|
41
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
42
|
+
targetSdkVersion getExtOrDefault("targetSdkVersion")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildFeatures {
|
|
46
|
+
buildConfig true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
buildTypes {
|
|
50
|
+
release {
|
|
51
|
+
minifyEnabled false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lint {
|
|
56
|
+
disable "GradleCompatible"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
compileOptions {
|
|
60
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
61
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dependencies {
|
|
66
|
+
implementation "com.facebook.react:react-android"
|
|
67
|
+
implementation "androidx.biometric:biometric:1.2.0-alpha05"
|
|
68
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
package com.hituchhimpa.reactnativeauthvault
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import android.security.keystore.KeyGenParameterSpec
|
|
6
|
+
import android.security.keystore.KeyProperties
|
|
7
|
+
import android.util.Base64
|
|
8
|
+
import androidx.biometric.BiometricManager
|
|
9
|
+
import androidx.biometric.BiometricPrompt
|
|
10
|
+
import androidx.core.content.ContextCompat
|
|
11
|
+
import androidx.fragment.app.FragmentActivity
|
|
12
|
+
import java.security.KeyStore
|
|
13
|
+
import javax.crypto.Cipher
|
|
14
|
+
import javax.crypto.KeyGenerator
|
|
15
|
+
import javax.crypto.SecretKey
|
|
16
|
+
import javax.crypto.spec.GCMParameterSpec
|
|
17
|
+
|
|
18
|
+
class CryptoEngine(private val context: Context) {
|
|
19
|
+
private val keyAliasBiometric = "AuthVaultKey_Biometric"
|
|
20
|
+
private val keyAliasNonBiometric = "AuthVaultKey_NonBiometric"
|
|
21
|
+
private val androidKeyStore = "AndroidKeyStore"
|
|
22
|
+
|
|
23
|
+
init {
|
|
24
|
+
generateKey(keyAliasBiometric, true)
|
|
25
|
+
generateKey(keyAliasNonBiometric, false)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private fun generateKey(alias: String, requireAuth: Boolean) {
|
|
29
|
+
val keyStore = KeyStore.getInstance(androidKeyStore)
|
|
30
|
+
keyStore.load(null)
|
|
31
|
+
|
|
32
|
+
if (!keyStore.containsAlias(alias)) {
|
|
33
|
+
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, androidKeyStore)
|
|
34
|
+
|
|
35
|
+
val builder = KeyGenParameterSpec.Builder(
|
|
36
|
+
alias,
|
|
37
|
+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
|
38
|
+
)
|
|
39
|
+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
|
40
|
+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
|
41
|
+
.setUserAuthenticationRequired(requireAuth)
|
|
42
|
+
|
|
43
|
+
if (requireAuth) {
|
|
44
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
45
|
+
builder.setUserAuthenticationParameters(
|
|
46
|
+
0,
|
|
47
|
+
KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
|
|
48
|
+
)
|
|
49
|
+
} else {
|
|
50
|
+
@Suppress("DEPRECATION")
|
|
51
|
+
builder.setUserAuthenticationValidityDurationSeconds(-1)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
56
|
+
if (context.packageManager.hasSystemFeature("android.hardware.strongbox_keystore")) {
|
|
57
|
+
builder.setIsStrongBoxBacked(true)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
keyGenerator.init(builder.build())
|
|
62
|
+
keyGenerator.generateKey()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private fun getCipher(): Cipher {
|
|
67
|
+
return Cipher.getInstance("${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_GCM}/${KeyProperties.ENCRYPTION_PADDING_NONE}")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private fun getSecretKey(alias: String): SecretKey {
|
|
71
|
+
val keyStore = KeyStore.getInstance(androidKeyStore)
|
|
72
|
+
keyStore.load(null)
|
|
73
|
+
return keyStore.getKey(alias, null) as SecretKey
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fun encrypt(activity: FragmentActivity, plainText: String, title: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
|
77
|
+
val requireAuth = title.isNotEmpty()
|
|
78
|
+
if (requireAuth) {
|
|
79
|
+
authenticate(activity, Cipher.ENCRYPT_MODE, null, title, { cipher ->
|
|
80
|
+
try {
|
|
81
|
+
val iv = cipher.iv
|
|
82
|
+
val encryptedData = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
|
|
83
|
+
|
|
84
|
+
val combined = ByteArray(iv.size + encryptedData.size)
|
|
85
|
+
System.arraycopy(iv, 0, combined, 0, iv.size)
|
|
86
|
+
System.arraycopy(encryptedData, 0, combined, iv.size, encryptedData.size)
|
|
87
|
+
|
|
88
|
+
onSuccess(Base64.encodeToString(combined, Base64.DEFAULT))
|
|
89
|
+
} catch (e: Exception) {
|
|
90
|
+
onError("Encryption failed: ${e.message}")
|
|
91
|
+
}
|
|
92
|
+
}, onError)
|
|
93
|
+
} else {
|
|
94
|
+
try {
|
|
95
|
+
val cipher = getCipher()
|
|
96
|
+
val secretKey = getSecretKey(keyAliasNonBiometric)
|
|
97
|
+
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
|
98
|
+
val iv = cipher.iv
|
|
99
|
+
val encryptedData = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
|
|
100
|
+
|
|
101
|
+
val combined = ByteArray(iv.size + encryptedData.size)
|
|
102
|
+
System.arraycopy(iv, 0, combined, 0, iv.size)
|
|
103
|
+
System.arraycopy(encryptedData, 0, combined, iv.size, encryptedData.size)
|
|
104
|
+
|
|
105
|
+
onSuccess(Base64.encodeToString(combined, Base64.DEFAULT))
|
|
106
|
+
} catch (e: Exception) {
|
|
107
|
+
onError("Encryption failed: ${e.message}")
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fun decrypt(activity: FragmentActivity, encryptedBase64: String, title: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
|
113
|
+
val requireAuth = title.isNotEmpty()
|
|
114
|
+
try {
|
|
115
|
+
val combined = Base64.decode(encryptedBase64, Base64.DEFAULT)
|
|
116
|
+
// GCM IV is 12 bytes
|
|
117
|
+
val iv = ByteArray(12)
|
|
118
|
+
val encryptedData = ByteArray(combined.size - 12)
|
|
119
|
+
System.arraycopy(combined, 0, iv, 0, 12)
|
|
120
|
+
System.arraycopy(combined, 12, encryptedData, 0, encryptedData.size)
|
|
121
|
+
|
|
122
|
+
if (requireAuth) {
|
|
123
|
+
authenticate(activity, Cipher.DECRYPT_MODE, iv, title, { cipher ->
|
|
124
|
+
try {
|
|
125
|
+
val decryptedData = cipher.doFinal(encryptedData)
|
|
126
|
+
onSuccess(String(decryptedData, Charsets.UTF_8))
|
|
127
|
+
} catch (e: Exception) {
|
|
128
|
+
onError("Decryption failed: ${e.message}")
|
|
129
|
+
}
|
|
130
|
+
}, onError)
|
|
131
|
+
} else {
|
|
132
|
+
try {
|
|
133
|
+
val cipher = getCipher()
|
|
134
|
+
val secretKey = getSecretKey(keyAliasNonBiometric)
|
|
135
|
+
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, iv))
|
|
136
|
+
val decryptedData = cipher.doFinal(encryptedData)
|
|
137
|
+
onSuccess(String(decryptedData, Charsets.UTF_8))
|
|
138
|
+
} catch (e: Exception) {
|
|
139
|
+
onError("Decryption failed: ${e.message}")
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (e: Exception) {
|
|
143
|
+
onError("Invalid data format")
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private fun authenticate(activity: FragmentActivity, mode: Int, iv: ByteArray?, title: String, onSuccess: (Cipher) -> Unit, onError: (String) -> Unit) {
|
|
148
|
+
activity.runOnUiThread {
|
|
149
|
+
val executor = ContextCompat.getMainExecutor(context)
|
|
150
|
+
val biometricPrompt = BiometricPrompt(activity, executor,
|
|
151
|
+
object : BiometricPrompt.AuthenticationCallback() {
|
|
152
|
+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
153
|
+
super.onAuthenticationError(errorCode, errString)
|
|
154
|
+
onError("Authentication error: $errString")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
158
|
+
super.onAuthenticationSucceeded(result)
|
|
159
|
+
result.cryptoObject?.cipher?.let {
|
|
160
|
+
onSuccess(it)
|
|
161
|
+
} ?: onError("Cipher not initialized")
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
override fun onAuthenticationFailed() {
|
|
165
|
+
super.onAuthenticationFailed()
|
|
166
|
+
onError("Authentication failed")
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
171
|
+
.setTitle(title)
|
|
172
|
+
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)
|
|
173
|
+
.build()
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
val cipher = getCipher()
|
|
177
|
+
val secretKey = getSecretKey(keyAliasBiometric)
|
|
178
|
+
if (mode == Cipher.ENCRYPT_MODE) {
|
|
179
|
+
cipher.init(mode, secretKey)
|
|
180
|
+
} else {
|
|
181
|
+
if (iv == null) throw IllegalArgumentException("IV required for decryption")
|
|
182
|
+
cipher.init(mode, secretKey, GCMParameterSpec(128, iv))
|
|
183
|
+
}
|
|
184
|
+
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
|
185
|
+
} catch (e: Exception) {
|
|
186
|
+
onError("Crypto initialization failed: ${e.message}")
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
package/android/src/main/java/com/hituchhimpa/reactnativeauthvault/ReactNativeAuthVaultModule.kt
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
package com.hituchhimpa.reactnativeauthvault
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Promise
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.bridge.WritableMap
|
|
6
|
+
import com.facebook.react.bridge.Arguments
|
|
7
|
+
import androidx.fragment.app.FragmentActivity
|
|
8
|
+
|
|
9
|
+
class ReactNativeAuthVaultModule(reactContext: ReactApplicationContext) :
|
|
10
|
+
NativeReactNativeAuthVaultSpec(reactContext) {
|
|
11
|
+
|
|
12
|
+
private val cryptoEngine = CryptoEngine(reactContext)
|
|
13
|
+
|
|
14
|
+
override fun audit(): WritableMap {
|
|
15
|
+
val map = Arguments.createMap()
|
|
16
|
+
val auditResult = SecurityEngine.audit(reactApplicationContext)
|
|
17
|
+
|
|
18
|
+
for ((key, value) in auditResult) {
|
|
19
|
+
when (value) {
|
|
20
|
+
is Boolean -> map.putBoolean(key, value)
|
|
21
|
+
is Int -> map.putInt(key, value)
|
|
22
|
+
is Double -> map.putDouble(key, value)
|
|
23
|
+
is String -> map.putString(key, value)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return map
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override fun encrypt(plainText: String, prompt: String, promise: Promise) {
|
|
31
|
+
val activity = currentActivity as? FragmentActivity
|
|
32
|
+
if (activity == null) {
|
|
33
|
+
promise.reject("ERR_ACTIVITY", "Activity is null or not a FragmentActivity")
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
cryptoEngine.encrypt(activity, plainText, prompt,
|
|
38
|
+
onSuccess = { encrypted -> promise.resolve(encrypted) },
|
|
39
|
+
onError = { error -> promise.reject("ERR_ENCRYPT", error) }
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override fun decrypt(encryptedBase64: String, prompt: String, promise: Promise) {
|
|
44
|
+
val activity = currentActivity as? FragmentActivity
|
|
45
|
+
if (activity == null) {
|
|
46
|
+
promise.reject("ERR_ACTIVITY", "Activity is null or not a FragmentActivity")
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
cryptoEngine.decrypt(activity, encryptedBase64, prompt,
|
|
51
|
+
onSuccess = { decrypted -> promise.resolve(decrypted) },
|
|
52
|
+
onError = { error -> promise.reject("ERR_DECRYPT", error) }
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun getSharedPreferences(): android.content.SharedPreferences {
|
|
57
|
+
return reactApplicationContext.getSharedPreferences("ReactNativeAuthVaultStorage", android.content.Context.MODE_PRIVATE)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override fun setItem(key: String, value: String, prompt: String, promise: Promise) {
|
|
61
|
+
val activity = currentActivity as? FragmentActivity
|
|
62
|
+
if (activity == null) {
|
|
63
|
+
promise.reject("ERR_ACTIVITY", "Activity is null or not a FragmentActivity")
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
cryptoEngine.encrypt(activity, value, prompt,
|
|
68
|
+
onSuccess = { encrypted ->
|
|
69
|
+
getSharedPreferences().edit().putString(key, encrypted).apply()
|
|
70
|
+
promise.resolve(true)
|
|
71
|
+
},
|
|
72
|
+
onError = { error -> promise.reject("ERR_ENCRYPT", error) }
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override fun getItem(key: String, prompt: String, promise: Promise) {
|
|
77
|
+
val encrypted = getSharedPreferences().getString(key, null)
|
|
78
|
+
if (encrypted == null) {
|
|
79
|
+
promise.resolve(null)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
val activity = currentActivity as? FragmentActivity
|
|
84
|
+
if (activity == null) {
|
|
85
|
+
promise.reject("ERR_ACTIVITY", "Activity is null or not a FragmentActivity")
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
cryptoEngine.decrypt(activity, encrypted, prompt,
|
|
90
|
+
onSuccess = { decrypted -> promise.resolve(decrypted) },
|
|
91
|
+
onError = { error -> promise.reject("ERR_DECRYPT", error) }
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override fun removeItem(key: String, promise: Promise) {
|
|
96
|
+
getSharedPreferences().edit().remove(key).apply()
|
|
97
|
+
promise.resolve(true)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
companion object {
|
|
101
|
+
const val NAME = NativeReactNativeAuthVaultSpec.NAME
|
|
102
|
+
}
|
|
103
|
+
}
|
package/android/src/main/java/com/hituchhimpa/reactnativeauthvault/ReactNativeAuthVaultPackage.kt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.hituchhimpa.reactnativeauthvault
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class ReactNativeAuthVaultPackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == ReactNativeAuthVaultModule.NAME) {
|
|
13
|
+
ReactNativeAuthVaultModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
|
20
|
+
mapOf(
|
|
21
|
+
ReactNativeAuthVaultModule.NAME to ReactModuleInfo(
|
|
22
|
+
name = ReactNativeAuthVaultModule.NAME,
|
|
23
|
+
className = ReactNativeAuthVaultModule.NAME,
|
|
24
|
+
canOverrideExistingModule = false,
|
|
25
|
+
needsEagerInit = false,
|
|
26
|
+
isCxxModule = false,
|
|
27
|
+
isTurboModule = true
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package com.hituchhimpa.reactnativeauthvault
|
|
2
|
+
|
|
3
|
+
import android.os.Build
|
|
4
|
+
import android.os.Debug
|
|
5
|
+
import java.io.File
|
|
6
|
+
|
|
7
|
+
object SecurityEngine {
|
|
8
|
+
|
|
9
|
+
fun audit(context: android.content.Context): Map<String, Any> {
|
|
10
|
+
val rooted = isRooted()
|
|
11
|
+
val emulator = isEmulator()
|
|
12
|
+
val debugger = isDebuggerAttached()
|
|
13
|
+
|
|
14
|
+
val hardwareBacked = context.packageManager.hasSystemFeature("android.hardware.strongbox_keystore")
|
|
15
|
+
val biometricEnabled = androidx.biometric.BiometricManager.from(context).canAuthenticate(androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG) == androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
|
|
16
|
+
|
|
17
|
+
var score = 100
|
|
18
|
+
if (rooted) score -= 50
|
|
19
|
+
if (emulator) score -= 20
|
|
20
|
+
if (debugger) score -= 20
|
|
21
|
+
if (!hardwareBacked) score -= 10
|
|
22
|
+
if (!biometricEnabled) score -= 5
|
|
23
|
+
|
|
24
|
+
return mapOf(
|
|
25
|
+
"secureStorage" to true,
|
|
26
|
+
"hardwareBacked" to hardwareBacked,
|
|
27
|
+
"biometricEnabled" to biometricEnabled,
|
|
28
|
+
"rooted" to rooted,
|
|
29
|
+
"jailbroken" to false,
|
|
30
|
+
"emulator" to emulator,
|
|
31
|
+
"debuggerAttached" to debugger,
|
|
32
|
+
"securityScore" to score
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private fun isRooted(): Boolean {
|
|
37
|
+
val buildTags = Build.TAGS
|
|
38
|
+
if (buildTags != null && buildTags.contains("test-keys")) {
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
val paths = arrayOf(
|
|
43
|
+
"/system/app/Superuser.apk",
|
|
44
|
+
"/sbin/su",
|
|
45
|
+
"/system/bin/su",
|
|
46
|
+
"/system/xbin/su",
|
|
47
|
+
"/data/local/xbin/su",
|
|
48
|
+
"/data/local/bin/su",
|
|
49
|
+
"/system/sd/xbin/su",
|
|
50
|
+
"/system/bin/failsafe/su",
|
|
51
|
+
"/data/local/su",
|
|
52
|
+
"/su/bin/su"
|
|
53
|
+
)
|
|
54
|
+
for (path in paths) {
|
|
55
|
+
if (File(path).exists()) return true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private fun isEmulator(): Boolean {
|
|
62
|
+
return (Build.FINGERPRINT.startsWith("generic")
|
|
63
|
+
|| Build.FINGERPRINT.startsWith("unknown")
|
|
64
|
+
|| Build.MODEL.contains("google_sdk")
|
|
65
|
+
|| Build.MODEL.contains("Emulator")
|
|
66
|
+
|| Build.MODEL.contains("Android SDK built for x86")
|
|
67
|
+
|| Build.MANUFACTURER.contains("Genymotion")
|
|
68
|
+
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|
|
69
|
+
|| "google_sdk" == Build.PRODUCT)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private fun isDebuggerAttached(): Boolean {
|
|
73
|
+
return Debug.isDebuggerConnected() || Debug.waitingForDebugger()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import LocalAuthentication
|
|
3
|
+
import Security
|
|
4
|
+
|
|
5
|
+
@objc(CryptoEngine)
|
|
6
|
+
public class CryptoEngine: NSObject {
|
|
7
|
+
private let keyTagBiometric = "com.hituchhimpa.reactnativeauthvault.key.biometric".data(using: .utf8)!
|
|
8
|
+
private let keyTagNonBiometric = "com.hituchhimpa.reactnativeauthvault.key.nonbiometric".data(using: .utf8)!
|
|
9
|
+
|
|
10
|
+
@objc
|
|
11
|
+
public func initializeKey() {
|
|
12
|
+
if getPublicKey(useBiometric: true) == nil {
|
|
13
|
+
try? generateKey(useBiometric: true)
|
|
14
|
+
}
|
|
15
|
+
if getPublicKey(useBiometric: false) == nil {
|
|
16
|
+
try? generateKey(useBiometric: false)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private func generateKey(useBiometric: Bool) throws {
|
|
21
|
+
var error: Unmanaged<CFError>?
|
|
22
|
+
|
|
23
|
+
let access: SecAccessControl
|
|
24
|
+
if useBiometric {
|
|
25
|
+
// Require FaceID / TouchID for private key usage
|
|
26
|
+
guard let acc = SecAccessControlCreateWithFlags(
|
|
27
|
+
kCFAllocatorDefault,
|
|
28
|
+
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
29
|
+
[.userPresence, .privateKeyUsage],
|
|
30
|
+
&error
|
|
31
|
+
) else {
|
|
32
|
+
throw error!.takeRetainedValue() as Error
|
|
33
|
+
}
|
|
34
|
+
access = acc
|
|
35
|
+
} else {
|
|
36
|
+
guard let acc = SecAccessControlCreateWithFlags(
|
|
37
|
+
kCFAllocatorDefault,
|
|
38
|
+
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
39
|
+
[.privateKeyUsage],
|
|
40
|
+
&error
|
|
41
|
+
) else {
|
|
42
|
+
throw error!.takeRetainedValue() as Error
|
|
43
|
+
}
|
|
44
|
+
access = acc
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let tag = useBiometric ? keyTagBiometric : keyTagNonBiometric
|
|
48
|
+
let attributes: [String: Any] = [
|
|
49
|
+
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
|
50
|
+
kSecAttrKeySizeInBits as String: 256,
|
|
51
|
+
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
|
|
52
|
+
kSecPrivateKeyAttrs as String: [
|
|
53
|
+
kSecAttrIsPermanent as String: true,
|
|
54
|
+
kSecAttrApplicationTag as String: tag,
|
|
55
|
+
kSecAttrAccessControl as String: access
|
|
56
|
+
]
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
let status = SecKeyCreateRandomKey(attributes as CFDictionary, &error)
|
|
60
|
+
if status == nil, let error = error {
|
|
61
|
+
throw error.takeRetainedValue() as Error
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private func getKey(useBiometric: Bool, prompt: String = "Authenticate to access key") -> SecKey? {
|
|
66
|
+
let tag = useBiometric ? keyTagBiometric : keyTagNonBiometric
|
|
67
|
+
var query: [String: Any] = [
|
|
68
|
+
kSecClass as String: kSecClassKey,
|
|
69
|
+
kSecAttrApplicationTag as String: tag,
|
|
70
|
+
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
|
71
|
+
kSecReturnRef as String: true
|
|
72
|
+
]
|
|
73
|
+
if useBiometric {
|
|
74
|
+
query[kSecUseOperationPrompt as String] = prompt
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
var item: CFTypeRef?
|
|
78
|
+
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
|
79
|
+
if status == errSecSuccess {
|
|
80
|
+
return (item as! SecKey)
|
|
81
|
+
}
|
|
82
|
+
return nil
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@objc
|
|
86
|
+
public func encrypt(plainText: String, prompt: String, completion: @escaping (String?, String?) -> Void) {
|
|
87
|
+
let useBiometric = !prompt.isEmpty
|
|
88
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
89
|
+
guard let publicKey = self.getPublicKey(useBiometric: useBiometric) else {
|
|
90
|
+
completion(nil, "Key not found")
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
guard let data = plainText.data(using: .utf8) else {
|
|
95
|
+
completion(nil, "Invalid string")
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var error: Unmanaged<CFError>?
|
|
100
|
+
guard let encryptedData = SecKeyCreateEncryptedData(
|
|
101
|
+
publicKey,
|
|
102
|
+
.eciesEncryptionStandardX963SHA256AESGCM,
|
|
103
|
+
data as CFData,
|
|
104
|
+
&error
|
|
105
|
+
) as Data? else {
|
|
106
|
+
completion(nil, error?.takeRetainedValue().localizedDescription ?? "Encryption failed")
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
completion(encryptedData.base64EncodedString(), nil)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@objc
|
|
115
|
+
public func decrypt(encryptedBase64: String, prompt: String, completion: @escaping (String?, String?) -> Void) {
|
|
116
|
+
let useBiometric = !prompt.isEmpty
|
|
117
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
118
|
+
guard let privateKey = self.getKey(useBiometric: useBiometric, prompt: prompt) else {
|
|
119
|
+
completion(nil, "Authentication failed or key not found")
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
guard let data = Data(base64Encoded: encryptedBase64) else {
|
|
124
|
+
completion(nil, "Invalid base64")
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
var error: Unmanaged<CFError>?
|
|
129
|
+
guard let decryptedData = SecKeyCreateDecryptedData(
|
|
130
|
+
privateKey,
|
|
131
|
+
.eciesEncryptionStandardX963SHA256AESGCM,
|
|
132
|
+
data as CFData,
|
|
133
|
+
&error
|
|
134
|
+
) as Data? else {
|
|
135
|
+
completion(nil, error?.takeRetainedValue().localizedDescription ?? "Decryption failed")
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
completion(String(data: decryptedData, encoding: .utf8), nil)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private func getPublicKey(useBiometric: Bool) -> SecKey? {
|
|
144
|
+
let tag = useBiometric ? keyTagBiometric : keyTagNonBiometric
|
|
145
|
+
let query: [String: Any] = [
|
|
146
|
+
kSecClass as String: kSecClassKey,
|
|
147
|
+
kSecAttrApplicationTag as String: tag,
|
|
148
|
+
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
|
149
|
+
kSecReturnRef as String: true
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
var item: CFTypeRef?
|
|
153
|
+
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
|
154
|
+
if status == errSecSuccess {
|
|
155
|
+
let privateKey = item as! SecKey
|
|
156
|
+
return SecKeyCopyPublicKey(privateKey)
|
|
157
|
+
}
|
|
158
|
+
return nil
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#import "ReactNativeAuthVault.h"
|
|
2
|
+
|
|
3
|
+
#if __has_include("react_native_auth_vault/react_native_auth_vault-Swift.h")
|
|
4
|
+
#import "react_native_auth_vault/react_native_auth_vault-Swift.h"
|
|
5
|
+
#else
|
|
6
|
+
#import "react_native_auth_vault-Swift.h"
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
@implementation ReactNativeAuthVault {
|
|
10
|
+
CryptoEngine *_cryptoEngine;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
- (instancetype)init {
|
|
14
|
+
self = [super init];
|
|
15
|
+
if (self) {
|
|
16
|
+
_cryptoEngine = [[CryptoEngine alloc] init];
|
|
17
|
+
[_cryptoEngine initializeKey];
|
|
18
|
+
}
|
|
19
|
+
return self;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
- (NSDictionary *)audit {
|
|
23
|
+
return [SecurityEngine audit];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
- (void)encrypt:(NSString *)plainText prompt:(NSString *)prompt resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
27
|
+
[_cryptoEngine encryptWithPlainText:plainText prompt:prompt completion:^(NSString * _Nullable encryptedBase64, NSString * _Nullable error) {
|
|
28
|
+
if (error) {
|
|
29
|
+
reject(@"ERR_ENCRYPT", error, nil);
|
|
30
|
+
} else {
|
|
31
|
+
resolve(encryptedBase64);
|
|
32
|
+
}
|
|
33
|
+
}];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- (void)decrypt:(NSString *)encryptedBase64 prompt:(NSString *)prompt resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
37
|
+
[_cryptoEngine decryptWithEncryptedBase64:encryptedBase64 prompt:prompt completion:^(NSString * _Nullable decryptedText, NSString * _Nullable error) {
|
|
38
|
+
if (error) {
|
|
39
|
+
reject(@"ERR_DECRYPT", error, nil);
|
|
40
|
+
} else {
|
|
41
|
+
resolve(decryptedText);
|
|
42
|
+
}
|
|
43
|
+
}];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
- (void)setItem:(NSString *)key value:(NSString *)value prompt:(NSString *)prompt resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
47
|
+
[_cryptoEngine encryptWithPlainText:value prompt:prompt completion:^(NSString * _Nullable encryptedBase64, NSString * _Nullable error) {
|
|
48
|
+
if (error) {
|
|
49
|
+
reject(@"ERR_ENCRYPT", error, nil);
|
|
50
|
+
} else {
|
|
51
|
+
[[NSUserDefaults standardUserDefaults] setObject:encryptedBase64 forKey:key];
|
|
52
|
+
resolve(@(YES));
|
|
53
|
+
}
|
|
54
|
+
}];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
- (void)getItem:(NSString *)key prompt:(NSString *)prompt resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
58
|
+
NSString *encryptedBase64 = [[NSUserDefaults standardUserDefaults] stringForKey:key];
|
|
59
|
+
if (!encryptedBase64) {
|
|
60
|
+
resolve([NSNull null]);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
[_cryptoEngine decryptWithEncryptedBase64:encryptedBase64 prompt:prompt completion:^(NSString * _Nullable decryptedText, NSString * _Nullable error) {
|
|
65
|
+
if (error) {
|
|
66
|
+
reject(@"ERR_DECRYPT", error, nil);
|
|
67
|
+
} else {
|
|
68
|
+
resolve(decryptedText);
|
|
69
|
+
}
|
|
70
|
+
}];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
- (void)removeItem:(NSString *)key resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
74
|
+
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
|
|
75
|
+
resolve(@(YES));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
79
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
80
|
+
{
|
|
81
|
+
return std::make_shared<facebook::react::NativeReactNativeAuthVaultSpecJSI>(params);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
+ (NSString *)moduleName
|
|
85
|
+
{
|
|
86
|
+
return @"ReactNativeAuthVault";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import Darwin
|
|
4
|
+
|
|
5
|
+
@objc(SecurityEngine)
|
|
6
|
+
public class SecurityEngine: NSObject {
|
|
7
|
+
|
|
8
|
+
@objc
|
|
9
|
+
public static func audit() -> [String: Any] {
|
|
10
|
+
let jailbroken = isJailbroken()
|
|
11
|
+
let emulator = isEmulator()
|
|
12
|
+
let debugger = isDebuggerAttached()
|
|
13
|
+
|
|
14
|
+
let context = LAContext()
|
|
15
|
+
var error: NSError?
|
|
16
|
+
let biometricEnabled = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
|
|
17
|
+
|
|
18
|
+
var score = 100
|
|
19
|
+
if jailbroken { score -= 50 }
|
|
20
|
+
if emulator { score -= 20 }
|
|
21
|
+
if debugger { score -= 20 }
|
|
22
|
+
if !biometricEnabled { score -= 5 }
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
"secureStorage": true,
|
|
26
|
+
"hardwareBacked": true,
|
|
27
|
+
"biometricEnabled": biometricEnabled,
|
|
28
|
+
"rooted": false,
|
|
29
|
+
"jailbroken": jailbroken,
|
|
30
|
+
"emulator": emulator,
|
|
31
|
+
"debuggerAttached": debugger,
|
|
32
|
+
"securityScore": score
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private static func isJailbroken() -> Bool {
|
|
37
|
+
#if targetEnvironment(simulator)
|
|
38
|
+
return false
|
|
39
|
+
#else
|
|
40
|
+
let paths = [
|
|
41
|
+
"/Applications/Cydia.app",
|
|
42
|
+
"/Library/MobileSubstrate/MobileSubstrate.dylib",
|
|
43
|
+
"/bin/bash",
|
|
44
|
+
"/usr/sbin/sshd",
|
|
45
|
+
"/etc/apt",
|
|
46
|
+
"/usr/bin/ssh"
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
for path in paths {
|
|
50
|
+
if FileManager.default.fileExists(atPath: path) {
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Try writing to a restricted area
|
|
56
|
+
do {
|
|
57
|
+
let path = "/private/jailbreak_test.txt"
|
|
58
|
+
try "test".write(toFile: path, atomically: true, encoding: .utf8)
|
|
59
|
+
try FileManager.default.removeItem(atPath: path)
|
|
60
|
+
return true
|
|
61
|
+
} catch {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
#endif
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static func isEmulator() -> Bool {
|
|
68
|
+
#if targetEnvironment(simulator)
|
|
69
|
+
return true
|
|
70
|
+
#else
|
|
71
|
+
return false
|
|
72
|
+
#endif
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private static func isDebuggerAttached() -> Bool {
|
|
76
|
+
var info = kinfo_proc()
|
|
77
|
+
var mib : [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
|
|
78
|
+
var size = MemoryLayout<kinfo_proc>.stride
|
|
79
|
+
let junk = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0)
|
|
80
|
+
if junk != 0 {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
return (info.kp_proc.p_flag & P_TRACED) != 0
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeReactNativeAuthVault.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;AAWpE,eAAeA,mBAAmB,CAACC,YAAY,CAAO,sBAAsB,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import ReactNativeAuthVault from "./NativeReactNativeAuthVault.js";
|
|
4
|
+
export const AuthVault = {
|
|
5
|
+
audit: () => ReactNativeAuthVault.audit(),
|
|
6
|
+
encrypt: (plainText, prompt) => ReactNativeAuthVault.encrypt(plainText, prompt),
|
|
7
|
+
decrypt: (encryptedBase64, prompt) => ReactNativeAuthVault.decrypt(encryptedBase64, prompt),
|
|
8
|
+
setItem: (key, value, prompt) => ReactNativeAuthVault.setItem(key, value, prompt),
|
|
9
|
+
getItem: (key, prompt) => ReactNativeAuthVault.getItem(key, prompt),
|
|
10
|
+
removeItem: key => ReactNativeAuthVault.removeItem(key)
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["ReactNativeAuthVault","AuthVault","audit","encrypt","plainText","prompt","decrypt","encryptedBase64","setItem","key","value","getItem","removeItem"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,OAAOA,oBAAoB,MAAM,iCAA8B;AAE/D,OAAO,MAAMC,SAAS,GAAG;EACvBC,KAAK,EAAEA,CAAA,KAAcF,oBAAoB,CAACE,KAAK,CAAC,CAAC;EACjDC,OAAO,EAAEA,CAACC,SAAiB,EAAEC,MAAc,KAAsBL,oBAAoB,CAACG,OAAO,CAACC,SAAS,EAAEC,MAAM,CAAC;EAChHC,OAAO,EAAEA,CAACC,eAAuB,EAAEF,MAAc,KAAsBL,oBAAoB,CAACM,OAAO,CAACC,eAAe,EAAEF,MAAM,CAAC;EAC5HG,OAAO,EAAEA,CAACC,GAAW,EAAEC,KAAa,EAAEL,MAAc,KAAuBL,oBAAoB,CAACQ,OAAO,CAACC,GAAG,EAAEC,KAAK,EAAEL,MAAM,CAAC;EAC3HM,OAAO,EAAEA,CAACF,GAAW,EAAEJ,MAAc,KAA6BL,oBAAoB,CAACW,OAAO,CAACF,GAAG,EAAEJ,MAAM,CAAC;EAC3GO,UAAU,EAAGH,GAAW,IAAuBT,oBAAoB,CAACY,UAAU,CAACH,GAAG;AACpF,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type TurboModule } from 'react-native';
|
|
2
|
+
export interface Spec extends TurboModule {
|
|
3
|
+
audit(): Object;
|
|
4
|
+
encrypt(plainText: string, prompt: string): Promise<string>;
|
|
5
|
+
decrypt(encryptedBase64: string, prompt: string): Promise<string>;
|
|
6
|
+
setItem(key: string, value: string, prompt: string): Promise<boolean>;
|
|
7
|
+
getItem(key: string, prompt: string): Promise<string | null>;
|
|
8
|
+
removeItem(key: string): Promise<boolean>;
|
|
9
|
+
}
|
|
10
|
+
declare const _default: Spec;
|
|
11
|
+
export default _default;
|
|
12
|
+
//# sourceMappingURL=NativeReactNativeAuthVault.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeReactNativeAuthVault.d.ts","sourceRoot":"","sources":["../../../src/NativeReactNativeAuthVault.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,KAAK,IAAI,MAAM,CAAC;IAChB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5D,OAAO,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClE,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtE,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7D,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3C;;AAED,wBAA8E"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const AuthVault: {
|
|
2
|
+
audit: () => Object;
|
|
3
|
+
encrypt: (plainText: string, prompt: string) => Promise<string>;
|
|
4
|
+
decrypt: (encryptedBase64: string, prompt: string) => Promise<string>;
|
|
5
|
+
setItem: (key: string, value: string, prompt: string) => Promise<boolean>;
|
|
6
|
+
getItem: (key: string, prompt: string) => Promise<string | null>;
|
|
7
|
+
removeItem: (key: string) => Promise<boolean>;
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS;iBACT,MAAM;yBACI,MAAM,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;+BAClC,MAAM,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;mBACpD,MAAM,SAAS,MAAM,UAAU,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;mBACxD,MAAM,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;sBAC5C,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;CAC5C,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hituchhimpa/react-native-auth-vault",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Native-first React Native security and authentication library",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
10
|
+
"default": "./lib/module/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib",
|
|
16
|
+
"android",
|
|
17
|
+
"ios",
|
|
18
|
+
"cpp",
|
|
19
|
+
"*.podspec",
|
|
20
|
+
"react-native.config.js",
|
|
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 @hituchhimpa/react-native-auth-vault-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
|
+
"test": "jest"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"react-native",
|
|
42
|
+
"ios",
|
|
43
|
+
"android"
|
|
44
|
+
],
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/hituchhimpa7/react-native-auth-vault.git.git"
|
|
48
|
+
},
|
|
49
|
+
"author": "Hitesh Chhimpa <hituchhimpa7@users.noreply.github.com> (https://github.com/hituchhimpa7)",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/hituchhimpa7/react-native-auth-vault.git/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/hituchhimpa7/react-native-auth-vault.git#readme",
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"registry": "https://registry.npmjs.org/"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@eslint/compat": "^2.0.3",
|
|
60
|
+
"@eslint/eslintrc": "^3.3.5",
|
|
61
|
+
"@eslint/js": "^10.0.1",
|
|
62
|
+
"@jest/globals": "^30.0.0",
|
|
63
|
+
"@react-native/babel-preset": "0.85.0",
|
|
64
|
+
"@react-native/eslint-config": "0.85.0",
|
|
65
|
+
"@react-native/jest-preset": "0.85.0",
|
|
66
|
+
"@types/react": "^19.2.0",
|
|
67
|
+
"del-cli": "^7.0.0",
|
|
68
|
+
"eslint": "^9.39.4",
|
|
69
|
+
"eslint-config-prettier": "^10.1.8",
|
|
70
|
+
"eslint-plugin-ft-flow": "^3.0.11",
|
|
71
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
72
|
+
"jest": "^30.3.0",
|
|
73
|
+
"prettier": "^3.8.1",
|
|
74
|
+
"react": "19.2.3",
|
|
75
|
+
"react-native": "0.85.0",
|
|
76
|
+
"react-native-builder-bob": "^0.41.0",
|
|
77
|
+
"turbo": "^2.8.21",
|
|
78
|
+
"typescript": "^6.0.2"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"react": "*",
|
|
82
|
+
"react-native": "*"
|
|
83
|
+
},
|
|
84
|
+
"workspaces": [
|
|
85
|
+
"example"
|
|
86
|
+
],
|
|
87
|
+
"packageManager": "yarn@4.11.0",
|
|
88
|
+
"react-native-builder-bob": {
|
|
89
|
+
"source": "src",
|
|
90
|
+
"output": "lib",
|
|
91
|
+
"targets": [
|
|
92
|
+
[
|
|
93
|
+
"module",
|
|
94
|
+
{
|
|
95
|
+
"esm": true
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
"typescript",
|
|
100
|
+
{
|
|
101
|
+
"project": "tsconfig.build.json"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
"codegenConfig": {
|
|
107
|
+
"name": "ReactNativeAuthVaultSpec",
|
|
108
|
+
"type": "modules",
|
|
109
|
+
"jsSrcsDir": "src",
|
|
110
|
+
"android": {
|
|
111
|
+
"javaPackageName": "com.hituchhimpa.reactnativeauthvault"
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"prettier": {
|
|
115
|
+
"quoteProps": "consistent",
|
|
116
|
+
"singleQuote": true,
|
|
117
|
+
"tabWidth": 2,
|
|
118
|
+
"trailingComma": "es5",
|
|
119
|
+
"useTabs": false
|
|
120
|
+
},
|
|
121
|
+
"jest": {
|
|
122
|
+
"preset": "@react-native/jest-preset",
|
|
123
|
+
"modulePathIgnorePatterns": [
|
|
124
|
+
"<rootDir>/example/node_modules",
|
|
125
|
+
"<rootDir>/lib/"
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
"create-react-native-library": {
|
|
129
|
+
"type": "turbo-module",
|
|
130
|
+
"languages": "kotlin-objc",
|
|
131
|
+
"tools": [
|
|
132
|
+
"eslint",
|
|
133
|
+
"jest"
|
|
134
|
+
],
|
|
135
|
+
"version": "0.62.0"
|
|
136
|
+
}
|
|
137
|
+
}
|