Files
fullstack-starter/docs/工具箱端-授权对接指南/工具箱端-任务二维码解密指南.md

22 KiB
Raw Blame History

工具箱端 - 任务二维码解密指南

概述

本文档说明工具箱端如何解密任务二维码数据。App 创建任务后,平台会生成加密的任务数据并返回给 AppApp 将其生成二维码。工具箱扫描二维码后,需要使用自己的 licencefingerprint 解密任务数据。

UX 集成模式补充(当前项目实现)

在当前集成模式中,工具箱扫描二维码后将密文提交给 UX 的 crypto.decryptTask 由 UX 使用设备绑定的 licence + fingerprint 执行 AES-256-GCM 解密并返回任务明文。

一、业务流程

App创建任务 → 平台加密任务数据 → 返回加密数据 → App生成二维码
                                                      ↓
工具箱扫描二维码 → 提取加密数据 → AES-256-GCM解密 → 获取任务信息

二、任务数据结构

2.1 任务数据 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-GCMGalois/Counter Mode
  • 密钥长度256 位32 字节)
  • IV 长度12 字节96 位)
  • 认证标签长度16 字节128 位)

3.2 密钥生成

密钥由工具箱的 licencefingerprint 生成:

密钥 = SHA-256(licence + fingerprint)

重要说明

  • licencefingerprint 直接字符串拼接(无分隔符)
  • 使用 SHA-256 哈希算法的全部 32 字节作为 AES-256 密钥
  • 工具箱必须使用与平台绑定时相同的 licencefingerprint

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 实现示例

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 实现示例

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<String, Any> {
        // 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<String, Any>
    }
}

// 使用示例
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# 实现示例

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位
    
    /// <summary>
    /// 解密任务二维码数据
    /// </summary>
    public static Dictionary<string, object> 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<Dictionary<string, object>>(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 完整示例(包含二维码扫描)

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 完整示例(包含二维码扫描)

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<String, Any> {
        // 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<String, Any> {
        // 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<String, Any>
    }
}

// 使用示例
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. 密钥不匹配licencefingerprint 与平台绑定时不一致

    • 确保使用与设备授权时相同的 licencefingerprint
    • 检查字符串拼接是否正确(无分隔符)
  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. 认证加密AEADGCM 模式提供加密和认证,防止数据被篡改
  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 格式

九、参考实现

  • PythoncryptographyAES-GCM 加密)、pyzbar 库(二维码扫描)
  • Java/KotlinJDK javax.cryptoAES-GCM 加密、ZXing 库(二维码扫描)
  • C#System.Security.CryptographyAES-GCM 加密、ZXing.Net 库(二维码扫描)

十、联系支持

如有问题,请联系平台技术支持团队获取:

  • 测试环境地址
  • 技术支持