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
- Generate random salt (32 bytes) and IV (16 bytes)
- Derive key from password using KDF (scrypt or PBKDF2)
- Split derived key: first 16 bytes for encryption, next 16 for MAC
- Encrypt private key with AES-128-CTR using encryption key and IV
- Compute MAC as
keccak256(macKey || ciphertext)
- Assemble keystore JSON structure
Basic Encryption
import * as Keystore from '@tevm/voltaire/crypto/Keystore';
import * as PrivateKey from '@tevm/voltaire/primitives/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
// PBKDF2 - faster, widely supported
const keystore = await Keystore.encrypt(privateKey, password, {
kdf: 'pbkdf2'
});
Pros:
- Faster (~500ms default)
- Lower memory usage
- Widely supported
Cons:
- Not memory-hard
- GPU-parallelizable
- Lower security per iteration
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
});
| Parameter | Default | Description |
|---|
scryptN | 262144 | CPU/memory cost (2^18). Higher = slower, more secure |
scryptR | 8 | Block size. Higher = more memory |
scryptP | 1 | Parallelization. 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
});
| Parameter | Default | Description |
|---|
pbkdf2C | 262144 | Iteration 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'
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