杰瑞科技汇

AES在Java Android中如何实现加密解密?

Of course! Here is a comprehensive guide to using AES (Advanced Encryption Standard) in a Java Android application.

AES在Java Android中如何实现加密解密?-图1
(图片来源网络,侵删)

This guide will cover:

  1. Why AES is the standard choice.
  2. The two main modes of operation: Cipher (for general data) and Keystore (for keys).
  3. A complete, production-ready example of AES/GCM for encrypting and decrypting strings.
  4. Best practices and critical security considerations.

Why AES?

AES is a symmetric encryption algorithm, meaning the same key is used for both encryption and decryption. It is the current global standard for encrypting data and is considered secure when implemented correctly.

For Android development, you have two primary ways to handle AES:

Method Description Pros Cons Best For
javax.crypto.Cipher The standard Java API for encryption. You manage the encryption keys yourself. - Full control over the algorithm and key.
- Works on any Java platform.
- You are responsible for key management. This is very hard to do securely.
- Risk of accidentally using insecure modes (like ECB).
General-purpose encryption where you need to exchange data with a non-Android system (like a backend server) and can manage the key securely (e.g., via a Key Management Service).
AndroidKeyStore An Android-specific API that stores encryption keys securely within the device's hardware-backed keystore. - Keys are never exposed to your app's memory or bytecode.
- Keys can be bound to the device, user, or app.
- Can enforce hardware-backed security (if available).
- Automatically handles key generation and storage.
- Android-specific (not portable to other Java environments).
- Slightly more setup code.
Almost all cases on Android. Storing user data, securing local databases, authentication tokens, etc. This is the recommended approach for most Android applications.

The Recommended Approach: AndroidKeyStore with AES-GCM

We will focus on the best practice: using the AndroidKeyStore to generate and store a key, and using the AES/GCM mode for encryption.

AES在Java Android中如何实现加密解密?-图2
(图片来源网络,侵删)

Why AES/GCM?

  • Confidentiality: AES encrypts the data.
  • Authenticity & Integrity: GCM (Galois/Counter Mode) provides a built-in authentication tag. This tag verifies that the data has not been tampered with. This is a critical security feature that older modes like CBC lack.
  • Nonce: GCM uses a "nonce" (Number used once) to ensure that encrypting the same plaintext with the same key produces a different ciphertext every time. You must never reuse a nonce with the same key. The AndroidKeyStore helps manage this.

Complete Example: AES-GCM with AndroidKeyStore

This example will show you how to:

  1. Generate a secret key and store it in the AndroidKeyStore.
  2. Encrypt a String into a format that can be safely stored (e.g., in SharedPreferences or a database).
  3. Decrypt the data back to the original String.

Step 1: Add Dependencies

In your build.gradle.kts (or build.gradle) file for the app module, make sure you have the necessary dependencies. kotlinx-coroutines-android is recommended for asynchronous operations.

// build.gradle.kts
dependencies {
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    // For coroutines, to handle encryption/decryption on a background thread
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}

Step 2: Add Permissions to AndroidManifest.xml

You need the INTERNET permission to use the Bouncy Castle provider, which is the most reliable way to get AES/GCM support on older Android versions. For Android 10 (API 29) and above, this is less critical as GCM is built-in, but it's good practice for compatibility.

AES在Java Android中如何实现加密解密?-图3
(图片来源网络,侵删)
<!-- AndroidManifest.xml -->
<manifest ...>
    <uses-permission android:name="android.permission.INTERNET" />
    <application ...>
        ...
    </application>
</manifest>

Step 3: Create the AesGcmUtils Class

This class will encapsulate all the logic for key generation, encryption, and decryption.

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.nio.charset.StandardCharsets
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
object AesGcmUtils {
    // The alias for the key in the Android Keystore
    private const val KEY_ALIAS = "my_aes_key"
    // The standard transformation string for AES/GCM
    private const val TRANSFORMATION = "AES/GCM/NoPadding"
    // The length of the authentication tag in bits
    private const val TAG_LENGTH_BIT = 128
    // The length of the nonce in bytes
    private const val NONCE_LENGTH_BYTE = 12
    /**
     * Generates a new AES key in the Android Keystore if it doesn't already exist.
     */
    private fun getOrCreateKey(): SecretKey {
        val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
        if (!keyStore.containsAlias(KEY_ALIAS)) {
            KeyGenerator.getInstance(
                KeyProperties.KEY_ALGORITHM_AES,
                "AndroidKeyStore"
            ).apply {
                init(
                    KeyGenParameterSpec.Builder(
                        KEY_ALIAS,
                        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
                    )
                        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                        .setRandomizedEncryptionRequired(true) // Each encryption must use a different IV
                        .build()
                )
                generateKey()
            }
        }
        return keyStore.getKey(KEY_ALIAS, null) as SecretKey
    }
    /**
     * Encrypts a plaintext string.
     * @return A Base64 encoded string containing the IV and the ciphertext.
     */
    suspend fun encrypt(plaintext: String): String = withContext(Dispatchers.IO) {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
        val iv = cipher.iv // The IV is generated automatically by the cipher
        val ciphertext = cipher.doFinal(plaintext.toByteArray(StandardCharsets.UTF_8))
        // Combine IV and ciphertext into a single Base64 string
        // Format: Base64(IV) + ":" + Base64(ciphertext)
        val ivString = Base64.encodeToString(iv, Base64.NO_WRAP)
        val ciphertextString = Base64.encodeToString(ciphertext, Base64.NO_WRAP)
        "$ivString:$ciphertextString"
    }
    /**
     * Decrypts a Base64 encoded string that was encrypted by this class.
     * @return The original plaintext string.
     */
    suspend fun decrypt(encryptedData: String): String = withContext(Dispatchers.IO) {
        val (ivString, ciphertextString) = encryptedData.split(":")
        val iv = Base64.decode(ivString, Base64.NO_WRAP)
        val ciphertext = Base64.decode(ciphertextString, Base64.NO_WRAP)
        val cipher = Cipher.getInstance(TRANSFORMATION)
        // The GCMParameterSpec is required for decryption
        val spec = GCMParameterSpec(TAG_LENGTH_BIT, iv)
        cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec)
        val plaintext = cipher.doFinal(ciphertext)
        String(plaintext, StandardCharsets.UTF_8)
    }
}

Step 4: Use the Utility in an Activity

Now, let's use this AesGcmUtils in an Android Activity.

// MainActivity.kt
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
    private lateinit var editText: EditText
    private lateinit var encryptButton: Button
    private lateinit var decryptButton: Button
    private lateinit var resultTextView: TextView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        editText = findViewById(R.id.editText)
        encryptButton = findViewById(R.id.encryptButton)
        decryptButton = findViewById(R.id.decryptButton)
        resultTextView = findViewById(R.id.resultTextView)
        encryptButton.setOnClickListener {
            val plaintext = editText.text.toString()
            if (plaintext.isNotEmpty()) {
                // Launch a coroutine to perform encryption on a background thread
                CoroutineScope(Dispatchers.Main).launch {
                    try {
                        val encrypted = AesGcmUtils.encrypt(plaintext)
                        resultTextView.text = "Encrypted:\n$encrypted"
                        Toast.makeText(this@MainActivity, "Encryption successful!", Toast.LENGTH_SHORT).show()
                    } catch (e: Exception) {
                        resultTextView.text = "Error: ${e.message}"
                        Toast.makeText(this@MainActivity, "Encryption failed!", Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }
        decryptButton.setOnClickListener {
            val encryptedText = resultTextView.text.toString().removePrefix("Encrypted:\n")
            if (encryptedText.isNotEmpty()) {
                // Launch a coroutine to perform decryption on a background thread
                CoroutineScope(Dispatchers.Main).launch {
                    try {
                        val decrypted = AesGcmUtils.decrypt(encryptedText)
                        resultTextView.text = "Decrypted:\n$decrypted"
                        Toast.makeText(this@MainActivity, "Decryption successful!", Toast.LENGTH_SHORT).show()
                    } catch (e: Exception) {
                        // This will happen if the data is tampered with or the key is wrong
                        resultTextView.text = "Error: ${e.message}"
                        Toast.makeText(this@MainActivity, "Decryption failed!", Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }
    }
}

Step 5: Create the Layout (activity_main.xml)

<!-- res/layout/activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="16dp"
    tools:context=".MainActivity">
    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter text to encrypt"
        android:inputType="textMultiLine"
        android:minHeight="100dp" />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="16dp">
        <Button
            android:id="@+id/encryptButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Encrypt" />
        <Button
            android:id="@+id/decryptButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Decrypt" />
    </LinearLayout>
    <TextView
        android:id="@+id/resultTextView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:padding="12dp"
        android:background="@android:color/darker_gray"
        android:textColor="@android:color/white"
        android:gravity="start"
        android:text="Result will be shown here..."
        tools:text="Encrypted: ...\n\nDecrypted: ..." />
</LinearLayout>

Best Practices and Critical Considerations

  1. Never Hardcode Keys: The AndroidKeyStore is designed to solve this. If you were using Cipher without Keystore, you would never put a key in your source code. It would be easily extracted.

  2. Run on a Background Thread: Encryption and decryption are CPU-intensive and can block the UI thread, causing your app to freeze. Always perform these operations in a background thread, using Kotlin Coroutines, AsyncTask (deprecated), or an ExecutorService. The example above uses Coroutines.

  3. Handle Authentication: You can bind your keys to the user's lock screen. If the user unlocks their device with a PIN, password, or fingerprint, your app can use the key. If the device is rebooted, the key remains inaccessible until the user unlocks it again. This is configured in KeyGenParameterSpec.Builder:

    .setUserAuthenticationRequired(true)
    .setUserAuthenticationValidityDurationSeconds(30) // Key is valid for 30s after unlock
  4. Key Management is Hard: Using AndroidKeyStore is the single most important thing you can do right. It abstracts away the complexities of storing keys securely in the device's Trusted Execution Environment (TEE) or Secure Element.

  5. Error Handling: Pay close attention to exceptions. A BadPaddingException or AEADBadTagException during decryption is a critical security signal. It almost always means the ciphertext has been tampered with. You should treat this as a security incident and not just show an error to the user.

  6. IV/Nonce Management: In our example, the Cipher generates the IV/nonce for us. This is the recommended approach. The IV is then stored alongside the ciphertext. Never reuse a nonce with the same key. The setRandomizedEncryptionRequired(true) flag in the KeyGenParameterSpec enforces this at the API level.

分享:
扫描分享到社交APP
上一篇
下一篇