757 lines
25 KiB
Markdown
757 lines
25 KiB
Markdown
# 工具箱端 - 摘要信息二维码生成指南
|
||
|
||
## 概述
|
||
|
||
本文档说明工具箱端如何生成摘要信息二维码。工具箱完成检查任务后,需要将摘要信息加密并生成二维码,供 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 库(二维码生成)
|
||
|
||
## 十三、联系支持
|
||
|
||
如有问题,请联系平台技术支持团队获取:
|
||
- 测试环境地址
|
||
- 技术支持
|
||
|