Skip to main content

Overview

Keystore encryption transforms a raw private key into a password-protected JSON structure. The process uses key derivation functions (KDF) and symmetric encryption to ensure the private key cannot be recovered without the correct password.

Encryption Process

Algorithm Flow

Password + Salt


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


 Derived Key (32 bytes)

       ├──────────────────┐
       │                  │
       ▼                  ▼
 Encryption Key     MAC Key
  (16 bytes)       (16 bytes)
       │                  │
       ▼                  │
┌─────────────┐           │
│ AES-128-CTR │           │
└─────────────┘           │
       │                  │
       ▼                  │
   Ciphertext ───────────►│


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


                        MAC

Step by Step

  1. Generate random salt (32 bytes) and IV (16 bytes)
  2. Derive key from password using KDF (scrypt or PBKDF2)
  3. Split derived key: first 16 bytes for encryption, next 16 for MAC
  4. Encrypt private key with AES-128-CTR using encryption key and IV
  5. Compute MAC as keccak256(macKey || ciphertext)
  6. Assemble keystore JSON structure

Basic Encryption

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

const privateKey = PrivateKey.from(
  '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
);

// Default encryption (scrypt)
const keystore = await Keystore.encrypt(privateKey, 'my-password');

console.log(JSON.stringify(keystore, null, 2));
Output:
{
  "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..."
  }
}

Encryption Options

KDF Selection

// Scrypt - memory-hard, GPU-resistant (recommended)
const keystore = await Keystore.encrypt(privateKey, password, {
  kdf: 'scrypt'
});
Pros:
  • Memory-hard (expensive to parallelize)
  • GPU/ASIC resistant
  • Higher security against brute-force
Cons:
  • Slower (~2-5 seconds default)
  • Higher memory usage

Scrypt Parameters

const keystore = await Keystore.encrypt(privateKey, password, {
  kdf: 'scrypt',
  scryptN: 262144,  // CPU/memory cost (power of 2)
  scryptR: 8,       // Block size
  scryptP: 1        // Parallelization factor
});
ParameterDefaultDescription
scryptN262144CPU/memory cost (2^18). Higher = slower, more secure
scryptR8Block size. Higher = more memory
scryptP1Parallelization. Higher = more parallelizable
Memory formula: 128 * N * r * p bytes Default: 128 * 262144 * 8 * 1 = 256 MB

PBKDF2 Parameters

const keystore = await Keystore.encrypt(privateKey, password, {
  kdf: 'pbkdf2',
  pbkdf2C: 262144  // Iteration count
});
ParameterDefaultDescription
pbkdf2C262144Iteration count. Higher = slower, more secure

Custom Salt and IV

// For deterministic testing or specific requirements
const salt = new Uint8Array(32);
crypto.getRandomValues(salt);

const iv = new Uint8Array(16);
crypto.getRandomValues(iv);

const keystore = await Keystore.encrypt(privateKey, password, {
  salt,
  iv
});
Only provide custom salt/IV for testing or specific compliance requirements. Random generation (default) is recommended for security.

Custom UUID

const keystore = await Keystore.encrypt(privateKey, password, {
  uuid: 'my-custom-uuid-12345678'
});

console.log(keystore.id); // 'my-custom-uuid-12345678'

Performance Tuning

Fast Encryption (Testing/Development)

// Much faster (~50-100ms) but less secure
const keystore = await Keystore.encrypt(privateKey, password, {
  kdf: 'scrypt',
  scryptN: 1024,  // Very low
  scryptR: 1,
  scryptP: 1
});

Balanced (Mobile/Web)

// Balance of speed and security (~200-500ms)
const keystore = await Keystore.encrypt(privateKey, password, {
  kdf: 'scrypt',
  scryptN: 16384,
  scryptR: 8,
  scryptP: 1
});

Maximum Security (Cold Storage)

// Slower (~10-30s) but maximum security
const keystore = await Keystore.encrypt(privateKey, password, {
  kdf: 'scrypt',
  scryptN: 1048576,  // 2^20
  scryptR: 8,
  scryptP: 1
});

Error Handling

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

try {
  const keystore = await Keystore.encrypt(privateKey, password);
  console.log('Encryption successful');
} catch (error) {
  if (error instanceof Keystore.EncryptionError) {
    console.error('Encryption failed:', error.message);
  }
}

Advanced Usage

Deterministic Encryption (Testing)

// Same inputs = same output (for testing only)
const fixedSalt = new Uint8Array(32).fill(1);
const fixedIv = new Uint8Array(16).fill(2);
const fixedUuid = 'test-uuid-12345678';

const keystore1 = await Keystore.encrypt(privateKey, password, {
  salt: fixedSalt,
  iv: fixedIv,
  uuid: fixedUuid
});

const keystore2 = await Keystore.encrypt(privateKey, password, {
  salt: fixedSalt,
  iv: fixedIv,
  uuid: fixedUuid
});

// keystore1 and keystore2 are identical

Batch Encryption

async function encryptMultiple(privateKeys, password) {
  return Promise.all(
    privateKeys.map(pk => Keystore.encrypt(pk, password))
  );
}

const keystores = await encryptMultiple(
  [privateKey1, privateKey2, privateKey3],
  'shared-password'
);

Progress Indication

Since encryption can take several seconds with default parameters:
async function encryptWithProgress(privateKey, password, onProgress) {
  onProgress('Generating salt and IV...');

  onProgress('Deriving key (this may take a moment)...');
  const keystore = await Keystore.encrypt(privateKey, password);

  onProgress('Complete!');
  return keystore;
}

// Usage
const keystore = await encryptWithProgress(
  privateKey,
  password,
  (status) => console.log(status)
);

Encryption Components Explained

Salt

  • Purpose: Ensures different derived keys for same password
  • Size: 32 bytes (256 bits)
  • Generation: crypto.getRandomValues()
  • Storage: Stored in kdfparams.salt (hex-encoded)

IV (Initialization Vector)

  • Purpose: Ensures different ciphertexts for same key
  • Size: 16 bytes (128 bits)
  • Generation: crypto.getRandomValues()
  • Storage: Stored in cipherparams.iv (hex-encoded)

Derived Key

  • Purpose: Convert password to fixed-length encryption key
  • Size: 32 bytes (256 bits)
  • Split: First 16 bytes = encryption key, last 16 bytes = MAC key

MAC (Message Authentication Code)

  • Purpose: Verify password correctness and data integrity
  • Algorithm: Keccak256
  • Input: macKey || ciphertext
  • Size: 32 bytes (256 bits)

References