@okint-digital/okint-rn-storage 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/android/build.gradle +59 -0
  4. package/android/proguard-rules.pro +8 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/com/okint/rnstorage/OkintRnStorageModule.kt +354 -0
  7. package/android/src/main/java/com/okint/rnstorage/OkintRnStoragePackage.kt +14 -0
  8. package/android/src/main/jni/CMakeLists.txt +24 -0
  9. package/android/src/main/jni/OkintJNI.cpp +15 -0
  10. package/cpp/OkintJSI.cpp +170 -0
  11. package/cpp/OkintJSI.h +18 -0
  12. package/ios/OkintRnStorage.m +457 -0
  13. package/ios/OkintRnStorageJSI.mm +24 -0
  14. package/lib/backends/memory.d.ts +16 -0
  15. package/lib/backends/memory.js +31 -0
  16. package/lib/backends/native-backend.d.ts +24 -0
  17. package/lib/backends/native-backend.js +73 -0
  18. package/lib/errors.d.ts +9 -0
  19. package/lib/errors.js +19 -0
  20. package/lib/facade.d.ts +29 -0
  21. package/lib/facade.js +93 -0
  22. package/lib/index.d.ts +48 -0
  23. package/lib/index.js +172 -0
  24. package/lib/native/bridge.d.ts +12 -0
  25. package/lib/native/bridge.js +23 -0
  26. package/lib/native/jsi.d.ts +7 -0
  27. package/lib/native/jsi.js +33 -0
  28. package/lib/sync/jsi-store.d.ts +28 -0
  29. package/lib/sync/jsi-store.js +81 -0
  30. package/lib/sync/persistence.d.ts +19 -0
  31. package/lib/sync/persistence.js +49 -0
  32. package/lib/sync/sync-store.d.ts +49 -0
  33. package/lib/sync/sync-store.js +159 -0
  34. package/lib/types.d.ts +140 -0
  35. package/lib/types.js +10 -0
  36. package/lib/validate.d.ts +20 -0
  37. package/lib/validate.js +91 -0
  38. package/okint-rn-storage.podspec +27 -0
  39. package/package.json +74 -0
  40. package/react-native.config.js +15 -0
  41. package/src/backends/memory.ts +35 -0
  42. package/src/backends/native-backend.ts +69 -0
  43. package/src/errors.ts +26 -0
  44. package/src/facade.ts +118 -0
  45. package/src/index.ts +194 -0
  46. package/src/native/bridge.ts +28 -0
  47. package/src/native/jsi.ts +37 -0
  48. package/src/sync/jsi-store.ts +98 -0
  49. package/src/sync/persistence.ts +47 -0
  50. package/src/sync/sync-store.ts +186 -0
  51. package/src/types.ts +174 -0
  52. package/src/validate.ts +102 -0
@@ -0,0 +1,24 @@
1
+ cmake_minimum_required(VERSION 3.13)
2
+ project(okint)
3
+
4
+ set(CMAKE_CXX_STANDARD 17)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+
7
+ # React Native ships JSI as a prefab consumed via find_package + buildFeatures.prefab.
8
+ find_package(ReactAndroid REQUIRED CONFIG)
9
+
10
+ add_library(
11
+ okint
12
+ SHARED
13
+ OkintJNI.cpp
14
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../../../cpp/OkintJSI.cpp
15
+ )
16
+
17
+ target_include_directories(okint PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../../../cpp)
18
+
19
+ target_link_libraries(
20
+ okint
21
+ ReactAndroid::jsi
22
+ android
23
+ log
24
+ )
@@ -0,0 +1,15 @@
1
+ #include <jni.h>
2
+ #include <jsi/jsi.h>
3
+ #include <string>
4
+
5
+ #include "OkintJSI.h"
6
+
7
+ extern "C" JNIEXPORT void JNICALL
8
+ Java_com_okint_rnstorage_OkintRnStorageModule_nativeInstallJSI(
9
+ JNIEnv *env, jobject /* thiz */, jlong jsiPtr, jstring dir) {
10
+ if (jsiPtr == 0) return;
11
+ auto *runtime = reinterpret_cast<facebook::jsi::Runtime *>(jsiPtr);
12
+ const char *d = env->GetStringUTFChars(dir, nullptr);
13
+ okint::install(*runtime, std::string(d ? d : ""));
14
+ if (d) env->ReleaseStringUTFChars(dir, d);
15
+ }
@@ -0,0 +1,170 @@
1
+ #include "OkintJSI.h"
2
+
3
+ #include <cstdint>
4
+ #include <fstream>
5
+ #include <memory>
6
+ #include <mutex>
7
+ #include <string>
8
+ #include <unordered_map>
9
+ #include <vector>
10
+
11
+ using namespace facebook::jsi;
12
+
13
+ namespace okint {
14
+
15
+ /**
16
+ * In-memory key/value store with simple length-prefixed file persistence.
17
+ * Synchronous and thread-safe. Loaded once on construction; rewritten on each
18
+ * mutation (correct + simple; a future mmap engine can make writes incremental).
19
+ */
20
+ class Store {
21
+ public:
22
+ explicit Store(std::string path) : path_(std::move(path)) { load(); }
23
+
24
+ bool get(const std::string &key, std::string &out) {
25
+ std::lock_guard<std::mutex> lock(mutex_);
26
+ auto it = map_.find(key);
27
+ if (it == map_.end()) return false;
28
+ out = it->second;
29
+ return true;
30
+ }
31
+
32
+ void set(const std::string &key, const std::string &value) {
33
+ std::lock_guard<std::mutex> lock(mutex_);
34
+ map_[key] = value;
35
+ persist();
36
+ }
37
+
38
+ void remove(const std::string &key) {
39
+ std::lock_guard<std::mutex> lock(mutex_);
40
+ if (map_.erase(key) > 0) persist();
41
+ }
42
+
43
+ void clear() {
44
+ std::lock_guard<std::mutex> lock(mutex_);
45
+ map_.clear();
46
+ persist();
47
+ }
48
+
49
+ bool contains(const std::string &key) {
50
+ std::lock_guard<std::mutex> lock(mutex_);
51
+ return map_.find(key) != map_.end();
52
+ }
53
+
54
+ std::vector<std::string> keys() {
55
+ std::lock_guard<std::mutex> lock(mutex_);
56
+ std::vector<std::string> out;
57
+ out.reserve(map_.size());
58
+ for (const auto &kv : map_) out.push_back(kv.first);
59
+ return out;
60
+ }
61
+
62
+ private:
63
+ void load() {
64
+ std::ifstream f(path_, std::ios::binary);
65
+ if (!f) return;
66
+ while (true) {
67
+ uint32_t kl = 0, vl = 0;
68
+ if (!f.read(reinterpret_cast<char *>(&kl), 4)) break;
69
+ std::string k(kl, '\0');
70
+ if (kl && !f.read(&k[0], kl)) break;
71
+ if (!f.read(reinterpret_cast<char *>(&vl), 4)) break;
72
+ std::string v(vl, '\0');
73
+ if (vl && !f.read(&v[0], vl)) break;
74
+ map_[std::move(k)] = std::move(v);
75
+ }
76
+ }
77
+
78
+ void persist() {
79
+ std::ofstream f(path_, std::ios::binary | std::ios::trunc);
80
+ if (!f) return;
81
+ for (const auto &kv : map_) {
82
+ uint32_t kl = static_cast<uint32_t>(kv.first.size());
83
+ uint32_t vl = static_cast<uint32_t>(kv.second.size());
84
+ f.write(reinterpret_cast<const char *>(&kl), 4);
85
+ f.write(kv.first.data(), kl);
86
+ f.write(reinterpret_cast<const char *>(&vl), 4);
87
+ f.write(kv.second.data(), vl);
88
+ }
89
+ }
90
+
91
+ std::string path_;
92
+ std::unordered_map<std::string, std::string> map_;
93
+ std::mutex mutex_;
94
+ };
95
+
96
+ namespace {
97
+
98
+ Function hostFn(Runtime &rt, const char *name, unsigned argc, HostFunctionType fn) {
99
+ return Function::createFromHostFunction(rt, PropNameID::forAscii(rt, name), argc, std::move(fn));
100
+ }
101
+
102
+ /** HostObject exposing the synchronous store API to JS. */
103
+ class OkintHostObject : public HostObject {
104
+ public:
105
+ explicit OkintHostObject(std::shared_ptr<Store> store) : store_(std::move(store)) {}
106
+
107
+ Value get(Runtime &rt, const PropNameID &name) override {
108
+ std::string prop = name.utf8(rt);
109
+ auto store = store_;
110
+
111
+ if (prop == "getString") {
112
+ return hostFn(rt, "getString", 1, [store](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
113
+ if (count < 1 || !args[0].isString()) return Value::null();
114
+ std::string out;
115
+ if (store->get(args[0].asString(rt).utf8(rt), out)) return String::createFromUtf8(rt, out);
116
+ return Value::null();
117
+ });
118
+ }
119
+ if (prop == "setString") {
120
+ return hostFn(rt, "setString", 2, [store](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
121
+ if (count < 2 || !args[0].isString() || !args[1].isString()) return Value::undefined();
122
+ store->set(args[0].asString(rt).utf8(rt), args[1].asString(rt).utf8(rt));
123
+ return Value::undefined();
124
+ });
125
+ }
126
+ if (prop == "remove") {
127
+ return hostFn(rt, "remove", 1, [store](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
128
+ if (count >= 1 && args[0].isString()) store->remove(args[0].asString(rt).utf8(rt));
129
+ return Value::undefined();
130
+ });
131
+ }
132
+ if (prop == "clear") {
133
+ return hostFn(rt, "clear", 0, [store](Runtime &, const Value &, const Value *, size_t) -> Value {
134
+ store->clear();
135
+ return Value::undefined();
136
+ });
137
+ }
138
+ if (prop == "contains") {
139
+ return hostFn(rt, "contains", 1, [store](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
140
+ if (count < 1 || !args[0].isString()) return Value(false);
141
+ return Value(store->contains(args[0].asString(rt).utf8(rt)));
142
+ });
143
+ }
144
+ if (prop == "getAllKeys") {
145
+ return hostFn(rt, "getAllKeys", 0, [store](Runtime &rt, const Value &, const Value *, size_t) -> Value {
146
+ auto ks = store->keys();
147
+ Array arr(rt, ks.size());
148
+ for (size_t i = 0; i < ks.size(); i++) arr.setValueAtIndex(rt, i, String::createFromUtf8(rt, ks[i]));
149
+ return arr;
150
+ });
151
+ }
152
+ return Value::undefined();
153
+ }
154
+
155
+ private:
156
+ std::shared_ptr<Store> store_;
157
+ };
158
+
159
+ } // namespace
160
+
161
+ void install(Runtime &rt, const std::string &storageDir) {
162
+ auto create = hostFn(rt, "__okintCreateJSI", 1, [storageDir](Runtime &rt, const Value &, const Value *args, size_t count) -> Value {
163
+ std::string ns = (count >= 1 && args[0].isString()) ? args[0].asString(rt).utf8(rt) : "okint";
164
+ auto store = std::make_shared<Store>(storageDir + "/okint_jsi_" + ns + ".bin");
165
+ return Object::createFromHostObject(rt, std::make_shared<OkintHostObject>(store));
166
+ });
167
+ rt.global().setProperty(rt, "__okintCreateJSI", create);
168
+ }
169
+
170
+ } // namespace okint
package/cpp/OkintJSI.h ADDED
@@ -0,0 +1,18 @@
1
+ #pragma once
2
+
3
+ #include <jsi/jsi.h>
4
+ #include <string>
5
+
6
+ namespace okint {
7
+
8
+ /**
9
+ * Installs the okint JSI fast path into the given runtime. Exposes a global
10
+ * function `global.__okintCreateJSI(namespace)` that returns a HostObject with
11
+ * synchronous get/set/remove/clear/getAllKeys/contains — direct C++ access with
12
+ * no bridge serialization (the maximum-performance synchronous path).
13
+ *
14
+ * `storageDir` is a writable directory; each namespace persists to its own file.
15
+ */
16
+ void install(facebook::jsi::Runtime &rt, const std::string &storageDir);
17
+
18
+ } // namespace okint
@@ -0,0 +1,457 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <Security/Security.h>
3
+ #import <CommonCrypto/CommonCrypto.h>
4
+ #import <sqlite3.h>
5
+
6
+ /**
7
+ * okint-rn-storage — iOS native module (Objective-C; no Swift bridging pitfalls).
8
+ *
9
+ * One module, four stores selected by `store`:
10
+ * - "secure" → Keychain (kSecClassGenericPassword, AfterFirstUnlock,
11
+ * this-device-only). For JWTs / FCM / secrets.
12
+ * - "async" → a per-namespace NSUserDefaults suite (plaintext).
13
+ * - "encrypted" → a fully-encrypted SQLite table: KEYS and VALUES sealed with
14
+ * AES-256-CBC + HMAC-SHA256 (encrypt-then-MAC, public
15
+ * CommonCrypto); lookups via a deterministic HMAC token. The
16
+ * 96-byte key (enc|mac|token) lives in the Keychain. No
17
+ * plaintext in the database — an encrypted DB, no SQLCipher dep.
18
+ * - "sqlite" → plaintext values in a separate SQLite table.
19
+ *
20
+ * Plus a blocking-sync bulk read (`getEntriesSync`) and a C++/JSI installer
21
+ * (`installJSI`, implemented in OkintRnStorageJSI.mm).
22
+ *
23
+ * NOTE: reviewed against current Apple APIs; verified at app build time.
24
+ */
25
+
26
+ // Implemented in OkintRnStorageJSI.mm (Obj-C++).
27
+ extern BOOL OkintInstallJSIForBridge(RCTBridge *bridge);
28
+
29
+ @interface OkintRnStorage : NSObject <RCTBridgeModule>
30
+ @end
31
+
32
+ @implementation OkintRnStorage
33
+
34
+ @synthesize bridge = _bridge;
35
+
36
+ RCT_EXPORT_MODULE()
37
+
38
+ + (BOOL)requiresMainQueueSetup { return NO; }
39
+
40
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(installJSI) {
41
+ return @(OkintInstallJSIForBridge(self.bridge));
42
+ }
43
+
44
+ #pragma mark - Scoping
45
+
46
+ static NSString *OkintScope(NSString *store, NSString *service) {
47
+ return [NSString stringWithFormat:@"okint.%@.%@", store, service];
48
+ }
49
+
50
+ static NSUserDefaults *OkintDefaults(NSString *store, NSString *service) {
51
+ NSUserDefaults *d = [[NSUserDefaults alloc] initWithSuiteName:OkintScope(store, service)];
52
+ return d ?: [NSUserDefaults standardUserDefaults];
53
+ }
54
+
55
+ #pragma mark - Keychain (secure store + encrypted-key storage)
56
+
57
+ static NSMutableDictionary *OkintKCQuery(NSString *scope, NSString *_Nullable key) {
58
+ NSMutableDictionary *q = [NSMutableDictionary dictionary];
59
+ q[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword;
60
+ q[(__bridge id)kSecAttrService] = scope;
61
+ q[(__bridge id)kSecUseDataProtectionKeychain] = @YES;
62
+ if (key) q[(__bridge id)kSecAttrAccount] = key;
63
+ return q;
64
+ }
65
+
66
+ static OSStatus OkintKCSetData(NSString *scope, NSString *key, NSData *data) {
67
+ NSMutableDictionary *q = OkintKCQuery(scope, key);
68
+ OSStatus s = SecItemUpdate((__bridge CFDictionaryRef)q, (__bridge CFDictionaryRef)@{ (__bridge id)kSecValueData: data });
69
+ if (s == errSecItemNotFound) {
70
+ NSMutableDictionary *add = OkintKCQuery(scope, key);
71
+ add[(__bridge id)kSecValueData] = data;
72
+ add[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
73
+ s = SecItemAdd((__bridge CFDictionaryRef)add, NULL);
74
+ }
75
+ return s;
76
+ }
77
+
78
+ static NSData *OkintKCGetData(NSString *scope, NSString *key) {
79
+ NSMutableDictionary *q = OkintKCQuery(scope, key);
80
+ q[(__bridge id)kSecReturnData] = @YES;
81
+ q[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
82
+ CFTypeRef result = NULL;
83
+ if (SecItemCopyMatching((__bridge CFDictionaryRef)q, &result) == errSecSuccess) {
84
+ return (__bridge_transfer NSData *)result;
85
+ }
86
+ return nil;
87
+ }
88
+
89
+ #pragma mark - Crypto (encrypted store)
90
+
91
+ static NSData *OkintRandom(size_t n) {
92
+ NSMutableData *d = [NSMutableData dataWithLength:n];
93
+ if (SecRandomCopyBytes(kSecRandomDefault, n, d.mutableBytes) != errSecSuccess) return nil;
94
+ return d;
95
+ }
96
+
97
+ static NSData *OkintAES(CCOperation op, NSData *key, NSData *iv, NSData *in) {
98
+ size_t bufLen = in.length + kCCBlockSizeAES128;
99
+ NSMutableData *out = [NSMutableData dataWithLength:bufLen];
100
+ size_t moved = 0;
101
+ CCCryptorStatus s = CCCrypt(op, kCCAlgorithmAES, kCCOptionPKCS7Padding,
102
+ key.bytes, kCCKeySizeAES256, iv.bytes,
103
+ in.bytes, in.length, out.mutableBytes, bufLen, &moved);
104
+ if (s != kCCSuccess) return nil;
105
+ out.length = moved;
106
+ return out;
107
+ }
108
+
109
+ static NSData *OkintHMAC(NSData *key, NSData *data) {
110
+ NSMutableData *mac = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
111
+ CCHmac(kCCHmacAlgSHA256, key.bytes, key.length, data.bytes, data.length, mac.mutableBytes);
112
+ return mac;
113
+ }
114
+
115
+ static BOOL OkintConstEq(NSData *a, NSData *b) {
116
+ if (a.length != b.length) return NO;
117
+ const uint8_t *pa = a.bytes;
118
+ const uint8_t *pb = b.bytes;
119
+ uint8_t r = 0;
120
+ for (NSUInteger i = 0; i < a.length; i++) r |= (uint8_t)(pa[i] ^ pb[i]);
121
+ return r == 0;
122
+ }
123
+
124
+ /** 96-byte key: [0,32) AES enc, [32,64) HMAC for MAC, [64,96) HMAC for token. */
125
+ static NSData *OkintEncKey(NSString *service) {
126
+ NSString *scope = OkintScope(@"enckey", service);
127
+ NSData *existing = OkintKCGetData(scope, @"key");
128
+ if (existing && existing.length == 96) return existing;
129
+ NSData *fresh = OkintRandom(96);
130
+ if (fresh) OkintKCSetData(scope, @"key", fresh);
131
+ return fresh;
132
+ }
133
+
134
+ static NSString *OkintToken(NSString *service, NSString *key) {
135
+ NSData *k = OkintEncKey(service);
136
+ if (k.length != 96) return nil;
137
+ NSData *tokKey = [k subdataWithRange:NSMakeRange(64, 32)];
138
+ NSData *mac = OkintHMAC(tokKey, [key dataUsingEncoding:NSUTF8StringEncoding]);
139
+ return [mac base64EncodedStringWithOptions:0];
140
+ }
141
+
142
+ static NSString *OkintEncrypt(NSString *service, NSString *value) {
143
+ NSData *k = OkintEncKey(service);
144
+ if (k.length != 96) return nil;
145
+ NSData *encKey = [k subdataWithRange:NSMakeRange(0, 32)];
146
+ NSData *macKey = [k subdataWithRange:NSMakeRange(32, 32)];
147
+ NSData *iv = OkintRandom(16);
148
+ NSData *ct = OkintAES(kCCEncrypt, encKey, iv, [value dataUsingEncoding:NSUTF8StringEncoding]);
149
+ if (!ct) return nil;
150
+ NSMutableData *ivct = [NSMutableData dataWithData:iv];
151
+ [ivct appendData:ct];
152
+ NSData *mac = OkintHMAC(macKey, ivct);
153
+ NSMutableData *blob = [NSMutableData dataWithData:ivct];
154
+ [blob appendData:mac];
155
+ return [blob base64EncodedStringWithOptions:0];
156
+ }
157
+
158
+ static NSString *OkintDecrypt(NSString *service, NSString *b64) {
159
+ NSData *blob = [[NSData alloc] initWithBase64EncodedString:b64 options:0];
160
+ if (!blob || blob.length < 16 + CC_SHA256_DIGEST_LENGTH) return nil;
161
+ NSData *k = OkintEncKey(service);
162
+ if (k.length != 96) return nil;
163
+ NSData *encKey = [k subdataWithRange:NSMakeRange(0, 32)];
164
+ NSData *macKey = [k subdataWithRange:NSMakeRange(32, 32)];
165
+ NSUInteger ctLen = blob.length - 16 - CC_SHA256_DIGEST_LENGTH;
166
+ NSData *iv = [blob subdataWithRange:NSMakeRange(0, 16)];
167
+ NSData *ct = [blob subdataWithRange:NSMakeRange(16, ctLen)];
168
+ NSData *mac = [blob subdataWithRange:NSMakeRange(16 + ctLen, CC_SHA256_DIGEST_LENGTH)];
169
+ NSData *ivct = [blob subdataWithRange:NSMakeRange(0, 16 + ctLen)];
170
+ if (!OkintConstEq(OkintHMAC(macKey, ivct), mac)) return nil;
171
+ NSData *pt = OkintAES(kCCDecrypt, encKey, iv, ct);
172
+ if (!pt) return nil;
173
+ return [[NSString alloc] initWithData:pt encoding:NSUTF8StringEncoding];
174
+ }
175
+
176
+ #pragma mark - SQLite
177
+
178
+ static sqlite3 *gOkintDB = NULL;
179
+
180
+ static sqlite3 *OkintDB(void) {
181
+ if (gOkintDB == NULL) {
182
+ NSString *dir = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;
183
+ NSString *path = [dir stringByAppendingPathComponent:@"okint_sqlite.db"];
184
+ sqlite3_open([path UTF8String], &gOkintDB);
185
+ }
186
+ return gOkintDB;
187
+ }
188
+
189
+ static NSString *OkintSanitize(NSString *prefix, NSString *service) {
190
+ NSMutableString *t = [NSMutableString stringWithString:prefix];
191
+ for (NSUInteger i = 0; i < service.length; i++) {
192
+ unichar c = [service characterAtIndex:i];
193
+ BOOL ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_';
194
+ [t appendFormat:@"%C", ok ? c : (unichar)'_'];
195
+ }
196
+ return t;
197
+ }
198
+
199
+ static void OkintExec(NSString *sql) {
200
+ sqlite3_exec(OkintDB(), [sql UTF8String], NULL, NULL, NULL);
201
+ }
202
+
203
+ // ── sqlite store (plaintext, kv_ table) ──────────────────────────────────────
204
+
205
+ static NSString *OkintKvTable(NSString *service) { return OkintSanitize(@"kv_", service); }
206
+
207
+ static void OkintKvEnsure(NSString *service) {
208
+ OkintExec([NSString stringWithFormat:@"CREATE TABLE IF NOT EXISTS %@ (k TEXT PRIMARY KEY, v TEXT NOT NULL)", OkintKvTable(service)]);
209
+ }
210
+
211
+ static BOOL OkintKvSet(NSString *service, NSString *key, NSString *value) {
212
+ OkintKvEnsure(service);
213
+ sqlite3_stmt *st = NULL;
214
+ BOOL ok = NO;
215
+ NSString *sql = [NSString stringWithFormat:@"INSERT OR REPLACE INTO %@ (k, v) VALUES (?, ?)", OkintKvTable(service)];
216
+ if (sqlite3_prepare_v2(OkintDB(), [sql UTF8String], -1, &st, NULL) == SQLITE_OK) {
217
+ sqlite3_bind_text(st, 1, [key UTF8String], -1, SQLITE_TRANSIENT);
218
+ sqlite3_bind_text(st, 2, [value UTF8String], -1, SQLITE_TRANSIENT);
219
+ ok = (sqlite3_step(st) == SQLITE_DONE);
220
+ }
221
+ sqlite3_finalize(st);
222
+ return ok;
223
+ }
224
+
225
+ static NSString *OkintKvGet(NSString *service, NSString *key) {
226
+ OkintKvEnsure(service);
227
+ sqlite3_stmt *st = NULL;
228
+ NSString *out = nil;
229
+ NSString *sql = [NSString stringWithFormat:@"SELECT v FROM %@ WHERE k = ?", OkintKvTable(service)];
230
+ if (sqlite3_prepare_v2(OkintDB(), [sql UTF8String], -1, &st, NULL) == SQLITE_OK) {
231
+ sqlite3_bind_text(st, 1, [key UTF8String], -1, SQLITE_TRANSIENT);
232
+ if (sqlite3_step(st) == SQLITE_ROW) {
233
+ const unsigned char *t = sqlite3_column_text(st, 0);
234
+ if (t) out = [NSString stringWithUTF8String:(const char *)t];
235
+ }
236
+ }
237
+ sqlite3_finalize(st);
238
+ return out;
239
+ }
240
+
241
+ static void OkintKvDelete(NSString *service, NSString *key) {
242
+ OkintKvEnsure(service);
243
+ sqlite3_stmt *st = NULL;
244
+ NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE k = ?", OkintKvTable(service)];
245
+ if (sqlite3_prepare_v2(OkintDB(), [sql UTF8String], -1, &st, NULL) == SQLITE_OK) {
246
+ sqlite3_bind_text(st, 1, [key UTF8String], -1, SQLITE_TRANSIENT);
247
+ sqlite3_step(st);
248
+ }
249
+ sqlite3_finalize(st);
250
+ }
251
+
252
+ static NSDictionary *OkintKvAll(NSString *service) {
253
+ OkintKvEnsure(service);
254
+ sqlite3_stmt *st = NULL;
255
+ NSMutableDictionary *out = [NSMutableDictionary dictionary];
256
+ NSString *sql = [NSString stringWithFormat:@"SELECT k, v FROM %@", OkintKvTable(service)];
257
+ if (sqlite3_prepare_v2(OkintDB(), [sql UTF8String], -1, &st, NULL) == SQLITE_OK) {
258
+ while (sqlite3_step(st) == SQLITE_ROW) {
259
+ const unsigned char *k = sqlite3_column_text(st, 0);
260
+ const unsigned char *v = sqlite3_column_text(st, 1);
261
+ if (k && v) out[[NSString stringWithUTF8String:(const char *)k]] = [NSString stringWithUTF8String:(const char *)v];
262
+ }
263
+ }
264
+ sqlite3_finalize(st);
265
+ return out;
266
+ }
267
+
268
+ // ── encrypted store (enc_ table, encrypted keys + values, HMAC token) ─────────
269
+
270
+ static NSString *OkintEncTable(NSString *service) { return OkintSanitize(@"enc_", service); }
271
+
272
+ static void OkintEncEnsure(NSString *service) {
273
+ OkintExec([NSString stringWithFormat:@"CREATE TABLE IF NOT EXISTS %@ (kt TEXT PRIMARY KEY, ke TEXT NOT NULL, ve TEXT NOT NULL)", OkintEncTable(service)]);
274
+ }
275
+
276
+ static BOOL OkintEncSet(NSString *service, NSString *key, NSString *value) {
277
+ NSString *ke = OkintEncrypt(service, key);
278
+ NSString *ve = OkintEncrypt(service, value);
279
+ NSString *kt = OkintToken(service, key);
280
+ if (!ke || !ve || !kt) return NO;
281
+ OkintEncEnsure(service);
282
+ sqlite3_stmt *st = NULL;
283
+ BOOL ok = NO;
284
+ NSString *sql = [NSString stringWithFormat:@"INSERT OR REPLACE INTO %@ (kt, ke, ve) VALUES (?, ?, ?)", OkintEncTable(service)];
285
+ if (sqlite3_prepare_v2(OkintDB(), [sql UTF8String], -1, &st, NULL) == SQLITE_OK) {
286
+ sqlite3_bind_text(st, 1, [kt UTF8String], -1, SQLITE_TRANSIENT);
287
+ sqlite3_bind_text(st, 2, [ke UTF8String], -1, SQLITE_TRANSIENT);
288
+ sqlite3_bind_text(st, 3, [ve UTF8String], -1, SQLITE_TRANSIENT);
289
+ ok = (sqlite3_step(st) == SQLITE_DONE);
290
+ }
291
+ sqlite3_finalize(st);
292
+ return ok;
293
+ }
294
+
295
+ static NSString *OkintEncGet(NSString *service, NSString *key) {
296
+ OkintEncEnsure(service);
297
+ NSString *kt = OkintToken(service, key);
298
+ if (!kt) return nil;
299
+ sqlite3_stmt *st = NULL;
300
+ NSString *ve = nil;
301
+ NSString *sql = [NSString stringWithFormat:@"SELECT ve FROM %@ WHERE kt = ?", OkintEncTable(service)];
302
+ if (sqlite3_prepare_v2(OkintDB(), [sql UTF8String], -1, &st, NULL) == SQLITE_OK) {
303
+ sqlite3_bind_text(st, 1, [kt UTF8String], -1, SQLITE_TRANSIENT);
304
+ if (sqlite3_step(st) == SQLITE_ROW) {
305
+ const unsigned char *t = sqlite3_column_text(st, 0);
306
+ if (t) ve = [NSString stringWithUTF8String:(const char *)t];
307
+ }
308
+ }
309
+ sqlite3_finalize(st);
310
+ return ve ? OkintDecrypt(service, ve) : nil;
311
+ }
312
+
313
+ static void OkintEncDelete(NSString *service, NSString *key) {
314
+ OkintEncEnsure(service);
315
+ NSString *kt = OkintToken(service, key);
316
+ if (!kt) return;
317
+ sqlite3_stmt *st = NULL;
318
+ NSString *sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE kt = ?", OkintEncTable(service)];
319
+ if (sqlite3_prepare_v2(OkintDB(), [sql UTF8String], -1, &st, NULL) == SQLITE_OK) {
320
+ sqlite3_bind_text(st, 1, [kt UTF8String], -1, SQLITE_TRANSIENT);
321
+ sqlite3_step(st);
322
+ }
323
+ sqlite3_finalize(st);
324
+ }
325
+
326
+ static NSDictionary *OkintEncAll(NSString *service) {
327
+ OkintEncEnsure(service);
328
+ sqlite3_stmt *st = NULL;
329
+ NSMutableDictionary *out = [NSMutableDictionary dictionary];
330
+ NSString *sql = [NSString stringWithFormat:@"SELECT ke, ve FROM %@", OkintEncTable(service)];
331
+ if (sqlite3_prepare_v2(OkintDB(), [sql UTF8String], -1, &st, NULL) == SQLITE_OK) {
332
+ while (sqlite3_step(st) == SQLITE_ROW) {
333
+ const unsigned char *ke = sqlite3_column_text(st, 0);
334
+ const unsigned char *ve = sqlite3_column_text(st, 1);
335
+ if (ke && ve) {
336
+ NSString *k = OkintDecrypt(service, [NSString stringWithUTF8String:(const char *)ke]);
337
+ NSString *v = OkintDecrypt(service, [NSString stringWithUTF8String:(const char *)ve]);
338
+ if (k && v) out[k] = v;
339
+ }
340
+ }
341
+ }
342
+ sqlite3_finalize(st);
343
+ return out;
344
+ }
345
+
346
+ #pragma mark - Read dispatch
347
+
348
+ static NSString *OkintReadOne(NSString *service, NSString *key, NSString *store) {
349
+ if ([store isEqualToString:@"secure"]) {
350
+ NSData *d = OkintKCGetData(OkintScope(@"secure", service), key);
351
+ return d ? [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding] : nil;
352
+ }
353
+ if ([store isEqualToString:@"encrypted"]) return OkintEncGet(service, key);
354
+ if ([store isEqualToString:@"sqlite"]) return OkintKvGet(service, key);
355
+ return [OkintDefaults(store, service) stringForKey:key];
356
+ }
357
+
358
+ #pragma mark - Methods
359
+
360
+ RCT_EXPORT_METHOD(setItem:(NSString *)service key:(NSString *)key value:(NSString *)value store:(NSString *)store
361
+ resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
362
+ if ([store isEqualToString:@"secure"]) {
363
+ OSStatus s = OkintKCSetData(OkintScope(@"secure", service), key, [value dataUsingEncoding:NSUTF8StringEncoding]);
364
+ if (s == errSecSuccess) resolve([NSNull null]);
365
+ else reject(@"E_OKINT_SET", [NSString stringWithFormat:@"Keychain set failed (%d)", (int)s], nil);
366
+ return;
367
+ }
368
+ if ([store isEqualToString:@"encrypted"]) {
369
+ if (OkintEncSet(service, key, value)) resolve([NSNull null]);
370
+ else reject(@"E_OKINT_SET", @"Encrypted set failed", nil);
371
+ return;
372
+ }
373
+ if ([store isEqualToString:@"sqlite"]) {
374
+ if (OkintKvSet(service, key, value)) resolve([NSNull null]);
375
+ else reject(@"E_OKINT_SET", @"SQLite insert failed", nil);
376
+ return;
377
+ }
378
+ [OkintDefaults(store, service) setObject:value forKey:key];
379
+ resolve([NSNull null]);
380
+ }
381
+
382
+ RCT_EXPORT_METHOD(getItem:(NSString *)service key:(NSString *)key store:(NSString *)store
383
+ resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
384
+ NSString *v = OkintReadOne(service, key, store);
385
+ resolve(v ?: [NSNull null]);
386
+ }
387
+
388
+ RCT_EXPORT_METHOD(removeItem:(NSString *)service key:(NSString *)key store:(NSString *)store
389
+ resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
390
+ if ([store isEqualToString:@"secure"]) {
391
+ OSStatus s = SecItemDelete((__bridge CFDictionaryRef)OkintKCQuery(OkintScope(@"secure", service), key));
392
+ if (s == errSecSuccess || s == errSecItemNotFound) resolve([NSNull null]);
393
+ else reject(@"E_OKINT_REMOVE", [NSString stringWithFormat:@"Keychain delete failed (%d)", (int)s], nil);
394
+ return;
395
+ }
396
+ if ([store isEqualToString:@"encrypted"]) { OkintEncDelete(service, key); resolve([NSNull null]); return; }
397
+ if ([store isEqualToString:@"sqlite"]) { OkintKvDelete(service, key); resolve([NSNull null]); return; }
398
+ [OkintDefaults(store, service) removeObjectForKey:key];
399
+ resolve([NSNull null]);
400
+ }
401
+
402
+ RCT_EXPORT_METHOD(clear:(NSString *)service store:(NSString *)store
403
+ resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
404
+ if ([store isEqualToString:@"secure"]) {
405
+ OSStatus s = SecItemDelete((__bridge CFDictionaryRef)OkintKCQuery(OkintScope(@"secure", service), nil));
406
+ if (s == errSecSuccess || s == errSecItemNotFound) resolve([NSNull null]);
407
+ else reject(@"E_OKINT_CLEAR", [NSString stringWithFormat:@"Keychain clear failed (%d)", (int)s], nil);
408
+ return;
409
+ }
410
+ if ([store isEqualToString:@"encrypted"]) { OkintExec([NSString stringWithFormat:@"DELETE FROM %@", OkintEncTable(service)]); resolve([NSNull null]); return; }
411
+ if ([store isEqualToString:@"sqlite"]) { OkintExec([NSString stringWithFormat:@"DELETE FROM %@", OkintKvTable(service)]); resolve([NSNull null]); return; }
412
+ [OkintDefaults(store, service) removePersistentDomainForName:OkintScope(store, service)];
413
+ resolve([NSNull null]);
414
+ }
415
+
416
+ RCT_EXPORT_METHOD(getAllKeys:(NSString *)service store:(NSString *)store
417
+ resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
418
+ if ([store isEqualToString:@"secure"]) {
419
+ NSMutableDictionary *q = OkintKCQuery(OkintScope(@"secure", service), nil);
420
+ q[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
421
+ q[(__bridge id)kSecReturnAttributes] = @YES;
422
+ CFTypeRef result = NULL;
423
+ OSStatus s = SecItemCopyMatching((__bridge CFDictionaryRef)q, &result);
424
+ if (s == errSecItemNotFound) { resolve(@[]); return; }
425
+ if (s != errSecSuccess) { reject(@"E_OKINT_KEYS", [NSString stringWithFormat:@"Keychain enumerate failed (%d)", (int)s], nil); return; }
426
+ NSArray *items = (__bridge_transfer NSArray *)result;
427
+ NSMutableArray<NSString *> *keys = [NSMutableArray array];
428
+ for (NSDictionary *item in items) {
429
+ NSString *account = item[(__bridge id)kSecAttrAccount];
430
+ if (account) [keys addObject:account];
431
+ }
432
+ resolve(keys);
433
+ return;
434
+ }
435
+ if ([store isEqualToString:@"encrypted"]) { resolve([OkintEncAll(service) allKeys]); return; }
436
+ if ([store isEqualToString:@"sqlite"]) { resolve([OkintKvAll(service) allKeys]); return; }
437
+ NSDictionary *domain = [OkintDefaults(store, service) persistentDomainForName:OkintScope(store, service)];
438
+ resolve(domain ? [domain allKeys] : @[]);
439
+ }
440
+
441
+ /** Blocking-synchronous bulk read for the zero-load sync store. */
442
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getEntriesSync:(NSString *)service store:(NSString *)store) {
443
+ if ([store isEqualToString:@"async"]) {
444
+ NSDictionary *domain = [OkintDefaults(store, service) persistentDomainForName:OkintScope(store, service)];
445
+ NSMutableDictionary *out = [NSMutableDictionary dictionary];
446
+ for (NSString *k in domain.allKeys) {
447
+ id v = domain[k];
448
+ if ([v isKindOfClass:[NSString class]]) out[k] = v;
449
+ }
450
+ return out;
451
+ }
452
+ if ([store isEqualToString:@"encrypted"]) return OkintEncAll(service);
453
+ if ([store isEqualToString:@"sqlite"]) return OkintKvAll(service);
454
+ return @{};
455
+ }
456
+
457
+ @end