res加密
This commit is contained in:
@@ -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
|
||||||
|
)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.ckkj.defense_device.utils
|
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 com.google.gson.Gson
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
@@ -12,8 +14,8 @@ import java.util.concurrent.TimeUnit
|
|||||||
object RetrofitNetwork {
|
object RetrofitNetwork {
|
||||||
|
|
||||||
private const val DEFAULT_TIMEOUT = 30L
|
private const val DEFAULT_TIMEOUT = 30L
|
||||||
//const val BASE_URL = "https://stem-boozy-margarine.ngrok-free.dev"
|
const val BASE_URL = "https://stem-boozy-margarine.ngrok-free.dev"
|
||||||
const val BASE_URL = "http://service.reinkun.com:8001"
|
//const val BASE_URL = "http://service.reinkun.com:8001"
|
||||||
|
|
||||||
private var retrofit: Retrofit? = null
|
private var retrofit: Retrofit? = null
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
@@ -65,12 +67,39 @@ object RetrofitNetwork {
|
|||||||
.readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
|
.readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
|
||||||
.writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
|
.writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
|
||||||
.addInterceptor(ApiConfig.createHeadersInterceptor())
|
.addInterceptor(ApiConfig.createHeadersInterceptor())
|
||||||
.addInterceptor(ApiConfig.getUnifiedInterceptor()) // 使用 ApiConfig 中的统一拦截器
|
.addInterceptor(ApiConfig.getUnifiedInterceptor())
|
||||||
|
|
||||||
|
// 添加加密拦截器(仅在密钥已配置时启用)
|
||||||
|
if (CryptoKeyManager.isKeyConfigured()) {
|
||||||
|
builder.addInterceptor(getCryptoInterceptor())
|
||||||
|
}
|
||||||
|
|
||||||
if (enableLogging) {
|
if (enableLogging) {
|
||||||
builder.addInterceptor(ApiConfig.getLoggingInterceptor()) // 使用 ApiConfig 中的日志拦截器
|
builder.addInterceptor(ApiConfig.getLoggingInterceptor())
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取加密拦截器实例
|
||||||
|
* 每次都重新创建,确保使用最新密钥
|
||||||
|
*/
|
||||||
|
private fun getCryptoInterceptor(): CryptoInterceptor {
|
||||||
|
// 清除旧实例,确保使用最新密钥
|
||||||
|
CryptoInterceptor.clearInstance()
|
||||||
|
return CryptoInterceptor.getInstance(
|
||||||
|
publicKey = CryptoKeyManager.requestPublicKey,
|
||||||
|
privateKey = CryptoKeyManager.responsePrivateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新初始化网络客户端(密钥更新后调用)
|
||||||
|
*/
|
||||||
|
fun resetClient() {
|
||||||
|
retrofit = null
|
||||||
|
// 强制重新创建 Retrofit
|
||||||
|
getNetworkService()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String>()
|
||||||
|
|
||||||
|
@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)
|
||||||
|
}
|
||||||
@@ -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<Boolean, String> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user