优化res加密
This commit is contained in:
@@ -39,6 +39,25 @@ import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Query
|
||||
|
||||
// ┏┓ ┏┓
|
||||
//┏┛┻━━━┛┻┓
|
||||
//┃ ┃
|
||||
//┃ ━ ┃
|
||||
//┃ ┳┛ ┗┳ ┃
|
||||
//┃ ┃
|
||||
//┃ ┻ ┃
|
||||
//┃ ┃
|
||||
//┗━┓ ┏━┛
|
||||
// ┃ ┃
|
||||
// ┃ ┃
|
||||
// ┃ ┗━━━┓
|
||||
// ┃ ┣┓
|
||||
// ┃ ┏┛
|
||||
// ┗┓┓┏━┳┓┏┛
|
||||
// ┃┫┫ ┃┫┫
|
||||
// ┗┻┛ ┗┻┛
|
||||
//神兽护体,代码零Bug,需求不改,一次编译通过
|
||||
|
||||
interface ApiService {
|
||||
/**
|
||||
* 获取登录验证码
|
||||
|
||||
@@ -69,8 +69,8 @@ object RetrofitNetwork {
|
||||
.addInterceptor(ApiConfig.createHeadersInterceptor())
|
||||
.addInterceptor(ApiConfig.getUnifiedInterceptor())
|
||||
|
||||
// 添加加密拦截器(仅在密钥已配置时启用)
|
||||
if (CryptoKeyManager.isKeyConfigured()) {
|
||||
// 加密拦截器(密钥就绪即启用,按开关自动判断)
|
||||
if (CryptoKeyManager.isCryptoEnabled && CryptoKeyManager.isKeyConfigured()) {
|
||||
builder.addInterceptor(getCryptoInterceptor())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.ckkj.defense_device.utils.crypto
|
||||
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.google.gson.JsonParser
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.Request
|
||||
@@ -61,8 +62,20 @@ class CryptoInterceptor private constructor(
|
||||
"app/stream/start"
|
||||
)
|
||||
|
||||
// 需要解密响应的接口列表
|
||||
private val DECRYPT_RESPONSE_ENDPOINTS = setOf<String>()
|
||||
// 需要解密响应的接口列表(默认与加密列表对称 —— 加密请求的接口响应也会被加密)
|
||||
// 额外加上 GET 接口(GET 无 body 不加密请求,但后端仍可能加密响应)
|
||||
private val DECRYPT_RESPONSE_ENDPOINTS: Set<String> = ENCRYPT_ENDPOINTS + setOf(
|
||||
"app/selectFamily", // 获取家庭列表(GET)
|
||||
"app/getdeviceList", // 获取设备列表(GET)
|
||||
"app/deviceInfo", // 获取设备信息(GET)
|
||||
"app/appMessage/list", // 消息列表(GET)
|
||||
"app/appMessageInfo", // 告警消息详情(GET)
|
||||
"app/appMessage/deviceMeslist", // 设备告警日志(GET)
|
||||
"app/familyMembers/list", // 家庭成员列表(GET)
|
||||
"app/deviceUser/list", // 设备关联用户列表(GET)
|
||||
"app/userInfo", // 当前用户信息(GET)
|
||||
"app/deleteMessage" // 删除消息(DELETE)
|
||||
)
|
||||
|
||||
@Volatile
|
||||
private var instance: CryptoInterceptor? = null
|
||||
@@ -186,44 +199,92 @@ class CryptoInterceptor private constructor(
|
||||
|
||||
/**
|
||||
* 解密响应
|
||||
*
|
||||
* 兼容三种后端响应形态:
|
||||
* 1. 普通 JSON(无 encrypt-key 头)→ 原样放行
|
||||
* 2. 整 body 加密(响应头有 encrypt-key 且 body 不是合法 JSON)→ 整体 AES 解密
|
||||
* 3. 嵌套加密(响应头有 encrypt-key,body 是 {code,msg,data:"加密串"})
|
||||
* → 只解 data 字段,保持外层 JSON 结构(与 iOS 端一致)
|
||||
*/
|
||||
private fun decryptResponse(response: Response, originalRequest: Request): Response {
|
||||
val responseBody = response.body ?: return response
|
||||
|
||||
// 检查是否有 encrypt-key 头
|
||||
val encryptedKeyBase64 = response.header(HEADER_ENCRYPT_KEY)
|
||||
if (encryptedKeyBase64.isNullOrBlank()) {
|
||||
Log.w(TAG, "No encrypt-key header in response, skip decryption")
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 解密 encrypt-key 得到 AES 密钥
|
||||
val rawBody: String = try {
|
||||
responseBody.source().apply { request(Long.MAX_VALUE) }
|
||||
.buffer.clone().readString(Charset.defaultCharset())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Read response body failed", e)
|
||||
return response
|
||||
}
|
||||
|
||||
// 先拿到 AES 密钥(RSA 私钥解密 encrypt-key 头)
|
||||
val aesKey = try {
|
||||
val aesKeyBase64 = RSAUtil.decryptByPrivateKeyBase64(privateKey, encryptedKeyBase64)
|
||||
val aesKey = String(Base64.decode(aesKeyBase64, Base64.NO_WRAP), Charsets.UTF_8)
|
||||
Log.d(TAG, "Decrypted AES key: $aesKey")
|
||||
String(Base64.decode(aesKeyBase64, Base64.NO_WRAP), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "RSA decrypt encrypt-key failed", e)
|
||||
return response
|
||||
}
|
||||
|
||||
// 2. 读取响应体
|
||||
val source = responseBody.source()
|
||||
source.request(Long.MAX_VALUE)
|
||||
val encryptedBody = source.buffer.clone().readString(Charset.defaultCharset())
|
||||
Log.d(TAG, "Encrypted response body: $encryptedBody")
|
||||
// 形态 3:嵌套加密 —— 响应是 JSON,data 字段是加密串
|
||||
val trimmed = rawBody.trim()
|
||||
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
||||
try {
|
||||
val jsonElement = JsonParser().parse(trimmed)
|
||||
if (jsonElement.isJsonObject) {
|
||||
val obj = jsonElement.asJsonObject
|
||||
if (obj.has("data") && obj.get("data").isJsonPrimitive
|
||||
&& obj.get("data").asJsonPrimitive.isString
|
||||
) {
|
||||
val encryptedData = obj.get("data").asString
|
||||
val decryptedData = AESUtil.decrypt(aesKey, encryptedData)
|
||||
// 把 data 字段替换为解密后的 JSON 字符串(让 Gson 当成对象继续解析)
|
||||
obj.add("data", parseDataAsJsonElement(decryptedData))
|
||||
val newBody = obj.toString().toResponseBody(CONTENT_TYPE_JSON.toMediaTypeOrNull())
|
||||
return response.newBuilder()
|
||||
.body(newBody)
|
||||
.removeHeader(HEADER_ENCRYPT_KEY)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Nested decrypt failed, fallback to whole-body decrypt", e)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. AES 解密
|
||||
val decryptedBody = AESUtil.decrypt(aesKey, encryptedBody)
|
||||
Log.d(TAG, "Decrypted response body: $decryptedBody")
|
||||
|
||||
// 构建新响应
|
||||
val newBody = decryptedBody.toResponseBody(CONTENT_TYPE_JSON.toMediaTypeOrNull())
|
||||
return response.newBuilder()
|
||||
.body(newBody)
|
||||
// 形态 2:整 body 加密
|
||||
return try {
|
||||
val decryptedBody = AESUtil.decrypt(aesKey, rawBody)
|
||||
response.newBuilder()
|
||||
.body(decryptedBody.toResponseBody(CONTENT_TYPE_JSON.toMediaTypeOrNull()))
|
||||
.removeHeader(HEADER_ENCRYPT_KEY)
|
||||
.build()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Decryption failed", e)
|
||||
// 解密失败时返回原始响应
|
||||
return response
|
||||
Log.e(TAG, "Whole-body decrypt failed, return raw response", e)
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把解密后的字符串解析成 JsonElement(如果是合法 JSON);否则当成普通字符串返回
|
||||
*/
|
||||
private fun parseDataAsJsonElement(text: String): com.google.gson.JsonElement {
|
||||
val s = text.trim()
|
||||
return try {
|
||||
if ((s.startsWith("{") && s.endsWith("}")) ||
|
||||
(s.startsWith("[") && s.endsWith("]"))
|
||||
) {
|
||||
JsonParser().parse(s)
|
||||
} else {
|
||||
com.google.gson.JsonPrimitive(s)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
com.google.gson.JsonPrimitive(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,8 @@ object CryptoKeyManager {
|
||||
// 注意:这些密钥必须与后端配置配对使用
|
||||
// App端公钥 = 后端私钥(App加密请求,后端解密)
|
||||
// App端私钥 = 后端公钥(后端加密响应,App解密)
|
||||
private const val DEFAULT_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw2GulSqzHcLxl/l8UXPzc3WeS6QtsswRaYpoeMBwOU5/c5QRMhZm613IV754slgK2pA9ndSHzO6hGvNg5bsioCgtFGWRCe/UzFQjfnIMcva+7Vd1/dNgrF/5EUkOow5LEe8svaFpHYi+Pzo+qXt8/+MaJxu5gdv4W+VJsf1MYmiDvMxRTvFAjm2SY345uTWDlUrNFO1ozYkmqot3Z6CqNNYASmc10F3EZdNjXkSdcUNom5+svtMJCAAZWiBop3DxQS51lfzO6ukLFIypv7B4hwONrifV0mgMQu4SlOVAFCzBkgKqQzLySMxaB7XjeQeq5usuZDqMWxb6bYvHodGDBQIDAQAB"
|
||||
private const val DEFAULT_PRIVATE_KEY = "MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQC+iTaWIfJdkGEnGZD1CkYkl0r1QdwlIQS5wh+CSL5yL/oEwI3uojYDR6w/bmcsskqgCQvLN9V71L68t+tWHjQA6Ra+KXwfuKbswxvgd0HXCzreB7P/ccvnYuA8gxkR4N0KaPKUsvQORxUgXuqWddUfTxbvahvWj3Cs3DwXqIaFRKnFRHVUUmIyQVDURb/5DZLrAA5MOm6ZS1bPdtBiEMcdS90Mcu2pGNgvVzzkbKT1KK4ZIDG5EUIQ02jF4c1i50/qFW+S7e9oRtiyktDj8EKfSz3cyMjphMwZfJAtwV6WhxMt/TKT5uIIc985MhDLfJlEmzFJNVy4ftiStX7M6rn5AgMBAAECggEBALbstvpmNvC693r5YaDPr03b7nwOMdN3orjQdT24pxarluZPEzSiiSaW/fib/vP7bAe/B5Kf1RagxkoabRbtwlGJdms1GtS9i4zNOItFE3HodyjIzneVAgTweYW++yed8bjXzuUR9ZtrhQ3h8mhnUihP3x4VigpxBeT/P7Ya9qMkAavxdr7o3SdSubvePf/bxJHT1Ehwu0SShz2p/FXRK7GTywTHp+Q6M5lzTAiat52U/zANcoZ7Acj1fdWy1hBExQbbYwsCFonIRYoOKKmJ0lG6akm2Zzcls89VOfLBn7jtz4ts64AFVFWpw2Z6ho8YiU2NceZL6gWuz2JB9Ih7ay0CgYEA8ysHBcPe45ArnVyd0v7OzW8KTa3Yyhsl2OwAG3nHU/lVhhvSZnkxmNWzV0i7yfqHCTo9iHASeD3u7dsYwsdt4j3mVjP7hHnSalPu67TvYoinbb0zdgl1s1PccZGZt/w82pr0sor63te8fLd53s6HMn56cvTj4sJEUN5hoS8rOy8CgYEAyJcs+Zoxea06wCfERNp4K9WoTLx8YBBQ8Db9Vi8pecT64cda8jQCHjY5+lnV41S14PQKSMJQdXJeekLkCt4G8QQ93nVO4JEhm11HRqau1Y1h8cFCZCgsYSmdO9PadxTykx9xZWz24fT9vKDBi4exi7WeOJdrn/L2TjNrwhG081cCgYEA1IYghxtfbEgOtmPB1Cy7FbUcrjNoq4r6kxIALgNLAMuDwcgDJWh6vnbXXUvGRIrfBmOOb/ZzTf/jGKNKisG9+ePbsi+QfYHzMtbUk9D4/4crva+EkH9IDUkX+3IjhTfA/9Y6UulAxQ6MP/wjWWXrp2f/IQWDPiZHit4xOax9/iUCgYEAjTsbmHY/RXt5dRfKsevcVoitYbIIJB74zdj5z/eOzUSqm9MgLo39u8XUVndwCAp5sHRC1W2y9q5a5Nw58YjX/FRPN1UC/0xQDDLF33G2gXd1MaEvnJNIg/Sn1xrXo6ZlEL8/uPjabsdu66OIAjS9BjAOwaujRE/tqau8wDx1WisCgYEA6MTGurUrOtTD/vj+MItCWDhMK1T6ViA+gSsX+afqyjriNKlyrviuavkEM2+8fgHcqCy5isvhBtYhxESownVK19nr3WbTYtvgj/C5JAkJeO/V43sUqfjYYkbQd4vL5iNfMrC3dXSfr2V0dAjwZF4T2Ns+FHyKT5MSZSux+0mTU98="
|
||||
|
||||
private const val DEFAULT_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtVIQm+1AWMfkT1uVOyugKQwBeHtgpulABOrVfaL7Xzc1/rKSdLdjYZLZnnOU+rdE1jqz20MXDBozK22spH/j53d9nFLftHkMCRUlfc0MWEqtSla+2MdzElIHEbq4CSizZOcT8QXCRw7V2OuDqnC7XRQmnpA5Ks1WaKH5V0aoKWztjsqZV6vVWPqyDC94mvu/Q/9e6KVfkqAzR+afuo9OoBFewDGQ6jfqyu1TxoGZrfSr5lXOVeADf0CVfWCDze5y4f7er4JjPHWA2+yv8Scd9bqVJ4Q+/yR2Gd516Mi11OgBKc8GdkuA4/kfmXc7n91SMjgQP6DlTVuPc/6KnB/pyQIDAQAB"
|
||||
private const val DEFAULT_PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDYg1dC9XDMRtKJ5IFj+Hd3bq7+r2YL1y8FM9HD111PN/YVy4K6O67cRA/TLG7+tl6eYiA73f5lzPgv1riOwBPSCmvVopxq2KiJs30uEnug0qGp6l2cwQOmjitFAQF8dOtAl4m6EM/jdXCZYbQrthJ6n3tR+vGj2IrGSJEjPrj3VRljK9I7Zw0Be0rhJn/vZw2CA4GoVfE/3gqa5bpl+3SFxMP7TwF5uZueCNqLaPnd7jK5jB1SUghI7+pq897Ng4EG6kdOGsiQe5kbHWkSPTmPEObMwgLJ18i/XCVg+LUkTDJOC/MAfB+WGcKryNHrhcbhB6+XstpoGT8JlOPOgh0BAgMBAAECggEBALZG7MH8bDguL7XTYHNPjRRJZJ4aAGlbgcR5edHMkEvPKyfyK16qPG5IBqKcN93Mnx/sMIL2Q+RkiVWNCdd0MbuU9m/m9JSnTkyPhYWyHc6pRV0NaD496Nrhud/gFuY2cI/yhArXeI1gI6mdrddW83u0pFfCUojEFyETmsz3UzU1LMkVbwCXNbZ1b3GfcVnONMn8T/7CMlRPlKyOuE0OPnTut4wddAybe5BKDQYnGzuosUdO+Q+2K3hqaTD6Bb33D4ATHjvR3Vt9itE07BiaBFjZkE2odP4x3fSsnc7Cw3A7zHQivpfgqcoBcGUaMcN1FdgpaF2mbe9/eHn/lYCpevkCgYEA4FoKq3fPOITAtA6bjwCHXvIQUjv4vQ0BZtt7OX7zwpRwqOhJfN0ZRkd7NMUqYnB10N9NgPRz0kKC6qff5duN+zkYIzjHrlm5lzdIEAouovRlrGXST4n8gAYteh5D5Or9wRXj7USQoaNyiWKB9A/07ortJvul09sPjJU7IVfzzXMCgYEA9w45Gh2bAvYvm2bWieKZRhcV6Pwy9R8587qmKLC7V/0gfVojBHx7xTmTEfKCGrt3fK4jB5v/2Dar8gDF+RM7g01u0YuFJt17jeFr/4o5KMimfXhoEulDbgjfvhxQOrtkrF5iHpKxiEXqIPiH+B7155PAnE43011AAJCBSylmTrsCgYEAkxr3PA9HFKwXHvklDtMt8BeQlBs2sd9BOAxZ9A1GECP86wPEi9b9p5NfOe6+J+XNmrOQwimHeCqcZPjGWpVnt35sUUv9wlia1Igu/DVw9vCBalUpXXYA1oE2eIg3xHZBBMYxuXXnz9S4WVT6GOoNlAwMDC+dQBi3TVrcdrSQ6/kCgYBA254ICh+ovmKvJGdMGY3thZ/94z+pdEIthyGZ6xOzvMMrxV8ODXQcycmfW4/mXrK2q6yMkdqvs2KejK9sfS0RgmGGZ19UXa7TB4vnsSziVRLIO7TuyggmufOrIBm74XhDfB+8MPykbt5RO43OiKo72mElZ69mMMPdohIfXkX6gwKBgH4L9qJRhNunWSfHO4/8dST+2meSV+Cpqf0b/9cV6v5FMS22m+szzW9XoBXnI9dbAqiEDlCHCLw34pRlC3OzBNlbT6HzNmYXLko2Ttg9thiDq7WS5tV+EHJRP6kLabYGq0Aghtq+6LIwwogbHzhRi8Q52o3CNmM9lThNWZIkzaBe"
|
||||
/**
|
||||
* 是否启用加密
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user