IBEx.Fi deterministically derives wallet keys from a single master secret tied to the user's passkey. One passkey β one master β addresses on every supported blockchain. The server never stores private keys in plaintext.
A Key Derivation Function (KDF) takes a high-entropy secret (the master) and produces one or more cryptographic keys. IBEx uses HKDF-SHA256 (RFC 5869):
derivedKey = HKDF(SHA-256, masterBytes, salt, info, length)
β β β
"ibexfi:derivation:v1"β β βββ 32 bytes
ecosystem label ββ
The info string uniquely identifies the target ecosystem, ensuring each chain
gets a different key from the same master β without the master being recoverable from
any derived key.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Passkey (FIDO2 authenticator) β
β β
β Option 1: PRF extension (preferred) β
β salt = SHA-256("ibexfi:prf:v1:master|rpId:<rpId>") β
β β authenticator returns prf.first bytes (32 bytes) β
β β
β Option 2: Sealed master fallback β
β β backend generates 32 random bytes β
β β encrypted with AES-256-GCM, stored in DB β
β β decrypted at runtime when derivations needed β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
masterBytes (32 bytes)
β
βββββββββββββββββΌββββββββββββββββ
βΌ βΌ βΌ
βββββββββββββββ βββββββββββββ ββββββββββββββββ
β EVM EOA β β Solana β β Bitcoin β
β secp256k1 β β ed25519 β β secp256k1 β
βββββββββββββββ βββββββββββββ ββββββββββββββββ
β β β
βΌ βΌ βΌ
And also: Cosmos, Polkadot, Tezos, NEAR, Stellar, Cardano
The WebAuthn PRF extension lets the authenticator evaluate a pseudorandom function over a server-provided salt. The result is deterministic for the same passkey + salt, but unpredictable to anyone without the authenticator.
// PRF salt (tenant-scoped, deterministic)
salt = SHA-256("ibexfi:prf:v1:master|rpId:" + effectiveRpId)
// Browser sends to authenticator β returns prf.first (32 bytes)
With PRF, no secret material is stored server-side β the master only exists transiently in memory during the authentication response.
When the authenticator doesn't support PRF, the backend generates 32 random bytes and stores them encrypted in the database:
// Encryption key
key = SHA-256(DERIVATION_SECRET + "|" + DERIVATION_SALT) // 32 bytes
// Seal
AES-256-GCM(key, nonce=12 bytes, plaintext=masterBytes)
β stored as { ct, iv, tag } in Signer.data.derivation.sealedMaster
Decryption requires both the database (ciphertext) and the server secrets
(DERIVATION_SECRET / DERIVATION_SALT).
The choice between PRF and sealed master is made automatically at each authentication ceremony. Here is the concrete flow:
clientExtensionResults.prf.results.first?
masterBytes (32 bytes)sealMaster() (AES-256-GCM),
stores { ct, iv, tag } in Signer.data.derivation.sealedMastermasterBytes, all chain addresses are derived via HKDFSigner.data.derivationprf.results.first:
masterBytes, source = PRFsealedMaster in DB:
unsealMaster() to decrypt, source =
SEALED
FRESH
Signer.data.derivation in DB
Note: PRF-capable devices produce the same masterBytes every time
for the same passkey + salt. The master only exists transiently in memory β
it is never stored. Non-PRF devices rely on the sealed envelope, which requires both
DB access and server secrets to unlock.
All derivations use HKDF(SHA-256, masterBytes, salt, info, 32) with
salt = "ibexfi:derivation:v1":
| Ecosystem | Info Label | Curve / Algo | Address Format |
|---|---|---|---|
| EVM (EOA) | global:single_eoa |
secp256k1 | Keccak-256, last 20 bytes, checksum |
| Solana | solana:global |
Ed25519 | base58-encoded public key |
| Bitcoin P2WPKH | bitcoin:global |
secp256k1 | bech32 (witness v0, bc1q...) |
| Bitcoin Taproot | bitcoin:taproot |
secp256k1 + TapTweak | bech32m (witness v1, bc1p...) |
| Cosmos | cosmos:<hrp> |
secp256k1 | bech32 (cosmos1..., osmo1...) |
| Polkadot | polkadot:ss58 |
Ed25519 | SS58 (base58 + BLAKE2b) |
| Tezos tz1 | tezos:tz1 |
Ed25519 | tz1... (base58check) |
| Tezos tz2 | tezos:tz2 |
secp256k1 | tz2... (base58check) |
| Tezos tz3 | tezos:tz3 |
P-256 | tz3... (base58check) |
| NEAR | near:implicit |
Ed25519 | hex public key (implicit account) |
| Stellar | stellar:global |
Ed25519 | G... (StrKey) |
| Cardano | cardano:enterprise |
Ed25519 | addr1... (bech32, BLAKE2b-224) |
// 1. HKDF-SHA256 extract + expand
ikm = masterBytes // 32 bytes
salt = "ibexfi:derivation:v1"
info = "global:single_eoa"
okm = HKDF(SHA-256, ikm, salt, info, 32) // 32 bytes output
// 2. Reduce mod secp256k1 curve order
n = secp256k1.CURVE.n // ~1.158 Γ 10β·β·
num = bytesToNumberBE(okm) % n
if (num == 0) num = 1 // avoid zero scalar
// 3. Derive public key + Ethereum address
privateKey = numberToBytesBE(num, 32)
publicKey = secp256k1.getPublicKey(privateKey, false).slice(1) // 64 bytes uncompressed
addressHash = Keccak-256(publicKey)
address = "0x" + last20bytes(addressHash) // checksum
The Safe{Wallet} and derived wallets use different mechanisms from the same identity anchor (the passkey Signer):
| Aspect | Safe{Wallet} | Derived Wallets (EOA, Solana, BTC...) |
|---|---|---|
| Type | Smart contract wallet | Native chain addresses |
| Derivation | X/Y coordinates of the P-256 passkey public key β WebAuthn signer contract β Safe address | Master bytes (PRF or sealed) β HKDF β chain-specific key |
| Private key | None (smart contract, no ECDSA key) | Derived in-memory, never stored |
| Signing | WebAuthn assertion (authenticator signs) | Passkey PRF β master β derive key β sign |
For clients that cannot use passkeys, IBEx supports a PIN/KDF mode
(wallet=kdf) where the master is derived client-side from a PIN or password:
Key difference: In passkey mode, the server can request WebAuthn signatures via the authenticator. In KDF mode, the client must sign explicitly β the server only verifies.
| Threat | Protection |
|---|---|
| DB breach (read sealed master) | AES-256-GCM β requires server secrets to decrypt |
| Server secret leak | Without DB ciphertext, secrets alone are useless |
| Both DB + secrets compromised | Can derive keys β mitigate with KMS, rotation, monitoring |
| PRF mode: backend compromise | Master never stored β only in memory during auth, from authenticator |
| Derived key recovery from address | HKDF is a one-way function β address β key is infeasible |
DERIVATION_SECRET / DERIVATION_SALT at startup (no defaults)β οΈ Warning: If the master secret is lost β passkey deleted without PRF backup, orDERIVATION_SECRET/DERIVATION_SALTlost for sealed envelopes β all derived wallets become permanently inaccessible. Funds on those addresses cannot be recovered.
The only safety net is the Recovery module: when enabled, a secondary signer (recovery key or social recovery guardian) can regain control of the Safe{Wallet}. Recovery does not restore the derived multi-chain addresses (Solana, Bitcoinβ¦), only the Safe smart wallet.
The derivation logic is in derivationService.ts, using
@noble/hashes
and @noble/curves:
deriveGlobalPrivateKeyFromPrf() β EVM EOA (secp256k1)deriveSolanaAddressFromPrf() β Solana (Ed25519)deriveBitcoinP2WPKHFromPrf() β Bitcoin P2WPKH (bech32)deriveBitcoinP2TRFromPrf() β Bitcoin Taproot (bech32m)deriveCosmosAddressFromPrf() β Cosmos (bech32)derivePolkadotAddressFromPrf() β Polkadot (SS58)deriveTezosTz1/Tz2/Tz3FromPrf() β Tezos (base58check)deriveNearImplicitFromPrf() β NEAR (hex)deriveStellarFromPrf() β Stellar (StrKey)deriveCardanoAddrFromPrf() β Cardano (bech32)sealMaster() / unsealMaster() β AES-256-GCM fallback