Summary
Add optional BIP39 passphrase support to seed derivation across xchainjs keystore clients so consumers (notably asgardex-desktop) can offer a “25th word” for single-sig wallets.
Same recovery words + different passphrase → different keys/addresses. Empty passphrase must preserve today’s addresses (non-breaking).
Why
- High-value software single-sig hardening without hardware
- Duress / plausible deniability (decoy wallet vs passphrase wallet)
- Passphrase is not part of the written mnemonic backup
- App cannot do this alone: every client currently derives via
getSeed(phrase) / fromMnemonic(phrase) with an implicit empty passphrase
Design (lib)
@xchainjs/xchain-crypto: getSeed(phrase, bip39Passphrase = '') → mnemonicToSeedSync(phrase, bip39Passphrase)
@xchainjs/xchain-client: optional bip39Passphrase on XChainClientParams; store on BaseXChainClient; optional 3rd arg on setPhrase; clear on purgeClient
- Keystore clients: thread
this.bip39Passphrase into all seed paths:
- UTXO: btc, ltc, doge, bch, dash, zcash →
getSeed(phrase, this.bip39Passphrase)
- Cosmos family: thor, maya, cosmos, kuji →
getSeed + cosmjs bip39Password
- EVM:
Mnemonic.fromPhrase(phrase, bip39Passphrase) in KeystoreSigner
- solana, sui, radix, tron
- cardano:
from_bip39_entropy(entropy, passphraseBytes) (2nd arg is CIP-3 password; currently '')
- ripple:
fromMnemonic ignores passphrase in xrpl lib → use getSeed + fromSeed
- Skip
xchain-monero (WIP / out of scope per monorepo rules) unless explicitly requested
- Changesets: minor (optional params only)
Non-goals (this issue)
- Storing passphrase inside keystore JSON
- Asgardex UI (follow-up app PR after publish)
- Ledger / Vultisig passphrase models
- Defaulting all users onto a passphrase
Security notes
- Empty string === current BIP39 behavior
- Passphrase should never be persisted by clients (app holds it only while unlocked)
- Wrong passphrase cannot be cryptographically detected at unlock (valid alternate wallet)
Full plan
Living doc in this repo:
docs/BIP39_PASSPHRASE_PLAN.md
(If the file is only on a branch until merged, see that branch’s docs/BIP39_PASSPHRASE_PLAN.md.)
Includes: Asgardex Phase 2 UX, storage flag usesBip39Passphrase, QA matrix, risks.
Acceptance criteria
Follow-up
- asgardex-desktop: create/import/unlock UX, in-memory passphrase,
usesBip39Passphrase wallet metadata, version bumps
Summary
Add optional BIP39 passphrase support to seed derivation across xchainjs keystore clients so consumers (notably asgardex-desktop) can offer a “25th word” for single-sig wallets.
Same recovery words + different passphrase → different keys/addresses. Empty passphrase must preserve today’s addresses (non-breaking).
Why
getSeed(phrase)/fromMnemonic(phrase)with an implicit empty passphraseDesign (lib)
@xchainjs/xchain-crypto:getSeed(phrase, bip39Passphrase = '')→mnemonicToSeedSync(phrase, bip39Passphrase)@xchainjs/xchain-client: optionalbip39PassphraseonXChainClientParams; store onBaseXChainClient; optional 3rd arg onsetPhrase; clear onpurgeClientthis.bip39Passphraseinto all seed paths:getSeed(phrase, this.bip39Passphrase)getSeed+ cosmjsbip39PasswordMnemonic.fromPhrase(phrase, bip39Passphrase)inKeystoreSignerfrom_bip39_entropy(entropy, passphraseBytes)(2nd arg is CIP-3 password; currently'')fromMnemonicignores passphrase in xrpl lib → usegetSeed+fromSeedxchain-monero(WIP / out of scope per monorepo rules) unless explicitly requestedNon-goals (this issue)
Security notes
Full plan
Living doc in this repo:
docs/BIP39_PASSPHRASE_PLAN.md(If the file is only on a branch until merged, see that branch’s
docs/BIP39_PASSPHRASE_PLAN.md.)Includes: Asgardex Phase 2 UX, storage flag
usesBip39Passphrase, QA matrix, risks.Acceptance criteria
getSeed(phrase)unchanged vs today when passphrase omitted/emptygetSeed(phrase, p)differs by passphrase; covered by unit testFollow-up
usesBip39Passphrasewallet metadata, version bumps