Overview
Trustodian is non-custodial: the backend never holds a complete private key. The account’s recovery phrase is the one custody secret. It is split 2-of-3, and every wallet is re-derived from it when a key is needed, so no wallet has a key of its own to store. See Recovery phrase for the seed itself; this page covers the shares, the wallet password and the session.The three shares
Every secret is split into three shares; any two of them reconstruct it.
Reconstruction always happens inside your process, never on the server:
- Signing uses the device share plus the server share.
- Recovery, rotation and password change use the server share plus the recovery share, because the device share is the piece that is missing or stale.
The wallet password
The wallet password never leaves your process and is never sent to Trustodian, not even hashed. It is turned into key material through an exchange with the server in which the server learns nothing about the password itself (an oblivious PRF). Two independent keys come out of it:
That is why create, import, recover, rotate, change password and seed reads all take a
password argument even when the session is already unlocked: they touch the recovery share, and the session cannot supply the recovery key.
Strength rules
setWalletPassword and changeWalletPassword reject a new password that does not meet all of these.
Unlocking is deliberately exempt, so an account created under the old rules still opens.
Validate in your own UI before you call the SDK, so the user sees the rule next to the field rather
than as a thrown error:
SESSION_LOCKED. See Authentication for unlocking, locking and the keyTtlMs session TTL.
The share store
You must supply ashareStore, or the SDK constructor throws. It is where the device share for each wallet lives. This is the contract:
saveoverwrites. Rotation, recovery and password change all save a new record for the same(userRef, walletId), so upsert on that pair.versionis opaque to you. Store it and return it unchanged; the SDK uses it to tell a current device share from a stale one.findByAddressmust normalize XRP. Addresses match exactly for every chain except XRP, which is lower-cased on both sides.deletemust actually delete. The SDK removes a device share once it has established that the share is unusable, before rebuilding it.
InMemoryShareStore is for local development and tests only. It dies with the process, so every device share is lost and every wallet needs a password-based recovery on the next start.
Wallets the user also uses elsewhere
A user who also uses the Trustodian web or mobile app has a separate set of encrypted shares for the same wallets, so a rotation on one side does not invalidate the other. The wallet password is shared across them; the device share is not. The first time your integration touches a wallet that was created in another app, the SDK sets your side up automatically on the first operation. There is nothing to configure.Keeping a device share current
Before signing, the SDK checks that the device share it holds for the wallet is still the current one and, if it is not, rebuilds it from the server and recovery shares. That rebuild needs the wallet password. There are two ways to work with this:- Pass
walletPasswordon the crypto call and the SDK recovers silently before signing. This is the simple path. - Or recover explicitly, when you would rather control when it happens:
isDeviceShareRecoveryError(error) is true exactly when the failure is one the wallet password can fix: no device share on this host, a stale one, or one that no longer decrypts.
Seed wallets skip all of this. They have no device share; signing re-derives the key from the
account seed. See Recovery phrase.
Encryption key versions
The server key used in the password exchange is versioned. Every encrypted share records the version it was wrapped under, and the SDK reads that version off each share and derives the matching key. An account whose shares sit on several versions at once therefore keeps working. Shares move onto the current version only when someone asks. Nothing rotates on unlock, because the rotate endpoint allows three calls an hour and the SDK will not spend that budget on your behalf.{ rotated: 0, skipped: true } when nothing is
stale, so it is safe to call every time. Cache the attempt for the life of the session — including a
failed one, since a failure leaves the account exactly as it was and retrying only burns the hourly
budget the user may need for their own recovery.
stepUp is accepted but not required: the passkey step-up the web app performs is skipped for
API-key callers, so your key’s own permissions are the gate. Rotating an account that has a recovery
phrase needs wallet_management.
Share recovery API
Unlocking and locking a session, and reading password status, live in Authentication. The methods below rebuild device shares and rotate the password.Batch recovery
UserecoverWalletsBatch to rebuild many device shares at once, after restoring a host or after the user changed their password elsewhere. Large lists are chunked automatically, and a wallet that fails is reported in the results rather than aborting the batch. The password is checked once for the whole call, so a wrong one spends a single lockout attempt.
vaultKeys, each entry recovered, skipped or failed. An employee who accepted a seat on
another device gets its local half back here, so pass the call an empty wallets list when the seats
are all you need to repair.
Changing the wallet password
changeWalletPassword re-encrypts every share under keys derived from the new password: wallet shares, vault activation shares, provisioned vault seats, and, if the account has a recovery phrase, the seed shares. It is a full re-encryption pass, not a flag on the server. New device shares are written to your share store, and the session continues under the new password.
The result counts what moved, including cosignerKeysRotated for vault seats, and oprfKeyVersion reports the encryption key version everything now sits on.
The change proves the current password to the server, not only to your process: the request carries a proof derived from currentPassword, which the server compares with the one it stored when the password was last unlocked. Requires SDK v3.3.2 or newer.
Wallet creation
createWallet accepts an optional mode, but you should omit it. Every account reaching the SDK already has a recovery phrase, so the wallet is derived from the account seed: shareless, with nothing written to your share store. The private key exists only in your process, and what crosses the wire is an address.
importWallet({ chain, privateKey, userRef, password }) takes an existing private key, checks that it derives the address it claims, and splits it the same way. It accepts the key formats the major wallets export. An imported key can never come from the account seed, so its device share is written to your share store, unlike a wallet created from the seed.
Exporting private keys
What is kept in memory, and for how long
The SDK wipes every secret buffer it owns as soon as it is finished with it: derived keys, decrypted shares and reconstructed private keys.Two things cannot be wiped: JavaScript strings (the wallet password, a recovery phrase shown at
establish time) and anything you copy out of the SDK. Keep those alive for as short a time as
possible, and never place them in a log line, an error message, an APM span or a crash dump.
seedEntropyCache is deprecated and
ignored. See Strict authentication.
Vault activation shares
Creating and activating a multisig vault on Solana, Tron and XRP produces an activation secret, split the same 2-of-3 way. The SDK handles it when you pass an unlockeduserRef to createMultisig / activateMultisig, and changeWalletPassword rotates it along with everything else. See Multisig and Organization vaults.
Security responsibilities
No single party holds two usable shares. Compromise of the Trustodian side yields ciphertext with no key; compromise of your share store yields one share, which rebuilds nothing.