System flow chart
Verification
Solve the problem:
1. Identity verification: Is it the person I specified?
2. Anti-tampering: Whether the parameters are hijacked and tampered with by a third party
3. Anti-replay: whether to repeat the request
Specific algorithm:
1. Agree on the appKey to ensure that the call request is issued by a caller authorized by the platform to ensure the uniqueness of the requester.
2. Add appKey to the value request parameter, such as: http://****?appKey=1232456& other parameters.
3. Sort the parameters (the sorting method can be agreed upon, such as ASCII code comparison), and splice the parameter names and values into strings.
4. Use the md5 digest algorithm to obtain the digest and put the digest into the request header.
package com.zhangteng.rxhttputils.interceptor
import android.text.TextUtils
import com.google.gson.JsonParser
import com.zhangteng.rxhttputils.utils.MD5Util.md5Decode32
import okhttp3.FormBody
import okhttp3.Interceptor
import okhttp3.RequestBody
import okhttp3.Response
import okio.Buffer
import java.io.IOException
import java.nio.charset.Charset
import java.util.*
import kotlin.collections.set
/**
* 添加签名拦截器
* Created by Swing on 2019/10/20.
*/
class SignInterceptor(private val appKey: String) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val requestBuilder = request.newBuilder()
val urlBuilder = request.url().newBuilder()
val params: MutableMap<String, Any?> =
TreeMap()
if (METHOD_GET == request.method()) {
val httpUrl = urlBuilder.build()
val paramKeys = httpUrl.queryParameterNames()
for (key in paramKeys) {
val value = httpUrl.queryParameter(key)
if (!TextUtils.isEmpty(value)) params[key] = value
}
} else if (METHOD_POST == request.method()) {
if (request.body() is FormBody) {
val formBody = request.body() as FormBody?
for (i in 0 until formBody!!.size()) {
params[formBody.encodedName(i)] = formBody.encodedValue(i)
}
} else if (request.body() is RequestBody) {
val requestBody = request.body()
val buffer = Buffer()
requestBody!!.writeTo(buffer)
var charset = Charset.forName("UTF-8")
val contentType = requestBody.contentType()
if (contentType != null) {
charset = contentType.charset()
}
val paramJson =
buffer.readString(charset ?: Charset.defaultCharset())
val jsonObject = JsonParser().parse(paramJson).asJsonObject
jsonObject.entrySet().forEach {
val jsonElement = it.value
if (jsonElement != null && !jsonElement.isJsonArray && !jsonElement.isJsonObject && !jsonElement.isJsonNull) {
val value = jsonElement.asString
if (!TextUtils.isEmpty(value)) params[it.key] = value
}
}
}
}
val sign = StringBuilder()
sign.append(appKey)
for (key in params.keys) {
sign.append(key).append(params[key])
}
val _timestamp = System.currentTimeMillis()
sign.append("_timestamp").append(_timestamp)
sign.append(appKey)
requestBuilder.addHeader("_timestamp", _timestamp.toString())
requestBuilder.addHeader("_sign", md5Decode32(sign.toString()))
return chain.proceed(requestBuilder.build())
}
companion object {
private const val METHOD_GET = "GET"
private const val METHOD_POST = "POST"
}
}
AES encryption
Solve the problem:
Data encryption to prevent information interception, RSA encryption and decryption is more time-consuming
Specific algorithm:
AES/CBC/NoPadding
package com.zhangteng.rxhttputils.utils
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
/**
* Created by Swing on 2017/12/6.
*/
object AESUtils {
/**
* 随机生成秘钥
*/
val key: String
get() = try {
val kg = KeyGenerator.getInstance("AES")
kg.init(128)
val sk = kg.generateKey()
val b = sk.encoded
byteToHexString(b)
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
""
}
/**
* 使用指定的字符串生成秘钥
*/
fun getKeyByPass(keyRaw: String): String {
return try {
val kg = KeyGenerator.getInstance("AES")
// kg.init(128);//要生成多少位,只需要修改这里即可128, 192或256
//SecureRandom是生成安全随机数序列,password.getBytes()是种子,只要种子相同,序列就一样,所以生成的秘钥就一样。
kg.init(128, SecureRandom(keyRaw.toByteArray()))
val sk = kg.generateKey()
val b = sk.encoded
byteToHexString(b)
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
""
}
}
/**
* byte数组转化为16进制字符串
*
* @param bytes
* @return
*/
fun byteToHexString(bytes: ByteArray): String {
val sb = StringBuffer()
for (i in bytes.indices) {
val strHex = Integer.toHexString(bytes[i].toInt())
if (strHex.length > 3) {
sb.append(strHex.substring(6))
} else {
if (strHex.length < 2) {
sb.append("0$strHex")
} else {
sb.append(strHex)
}
}
}
return sb.toString()
}
//加密
@Throws(Exception::class)
fun encrypt(data: String, key: String, iv: String): String? {
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
val blockSize = cipher.blockSize
val dataBytes = data.toByteArray()
var plaintextLength = dataBytes.size
if (plaintextLength % blockSize != 0) {
plaintextLength = plaintextLength + (blockSize - plaintextLength % blockSize)
}
val plaintext = ByteArray(plaintextLength)
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.size)
val keyspec =
SecretKeySpec(key.toByteArray(), "AES")
val ivspec =
IvParameterSpec(iv.toByteArray())
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec)
val encrypted = cipher.doFinal(plaintext)
return Base64Utils.encode(encrypted)
}
//解密
@Throws(Exception::class)
fun decrypt(data: String, key: String, iv: String): String {
val encrypted1 = Base64Utils.decode(data)
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
val keyspec =
SecretKeySpec(key.toByteArray(), "AES")
val ivspec =
IvParameterSpec(iv.toByteArray())
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec)
val original = cipher.doFinal(encrypted1)
return String(original)
}
}
RSA encryption
Solve the problem:
AES key exchange is difficult
Specific algorithm:
RSA/ECB/PKCS1Padding
package com.zhangteng.rxhttputils.utils
import android.util.Base64
import java.security.KeyFactory
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.NoSuchAlgorithmException
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.*
import javax.crypto.Cipher
/**
* 字符串格式的密钥在未在特殊说明情况下都为BASE64编码格式<br></br>
* 由于非对称加密速度极其缓慢,一般文件不使用它来加密而是使用对称加密,<br></br>
* 非对称加密算法可以用来对对称加密的密钥加密,这样保证密钥的安全也就保证了数据的安全
*/
object RSAUtils {
/**
* 非对称加密密钥算法
*/
const val RSA = "RSA"
/**
* 加密填充方式
*/
const val ECB_PKCS1_PADDING = "RSA/ECB/PKCS1Padding"
/**
* 秘钥默认长度
*/
const val DEFAULT_KEY_SIZE = 1024
/**
* 当要加密的内容超过bufferSize,则采用partSplit进行分块加密
*/
val DEFAULT_SPLIT = "#PART#".toByteArray()
/**
* 当前秘钥支持加密的最大字节数
*/
const val DEFAULT_BUFFERSIZE = DEFAULT_KEY_SIZE / 8 - 11
/**
* 随机生成RSA密钥对
*
* @param keyLength 密钥长度,范围:512~2048
* 一般1024
* @return
*/
fun generateRSAKeyPair(keyLength: Int): KeyPair? {
return try {
val kpg =
KeyPairGenerator.getInstance(RSA)
kpg.initialize(keyLength)
kpg.genKeyPair()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
null
}
}
/**
*
*
* 公钥加密
*
*
* @param data 源数据
* @param publicKey 公钥(BASE64编码)
* @return
* @throws Exception
*/
@Throws(Exception::class)
fun encryptByPublicKey(data: String, publicKey: String?): String {
return String(
Base64.encode(
encryptByPublicKey(
data.toByteArray(),
Base64.decode(publicKey, 0)
), 0
)
).replace("\n", "").replace("\\", "")
}
/**
* 用公钥对字符串进行加密
*
* @param data 原文
*/
@Throws(Exception::class)
fun encryptByPublicKey(
data: ByteArray?,
publicKey: ByteArray?
): ByteArray {
// 得到公钥
val keySpec =
X509EncodedKeySpec(publicKey)
val kf = KeyFactory.getInstance(RSA)
val keyPublic = kf.generatePublic(keySpec)
// 加密数据
val cp = Cipher.getInstance(ECB_PKCS1_PADDING)
cp.init(Cipher.ENCRYPT_MODE, keyPublic)
return cp.doFinal(data)
}
/**
*
*
* 私钥加密
*
*
* @param data 源数据
* @param publicKey 公钥(BASE64编码)
* @return
* @throws Exception
*/
@Throws(Exception::class)
fun encryptByPrivateKey(data: String, publicKey: String?): String {
return String(
Base64.encode(
encryptByPrivateKey(
data.toByteArray(),
Base64.decode(publicKey, 0)
), 0
)
).replace("\n", "").replace("\\", "")
}
/**
* 私钥加密
*
* @param data 待加密数据
* @param privateKey 密钥
* @return byte[] 加密数据
*/
@Throws(Exception::class)
fun encryptByPrivateKey(
data: ByteArray?,
privateKey: ByteArray?
): ByteArray {
// 得到私钥
val keySpec =
PKCS8EncodedKeySpec(privateKey)
val kf = KeyFactory.getInstance(RSA)
val keyPrivate = kf.generatePrivate(keySpec)
// 数据加密
val cipher =
Cipher.getInstance(ECB_PKCS1_PADDING)
cipher.init(Cipher.ENCRYPT_MODE, keyPrivate)
return cipher.doFinal(data)
}
/**
*
*
* 公钥解密
*
*
* @param encryptedData 已加密数据(BASE64编码)
* @param publicKey 公钥(BASE64编码)
* @return
* @throws Exception
*/
@Throws(Exception::class)
fun decryptByPublicKey(
encryptedData: String?,
publicKey: String?
): String {
return String(
decryptByPublicKey(
Base64.decode(encryptedData, 0),
Base64.decode(publicKey, 0)
)
)
}
/**
* 公钥解密
*
* @param data 待解密数据
* @param publicKey 密钥
* @return byte[] 解密数据
*/
@Throws(Exception::class)
fun decryptByPublicKey(
data: ByteArray?,
publicKey: ByteArray?
): ByteArray {
// 得到公钥
val keySpec =
X509EncodedKeySpec(publicKey)
val kf = KeyFactory.getInstance(RSA)
val keyPublic = kf.generatePublic(keySpec)
// 数据解密
val cipher =
Cipher.getInstance(ECB_PKCS1_PADDING)
cipher.init(Cipher.DECRYPT_MODE, keyPublic)
return cipher.doFinal(data)
}
/**
* <P>
* 私钥解密
</P> *
*
* @param encryptedData 已加密数据(BASE64编码)
* @param privateKey 私钥(BASE64编码)
* @return
* @throws Exception
*/
@Throws(Exception::class)
fun decryptByPrivateKey(
encryptedData: String?,
privateKey: String?
): String {
return String(
decryptByPrivateKey(
Base64.decode(encryptedData, 0),
Base64.decode(privateKey, 0)
)
)
}
/**
* 使用私钥进行解密
*/
@Throws(Exception::class)
fun decryptByPrivateKey(
encrypted: ByteArray?,
privateKey: ByteArray?
): ByteArray {
// 得到私钥
val keySpec =
PKCS8EncodedKeySpec(privateKey)
val kf = KeyFactory.getInstance(RSA)
val keyPrivate = kf.generatePrivate(keySpec)
// 解密数据
val cp = Cipher.getInstance(ECB_PKCS1_PADDING)
cp.init(Cipher.DECRYPT_MODE, keyPrivate)
return cp.doFinal(encrypted)
}
}
okhttp interceptor implements encryption and decryption
package com.zhangteng.rxhttputils.interceptor
import android.text.TextUtils
import com.google.gson.JsonParser
import com.zhangteng.rxhttputils.http.HttpUtils
import com.zhangteng.rxhttputils.http.OkHttpClient
import com.zhangteng.rxhttputils.utils.AESUtils
import com.zhangteng.rxhttputils.utils.DiskLruCacheUtils
import com.zhangteng.rxhttputils.utils.RSAUtils
import com.zhangteng.rxhttputils.utils.SPUtils
import okhttp3.*
import okio.Buffer
import java.io.IOException
import java.nio.charset.Charset
/**
* 添加加解密拦截器
* Created by Swing on 2019/10/20.
*/
class EncryptionInterceptor(private val publicKeyUrl: HttpUrl) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val request = chain.request()
val headers = request.headers()
if (headers.names().contains(SECRET) && "true" == headers[SECRET]) {
var secretRequest: Request? = buildRequest(request) ?: return errorSecretResponse
var secretResponse = chain.proceed(secretRequest!!)
val secretResponseBody = secretResponse.body()
val secretResponseStr =
if (secretResponseBody != null) secretResponseBody.string() else ""
val jsonObject = JsonParser().parse(
secretResponseStr.substring(
0,
secretResponseStr.lastIndexOf("}") + 1
)
).asJsonObject
val jsonElement = jsonObject["status"]
if (jsonElement != null
&& !jsonElement.isJsonArray
&& !jsonElement.isJsonObject
&& !jsonElement.isJsonNull
&& "2100" == jsonElement.asString
) {
SPUtils.put(HttpUtils.getInstance().getContext()!!, SPUtils.FILE_NAME, SECRET, "")
DiskLruCacheUtils.remove(publicKeyUrl)
DiskLruCacheUtils.flush()
secretRequest = buildRequest(request)
if (secretRequest == null) {
return errorSecretResponse
}
secretResponse = chain.proceed(secretRequest)
} else {
val mediaType =
if (secretResponseBody != null) secretResponseBody.contentType() else MediaType.parse(
"application/json;charset=UTF-8"
)
val newResonseBody = ResponseBody.create(mediaType, secretResponseStr)
secretResponse = secretResponse.newBuilder().body(newResonseBody).build()
}
return secretResponse
}
return chain.proceed(request)
}
/**
* 构建加密请求
*
* @param request 原请求
*/
@Throws(IOException::class)
private fun buildRequest(request: Request): Request? {
if (TextUtils.isEmpty(
SPUtils[HttpUtils.getInstance()
.getContext()!!, SPUtils.FILE_NAME, SECRET, ""].toString()
)
) {
val secretResponse = OkHttpClient.getInstance().client.newCall(
Request.Builder().url(publicKeyUrl).build()
).execute()
val secretResponseString = secretResponse.body()?.string()
if (secretResponse.code() == 200) {
val jsonObject = JsonParser().parse(secretResponseString).asJsonObject
val jsonElement = jsonObject["result"].asJsonObject["publicKey"]
SPUtils.put(
HttpUtils.getInstance().getContext()!!,
SPUtils.FILE_NAME,
SECRET,
jsonElement.asString
)
} else {
return null
}
}
val aesRequestKey: String = AESUtils.key
val requestBuilder = request.newBuilder()
requestBuilder.removeHeader(SECRET)
try {
requestBuilder.addHeader(
SECRET,
RSAUtils.encryptByPublicKey(
aesRequestKey,
SPUtils[HttpUtils.getInstance()
.getContext()!!, SPUtils.FILE_NAME, SECRET, publicKey] as String
)
)
} catch (e: Exception) {
return null
}
if (METHOD_GET == request.method()) {
val url = request.url().url().toString()
val paramsBuilder = url.substring(url.indexOf("?") + 1)
try {
val encryptParams =
AESUtils.encrypt(paramsBuilder, aesRequestKey, aesRequestKey.substring(0, 16))
requestBuilder.url(url.substring(0, url.indexOf("?")) + "?" + encryptParams)
} catch (e: Exception) {
return null
}
} else if (METHOD_POST == request.method()) {
val requestBody = request.body()
if (requestBody != null && aesRequestKey.length >= 16) {
if (requestBody is FormBody) {
val formBody = request.body() as FormBody?
val bodyBuilder = FormBody.Builder()
try {
if (formBody != null) {
for (i in 0 until formBody.size()) {
val value = formBody.encodedValue(i)
if (!TextUtils.isEmpty(value)) {
val encryptParams = AESUtils.encrypt(
value,
aesRequestKey,
aesRequestKey.substring(0, 16)
)
bodyBuilder.addEncoded(formBody.encodedName(i), encryptParams)
}
}
requestBuilder.post(bodyBuilder.build())
}
} catch (e: Exception) {
return null
}
} else {
val buffer = Buffer()
requestBody.writeTo(buffer)
var charset =
Charset.forName("UTF-8")
val contentType = requestBody.contentType()
if (contentType != null) {
charset = contentType.charset()
}
val paramsRaw =
buffer.readString(charset ?: Charset.defaultCharset())
if (!TextUtils.isEmpty(paramsRaw)) {
try {
val encryptParams = AESUtils.encrypt(
paramsRaw,
aesRequestKey,
aesRequestKey.substring(0, 16)
)
requestBuilder.post(
RequestBody.create(
requestBody.contentType(),
encryptParams
)
)
} catch (e: Exception) {
return null
}
}
}
}
}
return requestBuilder.build()
}
/**
* 获取加密失败响应
*/
private val errorSecretResponse: okhttp3.Response
private get() {
val failureResponseBuilder = okhttp3.Response.Builder()
failureResponseBuilder.body(
ResponseBody.create(
MediaType.parse("application/json;charset=UTF-8"),
"{\"message\": \"移动端加密失败\",\"status\": ${SECRET_ERROR}}"
)
)
return failureResponseBuilder.build()
}
companion object {
private const val METHOD_GET: String = "GET"
private const val METHOD_POST: String = "POST"
const val SECRET: String = "_secret"
const val SECRET_ERROR: Int = 2100
const val publicKey: String = ""
}
}
package com.zhangteng.rxhttputils.interceptor
import android.text.TextUtils
import com.zhangteng.rxhttputils.http.HttpUtils
import com.zhangteng.rxhttputils.interceptor.EncryptionInterceptor.Companion.SECRET
import com.zhangteng.rxhttputils.interceptor.EncryptionInterceptor.Companion.SECRET_ERROR
import com.zhangteng.rxhttputils.utils.AESUtils.decrypt
import com.zhangteng.rxhttputils.utils.RSAUtils
import com.zhangteng.rxhttputils.utils.SPUtils
import okhttp3.Interceptor
import okhttp3.MediaType
import okhttp3.Response
import okhttp3.ResponseBody
import java.io.IOException
/**
* 添加加解密拦截器
* Created by Swing on 2019/10/20.
*/
class DecryptionInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (!response.isSuccessful || response.code() != 200) {
return response
}
val responseBuilder = response.newBuilder()
val responseBody = response.body()
val responseHeaders = response.headers()
for (name in responseHeaders.names()) {
if (SECRET.contains(name!!) && !TextUtils.isEmpty(responseHeaders[name])) {
return try {
val encryptKey = responseHeaders[name]
val aesResponseKey: String = RSAUtils.decryptByPublicKey(
encryptKey,
SPUtils[HttpUtils.getInstance()
.getContext()!!, SPUtils.FILE_NAME, SECRET, EncryptionInterceptor.publicKey] as String
)
val mediaType =
if (responseBody != null) responseBody.contentType() else MediaType.parse(
"application/json;charset=UTF-8"
)
val responseStr =
if (responseBody != null) responseBody.string() else ""
val rawResponseStr = decrypt(
responseStr,
aesResponseKey,
aesResponseKey.substring(0, 16)
)
responseBuilder.body(ResponseBody.create(mediaType, rawResponseStr))
responseBuilder.build()
} catch (e: Exception) {
val failureResponse = Response.Builder()
failureResponse.body(
ResponseBody.create(
MediaType.parse("application/json;charset=UTF-8"),
"{\"message\": \"移动端解密失败${e.message}\",\"status\": ${SECRET_ERROR}}"
)
)
failureResponse.build()
}
}
}
return response
}
}
optimization
There is a problem:
When the client obtains the server's RSA public key in plain text, the server's public key is leaked, causing the server's response data to be leaked.
Solution:
1. The client obtains the RSA public key of the server;
2. The client randomly generates an RSA key pair
3. The client uses the server’s public key to encrypt the client’s public key and then delivers it to the server;
4. The client and server use the client’s public and private keys to encrypt and decrypt data to complete the security key exchange.