From 195fb7362077adf2755b5cc0d714db9688c84e83 Mon Sep 17 00:00:00 2001 From: maxinyu Date: Sat, 18 Jul 2026 17:24:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96res=E5=8A=A0=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ckkj/defense_device/utils/ApiService.kt | 19 +++ .../defense_device/utils/RetrofitNetwork.kt | 4 +- .../utils/crypto/CryptoInterceptor.kt | 111 ++++++++++++++---- .../utils/crypto/CryptoKeyManager.kt | 5 +- 4 files changed, 109 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/ckkj/defense_device/utils/ApiService.kt b/app/src/main/java/com/ckkj/defense_device/utils/ApiService.kt index 2c06c76..56925fc 100644 --- a/app/src/main/java/com/ckkj/defense_device/utils/ApiService.kt +++ b/app/src/main/java/com/ckkj/defense_device/utils/ApiService.kt @@ -39,6 +39,25 @@ import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Query +// ┏┓   ┏┓ +//┏┛┻━━━┛┻┓ +//┃       ┃ +//┃   ━   ┃ +//┃ ┳┛ ┗┳ ┃ +//┃       ┃ +//┃   ┻   ┃ +//┃       ┃ +//┗━┓   ┏━┛ +// ┃   ┃ +// ┃   ┃ +// ┃   ┗━━━┓ +// ┃       ┣┓ +// ┃       ┏┛ +// ┗┓┓┏━┳┓┏┛ +// ┃┫┫ ┃┫┫ +// ┗┻┛ ┗┻┛ +//神兽护体,代码零Bug,需求不改,一次编译通过 + interface ApiService { /** * 获取登录验证码 diff --git a/app/src/main/java/com/ckkj/defense_device/utils/RetrofitNetwork.kt b/app/src/main/java/com/ckkj/defense_device/utils/RetrofitNetwork.kt index ad7d3d6..e7a263f 100644 --- a/app/src/main/java/com/ckkj/defense_device/utils/RetrofitNetwork.kt +++ b/app/src/main/java/com/ckkj/defense_device/utils/RetrofitNetwork.kt @@ -69,8 +69,8 @@ object RetrofitNetwork { .addInterceptor(ApiConfig.createHeadersInterceptor()) .addInterceptor(ApiConfig.getUnifiedInterceptor()) - // 添加加密拦截器(仅在密钥已配置时启用) - if (CryptoKeyManager.isKeyConfigured()) { + // 加密拦截器(密钥就绪即启用,按开关自动判断) + if (CryptoKeyManager.isCryptoEnabled && CryptoKeyManager.isKeyConfigured()) { builder.addInterceptor(getCryptoInterceptor()) } diff --git a/app/src/main/java/com/ckkj/defense_device/utils/crypto/CryptoInterceptor.kt b/app/src/main/java/com/ckkj/defense_device/utils/crypto/CryptoInterceptor.kt index ca3ba89..f4d9948 100644 --- a/app/src/main/java/com/ckkj/defense_device/utils/crypto/CryptoInterceptor.kt +++ b/app/src/main/java/com/ckkj/defense_device/utils/crypto/CryptoInterceptor.kt @@ -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() + // 需要解密响应的接口列表(默认与加密列表对称 —— 加密请求的接口响应也会被加密) + // 额外加上 GET 接口(GET 无 body 不加密请求,但后端仍可能加密响应) + private val DECRYPT_RESPONSE_ENDPOINTS: Set = 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) } } } diff --git a/app/src/main/java/com/ckkj/defense_device/utils/crypto/CryptoKeyManager.kt b/app/src/main/java/com/ckkj/defense_device/utils/crypto/CryptoKeyManager.kt index e3e2f17..dbeb953 100644 --- a/app/src/main/java/com/ckkj/defense_device/utils/crypto/CryptoKeyManager.kt +++ b/app/src/main/java/com/ckkj/defense_device/utils/crypto/CryptoKeyManager.kt @@ -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" /** * 是否启用加密 */