Skip to main content

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 TypeEntropyTime to Crack (scrypt N=262144)
“password”~20 bitsInstant
”correcthorse”~40 bitsHours
”correct-horse-battery”~60 bitsYears
Random 16 chars~80 bitsCenturies
Random 24 chars~120 bitsHeat 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 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 CaseKDFParametersTimeSecurity
TestingScryptN=1024~50msLow
MobileScryptN=16384~200msMedium
DesktopScryptN=262144~3sHigh
Cold storageScryptN=1048576~15sVery High
LegacyPBKDF2c=1000000~2sMedium

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:
  • Using strong passwords (16+ chars, high entropy)
  • Using scrypt KDF (not PBKDF2) when possible
  • KDF parameters appropriate for use case (N >= 16384)
  • Keystore files encrypted at rest (if stored)
  • Private keys cleared from memory after use
  • Address verification after decryption
  • Generic error messages to users
  • No keystores in version control
  • No keystores on unencrypted cloud storage
  • Backup procedures documented and tested

Compliance

Standards Alignment

  • Web3 Secret Storage v3: Full compliance
  • NIST SP 800-132: PBKDF2 usage follows recommendations
  • OWASP: Password hashing guidelines followed

Known Limitations

  1. IV not in MAC: Corrupted IV produces wrong output without error
  2. No key stretching metadata: Can’t verify KDF parameters were followed
  3. Password in memory: Brief exposure during KDF computation

Hardware Wallet Alternative

For high-value keys, consider hardware wallets instead of keystores:
FeatureKeystoreHardware Wallet
Key exposureIn memory during useNever leaves device
Password attackVulnerablePIN with attempt limits
Malware protectionLimitedStrong
CostFree$50-200
BackupFile + passwordRecovery phrase
Use keystores for convenience (hot wallets) and hardware wallets for security (cold storage).

References