Fix hardware derivation path with accounts bigger than zero (#3203)
What changed, and why it matters
This commit fixes a bug where Bitcoin wallets created from a hardware wallet seed always used account 0, even when the user had selected a different account (for example, account 1 or 2). The change reads the correct account number from the derivation path and uses it when deriving keys. This is a correctness/functional bug rather than a direct remote-exploitable vulnerability, but it could cause users to manage the wrong wallet account or expose unexpected addresses.
Review and merge the fix; verify that _parseAccountIndex handles all supported derivation path formats and hardened/non-hardened variants. Add tests for account indices greater than 0 and for malformed paths. Consider whether users who created wallets under the buggy behavior need migration or warning.
Security signals we found
Incorrect key derivation path (account index hard-coded to 0)
Potential mismatch between intended account and derived keys
Fix is localized and adds input parsing for derivation path
Evidence from the diff
In cw_bitcoin/lib/electrum_wallet.dart, two code paths previously hard-coded the BIP-44/49/84 account index as 0 in derivation paths (m/
Changed components
cw_bitcoin/lib/electrum_wallet.dartBitcoin wallet key derivationHardware-wallet-derived seed accounts with account index > 0Inspect captured patch +13 / −2
diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart
index b7983fd4..243a88b2 100644
--- a/cw_bitcoin/lib/electrum_wallet.dart
+++ b/cw_bitcoin/lib/electrum_wallet.dart
@@ -132,10 +132,11 @@ abstract class ElectrumWalletBase
}
} else if (canDeriveFromSeed) {
final coinType = _coinTypeFor(currency);
+ final accountIndex = _parseAccountIndex(derivationInfo.derivationPath);
for (final type in supportedTypes) {
final purpose = _purposeForType(type);
- final accountPath = "m/$purpose'/$coinType'/0'";
+ final accountPath = "m/$purpose'/$coinType'/$accountIndex'";
mainHdByType[type] = _masterHD!.derivePath("$accountPath/0") as Bip32Slip10Secp256k1;
sideHdByType[type] = _masterHD!.derivePath("$accountPath/1") as Bip32Slip10Secp256k1;
@@ -197,7 +198,8 @@ abstract class ElectrumWalletBase
final coinType = _coinTypeFor(currency);
final purpose = _purposeForType(record.type);
- return "m/$purpose'/$coinType'/0'";
+ final accountIndex = _parseAccountIndex(derivationInfo.derivationPath);
+ return "m/$purpose'/$coinType'/$accountIndex'";
}
List<BitcoinAddressType> supportedAddressTypes(WalletType type) {
@@ -259,6 +261,15 @@ abstract class ElectrumWalletBase
static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
inputsCount * 68 + outputsCounts * 34 + 10;
+ // Parses the account index from a BIP-44/49/84/86 derivation path.
+ // e.g. "m/84'/0'/1'" → 1. Returns 0 for unrecognised formats.
+ static int _parseAccountIndex(String? derivationPath) {
+ if (derivationPath == null) return 0;
+ final parts = derivationPath.split('/');
+ if (parts.length < 4) return 0;
+ return int.tryParse(parts[3].replaceAll("'", "")) ?? 0;
+ }
+
static Bip32KeyNetVersions? getKeyNetVersion(BasedUtxoNetwork network,
[HardwareWalletType? hardwareWalletType]) {
switch (network) {
Why this scored 42/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.