Skip to main content

Overview

Keystore decryption reverses the encryption process to recover a private key. It derives the same encryption key from the password, verifies the MAC to ensure correctness, then decrypts the ciphertext.

Decryption Process

Algorithm Flow

Password + Salt (from keystore)


   ┌───────┐
   │  KDF  │ (scrypt or PBKDF2)
   └───────┘


 Derived Key (32 bytes)

       ├──────────────────┐
       │                  │
       ▼                  ▼
 Encryption Key     MAC Key
  (16 bytes)       (16 bytes)

   Ciphertext ───────────►│
   (from keystore)        │

                   ┌─────────────┐
                   │ Keccak256   │
                   │(MAC Key +   │
                   │ Ciphertext) │
                   └─────────────┘


                   Computed MAC


            ┌─────────────────────────┐
            │ Compare with stored MAC │
            │   (constant-time)       │
            └─────────────────────────┘

              ┌───────────┴───────────┐
              │                       │
         MAC matches            MAC differs
              │                       │
              ▼                       ▼
       ┌─────────────┐         InvalidMacError
       │ AES-128-CTR │         (wrong password)
       │  Decrypt    │
       └─────────────┘


         Private Key

Step by Step

  1. Validate keystore version (must be 3) and KDF (scrypt or pbkdf2)
  2. Extract parameters from keystore (salt, IV, ciphertext, MAC)
  3. Derive key from password using same KDF and parameters
  4. Split derived key: first 16 bytes for decryption, next 16 for MAC verification
  5. Compute expected MAC as keccak256(macKey || ciphertext)
  6. Verify MAC using constant-time comparison
  7. Decrypt ciphertext with AES-128-CTR if MAC matches

Basic Decryption

import * as Keystore from '@tevm/voltaire/crypto/Keystore';

// Load keystore (from file, localStorage, etc.)
const keystore = {
  version: 3,
  id: 'e4b8a7c2-1234-5678-9abc-def012345678',
  crypto: {
    cipher: 'aes-128-ctr',
    ciphertext: 'a7b8c9d0e1f2...',
    cipherparams: { iv: '1a2b3c4d5e6f...' },
    kdf: 'scrypt',
    kdfparams: {
      dklen: 32,
      n: 262144,
      r: 8,
      p: 1,
      salt: '9a8b7c6d5e4f...'
    },
    mac: 'f1e2d3c4b5a6...'
  }
};

// Decrypt
const privateKey = Keystore.decrypt(keystore, 'my-password');

console.log(privateKey); // Uint8Array (32 bytes)

Error Handling

Decryption can fail for several reasons. Always handle errors:
import * as Keystore from '@tevm/voltaire/crypto/Keystore';

try {
  const privateKey = Keystore.decrypt(keystore, password);
  console.log('Decryption successful');
} catch (error) {
  if (error instanceof Keystore.InvalidMacError) {
    // Most common: wrong password
    console.error('Invalid password or corrupted keystore');
  } else if (error instanceof Keystore.UnsupportedVersionError) {
    console.error('Keystore version not supported:', error.version);
  } else if (error instanceof Keystore.UnsupportedKdfError) {
    console.error('KDF not supported:', error.kdf);
  } else if (error instanceof Keystore.DecryptionError) {
    console.error('Decryption failed:', error.message);
  }
}

Error Types

ErrorCauseSolution
InvalidMacErrorWrong password or corrupted dataRetry with correct password
UnsupportedVersionErrorKeystore version not 3Only v3 keystores supported
UnsupportedKdfErrorKDF not scrypt or pbkdf2Convert to supported format
DecryptionErrorGeneral decryption failureCheck keystore integrity

Security Features

Constant-Time MAC Comparison

MAC verification uses constant-time comparison to prevent timing attacks:
// Internal implementation
function constantTimeEqual(a, b) {
  if (a.length !== b.length) return false;

  let result = 0;
  for (let i = 0; i < a.length; i++) {
    result |= a[i] ^ b[i];
  }

  return result === 0;
}
This ensures:
  • Attackers cannot determine how many bytes of the MAC matched
  • Password guessing attacks don’t get timing hints
  • Both correct and incorrect passwords take the same time to compare

No Partial Decryption

If MAC verification fails, no decryption attempt is made:
try {
  const privateKey = Keystore.decrypt(keystore, 'wrong-password');
} catch (error) {
  // error instanceof InvalidMacError
  // No plaintext data is leaked - decryption never occurred
}

Advanced Usage

Wallet Unlock Flow

async function unlockWallet(keystore, password) {
  try {
    const privateKey = Keystore.decrypt(keystore, password);

    return {
      success: true,
      privateKey,
      address: deriveAddress(privateKey)
    };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Keystore.InvalidMacError
        ? 'Invalid password'
        : 'Decryption failed'
    };
  }
}

// Usage with retry logic
let attempts = 0;
const MAX_ATTEMPTS = 3;

while (attempts < MAX_ATTEMPTS) {
  const password = await promptPassword();
  const result = await unlockWallet(keystore, password);

  if (result.success) {
    console.log('Wallet unlocked:', result.address);
    break;
  }

  attempts++;
  console.error(`${result.error}. ${MAX_ATTEMPTS - attempts} attempts remaining.`);
}

Loading from File

import * as fs from 'fs';
import * as Keystore from '@tevm/voltaire/crypto/Keystore';

function loadAndDecrypt(filepath, password) {
  // Read keystore file
  const content = fs.readFileSync(filepath, 'utf8');
  const keystore = JSON.parse(content);

  // Validate structure
  if (keystore.version !== 3) {
    throw new Error(`Unsupported keystore version: ${keystore.version}`);
  }

  // Decrypt
  return Keystore.decrypt(keystore, password);
}

// Usage
const privateKey = loadAndDecrypt('./keystore.json', 'my-password');

Browser localStorage

function loadFromStorage(key, password) {
  const stored = localStorage.getItem(key);

  if (!stored) {
    throw new Error('No keystore found');
  }

  const keystore = JSON.parse(stored);
  return Keystore.decrypt(keystore, password);
}

// Usage
try {
  const privateKey = loadFromStorage('wallet', userPassword);
} catch (error) {
  console.error('Failed to unlock wallet');
}

Batch Decryption

async function decryptMultiple(keystores, password) {
  const results = [];

  for (const keystore of keystores) {
    try {
      const privateKey = Keystore.decrypt(keystore, password);
      results.push({ id: keystore.id, success: true, privateKey });
    } catch (error) {
      results.push({ id: keystore.id, success: false, error: error.message });
    }
  }

  return results;
}

Performance Considerations

Decryption time depends on KDF parameters stored in the keystore:
KDFParametersTypical Time
ScryptN=262144 (default)2-5 seconds
ScryptN=16384100-200ms
PBKDF2c=262144 (default)500ms-1s
PBKDF2c=1000020-50ms

Showing Progress

async function decryptWithUI(keystore, password) {
  showLoadingSpinner('Decrypting...');

  try {
    // Decryption is synchronous but CPU-intensive
    // Consider using a Web Worker for non-blocking UI
    const privateKey = Keystore.decrypt(keystore, password);
    hideLoadingSpinner();
    return privateKey;
  } catch (error) {
    hideLoadingSpinner();
    throw error;
  }
}

Web Worker (Non-blocking)

// worker.js
import * as Keystore from '@tevm/voltaire/crypto/Keystore';

self.onmessage = (event) => {
  const { keystore, password } = event.data;

  try {
    const privateKey = Keystore.decrypt(keystore, password);
    self.postMessage({ success: true, privateKey: Array.from(privateKey) });
  } catch (error) {
    self.postMessage({ success: false, error: error.message });
  }
};

// main.js
const worker = new Worker('worker.js');

function decryptInWorker(keystore, password) {
  return new Promise((resolve, reject) => {
    worker.onmessage = (event) => {
      if (event.data.success) {
        resolve(new Uint8Array(event.data.privateKey));
      } else {
        reject(new Error(event.data.error));
      }
    };

    worker.postMessage({ keystore, password });
  });
}

Common Issues

Wrong Password

The most common decryption error:
// This always throws InvalidMacError for wrong passwords
try {
  Keystore.decrypt(keystore, 'wrong-password');
} catch (error) {
  // error instanceof Keystore.InvalidMacError === true
}
There’s no way to “recover” a keystore with a forgotten password. The only option is to try different passwords or use the original private key if backed up elsewhere.

Corrupted Keystore

If the keystore JSON is modified, decryption fails:
// Corrupted ciphertext
keystore.crypto.ciphertext = 'modified-value';

try {
  Keystore.decrypt(keystore, password);
} catch (error) {
  // InvalidMacError - MAC no longer matches
}

IV Corruption

A special case: corrupted IV passes MAC verification but produces wrong plaintext:
// IV corruption doesn't affect MAC (MAC doesn't include IV)
keystore.crypto.cipherparams.iv = 'different-iv-value';

const decrypted = Keystore.decrypt(keystore, password);
// Decryption "succeeds" but returns garbage data!
Always validate the resulting private key (e.g., check that it produces the expected address) after decryption.

References