Backup Wallet

Use backupWallet() to create encrypted recovery shares, securely store backup data, and enable reliable wallet recovery for MPC wallets.

A backup lets your users recover their wallet if the client share is lost. The backup process produces a second set of shares (one per curve) using a different MPC round — Tatum's backup share is different from its signing share, so backups don't weaken the signing security model.

How Backups Work

  1. backupWallet() — the enclave produces a backup share pair using the original generate response
  2. You encrypt the client backup share and store it (in Portal, your own backend, or the user's device)
  3. updateBackupSharePairs() — you confirm the backup shares are stored

During recovery, both the encrypted client backup share and Tatum's custodian backup share are required. Neither alone can reconstruct the signing shares.

Step 1: Generate Backup Shares

Pass the original generateWallet() response (JSON-stringified) to backupWallet():

// shares = result of generateWallet()
const backup = await client.backupWallet({
  body: {
    generateResponse: JSON.stringify(shares),
  },
});

// backup.SECP256K1 → { share: string, id: string }
// backup.ED25519   → { share: string, id: string }

The backup.*.id values are the backupSharePairIds used in subsequent steps.

Step 2: Encrypt Backup Shares

Encrypt each backup share with a key only the user controls (passphrase, passkey, KMS, etc.):

import { createCipheriv, randomBytes, createDecipheriv } from "crypto";

function encryptShare(share: string, keyHex: string): string {
  const key = Buffer.from(keyHex, "hex");
  const iv = randomBytes(12);
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  const ciphertext = Buffer.concat([cipher.update(share, "utf8"), cipher.final()]);
  const tag = cipher.getAuthTag();
  return Buffer.concat([iv, tag, ciphertext]).toString("base64");
}

const userKey = deriveKeyFromPassphrase(userPassphrase); // your implementation

const encryptedSecp = encryptShare(backup.SECP256K1.share, userKey);
const encryptedEd   = encryptShare(backup.ED25519.share,   userKey);

Step 3: Store Encrypted Backup Shares

Option A: Store with Portal (Portal-Managed Backup)

await client.storeEncryptedBackupShare({
  path: { backupSharePairId: backup.SECP256K1.id },
  body: { clientCipherText: encryptedSecp },
});

await client.storeEncryptedBackupShare({
  path: { backupSharePairId: backup.ED25519.id },
  body: { clientCipherText: encryptedEd },
});

Option B: Store in Your Own Backend

Store the ciphertext in your database alongside the backupSharePairId. Skip the storeEncryptedBackupShare() call — you'll retrieve it yourself during recovery.

Step 4: Mark Backup Complete

await client.updateBackupSharePairs({
  body: {
    backupSharePairIds: [backup.SECP256K1.id, backup.ED25519.id],
    status: "STORED_CLIENT_BACKUP_SHARE",
  },
});

Use "STORED_CLIENT_BACKUP_SHARE_KEY" if you're using a passkey or hardware key method.

Full Backup Flow

// 1. Generate backup shares
const backup = await client.backupWallet({
  body: { generateResponse: JSON.stringify(shares) },
});

// 2. Encrypt
const encrypted = {
  SECP256K1: encryptShare(backup.SECP256K1.share, userKey),
  ED25519:   encryptShare(backup.ED25519.share,   userKey),
};

// 3. Store with Portal
for (const curve of ["SECP256K1", "ED25519"] as const) {
  await client.storeEncryptedBackupShare({
    path: { backupSharePairId: backup[curve].id },
    body: { clientCipherText: encrypted[curve] },
  });
}

// 4. Confirm stored
await client.updateBackupSharePairs({
  body: {
    backupSharePairIds: [backup.SECP256K1.id, backup.ED25519.id],
    status: "STORED_CLIENT_BACKUP_SHARE",
  },
});