生成安全哈希检测Android SQLite加密数据篡改的方案咨询
Hey there! Let's dive into your Android app's integrity check needs—since you're already using AES-256 with PBKDF2-HMACSHA256 for key derivation, you're starting from a strong security baseline.
First, let's clarify the core goals here: you need to detect both malicious tampering (someone intentionally altering data) and accidental corruption (storage glitches, etc.). The key distinction between a good solution and a mediocre one is whether it can defend against active attackers—not just passive errors.
Let's break down the two common hash-based approaches you're likely considering, and which is better:
Scenario 1: Hash the plaintext, then encrypt it alongside the data
This approach involves generating a hash (like SHA-256) of your raw sensitive data, then encrypting both the plaintext and its hash together into a single ciphertext. When you retrieve the data, you decrypt it, extract the stored hash, recompute the hash of the decrypted plaintext, and compare.
Pros: Simple to implement, works for detecting accidental corruption.
Cons:
- You have to decrypt the entire blob before you can verify integrity—this means processing potentially malicious/corrupted data, which exposes you to edge-case attacks (like padding oracle attacks if you're using AES-CBC).
- While it stops accidental corruption, it's weak against active attackers if you're using a plain hash (not HMAC). Even if you encrypt the hash, an attacker could tamper with the ciphertext, and you'd only know after decrypting (which is too late in some cases).
Scenario 2: Use HMAC on the encrypted ciphertext + IV (with a separate derived key)
This approach uses a keyed hash (HMAC) instead of a plain hash, computed over the ciphertext and the AES initialization vector (IV—you are using a random IV for every encryption, right? You should be!). The HMAC value is stored alongside the IV and ciphertext. When retrieving data, you first recompute the HMAC and compare it to the stored value—only if they match do you proceed to decrypt.
Pros:
- Defends against both tampering and corruption: HMAC requires a secret key, so attackers can't forge a valid HMAC for tampered data.
- Secure by design: You never decrypt data that fails the integrity check, eliminating exposure to attacks that target decryption logic.
- Efficient: HMAC is fast to compute, and you avoid the overhead of decrypting bad data upfront.
Bonus: Even better—switch to AES-GCM (AEAD mode)
If you can adjust your existing AES implementation, using an Authenticated Encryption with Associated Data (AEAD) mode like AES-GCM is the gold standard here. AEAD combines encryption and integrity verification into a single step, so you don't need a separate HMAC or hash.
When you encrypt with AES-GCM, it generates an authentication tag alongside the ciphertext. During decryption, the cipher automatically validates this tag—if it's invalid (due to tampering or corruption), it throws an AEADBadTagException, and you know to reject the data immediately.
Optimal Implementation Steps
Here's a concrete, secure workflow tailored to your setup:
Derive separate keys for encryption and integrity
- Use PBKDF2-HMACSHA256 to generate a master key from the user's password (use a random salt, store it with your data, and set iterations to at least 100,000 for modern security).
- Use HKDF (HMAC-based Key Derivation Function) to split the master key into two distinct sub-keys: one for AES encryption, one for HMAC (or skip this if using AES-GCM, since it only needs one key).
For AES-GCM (preferred)
- Generate a random 12-byte IV (the recommended size for GCM) for each encryption.
- Encrypt your plaintext with AES-GCM—this produces a ciphertext plus an authentication tag (some implementations append the tag to the ciphertext, others let you retrieve it separately).
- Store the IV and ciphertext+tag in SQLite.
- When decrypting, initialize the cipher with the IV and key—if the tag is invalid, it throws an exception, and you can discard the data.
Example Kotlin snippet for AES-GCM decryption:
fun decryptData(encryptionKey: SecretKey, iv: ByteArray, ciphertext: ByteArray): ByteArray? { return try { val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.DECRYPT_MODE, encryptionKey, GCMParameterSpec(128, iv)) cipher.doFinal(ciphertext) } catch (e: AEADBadTagException) { // Data is tampered or corrupted—return null or handle error null } }If you can't switch to AEAD (stick with AES-CBC + HMAC)
- Generate a random IV for each encryption, encrypt the plaintext with AES-CBC.
- Compute HMAC-SHA256 over the IV + ciphertext using your dedicated HMAC key.
- Store IV, ciphertext, and HMAC value in SQLite.
- When retrieving, recompute the HMAC first—use
MessageDigest.isEqual()to compare (never use==for byte arrays!)—only proceed to decrypt if the HMAC matches.
Final Notes
- Always use secure random number generators (like
SecureRandom.getInstanceStrong()) for salts and IVs—never hardcode these values. - Avoid plain hashes (SHA-256 without a key) for integrity checks—they don't defend against active attackers.
- If you're storing large datasets, consider computing HMACs per record instead of over the entire database to make verification faster.
内容的提问来源于stack exchange,提问作者dFrancisco

