# 工具箱端 - 任务二维码解密指南 ## 概述 本文档说明工具箱端如何解密任务二维码数据。App 创建任务后,平台会生成加密的任务数据并返回给 App,App 将其生成二维码。工具箱扫描二维码后,需要使用自己的 `licence` 和 `fingerprint` 解密任务数据。 > ### UX 集成模式补充(当前项目实现) > > 在当前集成模式中,工具箱扫描二维码后将密文提交给 UX 的 `crypto.decryptTask`, > 由 UX 使用设备绑定的 `licence + fingerprint` 执行 AES-256-GCM 解密并返回任务明文。 ## 一、业务流程 ``` App创建任务 → 平台加密任务数据 → 返回加密数据 → App生成二维码 ↓ 工具箱扫描二维码 → 提取加密数据 → AES-256-GCM解密 → 获取任务信息 ``` ## 二、任务数据结构 ### 2.1 任务数据 JSON 格式 解密后的任务数据为 JSON 格式,包含以下字段: ```json { "taskId": "TASK-20260115-4875", "enterpriseId": "1173040813421105152", "orgName": "超艺科技有限公司", "inspectionId": "702286470691215417", "inspectionPerson": "警务通", "issuedAt": 1734571234567 } ``` ### 2.2 字段说明 | 字段名 | 类型 | 说明 | 示例 | |--------|------|------|------| | `taskId` | String | 任务唯一ID(格式:TASK-YYYYMMDD-XXXX) | `"TASK-20260115-4875"` | | `enterpriseId` | String | 企业ID | `"1173040813421105152"` | | `orgName` | String | 单位名称 | `"超艺科技有限公司"` | | `inspectionId` | String | 检查ID | `"702286470691215417"` | | `inspectionPerson` | String | 检查人 | `"警务通"` | | `issuedAt` | Number | 任务发布时间戳(毫秒) | `1734571234567` | ## 三、加密算法说明 ### 3.1 加密方式 - **算法**:AES-256-GCM(Galois/Counter Mode) - **密钥长度**:256 位(32 字节) - **IV 长度**:12 字节(96 位) - **认证标签长度**:16 字节(128 位) ### 3.2 密钥生成 密钥由工具箱的 `licence` 和 `fingerprint` 生成: ``` 密钥 = SHA-256(licence + fingerprint) ``` **重要说明**: - `licence` 和 `fingerprint` 直接字符串拼接(无分隔符) - 使用 SHA-256 哈希算法的全部 32 字节作为 AES-256 密钥 - 工具箱必须使用与平台绑定时相同的 `licence` 和 `fingerprint` ### 3.3 加密数据格式 加密后的数据格式(Base64 编码前): ``` [IV(12字节)] + [加密数据] + [认证标签(16字节)] ``` **数据布局**: ``` +------------------+------------------+------------------+ | IV (12字节) | 加密数据 | 认证标签(16字节)| +------------------+------------------+------------------+ ``` ## 四、解密步骤 ### 4.1 解密流程 1. **扫描二维码**:获取 Base64 编码的加密数据 2. **Base64 解码**:将 Base64 字符串解码为字节数组 3. **分离数据**:从字节数组中分离 IV、加密数据和认证标签 4. **生成密钥**:使用 `licence + fingerprint` 生成 AES-256 密钥 5. **解密数据**:使用 AES-256-GCM 解密(自动验证认证标签) 6. **解析 JSON**:将解密后的字符串解析为 JSON 对象 ### 4.2 Python 实现示例 ```python import base64 import json import hashlib from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.backends import default_backend def decrypt_task_data( encrypted_data_base64: str, licence: str, fingerprint: str ) -> dict: """ 解密任务二维码数据 Args: encrypted_data_base64: Base64编码的加密数据 licence: 设备授权码 fingerprint: 设备硬件指纹 Returns: 解密后的任务数据(字典) """ # 1. Base64 解码 encrypted_bytes = base64.b64decode(encrypted_data_base64) # 2. 分离 IV 和加密数据(包含认证标签) if len(encrypted_bytes) < 12: raise ValueError("加密数据格式错误:数据长度不足") iv = encrypted_bytes[:12] # IV: 前12字节 ciphertext_with_tag = encrypted_bytes[12:] # 加密数据 + 认证标签 # 3. 生成密钥:SHA-256(licence + fingerprint) combined = licence + fingerprint key = hashlib.sha256(combined.encode('utf-8')).digest() # 4. 使用 AES-256-GCM 解密 aesgcm = AESGCM(key) decrypted_bytes = aesgcm.decrypt(iv, ciphertext_with_tag, None) # 5. 解析 JSON decrypted_json = decrypted_bytes.decode('utf-8') task_data = json.loads(decrypted_json) return task_data # 使用示例 if __name__ == "__main__": # 从二维码扫描获取的加密数据 encrypted_data = "Base64编码的加密数据..." # 工具箱的授权信息(必须与平台绑定时一致) licence = "LIC-8F2A-XXXX" fingerprint = "FP-2c91e9f3" # 解密任务数据 task_data = decrypt_task_data(encrypted_data, licence, fingerprint) print("任务ID:", task_data["taskId"]) print("企业ID:", task_data["enterpriseId"]) print("单位名称:", task_data["orgName"]) print("检查ID:", task_data["inspectionId"]) print("检查人:", task_data["inspectionPerson"]) print("发布时间:", task_data["issuedAt"]) ``` ### 4.3 Java/Kotlin 实现示例 ```kotlin import com.fasterxml.jackson.databind.ObjectMapper import java.nio.charset.StandardCharsets import java.security.MessageDigest import java.util.Base64 import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec object TaskDecryptionUtil { private const val ALGORITHM = "AES" private const val TRANSFORMATION = "AES/GCM/NoPadding" private const val GCM_IV_LENGTH = 12 // GCM 推荐使用 12 字节 IV private const val GCM_TAG_LENGTH = 16 // GCM 认证标签长度(128位) private const val KEY_LENGTH = 32 // AES-256 密钥长度(256位 = 32字节) private val objectMapper = ObjectMapper() /** * 解密任务二维码数据 * * @param encryptedDataBase64 Base64编码的加密数据 * @param licence 设备授权码 * @param fingerprint 设备硬件指纹 * @return 解密后的任务数据(Map) */ fun decryptTaskData( encryptedDataBase64: String, licence: String, fingerprint: String ): Map { // 1. Base64 解码 val encryptedBytes = Base64.getDecoder().decode(encryptedDataBase64) // 2. 分离 IV 和加密数据(包含认证标签) if (encryptedBytes.size < GCM_IV_LENGTH) { throw IllegalArgumentException("加密数据格式错误:数据长度不足") } val iv = encryptedBytes.sliceArray(0 until GCM_IV_LENGTH) val ciphertextWithTag = encryptedBytes.sliceArray(GCM_IV_LENGTH until encryptedBytes.size) // 3. 生成密钥:SHA-256(licence + fingerprint) val combined = "$licence$fingerprint" val digest = MessageDigest.getInstance("SHA-256") val keyBytes = digest.digest(combined.toByteArray(StandardCharsets.UTF_8)) val key = SecretKeySpec(keyBytes, ALGORITHM) // 4. 使用 AES-256-GCM 解密 val cipher = Cipher.getInstance(TRANSFORMATION) val parameterSpec = GCMParameterSpec(GCM_TAG_LENGTH * 8, iv) // 标签长度以位为单位 cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec) // 解密数据(GCM 会自动验证认证标签) val decryptedBytes = cipher.doFinal(ciphertextWithTag) // 5. 解析 JSON val decryptedJson = String(decryptedBytes, StandardCharsets.UTF_8) @Suppress("UNCHECKED_CAST") return objectMapper.readValue(decryptedJson, Map::class.java) as Map } } // 使用示例 fun main() { // 从二维码扫描获取的加密数据 val encryptedData = "Base64编码的加密数据..." // 工具箱的授权信息(必须与平台绑定时一致) val licence = "LIC-8F2A-XXXX" val fingerprint = "FP-2c91e9f3" // 解密任务数据 val taskData = TaskDecryptionUtil.decryptTaskData(encryptedData, licence, fingerprint) println("任务ID: ${taskData["taskId"]}") println("企业ID: ${taskData["enterpriseId"]}") println("单位名称: ${taskData["orgName"]}") println("检查ID: ${taskData["inspectionId"]}") println("检查人: ${taskData["inspectionPerson"]}") println("发布时间: ${taskData["issuedAt"]}") } ``` ### 4.4 C# 实现示例 ```csharp using System; using System.Security.Cryptography; using System.Text; using System.Text.Json; public class TaskDecryptionUtil { private const int GcmIvLength = 12; // GCM 推荐使用 12 字节 IV private const int GcmTagLength = 16; // GCM 认证标签长度(128位) /// /// 解密任务二维码数据 /// public static Dictionary DecryptTaskData( string encryptedDataBase64, string licence, string fingerprint ) { // 1. Base64 解码 byte[] encryptedBytes = Convert.FromBase64String(encryptedDataBase64); // 2. 分离 IV 和加密数据(包含认证标签) if (encryptedBytes.Length < GcmIvLength) { throw new ArgumentException("加密数据格式错误:数据长度不足"); } byte[] iv = new byte[GcmIvLength]; Array.Copy(encryptedBytes, 0, iv, 0, GcmIvLength); byte[] ciphertextWithTag = new byte[encryptedBytes.Length - GcmIvLength]; Array.Copy(encryptedBytes, GcmIvLength, ciphertextWithTag, 0, ciphertextWithTag.Length); // 3. 生成密钥:SHA-256(licence + fingerprint) string combined = licence + fingerprint; byte[] keyBytes = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(combined)); // 4. 使用 AES-256-GCM 解密 using (AesGcm aesGcm = new AesGcm(keyBytes)) { byte[] decryptedBytes = new byte[ciphertextWithTag.Length - GcmTagLength]; byte[] tag = new byte[GcmTagLength]; Array.Copy(ciphertextWithTag, ciphertextWithTag.Length - GcmTagLength, tag, 0, GcmTagLength); Array.Copy(ciphertextWithTag, 0, decryptedBytes, 0, decryptedBytes.Length); aesGcm.Decrypt(iv, decryptedBytes, tag, null, decryptedBytes); // 5. 解析 JSON string decryptedJson = Encoding.UTF8.GetString(decryptedBytes); return JsonSerializer.Deserialize>(decryptedJson); } } } // 使用示例 class Program { static void Main() { // 从二维码扫描获取的加密数据 string encryptedData = "Base64编码的加密数据..."; // 工具箱的授权信息(必须与平台绑定时一致) string licence = "LIC-8F2A-XXXX"; string fingerprint = "FP-2c91e9f3"; // 解密任务数据 var taskData = TaskDecryptionUtil.DecryptTaskData(encryptedData, licence, fingerprint); Console.WriteLine($"任务ID: {taskData["taskId"]}"); Console.WriteLine($"企业ID: {taskData["enterpriseId"]}"); Console.WriteLine($"单位名称: {taskData["orgName"]}"); Console.WriteLine($"检查ID: {taskData["inspectionId"]}"); Console.WriteLine($"检查人: {taskData["inspectionPerson"]}"); Console.WriteLine($"发布时间: {taskData["issuedAt"]}"); } } ``` ## 五、完整流程示例 ### 5.1 Python 完整示例(包含二维码扫描) ```python import base64 import json import hashlib from cryptography.hazmat.primitives.ciphers.aead import AESGCM from pyzbar import pyzbar from PIL import Image class TaskQRCodeDecoder: """任务二维码解码器""" def __init__(self, licence: str, fingerprint: str): """ 初始化解码器 Args: licence: 设备授权码 fingerprint: 设备硬件指纹 """ self.licence = licence self.fingerprint = fingerprint self._key = self._generate_key() def _generate_key(self) -> bytes: """生成 AES-256 密钥""" combined = self.licence + self.fingerprint return hashlib.sha256(combined.encode('utf-8')).digest() def scan_qr_code(self, qr_image_path: str) -> dict: """ 扫描二维码并解密任务数据 Args: qr_image_path: 二维码图片路径 Returns: 解密后的任务数据(字典) """ # 1. 扫描二维码 image = Image.open(qr_image_path) qr_codes = pyzbar.decode(image) if not qr_codes: raise ValueError("未找到二维码") # 获取二维码内容(Base64编码的加密数据) encrypted_data_base64 = qr_codes[0].data.decode('utf-8') print(f"扫描到二维码内容: {encrypted_data_base64[:50]}...") # 2. 解密任务数据 return self.decrypt_task_data(encrypted_data_base64) def decrypt_task_data(self, encrypted_data_base64: str) -> dict: """ 解密任务数据 Args: encrypted_data_base64: Base64编码的加密数据 Returns: 解密后的任务数据(字典) """ # 1. Base64 解码 encrypted_bytes = base64.b64decode(encrypted_data_base64) # 2. 分离 IV 和加密数据(包含认证标签) if len(encrypted_bytes) < 12: raise ValueError("加密数据格式错误:数据长度不足") iv = encrypted_bytes[:12] # IV: 前12字节 ciphertext_with_tag = encrypted_bytes[12:] # 加密数据 + 认证标签 # 3. 使用 AES-256-GCM 解密 aesgcm = AESGCM(self._key) decrypted_bytes = aesgcm.decrypt(iv, ciphertext_with_tag, None) # 4. 解析 JSON decrypted_json = decrypted_bytes.decode('utf-8') task_data = json.loads(decrypted_json) return task_data # 使用示例 if __name__ == "__main__": # 工具箱的授权信息(必须与平台绑定时一致) licence = "LIC-8F2A-XXXX" fingerprint = "FP-2c91e9f3" # 创建解码器 decoder = TaskQRCodeDecoder(licence, fingerprint) # 扫描二维码并解密 try: task_data = decoder.scan_qr_code("task_qr_code.png") print("\n=== 任务信息 ===") print(f"任务ID: {task_data['taskId']}") print(f"企业ID: {task_data['enterpriseId']}") print(f"单位名称: {task_data['orgName']}") print(f"检查ID: {task_data['inspectionId']}") print(f"检查人: {task_data['inspectionPerson']}") print(f"发布时间: {task_data['issuedAt']}") # 可以使用任务信息执行检查任务 # execute_inspection_task(task_data) except Exception as e: print(f"解密失败: {e}") ``` ### 5.2 Java/Kotlin 完整示例(包含二维码扫描) ```kotlin import com.fasterxml.jackson.databind.ObjectMapper import com.google.zxing.BinaryBitmap import com.google.zxing.MultiFormatReader import com.google.zxing.Result import com.google.zxing.client.j2se.BufferedImageLuminanceSource import com.google.zxing.common.HybridBinarizer import java.awt.image.BufferedImage import java.io.File import java.nio.charset.StandardCharsets import java.security.MessageDigest import java.util.Base64 import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec import javax.imageio.ImageIO class TaskQRCodeDecoder( private val licence: String, private val fingerprint: String ) { private val key: SecretKeySpec by lazy { val combined = "$licence$fingerprint" val digest = MessageDigest.getInstance("SHA-256") val keyBytes = digest.digest(combined.toByteArray(StandardCharsets.UTF_8)) SecretKeySpec(keyBytes, "AES") } private val objectMapper = ObjectMapper() /** * 扫描二维码并解密任务数据 */ fun scanAndDecrypt(qrImagePath: String): Map { // 1. 扫描二维码 val image: BufferedImage = ImageIO.read(File(qrImagePath)) val source = BufferedImageLuminanceSource(image) val bitmap = BinaryBitmap(HybridBinarizer(source)) val reader = MultiFormatReader() val result: Result = reader.decode(bitmap) // 获取二维码内容(Base64编码的加密数据) val encryptedDataBase64 = result.text println("扫描到二维码内容: ${encryptedDataBase64.take(50)}...") // 2. 解密任务数据 return decryptTaskData(encryptedDataBase64) } /** * 解密任务数据 */ fun decryptTaskData(encryptedDataBase64: String): Map { // 1. Base64 解码 val encryptedBytes = Base64.getDecoder().decode(encryptedDataBase64) // 2. 分离 IV 和加密数据(包含认证标签) if (encryptedBytes.size < 12) { throw IllegalArgumentException("加密数据格式错误:数据长度不足") } val iv = encryptedBytes.sliceArray(0 until 12) val ciphertextWithTag = encryptedBytes.sliceArray(12 until encryptedBytes.size) // 3. 使用 AES-256-GCM 解密 val cipher = Cipher.getInstance("AES/GCM/NoPadding") val parameterSpec = GCMParameterSpec(16 * 8, iv) // 标签长度以位为单位 cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec) // 解密数据(GCM 会自动验证认证标签) val decryptedBytes = cipher.doFinal(ciphertextWithTag) // 4. 解析 JSON val decryptedJson = String(decryptedBytes, StandardCharsets.UTF_8) @Suppress("UNCHECKED_CAST") return objectMapper.readValue(decryptedJson, Map::class.java) as Map } } // 使用示例 fun main() { // 工具箱的授权信息(必须与平台绑定时一致) val licence = "LIC-8F2A-XXXX" val fingerprint = "FP-2c91e9f3" // 创建解码器 val decoder = TaskQRCodeDecoder(licence, fingerprint) // 扫描二维码并解密 try { val taskData = decoder.scanAndDecrypt("task_qr_code.png") println("\n=== 任务信息 ===") println("任务ID: ${taskData["taskId"]}") println("企业ID: ${taskData["enterpriseId"]}") println("单位名称: ${taskData["orgName"]}") println("检查ID: ${taskData["inspectionId"]}") println("检查人: ${taskData["inspectionPerson"]}") println("发布时间: ${taskData["issuedAt"]}") // 可以使用任务信息执行检查任务 // executeInspectionTask(taskData) } catch (e: Exception) { println("解密失败: ${e.message}") } } ``` ## 六、常见错误和注意事项 ### 6.1 解密失败 **可能原因**: 1. **密钥不匹配**:`licence` 或 `fingerprint` 与平台绑定时不一致 - 确保使用与设备授权时相同的 `licence` 和 `fingerprint` - 检查字符串拼接是否正确(无分隔符) 2. **数据格式错误**:Base64 编码或数据布局错误 - 确保 Base64 解码正确 - 确保 IV 长度正确(12 字节) 3. **认证标签验证失败**:数据被篡改或损坏 - GCM 模式会自动验证认证标签 - 如果验证失败,说明数据被篡改或密钥错误 4. **算法不匹配**:必须使用 `AES/GCM/NoPadding` - 确保使用正确的加密算法 - 确保认证标签长度为 128 位(16 字节) ### 6.2 二维码扫描失败 **可能原因**: 1. **二维码图片质量差**:确保图片清晰,有足够的对比度 2. **二维码内容过长**:如果加密数据过长,可能需要更高版本的二维码 3. **扫描库不支持**:确保使用支持 Base64 字符串的二维码扫描库 ### 6.3 JSON 解析失败 **可能原因**: 1. **字符编码错误**:确保使用 UTF-8 编码 2. **JSON 格式错误**:确保解密后的字符串是有效的 JSON 3. **字段缺失**:确保所有必需字段都存在 ## 七、安全设计说明 ### 7.1 为什么使用 AES-256-GCM 1. **认证加密(AEAD)**:GCM 模式提供加密和认证,防止数据被篡改 2. **强安全性**:AES-256 提供 256 位密钥强度 3. **自动验证**:GCM 模式会自动验证认证标签,任何篡改都会导致解密失败 ### 7.2 为什么第三方无法解密 1. **密钥绑定**:只有拥有正确 `licence + fingerprint` 的工具箱才能生成正确的密钥 2. **认证标签**:GCM 模式会验证认证标签,任何篡改都会导致解密失败 3. **密钥唯一性**:每个设备的 `licence + fingerprint` 组合是唯一的 ### 7.3 密钥生成的安全性 1. **SHA-256 哈希**:使用强哈希算法生成密钥 2. **密钥长度**:使用全部 32 字节作为 AES-256 密钥 3. **密钥隔离**:每个设备的密钥是独立的,互不影响 ## 八、测试建议 1. **单元测试**: - 测试密钥生成是否正确 - 测试解密功能是否正常 - 测试 JSON 解析是否正确 2. **集成测试**: - 使用真实平台生成的二维码进行测试 - 测试不同长度的任务数据 - 测试错误的密钥是否会导致解密失败 3. **边界测试**: - 测试超长的任务数据 - 测试特殊字符的处理 - 测试错误的 Base64 格式 ## 九、参考实现 - **Python**:`cryptography` 库(AES-GCM 加密)、`pyzbar` 库(二维码扫描) - **Java/Kotlin**:JDK `javax.crypto`(AES-GCM 加密)、ZXing 库(二维码扫描) - **C#**:`System.Security.Cryptography`(AES-GCM 加密)、ZXing.Net 库(二维码扫描) ## 十、联系支持 如有问题,请联系平台技术支持团队获取: - 测试环境地址 - 技术支持