前言
最近研究SM2加密在app中的應(yīng)用冈止,在掉了1024根頭發(fā)的時(shí)候酌媒,終于給找到解決方案了。
背景
項(xiàng)目中使用的是簽發(fā)公私鑰證書進(jìn)行的加解密的嗤军,證書的格式為.cer文件格式注盈。也就是app端在應(yīng)用中載入公鑰,然后調(diào)用SM2的加密方法對(duì)數(shù)據(jù)進(jìn)行加密叙赚,將加密的數(shù)據(jù)傳給后端老客,后端通過對(duì)應(yīng)的私鑰解密。
long long ago震叮,在iOS系統(tǒng)進(jìn)行過一次大版本的升級(jí)過后胧砰,iPhone端的SM2加密就失效了,官方的原生方法無法讀取.cer文件苇瓣,這樣就無法獲取到證書中的公鑰尉间,也就無法完成整個(gè)加密過程了。
而今在Android端也出現(xiàn)了這樣的問題击罪,大概看了一下項(xiàng)目中使用的一些第三方框架哲嘲,大體就是對(duì)官方這樣的方法做了幾層封裝,然后將國(guó)密的一些加密方式整合在一起媳禁。所以這樣的第三方框架已經(jīng)無法繼續(xù)使用眠副。
沒辦法,項(xiàng)目還堅(jiān)持使用SM2進(jìn)行加密竣稽,那么只能繼續(xù)尋找解決辦法囱怕。作為一個(gè)非專業(yè)Android開發(fā)者來說,具有鉆研的精神可能是我最后的倔強(qiáng)了毫别。首先是全網(wǎng)各種查找方案娃弓,無一例外,都是老舊的實(shí)現(xiàn)方法岛宦,中間確實(shí)想過放棄台丛,也討論過切換加密方案,但是又需要考慮切換加密方案對(duì)用戶的影響已經(jīng)各端的工作量砾肺,也就暫時(shí)擱淺了齐佳。
大概是思維進(jìn)入死胡同了,漫無目的的看著各種各樣的處理方法债沮。最后還是回歸到原始方法。
解決方案
方法一
- build.gradle(:app)添加兩個(gè)庫的支持
hutool關(guān)于SM2的使用
bouncycastle一個(gè)很強(qiáng)的加密算法庫
dependencies {
// 強(qiáng)強(qiáng)聯(lián)手
implementation 'cn.hutool:hutool-all:5.8.21'
implementation 'org.bouncycastle:bcprov-jdk15to18:1.76'
}
- 實(shí)現(xiàn)
// 重要的導(dǎo)包
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.PublicKey;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.util.encoders.Hex;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import cn.hutool.crypto.ECKeyUtil;
try {
// 將.cer文件放置在Android應(yīng)用的res/raw目錄下本鸣。如果該目錄不存在疫衩,可以手動(dòng)創(chuàng)建它。
// 請(qǐng)確保將your_certificate替換為你實(shí)際的.cer文件的名稱荣德,而且要處理異常以確保代碼的健壯性闷煤。
InputStream is = getResources().openRawResource(R.raw. your_certificate); // 讀取.cer文件童芹,不限于這種讀取方式
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate)cf.generateCertificate(is);
is.close();
PublicKey pk = cert.getPublicKey();
byte[] pubkey = pk.getEncoded();
byte[] publicKey_XY = readPublicKey(pubkey);
// 獲取公鑰值,在公鑰的前面加上04前綴
String pubkeyStr = "04" + bytesToHex(publicKey_XY);
// 待加密內(nèi)容
String jsonStr = "{\"name\":\"zhangsan\",\"userid\":\"1234567890\"}";
final SM2 sm2 = new SM2(null, ECKeyUtil.toSm2PublicParams(pubkeyStr));
sm2.usePlainEncoding();
// jsonStr通過公鑰加密后的加密內(nèi)容鲤拿,轉(zhuǎn)成十六進(jìn)制
String encryptStr = sm2.encryptHex(jsonStr, KeyType.PublicKey).toUpperCase();
System.out.println(encryptStr);
} catch (CertificateException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
public static byte[] readPublicKey(byte[] pubkey) throws Exception {
SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(pubkey);
DERBitString publicKeyData = (DERBitString) subjectPublicKeyInfo.getPublicKeyData();
byte[] publicKey = publicKeyData.getEncoded();
byte[] encodedPublicKey = publicKey;
byte[] ecP = new byte[64];
System.arraycopy(encodedPublicKey, 4, ecP, 0, ecP.length);
// 公鑰的X值
byte[] certPKX = new byte[32];
// 公鑰的Y值
byte[] certPKY = new byte[32];
System.arraycopy(ecP, 0, certPKX, 0, 32);
System.arraycopy(ecP, 32, certPKY, 0, 32);
return ecP;
}
/**
* 字節(jié)轉(zhuǎn)16進(jìn)制
* @param b 字節(jié)
* @return 返回轉(zhuǎn)換后的十六進(jìn)制值
*/
public static String byteToHex(byte b) {
String hexString = Integer.toHexString(b & 0xFF);
if (hexString.length() < 2) {
hexString = new StringBuilder(String.valueOf(0)).append(hexString).toString();
}
return hexString.toUpperCase(Locale.ROOT);
}
/**
* 字節(jié)數(shù)組轉(zhuǎn)16進(jìn)制
* @param bytes 字節(jié)數(shù)組
* @return 返回十六進(jìn)制字符串
*/
public static String bytesToHex(byte[] bytes) {
StringBuffer buffer = new StringBuffer();
if (bytes != null && bytes.length > 0) {
for (int i = 0; i < bytes.length; i++) {
String hex = byteToHex(bytes[i]);
buffer.append(hex);
}
}
return buffer.toString();
}
方法二
- 使用OpenSSL庫或者使用gmssl
兩大利器假褪,都是從c++底層去處理證書文件結(jié)構(gòu),這個(gè)就不會(huì)受系統(tǒng)大版本升級(jí)影響了近顷。我的iOS端app內(nèi)就是使用OpenSSL的方式去實(shí)現(xiàn)的生音。
首先需要在Android端配置支持C++混合開發(fā)的環(huán)境,見官方介紹 - native-lib.cpp
#include <jni.h>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fstream>
#include <iostream>
#include "openssl/crypto.h"
#include "openssl/x509.h"
using namespace std;
extern "C" JNIEXPORT jstring JNICALL
Java_com_zsplat_opensslsupportedapp_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
unsigned char usrCertificate[4096];
const unsigned char *certDataBytes = NULL;
unsigned long usrCertificateLen;
X509 *x509Cert = NULL;
FILE *fp = NULL;
fp = fopen("./app/src/main/cpp/test.cer", "rb");
if (fp == NULL) {
cout << "讀取文件錯(cuò)誤窒升。缀遍。。" << endl;
} else {
usrCertificateLen = fread(usrCertificate, 1, 4096, fp);
fclose(fp);
certDataBytes = usrCertificate;
x509Cert = d2i_X509(NULL, &certDataBytes, usrCertificateLen);
if (x509Cert == NULL) {
cout << "x509錯(cuò)誤" << endl;
return "";
}
ASN1_BIT_STRING *pubkey = X509_get0_pubkey_bitstr(x509Cert);
for (int i = 0; i < pubkey->length; i++) {
// 打印公鑰
printf("%02x", pubkey->data[i]);
// 將(char *)data數(shù)據(jù)轉(zhuǎn)為string
}
// 返回公鑰即可
}
return env->NewStringUTF(OpenSSL_version(OPENSSL_VERSION));
}
- CMakeLists.txt
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Declares and names the project.
project("native-lib")
#將openssl的頭文件目錄包含進(jìn)來
include_directories(src/main/cpp)
#添加兩個(gè)靜態(tài)庫文件
add_library(
openssl-crypto
STATIC
IMPORTED)
set_target_properties(
openssl-crypto
PROPERTIES
IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/cpp/libs/libcrypto.a)
add_library(
openssl-ssl
STATIC
IMPORTED)
set_target_properties(
openssl-ssl
PROPERTIES
IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/src/main/cpp/libs/libssl.a)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
# opensslsupportedapp
native-lib
# Sets the library as a shared library.
# SHARED不注釋程序會(huì)報(bào)錯(cuò)饱须,項(xiàng)目啟動(dòng)閃退域醇,不知道是不是這邊造成的,還沒找到解決辦法
# SHARED
# Provides a relative path to your source file(s).
native-lib.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
# opensslsupportedapp
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib}
openssl-ssl
openssl-crypto)
項(xiàng)目中的.a靜態(tài)文件庫是使用NDK和OpenSSL源碼編譯的蓉媳,根據(jù)Android Studio中使用的NDK版本去做編譯譬挚。具體編譯方法可以去搜索。后續(xù)抽空寫一個(gè)關(guān)于編譯OpenSSL靜態(tài)庫的操作吧酪呻。
可能安卓端配置C++環(huán)境對(duì)我來說稍微麻煩點(diǎn)减宣,不過只要配置好,那是真的方便号杠,對(duì)于大部分加密的問題蚪腋,OpenSSL都能解決的。