Java与Android端AES-128跨平台加解密实战:CBC与GCM模式详解
1. 项目概述与核心价值最近在做一个需要跨平台数据安全传输的项目后端用Java移动端是Android数据加密这块自然就选定了AES-128。网上资料虽然多但要么是纯Java的要么是Android里用KeyStore的真正把两端打通、能直接拿来跑通的完整Demo不多。踩了几个坑之后我把整个流程梳理了一遍从密钥生成、模式选择到两端代码的适配做成了这个实战Demo。这个Demo的价值在于它不是一个孤立的代码片段而是一个可验证的、端到端的加解密闭环。无论是刚接触安全开发的工程师还是需要在现有项目中快速集成AES功能的开发者都能直接参考避免在“Java加密的密文Android解不开”这类兼容性问题上浪费时间。AES-128作为对称加密的黄金标准在性能和安全之间取得了很好的平衡广泛应用于网络通信、本地存储、支付等场景。这个Demo将聚焦于最常用的CBC模式和GCM模式分别对应需要填充的场景和需要同时提供保密性与完整性的场景。我会详细解释为什么选这些参数比如IV初始化向量怎么生成和传递PKCS5Padding和NoPadding的区别以及Android上那些“不起眼”但至关重要的API版本差异。你会发现把加解密跑通只是第一步写出健壮、安全的代码才是真正的挑战。2. 核心设计与思路拆解2.1 为什么是AES-128在项目启动时我们面临几个选择DES、3DES、AES-128/192/256甚至国密SM4。AES-128最终胜出原因很实际DES和3DES已被认为不够安全AES-192和256虽然更安全但在移动端某些老旧设备上可能会有性能损耗且对于绝大多数业务场景128位的密钥强度已足够抵御可预见的暴力破解。AES也是行业事实标准各平台支持度最高社区资源和审计也最充分。选择它意味着更少的兼容性风险和更可控的实现成本。2.2 加密模式CBC vs GCM这是两个最核心的模式选择取决于你的需求。CBC模式需要搭配填充方案如PKCS5Padding。它的原理是每个明文块在加密前先与前一个密文块进行异或操作。这就要求第一个块需要一个初始化向量。CBC能提供机密性但本身不提供完整性校验。如果有人篡改了密文中的某一位解密时通常只会导致对应块及后续块解密错误Padding错误但无法主动发现篡改行为。它适用于文件加密、需要兼容老旧系统等场景。GCM模式这是一种认证加密模式。它在提供机密性的同时还会生成一个消息认证码用于验证密文在传输过程中是否被篡改。GCM内部基于CTR模式并行度高性能好且不需要填充。在现代网络通信如TLS 1.3和需要防篡改的数据存储中GCM是首选。它的两个关键输出是密文和认证标签。在这个Demo中我将同时实现这两种模式让你能直观地对比其使用方式和输出结果。2.3 密钥管理与安全红线绝对不要将硬编码的密钥放在客户端代码中这是移动安全的大忌。在Demo中为了演示的完整性我们会在一开始使用一个字符串生成密钥。但在真实项目中Android端应使用AndroidKeyStore系统来生成和存储密钥确保密钥材料不出现在应用进程的普通内存中。后端Java服务则应将密钥存储在安全的配置中心或硬件安全模块中。Demo会演示基础的密钥生成并会明确指出生产环境的安全升级路径。2.4 跨平台兼容性核心参数同步Java和Android尤其是较新版本的加密实现都基于相同的提供者如Bouncy Castle或Android内置的Conscrypt理论上应该兼容。但实践中导致“解密失败”的罪魁祸首往往是参数不一致。主要有三点密钥必须完全相同包括密钥字节和算法AES。模式与填充必须完全匹配如AES/CBC/PKCS5Padding。IV对于CBC/GCM必须使用加密端生成的同一个IV。IV不需要保密但必须不可预测通常随密文一起传输。我们的Demo将严格保证这些参数在两端的一致性。3. 环境准备与基础工具类3.1 项目结构这是一个简单的Maven项目后端和一个Android Studio项目移动端。为了清晰我们创建一个共享的“核心”模块定义常量和方法签名但实际实现因平台而异。这里我们先聚焦于核心的加解密工具类。3.2 共享常量定义首先我们定义一个接口或类来存放双方必须一致的参数。这能有效避免因拼写错误导致的不兼容。// 文件CryptoConstants.java public class CryptoConstants { // 加密算法/模式/填充 public static final String CBC_TRANSFORMATION AES/CBC/PKCS5Padding; public static final String GCM_TRANSFORMATION AES/GCM/NoPadding; // 密钥算法 public static final String KEY_ALGORITHM AES; // 密钥长度位 public static final int KEY_SIZE 128; // GCM模式参数认证标签长度位 public static final int GCM_TAG_LENGTH 128; // 字符编码 public static final String CHARSET UTF-8; }3.3 基础密钥生成仅用于演示如前所述生产环境绝不可用此方式。这里演示从固定字符串生成密钥确保两端密钥一致。// 文件DemoKeyGenerator.java (Java Android 通用逻辑) import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64; public class DemoKeyGenerator { /** * 生成一个随机的AES-128密钥安全推荐 */ public static SecretKey generateRandomKey() throws NoSuchAlgorithmException { KeyGenerator keyGen KeyGenerator.getInstance(CryptoConstants.KEY_ALGORITHM); keyGen.init(CryptoConstants.KEY_SIZE, new SecureRandom()); return keyGen.generateKey(); } /** * 从一个固定字符串生成密钥仅用于演示和测试确保两端一致 * param password 用于生成密钥的字符串 */ public static SecretKey getKeyFromPassword(String password) throws Exception { // 使用SHA-256对密码进行哈希并取前16字节作为128位密钥 byte[] key password.getBytes(CryptoConstants.CHARSET); java.security.MessageDigest sha java.security.MessageDigest.getInstance(SHA-256); key sha.digest(key); // 取前16字节作为AES-128密钥 byte[] aesKey new byte[16]; System.arraycopy(key, 0, aesKey, 0, 16); return new SecretKeySpec(aesKey, CryptoConstants.KEY_ALGORITHM); } /** * 将SecretKey转换为Base64字符串便于打印和临时存储 */ public static String keyToBase64(SecretKey secretKey) { return Base64.getEncoder().encodeToString(secretKey.getEncoded()); } /** * 从Base64字符串恢复SecretKey */ public static SecretKey keyFromBase64(String base64Key) { byte[] decodedKey Base64.getDecoder().decode(base64Key); return new SecretKeySpec(decodedKey, CryptoConstants.KEY_ALGORITHM); } }注意getKeyFromPassword方法安全性很弱因为它没有使用盐值和迭代次数。生产环境若需从口令派生密钥必须使用PBKDF2WithHmacSHA256或Scrypt等密钥派生函数。这里仅用于保证Demo两端密钥一致。4. Java端加解密实现详解4.1 CBC模式加解密实现CBC模式需要处理IV。IV必须是随机的并且需要和密文一起保存或传输。// 文件AesCbcUtil.java import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Base64; public class AesCbcUtil { /** * AES-CBC加密 * param plainText 明文 * param secretKey 密钥 * return 一个包含IV和密文的字符串格式为 Base64(IV):Base64(CipherText) * throws Exception */ public static String encrypt(String plainText, SecretKey secretKey) throws Exception { // 1. 获取Cipher实例 Cipher cipher Cipher.getInstance(CryptoConstants.CBC_TRANSFORMATION); // 2. 生成随机IV (16字节 for AES) byte[] iv new byte[16]; SecureRandom random new SecureRandom(); random.nextBytes(iv); IvParameterSpec ivSpec new IvParameterSpec(iv); // 3. 初始化Cipher为加密模式 cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); // 4. 执行加密 byte[] plainTextBytes plainText.getBytes(StandardCharsets.UTF_8); byte[] encryptedBytes cipher.doFinal(plainTextBytes); // 5. 将IV和密文一起编码为Base64字符串返回 // 格式IV:密文方便解密方分离 String ivBase64 Base64.getEncoder().encodeToString(iv); String encryptedTextBase64 Base64.getEncoder().encodeToString(encryptedBytes); return ivBase64 : encryptedTextBase64; } /** * AES-CBC解密 * param encryptedData 加密后的数据格式为 Base64(IV):Base64(CipherText) * param secretKey 密钥必须与加密时相同 * return 解密后的明文 * throws Exception */ public static String decrypt(String encryptedData, SecretKey secretKey) throws Exception { // 1. 分割IV和密文 String[] parts encryptedData.split(:); if (parts.length ! 2) { throw new IllegalArgumentException(Invalid encrypted data format); } byte[] iv Base64.getDecoder().decode(parts[0]); byte[] encryptedBytes Base64.getDecoder().decode(parts[1]); // 2. 获取Cipher实例 Cipher cipher Cipher.getInstance(CryptoConstants.CBC_TRANSFORMATION); // 3. 初始化Cipher为解密模式传入相同的IV IvParameterSpec ivSpec new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); // 4. 执行解密 byte[] decryptedBytes cipher.doFinal(encryptedBytes); return new String(decryptedBytes, StandardCharsets.UTF_8); } // 简单的测试方法 public static void main(String[] args) throws Exception { String password MySuperSecretPassword123; SecretKey key DemoKeyGenerator.getKeyFromPassword(password); String originalText 这是一段需要加密的敏感数据Hello AES-128!; System.out.println(原始明文: originalText); // 加密 String encryptedData encrypt(originalText, key); System.out.println(加密后数据 (IV:CipherText): encryptedData); // 解密 String decryptedText decrypt(encryptedData, key); System.out.println(解密后明文: decryptedText); System.out.println(解密是否成功: originalText.equals(decryptedText)); } }关键点解析SecureRandom用于生成密码学安全的随机IV这是必须的禁止使用固定IV或简单序列。我们将IV和密文用冒号:拼接后返回。这是一种简单的序列化方式在实际项目中你可能使用JSON或Protocol Buffers。解密时必须先分离出IV并用它初始化Cipher。如果IV错误解密出的数据将是乱码。4.2 GCM模式加解密实现GCM模式更现代使用也更简单一些因为它将IV在GCM中常称为Nonce和认证标签的处理封装得更好。// 文件AesGcmUtil.java import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Base64; public class AesGcmUtil { /** * AES-GCM加密 * param plainText 明文 * param secretKey 密钥 * return 一个包含Nonce和密文的字符串格式为 Base64(Nonce):Base64(CipherText) * throws Exception */ public static String encrypt(String plainText, SecretKey secretKey) throws Exception { // 1. 获取Cipher实例 Cipher cipher Cipher.getInstance(CryptoConstants.GCM_TRANSFORMATION); // 2. 生成随机Nonce (推荐12字节) byte[] nonce new byte[12]; SecureRandom random new SecureRandom(); random.nextBytes(nonce); // 3. 创建GCMParameterSpec指定认证标签长度 GCMParameterSpec gcmParameterSpec new GCMParameterSpec(CryptoConstants.GCM_TAG_LENGTH, nonce); // 4. 初始化Cipher为加密模式 cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec); // 5. 执行加密 byte[] plainTextBytes plainText.getBytes(StandardCharsets.UTF_8); byte[] encryptedBytes cipher.doFinal(plainTextBytes); // 6. 返回Nonce和密文 String nonceBase64 Base64.getEncoder().encodeToString(nonce); String encryptedTextBase64 Base64.getEncoder().encodeToString(encryptedBytes); return nonceBase64 : encryptedTextBase64; } /** * AES-GCM解密 * param encryptedData 加密数据格式为 Base64(Nonce):Base64(CipherTextWithTag) * param secretKey 密钥 * return 解密后的明文 * throws Exception 如果认证失败密文被篡改会抛出AEADBadTagException等异常 */ public static String decrypt(String encryptedData, SecretKey secretKey) throws Exception { // 1. 分割Nonce和密文 String[] parts encryptedData.split(:); if (parts.length ! 2) { throw new IllegalArgumentException(Invalid encrypted data format); } byte[] nonce Base64.getDecoder().decode(parts[0]); byte[] encryptedBytesWithTag Base64.getDecoder().decode(parts[1]); // 2. 获取Cipher实例 Cipher cipher Cipher.getInstance(CryptoConstants.GCM_TRANSFORMATION); // 3. 初始化Cipher为解密模式 GCMParameterSpec gcmParameterSpec new GCMParameterSpec(CryptoConstants.GCM_TAG_LENGTH, nonce); cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); // 4. 执行解密内部会验证认证标签 byte[] decryptedBytes cipher.doFinal(encryptedBytesWithTag); return new String(decryptedBytes, StandardCharsets.UTF_8); } public static void main(String[] args) throws Exception { String password MySuperSecretPassword123; SecretKey key DemoKeyGenerator.getKeyFromPassword(password); String originalText 这是一段需要GCM加密并认证的数据; System.out.println(原始明文: originalText); // 加密 String encryptedData encrypt(originalText, key); System.out.println(GCM加密后数据: encryptedData); // 解密 String decryptedText decrypt(encryptedData, key); System.out.println(GCM解密后明文: decryptedText); System.out.println(解密是否成功: originalText.equals(decryptedText)); // 模拟篡改测试认证失败 try { String[] parts encryptedData.split(:); // 篡改密文的一个字符 String tamperedCipher parts[1].substring(0, parts[1].length()-2) XX; String tamperedData parts[0] : tamperedCipher; decrypt(tamperedData, key); System.out.println(错误篡改后竟然解密成功了); } catch (Exception e) { System.out.println(正确GCM认证失败捕获异常: e.getClass().getSimpleName() - e.getMessage()); } } }关键点解析GCMParameterSpec是关键它包装了Nonce和认证标签长度。标签长度通常为128位。GCM的doFinal方法在加密时会将认证标签自动附加到密文后面在解密时会自动验证标签。如果验证失败会抛出AEADBadTagException或BadPaddingException等这是GCM提供完整性保护的核心体现。Nonce的长度可以是12字节推荐或16字节。12字节是GCM标准推荐的高效长度。5. Android端加解密实现与适配Android端的核心逻辑与Java端几乎完全一致因为都使用标准的JCE API。主要差异在于包名和导入Android支持javax.crypto。Base64编码Android SDK 26 提供了java.util.Base64但为了兼容老版本我们通常使用android.util.Base64。密钥存储演示后我们会强调使用AndroidKeyStore的重要性。运行环境需要在主线程外执行加解密操作避免阻塞UI。5.1 Android工具类实现兼容低版本我们创建一个AndroidAesUtil类内部根据模式调用不同的方法。// 文件AndroidAesUtil.kt (使用Kotlin更简洁) import android.util.Base64 import java.security.SecureRandom import javax.crypto.Cipher import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.IvParameterSpec object AndroidAesUtil { // 使用Android的Base64 private const val BASE64_FLAGS Base64.NO_WRAP /** * CBC模式加密 */ Throws(Exception::class) fun encryptCbc(plainText: String, secretKey: SecretKey): String { val cipher Cipher.getInstance(CryptoConstants.CBC_TRANSFORMATION) val iv ByteArray(16) SecureRandom().nextBytes(iv) val ivSpec IvParameterSpec(iv) cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec) val plainTextBytes plainText.toByteArray(charset(CryptoConstants.CHARSET)) val encryptedBytes cipher.doFinal(plainTextBytes) val ivBase64 Base64.encodeToString(iv, BASE64_FLAGS) val cipherTextBase64 Base64.encodeToString(encryptedBytes, BASE64_FLAGS) return $ivBase64:$cipherTextBase64 } /** * CBC模式解密 */ Throws(Exception::class) fun decryptCbc(encryptedData: String, secretKey: SecretKey): String { val parts encryptedData.split(:) if (parts.size ! 2) { throw IllegalArgumentException(Invalid encrypted data format) } val iv Base64.decode(parts[0], Base64.DEFAULT) val encryptedBytes Base64.decode(parts[1], Base64.DEFAULT) val cipher Cipher.getInstance(CryptoConstants.CBC_TRANSFORMATION) val ivSpec IvParameterSpec(iv) cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec) val decryptedBytes cipher.doFinal(encryptedBytes) return String(decryptedBytes, charset(CryptoConstants.CHARSET)) } /** * GCM模式加密 */ Throws(Exception::class) fun encryptGcm(plainText: String, secretKey: SecretKey): String { val cipher Cipher.getInstance(CryptoConstants.GCM_TRANSFORMATION) val nonce ByteArray(12) SecureRandom().nextBytes(nonce) val gcmSpec GCMParameterSpec(CryptoConstants.GCM_TAG_LENGTH, nonce) cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec) val plainTextBytes plainText.toByteArray(charset(CryptoConstants.CHARSET)) val encryptedBytes cipher.doFinal(plainTextBytes) val nonceBase64 Base64.encodeToString(nonce, BASE64_FLAGS) val cipherTextBase64 Base64.encodeToString(encryptedBytes, BASE64_FLAGS) return $nonceBase64:$cipherTextBase64 } /** * GCM模式解密 */ Throws(Exception::class) fun decryptGcm(encryptedData: String, secretKey: SecretKey): String { val parts encryptedData.split(:) if (parts.size ! 2) { throw IllegalArgumentException(Invalid encrypted data format) } val nonce Base64.decode(parts[0], Base64.DEFAULT) val encryptedBytesWithTag Base64.decode(parts[1], Base64.DEFAULT) val cipher Cipher.getInstance(CryptoConstants.GCM_TRANSFORMATION) val gcmSpec GCMParameterSpec(CryptoConstants.GCM_TAG_LENGTH, nonce) cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec) val decryptedBytes cipher.doFinal(encryptedBytesWithTag) return String(decryptedBytes, charset(CryptoConstants.CHARSET)) } }5.2 在Activity中使用及线程处理在Android中加解密是耗时操作必须在后台线程执行。// 文件MainActivity.kt 示例片段 import android.os.Bundle import android.util.Log import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.crypto.SecretKey class MainActivity : AppCompatActivity() { private lateinit var secretKey: SecretKey override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 初始化密钥演示用生产环境用AndroidKeyStore secretKey DemoKeyGenerator.getKeyFromPassword(MySuperSecretPassword123) // 测试CBC加解密 testCbc(Hello from Android CBC!) // 测试GCM加解密 testGcm(Hello from Android GCM!) } private fun testCbc(originalText: String) { GlobalScope.launch(Dispatchers.Main) { try { val encrypted withContext(Dispatchers.IO) { AndroidAesUtil.encryptCbc(originalText, secretKey) } Log.d(AES_DEMO, CBC Encrypted: $encrypted) val decrypted withContext(Dispatchers.IO) { AndroidAesUtil.decryptCbc(encrypted, secretKey) } Log.d(AES_DEMO, CBC Decrypted: $decrypted) Toast.makeText(thisMainActivity, CBC成功: $decrypted, Toast.LENGTH_SHORT).show() } catch (e: Exception) { Log.e(AES_DEMO, CBC加解密失败, e) Toast.makeText(thisMainActivity, CBC失败: ${e.message}, Toast.LENGTH_LONG).show() } } } private fun testGcm(originalText: String) { // 类似testCbc调用encryptGcm和decryptGcm // ... } }5.3 生产环境密钥安全AndroidKeyStore上面的Demo将密钥保存在内存中不安全。对于生产环境必须使用AndroidKeyStore。// 文件SecureKeyManager.kt (简化示例) import android.content.Context import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import java.security.KeyStore import javax.crypto.KeyGenerator import javax.crypto.SecretKey class SecureKeyManager(private val context: Context, private val keyAlias: String) { private val androidKeyStore KeyStore.getInstance(AndroidKeyStore).apply { load(null) } /** * 生成或获取一个受AndroidKeyStore保护的AES密钥 */ fun getOrCreateSecretKey(): SecretKey { val existingKey androidKeyStore.getEntry(keyAlias, null) as? KeyStore.SecretKeyEntry return existingKey?.secretKey ?: createSecretKey() } private fun createSecretKey(): SecretKey { val keyGenerator KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore ) val keySpec KeyGenParameterSpec.Builder( keyAlias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ) .setBlockModes(KeyProperties.BLOCK_MODE_CBC, KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) // PKCS7对应PKCS5 .setKeySize(128) .setRandomizedEncryptionRequired(true) // 强制使用随机IV/Nonce .build() keyGenerator.init(keySpec) return keyGenerator.generateKey() } /** * 注意AndroidKeyStore中的密钥无法直接导出密钥材料。 * 加解密操作由系统在安全环境中完成我们只拿到Cipher对象进行操作。 * 因此这种密钥无法用于与Java后端共享。如需共享需使用非对称加密RSA来传递对称密钥。 */ }使用SecureKeyManager后你的加解密代码不再需要传入SecretKey对象而是由系统管理的Cipher直接完成。// 使用AndroidKeyStore进行加密 fun encryptWithKeyStore(plainText: String): String { val keyManager SecureKeyManager(context, my_app_key) val cipher Cipher.getInstance(CryptoConstants.GCM_TRANSFORMATION) val secretKey keyManager.getOrCreateSecretKey() cipher.init(Cipher.ENCRYPT_MODE, secretKey) // ... 后续操作与之前类似但密钥更安全 }6. 端到端测试与验证这是最关键的一步验证Java端加密的数据Android端能正确解密反之亦然。6.1 测试用例设计我们设计一个简单的测试流程Java端加密Android端解密在Java项目中运行加密函数将输出的字符串IV:密文或Nonce:密文通过网络请求、二维码、手动复制等方式发送到Android App由App解密并显示。Android端加密Java端解密在App中加密一段文本将结果发送到Java后端服务后端解密并验证。6.2 验证脚本示例Java端可以写一个简单的Java测试类模拟两端交互。// 文件CrossPlatformTest.java public class CrossPlatformTest { public static void main(String[] args) throws Exception { // 使用相同的密码生成相同的密钥 String password TestPassword123; SecretKey key DemoKeyGenerator.getKeyFromPassword(password); String keyBase64 DemoKeyGenerator.keyToBase64(key); System.out.println(共享密钥(Base64): keyBase64); System.out.println( CBC模式测试 ); String originalTextCbc 跨平台CBC测试消息 System.currentTimeMillis(); String encryptedByJavaCbc AesCbcUtil.encrypt(originalTextCbc, key); System.out.println(Java加密结果: encryptedByJavaCbc); // 假设这是从Android端接收到的密文 String encryptedByAndroidCbc encryptedByJavaCbc; // 实际应从Android获取 String decryptedByJavaCbc AesCbcUtil.decrypt(encryptedByAndroidCbc, key); System.out.println(Java解密Android数据结果: decryptedByJavaCbc); System.out.println(CBC一致性: originalTextCbc.equals(decryptedByJavaCbc)); System.out.println(\n GCM模式测试 ); String originalTextGcm 跨平台GCM测试消息 System.currentTimeMillis(); String encryptedByJavaGcm AesGcmUtil.encrypt(originalTextGcm, key); System.out.println(Java加密结果: encryptedByJavaGcm); // 模拟Android端解密 SecretKey keyFromBase64 DemoKeyGenerator.keyFromBase64(keyBase64); String decryptedByAndroidGcm AesGcmUtil.decrypt(encryptedByJavaGcm, keyFromBase64); System.out.println(Android解密Java数据结果: decryptedByAndroidGcm); System.out.println(GCM一致性: originalTextGcm.equals(decryptedByAndroidGcm)); } }在Android端你需要一个界面输入Java端产生的加密字符串然后调用对应的解密方法。确保两端使用的CryptoConstants完全一致特别是TRANSFORMATION字符串和GCM_TAG_LENGTH。7. 常见问题、排查技巧与性能优化7.1 典型错误与解决方案错误现象可能原因排查步骤与解决方案javax.crypto.BadPaddingException: Given final block not properly padded1. 密钥不一致。2. IV/Nonce不一致或未传递。3. 加密模式/填充方案不匹配。4. 密文在传输中被损坏或编码错误。1.核对密钥确保两端用于加解密的密钥字节完全相同。使用Base64打印并对比密钥。2.核对IV/Nonce确保解密时使用的IV/Nonce与加密时生成的完全一致。检查拼接和分割逻辑。3.核对算法字符串确保Cipher.getInstance()的参数字符串完全一致包括斜杠和大小写。4.检查编码确保Base64编码/解码方式一致标准Base64无换行。java.security.InvalidKeyException1. 密钥长度不符合算法要求。2. 密钥算法错误如用DES的密钥给AES用。3. Android上可能缺少相应算法的Provider。1. 确认密钥是SecretKeySpec且算法为“AES”。2. 在Android上如果使用AndroidKeyStore确保KeyGenParameterSpec中设置的KEY_ALGORITHM和BLOCK_MODE支持你选择的转换模式。javax.crypto.AEADBadTagException(GCM模式)1. 认证失败密文被篡改。2. Nonce不一致。3. 认证标签长度不匹配。1. 这是GCM的正常安全特性说明数据完整性被破坏。检查传输通道。2. 确保Nonce正确传递。3. 确保GCMParameterSpec中指定的标签长度与加密时一致通常128位。Android端解密正常但Java端解密乱码字符编码不一致。确保两端在将字符串转换为字节数组(getBytes)和将字节数组转回字符串(new String)时使用相同的字符编码如UTF-8。java.security.InvalidAlgorithmParameterException: GCMParameterSpec expected IV of length...GCM的Nonce长度不符合要求。GCM的Nonce推荐长度为12字节。确保你生成和传递的是12字节的随机数。7.2 性能与内存注意事项避免在主线程操作加解密尤其是处理大文件时是CPU密集型操作。在Android上务必在后台线程如Dispatchers.IO执行否则会导致ANR。流式处理大文件对于大文件不要一次性读取到内存中加密。应使用CipherInputStream和CipherOutputStream进行流式加密解密。// Java/Android 流式加密示例 try (FileInputStream fis new FileInputStream(inputFile); FileOutputStream fos new FileOutputStream(outputFile); CipherOutputStream cos new CipherOutputStream(fos, cipher)) { byte[] buffer new byte[8192]; int bytesRead; while ((bytesRead fis.read(buffer)) ! -1) { cos.write(buffer, 0, bytesRead); } }密钥生成开销KeyGenerator.getInstance().generateKey()和PBKDF2等密钥派生函数比较耗时应避免在关键路径上频繁调用。生成或获取密钥的操作应提前完成并缓存。7.3 安全强化建议密钥管理是核心服务器端使用专业的密钥管理服务或硬件安全模块。Android端对于本地加密如加密本地数据库优先使用AndroidKeyStore。对于需要与服务器共享密钥的场景考虑使用非对称加密RSA/ECC在安全通道内交换对称密钥。IV/Nonce必须随机且唯一重复使用相同的IV/Nonce与相同的密钥在CBC和GCM模式下都会严重削弱安全性。务必使用密码学安全的随机数生成器SecureRandom。优先使用GCM模式在新的项目中除非有明确的兼容性要求否则优先选择GCM模式。它提供了保密性和完整性且性能通常更好。及时更新加密库关注安全公告及时更新Java运行环境和Android SDK以获取最新的安全补丁。这个Demo提供了一个坚实且可运行的基础。将它集成到你的项目中时请务必根据上述“安全强化建议”升级密钥管理策略并根据实际数据量考虑性能优化。加密无小事细节决定安全性。