Encrypt Shares

Learn how to encrypt wallet backup shares using AES-256-GCM, securely manage encryption keys, and protect recovery data with passphrases, passkeys, or KMS integrations.

The SDK does not encrypt backup shares for you. You are responsible for encrypting them before storage. This page shows recommended encryption patterns.

AES-256-GCM (Recommended)

AES-256-GCM provides authenticated encryption — the ciphertext cannot be tampered with undetected.

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

/**
 * Encrypt a share string with a 32-byte key.
 * Returns a base64-encoded blob: iv (12 bytes) + auth tag (16 bytes) + ciphertext.
 */
function encryptShare(share: string, key: Buffer): string {
  const iv = randomBytes(12);
  const cipher = createCipheriv("aes-256-gcm", key, iv);

  const encrypted = Buffer.concat([
    cipher.update(share, "utf8"),
    cipher.final(),
  ]);
  const tag = cipher.getAuthTag();

  return Buffer.concat([iv, tag, encrypted]).toString("base64");
}

/**
 * Decrypt a share string encrypted with encryptShare().
 */
function decryptShare(ciphertext: string, key: Buffer): string {
  const buf = Buffer.from(ciphertext, "base64");
  const iv        = buf.subarray(0, 12);
  const tag       = buf.subarray(12, 28);
  const encrypted = buf.subarray(28);

  const decipher = createDecipheriv("aes-256-gcm", key, iv);
  decipher.setAuthTag(tag);

  return Buffer.concat([
    decipher.update(encrypted),
    decipher.final(),
  ]).toString("utf8");
}

Key Derivation from a Passphrase

If the encryption key comes from a user passphrase, use PBKDF2 or Argon2:

import { pbkdf2Sync, randomBytes } from "crypto";

function deriveKey(passphrase: string, salt: Buffer): Buffer {
  return pbkdf2Sync(
    passphrase,
    salt,
    600_000,    // iterations — OWASP 2023 recommendation
    32,         // key length in bytes
    "sha256"
  );
}

// Encrypt
const salt = randomBytes(16);
const key  = deriveKey(userPassphrase, salt);
const ciphertext = encryptShare(backup.SECP256K1.share, key);

// Store salt alongside ciphertext — you need it for decryption
await db.saveBackup(userId, {
  salt: salt.toString("hex"),
  ciphertext,
  backupSharePairId: backup.SECP256K1.id,
});

// Decrypt
const storedSalt = Buffer.from(record.salt, "hex");
const keyAgain   = deriveKey(userPassphrase, storedSalt);
const share      = decryptShare(record.ciphertext, keyAgain);

Key Management Options

ApproachProsCons
User passphrase + PBKDF2User controls key, no backend dependencyWeak if passphrase is weak
Passkey (WebAuthn)Phishing-resistant, hardware-backedBrowser/device dependent
AWS KMS / GCP KMSCentrally managed, auditableBackend dependency
Hardware Security ModuleHighest securityComplex, expensive

What to Store

After encrypting, you store:

  • The ciphertext (the encrypted backup share)
  • The salt or key identifier needed for decryption
  • The backupSharePairId (returned from backupWallet()) — needed when calling storeEncryptedBackupShare() or during recovery

Never store the plaintext share or the raw encryption key alongside the ciphertext.