尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

Java与Android端AES-128跨平台加解密实战:CBC与GCM模式详解

Java与Android端AES-128跨平台加解密实战:CBC与GCM模式详解
📅 发布时间:2026/7/26 9:15:13

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),理论上应该兼容。但实践中,导致“解密失败”的罪魁祸首往往是参数不一致。主要有三点:

  1. 密钥:必须完全相同,包括密钥字节和算法(AES)。
  2. 模式与填充:必须完全匹配,如AES/CBC/PKCS5Padding。
  3. 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)); } }

关键点解析:

  1. SecureRandom用于生成密码学安全的随机IV,这是必须的,禁止使用固定IV或简单序列。
  2. 我们将IV和密文用冒号:拼接后返回。这是一种简单的序列化方式,在实际项目中,你可能使用JSON或Protocol Buffers。
  3. 解密时,必须先分离出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()); } } }

关键点解析:

  1. GCMParameterSpec是关键,它包装了Nonce和认证标签长度。标签长度通常为128位。
  2. GCM的doFinal方法在加密时会将认证标签自动附加到密文后面;在解密时会自动验证标签。如果验证失败,会抛出AEADBadTagException(或BadPaddingException等),这是GCM提供完整性保护的核心体现。
  3. Nonce的长度可以是12字节(推荐)或16字节。12字节是GCM标准推荐的高效长度。

5. Android端加解密实现与适配

Android端的核心逻辑与Java端几乎完全一致,因为都使用标准的JCE API。主要差异在于:

  1. 包名和导入:Android支持javax.crypto。
  2. Base64编码:Android SDK 26+ 提供了java.util.Base64,但为了兼容老版本,我们通常使用android.util.Base64。
  3. 密钥存储:演示后我们会强调使用AndroidKeyStore的重要性。
  4. 运行环境:需要在主线程外执行加解密操作,避免阻塞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(this@MainActivity, "CBC成功: $decrypted", Toast.LENGTH_SHORT).show() } catch (e: Exception) { Log.e("AES_DEMO", "CBC加解密失败", e) Toast.makeText(this@MainActivity, "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 测试用例设计

我们设计一个简单的测试流程:

  1. Java端加密,Android端解密:在Java项目中运行加密函数,将输出的字符串(IV:密文或Nonce:密文)通过网络请求、二维码、手动复制等方式发送到Android App,由App解密并显示。
  2. Android端加密,Java端解密:在App中加密一段文本,将结果发送到Java后端服务,后端解密并验证。

6.2 验证脚本示例(Java端)

可以写一个简单的Java测试类,模拟两端交互。

// 文件:CrossPlatformTest.java public class CrossPlatformTest { public static void main(String[] args) throws Exception { // 使用相同的密码生成相同的密钥 String password = "TestPassword@123"; 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 性能与内存注意事项

  1. 避免在主线程操作:加解密,尤其是处理大文件时,是CPU密集型操作。在Android上务必在后台线程(如Dispatchers.IO)执行,否则会导致ANR。
  2. 流式处理大文件:对于大文件,不要一次性读取到内存中加密。应使用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); } }
  3. 密钥生成开销:KeyGenerator.getInstance().generateKey()和PBKDF2等密钥派生函数比较耗时,应避免在关键路径上频繁调用。生成或获取密钥的操作应提前完成并缓存。

7.3 安全强化建议

  1. 密钥管理是核心:
    • 服务器端:使用专业的密钥管理服务或硬件安全模块。
    • Android端:对于本地加密(如加密本地数据库),优先使用AndroidKeyStore。对于需要与服务器共享密钥的场景,考虑使用非对称加密(RSA/ECC)在安全通道内交换对称密钥。
  2. IV/Nonce必须随机且唯一:重复使用相同的IV/Nonce与相同的密钥,在CBC和GCM模式下都会严重削弱安全性。务必使用密码学安全的随机数生成器(SecureRandom)。
  3. 优先使用GCM模式:在新的项目中,除非有明确的兼容性要求,否则优先选择GCM模式。它提供了保密性和完整性,且性能通常更好。
  4. 及时更新加密库:关注安全公告,及时更新Java运行环境和Android SDK,以获取最新的安全补丁。

这个Demo提供了一个坚实且可运行的基础。将它集成到你的项目中时,请务必根据上述“安全强化建议”升级密钥管理策略,并根据实际数据量考虑性能优化。加密无小事,细节决定安全性。

相关新闻

  • 2026绍兴厨房渗水到楼下怎么办?自来水管暗管检测方法,仪器测漏收费标准 - 宅安选房屋修缮
  • AI驱动的SOC架构设计与实践:从多源数据整合到自然语言分析
  • 中文俚语俗语翻译实测:怎么保留原本意思不失真

最新新闻

  • 2026 北京名包回收易奢福靠谱吗?多间出售包间,隐私回收体验佳 - 奢侈品回收实体店
  • 大连能提供优质GEO服务的公司有哪些?2026年大连GEO实力口碑榜,价格透明避坑指南 - myqiye
  • AI编程核心技术解析与应用实践指南
  • C++拷贝构造函数:从深浅拷贝到移动语义的完整指南
  • 2026年国内招标代理公司 资质参差选品难 合规机构参考榜单 - 速递信息
  • 智能财务OCR:如何用NLP技术提升报表处理效率

日新闻

  • 大连理工大学与东京大学联手打造的“主动型AI助手“
  • 170.2026年国家级科研瓶颈:超精密单点金刚石切削(SPDT)光学表面生成
  • SongBloom:革命性歌曲生成框架深度解析——如何通过交织自回归与扩散模型创作完整音乐

周新闻

  • 大连理工大学与东京大学联手打造的“主动型AI助手“
  • 170.2026年国家级科研瓶颈:超精密单点金刚石切削(SPDT)光学表面生成
  • SongBloom:革命性歌曲生成框架深度解析——如何通过交织自回归与扩散模型创作完整音乐

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号