开发文档
可信设备SDK接入指南更新时间: 2025-01-22 10:42:00
一、集成SDK
1、添加AAR
添加小米安全开放服务SDK(sdk-xsof-20231229-v1.0.1-release.aar)到项目Module(app)的libs路径中
SDK文件:

注:图片仅供参考,以实际版本号为准
2、配置Gradle
在Module所属的Gradle文件(app/build.gradle)中增加编译依赖
// 只添加小米安全开放服务对应的一个aar
implementation files('libs/sdk-xsof-20231229-v1.0.1-release.aar')
或
// 添加libs文件夹下的所有aar
implementation fileTree(dir: "libs", includes: "*.aar")
注意:上述步骤中的文件路径和Gradle配置仅供参考, 可根据项目实际情况灵活调整,只要保证aar参与到项目编译即可。
3、配置appId、appKey
应用开启小米安全开放服务后,会分配对应的appId和appKey。
如何开启小米安全开放服务,参考小米安全开放服务后台使用说明
appId和appKey在SDK初始化时会用到,需要调用方添加到项目中。
appId和appKey可以在代码中写常量,也可以在工程中增加配置文件,然后在SDK初始化时读取。
二、API使用说明
完成步骤1后,即可使用小米安全开放服务客户端SDK提供的API。
1、API简要说明
public class MiSafetyDetectClient {
/**
* 初始化SDK
* @param appId 应用的AppId
* @param appKey 应用的AppKey
*/
public void init(String appId, String appKey);
/**
* 获取设备可信检测信息
* @param challenge 随机数
* @param onCompleteListener 接收检测结果的回调
*/
public void getTrustDeviceStatus(String challenge, OnCompleteListener<TrustDeviceStatusResponse> onCompleteListener);
/**
* 获取恶意应用检测信息
* @param onCompleteListener 接收检测结果的回调
*/
public void getRiskApps(OnCompleteListener<RiskAppResponse> onCompleteListener);
/**
* 获取模拟点击检测信息
* @param timeInterval 需要检测的时间范围
* @param onCompleteListener 接收检测结果的回调
*/
public void getSimulatedTouchDetectResult(long timeInterval, OnCompleteListener<SimulatedTouchResponse> onCompleteListener);
}
2、使用示例
2.1 SDK初始化
import com.xiaomi.security.xsof.MiSafetyDetect;
import com.xiaomi.security.xsof.safetydetect.MiSafetyDetectClient;
ClassName {
private static final String APP_ID = "***"; // replace with your appId
private static final String APP_KEY = "***"; // replace with your appKey
// 通过MiSafetyDetect#getMiSafetyDetectClient获取MiSafetyDetectClient实例
MiSafetyDetectClient miSafetyDetectClient = MiSafetyDetect.getMiSafetyDetectClient(getContext());
// 如果是null,说明当前系统版本不支持安全开放服务相关服务
if (miSafetyDetectClient != null) {
miSafetyDetectClient.init(APP_ID, APP_KEY);
}
}
初始化后即可使用小米安全开放服务提供的各项服务。
目前小米安全开放服务支持三项检测:
- 设备可信检测
- 恶意应用检测
- 模拟点击检测
2.2 获取设备可信检测结果
private void getSafetyDeviceStatus() {
/*
* challenge值为服务端下发的String类型的随机数,
* 要求:
* 1.长度为16至256个字符
* 2.不要包含换行等特殊字符
*/
String challenge = getChallengeFromServer();
/*
* 检测结果数据示例(JSON格式):
* {
* "data":{
* "package_name":"com.xiaomi.demo",
* "version_code":"1",
* "certificate_sha256":"edd320bb5a5708a818240760366c8d",
* "unlock_l1":"locked",
* "unlock_l2":"locked",
* "root_l1":"root",
* "selinux_l1":"enforcing",
* "fr_counter_l1":"0",
* "challenge":"xxxx"
* },
* "authenticity":{
* "hash":"xxxxx",
* "token":"xxxxx"
* }
* }
*
* 使用2.1中初始化得到的MiSafetyDetectClient实例进行调用
* @param challenge 随机数
* @param onCompleteListener 接收检测结果的回调
*/
miSafetyDetectClient.getTrustDeviceStatus(challenge, new OnCompleteListener<MiSafetyDetectClient.TrustDeviceStatusResponse>() {
@Override
public void onComplete(MiSafetyDetectClient.TrustDeviceStatusResponse response) {
// 成功获取到检测结果
if (response.isSuccess()) {
String resp = response.getTokenResult();
try {
JSONObject jsonObject = new JSONObject(resp);
// 获取data域
String data = jsonObject.get("data").toString();
// 获取authenticity域
JSONObject authenticity = (JSONObject) jsonObject.get("authenticity");
// 校验data域的hash值与authenticity域的hash值是否一致
Log.i(TAG, "check hash: " + HashUtils.checkHashSHA256(data, authenticity));
} catch (JSONException e) {
Log.e(TAG, "onComplete: ", e);;
}
} else {
// 获取相关信息时出现异常
final int statusCode = response.getStatusCode();
Log.w(TAG, "getTrustDeviceStatus result error, code = " + statusCode);
}
}
});
}
结果data域字段说明
字段 | 字段说明 |
data | 设备可信状态数据,数据hash的原始数据。 |
package_name | 调用方应用包名。 |
version_code | 调用方应用版本。 |
certificate_sha256 | 调用方应用APK 签名 SHA256 |
unlock_l1 | 从REE侧获取的bootloader解锁状态 |
unlock_l2 | 从TEE级别获取的bootloader解锁状态 |
root_l1 | 从REE侧获取的系统root状态 |
selinux_l1 | 从REE侧获取的selinux状态 |
fr_counter_l1 | 从REE侧获取到的历史恢复出厂次数 |
challenge | 调用方通过API接口传入的随机数,建议由业务服务端生成,长度为 16~256字符。 |
authenticity | 数据签名信息。 |
hash | 对data的value进行SHA256得到的哈希值。 |
token | 用于数据验签,可以将该数据上传到小米的服务器进行数据验证。 |
HashUtils工具类
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.MessageDigest;
public class HashUtils {
private static final String TAG = "HashUtils";
public static boolean checkHashSHA256(String data, JSONObject authenticity) {
String dataHash = getSHA256String(data);
String authenticityHash = "";
try {
authenticityHash = authenticity.getString("hash");
} catch (JSONException e) {
Log.e(TAG, "checkHashSHA256: ", e);
}
return dataHash != null && dataHash.equals(authenticityHash);
}
public static String getSHA256String(String originData) {
if (TextUtils.isEmpty(originData)) {
return null;
} else {
MessageDigest messageDigest;
String encodedString = "";
try {
messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(originData.getBytes());
encodedString = byte2Hex(messageDigest.digest());
} catch (Exception e) {
Log.e(TAG, "encrypt sha256 exception", e);
}
return encodedString;
}
}
private static String byte2Hex(byte[] bytes) {
StringBuilder stringBuffer = new StringBuilder();
String temp;
for (byte aByte : bytes) {
temp = Integer.toHexString(aByte & 0xFF);
if (temp.length() == 1) {
stringBuffer.append("0");
}
stringBuffer.append(temp);
}
return stringBuffer.toString();
}
}
2.3 获取恶意应用检测结果
private void getRiskAppList() {
/*
* 检测结果数据示例(List<AppData>格式):
* [
* AppData{
* packageName='package1',
* signatureMd5='package1_sig_md5',
* appCategory=5},
* AppData{
* packageName='package2',
* signatureMd5='package2_sig_md5',
* appCategory=5}
* ]
*
* 使用2.1中初始化得到的MiSafetyDetectClient实例进行调用
* @param onCompleteListener 接收检测结果的回调
*/
miSafetyDetectClient.getRiskApps(new OnCompleteListener<MiSafetyDetectClient.RiskAppResponse>() {
@Override
public void onComplete(MiSafetyDetectClient.RiskAppResponse riskAppResponse) {
// 成功获取到检测结果
if (riskAppResponse.isSuccess()) {
Log.i(TAG, "Installed risk apps: " + riskAppResponse.getAppsList());
} else {
// 获取相关信息时出现异常
int statusCode = riskAppResponse.getStatusCode();
Log.i(TAG, "Get risk app list error, code = " + statusCode);
}
}
});
}
2.4 获取模拟点击检测结果
private void getSimulatedTouchDetectResult() {
/*
* 检测结果数据示例(JSON格式):
* {
* "result":false
* }
*
* 使用2.1中初始化得到的MiSafetyDetectClient实例进行调用
* @param timeInterval 需要检测的时间范围
* @param onCompleteListener 接收检测结果的回调
*/
final long timeInterval = 3 * 60 * 1000L; // 检测过去3min是否发生过模拟点击
miSafetyDetectClient.getSimulatedTouchDetectResult(timeInterval, new OnCompleteListener<MiSafetyDetectClient.SimulatedTouchResponse>() {
@Override
public void onComplete(MiSafetyDetectClient.SimulatedTouchResponse response) {
// 成功获取到检测结果
if (response.isSuccess()) {
Log.i(TAG, "Get simulated touch detect result : " + response.getTokenResult());
} else {
// 获取相关信息时出现异常
final int statusCode = response.getStatusCode();
Log.i(TAG, "Get simulated touch detect result error, code = " + statusCode);
}
}
});
}
三、设备可信服务数据签名验证
针对2.2中获取到的设备可信检测结果,可以通过如下方式验证其真实性。
1、本地hash比对
通过2.2中提供的HashUtils工具类,可以计算data域的SHA256 hash值并将其与authenticity域中的hash值做比对,如果一致,则说明data域的数据与authenticity域中的hash值匹配。
如果不一致,则说明data域中的数据被篡改。
2、通过服务端进行签名校验
本地hash比对比较快捷,但由于hash值也可以被篡改,所以还存在一定风险。
针对需要高可靠性的场景,可以通过向小米服务端发起请求来进行在线验证。
注意:
- 在线验证需要申请网络权限。
- 此处仅为代码示例,实际调用时,请通过三方服务端发起。
示例代码
String result = HttpUtils.post(
context,
(JSONObject) mData2Verity.get("data"),
(JSONObject) mData2Verity.get("authenticity"),
APP_ID,
APP_KEY,
getPackageName());
HttpUtils工具类
package com.xiaomi.xsof.demo.utils;
import android.content.Context;
import android.util.Base64;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
public class HttpUtils {
private static final String TAG = "HttpUtils";
private static final String VERIFY_HOST = "https://framework.sec.miui.com/framework/device/status";
private static final String VERIFY_SALT = "33zt9239-a34f-3ty9-eb73-75456785ns5c";
public static String post(Context context, JSONObject data, JSONObject authenticity, String appId, String appKey, String packageName) {
JSONObject params = new JSONObject();
try {
params.put("data", data);
params.put("authenticity", authenticity);
params.put("pkgName", packageName);
params.put("appId", appId);
params.put("appKey", appKey);
params.put("timestamp", String.valueOf(System.currentTimeMillis()));
params.put("nonce", System.currentTimeMillis() + packageName);
params.put("appSignature", SignatureUtils.getSignatureSHA256(context, packageName));
params.put("sign", getParamsSignature(params));
} catch (JSONException e) {
throw new RuntimeException(e);
}
int responseCode;
InputStream is = null;
ByteArrayOutputStream bos = null;
try {
URL parsedUrl = new URL(VERIFY_HOST);
HttpURLConnection connection = openConnection(parsedUrl);
connection.setRequestMethod("POST");
addBodyIfExists(connection, params);
responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
is = connection.getInputStream();
bos = new ByteArrayOutputStream();
int len;
byte[] buffer = new byte[4096];
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
String result = bos.toString();
Log.d(TAG, "request result : " + result);
return result;
}
} catch (IOException e) {
Log.e(TAG, "post: ", e);
} finally {
close(is);
close(bos);
}
return "";
}
private static void close(Closeable close) {
if (close != null) {
try {
close.close();
} catch (IOException e) {
// ignore
}
}
}
private static void addBodyIfExists(HttpURLConnection connection, JSONObject params) throws IOException {
byte[] body = params.toString().getBytes();
if (body != null) {
connection.setDoOutput(true);
connection.addRequestProperty("Content-Type", "application/json");
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.write(body);
out.close();
}
}
private static HttpURLConnection openConnection(URL url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
int timeoutMs = 15000;
connection.setConnectTimeout(timeoutMs);
connection.setReadTimeout(timeoutMs);
connection.setUseCaches(false);
connection.setDoInput(true);
return connection;
}
private static String getParamsSignature(JSONObject params) throws JSONException {
StringBuilder keyBuilder = new StringBuilder();
Iterator<String> jsonKeys = params.keys();
List<String> keys = new ArrayList<>();
while (jsonKeys.hasNext()) {
keys.add(jsonKeys.next());
}
keys.sort(Comparator.naturalOrder());
for (String key : keys) {
if (params.get(key) instanceof JSONObject) {
continue;
}
Log.i(TAG, "getParamsSignature: append" + params.get(key));
keyBuilder.append(key)
.append("=")
.append(params.get(key))
.append("&");
}
keyBuilder.append(HttpUtils.VERIFY_SALT);
String keyString = keyBuilder.toString();
Log.i(TAG, "getParamsSignature: " + keyString);
byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
String base64 = Base64.encodeToString(keyBytes, Base64.NO_WRAP);
return getMd5Digest(base64);
}
public static String getMd5Digest(String pInput) {
try {
MessageDigest lDigest = MessageDigest.getInstance("MD5");
lDigest.update(pInput.getBytes(StandardCharsets.UTF_8));
BigInteger lHashInt = new BigInteger(1, lDigest.digest());
return String.format("%1$032X", lHashInt);
} catch (NoSuchAlgorithmException lException) {
throw new RuntimeException(lException);
}
}
}
SignatureUtils工具类
package com.xiaomi.xsof.demo.utils;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import java.security.MessageDigest;
public class SignatureUtils {
public static String getSignatureSHA256(Context context, String packageName) {
try {
PackageInfo pi = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
if (pi != null) {
return getPackageSHA256(pi);
}
} catch (Exception e) {
// empty
}
return "";
}
public static String getPackageSHA256(PackageInfo packageInfo) {
try {
MessageDigest localMessageDigest = MessageDigest.getInstance("SHA256");
localMessageDigest.update(packageInfo.signatures[0].toByteArray());
StringBuilder localStringBuilder = new StringBuilder();
byte[] arrayOfByte = localMessageDigest.digest();
int m = 0;
int n = arrayOfByte.length;
while (m < n) {
if (m > 0) {
localStringBuilder.append(":");
}
localStringBuilder.append(Integer.toString(256 + (0xFF & arrayOfByte[m]), 16).substring(1));
m++;
}
String str5 = localStringBuilder.toString();
return str5.toUpperCase();
} catch (Exception e) {
// ignore
}
return "";
}
}
上一篇:
下一篇:
文档内容是否有帮助?