mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-10-20 18:56:25 +00:00
Fix crash reading MetaAccountSecrets for accounts predating Tron/Bitcoin/Solana
Found on a real device: opening the backup/export screen for an existing wallet crashed with java.lang.IndexOutOfBoundsException: Cannot read 412 of 412 at .../scale/dataType/optional.read at .../scale/Schema.read (x3) at SecretStoreV2$getMetaAccountSecrets$2.invokeSuspend Root cause: MetaAccountSecrets is a positional (SCALE) binary schema. optional<T>.read() unconditionally reads a presence-flag byte with no EOF handling - reading a blob that predates a trailing field (written before that field existed) throws instead of yielding null. This is also why Solana's own address was never getting backfilled: the migration's own read of the account's stored secrets was throwing on every attempted run, silently caught by its own per-account try/catch. Confirmed via substrate-sdk-android's actual source (Dsl.kt/Delegate.kt) that Tron/Bitcoin's identical schema-growth pattern carried the exact same latent risk - it likely only avoided crashing so far by chance byte-alignment in previously-stored blobs, not because the format tolerates it. Fix: read Tron/Bitcoin/Solana's keypair/derivationPath fields (every field added after the original Substrate+Ethereum schema) through a SafeOptional DataType that catches a failed read and returns null, via the library's own custom() field-declaration escape hatch. Write behavior is byte-for-byte unchanged - only reading a shorter/older blob now correctly yields null for fields it doesn't have, instead of crashing every caller of getMetaAccountSecrets (balance sync, signing, backup/export, this Solana backfill migration itself).
This commit is contained in:
1 parent
db9962a00e
commit
9a11dc2c3d
1 file changed
+44
-6
+44
-6
@@ -1,11 +1,18 @@
|
|||||||
package io.novafoundation.nova.common.data.secrets.v2
|
package io.novafoundation.nova.common.data.secrets.v2
|
||||||
|
|
||||||
|
import io.emeraldpay.polkaj.scale.ScaleCodecReader
|
||||||
|
import io.emeraldpay.polkaj.scale.ScaleCodecWriter
|
||||||
import io.novafoundation.nova.common.utils.invoke
|
import io.novafoundation.nova.common.utils.invoke
|
||||||
import io.novasama.substrate_sdk_android.encrypt.keypair.Keypair
|
import io.novasama.substrate_sdk_android.encrypt.keypair.Keypair
|
||||||
import io.novasama.substrate_sdk_android.encrypt.keypair.substrate.Sr25519Keypair
|
import io.novasama.substrate_sdk_android.encrypt.keypair.substrate.Sr25519Keypair
|
||||||
import io.novasama.substrate_sdk_android.scale.EncodableStruct
|
import io.novasama.substrate_sdk_android.scale.EncodableStruct
|
||||||
import io.novasama.substrate_sdk_android.scale.Schema
|
import io.novasama.substrate_sdk_android.scale.Schema
|
||||||
import io.novasama.substrate_sdk_android.scale.byteArray
|
import io.novasama.substrate_sdk_android.scale.byteArray
|
||||||
|
import io.novasama.substrate_sdk_android.scale.custom
|
||||||
|
import io.novasama.substrate_sdk_android.scale.dataType.DataType
|
||||||
|
import io.novasama.substrate_sdk_android.scale.dataType.optional
|
||||||
|
import io.novasama.substrate_sdk_android.scale.dataType.scalable
|
||||||
|
import io.novasama.substrate_sdk_android.scale.dataType.string as stringDataType
|
||||||
import io.novasama.substrate_sdk_android.scale.schema
|
import io.novasama.substrate_sdk_android.scale.schema
|
||||||
import io.novasama.substrate_sdk_android.scale.string
|
import io.novasama.substrate_sdk_android.scale.string
|
||||||
|
|
||||||
@@ -16,6 +23,33 @@ object KeyPairSchema : Schema<KeyPairSchema>() {
|
|||||||
val Nonce by byteArray().optional()
|
val Nonce by byteArray().optional()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [io.novasama.substrate_sdk_android.scale.dataType.optional]'ın `read()`'i, arabellek (stored blob) bu alan
|
||||||
|
* eklenmeden ÖNCE yazılmış eski verilerde tükendiğinde - yani alan gerçekten "yok" demek istediğinde - zarifçe
|
||||||
|
* null dönmek yerine çöküyor (`reader.readBoolean()` EOF'ta istisna fırlatıyor, yakalama yok). Bu, Tron/Bitcoin
|
||||||
|
* eklenirken de aynı riskti; muhtemelen şans eseri (byte hizalaması tesadüfen tutmuş) hiç yüzeye çıkmamıştı -
|
||||||
|
* Solana'nın 2 yeni alanı arabelleği gerçekten aştırdı ve gerçek cihazda "Cannot read 412 of 412" ile çöktü
|
||||||
|
* (secrets okunan HER yerde: bakiye senkronu, backup ekranı, vs).
|
||||||
|
*
|
||||||
|
* Yazma tarafı [optional]'ın kendisiyle byte-byte aynı kalır - sadece okuma, bu alanın zaten taşıması gereken
|
||||||
|
* anlamı (eskiden hiç yazılmamışsa = yok = null) gerçekten sağlar, kütüphanenin eksik bıraktığı yerde.
|
||||||
|
*/
|
||||||
|
private class SafeOptional<T>(private val dataType: DataType<T>) : DataType<T?>() {
|
||||||
|
override fun read(reader: ScaleCodecReader): T? {
|
||||||
|
return try {
|
||||||
|
optional(dataType).read(reader)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(writer: ScaleCodecWriter, value: T?) {
|
||||||
|
optional(dataType).write(writer, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun conformsType(value: Any?) = value == null || dataType.conformsType(value)
|
||||||
|
}
|
||||||
|
|
||||||
object MetaAccountSecrets : Schema<MetaAccountSecrets>() {
|
object MetaAccountSecrets : Schema<MetaAccountSecrets>() {
|
||||||
val Entropy by byteArray().optional()
|
val Entropy by byteArray().optional()
|
||||||
val SubstrateSeed by byteArray().optional()
|
val SubstrateSeed by byteArray().optional()
|
||||||
@@ -26,14 +60,18 @@ object MetaAccountSecrets : Schema<MetaAccountSecrets>() {
|
|||||||
val EthereumKeypair by schema(KeyPairSchema).optional()
|
val EthereumKeypair by schema(KeyPairSchema).optional()
|
||||||
val EthereumDerivationPath by string().optional()
|
val EthereumDerivationPath by string().optional()
|
||||||
|
|
||||||
val TronKeypair by schema(KeyPairSchema).optional()
|
// Tron/Bitcoin/Solana are every chain-family added after the original Substrate+Ethereum schema - each is a
|
||||||
val TronDerivationPath by string().optional()
|
// trailing addition an old, pre-existing stored blob may simply not have any bytes for at all. custom()
|
||||||
|
// + SafeOptional (not the bare `.optional()` used above) makes reading such a blob correctly yield null for
|
||||||
|
// all of them, instead of crashing on the first one whose bytes don't exist - see SafeOptional's doc.
|
||||||
|
val TronKeypair by custom(SafeOptional(scalable(KeyPairSchema)))
|
||||||
|
val TronDerivationPath by custom(SafeOptional(stringDataType))
|
||||||
|
|
||||||
val BitcoinKeypair by schema(KeyPairSchema).optional()
|
val BitcoinKeypair by custom(SafeOptional(scalable(KeyPairSchema)))
|
||||||
val BitcoinDerivationPath by string().optional()
|
val BitcoinDerivationPath by custom(SafeOptional(stringDataType))
|
||||||
|
|
||||||
val SolanaKeypair by schema(KeyPairSchema).optional()
|
val SolanaKeypair by custom(SafeOptional(scalable(KeyPairSchema)))
|
||||||
val SolanaDerivationPath by string().optional()
|
val SolanaDerivationPath by custom(SafeOptional(stringDataType))
|
||||||
}
|
}
|
||||||
|
|
||||||
object ChainAccountSecrets : Schema<ChainAccountSecrets>() {
|
object ChainAccountSecrets : Schema<ChainAccountSecrets>() {
|
||||||
|
|||||||
Reference in new issue
Block a user