wallet-toolbox

WAB Shamir Key Recovery

This guide covers the Shamir Secret Sharing key recovery system, which provides secure wallet backup and recovery using a configurable threshold scheme.

Overview

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.

Generating a Secure Key with Entropy Collection

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:

  1. Extracts raw entropy from mouse positions and timing
  2. Hashes it with SHA-256 to whiten the data
  3. XORs with crypto.getRandomValues() output
  4. Final SHA-256 hash ensures uniform distribution

Using ShamirWalletManager

The ShamirWalletManager class handles the complete wallet lifecycle with Shamir shares.

Configuration

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`)

Creating a New Wallet

// 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()

Recovery with Server Share

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()

Recovery with User Shares Only (Offline)

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()

Using WABClient Directly

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
)

Key Rotation

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

Account Deletion

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

Share Format

Shamir shares use the format: x.y.threshold.integrity

Example share:

1.7KvWLhJ3rQ9FnBZxYmUdNpTsR6CwEiAoH8bVfGjDkM2.2.5XyZ

Security Considerations

  1. Share Storage: Store user shares in separate, secure locations
  2. Threshold Selection: Higher threshold = more security but less convenience
  3. OTP Security: Consider SIM-swap risks with SMS; email may be safer for some users
  4. Entropy Quality: Always collect full entropy before key generation
  5. User ID Hash: Store separately - it identifies your account but cannot recover keys

2-of-3 (default):

3-of-5 (high security):

Error Handling

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
    }
}

Return to Documentation