@gnolang/gnonative 4.7.2 → 4.7.3

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.
Binary file
Binary file
@@ -17,6 +17,7 @@ class GnonativeModule : Module() {
17
17
  private var rootDir: File? = null
18
18
  private var socketPort = 0
19
19
  private var bridgeGnoNative: Bridge? = null
20
+ private var nativeDBManager: NativeDBManager? = null
20
21
 
21
22
  // Each module class must implement the definition function. The definition consists of components
22
23
  // that describes the module's functionality and behavior.
@@ -33,6 +34,7 @@ class GnonativeModule : Module() {
33
34
  OnCreate {
34
35
  context = appContext.reactContext
35
36
  rootDir = context!!.filesDir
37
+ nativeDBManager = NativeDBManager(context!!)
36
38
  }
37
39
 
38
40
  OnDestroy {
@@ -53,8 +55,8 @@ class GnonativeModule : Module() {
53
55
  try {
54
56
  val config: BridgeConfig = Gnonative.newBridgeConfig() ?: throw Exception("")
55
57
  config.rootDir = rootDir!!.absolutePath
58
+ config.nativeDB = nativeDBManager
56
59
  bridgeGnoNative = Gnonative.newBridge(config)
57
-
58
60
  promise.resolve(true)
59
61
  } catch (err: CodedException) {
60
62
  promise.reject(err)
@@ -0,0 +1,325 @@
1
+ package land.gno.gnonative
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import android.os.Build
6
+ import android.security.keystore.KeyGenParameterSpec
7
+ import android.security.keystore.KeyProperties
8
+ import android.util.Base64
9
+ import gnolang.gno.gnonative.NativeDB
10
+ import java.io.ByteArrayOutputStream
11
+ import java.nio.ByteBuffer
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
+ import kotlin.math.min
18
+ import androidx.core.content.edit
19
+
20
+ class NativeDBManager(
21
+ context: Context,
22
+ private val prefsName: String = "gnonative_secure_db",
23
+ private val keyAlias: String = "gnonative_aes_key"
24
+ ) : NativeDB {
25
+
26
+ // -------- storage / index --------
27
+ private val prefs: SharedPreferences =
28
+ context.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
29
+ private val entryPrefix = "kv:" // entryPrefix + hexKey -> Base64(encrypted blob)
30
+ private val idxKey = "__idx__" // CSV of hex keys in ascending order
31
+
32
+ // -------- crypto --------
33
+ private val ks: KeyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
34
+
35
+ private val lock = Any()
36
+
37
+ init {
38
+ ensureAesKey()
39
+ if (!prefs.contains(idxKey)) prefs.edit { putString(idxKey, "") }
40
+ }
41
+
42
+ // ========== NativeDB implementation ==========
43
+
44
+ override fun delete(p0: ByteArray?) {
45
+ val key = requireKey(p0)
46
+ val hex = hex(key)
47
+ synchronized(lock) {
48
+ val idx = loadIndexAsc().toMutableList()
49
+ val pos = lowerBound(idx, hex)
50
+ if (pos < idx.size && idx[pos] == hex) {
51
+ idx.removeAt(pos)
52
+ saveIndexAsc(idx)
53
+ }
54
+ prefs.edit { remove("$entryPrefix$hex") }
55
+ }
56
+ }
57
+
58
+ override fun deleteSync(p0: ByteArray?) {
59
+ delete(p0)
60
+ }
61
+
62
+ override fun get(p0: ByteArray?): ByteArray {
63
+ val key = requireKey(p0)
64
+ val hex = hex(key)
65
+ val b64 = synchronized(lock) { prefs.getString("$entryPrefix$hex", null) }
66
+ ?: return ByteArray(0) // gomobile generated non-null return -> use empty on miss
67
+ val blob = Base64.decode(b64, Base64.NO_WRAP)
68
+ return decrypt(blob) ?: throw Exception("Failed to decrypt value for key: $hex")
69
+ }
70
+
71
+ override fun has(p0: ByteArray?): Boolean {
72
+ val key = requireKey(p0)
73
+ val hex = hex(key)
74
+ return synchronized(lock) { prefs.contains("$entryPrefix$hex") }
75
+ }
76
+
77
+ override fun scanChunk(
78
+ p0: ByteArray?, // start
79
+ p1: ByteArray?, // end
80
+ p2: ByteArray?, // seekKey
81
+ p3: Long, // limit
82
+ p4: Boolean // reverse
83
+ ): ByteArray {
84
+ val limit = if (p3 < 0) 0 else min(p3, Int.MAX_VALUE.toLong()).toInt()
85
+ return synchronized(lock) {
86
+ val asc = loadIndexAsc() // ascending hex keys
87
+ val startHex = p0?.let { hex(it) }
88
+ val endHex = p1?.let { hex(it) }
89
+ val seekHex = p2?.let { hex(it) }
90
+
91
+ val loBase = startHex?.let { lowerBound(asc, it) } ?: 0
92
+ val hiBase = endHex?.let { lowerBound(asc, it) } ?: asc.size
93
+ var slice: List<String> = if (hiBase <= loBase) emptyList() else asc.subList(loBase, hiBase)
94
+
95
+ // seek positioning & direction
96
+ slice = if (!p4) {
97
+ val from = seekHex?.let { upperBound(slice, it) } ?: 0
98
+ if (from >= slice.size) emptyList() else slice.subList(from, slice.size)
99
+ } else {
100
+ val positioned = if (seekHex != null) {
101
+ val idx = upperBound(slice, seekHex) - 1
102
+ if (idx < 0) emptyList() else slice.subList(0, idx + 1)
103
+ } else slice
104
+ positioned.asReversed()
105
+ }
106
+
107
+ val page = if (limit == 0) emptyList() else slice.take(limit)
108
+ val hasMore = page.isNotEmpty() && page.size < slice.size
109
+ val nextSeekHex = if (hasMore) page.last() else null
110
+
111
+ // materialize kv pairs in traversal order
112
+ val pairs = ArrayList<Pair<ByteArray, ByteArray>>(page.size)
113
+ for (h in page) {
114
+ val b64 = prefs.getString("$entryPrefix$h", null) ?: continue
115
+ val blob = Base64.decode(b64, Base64.NO_WRAP)
116
+ val v = decrypt(blob) ?: throw Exception("Failed to decrypt value for key: $h")
117
+ pairs += (unhex(h) to v)
118
+ }
119
+
120
+ // flags(1) | count(u32 BE) | [kLen k vLen v]* | nextSeekLen(u32 BE) | nextSeek
121
+ encodeChunkBlobBE(pairs, nextSeekHex?.let { unhex(it) }, hasMore)
122
+ }
123
+ }
124
+
125
+ override fun set(p0: ByteArray?, p1: ByteArray?) {
126
+ val key = requireKey(p0)
127
+ val value = requireValue(p1)
128
+ val hex = hex(key)
129
+ val enc = encrypt(value)
130
+ val b64 = Base64.encodeToString(enc, Base64.NO_WRAP)
131
+ synchronized(lock) {
132
+ val idx = loadIndexAsc().toMutableList()
133
+ val pos = lowerBound(idx, hex)
134
+ if (pos == idx.size || idx[pos] != hex) {
135
+ idx.add(pos, hex)
136
+ saveIndexAsc(idx)
137
+ }
138
+ prefs.edit { putString("$entryPrefix$hex", b64) }
139
+ }
140
+ }
141
+
142
+ override fun setSync(p0: ByteArray?, p1: ByteArray?) {
143
+ set(p0, p1)
144
+ }
145
+
146
+ // ========== helpers ==========
147
+
148
+ private fun requireKey(b: ByteArray?): ByteArray {
149
+ require(!(b == null || b.isEmpty())) { "key must not be null/empty" }
150
+ return b
151
+ }
152
+ private fun requireValue(b: ByteArray?): ByteArray {
153
+ require(b != null) { "value must not be null" }
154
+ return b
155
+ }
156
+
157
+ // ----- index (csv of hex keys, ascending) -----
158
+ private fun loadIndexAsc(): List<String> {
159
+ val csv = prefs.getString(idxKey, "") ?: ""
160
+ return if (csv.isEmpty()) emptyList() else csv.split(',').filter { it.isNotEmpty() }
161
+ }
162
+ private fun saveIndexAsc(keys: List<String>) {
163
+ prefs.edit { putString(idxKey, if (keys.isEmpty()) "" else keys.joinToString(",")) }
164
+ }
165
+
166
+ private fun lowerBound(list: List<String>, key: String): Int {
167
+ var lo = 0; var hi = list.size
168
+ while (lo < hi) {
169
+ val mid = (lo + hi) ushr 1
170
+ if (list[mid] < key) lo = mid + 1 else hi = mid
171
+ }
172
+ return lo
173
+ }
174
+ private fun upperBound(list: List<String>, key: String): Int {
175
+ var lo = 0; var hi = list.size
176
+ while (lo < hi) {
177
+ val mid = (lo + hi) ushr 1
178
+ if (list[mid] <= key) lo = mid + 1 else hi = mid
179
+ }
180
+ return lo
181
+ }
182
+
183
+ // crypto AES/GCM, StrongBox preferred
184
+ private fun ensureAesKey() {
185
+ if (getAesKey() != null) return
186
+
187
+ val kg = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
188
+ val base = KeyGenParameterSpec.Builder(
189
+ keyAlias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
190
+ )
191
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
192
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
193
+ .setKeySize(256)
194
+ .setRandomizedEncryptionRequired(true)
195
+
196
+ try {
197
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
198
+ base.setIsStrongBoxBacked(true)
199
+ }
200
+ kg.init(base.build())
201
+ kg.generateKey()
202
+ return
203
+ } catch (_: Throwable) {
204
+ // fall back below without StrongBox
205
+ }
206
+
207
+ kg.init(
208
+ KeyGenParameterSpec.Builder(
209
+ keyAlias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
210
+ )
211
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
212
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
213
+ .setKeySize(256)
214
+ .setRandomizedEncryptionRequired(true)
215
+ .build()
216
+ )
217
+ kg.generateKey()
218
+ }
219
+
220
+ private fun getAesKey(): SecretKey? {
221
+ val e = ks.getEntry(keyAlias, null) as? KeyStore.SecretKeyEntry
222
+ return e?.secretKey
223
+ }
224
+
225
+ private fun encrypt(plain: ByteArray): ByteArray {
226
+ val key = getAesKey() ?: error("AES key missing")
227
+
228
+ val c = Cipher.getInstance(AES_GCM)
229
+ c.init(Cipher.ENCRYPT_MODE, key)
230
+
231
+ val iv = c.iv
232
+ val ct = c.doFinal(plain)
233
+
234
+ // payload: [version=1][ivLen][iv][ct]
235
+ val out = ByteArray(1 + 1 + iv.size + ct.size)
236
+ var i = 0
237
+ out[i++] = 1
238
+ out[i++] = iv.size.toByte()
239
+ System.arraycopy(iv, 0, out, i, iv.size); i += iv.size
240
+ System.arraycopy(ct, 0, out, i, ct.size)
241
+ return out
242
+ }
243
+
244
+ private fun decrypt(blob: ByteArray?): ByteArray? {
245
+ if (blob == null || blob.size < 1 + 1 + 12) return null // iv is usually 12 bytes
246
+ var i = 0
247
+ val ver = blob[i++]
248
+ require(ver.toInt() == 1) { "bad payload version=$ver" }
249
+ val ivLen = blob[i++].toInt() and 0xFF
250
+ require(ivLen in 12..32) { "bad iv length" }
251
+ require(blob.size >= 1 + 1 + ivLen + 1) { "short blob" }
252
+ val iv = ByteArray(ivLen)
253
+ System.arraycopy(blob, i, iv, 0, ivLen); i += ivLen
254
+ val ct = ByteArray(blob.size - i)
255
+ System.arraycopy(blob, i, ct, 0, ct.size)
256
+
257
+ val key = getAesKey() ?: error("AES key missing")
258
+ val c = Cipher.getInstance(AES_GCM)
259
+ c.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, iv))
260
+ return c.doFinal(ct)
261
+ }
262
+
263
+ // chunk framing (match Go format)
264
+ private fun encodeChunkBlobBE(
265
+ entries: List<Pair<ByteArray, ByteArray>>,
266
+ nextSeek: ByteArray?,
267
+ hasMore: Boolean
268
+ ): ByteArray {
269
+ val bos = ByteArrayOutputStream()
270
+
271
+ // flags (bit0 = hasMore)
272
+ bos.write(if (hasMore) 0x01 else 0x00)
273
+
274
+ // count (u32 BE)
275
+ bos.write(u32be(entries.size))
276
+
277
+ // entries
278
+ for ((k, v) in entries) {
279
+ bos.write(u32be(k.size)); bos.write(k)
280
+ bos.write(u32be(v.size)); bos.write(v)
281
+ }
282
+
283
+ // nextSeek
284
+ val ns = nextSeek ?: ByteArray(0)
285
+ bos.write(u32be(ns.size))
286
+ if (ns.isNotEmpty()) bos.write(ns)
287
+
288
+ return bos.toByteArray()
289
+ }
290
+
291
+ // ----- utils -----
292
+ private fun u32be(n: Int): ByteArray {
293
+ val bb = ByteBuffer.allocate(4)
294
+ bb.putInt(n) // big-endian by default
295
+ return bb.array()
296
+ }
297
+
298
+ private fun hex(b: ByteArray): String {
299
+ val out = CharArray(b.size * 2)
300
+ val h = "0123456789abcdef".toCharArray()
301
+ var i = 0
302
+ for (v in b) {
303
+ val x = v.toInt() and 0xFF
304
+ out[i++] = h[x ushr 4]; out[i++] = h[x and 0x0F]
305
+ }
306
+ return String(out)
307
+ }
308
+
309
+ private fun unhex(s: String): ByteArray {
310
+ require(s.length % 2 == 0) { "odd hex length" }
311
+ val out = ByteArray(s.length / 2)
312
+ var i = 0; var j = 0
313
+ while (i < s.length) {
314
+ val hi = Character.digit(s[i++], 16)
315
+ val lo = Character.digit(s[i++], 16)
316
+ out[j++] = ((hi shl 4) or lo).toByte()
317
+ }
318
+ return out
319
+ }
320
+
321
+ companion object {
322
+ private const val ANDROID_KEYSTORE = "AndroidKeyStore"
323
+ private const val AES_GCM = "AES/GCM/NoPadding"
324
+ }
325
+ }
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>GnoCore.framework/Versions/A/GnoCore</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64</string>
11
+ <string>ios-arm64_x86_64-simulator</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>GnoCore.framework</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
+ <string>x86_64</string>
17
18
  </array>
18
19
  <key>SupportedPlatform</key>
19
20
  <string>ios</string>
21
+ <key>SupportedPlatformVariant</key>
22
+ <string>simulator</string>
20
23
  </dict>
21
24
  <dict>
22
25
  <key>BinaryPath</key>
23
26
  <string>GnoCore.framework/Versions/A/GnoCore</string>
24
27
  <key>LibraryIdentifier</key>
25
- <string>ios-arm64_x86_64-simulator</string>
28
+ <string>ios-arm64</string>
26
29
  <key>LibraryPath</key>
27
30
  <string>GnoCore.framework</string>
28
31
  <key>SupportedArchitectures</key>
29
32
  <array>
30
33
  <string>arm64</string>
31
- <string>x86_64</string>
32
34
  </array>
33
35
  <key>SupportedPlatform</key>
34
36
  <string>ios</string>
35
- <key>SupportedPlatformVariant</key>
36
- <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>
@@ -13,11 +13,23 @@
13
13
 
14
14
  @class GnoGnonativeBridge;
15
15
  @class GnoGnonativeBridgeConfig;
16
+ @protocol GnoGnonativeNativeDB;
17
+ @class GnoGnonativeNativeDB;
16
18
  @protocol GnoGnonativePromiseBlock;
17
19
  @class GnoGnonativePromiseBlock;
18
20
  @protocol GnoGnonativeServiceClient;
19
21
  @class GnoGnonativeServiceClient;
20
22
 
23
+ @protocol GnoGnonativeNativeDB <NSObject>
24
+ - (BOOL)delete:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
25
+ - (BOOL)deleteSync:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
26
+ - (NSData* _Nullable)get:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
27
+ - (BOOL)has:(NSData* _Nullable)key ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error;
28
+ - (NSData* _Nullable)scanChunk:(NSData* _Nullable)start end:(NSData* _Nullable)end seekKey:(NSData* _Nullable)seekKey limit:(long)limit reverse:(BOOL)reverse error:(NSError* _Nullable* _Nullable)error;
29
+ - (BOOL)set:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
30
+ - (BOOL)setSync:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
31
+ @end
32
+
21
33
  @protocol GnoGnonativePromiseBlock <NSObject>
22
34
  - (void)callReject:(NSError* _Nullable)error;
23
35
  - (void)callResolve:(NSString* _Nullable)reply;
@@ -53,6 +65,7 @@
53
65
 
54
66
  - (nonnull instancetype)initWithRef:(_Nonnull id)ref;
55
67
  - (nullable instancetype)init;
68
+ @property (nonatomic) id<GnoGnonativeNativeDB> _Nullable nativeDB;
56
69
  @property (nonatomic) NSString* _Nonnull rootDir;
57
70
  @property (nonatomic) NSString* _Nonnull tmpDir;
58
71
  @property (nonatomic) BOOL useTcpListener;
@@ -66,10 +79,29 @@ FOUNDATION_EXPORT GnoGnonativeBridgeConfig* _Nullable GnoGnonativeNewBridgeConfi
66
79
  // skipped function NewServiceClient with unsupported parameter or return types
67
80
 
68
81
 
82
+ @class GnoGnonativeNativeDB;
83
+
69
84
  @class GnoGnonativePromiseBlock;
70
85
 
71
86
  @class GnoGnonativeServiceClient;
72
87
 
88
+ /**
89
+ * NativeDB is implemented in the native (Kotlin/Swift) layer.
90
+ */
91
+ @interface GnoGnonativeNativeDB : NSObject <goSeqRefInterface, GnoGnonativeNativeDB> {
92
+ }
93
+ @property(strong, readonly) _Nonnull id _ref;
94
+
95
+ - (nonnull instancetype)initWithRef:(_Nonnull id)ref;
96
+ - (BOOL)delete:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
97
+ - (BOOL)deleteSync:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
98
+ - (NSData* _Nullable)get:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
99
+ - (BOOL)has:(NSData* _Nullable)key ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error;
100
+ - (NSData* _Nullable)scanChunk:(NSData* _Nullable)start end:(NSData* _Nullable)end seekKey:(NSData* _Nullable)seekKey limit:(long)limit reverse:(BOOL)reverse error:(NSError* _Nullable* _Nullable)error;
101
+ - (BOOL)set:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
102
+ - (BOOL)setSync:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
103
+ @end
104
+
73
105
  @interface GnoGnonativePromiseBlock : NSObject <goSeqRefInterface, GnoGnonativePromiseBlock> {
74
106
  }
75
107
  @property(strong, readonly) _Nonnull id _ref;
@@ -13,11 +13,23 @@
13
13
 
14
14
  @class GnoGnonativeBridge;
15
15
  @class GnoGnonativeBridgeConfig;
16
+ @protocol GnoGnonativeNativeDB;
17
+ @class GnoGnonativeNativeDB;
16
18
  @protocol GnoGnonativePromiseBlock;
17
19
  @class GnoGnonativePromiseBlock;
18
20
  @protocol GnoGnonativeServiceClient;
19
21
  @class GnoGnonativeServiceClient;
20
22
 
23
+ @protocol GnoGnonativeNativeDB <NSObject>
24
+ - (BOOL)delete:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
25
+ - (BOOL)deleteSync:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
26
+ - (NSData* _Nullable)get:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
27
+ - (BOOL)has:(NSData* _Nullable)key ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error;
28
+ - (NSData* _Nullable)scanChunk:(NSData* _Nullable)start end:(NSData* _Nullable)end seekKey:(NSData* _Nullable)seekKey limit:(long)limit reverse:(BOOL)reverse error:(NSError* _Nullable* _Nullable)error;
29
+ - (BOOL)set:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
30
+ - (BOOL)setSync:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
31
+ @end
32
+
21
33
  @protocol GnoGnonativePromiseBlock <NSObject>
22
34
  - (void)callReject:(NSError* _Nullable)error;
23
35
  - (void)callResolve:(NSString* _Nullable)reply;
@@ -53,6 +65,7 @@
53
65
 
54
66
  - (nonnull instancetype)initWithRef:(_Nonnull id)ref;
55
67
  - (nullable instancetype)init;
68
+ @property (nonatomic) id<GnoGnonativeNativeDB> _Nullable nativeDB;
56
69
  @property (nonatomic) NSString* _Nonnull rootDir;
57
70
  @property (nonatomic) NSString* _Nonnull tmpDir;
58
71
  @property (nonatomic) BOOL useTcpListener;
@@ -66,10 +79,29 @@ FOUNDATION_EXPORT GnoGnonativeBridgeConfig* _Nullable GnoGnonativeNewBridgeConfi
66
79
  // skipped function NewServiceClient with unsupported parameter or return types
67
80
 
68
81
 
82
+ @class GnoGnonativeNativeDB;
83
+
69
84
  @class GnoGnonativePromiseBlock;
70
85
 
71
86
  @class GnoGnonativeServiceClient;
72
87
 
88
+ /**
89
+ * NativeDB is implemented in the native (Kotlin/Swift) layer.
90
+ */
91
+ @interface GnoGnonativeNativeDB : NSObject <goSeqRefInterface, GnoGnonativeNativeDB> {
92
+ }
93
+ @property(strong, readonly) _Nonnull id _ref;
94
+
95
+ - (nonnull instancetype)initWithRef:(_Nonnull id)ref;
96
+ - (BOOL)delete:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
97
+ - (BOOL)deleteSync:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
98
+ - (NSData* _Nullable)get:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
99
+ - (BOOL)has:(NSData* _Nullable)key ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error;
100
+ - (NSData* _Nullable)scanChunk:(NSData* _Nullable)start end:(NSData* _Nullable)end seekKey:(NSData* _Nullable)seekKey limit:(long)limit reverse:(BOOL)reverse error:(NSError* _Nullable* _Nullable)error;
101
+ - (BOOL)set:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
102
+ - (BOOL)setSync:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
103
+ @end
104
+
73
105
  @interface GnoGnonativePromiseBlock : NSObject <goSeqRefInterface, GnoGnonativePromiseBlock> {
74
106
  }
75
107
  @property(strong, readonly) _Nonnull id _ref;
@@ -13,11 +13,23 @@
13
13
 
14
14
  @class GnoGnonativeBridge;
15
15
  @class GnoGnonativeBridgeConfig;
16
+ @protocol GnoGnonativeNativeDB;
17
+ @class GnoGnonativeNativeDB;
16
18
  @protocol GnoGnonativePromiseBlock;
17
19
  @class GnoGnonativePromiseBlock;
18
20
  @protocol GnoGnonativeServiceClient;
19
21
  @class GnoGnonativeServiceClient;
20
22
 
23
+ @protocol GnoGnonativeNativeDB <NSObject>
24
+ - (BOOL)delete:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
25
+ - (BOOL)deleteSync:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
26
+ - (NSData* _Nullable)get:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
27
+ - (BOOL)has:(NSData* _Nullable)key ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error;
28
+ - (NSData* _Nullable)scanChunk:(NSData* _Nullable)start end:(NSData* _Nullable)end seekKey:(NSData* _Nullable)seekKey limit:(long)limit reverse:(BOOL)reverse error:(NSError* _Nullable* _Nullable)error;
29
+ - (BOOL)set:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
30
+ - (BOOL)setSync:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
31
+ @end
32
+
21
33
  @protocol GnoGnonativePromiseBlock <NSObject>
22
34
  - (void)callReject:(NSError* _Nullable)error;
23
35
  - (void)callResolve:(NSString* _Nullable)reply;
@@ -53,6 +65,7 @@
53
65
 
54
66
  - (nonnull instancetype)initWithRef:(_Nonnull id)ref;
55
67
  - (nullable instancetype)init;
68
+ @property (nonatomic) id<GnoGnonativeNativeDB> _Nullable nativeDB;
56
69
  @property (nonatomic) NSString* _Nonnull rootDir;
57
70
  @property (nonatomic) NSString* _Nonnull tmpDir;
58
71
  @property (nonatomic) BOOL useTcpListener;
@@ -66,10 +79,29 @@ FOUNDATION_EXPORT GnoGnonativeBridgeConfig* _Nullable GnoGnonativeNewBridgeConfi
66
79
  // skipped function NewServiceClient with unsupported parameter or return types
67
80
 
68
81
 
82
+ @class GnoGnonativeNativeDB;
83
+
69
84
  @class GnoGnonativePromiseBlock;
70
85
 
71
86
  @class GnoGnonativeServiceClient;
72
87
 
88
+ /**
89
+ * NativeDB is implemented in the native (Kotlin/Swift) layer.
90
+ */
91
+ @interface GnoGnonativeNativeDB : NSObject <goSeqRefInterface, GnoGnonativeNativeDB> {
92
+ }
93
+ @property(strong, readonly) _Nonnull id _ref;
94
+
95
+ - (nonnull instancetype)initWithRef:(_Nonnull id)ref;
96
+ - (BOOL)delete:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
97
+ - (BOOL)deleteSync:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
98
+ - (NSData* _Nullable)get:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
99
+ - (BOOL)has:(NSData* _Nullable)key ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error;
100
+ - (NSData* _Nullable)scanChunk:(NSData* _Nullable)start end:(NSData* _Nullable)end seekKey:(NSData* _Nullable)seekKey limit:(long)limit reverse:(BOOL)reverse error:(NSError* _Nullable* _Nullable)error;
101
+ - (BOOL)set:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
102
+ - (BOOL)setSync:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
103
+ @end
104
+
73
105
  @interface GnoGnonativePromiseBlock : NSObject <goSeqRefInterface, GnoGnonativePromiseBlock> {
74
106
  }
75
107
  @property(strong, readonly) _Nonnull id _ref;
@@ -13,11 +13,23 @@
13
13
 
14
14
  @class GnoGnonativeBridge;
15
15
  @class GnoGnonativeBridgeConfig;
16
+ @protocol GnoGnonativeNativeDB;
17
+ @class GnoGnonativeNativeDB;
16
18
  @protocol GnoGnonativePromiseBlock;
17
19
  @class GnoGnonativePromiseBlock;
18
20
  @protocol GnoGnonativeServiceClient;
19
21
  @class GnoGnonativeServiceClient;
20
22
 
23
+ @protocol GnoGnonativeNativeDB <NSObject>
24
+ - (BOOL)delete:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
25
+ - (BOOL)deleteSync:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
26
+ - (NSData* _Nullable)get:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
27
+ - (BOOL)has:(NSData* _Nullable)key ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error;
28
+ - (NSData* _Nullable)scanChunk:(NSData* _Nullable)start end:(NSData* _Nullable)end seekKey:(NSData* _Nullable)seekKey limit:(long)limit reverse:(BOOL)reverse error:(NSError* _Nullable* _Nullable)error;
29
+ - (BOOL)set:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
30
+ - (BOOL)setSync:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
31
+ @end
32
+
21
33
  @protocol GnoGnonativePromiseBlock <NSObject>
22
34
  - (void)callReject:(NSError* _Nullable)error;
23
35
  - (void)callResolve:(NSString* _Nullable)reply;
@@ -53,6 +65,7 @@
53
65
 
54
66
  - (nonnull instancetype)initWithRef:(_Nonnull id)ref;
55
67
  - (nullable instancetype)init;
68
+ @property (nonatomic) id<GnoGnonativeNativeDB> _Nullable nativeDB;
56
69
  @property (nonatomic) NSString* _Nonnull rootDir;
57
70
  @property (nonatomic) NSString* _Nonnull tmpDir;
58
71
  @property (nonatomic) BOOL useTcpListener;
@@ -66,10 +79,29 @@ FOUNDATION_EXPORT GnoGnonativeBridgeConfig* _Nullable GnoGnonativeNewBridgeConfi
66
79
  // skipped function NewServiceClient with unsupported parameter or return types
67
80
 
68
81
 
82
+ @class GnoGnonativeNativeDB;
83
+
69
84
  @class GnoGnonativePromiseBlock;
70
85
 
71
86
  @class GnoGnonativeServiceClient;
72
87
 
88
+ /**
89
+ * NativeDB is implemented in the native (Kotlin/Swift) layer.
90
+ */
91
+ @interface GnoGnonativeNativeDB : NSObject <goSeqRefInterface, GnoGnonativeNativeDB> {
92
+ }
93
+ @property(strong, readonly) _Nonnull id _ref;
94
+
95
+ - (nonnull instancetype)initWithRef:(_Nonnull id)ref;
96
+ - (BOOL)delete:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
97
+ - (BOOL)deleteSync:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
98
+ - (NSData* _Nullable)get:(NSData* _Nullable)key error:(NSError* _Nullable* _Nullable)error;
99
+ - (BOOL)has:(NSData* _Nullable)key ret0_:(BOOL* _Nullable)ret0_ error:(NSError* _Nullable* _Nullable)error;
100
+ - (NSData* _Nullable)scanChunk:(NSData* _Nullable)start end:(NSData* _Nullable)end seekKey:(NSData* _Nullable)seekKey limit:(long)limit reverse:(BOOL)reverse error:(NSError* _Nullable* _Nullable)error;
101
+ - (BOOL)set:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
102
+ - (BOOL)setSync:(NSData* _Nullable)key value:(NSData* _Nullable)value error:(NSError* _Nullable* _Nullable)error;
103
+ @end
104
+
73
105
  @interface GnoGnonativePromiseBlock : NSObject <goSeqRefInterface, GnoGnonativePromiseBlock> {
74
106
  }
75
107
  @property(strong, readonly) _Nonnull id _ref;
@@ -60,6 +60,7 @@ public class GnonativeModule: Module {
60
60
  }
61
61
  config.rootDir = self.appRootDir!
62
62
  config.tmpDir = self.tmpDir!
63
+ config.nativeDB = NativeDBManager.shared
63
64
 
64
65
  // On simulator we can't create an UDS, see comment below
65
66
  #if targetEnvironment(simulator)
@@ -0,0 +1,317 @@
1
+ //
2
+ // NativeDBManager.swift
3
+ // Pods
4
+ //
5
+ // Created by Rémi BARBERO on 25/09/2025.
6
+ //
7
+
8
+ import Foundation
9
+ import Security
10
+ import GnoCore
11
+
12
+ public class NativeDBManager: NSObject, GnoGnonativeNativeDBProtocol {
13
+ public static var shared: NativeDBManager = NativeDBManager()
14
+
15
+ // MARK: - Private Properties
16
+ private let service: String
17
+ private let accessGroup: String?
18
+
19
+ // MARK: - Initialization
20
+ init(service: String = Bundle.main.bundleIdentifier ?? "GnoNativeService", accessGroup: String? = nil) {
21
+ self.service = service
22
+ self.accessGroup = accessGroup
23
+ }
24
+
25
+ // MARK: - Public Interface Implementation
26
+
27
+ public func get(_ key: Data?) throws -> Data {
28
+ guard let key = key, !key.isEmpty else {
29
+ throw NativeDBError.invalidArgument(description: "Key must not be nil or empty.")
30
+ }
31
+ let account = keyToAccount(key)
32
+
33
+ var query: [String: Any] = [
34
+ kSecClass as String: kSecClassGenericPassword,
35
+ kSecAttrService as String: service,
36
+ kSecAttrAccount as String: account,
37
+ kSecReturnData as String: true,
38
+ kSecMatchLimit as String: kSecMatchLimitOne
39
+ ]
40
+
41
+ if let accessGroup = accessGroup {
42
+ query[kSecAttrAccessGroup as String] = accessGroup
43
+ }
44
+
45
+ var result: AnyObject?
46
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
47
+
48
+ switch status {
49
+ case errSecSuccess:
50
+ return (result as? Data) ?? Data()
51
+ case errSecItemNotFound:
52
+ // This is not an error; it's the expected result for a missing key.
53
+ return Data()
54
+ default:
55
+ throw NativeDBError.keychainError(status: status, message: "Failed to get item.")
56
+ }
57
+ }
58
+
59
+ public func delete(_ key: Data?) throws {
60
+ try self.deleteSync(key)
61
+ }
62
+
63
+ public func deleteSync(_ key: Data?) throws {
64
+ guard let key = key, !key.isEmpty else {
65
+ throw NativeDBError.invalidArgument(description: "Key must not be nil or empty.")
66
+ }
67
+
68
+ let account = keyToAccount(key)
69
+
70
+ var query: [String: Any] = [
71
+ kSecClass as String: kSecClassGenericPassword,
72
+ kSecAttrService as String: service,
73
+ kSecAttrAccount as String: account
74
+ ]
75
+
76
+ if let accessGroup = accessGroup {
77
+ query[kSecAttrAccessGroup as String] = accessGroup
78
+ }
79
+
80
+ let status = SecItemDelete(query as CFDictionary)
81
+
82
+ // Deleting a non-existent item is not considered an error.
83
+ if status != errSecSuccess && status != errSecItemNotFound {
84
+ throw NativeDBError.keychainError(status: status, message: "Failed to delete item.")
85
+ }
86
+ }
87
+
88
+ public func has(_ key: Data?, ret0_ returnPointer: UnsafeMutablePointer<ObjCBool>?) throws {
89
+ // 1. Ensure the return pointer provided by the caller is valid.
90
+ guard let returnPointer = returnPointer else {
91
+ throw NativeDBError.invalidArgument(description: "Return pointer must not be nil.")
92
+ }
93
+
94
+ // 2. Validate the input key.
95
+ guard let key = key, !key.isEmpty else {
96
+ throw NativeDBError.invalidArgument(description: "Key must not be nil or empty.")
97
+ }
98
+
99
+ let account = keyToAccount(key)
100
+
101
+ var query: [String: Any] = [
102
+ kSecClass as String: kSecClassGenericPassword,
103
+ kSecAttrService as String: service,
104
+ kSecAttrAccount as String: account
105
+ ]
106
+ if let accessGroup = accessGroup {
107
+ query[kSecAttrAccessGroup as String] = accessGroup
108
+ }
109
+
110
+ let status = SecItemCopyMatching(query as CFDictionary, nil)
111
+
112
+ // 3. Handle the result from the Keychain.
113
+ switch status {
114
+ case errSecSuccess:
115
+ // Key exists. Write `true` to the pointer's memory location.
116
+ returnPointer.pointee = ObjCBool(true)
117
+ case errSecItemNotFound:
118
+ // Key does not exist. Write `false` to the pointer's memory location.
119
+ returnPointer.pointee = ObjCBool(false)
120
+ default:
121
+ // A real keychain error occurred. Throw an error.
122
+ throw NativeDBError.keychainError(status: status, message: "Failed to check for item existence.")
123
+ }
124
+ }
125
+
126
+ public func set(_ key: Data?, value: Data?) throws {
127
+ try self.setSync(key, value: value)
128
+ }
129
+
130
+ public func setSync(_ key: Data?, value: Data?) throws {
131
+ guard let key = key, !key.isEmpty else {
132
+ throw NativeDBError.invalidArgument(description: "Key must not be nil or empty.")
133
+ }
134
+ guard let value = value else {
135
+ throw NativeDBError.invalidArgument(description: "Value must not be nil.")
136
+ }
137
+
138
+ let account = keyToAccount(key)
139
+
140
+ // First, try to update existing item
141
+ var query: [String: Any] = [
142
+ kSecClass as String: kSecClassGenericPassword,
143
+ kSecAttrService as String: service,
144
+ kSecAttrAccount as String: account
145
+ ]
146
+
147
+ if let accessGroup = accessGroup {
148
+ query[kSecAttrAccessGroup as String] = accessGroup
149
+ }
150
+
151
+ let attributes: [String: Any] = [
152
+ kSecValueData as String: value
153
+ ]
154
+
155
+ let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
156
+
157
+ switch updateStatus {
158
+ case errSecSuccess:
159
+ // Update was successful.
160
+ return
161
+ case errSecItemNotFound:
162
+ // Item doesn't exist, so add it.
163
+ var newItem = query
164
+ newItem[kSecValueData as String] = value
165
+ newItem[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
166
+
167
+ let addStatus = SecItemAdd(newItem as CFDictionary, nil)
168
+ if addStatus != errSecSuccess {
169
+ throw NativeDBError.keychainError(status: addStatus, message: "Failed to add new item.")
170
+ }
171
+ default:
172
+ // Another error occurred during update.
173
+ throw NativeDBError.keychainError(status: updateStatus, message: "Failed to update item.")
174
+ }
175
+ }
176
+
177
+ public func scanChunk(_ start: Data?, end: Data?, seekKey: Data?, limit: Int, reverse: Bool) throws -> Data {
178
+ // 1) fetch all items for this service
179
+ var query: [String: Any] = [
180
+ kSecClass as String: kSecClassGenericPassword,
181
+ kSecAttrService as String: service,
182
+ kSecMatchLimit as String: kSecMatchLimitAll,
183
+ kSecReturnAttributes as String: true,
184
+ kSecReturnData as String: true,
185
+ ]
186
+
187
+ if let accessGroup = accessGroup {
188
+ query[kSecAttrAccessGroup as String] = accessGroup
189
+ }
190
+ var result: CFTypeRef?
191
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
192
+
193
+ var pairs: [(key: Data, val: Data)] = []
194
+ if status == errSecSuccess, let items = result as? [[String: Any]] {
195
+ for item in items {
196
+ guard let account = item[kSecAttrAccount as String] as? String,
197
+ let keyBytes = accountToKey(account),
198
+ let val = item[kSecValueData as String] as? Data else { continue }
199
+ if inRange(keyBytes, start: start, end: end) {
200
+ pairs.append((keyBytes, val))
201
+ }
202
+ }
203
+ }
204
+
205
+ // 2) sort
206
+ if reverse {
207
+ pairs.sort { a, b in
208
+ // descending: a > b
209
+ return lt(b.key, a.key)
210
+ }
211
+ } else {
212
+ pairs.sort { a, b in
213
+ // ascending: a < b
214
+ return lt(a.key, b.key)
215
+ }
216
+ }
217
+
218
+ // 3) apply seekKey (exclusive)
219
+ if let sk = seekKey, !sk.isEmpty {
220
+ if reverse {
221
+ // keep items with key < seekKey
222
+ let idx = pairs.firstIndex(where: { lt($0.key, sk) }) ?? pairs.count
223
+ // pairs are descending, so drop while key >= seekKey
224
+ pairs = Array(pairs[idx...])
225
+ } else {
226
+ // keep items with key > seekKey
227
+ let idx = pairs.lastIndex(where: { lte($0.key, sk) }) ?? -1
228
+ let startIdx = idx + 1
229
+ pairs = (startIdx < pairs.count) ? Array(pairs[startIdx...]) : []
230
+ }
231
+ }
232
+
233
+ // 4) limit
234
+ let lim = max(0, Int(limit))
235
+ let chunk = (lim > 0 && lim < pairs.count) ? Array(pairs.prefix(lim)) : pairs
236
+ let hasMore = chunk.count < pairs.count
237
+ let nextSeek = chunk.last?.key ?? Data()
238
+
239
+ // 6) Frame the blob
240
+ var blob = Data(capacity: 1 + 4) // will grow as needed
241
+ var flags: UInt8 = 0
242
+ if hasMore { flags |= 0x01 }
243
+ blob.append(&flags, count: 1)
244
+
245
+ var countBE = UInt32(chunk.count).bigEndian
246
+ withUnsafeBytes(of: &countBE) { blob.append($0.bindMemory(to: UInt8.self)) }
247
+
248
+ for (k, v) in chunk {
249
+ var klen = UInt32(k.count).bigEndian
250
+ var vlen = UInt32(v.count).bigEndian
251
+ withUnsafeBytes(of: &klen) { blob.append($0.bindMemory(to: UInt8.self)) }
252
+ blob.append(k)
253
+ withUnsafeBytes(of: &vlen) { blob.append($0.bindMemory(to: UInt8.self)) }
254
+ blob.append(v)
255
+ }
256
+
257
+ var nlen = UInt32(nextSeek.count).bigEndian
258
+ withUnsafeBytes(of: &nlen) { blob.append($0.bindMemory(to: UInt8.self)) }
259
+ blob.append(nextSeek)
260
+
261
+ return blob
262
+ }
263
+
264
+ // MARK: - Utility Methods
265
+
266
+ private func keyToAccount(_ key: Data) -> String {
267
+ return String(data: key, encoding: .utf8) ?? key.base64EncodedString()
268
+ }
269
+
270
+ private func accountToKey(_ account: String) -> Data? {
271
+ // Try UTF-8 first
272
+ if let utf8Data = account.data(using: .utf8) {
273
+ // Check if this was originally a base64 string by trying to decode it
274
+ if let base64Data = Data(base64Encoded: account), base64Data != utf8Data {
275
+ // This account was base64 encoded, return the decoded data
276
+ return base64Data
277
+ } else {
278
+ // This was a UTF-8 string, return the UTF-8 data
279
+ return utf8Data
280
+ }
281
+ }
282
+
283
+ // Fallback: try base64 decoding
284
+ return Data(base64Encoded: account)
285
+ }
286
+
287
+ // --- byte-wise comparisons on decoded keys ---
288
+ @inline(__always)
289
+ private func lt(_ a: Data, _ b: Data) -> Bool {
290
+ a.lexicographicallyPrecedes(b)
291
+ }
292
+ @inline(__always)
293
+ private func gte(_ a: Data, _ b: Data) -> Bool { !lt(a, b) }
294
+ @inline(__always)
295
+ private func lte(_ a: Data, _ b: Data) -> Bool { !lt(b, a) }
296
+ @inline(__always)
297
+ private func inRange(_ k: Data, start: Data?, end: Data?) -> Bool {
298
+ if let s = start, lt(k, s) { return false } // k >= s
299
+ if let e = end, !lt(k, e) { return false } // k < e
300
+ return true
301
+ }
302
+ }
303
+
304
+ public enum NativeDBError: Error, LocalizedError {
305
+ case invalidArgument(description: String)
306
+ case keychainError(status: OSStatus, message: String)
307
+
308
+ public var errorDescription: String? {
309
+ switch self {
310
+ case .invalidArgument(let description):
311
+ return "Invalid Argument: \(description)"
312
+ case .keychainError(let status, let message):
313
+ let statusMessage = SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error"
314
+ return "Keychain Error: \(message) (status: \(status), reason: \(statusMessage))"
315
+ }
316
+ }
317
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gnolang/gnonative",
3
- "version": "4.7.2",
3
+ "version": "4.7.3",
4
4
  "description": "Develop for Gno using your app's native language ",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",