This guide covers the Shamir Secret Sharing key recovery system, which provides secure wallet backup and recovery using a configurable threshold scheme.
The Shamir system splits your wallet’s root private key into multiple shares. A configurable threshold of shares can reconstruct the key:
Default configuration (2-of-3):
Example configurations:
| Threshold | Total | Server | User Shares | Use Case |
|---|---|---|---|---|
| 2 | 3 | 1 | 2 | Standard (default) |
| 2 | 4 | 1 | 3 | Extra redundancy |
| 3 | 5 | 1 | 4 | High security |
| 3 | 4 | 1 | 3 | Balanced security |
Important constraint: User must always have at least threshold shares so they can recover independently without the server. This prevents the WAB from becoming a custodian of user funds. For example, 2-of-2 is not allowed because the user would only have 1 share and could not recover without server cooperation.
The WAB server always stores exactly one share and cannot reconstruct the key alone.
For maximum security, keys are generated using mouse movement entropy mixed with the system’s cryptographically secure random number generator (CSPRNG).
import { EntropyCollector } from '@bsv/wallet-toolbox'
// Create collector (default: 256 samples)
const collector = new EntropyCollector({
targetSamples: 256,
minSampleInterval: 10 // ms between samples
})
// Option 1: Manual collection from mousemove events
document.addEventListener('mousemove', (event) => {
const progress = collector.addMouseSample(event.clientX, event.clientY)
if (progress) {
console.log(`Entropy: ${progress.percent}% (${progress.collected}/${progress.target})`)
}
})
// Check when complete
if (collector.isComplete()) {
const entropy = collector.generateEntropy() // 32 bytes
}
// Option 2: Automatic browser collection with progress callback
const entropy = await collector.collectFromBrowser(document, (progress) => {
updateProgressBar(progress.percent)
})
The generateEntropy() method:
crypto.getRandomValues() outputThe ShamirWalletManager class handles the complete wallet lifecycle with Shamir shares.
import { ShamirWalletManager, Setup, PrivateKey, PrivilegedKeyManager } from '@bsv/wallet-toolbox'
const manager = new ShamirWalletManager({
wabServerUrl: 'https://your-wab-server.com',
authMethodType: 'TwilioPhone', // or 'DevConsole' for development
// Optional: customize threshold scheme (defaults to 2-of-3)
threshold: 2, // shares needed to reconstruct (min: 2)
totalShares: 3, // total shares generated (min: 3, must be >= threshold + 1)
walletBuilder: async (privateKey, privilegedKeyManager) => {
const { wallet } = await Setup.createWalletSQLite({
filePath: './wallet.sqlite',
databaseName: 'myWallet',
chain: 'main',
rootKeyHex: privateKey.toHex(),
privilegedKeyManager
})
return wallet
}
})
// Check configuration
console.log(`Using ${manager.getThreshold()}-of-${manager.getTotalShares()} scheme`)
// 1. Collect entropy (show user a "move your mouse" UI)
await manager.collectEntropyFromBrowser(document, (progress) => {
document.getElementById('progress').textContent = `${progress.percent}%`
})
// 2. Start OTP verification (user receives SMS)
const wabClient = new WABClient('https://your-wab-server.com')
await wabClient.startShareAuth('TwilioPhone', userIdHash, {
phoneNumber: '+1234567890'
})
// 3. User enters OTP code, then create wallet
const result = await manager.createNewWallet(
{ phoneNumber: '+1234567890', otp: '123456' },
async (userShares, threshold, totalShares) => {
// Application decides how to handle user shares
// For 2-of-3: userShares has 2 shares
console.log(`Save these ${userShares.length} shares (${threshold}-of-${totalShares} scheme)`)
// Example: first share for printing, second for password manager
await showPrintableBackup(userShares[0])
await showCopyableText(userShares[1])
return await confirmUserSavedShares()
}
)
console.log('User ID Hash:', result.userIdHash)
console.log('User Shares:', result.userShares)
console.log(`Scheme: ${result.threshold}-of-${result.totalShares}`)
// 4. Build and use the wallet
const wallet = await manager.buildWallet()
When the user has enough shares but needs the server share to meet threshold:
const manager = new ShamirWalletManager({ /* config */ })
// User provides their userIdHash
manager.setUserIdHash(savedUserIdHash)
// Start OTP to retrieve server share
await manager.startOTPVerification({ phoneNumber: '+1234567890' })
// Recover with user shares (need threshold-1 shares)
// For 2-of-3: need 1 user share + server share
const privateKey = await manager.recoverWithServerShare(
[userShare1], // Array of user-held shares
{ phoneNumber: '+1234567890', otp: '123456' }
)
const wallet = await manager.buildWallet()
When the user has enough shares to meet threshold without the server:
const manager = new ShamirWalletManager({ /* config */ })
// Recover using user-held shares (need at least threshold shares)
// For 2-of-3: need 2 user shares
const privateKey = await manager.recoverWithUserShares([userShare1, userShare2])
const wallet = await manager.buildWallet()
For lower-level control, use WABClient directly:
import { WABClient } from '@bsv/wallet-toolbox'
const client = new WABClient('https://your-wab-server.com')
// Start OTP verification
await client.startShareAuth('TwilioPhone', userIdHash, {
phoneNumber: '+1234567890'
})
// Store the server share (after OTP verification)
const storeResult = await client.storeShare(
'TwilioPhone',
{ phoneNumber: '+1234567890', otp: '123456' },
serverShare, // The share to store (format: x.y.threshold.integrity)
userIdHash
)
// Retrieve the server share (requires OTP)
const retrieveResult = await client.retrieveShare(
'TwilioPhone',
{ phoneNumber: '+1234567890', otp: '654321' },
userIdHash
)
console.log('Retrieved server share:', retrieveResult.shareB)
// Update share (for key rotation)
await client.updateShare(
'TwilioPhone',
{ phoneNumber: '+1234567890', otp: '111222' },
userIdHash,
newServerShare
)
// Delete account and stored share
await client.deleteShamirUser(
'TwilioPhone',
{ phoneNumber: '+1234567890', otp: '333444' },
userIdHash
)
To rotate keys (generate new shares while maintaining access):
// Collect fresh entropy
manager.resetEntropy()
await manager.collectEntropyFromBrowser(document, onProgress)
// Rotate keys (requires OTP verification)
const newResult = await manager.rotateKeys(
{ phoneNumber: '+1234567890', otp: '123456' },
async (userShares, threshold, totalShares) => {
console.log(`Save these ${userShares.length} NEW shares`)
// User must save all new shares
return await confirmUserSavedShares()
}
)
// Server share is automatically updated
// User must save new user shares
To delete a Shamir account and its stored share:
// Requires OTP verification
await manager.deleteAccount({
phoneNumber: '+1234567890',
otp: '123456'
})
// WARNING: Server share is permanently deleted
// User needs enough remaining shares to meet threshold
Shamir shares use the format: x.y.threshold.integrity
Example share:
1.7KvWLhJ3rQ9FnBZxYmUdNpTsR6CwEiAoH8bVfGjDkM2.2.5XyZ
2-of-3 (default):
3-of-5 (high security):
try {
await manager.recoverWithServerShare(userShares, authPayload)
} catch (error) {
if (error.message.includes('Rate limited')) {
// Too many attempts - wait and retry
} else if (error.message.includes('OTP verification failed')) {
// Wrong code - let user retry
} else if (error.message.includes('integrity check failed')) {
// Shares don't match - wrong share or corrupted
} else if (error.message.includes('Need at least')) {
// Not enough shares provided
}
}