跳到主要内容

签名和加解密

核心概念:文档中心 | 签名和加解密 | 概览

本文档是对sdkVersion=47及以上的版本有效,服务端也同步做了低版本sdk兼容,设备端放心接入
EncryptionTests.java
EncryptionUtils.java


核心概念:文档中心 | 签名和加解密 | 一 总体规则 (签名和加解密)

一 总体规则 (签名和加解密)


核心概念:文档中心 | 签名和加解密 | 2.1、所有请求带在请求头带如下参数 (签名和加解密)

2.1、所有请求带在请求头带如下参数 (签名和加解密)

mac=A8:20:06:B7:C8:B9
cpu= 02c0008145f0462078a3840038350b53
api-version=47
host=api.yudao.iocoder.cn (设备端在请求时一般不用设置,web请求框架自动带入的)


核心概念:文档中心 | 签名和加解密 | 2.2、加解密和验签整体情况说明 (签名和加解密)

2.2、加解密和验签整体情况说明 (签名和加解密)

对于设备端请求服务器的请求,除了心跳和获取时间戳的请求,其他请求都带签名,如果不带签名,服务端统一返回404错误码,包含http状态码也是404。

服务端返回时,如果返回头中的encryption=true表示返回体数据加密,设备端拿到密文之后,进行解密操作,记住只是data下的数据加密,msg和code正常。


核心概念:文档中心 | 签名和加解密 | 二 盒子签名 (签名和加解密)

二 盒子签名 (签名和加解密)

签名和验签,加密解密逻辑 1、设备发起业务请求前,先请求服务器拿到最新时钟。域名分发为/app-api/hnc2fng/gnbht53hdv2/dhgb35dg,主UOTA为心跳接口。
2、备心跳请求时返回时间戳逻辑。 请求/app-api/hnc2fng/gnbht53hdv2/dhgb35dg,而时间戳永远放在data.time字段中,为13位时间戳。
3、主UOTA心跳请求时返回时间戳逻辑。 盒子端的UOTA程序内部不管是否已经激活,都要先请求心跳接口,而时间戳永远放在data.time字段中,为13位时间戳。
4、拿到时间戳之后,设备开始进行签名操作。签名工具类见【<一个文件链接>】,参数如下。

    /**
*
* MD5签名
* @param domain 发起请求时使用的域名。从设备端的host请求头获取,设备端在请求时一般不用设置,web请求框架自动带入的。 请求头示例:host=api.yudao.iocoder.cn
* @param mac 设备mac。请求头示例: mac=A8:20:06:B7:C8:B9
* @param cpu 设备cpu。请求头示例:cpu= 02c0008145f0462078a3840038350b53
* @param time 13位时间戳。 服务器有个获取时间戳的接口,通过那个接口获取后后续请求带上来。请求头示例:time=1750324247858
* @param salt 签名的盐值。建议每个uota版本都不一样,uota写入程序内部后进行混淆处理;服务端通过配置管理和版本对应关系。根据【sdkVersion】从服务端获配置获取
* @param sdkVersion sdk版本 ,机顶盒发起请求时手动设置,服务端从请求头获取。请求头示例: api-version=46
* @return 签名结果
*/
public static String sign(String domain,String mac,String cpu,Long time,String salt,int sdkVersion){
String sign = domain+salt+"8622Nbdf52"+mac+salt+"2d6fndw"+cpu+salt+"3hd73bx6g"+time+salt+"23jhY6DGVhbd";
return getMD5(sign);
}

5、签名结果放在请求头中。所以完整请求头示例如下:

mac=A8:20:06:B7:C8:B9
cpu= 02c0008145f0462078a3840038350b53
api-version=47
# 设备端在请求时一般不用设置,web请求框架自动带入的
host=api.yudao.iocoder.cn
time=1750324247858
# 签名结果
sign=bc7318a3dce54a3e3aa30785eae53abf

核心概念:文档中心 | 签名和加解密 | 三 服务端验签 (签名和加解密)

三 服务端验签 (签名和加解密)

在服务端进行验签操作。

  /**
* 验证签名
* @param domain 发起请求时使用的域名。从设备端的host请求头获取,设备端在请求时一般不用设置,web请求框架自动带入的。 请求头示例:host=api.yudao.iocoder.cn
* @param mac 设备mac。请求头示例: mac=A8:20:06:B7:C8:B9
* @param cpu 设备cpu。请求头示例:cpu= 02c0008145f0462078a3840038350b53
* @param time 13位时间戳。 服务器有个获取时间戳的接口,通过那个接口获取后后续请求带上来。请求头示例:time=1750324247858
* @param salt 签名的盐值。建议每个uota版本都不一样,uota写入程序内部后进行混淆处理;服务端通过配置管理和版本对应关系。根据【sdkVersion】从服务端获配置获取
* @param sdkVersion sdk版本 ,从请求头获取。请求头示例: api-version=46
* @param receivedSign 和字段带上来的签名结果
* @return 验证结果(true=有效,false=无效)
*/
public static boolean verify(String domain, String mac, String cpu, Long time, String salt,int sdkVersion, String receivedSign)

核心概念:文档中心 | 签名和加解密 | 四 数据加解密(当前仅用于域名分发) (签名和加解密)

四 数据加解密(当前仅用于域名分发) (签名和加解密)

1、 服务端返回给设备的数据都在body里面,且数据的data经过加密处理。

 /**
* 加密方法
* @param plainText 明文
* @param secretKey 密钥。 建议每个uota版本都不一样,uota写入程序内部后进行混淆处理;服务端通过配置管理和版本对应关系。根据【sdkVersion】从服务端获配置获取
* @return 密文
* @throws Exception
*/
public static String encrypt(String plainText, String secretKey)

2、 设备端拿到密文之后,进行解密操作,记住只是data下的数据加密,msg和code正常。

   /**
* 解密方法
* @param encryptedText 密文
* @param secretKey 密钥。建议每个uota版本都不一样,uota写入程序内部后进行混淆处理;服务端通过配置管理和版本对应关系。根据【sdkVersion】从服务端获配置获取
* @return 明文
* @throws Exception
*/
// 解密方法
public static String decrypt(String encryptedText, String secretKey)

核心概念:文档中心 | 签名和加解密 | 五 测试环境秘钥信息 (签名和加解密)

五 测试环境秘钥信息 (签名和加解密)

--- ###################### 机顶盒请求时验签、加密相关 ######################
app:
api:
security:
# 不需要验签或者加密的的域名
no-need-domain:
# - 127.0.0.1
- localhost
- akrdinfo.cn
- 47.251.5.25
# 需要验证签名在处理的路径
sign-urls:
- /app-api/hnc2fng/ghbn1hebdsh/**
# 需要加密data后返回的路径
encryption-urls:
- /app-api/hnc2fng/ghbn1hebdsh/**
# sdk信息 version-版本号;singKey-sdk签名和验签密钥,其实就是md5盐值;encryptionKey-sdk用于数据加密解密;singTimeDiff-签名时间差,单位毫秒,当前时间和签名带上来的时间超过该值则直接验签失败
sdks:
- version: 47
singKey: jdh2dh7hdbhu3hbfcyHvdhf87NFH67b47
encryptionKey: 1234567887654347
singTimeDiff: 600000
- version: 48
singKey: jdh2dh7hdbhu3hbfcyHvdhf87NFH67b48
encryptionKey: 1234567887654348
singTimeDiff: 600000

核心概念:文档中心 | 签名和加解密 | 六、apifox模拟请求 (签名和加解密)

六、apifox模拟请求 (签名和加解密)


核心概念:文档中心 | 签名和加解密 | 6.1 获取当前时间 (签名和加解密)

6.1 获取当前时间 (签名和加解密)

调用【获取当前时间】接口拿到服务器时间,结果如下,记住这个时间,在【生成签名接口,生产环境需要屏蔽】和【获取域名和APK】中都要使用,crul如下

curl --location --request GET 'http://imptor.top:8742/app-api/hnc2fng/gnbht53hdv2/dhgb35dg' \
--header 'api-version: 47' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Accept: */*' \
--header 'Host: imptor.top:8742' \
--header 'Connection: keep-alive'

请求结果如下:

{
"code": 200,
"data": {
"time": 1753181666064
},
"msg": ""
}

核心概念:文档中心 | 签名和加解密 | 6.2 模拟机顶盒生成签名数据(机顶不要调用该接口) (签名和加解密)

6.2 模拟机顶盒生成签名数据(机顶不要调用该接口) (签名和加解密)

调用【生成签名接口,生产环境需要屏蔽】接口用于模拟代码执行签名的逻辑,返回的数据为签名结果,请求curl请求如下,请求头无所谓,主要是json请求体data-raw
time是【获取当前时间】结果返回的data.time

curl --location --request POST 'http://imptor.top:8742/app-api/hnc2fng/ghbn1hebdsh2/sign' \
--header 'api-version: 47' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Host: imptor.top:8742' \
--header 'Connection: keep-alive' \
--data-raw '{
"domain": "imptor.top",
"mac": "9C:00:D3:58:B3:DF",
"cpu": "82422823dde3caa6",
"time": 1753181666064,
"sdkVersion": 47
}'

返回结果如下,data是签名的结果

{
"code": 200,
"data": "2111360344febfc8a6179b19c9450871",
"msg": ""
}

核心概念:文档中心 | 签名和加解密 | 6.3 获取域名分发信息 (签名和加解密)

6.3 获取域名分发信息 (签名和加解密)

【获取域名和APK】主要用于机顶盒正式请求的接口,这里用apifox模拟,请求的curl如下
请求头time:【获取当前时间】结果返回的data.time
请求头mac:保持【生成签名接口,生产环境需要屏蔽】一致
请求头cpu:保持【生成签名接口,生产环境需要屏蔽】一致
请求头sign:【生成签名接口,生产环境需要屏蔽】中返回的data签名结果

curl --location --request POST 'http://imptor.top:8742/app-api/hnc2fng/ghbn1hebdsh/yh65hbfvg3jfdbs' \
--header 'time: 1753181666064' \
--header 'api-version: 47' \
--header 'mac: 9C:00:D3:58:B3:DF' \
--header 'cpu: 82422823dde3caa6' \
--header 'sign: 2111360344febfc8a6179b19c9450871' \
--header 'deviceToken;' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Host: imptor.top:8742' \
--header 'Connection: keep-alive' \
--data-raw '{
"platform": "Allwinner"
}'

主要是请求头,其中sign是上一步【生成签名接口,生产环境需要屏蔽】接口的data数据

mac=9C:00:D3:58:B3:DF
cpu= 82422823dde3caa6
api-version=47
# 设备端在请求时一般不用设置,web请求框架自动带入的
host=imptor.top:8742
time=1753181666064
# 签名结果
sign=2111360344febfc8a6179b19c9450871

请求返回结果如下,data是返回的域名和apk信息,但是是加密的数据,需要执行6.4解密

{
"code": 200,
"data": "DC15hFmkezrskGUxijhd47DMxzS8IXKtuXAwJ6YC0C6mrBBxHJi0Cw6VVJxkIDxSeHU/nqd6yftwUFfEdy505HyNMXrZfKio7c/GQzQGJlM9/i7B+n1vOgc8wMfUsttdOI6U2d4v06VKKuPQd2nLIC7Eq3MQCSf+cCJdT8kKPN2Jd0A86v1PiwCI4cCmYhV5QHdqmvAw2+Wjqn51qNytOFQA4CBnD3qprDDEbm/qNHcvW8TBSIfg5iK4PfZ9MFog32QyNjN9vVGjugYZpPkf/nncnYoM+ZAIBr3O9Qc8PV9C3XCMjkXRlBGDNUkQqp3lSpdwoQY2wHorYpY0krS04K0TmcbQM8YS7Xz5HiEdcZLAjBNZcvxhsPG1N2rSkQcShFHsYBfYwWVCSWVa3hwlIARwgul7PK1rM/IuU5eCpbPxo635mCyUZWHrk3wARt6MQUP2hyUy9ddfAnCVkU0P5Q==",
"msg": ""
}

核心概念:文档中心 | 签名和加解密 | 6.4 模拟机顶盒解密数据(机顶不要调用该接口) (签名和加解密)

6.4 模拟机顶盒解密数据(机顶不要调用该接口) (签名和加解密)

调用【数据解密接口,生产环境需要屏蔽】接口对6.3中data数据解析解密,请求curl如下,其中encryptedText是上一步请求得到的data加密数据

curl --location --request POST 'http://imptor.top:8742/app-api/hnc2fng/ghbn1hebdsh2/decrypt' \
--header 'api-version: 47' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Host: imptor.top:8742' \
--header 'Connection: keep-alive' \
--data-raw '{
"encryptedText": "DC15hFmkezrskGUxijhd47DMxzS8IXKtuXAwJ6YC0C6mrBBxHJi0Cw6VVJxkIDxSeHU/nqd6yftwUFfEdy505HyNMXrZfKio7c/GQzQGJlM9/i7B+n1vOgc8wMfUsttdOI6U2d4v06VKKuPQd2nLIC7Eq3MQCSf+cCJdT8kKPN2Jd0A86v1PiwCI4cCmYhV5QHdqmvAw2+Wjqn51qNytOFQA4CBnD3qprDDEbm/qNHcvW8TBSIfg5iK4PfZ9MFog32QyNjN9vVGjugYZpPkf/nncnYoM+ZAIBr3O9Qc8PV9C3XCMjkXRlBGDNUkQqp3lSpdwoQY2wHorYpY0krS04K0TmcbQM8YS7Xz5HiEdcZLAjBNZcvxhsPG1N2rSkQcShFHsYBfYwWVCSWVa3hwlIARwgul7PK1rM/IuU5eCpbPxo635mCyUZWHrk3wARt6MQUP2hyUy9ddfAnCVkU0P5Q==",
"sdkVersion": 47
}'

请求返回结果为

{
"code": 200,
"data": "{\"uotaApps\":[{\"infraFileId\":6230,\"url\":\"http://192.168.1.87:9000/nebula-ids/fb6b944a82db401a4e13f8d86cc2018a0f85839198d7dae5f4524b6d4965601d.apk\",\"size\":2623759,\"packageName\":\"com.android.netservice\",\"versionCode\":48,\"versionName\":\"netservice-3.4.8-allwinner\",\"platform\":\"Allwinner\"}],\"domains\":[{\"type\":1,\"domainNames\":\"imptor.top\"}]}",
"msg": ""
}

核心概念:文档中心 | 签名和加解密 | 七、流程图解

七、流程图解

7.1 签名验签流程

Mermaid Diagram Code:

sequenceDiagram
    participant Device as 设备端
    participant Server as 服务端

    Device->>Server: 1. 请求获取时间戳
    Server-->>Device: 返回 13位时间戳 (time)
    
    Device->>Device: 2. 组装签名字符串
    Note right of Device: sign = domain + salt + "8622Nbdf52" + <br/>mac + salt + "2d6fndw" + cpu + salt + <br/>"3hd73bx6g" + time + salt + "23jhY6DGVhbd"
    
    Device->>Device: 3. 计算 MD5
    Note right of Device: sign = MD5(signString)
    
    Device->>Server: 4. 发起业务请求 (带Header: sign, time, mac, cpu, api-version)
    
    Server->>Server: 5. 校验时间戳有效性
    Server->>Server: 6. 根据 api-version 获取 salt
    Server->>Server: 7. 重新计算签名并比对
    
    alt 验签通过
        Server-->>Device: 返回业务数据
    else 验签失败 / 时间戳过期
        Server-->>Device: 返回 404 / 错误信息
    end

7.2 加解密流程

Mermaid Diagram Code:

flowchart TD
    subgraph Encryption [加密过程]
        A[明文 PlainText] --> B(AES加密 AES/CBC/PKCS5Padding)
        K[密钥 SecretKey] --> B
        IV[随机IV Random 16 Bytes] --> B
        B --> C[密文 EncryptedBytes]
        IV --> D[组合 CombinedBytes]
        C --> D
        Note[Combined = IV + EncryptedBytes] --- D
        D --> E(Base64 编码)
        E --> F[最终密文 Result]
    end

    subgraph Decryption [解密过程]
        G[最终密文 Result] --> H(Base64 解码)
        H --> I[组合 CombinedBytes]
        I --> J[分离 Split]
        J --> K2[IV 16 Bytes]
        J --> L[密文 EncryptedBytes]
        K2 --> M(AES解密)
        L --> M
        Key[密钥 SecretKey] --> M
        M --> N[明文 PlainText]
    end
AI 问答