@rajeev02/vault 0.1.0 → 0.2.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/README.md +142 -0
- package/package.json +4 -7
- package/src/hooks.ts +169 -0
- package/src/index.ts +52 -0
- package/src/native-bridge.ts +33 -0
- package/src/types.ts +131 -0
- package/src/vault.ts +396 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @rajeev02/vault
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@rajeev02/vault)
|
|
4
|
+
[](https://github.com/Rajeev02/rajeev-sdk/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
**AES-256-GCM encrypted key-value storage** with namespaces, expiry, hashing, and biometric gating — powered by Rust for maximum performance and security.
|
|
7
|
+
|
|
8
|
+
Part of [Rajeev SDK](https://github.com/Rajeev02/rajeev-sdk) — cross-platform infrastructure libraries for building apps that work everywhere.
|
|
9
|
+
|
|
10
|
+
## Why use this?
|
|
11
|
+
|
|
12
|
+
- **Bank-grade encryption** — AES-256-GCM (the same standard used by banks and governments)
|
|
13
|
+
- **Rust-powered** — Crypto logic runs in native Rust, not JavaScript. No key exposure in JS memory.
|
|
14
|
+
- **Offline-first** — SQLite-backed storage works without network. Data persists across app restarts.
|
|
15
|
+
- **Biometric gating** — Optionally require Face ID / Touch ID / fingerprint before reading sensitive keys
|
|
16
|
+
- **Namespaces + expiry** — Organize keys by context, auto-expire tokens after TTL
|
|
17
|
+
- **Truly cross-platform** — Same API on iOS, Android, Web (WASM), watchOS, and Wear OS
|
|
18
|
+
|
|
19
|
+
## Platform Support
|
|
20
|
+
|
|
21
|
+
| Platform | Engine | Status |
|
|
22
|
+
| ------------ | ----------------- | ------ |
|
|
23
|
+
| iOS 16+ | Rust → UniFFI | ✅ |
|
|
24
|
+
| Android 7+ | Rust → UniFFI JNI | ✅ |
|
|
25
|
+
| Web | Rust → WASM | ✅ |
|
|
26
|
+
| watchOS 9+ | Rust → UniFFI | ✅ |
|
|
27
|
+
| Wear OS | Rust → UniFFI | ✅ |
|
|
28
|
+
| Android Auto | Rust → UniFFI | ✅ |
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @rajeev02/vault
|
|
34
|
+
|
|
35
|
+
# iOS (after install)
|
|
36
|
+
cd ios && pod install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Peer Dependencies
|
|
40
|
+
|
|
41
|
+
- `react` >= 18.3.0
|
|
42
|
+
- `react-native` >= 0.84.0
|
|
43
|
+
- `expo` >= 54.0.0 _(optional)_
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { Vault } from "@rajeev02/vault";
|
|
49
|
+
|
|
50
|
+
// Create an encrypted vault
|
|
51
|
+
const vault = await Vault.create({
|
|
52
|
+
appId: "my-app",
|
|
53
|
+
encryption: "AES-256-GCM",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Store and retrieve secrets
|
|
57
|
+
await vault.set("auth_token", "eyJhbGciOi…");
|
|
58
|
+
const token = await vault.get("auth_token"); // → 'eyJhbGciOi…'
|
|
59
|
+
|
|
60
|
+
// JSON convenience
|
|
61
|
+
await vault.setJSON("profile", { name: "Rajeev", lang: "hi" });
|
|
62
|
+
const profile = await vault.getJSON<{ name: string }>("profile");
|
|
63
|
+
|
|
64
|
+
// Namespaces — isolate data by context
|
|
65
|
+
const userVault = vault.namespace("user-123");
|
|
66
|
+
await userVault.set("preferences", "dark-mode");
|
|
67
|
+
|
|
68
|
+
// Expiry — auto-delete after TTL
|
|
69
|
+
await vault.set("otp", "483921", { expiry: "5m" });
|
|
70
|
+
|
|
71
|
+
// Biometric gating (iOS Face ID / Android fingerprint)
|
|
72
|
+
await vault.set("bank_pin", "8491", { biometricRequired: true });
|
|
73
|
+
const pin = await vault.get("bank_pin"); // triggers biometric prompt
|
|
74
|
+
|
|
75
|
+
// Hashing — one-way hash for password verification
|
|
76
|
+
const hash = await vault.hash("my-password");
|
|
77
|
+
const matches = await vault.verifyHash("my-password", hash); // → true
|
|
78
|
+
|
|
79
|
+
// Bulk operations
|
|
80
|
+
await vault.setMany([
|
|
81
|
+
{ key: "a", value: "1" },
|
|
82
|
+
{ key: "b", value: "2" },
|
|
83
|
+
]);
|
|
84
|
+
const all = await vault.getAll(); // → Map of all key-value pairs
|
|
85
|
+
|
|
86
|
+
// Cleanup
|
|
87
|
+
await vault.delete("auth_token");
|
|
88
|
+
await vault.clear(); // Remove all keys
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## React Hooks
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { useVault, useVaultValue } from "@rajeev02/vault";
|
|
95
|
+
|
|
96
|
+
function SecureComponent() {
|
|
97
|
+
const vault = useVault({ appId: "my-app" });
|
|
98
|
+
const [token, setToken] = useVaultValue(vault, "auth_token");
|
|
99
|
+
|
|
100
|
+
return <Text>{token ?? "No token"}</Text>;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## API Reference
|
|
105
|
+
|
|
106
|
+
| Method | Returns | Description |
|
|
107
|
+
| -------------------------------- | ------------------------- | ------------------------------- |
|
|
108
|
+
| `Vault.create(config)` | `Promise<Vault>` | Create encrypted vault instance |
|
|
109
|
+
| `vault.set(key, value, opts?)` | `Promise<void>` | Store encrypted value |
|
|
110
|
+
| `vault.get(key)` | `Promise<string \| null>` | Retrieve decrypted value |
|
|
111
|
+
| `vault.setJSON(key, obj, opts?)` | `Promise<void>` | Store JSON object |
|
|
112
|
+
| `vault.getJSON<T>(key)` | `Promise<T \| null>` | Retrieve parsed JSON |
|
|
113
|
+
| `vault.delete(key)` | `Promise<void>` | Delete a key |
|
|
114
|
+
| `vault.clear()` | `Promise<void>` | Delete all keys |
|
|
115
|
+
| `vault.has(key)` | `Promise<boolean>` | Check if key exists |
|
|
116
|
+
| `vault.keys()` | `Promise<string[]>` | List all keys |
|
|
117
|
+
| `vault.getAll()` | `Promise<Map>` | Get all key-value pairs |
|
|
118
|
+
| `vault.namespace(ns)` | `Vault` | Create namespaced sub-vault |
|
|
119
|
+
| `vault.hash(input)` | `Promise<string>` | One-way hash |
|
|
120
|
+
| `vault.verifyHash(input, hash)` | `Promise<boolean>` | Verify hash match |
|
|
121
|
+
|
|
122
|
+
## Architecture
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
TypeScript API (this package)
|
|
126
|
+
↓
|
|
127
|
+
Native Bridge (auto-generated by UniFFI)
|
|
128
|
+
↓
|
|
129
|
+
Rust Core (rajeev-vault-core)
|
|
130
|
+
├── AES-256-GCM encryption (aes-gcm 0.10)
|
|
131
|
+
├── SQLite storage (rusqlite 0.38)
|
|
132
|
+
├── Key derivation (PBKDF2 / Argon2)
|
|
133
|
+
└── Secure random (rand 0.9)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Full Documentation
|
|
137
|
+
|
|
138
|
+
📖 [Complete API docs with platform-specific examples](https://github.com/Rajeev02/rajeev-sdk/blob/main/docs/usage/VAULT.md)
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT © 2026 [Rajeev Kumar Joshi](https://rajeev02.github.io)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rajeev02/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Universal secure storage for React Native — AES-256-GCM encrypted, cross-platform",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"clean": "rm -rf lib",
|
|
10
10
|
"test": "jest",
|
|
11
|
-
"prepublishOnly": "
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"react-native",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
26
26
|
"url": "https://github.com/Rajeev02/rajeev-sdk",
|
|
27
|
-
"directory": "packages/vault"
|
|
27
|
+
"directory": "packages/vault/ts-wrapper"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"react": ">=18.3.0",
|
|
@@ -44,10 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
46
46
|
"lib/",
|
|
47
|
-
"
|
|
48
|
-
"ios/",
|
|
49
|
-
"rust-core/",
|
|
50
|
-
"rajeev-vault.podspec",
|
|
47
|
+
"src/",
|
|
51
48
|
"README.md"
|
|
52
49
|
],
|
|
53
50
|
"publishConfig": {
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Vault } from './vault';
|
|
3
|
+
import type { VaultConfig, StoreOptions, VaultStats } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* React hook to create and manage a Vault instance.
|
|
7
|
+
*
|
|
8
|
+
* @param config - Vault configuration
|
|
9
|
+
* @returns Vault instance and loading state
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* function App() {
|
|
14
|
+
* const { vault, isReady, error } = useVault({ appId: 'com.myapp' });
|
|
15
|
+
*
|
|
16
|
+
* if (!isReady) return <LoadingScreen />;
|
|
17
|
+
* if (error) return <ErrorScreen error={error} />;
|
|
18
|
+
*
|
|
19
|
+
* return <MainApp vault={vault!} />;
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function useVault(config: VaultConfig) {
|
|
24
|
+
const [vault, setVault] = useState<Vault | null>(null);
|
|
25
|
+
const [isReady, setIsReady] = useState(false);
|
|
26
|
+
const [error, setError] = useState<Error | null>(null);
|
|
27
|
+
const initRef = useRef(false);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (initRef.current) return;
|
|
31
|
+
initRef.current = true;
|
|
32
|
+
|
|
33
|
+
Vault.create(config)
|
|
34
|
+
.then((v) => {
|
|
35
|
+
setVault(v);
|
|
36
|
+
setIsReady(true);
|
|
37
|
+
})
|
|
38
|
+
.catch((e) => {
|
|
39
|
+
setError(e);
|
|
40
|
+
setIsReady(true);
|
|
41
|
+
});
|
|
42
|
+
}, [config.appId]);
|
|
43
|
+
|
|
44
|
+
return { vault, isReady, error };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* React hook to read a value from the vault reactively.
|
|
49
|
+
* Re-reads when the key or namespace changes.
|
|
50
|
+
*
|
|
51
|
+
* @param vault - Vault instance
|
|
52
|
+
* @param key - Key to read
|
|
53
|
+
* @param namespace - Optional namespace
|
|
54
|
+
* @returns Value, loading state, and refresh function
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* function TokenDisplay({ vault }: { vault: Vault }) {
|
|
59
|
+
* const { value, isLoading, refresh } = useVaultValue(vault, 'auth_token');
|
|
60
|
+
*
|
|
61
|
+
* if (isLoading) return <Text>Loading...</Text>;
|
|
62
|
+
*
|
|
63
|
+
* return (
|
|
64
|
+
* <View>
|
|
65
|
+
* <Text>Token: {value ?? 'Not set'}</Text>
|
|
66
|
+
* <Button title="Refresh" onPress={refresh} />
|
|
67
|
+
* </View>
|
|
68
|
+
* );
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function useVaultValue(
|
|
73
|
+
vault: Vault | null,
|
|
74
|
+
key: string,
|
|
75
|
+
namespace?: string,
|
|
76
|
+
) {
|
|
77
|
+
const [value, setValue] = useState<string | null>(null);
|
|
78
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
79
|
+
const [error, setError] = useState<Error | null>(null);
|
|
80
|
+
|
|
81
|
+
const refresh = useCallback(async () => {
|
|
82
|
+
if (!vault) return;
|
|
83
|
+
setIsLoading(true);
|
|
84
|
+
try {
|
|
85
|
+
const result = await vault.get(key, namespace);
|
|
86
|
+
setValue(result);
|
|
87
|
+
setError(null);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
setError(e as Error);
|
|
90
|
+
} finally {
|
|
91
|
+
setIsLoading(false);
|
|
92
|
+
}
|
|
93
|
+
}, [vault, key, namespace]);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
refresh();
|
|
97
|
+
}, [refresh]);
|
|
98
|
+
|
|
99
|
+
return { value, isLoading, error, refresh };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* React hook to read a JSON value from the vault.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* interface UserProfile {
|
|
108
|
+
* name: string;
|
|
109
|
+
* email: string;
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* function Profile({ vault }: { vault: Vault }) {
|
|
113
|
+
* const { data, isLoading } = useVaultJSON<UserProfile>(vault, 'profile');
|
|
114
|
+
*
|
|
115
|
+
* if (isLoading) return <Text>Loading...</Text>;
|
|
116
|
+
* return <Text>Hello, {data?.name ?? 'Guest'}</Text>;
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function useVaultJSON<T = unknown>(
|
|
121
|
+
vault: Vault | null,
|
|
122
|
+
key: string,
|
|
123
|
+
namespace?: string,
|
|
124
|
+
) {
|
|
125
|
+
const { value, isLoading, error, refresh } = useVaultValue(vault, key, namespace);
|
|
126
|
+
|
|
127
|
+
const data = value ? (JSON.parse(value) as T) : null;
|
|
128
|
+
|
|
129
|
+
return { data, isLoading, error, refresh };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* React hook to get vault statistics.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```tsx
|
|
137
|
+
* function VaultInfo({ vault }: { vault: Vault }) {
|
|
138
|
+
* const { stats, isLoading } = useVaultStats(vault);
|
|
139
|
+
*
|
|
140
|
+
* return (
|
|
141
|
+
* <Text>
|
|
142
|
+
* Entries: {stats?.totalEntries ?? 0}
|
|
143
|
+
* Size: {stats?.storageBytes ?? 0} bytes
|
|
144
|
+
* </Text>
|
|
145
|
+
* );
|
|
146
|
+
* }
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export function useVaultStats(vault: Vault | null) {
|
|
150
|
+
const [stats, setStats] = useState<VaultStats | null>(null);
|
|
151
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
152
|
+
|
|
153
|
+
const refresh = useCallback(async () => {
|
|
154
|
+
if (!vault) return;
|
|
155
|
+
setIsLoading(true);
|
|
156
|
+
try {
|
|
157
|
+
const s = await vault.stats();
|
|
158
|
+
setStats(s);
|
|
159
|
+
} finally {
|
|
160
|
+
setIsLoading(false);
|
|
161
|
+
}
|
|
162
|
+
}, [vault]);
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
refresh();
|
|
166
|
+
}, [refresh]);
|
|
167
|
+
|
|
168
|
+
return { stats, isLoading, refresh };
|
|
169
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rajeev02/vault
|
|
3
|
+
*
|
|
4
|
+
* Universal Secure Storage for React Native
|
|
5
|
+
* AES-256-GCM encrypted, cross-platform (Android, iOS, Web, Watch, Auto, IoT)
|
|
6
|
+
*
|
|
7
|
+
* Built with Rust core for maximum security and performance.
|
|
8
|
+
*
|
|
9
|
+
* @author Rajeev Kumar Joshi (https://github.com/Rajeev02)
|
|
10
|
+
* @license MIT
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { Vault } from '@rajeev02/vault';
|
|
15
|
+
*
|
|
16
|
+
* const vault = await Vault.create({ appId: 'com.myapp' });
|
|
17
|
+
*
|
|
18
|
+
* // Store
|
|
19
|
+
* await vault.set('token', 'my-secret', { expiry: '24h' });
|
|
20
|
+
*
|
|
21
|
+
* // Retrieve
|
|
22
|
+
* const token = await vault.get('token');
|
|
23
|
+
*
|
|
24
|
+
* // Namespaces
|
|
25
|
+
* const payments = vault.namespace('payments');
|
|
26
|
+
* await payments.set('upi_pin', '1234', { biometricRequired: true });
|
|
27
|
+
*
|
|
28
|
+
* // JSON helpers
|
|
29
|
+
* await vault.setJSON('user', { name: 'Rajeev', role: 'admin' });
|
|
30
|
+
* const user = await vault.getJSON<User>('user');
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// Core
|
|
35
|
+
export { Vault } from './vault';
|
|
36
|
+
|
|
37
|
+
// Types
|
|
38
|
+
export {
|
|
39
|
+
VaultConfig,
|
|
40
|
+
StoreOptions,
|
|
41
|
+
VaultStats,
|
|
42
|
+
VaultError,
|
|
43
|
+
VaultErrorCode,
|
|
44
|
+
} from './types';
|
|
45
|
+
|
|
46
|
+
// React Hooks
|
|
47
|
+
export {
|
|
48
|
+
useVault,
|
|
49
|
+
useVaultValue,
|
|
50
|
+
useVaultJSON,
|
|
51
|
+
useVaultStats,
|
|
52
|
+
} from './hooks';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NativeModules, Platform } from 'react-native';
|
|
2
|
+
import type { NativeVaultModule } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the native vault module for the current platform.
|
|
6
|
+
*
|
|
7
|
+
* Android: Uses JNI → Rust .so library
|
|
8
|
+
* iOS: Uses C FFI → Rust .a static library
|
|
9
|
+
* Web: Uses WASM → Rust compiled to WebAssembly
|
|
10
|
+
*/
|
|
11
|
+
function getNativeModule(): NativeVaultModule {
|
|
12
|
+
const { RajeevVault } = NativeModules;
|
|
13
|
+
|
|
14
|
+
if (!RajeevVault) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`@rajeev02/vault: Native module not found. ` +
|
|
17
|
+
`Make sure you have run 'pod install' (iOS) or rebuilt the app (Android).\n\n` +
|
|
18
|
+
`Platform: ${Platform.OS}\n` +
|
|
19
|
+
`If you're on web, import from '@rajeev02/vault/web' instead.`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return RajeevVault as NativeVaultModule;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const NativeBridge = {
|
|
27
|
+
/**
|
|
28
|
+
* Get the native module, lazy-loaded and cached
|
|
29
|
+
*/
|
|
30
|
+
get module(): NativeVaultModule {
|
|
31
|
+
return getNativeModule();
|
|
32
|
+
},
|
|
33
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rajeev02/vault - Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Universal secure storage for React Native, Web, Watch, Auto, IoT
|
|
5
|
+
* Built by Rajeev Kumar Joshi (https://github.com/Rajeev02)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Configuration for creating a vault instance */
|
|
9
|
+
export interface VaultConfig {
|
|
10
|
+
/** Unique app identifier (e.g., 'com.myapp') */
|
|
11
|
+
appId: string;
|
|
12
|
+
|
|
13
|
+
/** Custom database path. Defaults to app-specific location */
|
|
14
|
+
dbPath?: string;
|
|
15
|
+
|
|
16
|
+
/** Encryption algorithm. Default: 'AES-256-GCM' */
|
|
17
|
+
encryption?: 'AES-256-GCM';
|
|
18
|
+
|
|
19
|
+
/** Whether biometric auth is available on this device */
|
|
20
|
+
biometricAvailable?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Options when storing a value */
|
|
24
|
+
export interface StoreOptions {
|
|
25
|
+
/**
|
|
26
|
+
* When the value should expire and auto-delete.
|
|
27
|
+
* Format: "30m" (minutes), "24h" (hours), "7d" (days), "4w" (weeks)
|
|
28
|
+
* Default: null (never expires)
|
|
29
|
+
*/
|
|
30
|
+
expiry?: string | null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether biometric authentication is required to read this value.
|
|
34
|
+
* Default: false
|
|
35
|
+
*/
|
|
36
|
+
biometricRequired?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether this entry can be exported/backed up.
|
|
40
|
+
* Set to false for highly sensitive data like PINs.
|
|
41
|
+
* Default: true
|
|
42
|
+
*/
|
|
43
|
+
exportable?: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Namespace to group related entries.
|
|
47
|
+
* e.g., 'auth', 'payments', 'health'
|
|
48
|
+
* Default: 'default'
|
|
49
|
+
*/
|
|
50
|
+
namespace?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Statistics about the vault */
|
|
54
|
+
export interface VaultStats {
|
|
55
|
+
/** Total number of stored entries */
|
|
56
|
+
totalEntries: number;
|
|
57
|
+
|
|
58
|
+
/** Number of distinct namespaces */
|
|
59
|
+
totalNamespaces: number;
|
|
60
|
+
|
|
61
|
+
/** Number of expired entries pending cleanup */
|
|
62
|
+
expiredEntries: number;
|
|
63
|
+
|
|
64
|
+
/** Approximate storage used in bytes */
|
|
65
|
+
storageBytes: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Error codes returned by the vault */
|
|
69
|
+
export enum VaultErrorCode {
|
|
70
|
+
ENCRYPTION_FAILED = 'ENCRYPTION_FAILED',
|
|
71
|
+
DECRYPTION_FAILED = 'DECRYPTION_FAILED',
|
|
72
|
+
KEY_NOT_FOUND = 'KEY_NOT_FOUND',
|
|
73
|
+
KEY_EXPIRED = 'KEY_EXPIRED',
|
|
74
|
+
STORAGE_ERROR = 'STORAGE_ERROR',
|
|
75
|
+
INVALID_CONFIG = 'INVALID_CONFIG',
|
|
76
|
+
BIOMETRIC_REQUIRED = 'BIOMETRIC_REQUIRED',
|
|
77
|
+
BIOMETRIC_FAILED = 'BIOMETRIC_FAILED',
|
|
78
|
+
NOT_INITIALIZED = 'NOT_INITIALIZED',
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Custom error class for vault operations */
|
|
82
|
+
export class VaultError extends Error {
|
|
83
|
+
public readonly code: VaultErrorCode;
|
|
84
|
+
|
|
85
|
+
constructor(code: VaultErrorCode, message?: string) {
|
|
86
|
+
super(message || code);
|
|
87
|
+
this.name = 'VaultError';
|
|
88
|
+
this.code = code;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Internal native module interface (platform-specific) */
|
|
93
|
+
export interface NativeVaultModule {
|
|
94
|
+
create(config: {
|
|
95
|
+
appId: string;
|
|
96
|
+
dbPath?: string;
|
|
97
|
+
encryptionAlgo: string;
|
|
98
|
+
biometricAvailable: boolean;
|
|
99
|
+
}): Promise<void>;
|
|
100
|
+
|
|
101
|
+
store(
|
|
102
|
+
key: string,
|
|
103
|
+
value: string,
|
|
104
|
+
expiry: string | null,
|
|
105
|
+
biometricRequired: boolean,
|
|
106
|
+
exportable: boolean,
|
|
107
|
+
namespace: string | null,
|
|
108
|
+
): Promise<void>;
|
|
109
|
+
|
|
110
|
+
retrieve(key: string, namespace: string | null): Promise<string | null>;
|
|
111
|
+
delete(key: string, namespace: string | null): Promise<boolean>;
|
|
112
|
+
exists(key: string, namespace: string | null): Promise<boolean>;
|
|
113
|
+
listKeys(namespace: string | null): Promise<string[]>;
|
|
114
|
+
listNamespaces(): Promise<string[]>;
|
|
115
|
+
wipeNamespace(namespace: string): Promise<void>;
|
|
116
|
+
wipeAll(): Promise<void>;
|
|
117
|
+
getStats(): Promise<VaultStats>;
|
|
118
|
+
cleanupExpired(): Promise<void>;
|
|
119
|
+
exportEntry(key: string, namespace: string | null): Promise<string | null>;
|
|
120
|
+
importEntry(
|
|
121
|
+
key: string,
|
|
122
|
+
encryptedData: string,
|
|
123
|
+
expiry: string | null,
|
|
124
|
+
biometricRequired: boolean,
|
|
125
|
+
exportable: boolean,
|
|
126
|
+
namespace: string | null,
|
|
127
|
+
): Promise<void>;
|
|
128
|
+
generateKey(): Promise<string>;
|
|
129
|
+
hash(input: string): Promise<string>;
|
|
130
|
+
verifyHash(input: string, hash: string): Promise<boolean>;
|
|
131
|
+
}
|
package/src/vault.ts
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { NativeBridge } from './native-bridge';
|
|
2
|
+
import {
|
|
3
|
+
VaultConfig,
|
|
4
|
+
StoreOptions,
|
|
5
|
+
VaultStats,
|
|
6
|
+
VaultError,
|
|
7
|
+
VaultErrorCode,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 🔐 Rajeev Vault — Universal Secure Storage
|
|
12
|
+
*
|
|
13
|
+
* AES-256-GCM encrypted storage that works across all platforms.
|
|
14
|
+
* Built with Rust core for maximum security and performance.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { Vault } from '@rajeev02/vault';
|
|
19
|
+
*
|
|
20
|
+
* // Create vault
|
|
21
|
+
* const vault = await Vault.create({ appId: 'com.myapp' });
|
|
22
|
+
*
|
|
23
|
+
* // Store securely
|
|
24
|
+
* await vault.set('token', 'my-secret-value', { expiry: '24h' });
|
|
25
|
+
*
|
|
26
|
+
* // Retrieve
|
|
27
|
+
* const token = await vault.get('token');
|
|
28
|
+
*
|
|
29
|
+
* // Use namespaces
|
|
30
|
+
* await vault.namespace('payments').set('upi_pin', '1234', {
|
|
31
|
+
* biometricRequired: true,
|
|
32
|
+
* exportable: false,
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @author Rajeev Kumar Joshi (https://github.com/Rajeev02)
|
|
37
|
+
*/
|
|
38
|
+
export class Vault {
|
|
39
|
+
private initialized = false;
|
|
40
|
+
private config: VaultConfig;
|
|
41
|
+
private currentNamespace: string | null = null;
|
|
42
|
+
|
|
43
|
+
private constructor(config: VaultConfig) {
|
|
44
|
+
this.config = config;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a new Vault instance.
|
|
49
|
+
* This is the main entry point — use this instead of `new Vault()`.
|
|
50
|
+
*
|
|
51
|
+
* @param config - Vault configuration
|
|
52
|
+
* @returns Initialized Vault instance
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* const vault = await Vault.create({
|
|
57
|
+
* appId: 'com.myapp',
|
|
58
|
+
* biometricAvailable: true,
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
static async create(config: VaultConfig): Promise<Vault> {
|
|
63
|
+
const vault = new Vault(config);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await NativeBridge.module.create({
|
|
67
|
+
appId: config.appId,
|
|
68
|
+
dbPath: config.dbPath,
|
|
69
|
+
encryptionAlgo: config.encryption || 'AES-256-GCM',
|
|
70
|
+
biometricAvailable: config.biometricAvailable ?? false,
|
|
71
|
+
});
|
|
72
|
+
vault.initialized = true;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new VaultError(
|
|
75
|
+
VaultErrorCode.INVALID_CONFIG,
|
|
76
|
+
`Failed to initialize vault: ${error}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return vault;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Ensure the vault is initialized before any operation
|
|
85
|
+
*/
|
|
86
|
+
private ensureInitialized(): void {
|
|
87
|
+
if (!this.initialized) {
|
|
88
|
+
throw new VaultError(
|
|
89
|
+
VaultErrorCode.NOT_INITIALIZED,
|
|
90
|
+
'Vault not initialized. Call Vault.create() first.',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Core Operations ─────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Store a value securely.
|
|
99
|
+
*
|
|
100
|
+
* @param key - Unique key for the value
|
|
101
|
+
* @param value - String value to encrypt and store
|
|
102
|
+
* @param options - Storage options (expiry, biometric, namespace)
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* // Simple store
|
|
107
|
+
* await vault.set('token', 'eyJhbG...');
|
|
108
|
+
*
|
|
109
|
+
* // With expiry
|
|
110
|
+
* await vault.set('otp', '4829', { expiry: '5m' });
|
|
111
|
+
*
|
|
112
|
+
* // With biometric protection
|
|
113
|
+
* await vault.set('pin', '1234', {
|
|
114
|
+
* biometricRequired: true,
|
|
115
|
+
* exportable: false,
|
|
116
|
+
* });
|
|
117
|
+
*
|
|
118
|
+
* // Store JSON
|
|
119
|
+
* await vault.set('user', JSON.stringify({ name: 'Rajeev', role: 'admin' }));
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
async set(key: string, value: string, options?: StoreOptions): Promise<void> {
|
|
123
|
+
this.ensureInitialized();
|
|
124
|
+
|
|
125
|
+
const ns = options?.namespace ?? this.currentNamespace;
|
|
126
|
+
|
|
127
|
+
await NativeBridge.module.store(
|
|
128
|
+
key,
|
|
129
|
+
value,
|
|
130
|
+
options?.expiry ?? null,
|
|
131
|
+
options?.biometricRequired ?? false,
|
|
132
|
+
options?.exportable ?? true,
|
|
133
|
+
ns,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Retrieve a decrypted value.
|
|
139
|
+
* Returns null if the key doesn't exist or has expired.
|
|
140
|
+
*
|
|
141
|
+
* @param key - Key to retrieve
|
|
142
|
+
* @param namespace - Optional namespace override
|
|
143
|
+
* @returns Decrypted value or null
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const token = await vault.get('token');
|
|
148
|
+
* if (token) {
|
|
149
|
+
* console.log('Got token:', token);
|
|
150
|
+
* } else {
|
|
151
|
+
* console.log('Token not found or expired');
|
|
152
|
+
* }
|
|
153
|
+
*
|
|
154
|
+
* // Get JSON
|
|
155
|
+
* const userJson = await vault.get('user');
|
|
156
|
+
* const user = userJson ? JSON.parse(userJson) : null;
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
async get(key: string, namespace?: string): Promise<string | null> {
|
|
160
|
+
this.ensureInitialized();
|
|
161
|
+
const ns = namespace ?? this.currentNamespace;
|
|
162
|
+
return NativeBridge.module.retrieve(key, ns);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get a value, parsed as JSON.
|
|
167
|
+
* Convenience method that combines get() + JSON.parse().
|
|
168
|
+
*
|
|
169
|
+
* @param key - Key to retrieve
|
|
170
|
+
* @param namespace - Optional namespace override
|
|
171
|
+
* @returns Parsed object or null
|
|
172
|
+
*/
|
|
173
|
+
async getJSON<T = unknown>(key: string, namespace?: string): Promise<T | null> {
|
|
174
|
+
const raw = await this.get(key, namespace);
|
|
175
|
+
if (raw === null) return null;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(raw) as T;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Store a value as JSON.
|
|
186
|
+
* Convenience method that combines JSON.stringify() + set().
|
|
187
|
+
*/
|
|
188
|
+
async setJSON<T>(key: string, value: T, options?: StoreOptions): Promise<void> {
|
|
189
|
+
const serialized = JSON.stringify(value);
|
|
190
|
+
await this.set(key, serialized, options);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Delete a key from the vault.
|
|
195
|
+
*
|
|
196
|
+
* @param key - Key to delete
|
|
197
|
+
* @param namespace - Optional namespace override
|
|
198
|
+
* @returns true if the key was deleted, false if not found
|
|
199
|
+
*/
|
|
200
|
+
async delete(key: string, namespace?: string): Promise<boolean> {
|
|
201
|
+
this.ensureInitialized();
|
|
202
|
+
const ns = namespace ?? this.currentNamespace;
|
|
203
|
+
return NativeBridge.module.delete(key, ns);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if a key exists (and is not expired).
|
|
208
|
+
*
|
|
209
|
+
* @param key - Key to check
|
|
210
|
+
* @param namespace - Optional namespace override
|
|
211
|
+
* @returns true if key exists and is valid
|
|
212
|
+
*/
|
|
213
|
+
async has(key: string, namespace?: string): Promise<boolean> {
|
|
214
|
+
this.ensureInitialized();
|
|
215
|
+
const ns = namespace ?? this.currentNamespace;
|
|
216
|
+
return NativeBridge.module.exists(key, ns);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── Namespace Operations ─────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create a namespace-scoped vault view.
|
|
223
|
+
* All operations on the returned object are scoped to this namespace.
|
|
224
|
+
*
|
|
225
|
+
* @param name - Namespace name
|
|
226
|
+
* @returns Namespace-scoped vault
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* const payments = vault.namespace('payments');
|
|
231
|
+
* await payments.set('upi_pin', '1234');
|
|
232
|
+
* await payments.set('card_last4', '4242');
|
|
233
|
+
*
|
|
234
|
+
* const health = vault.namespace('health');
|
|
235
|
+
* await health.set('blood_group', 'O+');
|
|
236
|
+
*
|
|
237
|
+
* // Keys don't collide across namespaces
|
|
238
|
+
* await vault.namespace('a').set('key', 'value-a');
|
|
239
|
+
* await vault.namespace('b').set('key', 'value-b');
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
namespace(name: string): NamespacedVault {
|
|
243
|
+
this.ensureInitialized();
|
|
244
|
+
return new NamespacedVault(this, name);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* List all keys in a namespace.
|
|
249
|
+
*
|
|
250
|
+
* @param namespace - Namespace to list (default namespace if not specified)
|
|
251
|
+
* @returns Array of key names
|
|
252
|
+
*/
|
|
253
|
+
async keys(namespace?: string): Promise<string[]> {
|
|
254
|
+
this.ensureInitialized();
|
|
255
|
+
const ns = namespace ?? this.currentNamespace;
|
|
256
|
+
return NativeBridge.module.listKeys(ns);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* List all namespaces in the vault.
|
|
261
|
+
*
|
|
262
|
+
* @returns Array of namespace names
|
|
263
|
+
*/
|
|
264
|
+
async namespaces(): Promise<string[]> {
|
|
265
|
+
this.ensureInitialized();
|
|
266
|
+
return NativeBridge.module.listNamespaces();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── Destructive Operations ───────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Delete all entries in a namespace.
|
|
273
|
+
*
|
|
274
|
+
* @param namespace - Namespace to wipe
|
|
275
|
+
*/
|
|
276
|
+
async wipeNamespace(namespace: string): Promise<void> {
|
|
277
|
+
this.ensureInitialized();
|
|
278
|
+
await NativeBridge.module.wipeNamespace(namespace);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* ⚠️ Delete EVERYTHING in the vault. Cannot be undone.
|
|
283
|
+
* Typically used during logout.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```typescript
|
|
287
|
+
* async function logout() {
|
|
288
|
+
* await vault.wipeAll();
|
|
289
|
+
* navigation.reset('LoginScreen');
|
|
290
|
+
* }
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
async wipeAll(): Promise<void> {
|
|
294
|
+
this.ensureInitialized();
|
|
295
|
+
await NativeBridge.module.wipeAll();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ─── Utility Operations ───────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get storage statistics.
|
|
302
|
+
*
|
|
303
|
+
* @returns Stats including entry count, namespaces, storage size
|
|
304
|
+
*/
|
|
305
|
+
async stats(): Promise<VaultStats> {
|
|
306
|
+
this.ensureInitialized();
|
|
307
|
+
return NativeBridge.module.getStats();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clean up expired entries. Call periodically to reclaim space.
|
|
312
|
+
*/
|
|
313
|
+
async cleanup(): Promise<void> {
|
|
314
|
+
this.ensureInitialized();
|
|
315
|
+
await NativeBridge.module.cleanupExpired();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─── Static Utilities ─────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Generate a cryptographically secure random key.
|
|
322
|
+
* Useful for creating encryption keys, API tokens, etc.
|
|
323
|
+
*
|
|
324
|
+
* @returns Base64-encoded random key
|
|
325
|
+
*/
|
|
326
|
+
static async generateKey(): Promise<string> {
|
|
327
|
+
return NativeBridge.module.generateKey();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Hash a value (one-way). Useful for storing passwords.
|
|
332
|
+
*
|
|
333
|
+
* @param input - Value to hash
|
|
334
|
+
* @returns Salted hash string
|
|
335
|
+
*/
|
|
336
|
+
static async hash(input: string): Promise<string> {
|
|
337
|
+
return NativeBridge.module.hash(input);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Verify a value against a hash.
|
|
342
|
+
*
|
|
343
|
+
* @param input - Value to verify
|
|
344
|
+
* @param hash - Hash to verify against
|
|
345
|
+
* @returns true if the value matches the hash
|
|
346
|
+
*/
|
|
347
|
+
static async verifyHash(input: string, hash: string): Promise<boolean> {
|
|
348
|
+
return NativeBridge.module.verifyHash(input, hash);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* A namespace-scoped view of the vault.
|
|
354
|
+
* All operations are automatically scoped to the namespace.
|
|
355
|
+
*/
|
|
356
|
+
class NamespacedVault {
|
|
357
|
+
private vault: Vault;
|
|
358
|
+
private name: string;
|
|
359
|
+
|
|
360
|
+
constructor(vault: Vault, name: string) {
|
|
361
|
+
this.vault = vault;
|
|
362
|
+
this.name = name;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async set(key: string, value: string, options?: Omit<StoreOptions, 'namespace'>): Promise<void> {
|
|
366
|
+
return this.vault.set(key, value, { ...options, namespace: this.name });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async get(key: string): Promise<string | null> {
|
|
370
|
+
return this.vault.get(key, this.name);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async getJSON<T = unknown>(key: string): Promise<T | null> {
|
|
374
|
+
return this.vault.getJSON<T>(key, this.name);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async setJSON<T>(key: string, value: T, options?: Omit<StoreOptions, 'namespace'>): Promise<void> {
|
|
378
|
+
return this.vault.setJSON(key, value, { ...options, namespace: this.name });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async delete(key: string): Promise<boolean> {
|
|
382
|
+
return this.vault.delete(key, this.name);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async has(key: string): Promise<boolean> {
|
|
386
|
+
return this.vault.has(key, this.name);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async keys(): Promise<string[]> {
|
|
390
|
+
return this.vault.keys(this.name);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async wipe(): Promise<void> {
|
|
394
|
+
return this.vault.wipeNamespace(this.name);
|
|
395
|
+
}
|
|
396
|
+
}
|