From 46385d53582f864d967468408c30a3a4f505a623 Mon Sep 17 00:00:00 2001 From: maxinyu Date: Wed, 15 Jul 2026 15:26:00 +0800 Subject: [PATCH] =?UTF-8?q?res=E5=8A=A0=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../defense_device/annotation/ApiEncrypt.kt | 23 ++ .../defense_device/utils/RetrofitNetwork.kt | 37 ++- .../defense_device/utils/crypto/AESUtil.kt | 96 +++++++ .../utils/crypto/CryptoInterceptor.kt | 234 ++++++++++++++++++ .../utils/crypto/CryptoKeyManager.kt | 182 ++++++++++++++ .../defense_device/utils/crypto/RSAUtil.kt | 122 +++++++++ 6 files changed, 690 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/ckkj/defense_device/annotation/ApiEncrypt.kt create mode 100644 app/src/main/java/com/ckkj/defense_device/utils/crypto/AESUtil.kt create mode 100644 app/src/main/java/com/ckkj/defense_device/utils/crypto/CryptoInterceptor.kt create mode 100644 app/src/main/java/com/ckkj/defense_device/utils/crypto/CryptoKeyManager.kt create mode 100644 app/src/main/java/com/ckkj/defense_device/utils/crypto/RSAUtil.kt diff --git a/app/src/main/java/com/ckkj/defense_device/annotation/ApiEncrypt.kt b/app/src/main/java/com/ckkj/defense_device/annotation/ApiEncrypt.kt new file mode 100644 index 0000000..28b909e --- /dev/null +++ b/app/src/main/java/com/ckkj/defense_device/annotation/ApiEncrypt.kt @@ -0,0 +1,23 @@ +package com.ckkj.defense_device.annotation + +/** + * API 加密注解 + * + * 用于标记需要加密的 API 接口 + * + * @param response 是否同时加密响应体,默认 false(仅加密请求) + * + * 使用示例: + * - @ApiEncrypt // 仅加密请求 + * - @ApiEncrypt(response = true) // 请求和响应都加密 + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class ApiEncrypt( + /** + * 是否加密响应体 + * - false (默认): 仅加密请求体 + * - true: 请求体和响应体都加密 + */ + val response: Boolean = false +) 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 b1c0081..ad7d3d6 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 @@ -1,5 +1,7 @@ package com.ckkj.defense_device.utils +import com.ckkj.defense_device.utils.crypto.CryptoInterceptor +import com.ckkj.defense_device.utils.crypto.CryptoKeyManager import com.google.gson.Gson import okhttp3.OkHttpClient import retrofit2.Retrofit @@ -12,8 +14,8 @@ import java.util.concurrent.TimeUnit object RetrofitNetwork { private const val DEFAULT_TIMEOUT = 30L - //const val BASE_URL = "https://stem-boozy-margarine.ngrok-free.dev" - const val BASE_URL = "http://service.reinkun.com:8001" + const val BASE_URL = "https://stem-boozy-margarine.ngrok-free.dev" + //const val BASE_URL = "http://service.reinkun.com:8001" private var retrofit: Retrofit? = null private val gson = Gson() @@ -65,12 +67,39 @@ object RetrofitNetwork { .readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS) .writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS) .addInterceptor(ApiConfig.createHeadersInterceptor()) - .addInterceptor(ApiConfig.getUnifiedInterceptor()) // 使用 ApiConfig 中的统一拦截器 + .addInterceptor(ApiConfig.getUnifiedInterceptor()) + + // 添加加密拦截器(仅在密钥已配置时启用) + if (CryptoKeyManager.isKeyConfigured()) { + builder.addInterceptor(getCryptoInterceptor()) + } if (enableLogging) { - builder.addInterceptor(ApiConfig.getLoggingInterceptor()) // 使用 ApiConfig 中的日志拦截器 + builder.addInterceptor(ApiConfig.getLoggingInterceptor()) } return builder.build() } + + /** + * 获取加密拦截器实例 + * 每次都重新创建,确保使用最新密钥 + */ + private fun getCryptoInterceptor(): CryptoInterceptor { + // 清除旧实例,确保使用最新密钥 + CryptoInterceptor.clearInstance() + return CryptoInterceptor.getInstance( + publicKey = CryptoKeyManager.requestPublicKey, + privateKey = CryptoKeyManager.responsePrivateKey + ) + } + + /** + * 重新初始化网络客户端(密钥更新后调用) + */ + fun resetClient() { + retrofit = null + // 强制重新创建 Retrofit + getNetworkService() + } } \ No newline at end of file diff --git a/app/src/main/java/com/ckkj/defense_device/utils/crypto/AESUtil.kt b/app/src/main/java/com/ckkj/defense_device/utils/crypto/AESUtil.kt new file mode 100644 index 0000000..de897f6 --- /dev/null +++ b/app/src/main/java/com/ckkj/defense_device/utils/crypto/AESUtil.kt @@ -0,0 +1,96 @@ +package com.ckkj.defense_device.utils.crypto + +import android.util.Base64 +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec + +/** + * AES 加密工具类 + * + * 算法:AES/ECB/PKCS5Padding (Android) 与 AES/ECB/PKCS7Padding (iOS) 兼容 + */ +object AESUtil { + + private const val ALGORITHM = "AES" + private const val TRANSFORMATION = "AES/ECB/PKCS5Padding" + + /** + * AES ECB 模式加密 + * + * @param key 32字节 ASCII 字符串密钥 + * @param data UTF-8 编码的明文字节数组 + * @return Base64 编码的密文字节数组 + */ + fun encrypt(key: String, data: ByteArray): ByteArray { + val keySpec = SecretKeySpec(key.toByteArray(Charsets.UTF_8), ALGORITHM) + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, keySpec) + return cipher.doFinal(data) + } + + /** + * AES ECB 模式加密,返回 Base64 字符串 + * + * @param key 32字节 ASCII 字符串密钥 + * @param plainText UTF-8 明文字符串 + * @return Base64 编码的密文字符串 + */ + fun encrypt(key: String, plainText: String): String { + val encrypted = encrypt(key, plainText.toByteArray(Charsets.UTF_8)) + return Base64.encodeToString(encrypted, Base64.NO_WRAP) + } + + /** + * AES ECB 模式解密 + * + * @param key 32字节 ASCII 字符串密钥 + * @param encryptedData Base64 编码的密文字节数组 + * @return UTF-8 解密后的明文字节数组 + */ + fun decrypt(key: String, encryptedData: ByteArray): ByteArray { + val keySpec = SecretKeySpec(key.toByteArray(Charsets.UTF_8), ALGORITHM) + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.DECRYPT_MODE, keySpec) + return cipher.doFinal(encryptedData) + } + + /** + * AES ECB 模式解密,返回 UTF-8 字符串 + * + * @param key 32字节 ASCII 字符串密钥 + * @param encryptedText Base64 编码的密文字符串 + * @return UTF-8 解密后的明文字符串 + */ + fun decrypt(key: String, encryptedText: String): String { + val encryptedData = Base64.decode(encryptedText, Base64.NO_WRAP) + val decrypted = decrypt(key, encryptedData) + return String(decrypted, Charsets.UTF_8) + } + + /** + * 生成随机 AES 密钥 + * + * @param length 密钥长度,默认 32 字节 + * @return 随机 ASCII 字符串密钥 + */ + fun generateKey(length: Int = 32): String { + val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + return (1..length) + .map { chars.random() } + .joinToString("") + } + + /** + * Base64 编码 + */ + fun base64Encode(data: ByteArray): String { + return Base64.encodeToString(data, Base64.NO_WRAP) + } + + /** + * Base64 解码 + */ + fun base64Decode(encoded: String): ByteArray { + return Base64.decode(encoded, Base64.NO_WRAP) + } +} 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 new file mode 100644 index 0000000..ca3ba89 --- /dev/null +++ b/app/src/main/java/com/ckkj/defense_device/utils/crypto/CryptoInterceptor.kt @@ -0,0 +1,234 @@ +package com.ckkj.defense_device.utils.crypto + +import android.util.Base64 +import android.util.Log +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import java.nio.charset.Charset + +/** + * 加密拦截器 + * + * 实现 App 请求加密和服务端响应解密 + * + * 加密流程: + * 1. 生成随机 AES 密钥 (32字节 ASCII) + * 2. 用 RSA 公钥加密 AES 密钥 + * 3. 用 AES 加密请求体 + * 4. 设置请求头 encrypt-key 为 RSA 加密后的 AES 密钥 + * 5. 请求体为 AES 加密后的 Base64 字符串 + * + * 解密流程(仅需要对响应解密的接口): + * 1. 从响应头获取 encrypt-key + * 2. 用 RSA 私钥解密得到 AES 密钥 + * 3. 用 AES 解密响应体 + */ +class CryptoInterceptor private constructor( + private val publicKey: String, + private val privateKey: String +) : Interceptor { + + companion object { + private const val TAG = "CryptoInterceptor" + private const val HEADER_ENCRYPT_KEY = "encrypt-key" + private const val HEADER_IS_ENCRYPT = "isEncrypt" + private const val CONTENT_TYPE_JSON = "application/json; charset=utf-8" + + // 需要加密的接口列表(POST/PUT 请求) + private val ENCRYPT_ENDPOINTS = setOf( + "app/login", + "app/register", + "app/forgot", + "app/switchFamily", + "app/addDevice", + "app/updateDeviceInfo", + "app/deleteDevice", + "app/addDeviceUser", + "app/updateUserInfo", + "app/forgotPassword", + "app/retrievePassword", + "app/verifyOptionPassword", + "app/obtainInviterInformation", + "app/selectRecommendationCode", + "app/generateRecommendationCode", + "app/revokeInvitation", + "app/updateFamilyUser", + "app/selectInviterInfo", + "app/joinTheFamily", + "app/stream/start" + ) + + // 需要解密响应的接口列表 + private val DECRYPT_RESPONSE_ENDPOINTS = setOf() + + @Volatile + private var instance: CryptoInterceptor? = null + + fun getInstance(publicKey: String, privateKey: String): CryptoInterceptor { + return instance ?: synchronized(this) { + instance ?: CryptoInterceptor(publicKey, privateKey).also { + instance = it + } + } + } + + fun clearInstance() { + instance = null + } + + fun isEncryptEndpoint(url: String): Boolean { + return ENCRYPT_ENDPOINTS.any { url.contains(it, ignoreCase = true) } + } + + fun isDecryptEndpoint(url: String): Boolean { + return DECRYPT_RESPONSE_ENDPOINTS.any { url.contains(it, ignoreCase = true) } + } + } + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val url = request.url.toString() + + // 检查是否需要加密 + val needsEncrypt = isEncryptEndpoint(url) + val needsDecrypt = isDecryptEndpoint(url) + + if (!needsEncrypt && !needsDecrypt) { + return chain.proceed(request) + } + + // 调试日志 + Log.d(TAG, "=== CryptoInterceptor Debug ===") + Log.d(TAG, "URL: $url") + Log.d(TAG, "needsEncrypt: $needsEncrypt, needsDecrypt: $needsDecrypt") + Log.d(TAG, "PublicKey available: ${publicKey.isNotBlank()}, length: ${publicKey.length}") + Log.d(TAG, "PrivateKey available: ${privateKey.isNotBlank()}, length: ${privateKey.length}") + + // 加密请求 + val encryptedRequest = if (needsEncrypt) { + encryptRequest(request) + } else { + request + } + + // 发起请求 + val response = try { + chain.proceed(encryptedRequest) + } catch (e: Exception) { + Log.e(TAG, "Request failed", e) + throw e + } + + // 解密响应 + return if (needsDecrypt) { + decryptResponse(response, request) + } else { + response + } + } + + /** + * 加密请求 + */ + private fun encryptRequest(request: Request): Request { + val requestBody = request.body ?: return request + + // 只处理 JSON 请求 + val contentType = requestBody.contentType()?.toString() ?: "" + if (!contentType.contains("application/json")) { + return request + } + + // 只处理 POST 和 PUT + val method = request.method + if (method != "POST" && method != "PUT") { + return request + } + + // 读取原始请求体 + val buffer = okio.Buffer() + requestBody.writeTo(buffer) + val originalBody = buffer.readString(Charset.defaultCharset()) + Log.d(TAG, "Original request body: $originalBody") + + // 生成随机 AES 密钥 + val aesKey = AESUtil.generateKey(32) + Log.d(TAG, "Generated AES key: $aesKey") + + // 1. keyBase64 = Base64(UTF8(aesKey)) + val keyBase64 = AESUtil.base64Encode(aesKey.toByteArray(Charsets.UTF_8)) + Log.d(TAG, "keyBase64: $keyBase64") + + // 2. encrypt-key = RSA-PKCS1-Encrypt(请求公钥, UTF8(keyBase64)) + // encryptByPublicKey(String, String) 已返回 Base64 编码结果 + val encryptedKeyBase64 = RSAUtil.encryptByPublicKey(publicKey, keyBase64) + Log.d(TAG, "Encrypted key (Base64): $encryptedKeyBase64") + + // 3. body = AES-ECB-PKCS5-Encrypt(aesKey, UTF8(JSON)) + // encrypt(String, String) 已返回 Base64 编码结果 + val encryptedBodyBase64 = AESUtil.encrypt(aesKey, originalBody) + Log.d(TAG, "Encrypted body (Base64): $encryptedBodyBase64") + + // 构建新请求(根据原方法选择 POST 或 PUT) + val newRequestBuilder = request.newBuilder() + .header(HEADER_ENCRYPT_KEY, encryptedKeyBase64) + .header(HEADER_IS_ENCRYPT, "true") + + return if (method == "PUT") { + newRequestBuilder.put(encryptedBodyBase64.toRequestBody(CONTENT_TYPE_JSON.toMediaTypeOrNull())) + } else { + newRequestBuilder.post(encryptedBodyBase64.toRequestBody(CONTENT_TYPE_JSON.toMediaTypeOrNull())) + }.build() + } + + /** + * 解密响应 + */ + 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 aesKeyBase64 = RSAUtil.decryptByPrivateKeyBase64(privateKey, encryptedKeyBase64) + val aesKey = String(Base64.decode(aesKeyBase64, Base64.NO_WRAP), Charsets.UTF_8) + Log.d(TAG, "Decrypted AES key: $aesKey") + + // 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. 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) + .removeHeader(HEADER_ENCRYPT_KEY) + .build() + + } catch (e: Exception) { + Log.e(TAG, "Decryption failed", e) + // 解密失败时返回原始响应 + return response + } + } +} + +// 扩展函数:String 转 RequestBody +private fun String.toRequestBody(mediaType: okhttp3.MediaType?): okhttp3.RequestBody { + return okhttp3.RequestBody.create(mediaType, this) +} 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 new file mode 100644 index 0000000..e3e2f17 --- /dev/null +++ b/app/src/main/java/com/ckkj/defense_device/utils/crypto/CryptoKeyManager.kt @@ -0,0 +1,182 @@ +package com.ckkj.defense_device.utils.crypto + +import android.content.Context +import android.util.Log +import com.blankj.utilcode.util.SPUtils +import com.ckkj.defense_device.utils.UserinfoUtil + +/** + * 密钥管理类 + * + * 管理 RSA 加密所需的公钥和私钥 + * + * 密钥关系: + * ┌─────────────┬──────────────────┬──────────────────┐ + * │ 方向 │ App 持有 │ 服务端持有 │ + * ├─────────────┼──────────────────┼──────────────────┤ + * │ App请求服务端 │ 请求公钥 (RSA-P) │ 请求私钥 (RSA-S) │ + * ├─────────────┼──────────────────┼──────────────────┤ + * │ 服务端响应App │ 响应私钥 (RSA-S) │ 响应公钥 (RSA-P) │ + * └─────────────┴──────────────────┴──────────────────┘ + * + * 需要生成两套独立 RSA 2048/3072 密钥对 + */ +object CryptoKeyManager { + + private const val TAG = "CryptoKeyManager" + + // SPUtils 用于持久化密钥配置 + private val spUtils: SPUtils by lazy { + SPUtils.getInstance("crypto_keys") + } + + // 配置项 Keys + private const val KEY_REQUEST_PUBLIC_KEY = "request_public_key" + private const val KEY_RESPONSE_PRIVATE_KEY = "response_private_key" + private const val KEY_CRYPTO_ENABLED = "crypto_enabled" + + // 配置项默认值 + // 注意:这些密钥必须与后端配置配对使用 + // 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=" + + /** + * 是否启用加密 + */ + var isCryptoEnabled: Boolean + get() { + val stored = spUtils.getBoolean(KEY_CRYPTO_ENABLED, false) + // 如果存储值为 false 但密钥已配置,也返回 true + return stored || (DEFAULT_PUBLIC_KEY.isNotBlank() && DEFAULT_PRIVATE_KEY.isNotBlank()) + } + set(value) { + spUtils.put(KEY_CRYPTO_ENABLED, value) + Log.d(TAG, "Crypto enabled: $value") + } + + /** + * 请求公钥(用于 App 加密请求) + * App 使用此公钥加密 AES 密钥,服务端使用对应的私钥解密 + */ + var requestPublicKey: String + get() { + val stored = spUtils.getString(KEY_REQUEST_PUBLIC_KEY, "") + return if (stored.isNotBlank()) stored else DEFAULT_PUBLIC_KEY + } + set(value) { + spUtils.put(KEY_REQUEST_PUBLIC_KEY, value) + Log.d(TAG, "Request public key updated: ${value.take(20)}...") + } + + /** + * 响应私钥(用于 App 解密响应) + * 服务端使用对应的响应公钥加密响应,App 使用此私钥解密 + */ + var responsePrivateKey: String + get() { + val stored = spUtils.getString(KEY_RESPONSE_PRIVATE_KEY, "") + return if (stored.isNotBlank()) stored else DEFAULT_PRIVATE_KEY + } + set(value) { + spUtils.put(KEY_RESPONSE_PRIVATE_KEY, value) + Log.d(TAG, "Response private key updated: ${value.take(20)}...") + } + + /** + * 是否已配置密钥 + */ + fun isKeyConfigured(): Boolean { + // 检查密钥是否有内容(默认值或用户配置) + val hasPublicKey = requestPublicKey.isNotBlank() + val hasPrivateKey = responsePrivateKey.isNotBlank() + Log.d(TAG, "isKeyConfigured: publicKey=$hasPublicKey (length=${requestPublicKey.length}), privateKey=$hasPrivateKey (length=${responsePrivateKey.length})") + return hasPublicKey && hasPrivateKey + } + + /** + * 从服务端配置初始化密钥 + * + * 配置格式(可从服务端接口或配置中心获取): + * api-decrypt: + * enabled: true + * headerFlag: encrypt-key + * publicKey: ${APP_RESPONSE_PUBLIC_KEY} + * privateKey: ${APP_REQUEST_PRIVATE_KEY} + * + * @param enabled 是否启用加密 + * @param publicKey 服务端响应公钥(用于 App 加密请求) + * @param privateKey 服务端请求私钥(用于 App 解密响应) + */ + fun initialize(enabled: Boolean, publicKey: String, privateKey: String) { + isCryptoEnabled = enabled + requestPublicKey = publicKey + responsePrivateKey = privateKey + + Log.i(TAG, "CryptoKeyManager initialized: enabled=$enabled, keyConfigured=${isKeyConfigured()}") + } + + /** + * 从用户登录信息初始化密钥 + * 适用于密钥存储在用户账户中的场景 + * + * @param publicKey 公钥 + * @param privateKey 私钥 + */ + fun initializeFromLogin(publicKey: String, privateKey: String) { + initialize( + enabled = publicKey.isNotBlank() && privateKey.isNotBlank(), + publicKey = publicKey, + privateKey = privateKey + ) + } + + /** + * 清除密钥配置 + */ + fun clear() { + spUtils.clear() + Log.i(TAG, "CryptoKeyManager cleared") + } + + /** + * 获取密钥信息摘要(用于调试) + */ + fun getKeySummary(): String { + return buildString { + appendLine("CryptoKeyManager Summary:") + appendLine(" Enabled: $isCryptoEnabled") + appendLine(" RequestPublicKey: ${if (requestPublicKey.isNotBlank()) "${requestPublicKey.take(20)}..." else "NOT SET"}") + appendLine(" ResponsePrivateKey: ${if (responsePrivateKey.isNotBlank()) "${responsePrivateKey.take(20)}..." else "NOT SET"}") + appendLine(" Configured: ${isKeyConfigured()}") + } + } + + /** + * 验证密钥格式 + * + * @return true 如果密钥是有效的 Base64 编码 + */ + fun validateKeys(): Pair { + val publicKeyValid = validateBase64Key(requestPublicKey) + val privateKeyValid = validateBase64Key(responsePrivateKey) + + return when { + !publicKeyValid && !privateKeyValid -> false to "Both keys are invalid" + !publicKeyValid -> false to "Request public key is invalid" + !privateKeyValid -> false to "Response private key is invalid" + else -> true to "Keys are valid" + } + } + + private fun validateBase64Key(key: String): Boolean { + if (key.isBlank()) return false + return try { + android.util.Base64.decode(key, android.util.Base64.NO_WRAP) + true + } catch (e: Exception) { + false + } + } +} diff --git a/app/src/main/java/com/ckkj/defense_device/utils/crypto/RSAUtil.kt b/app/src/main/java/com/ckkj/defense_device/utils/crypto/RSAUtil.kt new file mode 100644 index 0000000..23d2f69 --- /dev/null +++ b/app/src/main/java/com/ckkj/defense_device/utils/crypto/RSAUtil.kt @@ -0,0 +1,122 @@ +package com.ckkj.defense_device.utils.crypto + +import android.util.Base64 +import java.security.KeyFactory +import java.security.PublicKey +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.RSAPrivateKeySpec +import java.security.spec.X509EncodedKeySpec +import javax.crypto.Cipher + +/** + * RSA 加密工具类 + * + * 算法:RSA/ECB/PKCS1Padding + * 支持 RSA 2048/3072 位密钥 + */ +object RSAUtil { + + private const val ALGORITHM = "RSA" + private const val TRANSFORMATION = "RSA/ECB/PKCS1Padding" + + /** + * 使用公钥加密(用于 App 加密请求) + * + * @param publicKeyStr Base64 编码的公钥字符串 + * @param data UTF-8 编码的明文字节数组 + * @return 密文字节数组 + */ + fun encryptByPublicKey(publicKeyStr: String, data: ByteArray): ByteArray { + val publicKey = parsePublicKey(publicKeyStr) + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, publicKey) + return cipher.doFinal(data) + } + + /** + * 使用公钥加密,返回 Base64 字符串 + * + * @param publicKeyStr Base64 编码的公钥字符串 + * @param plainText UTF-8 明文字符串 + * @return Base64 编码的密文字符串 + */ + fun encryptByPublicKey(publicKeyStr: String, plainText: String): String { + val encrypted = encryptByPublicKey(publicKeyStr, plainText.toByteArray(Charsets.UTF_8)) + return Base64.encodeToString(encrypted, Base64.NO_WRAP) + } + + /** + * 使用私钥解密(用于 App 解密响应) + * + * @param privateKeySpec RSA 私钥规格 + * @param encryptedData 密文字节数组 + * @return UTF-8 解密后的明文字节数组 + */ + fun decryptByPrivateKey(privateKeySpec: RSAPrivateKeySpec, encryptedData: ByteArray): ByteArray { + val keyFactory = KeyFactory.getInstance(ALGORITHM) + val privateKey = keyFactory.generatePrivate(privateKeySpec) + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.DECRYPT_MODE, privateKey) + return cipher.doFinal(encryptedData) + } + + /** + * 使用私钥解密,返回 UTF-8 字符串 + * + * @param privateKeySpec RSA 私钥规格 + * @param encryptedText Base64 编码的密文字符串 + * @return UTF-8 解密后的明文字符串 + */ + fun decryptByPrivateKey(privateKeySpec: RSAPrivateKeySpec, encryptedText: String): String { + val encryptedData = Base64.decode(encryptedText, Base64.NO_WRAP) + val decrypted = decryptByPrivateKey(privateKeySpec, encryptedData) + return String(decrypted, Charsets.UTF_8) + } + + /** + * 使用 Base64 编码的 PKCS#8 私钥字符串解密 + * + * @param privateKeyBase64 Base64 编码的 PKCS#8 私钥 + * @param encryptedText Base64 编码的密文字符串 + * @return UTF-8 解密后的明文字符串 + */ + fun decryptByPrivateKeyBase64(privateKeyBase64: String, encryptedText: String): String { + val encryptedData = Base64.decode(encryptedText, Base64.NO_WRAP) + val keyFactory = KeyFactory.getInstance(ALGORITHM) + val privateKeySpec = PKCS8EncodedKeySpec(Base64.decode(privateKeyBase64, Base64.NO_WRAP)) + val privateKey = keyFactory.generatePrivate(privateKeySpec) + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.DECRYPT_MODE, privateKey) + val decrypted = cipher.doFinal(encryptedData) + return String(decrypted, Charsets.UTF_8) + } + + /** + * 解析公钥 + * + * @param publicKeyStr Base64 编码的 X.509 公钥 + * @return PublicKey 对象 + */ + fun parsePublicKey(publicKeyStr: String): PublicKey { + val keyBytes = Base64.decode(publicKeyStr, Base64.NO_WRAP) + val keySpec = X509EncodedKeySpec(keyBytes) + val keyFactory = KeyFactory.getInstance(ALGORITHM) + return keyFactory.generatePublic(keySpec) + } + + /** + * 从 Base64 编码的 PKCS#8 私钥创建私钥规格 + * + * @param privateKeyBase64 Base64 编码的 PKCS#8 私钥 + * @return RSAPrivateKeySpec + */ + fun createPrivateKeySpecFromBase64(privateKeyBase64: String): RSAPrivateKeySpec { + val keyBytes = Base64.decode(privateKeyBase64, Base64.NO_WRAP) + val keyFactory = KeyFactory.getInstance(ALGORITHM) + val keySpec = keyFactory.getKeySpec( + keyFactory.generatePrivate(PKCS8EncodedKeySpec(keyBytes)), + RSAPrivateKeySpec::class.java + ) + return keySpec + } +}