Suggestion to improve upon all concerns, making sure each and every crack attempt requires: 1) iterated key strengthening 2) AES decryption 3) EC point multiplication 4) RIPEMD160+SHA256 hashing:
Master keys are stored using a db record:
- KEY = ("mkey", id)
- VAL = (salt, iterations, AES(key+iv=KeyDeriveSHA512(passphrase_id, salt, iterations), data=masterkey))
Wallet keys are stored using a db record:
- KEY = ("ekey", versionByte_n, RIPEMD160(SHA256(pubkey_n)))
- VAL = AES(key=masterkey, iv=RIPEMD160(SHA256(pubkey_n)), data=privkey_n)
Both legitimate access and crack attempts need to (for each mkey entry, until a match is found):
- perform iterated key derivation from the passphase and the mkey's salt, to find key/iv for the masterkey
- Decrypt an ekey's data using it
- Do EC point multiplication on the decrypted privkey to find the pubkey
- Hash the pubkey twice
- Compare this hash with the ekey's KEY
Depending on which encryption algorithm is used, RIPEM160(SHA256(pubkey_n)) may not have enough bits to use as IV, but I'm not sure to what extent that is an issue. Alternatively, a much shorter identifier-based ekey KEY can be used, making it even harder to verify a passphrase is correct (you'd need to go looking for transactions matching the given address and so on... this would be so fuzzy that some checksum-based verification mechanism would need to be put in place, making cracking a bit easier again as well). Another alternative is adding a additional nonce to ekey's VAL, used as IV.
The reason for "versionByte_n + RIPEMD160(SHA256(pubkey_n))" in the ekey's KEY, is that it matches addresses, and could be used in the future when the entire wallet isn't loaded from disk anymore. This is probably not really an issue now.
EDIT: as public keys are stored unhashed in several places in the wallet file anyway (txouts, keypools, ...), an attacker does not need to always do the RIPEMD160(SHA256(pubkey)) operation to verify correctness.