Skip to content

feat(sdk)!: optional BIP-39 passphrase through the mnemonic resolver and wallet creation - #4615

Draft
PastaPastaPasta wants to merge 2 commits into
dashpay:v4.2-devfrom
PastaPastaPasta:feat/seed-passphrase-resolver
Draft

feat(sdk)!: optional BIP-39 passphrase through the mnemonic resolver and wallet creation#4615
PastaPastaPasta wants to merge 2 commits into
dashpay:v4.2-devfrom
PastaPastaPasta:feat/seed-passphrase-resolver

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

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 Mnemonic wallet variant calls to_seed("") (rust-dashcore #747 removed MnemonicWithPassphrase), and the Swift→Rust MnemonicResolveCallback could only hand back the mnemonic, so every resolver consumer in rs-sdk-ffi / rs-platform-wallet-ffi derived with to_seed("") too. A wallet created from a raw seed via createWallet(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: MnemonicResolveCallback gains out_passphrase_utf8 / out_passphrase_capacity / out_passphrase_len (length 0 = no passphrase). New PASSPHRASE_RESOLVER_BUFFER_CAPACITY. New resolve_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. ResolveSeedError carries the failure kind.
  • Every previous hand-rolled resolve→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), and dash_sdk_derive_identity_key_at_slot_with_resolver.
  • platform-wallet: PlatformWalletManager::create_wallet_from_mnemonic takes passphrase: &str and builds a Seed-typed key-wallet from to_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 fn platform_wallet::seed_from_mnemonic.
  • platform-wallet-ffi: new exports platform_wallet_manager_create_wallet_from_mnemonic_with_passphrase_and_birth_height (NULL / "" passphrase ≡ existing export) and platform_wallet_mnemonic_to_seed (any-language; key-wallet-ffi's mnemonic_to_seed is English-only). Existing mnemonic create exports are unchanged wrappers.
  • rs-unified-sdk-jni: trampoline reports passphrase length 0 (TODO(seed-passphrase)), so Android behaviour is unchanged.

Swift SDK

  • WalletStorage: per-wallet wallet.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. mnemonicKeychainStamp folds 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; deleteWallet deletes the passphrase item before the mnemonic.
  • Mnemonic.toSeed(mnemonic:passphrase:) now calls the any-language FFI.
  • Removed the throwing passphrase: parameter on WalletManager.addWalletAndSerialize and the stale README examples that documented a passphrase API that did not exist.

How Has This Been Tested?

  • cargo test --lib for rs-sdk-ffi (330 pass), platform-wallet-ffi (329 pass), rs-unified-sdk-jni (37 pass); targeted platform-wallet lifecycle tests pass. cargo clippy / cargo fmt --check clean on the four touched crates.
  • New Rust tests: BIP-39 reference vectors (abandon…about with "" and TREZOR) through resolve_seed and platform_wallet_mnemonic_to_seed; a passphrase resolver signs with the passphrase-derived key and the empty-passphrase key is refused as PUBKEY_MISMATCH; mnemonic-create id == seed-create id, passphrase changes the id, "" keeps today's id.
  • Swift: built a macOS xcframework slice from this branch (build_ios.sh --target mac --profile dev), then swift build --build-tests and the full hermetic suite: 429 tests, 0 failures. New SeedPassphraseResolverTests covers toSeed vectors + Japanese phrase, resolver signing with/without a stored passphrase, passphrase deletion, and the empty-passphrase rejection; PlatformWalletCreateWalletTests asserts the passphrase reaches the native create verbatim.
  • Not yet done: an on-device iOS smoke (create passphrase wallet → relaunch → spend). That happens with the app-side PR.

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 = 0 when they have no passphrase.
  • PlatformWalletManager::create_wallet_from_mnemonic (Rust) takes a passphrase: &str argument.
  • Swift WalletManager.addWalletAndSerialize no longer has a passphrase: parameter (it only ever threw on a non-empty value).

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

… 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>
@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 48 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 816756fa-83fb-4b12-aebc-1696bab8bfa1

📥 Commits

Reviewing files that changed from the base of the PR and between ca1612e and 1dedb36.

📒 Files selected for processing (26)
  • packages/rs-platform-wallet-ffi/src/core_wallet/sign_message.rs
  • packages/rs-platform-wallet-ffi/src/derive_identity_key_at_slot.rs
  • packages/rs-platform-wallet-ffi/src/identity_derive_and_persist.rs
  • packages/rs-platform-wallet-ffi/src/identity_keys_from_mnemonic.rs
  • packages/rs-platform-wallet-ffi/src/manager.rs
  • packages/rs-platform-wallet-ffi/src/masternode_withdrawal.rs
  • packages/rs-platform-wallet-ffi/src/sign_with_mnemonic_resolver.rs
  • packages/rs-platform-wallet/examples/dpns_marketplace_testnet.rs
  • packages/rs-platform-wallet/src/lib.rs
  • packages/rs-platform-wallet/src/manager/mod.rs
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/discovery.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/loading.rs
  • packages/rs-sdk-ffi/src/mnemonic_resolver.rs
  • packages/rs-sdk-ffi/src/mnemonic_resolver_core_signer.rs
  • packages/rs-unified-sdk-jni/src/mnemonic.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/WalletStorage.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/FFI/MnemonicResolverAndPersister.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Mnemonic.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/README.md
  • packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/WalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/README.md
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/ContentView.swift
  • packages/swift-sdk/SwiftTests/SwiftDashSDKTests/PlatformWalletCreateWalletTests.swift
  • packages/swift-sdk/SwiftTests/SwiftDashSDKTests/SeedPassphraseResolverTests.swift

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 7, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant