@chainberry/trust-wallet-core 2.5.0 → 2.5.1
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/ChainberryTrustWalletCoreModule.podspec +3 -2
- package/README.md +14 -5
- package/android/src/main/java/com/chainberry/trustwalletcore/ChainSigning.kt +368 -10
- package/android/src/main/java/com/chainberry/trustwalletcore/ChainberryTrustWalletCoreModule.kt +86 -6
- package/android/src/main/java/com/chainberry/trustwalletcore/NativeWalletStore.kt +92 -0
- package/android/src/test/java/com/chainberry/trustwalletcore/NativeWalletStoreTest.kt +70 -0
- package/ios/ChainSigning.swift +409 -33
- package/ios/ChainberryTrustWalletCoreModule.swift +115 -10
- package/ios/ConformanceTests/AddressDerivationConformanceTests.swift +39 -0
- package/ios/ConformanceTests/SigningConformanceTests.swift +295 -0
- package/ios/NativeWalletStore.swift +56 -0
- package/package.json +1 -1
- package/src/index.ts +18 -4
|
@@ -21,6 +21,7 @@ import org.json.JSONException
|
|
|
21
21
|
import org.json.JSONObject
|
|
22
22
|
import java.io.File
|
|
23
23
|
import java.security.KeyStore
|
|
24
|
+
import java.util.Collections
|
|
24
25
|
import java.util.UUID
|
|
25
26
|
import javax.crypto.Cipher
|
|
26
27
|
import javax.crypto.KeyGenerator
|
|
@@ -236,6 +237,22 @@ object NativeWalletStore {
|
|
|
236
237
|
* metadata field needed. */
|
|
237
238
|
internal fun keyAlias(mode: AuthMode, walletId: String): String = KEY_ALIAS_PREFIX + mode.aliasInfix + walletId
|
|
238
239
|
|
|
240
|
+
/** Inverse of [keyAlias]: recovers `(mode, walletId)` from a raw Keystore alias string, or
|
|
241
|
+
* `null` if it doesn't match this module's alias scheme at all (some other feature's Keystore
|
|
242
|
+
* entry, sharing the same `AndroidKeyStore` provider). Checks the longer/specific infixes
|
|
243
|
+
* (`bio_`, `cred_`) before the empty [AuthMode.LEGACY_COMBINED] infix, which would otherwise
|
|
244
|
+
* match every alias under [KEY_ALIAS_PREFIX] — relies on [AuthMode.entries] iterating in
|
|
245
|
+
* declaration order, same assumption [resolveExistingMode] makes. Used by [reconcileOrphans]
|
|
246
|
+
* to identify which Keystore aliases belong to which wallet id, without needing a Context. */
|
|
247
|
+
internal fun parseKeyAlias(alias: String): Pair<AuthMode, String>? {
|
|
248
|
+
if (!alias.startsWith(KEY_ALIAS_PREFIX)) return null
|
|
249
|
+
for (mode in AuthMode.entries) {
|
|
250
|
+
val prefix = KEY_ALIAS_PREFIX + mode.aliasInfix
|
|
251
|
+
if (alias.startsWith(prefix)) return mode to alias.removePrefix(prefix)
|
|
252
|
+
}
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
|
|
239
256
|
private fun getOrCreateKey(walletId: String, mode: AuthMode): SecretKey {
|
|
240
257
|
val alias = keyAlias(mode, walletId)
|
|
241
258
|
val ks = keyStore()
|
|
@@ -581,6 +598,81 @@ object NativeWalletStore {
|
|
|
581
598
|
return loadMetadataFromFile(metadataFile(context))
|
|
582
599
|
}
|
|
583
600
|
|
|
601
|
+
// MARK: - Reconciliation (see CONTEXT.md "orphan"/"reconciliation pass", docs/adr/0001)
|
|
602
|
+
|
|
603
|
+
/** Every `.enc` file in [walletsDir] whose wallet id has no entry in [liveIds] — the
|
|
604
|
+
* file-side half of an orphan. Pure/`File`-based so it's unit-testable on the plain JVM.
|
|
605
|
+
* Deliberately does not also match `METADATA_FILE` itself: that file has no `.enc` suffix. */
|
|
606
|
+
internal fun findOrphanFiles(walletsDir: File, liveIds: Set<String>): List<File> =
|
|
607
|
+
walletsDir.listFiles { f -> f.name.endsWith(".enc") }
|
|
608
|
+
?.filter { it.name.removeSuffix(".enc") !in liveIds }
|
|
609
|
+
?: emptyList()
|
|
610
|
+
|
|
611
|
+
/** Every leftover `metadata.json.tmp-*` file in [walletsDir] — pure litter from a
|
|
612
|
+
* [saveMetadataToFile] interrupted between writing the temp file and renaming it over the
|
|
613
|
+
* target (the rename itself is atomic, so the target is never at risk; only the temp file
|
|
614
|
+
* can be left behind). Not a security or correctness concern, just disk hygiene swept up
|
|
615
|
+
* alongside the real orphan checks since reconciliation is already scanning this directory. */
|
|
616
|
+
internal fun findStaleMetadataTempFiles(walletsDir: File): List<File> =
|
|
617
|
+
walletsDir.listFiles { f -> f.name.startsWith("$METADATA_FILE.tmp-") }?.toList() ?: emptyList()
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Runs once at module init (see `TrustWalletCoreModule`'s `OnCreate`), before the JS layer can
|
|
621
|
+
* issue its first `createWallet`/`importWallet`/`deleteWallet` call — the actual source of
|
|
622
|
+
* crash-safety for an interrupted create or delete, not the in-call rollback in
|
|
623
|
+
* `persistNewWallet`. Android has *two* independently-persistable secret-side resources per
|
|
624
|
+
* wallet — the `.enc` file and its Keystore key alias, since [getOrCreateKey] creates the key
|
|
625
|
+
* before [saveMnemonic] ever writes the file — so both are checked against the metadata store
|
|
626
|
+
* independently, neither gated on the other's presence.
|
|
627
|
+
*
|
|
628
|
+
* Best-effort and never throws: any failure here is logged and skipped rather than propagated,
|
|
629
|
+
* since a broken reconciliation pass must never become "the app won't launch." A metadata entry
|
|
630
|
+
* with no matching secret-side resource (the reverse shape — a "zombie") is deliberately left
|
|
631
|
+
* untouched here; see [NativeWalletStoreError.NotFound] and docs/adr/0001 for why.
|
|
632
|
+
*/
|
|
633
|
+
fun reconcileOrphans(context: Context) {
|
|
634
|
+
val liveIds = try {
|
|
635
|
+
loadMetadata(context).keys
|
|
636
|
+
} catch (e: Exception) {
|
|
637
|
+
Log.w(TAG, "reconciliation: failed to load metadata, skipping this pass entirely", e)
|
|
638
|
+
return
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
val dir = walletsDir(context)
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
for (file in findOrphanFiles(dir, liveIds)) {
|
|
645
|
+
if (!file.delete()) {
|
|
646
|
+
Log.w(TAG, "reconciliation: failed to delete orphaned file ${file.name}")
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} catch (e: Exception) {
|
|
650
|
+
Log.w(TAG, "reconciliation: failed while cleaning up orphaned files", e)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
try {
|
|
654
|
+
val ks = keyStore()
|
|
655
|
+
for (alias in Collections.list(ks.aliases())) {
|
|
656
|
+
val (_, walletId) = parseKeyAlias(alias) ?: continue
|
|
657
|
+
if (walletId !in liveIds) {
|
|
658
|
+
try {
|
|
659
|
+
ks.deleteEntry(alias)
|
|
660
|
+
} catch (e: Exception) {
|
|
661
|
+
Log.w(TAG, "reconciliation: failed to delete orphaned key alias $alias", e)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
} catch (e: Exception) {
|
|
666
|
+
Log.w(TAG, "reconciliation: failed while enumerating Keystore aliases", e)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
findStaleMetadataTempFiles(dir).forEach { it.delete() }
|
|
671
|
+
} catch (e: Exception) {
|
|
672
|
+
Log.w(TAG, "reconciliation: failed while cleaning up stale metadata temp files", e)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
584
676
|
// MARK: - Biometric/device-credential prompt
|
|
585
677
|
|
|
586
678
|
/** Authenticates and returns a `Cipher` ready for [saveMnemonic], for a brand-new wallet id.
|
|
@@ -271,4 +271,74 @@ class NativeWalletStoreTest {
|
|
|
271
271
|
fun describeLegacySecurityLevel_notInsideSecureHardwareIsSoftware() {
|
|
272
272
|
assertEquals("SOFTWARE", NativeWalletStore.describeLegacySecurityLevel(false))
|
|
273
273
|
}
|
|
274
|
+
|
|
275
|
+
// ─── parseKeyAlias ───────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
@Test
|
|
278
|
+
fun parseKeyAlias_isTheInverseOfKeyAlias() {
|
|
279
|
+
val id = UUID.randomUUID().toString()
|
|
280
|
+
for (mode in AuthMode.entries) {
|
|
281
|
+
val alias = NativeWalletStore.keyAlias(mode, id)
|
|
282
|
+
assertEquals(mode to id, NativeWalletStore.parseKeyAlias(alias))
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@Test
|
|
287
|
+
fun parseKeyAlias_legacyInfixDoesNotSwallowBioOrCredAliases() {
|
|
288
|
+
// Regression check: LEGACY_COMBINED's infix is "", which is a prefix of every alias under
|
|
289
|
+
// KEY_ALIAS_PREFIX — parseKeyAlias must still resolve a bio_/cred_ alias to its actual mode,
|
|
290
|
+
// not fall through to LEGACY_COMBINED just because the empty-infix check would also match.
|
|
291
|
+
val id = UUID.randomUUID().toString()
|
|
292
|
+
assertEquals(
|
|
293
|
+
AuthMode.BIOMETRIC_STRONG to id,
|
|
294
|
+
NativeWalletStore.parseKeyAlias(NativeWalletStore.keyAlias(AuthMode.BIOMETRIC_STRONG, id)),
|
|
295
|
+
)
|
|
296
|
+
assertEquals(
|
|
297
|
+
AuthMode.DEVICE_CREDENTIAL to id,
|
|
298
|
+
NativeWalletStore.parseKeyAlias(NativeWalletStore.keyAlias(AuthMode.DEVICE_CREDENTIAL, id)),
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@Test
|
|
303
|
+
fun parseKeyAlias_unrelatedAliasReturnsNull() {
|
|
304
|
+
assertEquals(null, NativeWalletStore.parseKeyAlias("some_other_features_key"))
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ─── findOrphanFiles / findStaleMetadataTempFiles (reconciliation, docs/adr/0001) ────────────
|
|
308
|
+
|
|
309
|
+
@Test
|
|
310
|
+
fun findOrphanFiles_fileWithNoMetadataEntryIsOrphaned() {
|
|
311
|
+
File(tmp.root, "orphan-id.enc").writeBytes(byteArrayOf(1))
|
|
312
|
+
val orphans = NativeWalletStore.findOrphanFiles(tmp.root, liveIds = emptySet())
|
|
313
|
+
assertEquals(listOf("orphan-id.enc"), orphans.map { it.name })
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@Test
|
|
317
|
+
fun findOrphanFiles_fileWithMatchingMetadataEntryIsNotOrphaned() {
|
|
318
|
+
File(tmp.root, "live-id.enc").writeBytes(byteArrayOf(1))
|
|
319
|
+
val orphans = NativeWalletStore.findOrphanFiles(tmp.root, liveIds = setOf("live-id"))
|
|
320
|
+
assertTrue(orphans.isEmpty())
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@Test
|
|
324
|
+
fun findOrphanFiles_ignoresNonEncFiles() {
|
|
325
|
+
// metadata.json itself (and any stray temp file) must never be swept up as a wallet orphan.
|
|
326
|
+
File(tmp.root, "metadata.json").writeText("{}")
|
|
327
|
+
val orphans = NativeWalletStore.findOrphanFiles(tmp.root, liveIds = emptySet())
|
|
328
|
+
assertTrue(orphans.isEmpty())
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@Test
|
|
332
|
+
fun findStaleMetadataTempFiles_findsLeftoverTempFile() {
|
|
333
|
+
// Simulates a saveMetadataToFile interrupted after the temp write but before the rename.
|
|
334
|
+
File(tmp.root, "metadata.json.tmp-12345").writeText("{}")
|
|
335
|
+
val stale = NativeWalletStore.findStaleMetadataTempFiles(tmp.root)
|
|
336
|
+
assertEquals(listOf("metadata.json.tmp-12345"), stale.map { it.name })
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@Test
|
|
340
|
+
fun findStaleMetadataTempFiles_ignoresCommittedMetadataFile() {
|
|
341
|
+
File(tmp.root, "metadata.json").writeText("{}")
|
|
342
|
+
assertTrue(NativeWalletStore.findStaleMetadataTempFiles(tmp.root).isEmpty())
|
|
343
|
+
}
|
|
274
344
|
}
|