docs: 补充 UX 集成模式与授权对接说明
This commit is contained in:
756
docs/工具箱端-授权对接指南/工具箱端-摘要信息二维码生成指南.md
Normal file
756
docs/工具箱端-授权对接指南/工具箱端-摘要信息二维码生成指南.md
Normal file
@@ -0,0 +1,756 @@
|
||||
# 工具箱端 - 摘要信息二维码生成指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档说明工具箱端如何生成摘要信息二维码。工具箱完成检查任务后,需要将摘要信息加密并生成二维码,供 App 扫描后上传到平台。
|
||||
|
||||
> ### UX 集成模式补充(当前项目实现)
|
||||
>
|
||||
> 在当前集成模式中,工具箱将摘要明文传给 UX 的 `crypto.encryptSummary`,
|
||||
> 由 UX 执行 HKDF + AES-256-GCM 加密并返回二维码内容 JSON(`taskId + encrypted`)。
|
||||
|
||||
## 一、业务流程
|
||||
|
||||
```
|
||||
工具箱完成检查 → 准备摘要信息 → HKDF派生密钥 → AES-256-GCM加密 → 组装二维码内容 → 生成二维码
|
||||
↓
|
||||
App扫描二维码 → 提取taskId和encrypted → 提交到平台 → 平台解密验证 → 保存摘要信息
|
||||
```
|
||||
|
||||
## 二、二维码内容格式
|
||||
|
||||
二维码内容为 JSON 格式,包含以下字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"taskId": "TASK-20260115-4875",
|
||||
"encrypted": "uWUcAmp6UQd0w3G3crdsd4613QCxGLoEgslgXJ4G2hQhpQdjtghtQjCBUZwB/JO+NRgH1vSTr8dqBJRq7Qh4nugESrB2jUSGASTf4+5E7cLlDOmtDw7QlqS+6Hb7sn3daMSOovcna07huchHeesrJCiHV8ntEDXdCCdQOEHfkZAvy5gS8jQY41x5Qcnmqbz3qqHTmceIihTj4uqRVyKOE8jxzY6ko76jx0gW239gyFysJUTrqSPiFAr+gToi2l9SWP8ISViBmYmCY2cQtKvPfTKXwxGMid0zE/nDmb9n38X1oR05nAP0v1KaVY7iPcjsWySDGqO2iIbPzV8tQzq5TNuYqn9gvxIX/oRTFECP+aosfmOD5I8H8rVFTebyTHw+ONV3KoN2IMRqnG+a2lucbhzwQk7/cX1hs9lYm+yapmp+0MbLCtf2KMWqJPdeZqTVZgi3R181BCxo3OIwcCFTnZ/b9pdw+q8ai6SJpso5mA0TpUCvqYlGlKMZde0nj07kmLpdAm3AOg3GtPezfJu8iHmsc4PTa8RDsPgTIxcdyxNSMqo1Ws3VLQXm6DHK/kma/vbvSA/N7upPzi7wLvboig=="
|
||||
}
|
||||
```
|
||||
|
||||
### 2.1 字段说明
|
||||
|
||||
| 字段名 | 类型 | 说明 | 示例 |
|
||||
|--------|------|------|------|
|
||||
| `taskId` | String | 任务ID(从任务二维码中获取) | `"TASK-20260115-4875"` |
|
||||
| `encrypted` | String | Base64编码的加密数据 | `"uWUcAmp6UQd0w3G3..."` |
|
||||
|
||||
## 三、摘要信息数据结构
|
||||
|
||||
### 3.1 明文数据 JSON 格式
|
||||
|
||||
加密前的摘要信息为 JSON 格式,包含以下字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"enterpriseId": "1173040813421105152",
|
||||
"inspectionId": "702286470691215417",
|
||||
"summary": "检查摘要信息",
|
||||
"timestamp": 1734571234567
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 字段说明
|
||||
|
||||
| 字段名 | 类型 | 说明 | 示例 |
|
||||
|--------|------|------|------|
|
||||
| `enterpriseId` | String | 企业ID(从任务数据中获取) | `"1173040813421105152"` |
|
||||
| `inspectionId` | String | 检查ID(从任务数据中获取) | `"702286470691215417"` |
|
||||
| `summary` | String | 检查摘要信息 | `"检查摘要信息"` |
|
||||
| `timestamp` | Number | 时间戳(毫秒) | `1734571234567` |
|
||||
|
||||
## 四、密钥派生(HKDF-SHA256)
|
||||
|
||||
### 4.1 密钥派生参数
|
||||
|
||||
使用 **HKDF-SHA256** 从 `licence + fingerprint` 派生 AES 密钥:
|
||||
|
||||
```
|
||||
AES Key = HKDF(
|
||||
input = licence + fingerprint, # 输入密钥材料(字符串拼接)
|
||||
salt = taskId, # Salt值(任务ID)
|
||||
info = "inspection_report_encryption", # Info值(固定值)
|
||||
hash = SHA-256, # 哈希算法
|
||||
length = 32 # 输出密钥长度(32字节 = 256位)
|
||||
)
|
||||
```
|
||||
|
||||
**重要说明**:
|
||||
- `ikm`(输入密钥材料)= `licence + fingerprint`(直接字符串拼接,无分隔符)
|
||||
- `salt` = `taskId`(从任务二维码中获取的任务ID)
|
||||
- `info` = `"inspection_report_encryption"`(固定值,区分不同用途的密钥)
|
||||
- `length` = `32` 字节(AES-256 密钥长度)
|
||||
|
||||
### 4.2 Python 实现示例
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
import hkdf
|
||||
|
||||
def derive_aes_key(licence: str, fingerprint: str, task_id: str) -> bytes:
|
||||
"""
|
||||
使用 HKDF-SHA256 派生 AES-256 密钥
|
||||
|
||||
Args:
|
||||
licence: 设备授权码
|
||||
fingerprint: 设备硬件指纹
|
||||
task_id: 任务ID
|
||||
|
||||
Returns:
|
||||
派生出的密钥(32字节)
|
||||
"""
|
||||
# 输入密钥材料
|
||||
ikm = licence + fingerprint # 直接字符串拼接
|
||||
|
||||
# HKDF 参数
|
||||
salt = task_id
|
||||
info = "inspection_report_encryption"
|
||||
key_length = 32 # 32字节 = 256位
|
||||
|
||||
# 派生密钥
|
||||
derived_key = hkdf.HKDF(
|
||||
algorithm=hashlib.sha256,
|
||||
length=key_length,
|
||||
salt=salt.encode('utf-8'),
|
||||
info=info.encode('utf-8'),
|
||||
ikm=ikm.encode('utf-8')
|
||||
).derive()
|
||||
|
||||
return derived_key
|
||||
```
|
||||
|
||||
### 4.3 Java/Kotlin 实现示例
|
||||
|
||||
```kotlin
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest
|
||||
import org.bouncycastle.crypto.generators.HKDFBytesGenerator
|
||||
import org.bouncycastle.crypto.params.HKDFParameters
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
fun deriveAesKey(licence: String, fingerprint: String, taskId: String): ByteArray {
|
||||
// 输入密钥材料
|
||||
val ikm = (licence + fingerprint).toByteArray(StandardCharsets.UTF_8)
|
||||
|
||||
// HKDF 参数
|
||||
val salt = taskId.toByteArray(StandardCharsets.UTF_8)
|
||||
val info = "inspection_report_encryption".toByteArray(StandardCharsets.UTF_8)
|
||||
val keyLength = 32 // 32字节 = 256位
|
||||
|
||||
// 派生密钥
|
||||
val hkdf = HKDFBytesGenerator(SHA256Digest())
|
||||
val params = HKDFParameters(ikm, salt, info)
|
||||
hkdf.init(params)
|
||||
|
||||
val derivedKey = ByteArray(keyLength)
|
||||
hkdf.generateBytes(derivedKey, 0, keyLength)
|
||||
|
||||
return derivedKey
|
||||
}
|
||||
```
|
||||
|
||||
## 五、AES-256-GCM 加密
|
||||
|
||||
### 5.1 加密算法
|
||||
|
||||
- **算法**:AES-256-GCM(Galois/Counter Mode)
|
||||
- **密钥长度**:256 位(32 字节)
|
||||
- **IV 长度**:12 字节(96 位)
|
||||
- **认证标签长度**:16 字节(128 位)
|
||||
|
||||
### 5.2 加密数据格式
|
||||
|
||||
加密后的数据格式(Base64 编码前):
|
||||
|
||||
```
|
||||
[IV(12字节)] + [加密数据] + [认证标签(16字节)]
|
||||
```
|
||||
|
||||
**数据布局**:
|
||||
```
|
||||
+------------------+------------------+------------------+
|
||||
| IV (12字节) | 加密数据 | 认证标签(16字节)|
|
||||
+------------------+------------------+------------------+
|
||||
```
|
||||
|
||||
### 5.3 Python 实现示例
|
||||
|
||||
```python
|
||||
import base64
|
||||
import hashlib
|
||||
import hkdf
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
import json
|
||||
import time
|
||||
|
||||
def encrypt_summary_data(
|
||||
enterprise_id: str,
|
||||
inspection_id: str,
|
||||
summary: str,
|
||||
licence: str,
|
||||
fingerprint: str,
|
||||
task_id: str
|
||||
) -> str:
|
||||
"""
|
||||
加密摘要信息数据
|
||||
|
||||
Args:
|
||||
enterprise_id: 企业ID
|
||||
inspection_id: 检查ID
|
||||
summary: 摘要信息
|
||||
licence: 设备授权码
|
||||
fingerprint: 设备硬件指纹
|
||||
task_id: 任务ID
|
||||
|
||||
Returns:
|
||||
Base64编码的加密数据
|
||||
"""
|
||||
# 1. 组装明文数据(JSON格式)
|
||||
timestamp = int(time.time() * 1000) # 毫秒时间戳
|
||||
plaintext_map = {
|
||||
"enterpriseId": str(enterprise_id),
|
||||
"inspectionId": str(inspection_id),
|
||||
"summary": summary,
|
||||
"timestamp": timestamp
|
||||
}
|
||||
plaintext = json.dumps(plaintext_map, ensure_ascii=False)
|
||||
|
||||
# 2. 使用 HKDF-SHA256 派生 AES 密钥
|
||||
ikm = licence + fingerprint
|
||||
salt = task_id
|
||||
info = "inspection_report_encryption"
|
||||
key_length = 32
|
||||
|
||||
aes_key = hkdf.HKDF(
|
||||
algorithm=hashlib.sha256,
|
||||
length=key_length,
|
||||
salt=salt.encode('utf-8'),
|
||||
info=info.encode('utf-8'),
|
||||
ikm=ikm.encode('utf-8')
|
||||
).derive()
|
||||
|
||||
# 3. 使用 AES-256-GCM 加密数据
|
||||
aesgcm = AESGCM(aes_key)
|
||||
iv = os.urandom(12) # 生成12字节随机IV
|
||||
encrypted_bytes = aesgcm.encrypt(iv, plaintext.encode('utf-8'), None)
|
||||
|
||||
# 4. 组装:IV + 加密数据(包含认证标签)
|
||||
# AESGCM.encrypt 返回的格式已经是:加密数据 + 认证标签
|
||||
combined = iv + encrypted_bytes
|
||||
|
||||
# 5. Base64 编码
|
||||
encrypted_base64 = base64.b64encode(combined).decode('utf-8')
|
||||
|
||||
return encrypted_base64
|
||||
```
|
||||
|
||||
### 5.4 Java/Kotlin 实现示例
|
||||
|
||||
```kotlin
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest
|
||||
import org.bouncycastle.crypto.generators.HKDFBytesGenerator
|
||||
import org.bouncycastle.crypto.params.HKDFParameters
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.SecureRandom
|
||||
import java.util.Base64
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object SummaryEncryptionUtil {
|
||||
|
||||
private const val ALGORITHM = "AES"
|
||||
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
||||
private const val GCM_IV_LENGTH = 12 // 12 bytes = 96 bits
|
||||
private const val GCM_TAG_LENGTH = 16 // 16 bytes = 128 bits
|
||||
private const val GCM_TAG_LENGTH_BITS = GCM_TAG_LENGTH * 8 // 128 bits
|
||||
|
||||
private val objectMapper = ObjectMapper()
|
||||
private val secureRandom = SecureRandom()
|
||||
|
||||
/**
|
||||
* 加密摘要信息数据
|
||||
*/
|
||||
fun encryptSummaryData(
|
||||
enterpriseId: String,
|
||||
inspectionId: String,
|
||||
summary: String,
|
||||
licence: String,
|
||||
fingerprint: String,
|
||||
taskId: String
|
||||
): String {
|
||||
// 1. 组装明文数据(JSON格式)
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val plaintextMap = mapOf(
|
||||
"enterpriseId" to enterpriseId,
|
||||
"inspectionId" to inspectionId,
|
||||
"summary" to summary,
|
||||
"timestamp" to timestamp
|
||||
)
|
||||
val plaintext = objectMapper.writeValueAsString(plaintextMap)
|
||||
|
||||
// 2. 使用 HKDF-SHA256 派生 AES 密钥
|
||||
val ikm = (licence + fingerprint).toByteArray(StandardCharsets.UTF_8)
|
||||
val salt = taskId.toByteArray(StandardCharsets.UTF_8)
|
||||
val info = "inspection_report_encryption".toByteArray(StandardCharsets.UTF_8)
|
||||
val keyLength = 32
|
||||
|
||||
val hkdf = HKDFBytesGenerator(SHA256Digest())
|
||||
val params = HKDFParameters(ikm, salt, info)
|
||||
hkdf.init(params)
|
||||
|
||||
val aesKey = ByteArray(keyLength)
|
||||
hkdf.generateBytes(aesKey, 0, keyLength)
|
||||
|
||||
// 3. 使用 AES-256-GCM 加密数据
|
||||
val iv = ByteArray(GCM_IV_LENGTH)
|
||||
secureRandom.nextBytes(iv)
|
||||
|
||||
val secretKey = SecretKeySpec(aesKey, ALGORITHM)
|
||||
val gcmSpec = GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv)
|
||||
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec)
|
||||
|
||||
val plaintextBytes = plaintext.toByteArray(StandardCharsets.UTF_8)
|
||||
val encryptedBytes = cipher.doFinal(plaintextBytes)
|
||||
|
||||
// 4. 组装:IV + 加密数据(包含认证标签)
|
||||
// GCM 模式会将认证标签附加到密文末尾
|
||||
val ciphertext = encryptedBytes.sliceArray(0 until encryptedBytes.size - GCM_TAG_LENGTH)
|
||||
val tag = encryptedBytes.sliceArray(encryptedBytes.size - GCM_TAG_LENGTH until encryptedBytes.size)
|
||||
|
||||
val combined = iv + ciphertext + tag
|
||||
|
||||
// 5. Base64 编码
|
||||
return Base64.getEncoder().encodeToString(combined)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 六、组装二维码内容
|
||||
|
||||
### 6.1 二维码内容 JSON
|
||||
|
||||
将 `taskId` 和加密后的 `encrypted` 组装成 JSON 格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"taskId": "TASK-20260115-4875",
|
||||
"encrypted": "Base64编码的加密数据"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Python 实现示例
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
def generate_qr_code_content(task_id: str, encrypted: str) -> str:
|
||||
"""
|
||||
生成二维码内容(JSON格式)
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
encrypted: Base64编码的加密数据
|
||||
|
||||
Returns:
|
||||
JSON格式的字符串
|
||||
"""
|
||||
qr_content = {
|
||||
"taskId": task_id,
|
||||
"encrypted": encrypted
|
||||
}
|
||||
return json.dumps(qr_content, ensure_ascii=False)
|
||||
```
|
||||
|
||||
## 七、完整流程示例
|
||||
|
||||
### 7.1 Python 完整示例
|
||||
|
||||
```python
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
import hkdf
|
||||
import qrcode
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
import os
|
||||
|
||||
class SummaryQRCodeGenerator:
|
||||
"""摘要信息二维码生成器"""
|
||||
|
||||
def __init__(self, licence: str, fingerprint: str):
|
||||
"""
|
||||
初始化生成器
|
||||
|
||||
Args:
|
||||
licence: 设备授权码
|
||||
fingerprint: 设备硬件指纹
|
||||
"""
|
||||
self.licence = licence
|
||||
self.fingerprint = fingerprint
|
||||
|
||||
def generate_summary_qr_code(
|
||||
self,
|
||||
task_id: str,
|
||||
enterprise_id: str,
|
||||
inspection_id: str,
|
||||
summary: str,
|
||||
output_path: str = "summary_qr.png"
|
||||
) -> str:
|
||||
"""
|
||||
生成摘要信息二维码
|
||||
|
||||
Args:
|
||||
task_id: 任务ID(从任务二维码中获取)
|
||||
enterprise_id: 企业ID(从任务数据中获取)
|
||||
inspection_id: 检查ID(从任务数据中获取)
|
||||
summary: 摘要信息
|
||||
output_path: 二维码图片保存路径
|
||||
|
||||
Returns:
|
||||
二维码内容(JSON字符串)
|
||||
"""
|
||||
# 1. 组装明文数据(JSON格式)
|
||||
timestamp = int(time.time() * 1000) # 毫秒时间戳
|
||||
plaintext_map = {
|
||||
"enterpriseId": str(enterprise_id),
|
||||
"inspectionId": str(inspection_id),
|
||||
"summary": summary,
|
||||
"timestamp": timestamp
|
||||
}
|
||||
plaintext = json.dumps(plaintext_map, ensure_ascii=False)
|
||||
print(f"明文数据: {plaintext}")
|
||||
|
||||
# 2. 使用 HKDF-SHA256 派生 AES 密钥
|
||||
ikm = self.licence + self.fingerprint
|
||||
salt = task_id
|
||||
info = "inspection_report_encryption"
|
||||
key_length = 32
|
||||
|
||||
aes_key = hkdf.HKDF(
|
||||
algorithm=hashlib.sha256,
|
||||
length=key_length,
|
||||
salt=salt.encode('utf-8'),
|
||||
info=info.encode('utf-8'),
|
||||
ikm=ikm.encode('utf-8')
|
||||
).derive()
|
||||
print(f"密钥派生成功: {len(aes_key)} 字节")
|
||||
|
||||
# 3. 使用 AES-256-GCM 加密数据
|
||||
aesgcm = AESGCM(aes_key)
|
||||
iv = os.urandom(12) # 生成12字节随机IV
|
||||
encrypted_bytes = aesgcm.encrypt(iv, plaintext.encode('utf-8'), None)
|
||||
|
||||
# 组装:IV + 加密数据(包含认证标签)
|
||||
combined = iv + encrypted_bytes
|
||||
|
||||
# Base64 编码
|
||||
encrypted_base64 = base64.b64encode(combined).decode('utf-8')
|
||||
print(f"加密成功: {encrypted_base64[:50]}...")
|
||||
|
||||
# 4. 组装二维码内容(JSON格式)
|
||||
qr_content = {
|
||||
"taskId": task_id,
|
||||
"encrypted": encrypted_base64
|
||||
}
|
||||
qr_content_json = json.dumps(qr_content, ensure_ascii=False)
|
||||
print(f"二维码内容: {qr_content_json[:100]}...")
|
||||
|
||||
# 5. 生成二维码
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_M,
|
||||
box_size=10,
|
||||
border=4,
|
||||
)
|
||||
qr.add_data(qr_content_json)
|
||||
qr.make(fit=True)
|
||||
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
img.save(output_path)
|
||||
print(f"二维码已生成: {output_path}")
|
||||
|
||||
return qr_content_json
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
# 工具箱的授权信息(必须与平台绑定时一致)
|
||||
licence = "LIC-8F2A-XXXX"
|
||||
fingerprint = "FP-2c91e9f3"
|
||||
|
||||
# 创建生成器
|
||||
generator = SummaryQRCodeGenerator(licence, fingerprint)
|
||||
|
||||
# 从任务二维码中获取的信息
|
||||
task_id = "TASK-20260115-4875"
|
||||
enterprise_id = "1173040813421105152"
|
||||
inspection_id = "702286470691215417"
|
||||
summary = "检查摘要信息:发现3个高危漏洞,5个中危漏洞"
|
||||
|
||||
# 生成二维码
|
||||
qr_content = generator.generate_summary_qr_code(
|
||||
task_id=task_id,
|
||||
enterprise_id=enterprise_id,
|
||||
inspection_id=inspection_id,
|
||||
summary=summary,
|
||||
output_path="summary_qr_code.png"
|
||||
)
|
||||
|
||||
print(f"\n二维码内容:\n{qr_content}")
|
||||
```
|
||||
|
||||
### 7.2 Java/Kotlin 完整示例
|
||||
|
||||
```kotlin
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest
|
||||
import org.bouncycastle.crypto.generators.HKDFBytesGenerator
|
||||
import org.bouncycastle.crypto.params.HKDFParameters
|
||||
import java.awt.image.BufferedImage
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.SecureRandom
|
||||
import java.util.Base64
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import javax.imageio.ImageIO
|
||||
import java.io.File
|
||||
|
||||
class SummaryQRCodeGenerator(
|
||||
private val licence: String,
|
||||
private val fingerprint: String
|
||||
) {
|
||||
|
||||
private const val ALGORITHM = "AES"
|
||||
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
||||
private const val GCM_IV_LENGTH = 12
|
||||
private const val GCM_TAG_LENGTH = 16
|
||||
private const val GCM_TAG_LENGTH_BITS = GCM_TAG_LENGTH * 8
|
||||
|
||||
private val objectMapper = ObjectMapper()
|
||||
private val secureRandom = SecureRandom()
|
||||
|
||||
/**
|
||||
* 生成摘要信息二维码
|
||||
*/
|
||||
fun generateSummaryQRCode(
|
||||
taskId: String,
|
||||
enterpriseId: String,
|
||||
inspectionId: String,
|
||||
summary: String,
|
||||
outputPath: String = "summary_qr.png"
|
||||
): String {
|
||||
// 1. 组装明文数据(JSON格式)
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val plaintextMap = mapOf(
|
||||
"enterpriseId" to enterpriseId,
|
||||
"inspectionId" to inspectionId,
|
||||
"summary" to summary,
|
||||
"timestamp" to timestamp
|
||||
)
|
||||
val plaintext = objectMapper.writeValueAsString(plaintextMap)
|
||||
println("明文数据: $plaintext")
|
||||
|
||||
// 2. 使用 HKDF-SHA256 派生 AES 密钥
|
||||
val ikm = (licence + fingerprint).toByteArray(StandardCharsets.UTF_8)
|
||||
val salt = taskId.toByteArray(StandardCharsets.UTF_8)
|
||||
val info = "inspection_report_encryption".toByteArray(StandardCharsets.UTF_8)
|
||||
val keyLength = 32
|
||||
|
||||
val hkdf = HKDFBytesGenerator(SHA256Digest())
|
||||
val params = HKDFParameters(ikm, salt, info)
|
||||
hkdf.init(params)
|
||||
|
||||
val aesKey = ByteArray(keyLength)
|
||||
hkdf.generateBytes(aesKey, 0, keyLength)
|
||||
println("密钥派生成功: ${aesKey.size} 字节")
|
||||
|
||||
// 3. 使用 AES-256-GCM 加密数据
|
||||
val iv = ByteArray(GCM_IV_LENGTH)
|
||||
secureRandom.nextBytes(iv)
|
||||
|
||||
val secretKey = SecretKeySpec(aesKey, ALGORITHM)
|
||||
val gcmSpec = GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv)
|
||||
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec)
|
||||
|
||||
val plaintextBytes = plaintext.toByteArray(StandardCharsets.UTF_8)
|
||||
val encryptedBytes = cipher.doFinal(plaintextBytes)
|
||||
|
||||
// 组装:IV + 加密数据(包含认证标签)
|
||||
val ciphertext = encryptedBytes.sliceArray(0 until encryptedBytes.size - GCM_TAG_LENGTH)
|
||||
val tag = encryptedBytes.sliceArray(encryptedBytes.size - GCM_TAG_LENGTH until encryptedBytes.size)
|
||||
|
||||
val combined = iv + ciphertext + tag
|
||||
|
||||
// Base64 编码
|
||||
val encryptedBase64 = Base64.getEncoder().encodeToString(combined)
|
||||
println("加密成功: ${encryptedBase64.take(50)}...")
|
||||
|
||||
// 4. 组装二维码内容(JSON格式)
|
||||
val qrContent = mapOf(
|
||||
"taskId" to taskId,
|
||||
"encrypted" to encryptedBase64
|
||||
)
|
||||
val qrContentJson = objectMapper.writeValueAsString(qrContent)
|
||||
println("二维码内容: ${qrContentJson.take(100)}...")
|
||||
|
||||
// 5. 生成二维码
|
||||
val hints = hashMapOf<EncodeHintType, Any>().apply {
|
||||
put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M)
|
||||
put(EncodeHintType.CHARACTER_SET, "UTF-8")
|
||||
put(EncodeHintType.MARGIN, 1)
|
||||
}
|
||||
|
||||
val writer = QRCodeWriter()
|
||||
val bitMatrix = writer.encode(qrContentJson, BarcodeFormat.QR_CODE, 300, 300, hints)
|
||||
|
||||
val width = bitMatrix.width
|
||||
val height = bitMatrix.height
|
||||
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
|
||||
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
image.setRGB(x, y, if (bitMatrix[x, y]) 0x000000 else 0xFFFFFF)
|
||||
}
|
||||
}
|
||||
|
||||
ImageIO.write(image, "PNG", File(outputPath))
|
||||
println("二维码已生成: $outputPath")
|
||||
|
||||
return qrContentJson
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
fun main() {
|
||||
// 工具箱的授权信息(必须与平台绑定时一致)
|
||||
val licence = "LIC-8F2A-XXXX"
|
||||
val fingerprint = "FP-2c91e9f3"
|
||||
|
||||
// 创建生成器
|
||||
val generator = SummaryQRCodeGenerator(licence, fingerprint)
|
||||
|
||||
// 从任务二维码中获取的信息
|
||||
val taskId = "TASK-20260115-4875"
|
||||
val enterpriseId = "1173040813421105152"
|
||||
val inspectionId = "702286470691215417"
|
||||
val summary = "检查摘要信息:发现3个高危漏洞,5个中危漏洞"
|
||||
|
||||
// 生成二维码
|
||||
val qrContent = generator.generateSummaryQRCode(
|
||||
taskId = taskId,
|
||||
enterpriseId = enterpriseId,
|
||||
inspectionId = inspectionId,
|
||||
summary = summary,
|
||||
outputPath = "summary_qr_code.png"
|
||||
)
|
||||
|
||||
println("\n二维码内容:\n$qrContent")
|
||||
}
|
||||
```
|
||||
|
||||
## 八、平台端验证流程
|
||||
|
||||
平台端会按以下流程验证:
|
||||
|
||||
1. **接收请求**:App 扫描二维码后,将 `taskId` 和 `encrypted` 提交到平台
|
||||
2. **查询任务**:根据 `taskId` 查询任务记录,获取 `deviceLicenceId`
|
||||
3. **获取设备信息**:根据 `deviceLicenceId` 查询设备授权记录,获取 `licence` 和 `fingerprint`
|
||||
4. **密钥派生**:使用相同的 HKDF 参数派生 AES 密钥
|
||||
5. **解密数据**:使用 AES-256-GCM 解密(自动验证认证标签)
|
||||
6. **时间戳校验**:验证 `timestamp` 是否在合理范围内(防止重放攻击)
|
||||
7. **保存摘要**:将摘要信息保存到数据库
|
||||
|
||||
## 九、常见错误和注意事项
|
||||
|
||||
### 9.1 加密失败
|
||||
|
||||
**可能原因**:
|
||||
1. **密钥派生错误**:确保使用正确的 HKDF 参数
|
||||
- `ikm` = `licence + fingerprint`(直接字符串拼接)
|
||||
- `salt` = `taskId`(必须与任务二维码中的 taskId 一致)
|
||||
- `info` = `"inspection_report_encryption"`(固定值)
|
||||
- `length` = `32` 字节
|
||||
|
||||
2. **数据格式错误**:确保 JSON 格式正确
|
||||
- 字段名和类型必须匹配
|
||||
- 时间戳必须是数字类型(毫秒)
|
||||
|
||||
3. **IV 生成错误**:确保使用安全的随机数生成器生成 12 字节 IV
|
||||
|
||||
### 9.2 平台验证失败
|
||||
|
||||
**可能原因**:
|
||||
1. **taskId 不匹配**:确保二维码中的 `taskId` 与任务二维码中的 `taskId` 一致
|
||||
2. **密钥不匹配**:确保 `licence` 和 `fingerprint` 与平台绑定时一致
|
||||
3. **时间戳过期**:平台会验证时间戳,确保时间戳在合理范围内
|
||||
4. **认证标签验证失败**:数据被篡改或密钥错误
|
||||
|
||||
### 9.3 二维码生成失败
|
||||
|
||||
**可能原因**:
|
||||
1. **内容过长**:如果加密数据过长,可能需要更高版本的二维码
|
||||
2. **JSON 格式错误**:确保 JSON 格式正确
|
||||
3. **字符编码错误**:确保使用 UTF-8 编码
|
||||
|
||||
## 十、安全设计说明
|
||||
|
||||
### 10.1 为什么使用 HKDF
|
||||
|
||||
1. **密钥分离**:使用 `info` 参数区分不同用途的密钥
|
||||
2. **Salt 随机性**:使用 `taskId` 作为 salt,确保每个任务的密钥不同
|
||||
3. **密钥扩展**:HKDF 提供更好的密钥扩展性
|
||||
|
||||
### 10.2 为什么第三方无法伪造
|
||||
|
||||
1. **密钥绑定**:只有拥有正确 `licence + fingerprint` 的工具箱才能生成正确的密钥
|
||||
2. **任务绑定**:使用 `taskId` 作为 salt,确保密钥与特定任务绑定
|
||||
3. **认证加密**:GCM 模式提供认证加密,任何篡改都会导致解密失败
|
||||
4. **时间戳校验**:平台会验证时间戳,防止重放攻击
|
||||
|
||||
### 10.3 密钥派生参数的重要性
|
||||
|
||||
- **ikm**:`licence + fingerprint` 是设备唯一标识
|
||||
- **salt**:`taskId` 确保每个任务使用不同的密钥
|
||||
- **info**:`"inspection_report_encryption"` 区分不同用途的密钥
|
||||
- **length**:`32` 字节提供 256 位密钥强度
|
||||
|
||||
## 十一、测试建议
|
||||
|
||||
1. **单元测试**:
|
||||
- 测试密钥派生是否正确
|
||||
- 测试加密和解密是否匹配
|
||||
- 测试 JSON 格式是否正确
|
||||
|
||||
2. **集成测试**:
|
||||
- 使用真实任务数据生成二维码
|
||||
- App 扫描二维码并提交到平台
|
||||
- 验证平台是否能正确解密和验证
|
||||
|
||||
3. **边界测试**:
|
||||
- 测试超长的摘要信息
|
||||
- 测试特殊字符的处理
|
||||
- 测试错误的 taskId 是否会导致解密失败
|
||||
|
||||
## 十二、参考实现
|
||||
|
||||
- **Python**:`hkdf` 库(HKDF)、`cryptography` 库(AES-GCM)、`qrcode` 库(二维码生成)
|
||||
- **Java/Kotlin**:BouncyCastle(HKDF)、JDK `javax.crypto`(AES-GCM)、ZXing 库(二维码生成)
|
||||
- **C#**:BouncyCastle.Net(HKDF)、`System.Security.Cryptography`(AES-GCM)、ZXing.Net 库(二维码生成)
|
||||
|
||||
## 十三、联系支持
|
||||
|
||||
如有问题,请联系平台技术支持团队获取:
|
||||
- 测试环境地址
|
||||
- 技术支持
|
||||
|
||||
Reference in New Issue
Block a user