PHP加密JSON传输至Kotlin应用的解密适配与密钥配置问题
PHP与Kotlin AES-128-GCM加解密互通配置方案
原代码无法互通的核心原因
- 算法模式完全错配:PHP侧使用
aes-128-ctr流模式,Kotlin侧实现为AES/CBC/PKCS7Padding块模式,加密逻辑、填充规则完全不兼容 - 密钥逻辑错配:PHP侧是直接将原始密钥字节传入openssl作为AES密钥,Kotlin侧却使用PBKDF2WithHmacSHA1算法从口令派生256位密钥,且引入了无对应PHP侧逻辑的
salt参数,两端密钥根本不一致 - 密钥长度错配:PHP侧
aes-128要求16字节长度的AES密钥,Kotlin侧派生出32字节的256位密钥,算法长度无法对应 - IV长度不兼容:切换到GCM模式后,原16字节IV不符合GCM安全规范,参数不一致会直接导致解密失败
- 认知偏差:GCM模式自带的认证标签是防篡改的核心组件,不存在“避免处理标签”的实现方式,跳过标签处理会直接失去防篡改能力
- 代码笔误:PHP侧将密钥变量
$key写为$kay,会直接导致密钥加载失败
统一互通的固定对齐规则
以下参数两端必须完全一致,无任何自主调整空间:
- 算法套件:AES/GCM/NoPadding(GCM为流模式,无需块填充)
- 密钥长度:128位(16字节,对应PHP侧
aes-128-gcm)- 认证标签长度:128位(GCM标准安全长度)
- IV长度:12字节(GCM推荐的安全IV长度)
- 传输格式:将「原始二进制密文 + 128位认证标签」拼接为整体后做Base64编码传输
对应实现代码
PHP端加密实现
// 固定参数,需与Kotlin侧完全对齐 $cipher = "aes-128-gcm"; // 16字节原始AES密钥,Base64编码后存储,需与Kotlin侧常量值一致 $key = base64_decode("MTIzNDU2Nzg5MDEyMzQ1Ng=="); // 12字节IV,Base64编码后存储,需与Kotlin侧常量值一致 $iv = base64_decode("dGhpMTJieXRlc0l2MDAw"); $plaintext = "Encryption information"; $authTag = ""; // 存储openssl生成的128位GCM认证标签 // 加密,输出原始二进制密文 $ciphertext = openssl_encrypt( $plaintext, $cipher, $key, OPENSSL_RAW_DATA, // 不自动Base64输出密文 $iv, $authTag // 传入标签变量引用,openssl自动写入生成的标签 ); // 拼接密文和标签,整体Base64作为最终传输内容 $finalPayload = base64_encode($ciphertext . $authTag); echo $finalPayload;
密钥对应关系说明:PHP侧解码后的16字节
$key就是需要放到Kotlin代码里的唯一密钥常量,无需做任何派生转换,原Kotlin代码中的salt、PBKDF2派生逻辑可直接删除。
Kotlin端解密实现
import android.util.Base64 import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec object AESEncryption { // 直接填入与PHP侧完全一致的参数,无派生逻辑 const val secretKeyBase64 = "MTIzNDU2Nzg5MDEyMzQ1Ng==" // 16字节密钥的Base64值 const val ivBase64 = "dGhpMTJieXRlc0l2MDAw" // 12字节IV的Base64值 fun decrypt(encodedPayload: String): String? { try { // 解码传输的Base64内容,拆分密文与16字节认证标签 val fullData = Base64.decode(encodedPayload, Base64.DEFAULT) val cipherTextPart = fullData.copyOfRange(0, fullData.size - 16) val tagPart = fullData.copyOfRange(fullData.size - 16, fullData.size) // 构造AES密钥:直接使用16字节原始密钥,无需PBKDF2派生 val secretKey = SecretKeySpec( Base64.decode(secretKeyBase64, Base64.DEFAULT), "AES" ) // 构造GCM参数:128位标签长度 + 12字节IV val gcmSpec = GCMParameterSpec( 128, Base64.decode(ivBase64, Base64.DEFAULT) ) // 算法完全对应PHP侧的aes-128-gcm val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec) // 传入拼接好的密文与标签,完成解密和篡改校验 val plainResult = cipher.doFinal(cipherTextPart + tagPart) return String(plainResult) } catch (e: Exception) { println("Decryption failed: $e") } return null } }
关键疑问对应说明
- 原Kotlin代码的
secretKey、salt配置:原代码使用PBKDF2从口令派生密钥的逻辑,仅适用于从用户输入的口令生成密钥的场景;如果PHP侧直接使用固定16字节密钥,需完全删除派生逻辑、salt参数,直接将PHP侧的原始密钥值填入Kotlin的常量即可 - PHP侧
$cipher参数的Kotlin对应:aes-128-gcm完全对应Kotlin的AES/GCM/NoPadding配置,无需额外转换 - 防篡改能力说明:GCM模式解密时会自动校验认证标签,如果传输的密文被篡改,标签校验会直接失败抛出异常,完全满足“加密锁定、无法篡改”的需求,认证标签是实现该能力的核心,不能跳过处理
内容的提问来源于stack exchange,提问作者mordi
相关产品推荐
相关产品推荐

