Overview
Keystore security relies on password strength, KDF parameters, and proper handling. Understanding the security model helps make informed decisions about parameter selection and usage patterns.
Security Properties
What Keystore Protects
- Private key confidentiality: Key cannot be recovered without password
- Password verification: Wrong passwords are detected via MAC
- Data integrity: Modifications to ciphertext are detected
What Keystore Does NOT Protect
- Weak passwords: Low-entropy passwords can be brute-forced
- Memory attacks: Key exists in plaintext in memory during use
- Side-channel attacks: Timing, power analysis (mostly mitigated)
- Keyloggers/malware: Password can be captured during entry
Password Security
Password Strength Requirements
The keystore is only as secure as the password:
| Password Type | Entropy | Time to Crack (scrypt N=262144) |
|---|
| “password” | ~20 bits | Instant |
| ”correcthorse” | ~40 bits | Hours |
| ”correct-horse-battery” | ~60 bits | Years |
| Random 16 chars | ~80 bits | Centuries |
| Random 24 chars | ~120 bits | Heat death of universe |
Recommendations:
- Minimum 16 characters
- Use passphrase (4+ random words) or random characters
- Include mixed case, numbers, symbols
- Never reuse passwords across keystores
Password Attacks
Dictionary Attack:
Attacker tries common passwords:
password, 123456, qwerty, ...
Mitigation: Use random passwords, avoid dictionary words.
Brute Force Attack:
Attacker tries all combinations:
a, b, c, ..., aa, ab, ac, ...
Mitigation: Use long passwords (16+ chars).
Rainbow Table Attack:
Attacker uses precomputed hashes
Mitigation: Salt prevents this (built into keystore).
KDF Security
Scrypt (Recommended)
Scrypt is memory-hard, making it resistant to parallel attacks:
// Default parameters
const keystore = await Keystore.encrypt(privateKey, password, {
kdf: 'scrypt',
scryptN: 262144, // 2^18 - CPU/memory cost
scryptR: 8, // Block size
scryptP: 1 // Parallelization
});
Security properties:
- Memory-hard: Requires ~256 MB RAM per attempt
- GPU-resistant: Memory bandwidth limits parallelization
- ASIC-resistant: Hard to build specialized hardware
Memory requirement: 128 * N * r * p bytes
- Default:
128 * 262144 * 8 * 1 = 256 MB
PBKDF2 (Less Secure)
PBKDF2 is not memory-hard, making it vulnerable to parallel attacks:
const keystore = await Keystore.encrypt(privateKey, password, {
kdf: 'pbkdf2',
pbkdf2C: 262144 // Iterations
});
Security concerns:
- GPU-parallelizable: Attackers can try millions of passwords/second
- ASIC-parallelizable: Custom hardware can be built
- Lower security per iteration: Compared to scrypt
Use PBKDF2 only when scrypt is unavailable or too slow. Increase iterations (1M+) for better security.
KDF Parameter Guidelines
| Use Case | KDF | Parameters | Time | Security |
|---|
| Testing | Scrypt | N=1024 | ~50ms | Low |
| Mobile | Scrypt | N=16384 | ~200ms | Medium |
| Desktop | Scrypt | N=262144 | ~3s | High |
| Cold storage | Scrypt | N=1048576 | ~15s | Very High |
| Legacy | PBKDF2 | c=1000000 | ~2s | Medium |
Attack Scenarios
Stolen Keystore File
Scenario: Attacker obtains keystore JSON file.
Attack: Offline password cracking
For each candidate password:
1. Derive key using KDF
2. Compute MAC
3. Compare with stored MAC
Defense:
- Strong password (16+ chars, high entropy)
- High KDF parameters (N=262144+)
- Don’t store keystores on shared/cloud storage without additional encryption
Timing Attack on MAC
Scenario: Attacker measures time to verify passwords.
Attack: Learn partial MAC by timing differences
password1: 0.100s (first byte wrong)
password2: 0.101s (first byte correct)
Defense: Constant-time MAC comparison (built-in)
// All comparisons take the same time
function constantTimeEqual(a, b) {
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i]; // No early exit
}
return result === 0;
}
Memory Dump
Scenario: Attacker dumps process memory while wallet is unlocked.
Attack: Find private key in memory
Defense:
- Clear private key from memory when done
- Use hardware wallets for high-value keys
- Minimize time wallet is unlocked
// Best effort memory clearing (not guaranteed in JS)
function secureUse(keystore, password, callback) {
let privateKey;
try {
privateKey = Keystore.decrypt(keystore, password);
callback(privateKey);
} finally {
if (privateKey) {
privateKey.fill(0); // Zero out memory
}
}
}
IV Corruption Attack
Scenario: Attacker modifies IV without detection.
Attack: Causes wrong decryption output
MAC = keccak256(macKey || ciphertext)
IV is NOT included in MAC!
Defense: Always verify decrypted key produces expected address
function safeDecrypt(keystore, password, expectedAddress) {
const privateKey = Keystore.decrypt(keystore, password);
const derivedAddress = deriveAddress(privateKey);
if (derivedAddress !== expectedAddress) {
privateKey.fill(0); // Clear
throw new Error('Keystore corrupted: address mismatch');
}
return privateKey;
}
Best Practices
Password Management
// DO: Use high-entropy passwords
const password = generateSecurePassword(24); // Random 24 chars
// DON'T: Use weak passwords
const password = 'password123'; // Crackable in seconds
KDF Parameter Selection
// DO: Use appropriate parameters for use case
const keystore = await Keystore.encrypt(privateKey, password, {
kdf: 'scrypt',
scryptN: 262144 // Production default
});
// DON'T: Use weak parameters in production
const keystore = await Keystore.encrypt(privateKey, password, {
scryptN: 1024 // Only for testing!
});
Keystore Storage
// DO: Encrypt keystore file at rest
await encryptFile(keystoreJson, filePassword);
// DO: Use secure storage APIs
await SecureStore.setItemAsync('keystore', keystoreJson);
// DON'T: Store in plaintext on cloud storage
await cloudStorage.upload('keystore.json', keystoreJson);
Keystore Handling
// DO: Clear sensitive data
const privateKey = Keystore.decrypt(keystore, password);
try {
// Use private key
} finally {
privateKey.fill(0); // Clear
}
// DON'T: Leave private key in memory
const privateKey = Keystore.decrypt(keystore, password);
// privateKey sits in memory indefinitely
Error Handling
// DO: Generic error messages to users
catch (error) {
console.log('Unable to unlock wallet'); // Don't reveal specifics
}
// DON'T: Reveal attack surface
catch (error) {
console.log(error.message); // "Invalid MAC" reveals timing info
}
Security Checklist
Before deploying keystore encryption:
Compliance
Standards Alignment
- Web3 Secret Storage v3: Full compliance
- NIST SP 800-132: PBKDF2 usage follows recommendations
- OWASP: Password hashing guidelines followed
Known Limitations
- IV not in MAC: Corrupted IV produces wrong output without error
- No key stretching metadata: Can’t verify KDF parameters were followed
- Password in memory: Brief exposure during KDF computation
Hardware Wallet Alternative
For high-value keys, consider hardware wallets instead of keystores:
| Feature | Keystore | Hardware Wallet |
|---|
| Key exposure | In memory during use | Never leaves device |
| Password attack | Vulnerable | PIN with attempt limits |
| Malware protection | Limited | Strong |
| Cost | Free | $50-200 |
| Backup | File + password | Recovery phrase |
Use keystores for convenience (hot wallets) and hardware wallets for security (cold storage).
References