Repository navigation
feat(sdk)!: optional BIP-39 passphrase through the mnemonic resolver and wallet creation - #5289
Draft
PastaPastaPasta wants to merge 3 commits into
Draft
PastaPastaPasta wants to merge 3 commits into
PastaPastaPasta wants to merge 3 commits into
Conversation
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Closed
5 of 6 tasks
Collaborator
|
🕓 Review not started yet because this PR is a draft.
Commit 043d12d. Normal review starts when eligible; priority review starts as soon as a slot is available. |
… resolver and wallet creation
Every seed derivation used to hardcode the empty passphrase: key-wallet's Mnemonic wallet variant calls to_seed(""), and the Swift-to-Rust mnemonic resolver vtable could only return the mnemonic. A host that wanted a BIP-39 passphrase ("25th word") wallet had no way to create one that would sign after relaunch.
The resolver callback now also returns the wallet's stored passphrase (length 0 = none). A single resolve_seed helper in rs-sdk-ffi is the only consumer of the vtable, so the passphrase cannot be honoured on one derivation path and dropped on another; the core signer, the resolver-driven sign FFI, identity derive-and-persist, identity discovery/preview/loading, and the at-slot preview all route through it. create_wallet_from_mnemonic takes a passphrase and builds a Seed-typed key-wallet (same network-scoped id as the mnemonic variant for the empty passphrase, verified by test). New FFI exports: platform_wallet_manager_create_wallet_from_mnemonic_with_passphrase_and_birth_height and an any-language platform_wallet_mnemonic_to_seed.
Swift SDK: WalletStorage stores the passphrase as its own per-wallet Keychain item next to the mnemonic and folds it into the seed-binding stamp; MnemonicResolver fills the passphrase buffer; PlatformWalletManager.createWallet(mnemonic:seedPassphrase:) on both overloads; deleteWallet removes the passphrase item; Mnemonic.toSeed uses the any-language FFI. The throwing passphrase parameter on WalletManager.addWalletAndSerialize is removed.
BREAKING: MnemonicResolveCallback gains three trailing parameters; PlatformWalletManager::create_wallet_from_mnemonic gains a passphrase argument. The Android JNI trampoline reports no passphrase, so Kotlin behaviour is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
create_wallet_from_mnemonic now calls seed_from_mnemonic instead of repeating it; ResolveSeedError derives thiserror; the resolver sign FFI maps errors to tags in one table; WalletStorage's per-wallet accessors share one set of Keychain primitives; MaskedMnemonicUTF8 is renamed MaskedSecretUTF8 now that it also masks the passphrase; Mnemonic.toSeed reuses withOptionalPassphraseCString. No behaviour change; public Swift surface and FFI symbols are identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…phrase plumbing rust-dashcore e4208c90 (#980/#981) folded every-wordlist detection into `Mnemonic::from_phrase(phrase)` and removed `from_phrase_in_any_language` and the `Language` argument, so the resolver and the id-pinning test no longer compiled after the rebase onto v5.0-dev. The same change makes key-wallet-ffi's `mnemonic_to_seed` language agnostic, which removes the reason for the new `platform_wallet_mnemonic_to_seed` export. Drop it and keep `Mnemonic.toSeed` on key-wallet-ffi as on v5.0-dev (already pinned for non-English phrases and passphrases by NonEnglishMnemonicTests). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PastaPastaPasta
force-pushed
the
feat/seed-passphrase-resolver
branch
from
October 6, 2026 18:15
f64c4e8 to
043d12d
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue being fixed or feature implemented
The iOS Dash Wallet wants to offer an optional BIP-39 passphrase ("25th word") as an advanced, hidden-by-default feature. The SDK could not support it: every seed derivation hardcoded the empty passphrase. key-wallet's
Mnemonicwallet variant callsto_seed("")(rust-dashcore #747 removedMnemonicWithPassphrase), and the Swift→RustMnemonicResolveCallbackcould only hand back the mnemonic, so every resolver consumer inrs-sdk-ffi/rs-platform-wallet-ffiderived withto_seed("")too. A wallet created from a raw seed viacreateWallet(seed:)would have had the right keys but no Keychain-backed resolver path, so it could not sign after a relaunch.This is phase 0 (the SDK half) of the app feature; the app-side work follows in dashwallet-ios.
What was done?
Rust
rs-sdk-ffi/src/mnemonic_resolver.rs:MnemonicResolveCallbackgainsout_passphrase_utf8/out_passphrase_capacity/out_passphrase_len(length0= no passphrase). NewPASSPHRASE_RESOLVER_BUFFER_CAPACITY. Newresolve_seed(handle, wallet_id) -> Zeroizing<[u8; 64]>is now the only consumer of the vtable: result-code mapping, length/UTF-8 checks, any-language parse,to_seed(passphrase), zeroization, in one place.ResolveSeedErrorcarries the failure kind.to_seed("")block routes through it:MnemonicResolverCoreSigner,dash_sdk_sign_with_mnemonic_resolver_and_path,dash_sdk_derive_and_persist_identity_keys,resolve_seed_from_resolver[_classified](which feeds identity discovery / preview / loading / provider keys / shielded / address private key), anddash_sdk_derive_identity_key_at_slot_with_resolver.platform-wallet:PlatformWalletManager::create_wallet_from_mnemonictakespassphrase: &strand builds aSeed-typed key-wallet fromto_seed(passphrase). The network-scoped wallet id is computed from the root xpub for both variants, so existing (empty-passphrase) wallets keep their ids — pinned by a new test. New free fnplatform_wallet::seed_from_mnemonic.platform-wallet-ffi: new exportplatform_wallet_manager_create_wallet_from_mnemonic_with_passphrase_and_birth_height(NULL /""passphrase ≡ existing export). Existing mnemonic create exports are unchanged wrappers. (Seed pre-derivation for the host uses key-wallet-ffi'smnemonic_to_seed, which auto-detects the language since rust-dashcore chore: v0.24-dev backports part 2 #980/fix(rs-drive-abci): Get rs-drive-abci to commit block 1 #981.)rs-unified-sdk-jni: trampoline reports passphrase length 0 (TODO(seed-passphrase)), so Android behaviour is unchanged.Swift SDK
WalletStorage: per-walletwallet.passphrase.<walletIdHex>Keychain item (storePassphrase/retrievePassphrase[UTF8Bytes]/passphraseAvailability/hasPassphrase/deletePassphrase), same accessibility class as the mnemonic. An empty passphrase is rejected; absence of the item is the discriminator.mnemonicKeychainStampfolds the passphrase item in so a changed passphrase invalidates the cached seed-binding marker.MnemonicResolver: fills the passphrase buffer (masked while idle, same as the mnemonic); an unreadable passphrase item fails closed.PlatformWalletManager.createWallet(mnemonic:seedPassphrase:network:…)on the sync and async overloads;deleteWalletdeletes the passphrase item before the mnemonic.passphrase:parameter onWalletManager.addWalletAndSerializeand the stale README examples that documented a passphrase API that did not exist.How Has This Been Tested?
cargo test --libforrs-sdk-ffi(330 pass),platform-wallet-ffi(329 pass),rs-unified-sdk-jni(37 pass); targetedplatform-walletlifecycle tests pass.cargo clippy/cargo fmt --checkclean on the four touched crates.abandon…aboutwith""andTREZOR) throughresolve_seed; a passphrase resolver signs with the passphrase-derived key and the empty-passphrase key is refused asPUBKEY_MISMATCH; mnemonic-create id == seed-create id, passphrase changes the id,""keeps today's id.build_ios.sh --target mac --profile dev), thenswift build --build-testsand the full hermetic suite: 429 tests, 0 failures. NewSeedPassphraseResolverTestscoverstoSeedvectors + Japanese phrase, resolver signing with/without a stored passphrase, passphrase deletion, and the empty-passphrase rejection;PlatformWalletCreateWalletTestsasserts the passphrase reaches the native create verbatim.v5.0-dev(rust-dashcoree4208c90), the Rust crates were re-checked and their unit tests re-run; the Swift numbers above are from before the rebase, and the macOS xcframework / Swift suite has not been rebuilt since.Breaking Changes
MnemonicResolveCallback(C ABI) has three new trailing parameters. Every in-tree implementor is updated (Swift trampoline, JNI trampoline, all test callbacks); out-of-tree hosts must add them and set*out_passphrase_len = 0when they have no passphrase.PlatformWalletManager::create_wallet_from_mnemonic(Rust) takes apassphrase: &strargument.WalletManager.addWalletAndSerializeno longer has apassphrase:parameter (it only ever threw on a non-empty value).Checklist:
For repository code-owners and collaborators only
🤖 Generated with Claude Code