Shamir Secret Sharing
Split and recover English BIP-39 mnemonics with threshold shares from @tetherto/wdk-utils
splitMnemonic() and combineMnemonic() are root exports of @tetherto/wdk-utils. They convert an English BIP-39 mnemonic to entropy, add a four-byte integrity check, and create hex-encoded Shamir shares. Install and import this capability from the WDK Utils package root; the package does not export an @tetherto/wdk-utils/shamir subpath.
Shares are unencrypted recovery material. Anyone who obtains the threshold number of shares can reconstruct the mnemonic. Store shares in separate trusted locations, authenticate the exact share bytes before recovery, and never send a mnemonic or its shares to logs, analytics, crash reports, or untrusted services.
Before you start
You need:
- A valid 12-, 15-, 18-, 21-, or 24-word English BIP-39 mnemonic generated with a cryptographically secure source. A valid word list and checksum do not prove that the original generation process was secure.
- A recovery policy that defines the total number of shares (
n) and the threshold needed to recover (k). Both values must be integers,2 <= k <= n <= 255. - A secure way to distribute and retain each share separately, plus a tamper-evident record for authenticating the share bytes during recovery.
- Cryptographically secure randomness. Node.js uses its
node:cryptoimplementation. Browser and React Native paths must provideglobalThis.crypto.getRandomValues; React Native runtimes may need a polyfill. - Bare
1.28.0or later when using the package's Bare entrypoint. That entrypoint loadsbare-node-runtime/global, and the dependency versions currently resolved for a fresh beta.11 installation enforce this minimum.
The helper receives only the mnemonic words. It does not include an optional BIP-39 passphrase, derivation path, network, account index, or other wallet metadata. Preserve everything your wallet needs for recovery through a separate protected process.
The returned values are package-specific hex shares. They are not SLIP-39 mnemonic shares and are not compatible with tools that expect word-based recovery shares.
Install WDK Utils
npm install @tetherto/wdk-utilsIn React Native, install a secure random-values polyfill when the runtime does not provide globalThis.crypto.getRandomValues:
npm install react-native-get-random-valuesLoad the polyfill in the application entrypoint before any file imports WDK Utils:
import 'react-native-get-random-values'Node.js and Bare do not need this React Native polyfill for splitMnemonic(). In Bare, import from the package root so its Bare entrypoint supplies the Node-compatible runtime globals.
Choose a threshold
A 3-of-5 policy creates five shares and requires any three from the same generated set to recover the mnemonic. It tolerates the loss of two shares, while compromise of any three shares exposes the mnemonic.
There is no universal threshold. Choose n and k from the number of independent storage locations, the people or systems involved in recovery, and the acceptable loss and compromise scenarios. Do not create more shares than the recovery process can inventory and protect.
Split the mnemonic
Keep the policy in application configuration or a protected recovery record. Do not rely on the share strings to describe it.
import { splitMnemonic } from '@tetherto/wdk-utils'
export const recoveryPolicy = {
shares: 5,
threshold: 3
}
export async function createRecoveryShares(mnemonic) {
return splitMnemonic(mnemonic, recoveryPolicy)
}splitMnemonic() returns five lowercase hex strings for this policy. It normalizes leading, trailing, and repeated whitespace in the mnemonic. It rejects invalid English BIP-39 words, word counts, and checksums.
Do not print the returned array or persist all shares as one application record.
Preserve the recovery record
The API returns share strings, not a self-describing recovery bundle. Keep protected operational metadata that identifies:
- The package and installed version used to create the shares.
- The total share count and recovery threshold.
- An application-defined identifier for this generated share set.
- The English BIP-39 format and hex share encoding.
- Whether the wallet also requires a separate BIP-39 passphrase or other recovery metadata.
- Which share belongs in each trusted location, without placing every share in the same record.
- An authenticated digest or signature for each exact share value, stored where an attacker cannot replace both a share and its integrity record.
- A collision-resistant expected public wallet identifier, such as a complete address, public key, or extended public key derived with the recorded passphrase and derivation settings, for validating the recovered wallet before use. Do not rely on a short wallet or BIP-32 key fingerprint for adversarial verification.
Each call to splitMnemonic() creates a new random share set. Do not mix shares from different calls, even when they were created from the same mnemonic.
The share strings do not carry a format-version marker, and the package does not document a cross-version compatibility guarantee. Record the installed package version and test recovery before depending on a package upgrade for an existing backup.
Verify the backup
Retrieve a threshold-sized subset from the intended independent storage locations and verify it in a trusted environment before treating the backup as recoverable. Use a subset rather than all generated shares so the test exercises the intended threshold and retrieval path.
The wrappers below show application-defined integration boundaries, not additional WDK Utils exports. Your recovery system must implement verifyShare and matchesExpectedWallet against independently protected records. Each callback must return true or false, or a Promise of that boolean. Stop recovery when either control is unavailable.
import { combineMnemonic } from '@tetherto/wdk-utils'
export async function verifyRecovery(
mnemonic,
retrievedShares,
{ threshold, verifyShare }
) {
if (!Array.isArray(retrievedShares) || retrievedShares.length > 255) {
throw new Error('Invalid recovery shares')
}
const shares = [...retrievedShares]
if (!Number.isInteger(threshold) || threshold < 2 || threshold > 255) {
throw new Error('Invalid recovery threshold')
}
if (typeof verifyShare !== 'function') {
throw new Error('Share verifier is required')
}
if (shares.length < threshold) {
throw new Error('Not enough recovery shares')
}
const verified = await Promise.all(
shares.map((share) => verifyShare(share))
)
if (!verified.every((result) => result === true)) {
throw new Error('Recovery share authentication failed')
}
const restored = await combineMnemonic(shares)
const expected = mnemonic.trim().replace(/\s+/g, ' ')
if (restored !== expected) {
throw new Error('Recovery verification failed')
}
}verifyShare must compare each exact share value with the authenticated digest or signature recorded when the share set was created. Checking only that a share is valid hex is not authentication.
Do not log either value during comparison. Keep the original recovery material until the recovery policy and independently stored threshold subset have been verified. Repeat recovery drills when storage locations, custodians, or package versions change.
Recover the mnemonic
Collect at least the recorded threshold number of shares from the same generated set. Authenticate each share before combining it, then validate the recovered wallet against an independently protected public identifier before use:
import { combineMnemonic } from '@tetherto/wdk-utils'
export async function recoverMnemonic(
recoveryShares,
{ threshold, verifyShare, matchesExpectedWallet }
) {
if (!Array.isArray(recoveryShares) || recoveryShares.length > 255) {
throw new Error('Invalid recovery shares')
}
const shares = [...recoveryShares]
if (!Number.isInteger(threshold) || threshold < 2 || threshold > 255) {
throw new Error('Invalid recovery threshold')
}
if (
typeof verifyShare !== 'function' ||
typeof matchesExpectedWallet !== 'function'
) {
throw new Error('Recovery verification callbacks are required')
}
if (shares.length < threshold) {
throw new Error('Not enough recovery shares')
}
const verified = await Promise.all(
shares.map((share) => verifyShare(share))
)
if (!verified.every((result) => result === true)) {
throw new Error('Recovery share authentication failed')
}
const mnemonic = await combineMnemonic(shares)
const walletMatches = await matchesExpectedWallet(mnemonic)
if (walletMatches !== true) {
throw new Error('Recovered wallet identity does not match')
}
return mnemonic
}verifyShare has the same authenticated-record requirement as the verification step. matchesExpectedWallet must derive and compare a collision-resistant expected public wallet identifier with the separately recorded passphrase and derivation settings. A short wallet or BIP-32 key fingerprint is insufficient. Do not transfer funds, discard another backup, or update a wallet identity based only on a successful checksum.
combineMnemonic() accepts case-insensitive hex strings and returns the normalized English BIP-39 mnemonic. The supplied shares must have the same length, use unique share coordinates, and belong to the same generated set. The helper accepts between 2 and 255 shares, but the application must retain and enforce the actual recovery threshold.
Both mnemonic helpers return Promises that reject with Error objects. For combineMnemonic(), non-array input, fewer than two shares, non-string entries, and malformed hex retain specific messages. Duplicate coordinates, mixed lengths, more than 255 shares, reconstruction failures, and checksum failures collapse to Invalid shares: could not reconstruct a valid mnemonic.
The embedded four-byte checksum normally detects accidental corruption and incorrect reconstruction, but its 32-bit value can collide. It is not a MAC or signature, does not authenticate the people or systems providing shares, and does not protect against deliberately forged input. Do not use combineMnemonic() itself as a share-authentication check.
Understand the limits
- WDK Utils does not generate or store the mnemonic, distribute shares, enforce access control, or manage recovery custodians.
- Sharing is not encryption. Protect every share as material that may later be combined with compromised shares.
- Only the English BIP-39 wordlist is supported.
- An optional BIP-39 passphrase and wallet-specific recovery metadata remain outside these shares.
- The implementation clears decoded mnemonic entropy and the reconstructed secret on a best-effort basis. It does not clear decoded share arrays, and JavaScript strings held by the application cannot be reliably zeroized. Keep mnemonic and share strings out of long-lived state.
- The returned Promises reject with
Errorobjects rather than structured error codes. Handle failures withawaitandtry...catchor with.catch(), show a safe generic message to users, and never include share values in diagnostics.
Next steps
WDK Utils Configuration
Review package imports and runtime requirements.
WDK Utils API Reference
Review exact signatures, constraints, and failure behavior.