From 04ff718f479f169c1478ffc71296358d42ac553d Mon Sep 17 00:00:00 2001 From: imbytecat Date: Tue, 10 Mar 2026 15:08:36 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E7=A7=BB=E9=99=A4=E6=97=A7=E7=89=88?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=AE=B1=E7=AB=AF=E6=8E=88=E6=9D=83=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5=E6=8C=87=E5=8D=97=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../工具箱端-授权对接指南/utils/AesGcmUtil.kt | 124 --- docs/工具箱端-授权对接指南/utils/DateUtil.kt | 42 - .../utils/DeviceSignatureUtil.kt | 129 --- .../utils/DistributedIdUtil.kt | 12 - docs/工具箱端-授权对接指南/utils/HashUtil.kt | 18 - docs/工具箱端-授权对接指南/utils/HkdfUtil.kt | 66 -- docs/工具箱端-授权对接指南/utils/JwtUtil.kt | 50 -- .../工具箱端-授权对接指南/utils/RegionUtil.kt | 22 - .../utils/RsaOaepDecryptionUtil.kt | 115 --- .../utils/RsaOaepDecryptionUtilV2.kt | 103 --- .../utils/TaskEncryptionUtil.kt | 120 --- .../utils/ZipVerifierUtil.kt | 134 ---- .../工具箱端-任务二维码解密指南.md | 644 --------------- .../工具箱端-报告加密与签名生成指南.md | 647 --------------- .../工具箱端-摘要信息二维码生成指南.md | 756 ------------------ .../工具箱端-设备授权二维码生成指南.md | 601 -------------- 16 files changed, 3583 deletions(-) delete mode 100644 docs/工具箱端-授权对接指南/utils/AesGcmUtil.kt delete mode 100644 docs/工具箱端-授权对接指南/utils/DateUtil.kt delete mode 100644 docs/工具箱端-授权对接指南/utils/DeviceSignatureUtil.kt delete mode 100644 docs/工具箱端-授权对接指南/utils/DistributedIdUtil.kt delete mode 100644 docs/工具箱端-授权对接指南/utils/HashUtil.kt delete mode 100644 docs/工具箱端-授权对接指南/utils/HkdfUtil.kt delete mode 100644 docs/工具箱端-授权对接指南/utils/JwtUtil.kt delete mode 100644 docs/工具箱端-授权对接指南/utils/RegionUtil.kt delete mode 100644 docs/工具箱端-授权对接指南/utils/RsaOaepDecryptionUtil.kt delete mode 100644 docs/工具箱端-授权对接指南/utils/RsaOaepDecryptionUtilV2.kt delete mode 100644 docs/工具箱端-授权对接指南/utils/TaskEncryptionUtil.kt delete mode 100644 docs/工具箱端-授权对接指南/utils/ZipVerifierUtil.kt delete mode 100644 docs/工具箱端-授权对接指南/工具箱端-任务二维码解密指南.md delete mode 100644 docs/工具箱端-授权对接指南/工具箱端-报告加密与签名生成指南.md delete mode 100644 docs/工具箱端-授权对接指南/工具箱端-摘要信息二维码生成指南.md delete mode 100644 docs/工具箱端-授权对接指南/工具箱端-设备授权二维码生成指南.md diff --git a/docs/工具箱端-授权对接指南/utils/AesGcmUtil.kt b/docs/工具箱端-授权对接指南/utils/AesGcmUtil.kt deleted file mode 100644 index 1fa2998..0000000 --- a/docs/工具箱端-授权对接指南/utils/AesGcmUtil.kt +++ /dev/null @@ -1,124 +0,0 @@ -package top.tangyh.lamp.filing.utils - -import io.github.oshai.kotlinlogging.KotlinLogging -import java.nio.charset.StandardCharsets -import java.util.* -import javax.crypto.Cipher -import javax.crypto.spec.GCMParameterSpec -import javax.crypto.spec.SecretKeySpec - -private val logger = KotlinLogging.logger {} - -/** - * AES-256-GCM 加密解密工具类 - * - * 安全设计说明: - * - 使用 AES-256-GCM 提供认证加密(AEAD) - * - GCM 模式自动提供认证标签(tag),防止数据被篡改 - * - IV(初始化向量)长度为 12 字节(96位),符合 GCM 推荐 - * - 认证标签长度为 16 字节(128位),提供强认证 - * - 加密数据格式:IV (12字节) + Ciphertext (变长) + Tag (16字节) - * - * 为什么第三方无法伪造: - * - 只有拥有正确 licence + fingerprint 的设备才能派生正确的 AES 密钥 - * - GCM 模式会验证认证标签,任何篡改都会导致解密失败 - * - 即使第三方获取了加密数据,也无法解密(缺少密钥) - */ -object AesGcmUtil { - - private const val ALGORITHM = "AES" - private const val TRANSFORMATION = "AES/GCM/NoPadding" - private const val IV_LENGTH = 12 // 12 bytes = 96 bits (GCM 推荐) - private const val TAG_LENGTH = 16 // 16 bytes = 128 bits (GCM 认证标签长度) - private const val GCM_TAG_LENGTH_BITS = TAG_LENGTH * 8 // 128 bits - - /** - * 解密 AES-256-GCM 加密的数据 - * - * @param encryptedData Base64 编码的加密数据(格式:iv + ciphertext + tag) - * @param key AES 密钥(32字节) - * @return 解密后的明文(UTF-8 字符串) - * @throws RuntimeException 如果解密失败(密钥错误、数据被篡改等) - */ - fun decrypt(encryptedData: String, key: ByteArray): String { - return try { - // 1. Base64 解码 - val encryptedBytes = Base64.getDecoder().decode(encryptedData) - - // 2. 提取 IV、密文和认证标签 - if (encryptedBytes.size < IV_LENGTH + TAG_LENGTH) { - throw IllegalArgumentException("加密数据长度不足,无法提取 IV 和 Tag") - } - - val iv = encryptedBytes.copyOfRange(0, IV_LENGTH) - val tag = encryptedBytes.copyOfRange(encryptedBytes.size - TAG_LENGTH, encryptedBytes.size) - val ciphertext = encryptedBytes.copyOfRange(IV_LENGTH, encryptedBytes.size - TAG_LENGTH) - - // 3. 创建 SecretKeySpec - val secretKey = SecretKeySpec(key, ALGORITHM) - - // 4. 创建 GCMParameterSpec(包含 IV 和认证标签长度) - val gcmSpec = GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv) - - // 5. 初始化 Cipher 进行解密 - val cipher = Cipher.getInstance(TRANSFORMATION) - cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec) - - // 6. 执行解密(GCM 模式会自动验证认证标签) - // 如果认证标签验证失败,会抛出异常 - val decryptedBytes = cipher.doFinal(ciphertext + tag) - - // 7. 转换为 UTF-8 字符串 - String(decryptedBytes, StandardCharsets.UTF_8) - } catch (e: javax.crypto.AEADBadTagException) { - logger.error(e) { "AES-GCM 认证标签验证失败,数据可能被篡改或密钥错误" } - throw RuntimeException("解密失败:认证标签验证失败,数据可能被篡改或密钥错误", e) - } catch (e: Exception) { - logger.error(e) { "AES-GCM 解密失败" } - throw RuntimeException("解密失败: ${e.message}", e) - } - } - - /** - * 加密数据(用于测试或客户端实现参考) - * - * @param plaintext 明文数据 - * @param key AES 密钥(32字节) - * @return Base64 编码的加密数据(格式:iv + ciphertext + tag) - */ - fun encrypt(plaintext: String, key: ByteArray): String { - return try { - // 1. 生成随机 IV - val iv = ByteArray(IV_LENGTH) - java.security.SecureRandom().nextBytes(iv) - - // 2. 创建 SecretKeySpec - val secretKey = SecretKeySpec(key, ALGORITHM) - - // 3. 创建 GCMParameterSpec - val gcmSpec = GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv) - - // 4. 初始化 Cipher 进行加密 - val cipher = Cipher.getInstance(TRANSFORMATION) - cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec) - - // 5. 执行加密 - val plaintextBytes = plaintext.toByteArray(StandardCharsets.UTF_8) - val encryptedBytes = cipher.doFinal(plaintextBytes) - - // 6. 组装:IV + Ciphertext + Tag - // GCM 模式会将认证标签附加到密文末尾 - val ciphertext = encryptedBytes.copyOfRange(0, encryptedBytes.size - TAG_LENGTH) - val tag = encryptedBytes.copyOfRange(encryptedBytes.size - TAG_LENGTH, encryptedBytes.size) - - val result = iv + ciphertext + tag - - // 7. Base64 编码返回 - Base64.getEncoder().encodeToString(result) - } catch (e: Exception) { - logger.error(e) { "AES-GCM 加密失败" } - throw RuntimeException("加密失败: ${e.message}", e) - } - } -} - diff --git a/docs/工具箱端-授权对接指南/utils/DateUtil.kt b/docs/工具箱端-授权对接指南/utils/DateUtil.kt deleted file mode 100644 index 99ac1b7..0000000 --- a/docs/工具箱端-授权对接指南/utils/DateUtil.kt +++ /dev/null @@ -1,42 +0,0 @@ -package top.tangyh.lamp.filing.utils - -import java.text.SimpleDateFormat -import java.util.* - -class DateUtil { - - companion object { - // 获取当前时间戳 - fun getCurrentTimestamp(): Long { - return System.currentTimeMillis() - } - - // 格式化日期 - fun formatDate(date: Date, format: String = "yyyy-MM-dd HH:mm:ss"): String { - val sdf = SimpleDateFormat(format) - return sdf.format(date) - } - - // 解析日期字符串 - fun parseDate(dateString: String, format: String = "yyyy-MM-dd HH:mm:ss"): Date? { - val sdf = SimpleDateFormat(format) - return try { - sdf.parse(dateString) - } catch (e: Exception) { - null - } - } - - // 计算两个日期之间的天数差 - fun getDaysBetweenDates(date1: Date, date2: Date): Long { - val diff = Math.abs(date1.time - date2.time) - return diff / (24 * 60 * 60 * 1000) - } - - // 获取当前时间并格式化为 yyyy-MM-dd_HH-mm-ss - fun getCurrentFormattedTime(format: String = "yyyy-MM-dd_HH-mm-ss"): String { - val sdf = SimpleDateFormat(format) - return sdf.format(Date()) - } - } -} \ No newline at end of file diff --git a/docs/工具箱端-授权对接指南/utils/DeviceSignatureUtil.kt b/docs/工具箱端-授权对接指南/utils/DeviceSignatureUtil.kt deleted file mode 100644 index f522fb3..0000000 --- a/docs/工具箱端-授权对接指南/utils/DeviceSignatureUtil.kt +++ /dev/null @@ -1,129 +0,0 @@ -package top.tangyh.lamp.filing.utils - -import io.github.oshai.kotlinlogging.KotlinLogging -import java.nio.charset.StandardCharsets -import java.security.MessageDigest -import java.util.* -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec - -private val logger = KotlinLogging.logger {} - -/** - * 设备签名工具类 - * 用于生成和验证设备报告签名 - * - * 签名算法:HMAC-SHA256 - * 签名数据(严格顺序): - * sign_payload = taskId + inspectionId + - * SHA256(assets.json) + - * SHA256(vulnerabilities.json) + - * SHA256(weakPasswords.json) + - * SHA256(漏洞评估报告.html) - * - * 安全设计说明: - * - 使用 HMAC-SHA256 提供消息认证,防止伪造和篡改 - * - 签名包含 taskId 和 inspectionId,确保签名与特定任务绑定 - * - 包含多个报告文件的 SHA256,确保报告内容完整性 - * - 只有拥有正确 licence + fingerprint 的设备才能生成有效签名 - */ -object DeviceSignatureUtil { - - private const val HMAC_ALGORITHM = "HmacSHA256" - - /** - * 签名数据文件列表(严格顺序) - */ - data class SignatureFileHashes( - val assetsJsonSha256: String, - val vulnerabilitiesJsonSha256: String, - val weakPasswordsJsonSha256: String, - val reportHtmlSha256: String - ) - - /** - * 生成设备签名 - * - * @param key 派生密钥(32字节) - * @param taskId 任务ID - * @param inspectionId 检查ID - * @param fileHashes 各个文件的 SHA256 哈希值(hex字符串) - * @return Base64 编码的签名 - */ - fun generateSignature( - key: ByteArray, - taskId: String, - inspectionId: Long, - fileHashes: SignatureFileHashes - ): String { - return try { - // 组装签名数据(严格顺序): - // taskId + inspectionId + SHA256(assets.json) + SHA256(vulnerabilities.json) + - // SHA256(weakPasswords.json) + SHA256(漏洞评估报告.html) - val signatureData = buildString { - append(taskId) - append(inspectionId) - append(fileHashes.assetsJsonSha256) - append(fileHashes.vulnerabilitiesJsonSha256) - append(fileHashes.weakPasswordsJsonSha256) - append(fileHashes.reportHtmlSha256) - } - val dataBytes = signatureData.toByteArray(StandardCharsets.UTF_8) - - // 使用 HMAC-SHA256 计算签名 - val mac = Mac.getInstance(HMAC_ALGORITHM) - val secretKey = SecretKeySpec(key, HMAC_ALGORITHM) - mac.init(secretKey) - val signatureBytes = mac.doFinal(dataBytes) - - // Base64 编码返回 - Base64.getEncoder().encodeToString(signatureBytes) - } catch (e: Exception) { - logger.error(e) { "生成设备签名失败: taskId=$taskId, inspectionId=$inspectionId" } - throw RuntimeException("生成设备签名失败: ${e.message}", e) - } - } - - /** - * 验证设备签名 - * - * @param key 派生密钥(32字节) - * @param taskId 任务ID - * @param inspectionId 检查ID - * @param fileHashes 各个文件的 SHA256 哈希值(hex字符串) - * @param expectedSignature Base64 编码的期望签名 - * @return true 如果签名匹配,false 否则 - */ - fun verifySignature( - key: ByteArray, - taskId: String, - inspectionId: Long, - fileHashes: SignatureFileHashes, - expectedSignature: String - ): Boolean { - return try { - val calculatedSignature = generateSignature(key, taskId, inspectionId, fileHashes) - // 使用时间安全的比较,防止时序攻击 - MessageDigest.isEqual( - Base64.getDecoder().decode(expectedSignature), - Base64.getDecoder().decode(calculatedSignature) - ) - } catch (e: Exception) { - logger.error(e) { "验证设备签名失败: taskId=$taskId, inspectionId=$inspectionId" } - false - } - } - - /** - * 计算文件的 SHA256 哈希值(hex字符串) - * - * @param fileContent 文件内容 - * @return SHA256 哈希值的 hex 字符串 - */ - fun calculateSha256(fileContent: ByteArray): String { - val digest = MessageDigest.getInstance("SHA-256") - val hashBytes = digest.digest(fileContent) - return hashBytes.joinToString("") { "%02x".format(it) } - } -} - diff --git a/docs/工具箱端-授权对接指南/utils/DistributedIdUtil.kt b/docs/工具箱端-授权对接指南/utils/DistributedIdUtil.kt deleted file mode 100644 index 2517e31..0000000 --- a/docs/工具箱端-授权对接指南/utils/DistributedIdUtil.kt +++ /dev/null @@ -1,12 +0,0 @@ -package top.tangyh.lamp.filing.utils - -object DistributedIdUtil { - fun generateId(platformId: Long, localId: Long): Long { - require(platformId in 0..0xFFFF) { "platformId must be 0-65535" } - val safeLocalId = localId and 0xFFFFFFFFFFFF - return (platformId shl 48) or safeLocalId - } - - fun parsePlatform(id: Long): Long = id ushr 48 - fun parseLocal(id: Long): Long = id and 0xFFFFFFFFFFFF -} diff --git a/docs/工具箱端-授权对接指南/utils/HashUtil.kt b/docs/工具箱端-授权对接指南/utils/HashUtil.kt deleted file mode 100644 index 11125d6..0000000 --- a/docs/工具箱端-授权对接指南/utils/HashUtil.kt +++ /dev/null @@ -1,18 +0,0 @@ -package top.tangyh.lamp.filing.utils - -import java.io.InputStream -import java.security.MessageDigest - -object HashUtil { - fun calculateFileHash(inputStream: InputStream): String { - val digest = MessageDigest.getInstance("SHA-256") - val buffer = ByteArray(8192) - var bytesRead: Int - - while (inputStream.read(buffer).also { bytesRead = it } != -1) { - digest.update(buffer, 0, bytesRead) - } - - return digest.digest().joinToString("") { "%02x".format(it) } - } -} \ No newline at end of file diff --git a/docs/工具箱端-授权对接指南/utils/HkdfUtil.kt b/docs/工具箱端-授权对接指南/utils/HkdfUtil.kt deleted file mode 100644 index 7e1c860..0000000 --- a/docs/工具箱端-授权对接指南/utils/HkdfUtil.kt +++ /dev/null @@ -1,66 +0,0 @@ -package top.tangyh.lamp.filing.utils - -import io.github.oshai.kotlinlogging.KotlinLogging -import org.bouncycastle.crypto.digests.SHA256Digest -import org.bouncycastle.crypto.generators.HKDFBytesGenerator -import org.bouncycastle.crypto.params.HKDFParameters -import java.nio.charset.StandardCharsets - -private val logger = KotlinLogging.logger {} - -/** - * HKDF (HMAC-based Key Derivation Function) 工具类 - * 用于从 licence + fingerprint 派生设备签名密钥 - * - * 安全设计说明: - * - 使用 HKDF 而非直接哈希,提供更好的密钥分离和扩展性 - * - Salt 固定为 "AUTH_V3_SALT",确保同一输入产生相同密钥 - * - Info 参数用于区分不同用途的密钥派生(device_report_signature) - * - 输出长度 32 字节(256位),适用于 HMAC-SHA256 - */ -object HkdfUtil { - - private const val SALT = "AUTH_V3_SALT" - private const val INFO = "device_report_signature" - private const val KEY_LENGTH = 32 // 32 bytes = 256 bits - - /** - * 使用 HKDF 派生密钥(使用默认 salt 和 info) - * - * @param input 输入密钥材料(licence + fingerprint) - * @return 派生出的密钥(32字节) - */ - fun deriveKey(input: String): ByteArray { - return deriveKey(input, SALT, INFO) - } - - /** - * 使用 HKDF 派生密钥(支持自定义 salt 和 info) - * - * @param input 输入密钥材料(licence + fingerprint) - * @param salt Salt 值(用于密钥派生) - * @param info Info 值(用于区分不同用途的密钥) - * @param keyLength 输出密钥长度(默认32字节) - * @return 派生出的密钥 - */ - fun deriveKey(input: String, salt: String, info: String, keyLength: Int = KEY_LENGTH): ByteArray { - return try { - val inputBytes = input.toByteArray(StandardCharsets.UTF_8) - val saltBytes = salt.toByteArray(StandardCharsets.UTF_8) - val infoBytes = info.toByteArray(StandardCharsets.UTF_8) - - val hkdf = HKDFBytesGenerator(SHA256Digest()) - val params = HKDFParameters(inputBytes, saltBytes, infoBytes) - hkdf.init(params) - - val derivedKey = ByteArray(keyLength) - hkdf.generateBytes(derivedKey, 0, keyLength) - - derivedKey - } catch (e: Exception) { - logger.error(e) { "HKDF 密钥派生失败: input=$input, salt=$salt, info=$info" } - throw RuntimeException("HKDF 密钥派生失败: ${e.message}", e) - } - } -} - diff --git a/docs/工具箱端-授权对接指南/utils/JwtUtil.kt b/docs/工具箱端-授权对接指南/utils/JwtUtil.kt deleted file mode 100644 index 4757e13..0000000 --- a/docs/工具箱端-授权对接指南/utils/JwtUtil.kt +++ /dev/null @@ -1,50 +0,0 @@ -package top.tangyh.lamp.filing.utils - -import io.jsonwebtoken.Jwts -import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.security.Keys -import org.springframework.beans.factory.annotation.Value -import org.springframework.stereotype.Component -import java.time.LocalDateTime -import java.time.ZoneId -import java.util.* -import javax.crypto.SecretKey - -@Component -class JwtUtil( - @Value("\${jwt.secret}") - private val secretKey: String -) { - - - // 生成签名 Key(HS256) - private val signingKey: SecretKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey)) - - /** - * 生成 Token - * */ - fun generateToken(subject: String, claims: Map = emptyMap(), expireDays: Long = 7): String { - val now = LocalDateTime.now() - val expiration = now.plusDays(expireDays) - - return Jwts.builder() - .setSubject(subject) - .setClaims(claims) - .setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant())) - .setExpiration(Date.from(expiration.atZone(ZoneId.systemDefault()).toInstant())) - .signWith(signingKey, SignatureAlgorithm.HS256) - .compact() - } - - - /** - * 解析 Token 获取 Claims - */ - fun parseToken(token: String): Map { - return Jwts.parserBuilder() - .setSigningKey(signingKey) - .build() - .parseClaimsJws(token) - .body - } -} diff --git a/docs/工具箱端-授权对接指南/utils/RegionUtil.kt b/docs/工具箱端-授权对接指南/utils/RegionUtil.kt deleted file mode 100644 index c078cdb..0000000 --- a/docs/工具箱端-授权对接指南/utils/RegionUtil.kt +++ /dev/null @@ -1,22 +0,0 @@ -package top.tangyh.lamp.filing.utils - -import kotlin.text.substring - -object RegionUtil { - fun getLevel(code: String?): String { - if (code == null || code.length != 6) { - return "无效编码" - } - - val province = code.substring(0, 2) - val city = code.substring(2, 4) - val county = code.substring(4, 6) - - return when { - city == "00" && county == "00" -> "province" - city != "00" && county == "00" -> "city" - county != "00" -> "county" - else -> "未知级别" - } - } -} diff --git a/docs/工具箱端-授权对接指南/utils/RsaOaepDecryptionUtil.kt b/docs/工具箱端-授权对接指南/utils/RsaOaepDecryptionUtil.kt deleted file mode 100644 index 2d7397f..0000000 --- a/docs/工具箱端-授权对接指南/utils/RsaOaepDecryptionUtil.kt +++ /dev/null @@ -1,115 +0,0 @@ -package top.tangyh.lamp.filing.utils - -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.beans.factory.annotation.Value -import org.springframework.stereotype.Component -import java.nio.charset.StandardCharsets -import java.security.KeyFactory -import java.security.PublicKey -import java.security.spec.PKCS8EncodedKeySpec -import java.security.spec.X509EncodedKeySpec -import java.util.* -import javax.crypto.Cipher - -private val logger = KotlinLogging.logger {} - -/** - * RSA-OAEP 解密工具类 - * 用于设备身份首次绑定时解密设备信息 - * - * 使用场景:设备使用平台的公钥加密数据,平台使用私钥解密 - */ -@Component -class RsaOaepDecryptionUtil( - @Value("\${device.encrypt.privateKey:}") - private val privateKeyBase64: String -) { - - private val keyFactory = KeyFactory.getInstance("RSA") - private val cipherAlgorithm = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" - - // 缓存私钥,避免每次解密都重新加载 - private val privateKey by lazy { - if (privateKeyBase64.isBlank()) { - throw IllegalStateException("RSA私钥未配置,无法解密设备信息") - } - val privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64) - val keySpec = PKCS8EncodedKeySpec(privateKeyBytes) - keyFactory.generatePrivate(keySpec) - } - - init { - if (privateKeyBase64.isBlank()) { - logger.warn { "RSA私钥未配置,设备授权解密功能可能无法使用" } - } - } - - /** - * 使用RSA-OAEP解密设备信息 - * @param encryptedData Base64编码的加密数据 - * @return 解密后的JSON字符串 - */ - fun decrypt(encryptedData: String): String { - if (privateKeyBase64.isBlank()) { - throw IllegalStateException("RSA私钥未配置,无法解密设备信息") - } - - return try { - // 创建新的Cipher实例(Cipher不是线程安全的) - val cipher = Cipher.getInstance(cipherAlgorithm) - - // 初始化解密器 - cipher.init(Cipher.DECRYPT_MODE, privateKey) - - // Base64解码加密数据 - val encryptedBytes = Base64.getDecoder().decode(encryptedData) - - // 解密数据 - val decryptedBytes = cipher.doFinal(encryptedBytes) - - // 返回解密后的字符串 - String(decryptedBytes, StandardCharsets.UTF_8) - } catch (e: Exception) { - logger.error(e) { "RSA-OAEP解密设备信息失败" } - throw RuntimeException("RSA-OAEP解密设备信息失败: ${e.message}", e) - } - } - - - /** - * 使用平台公钥加密数据 - * - * @param plainText 原始JSON字符串(设备信息) - * @param publicKeyBase64 平台公钥(Base64) - * @return Base64编码的密文 - */ - fun encrypt1( - plainText: String, - publicKeyBase64: String - ): String { - try { - val publicKey = loadPublicKey(publicKeyBase64) - - val cipher = Cipher.getInstance(cipherAlgorithm) - cipher.init(Cipher.ENCRYPT_MODE, publicKey) - - val encryptedBytes = cipher.doFinal( - plainText.toByteArray(StandardCharsets.UTF_8) - ) - - return Base64.getEncoder().encodeToString(encryptedBytes) - } catch (e: Exception) { - logger.error(e) { "RSA-OAEP 加密失败" } - throw RuntimeException("RSA-OAEP 加密失败: ${e.message}", e) - } - } - - private fun loadPublicKey(base64Key: String): PublicKey { - val keyBytes = Base64.getDecoder().decode(base64Key) - val keySpec = X509EncodedKeySpec(keyBytes) - return keyFactory.generatePublic(keySpec) - } - -} - - diff --git a/docs/工具箱端-授权对接指南/utils/RsaOaepDecryptionUtilV2.kt b/docs/工具箱端-授权对接指南/utils/RsaOaepDecryptionUtilV2.kt deleted file mode 100644 index 1de4781..0000000 --- a/docs/工具箱端-授权对接指南/utils/RsaOaepDecryptionUtilV2.kt +++ /dev/null @@ -1,103 +0,0 @@ -package top.tangyh.lamp.filing.utils - -import java.nio.charset.StandardCharsets -import java.security.KeyFactory -import java.security.KeyPairGenerator -import java.security.PublicKey -import java.security.spec.PKCS8EncodedKeySpec -import java.security.spec.X509EncodedKeySpec -import java.util.* -import javax.crypto.Cipher - - -object RsaOaepCryptoUtil { - - private const val cipherAlgorithm = - "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" - - private val keyFactory = KeyFactory.getInstance("RSA") - - fun encrypt( - plainText: String, - publicKeyBase64: String - ): String { - val publicKey = loadPublicKey(publicKeyBase64) - - val cipher = Cipher.getInstance(cipherAlgorithm) - cipher.init(Cipher.ENCRYPT_MODE, publicKey) - - val encryptedBytes = cipher.doFinal( - plainText.toByteArray(StandardCharsets.UTF_8) - ) - return Base64.getEncoder().encodeToString(encryptedBytes) - } - - fun decrypt( - encryptedData: String, - privateKeyBase64: String - ): String { - val privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64) - val keySpec = PKCS8EncodedKeySpec(privateKeyBytes) - val privateKey = keyFactory.generatePrivate(keySpec) - - val cipher = Cipher.getInstance(cipherAlgorithm) - cipher.init(Cipher.DECRYPT_MODE, privateKey) - - val decryptedBytes = cipher.doFinal( - Base64.getDecoder().decode(encryptedData) - ) - return String(decryptedBytes, StandardCharsets.UTF_8) - } - - private fun loadPublicKey(base64Key: String): PublicKey { - val keyBytes = Base64.getDecoder().decode(base64Key) - val keySpec = X509EncodedKeySpec(keyBytes) - return keyFactory.generatePublic(keySpec) - } -} - - -object Test { - @JvmStatic - fun main(args: Array) { - val keyPairGenerator = KeyPairGenerator.getInstance("RSA") - keyPairGenerator.initialize(2048) - val keyPair = keyPairGenerator.generateKeyPair() - -// val publicKey = Base64.getEncoder().encodeToString(keyPair.public.encoded) -// val privateKey = Base64.getEncoder().encodeToString(keyPair.private.encoded) - - val publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzDlZvMDVaL+fjl05Hi182JOAUAaN4gh9rOF+1NhKfO4J6e0HLy8lBuylp3A4xoTiyUejNm22h0dqAgDSPnY/xZR76POFTD1soHr2LaFCN8JAbQ96P8gE7wC9qpoTssVvIVRH7QbVd260J6eD0Szwcx9cg591RSN69pMpe5IVRi8T99Hhql6/wnZHORPr18eESLOY93jRskLzc0q18r68RRoTJiQf+9YC8ub5iKp7rCjVnPi1UbIYmXmL08tk5mksYA0NqWQAa1ofKxx/9tQtB9uTjhTxuTu94XU9jlGU87qaHZs+kpqa8CAbYYJFbSP1xHwoZzpU2jpw2aF22HBYxwIDAQAB" - val privateKey = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDMOVm8wNVov5+OXTkeLXzYk4BQBo3iCH2s4X7U2Ep87gnp7QcvLyUG7KWncDjGhOLJR6M2bbaHR2oCANI+dj/FlHvo84VMPWygevYtoUI3wkBtD3o/yATvAL2qmhOyxW8hVEftBtV3brQnp4PRLPBzH1yDn3VFI3r2kyl7khVGLxP30eGqXr/Cdkc5E+vXx4RIs5j3eNGyQvNzSrXyvrxFGhMmJB/71gLy5vmIqnusKNWc+LVRshiZeYvTy2TmaSxgDQ2pZABrWh8rHH/21C0H25OOFPG5O73hdT2OUZTzupodmz6SmprwIBthgkVtI/XEfChnOlTaOnDZoXbYcFjHAgMBAAECggEBAKUDagjj3wwmWWwU00QZCRmOBU2kHhyXM8Tu5yZgJb/QLt5/ESEE/OwdZrRmLtnpIb31TmF6QNQJ1hQMSdUCgwDIu1489mXl89GvyR6RgAnBwYDd0q+alLHxbU9d87CtauAJU5ynuvAn2RV5ez8XCqpamswXmg/lXUPDIO+h1+K+UJPwYFhzHL3WpZQXHofcSUCRn0JONbz7c9OwVFYe0v1g18wwAvIAsEEimcnXFZaIJ/md2md0Jk4+LOFUsbgCM5skNTooXarDiHGCRFMJXxvYpavC6hhhcWfh3K2ydHMhFdF70ZOs169ShDtYq4i/hkaZ+p7kFVo8Z6oxVUpC8IECgYEA+OQxfc2iGMD9uus/r1yjUx0kdhYKik202tE53C4ikAn490+Lb8qcY/kykFj721MHBc73ijq/ZNU21JxyMx4T1jbdvl+5Kv9EUFAQmsJuTSudC1Dud3MlUgYEtVUhau2WOexUIRyCda6V4NUYkJG4vLRyMloPprF3xThmJRgG9qcCgYEA0g6QrU1e/DjADSpymRfqoOX4ASp7G++CRHTB2NZvA6ZxnWoEX59TO3mW4z4fgJkIwevkPUA9nfO2bRQWph+aom5qAczsPmZ2OpR9cjel63AIV1AboUV2Gr/H4IfHs7qGX7JWmW1SnZyvvGuI5MdBHEcysJO+L5V1OVyFRzvj8OECgYAO21A4+jVa1OpQZgp/JUB6jZrHkbk/WDQbe7HAeuCFSJMb8BuaqLV9IjrqcuVVyjb5Gcmc7rTOCAwl1NDcTEdS2iOSYZRkBKjHQoA7PK/o21mce1BAwRbRNprBWDuObnAxNPIwp8sBy1IXAaFdv9UPLpZCey3D/YPwudUfEbgYsQKBgQCZax3sFYh8ew56Dzin7Dnnzk72uwozexkP2p8COovWhKiSqi4LkRh/Zez4iBUGHb+xsxJ+Uf8u8CObQ4LPTmHopPAz5HHfmYJcgrukwlQiwy60ZsPnZA5AtzXLHiCTenZOSrjJUnl2uEv6OChBv+4kMzQol5/erTBy9so5Htr6wQKBgQC3apeFD/x+0FjABleVITzGyAWj/Kxl/OOiL4dQAYW49wVD/j0ujG3CvbK0GHZFwy/Ju+pWlHISbiZiKYko/4GBtp+JwxG9fFmbHdl4BZSTwfQMYdNq8hN+0hBfWwcceIhbEZcFLIvG/ZhkcT3yh874XRn1A5V/AR8W9YFH1EWYwQ==" - -// val plainText = "{\n" + -// " \"taskId\": 723047797139586052,\n" + -// " \"licence\": \"LIC-8F2A-XXXX\",\n" + -// " \"fingerprint\": \"FP-2c91e9f3\",\n" + -// " \"enterpriseId\": \"1173040813421105152\",\n" + -// " \"inspectionId\": \"702286470691215417\",\n" + -// " \"summary\": \"1\"\n" + -// "}" - - val plainText = "{\n" + - " \"licence\": \"lic-1234567890\",\n" + - " \"fingerprint\": \"e19c60d21c544c1118e3b633eae1bb935e2762ebddbc671b60b8b61c65c05d1c\"\n" + - "}" - - val encryptedText = - RsaOaepCryptoUtil.encrypt(plainText, publicKey) - - val decryptedText = - RsaOaepCryptoUtil.decrypt(encryptedText, privateKey) - - println("Plain Text: $plainText") - - println("Public Key: $publicKey") - println("Private Key: $privateKey") - - - println("Encrypted: $encryptedText") - println("Decrypted: $decryptedText") - } -} - diff --git a/docs/工具箱端-授权对接指南/utils/TaskEncryptionUtil.kt b/docs/工具箱端-授权对接指南/utils/TaskEncryptionUtil.kt deleted file mode 100644 index bd138b8..0000000 --- a/docs/工具箱端-授权对接指南/utils/TaskEncryptionUtil.kt +++ /dev/null @@ -1,120 +0,0 @@ -package top.tangyh.lamp.filing.utils - -import io.github.oshai.kotlinlogging.KotlinLogging -import java.nio.charset.StandardCharsets -import java.security.MessageDigest -import java.security.SecureRandom -import java.util.* -import javax.crypto.Cipher -import javax.crypto.spec.GCMParameterSpec -import javax.crypto.spec.SecretKeySpec - -private val logger = KotlinLogging.logger {} - -/** - * 任务加密工具类 - * 使用 licence + fingerprint 作为密钥对任务数据进行 AES-256-GCM 对称加密 - * - * GCM 模式提供认证加密,比 ECB 模式更安全 - * 加密数据格式:IV(12字节) + 加密数据 + 认证标签(16字节) - */ -object TaskEncryptionUtil { - - 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 secureRandom = SecureRandom() - - /** - * 使用 licence + fingerprint 加密任务数据(AES-256-GCM) - * @param data 待加密的数据(JSON字符串) - * @param licence 授权码 - * @param fingerprint 硬件指纹 - * @return Base64编码的加密数据(包含IV + 加密数据 + 认证标签) - */ - fun encrypt(data: String, licence: String, fingerprint: String): String { - return try { - // 使用 licence + fingerprint 生成密钥 - val key = generateKey(licence, fingerprint) - - // 生成随机 IV(12字节) - val iv = ByteArray(GCM_IV_LENGTH) - secureRandom.nextBytes(iv) - - // 创建加密器 - val cipher = Cipher.getInstance(TRANSFORMATION) - val parameterSpec = GCMParameterSpec(GCM_TAG_LENGTH * 8, iv) // 标签长度以位为单位 - cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec) - - // 加密数据 - val encryptedBytes = cipher.doFinal(data.toByteArray(StandardCharsets.UTF_8)) - - // 组合:IV + 加密数据(包含认证标签) - val combined = ByteArray(iv.size + encryptedBytes.size) - System.arraycopy(iv, 0, combined, 0, iv.size) - System.arraycopy(encryptedBytes, 0, combined, iv.size, encryptedBytes.size) - - // 返回 Base64 编码的加密数据 - Base64.getEncoder().encodeToString(combined) - } catch (e: Exception) { - logger.error(e) { "AES-256-GCM加密任务数据失败" } - throw RuntimeException("加密任务数据失败: ${e.message}", e) - } - } - - /** - * 使用 licence + fingerprint 解密任务数据(AES-256-GCM) - * @param encryptedData Base64编码的加密数据(包含IV + 加密数据 + 认证标签) - * @param licence 授权码 - * @param fingerprint 硬件指纹 - * @return 解密后的数据(JSON字符串) - */ - fun decrypt(encryptedData: String, licence: String, fingerprint: String): String { - return try { - // 使用 licence + fingerprint 生成密钥 - val key = generateKey(licence, fingerprint) - - // Base64 解码 - val combined = Base64.getDecoder().decode(encryptedData) - - // 分离 IV 和加密数据 - if (combined.size < GCM_IV_LENGTH) { - throw IllegalArgumentException("加密数据格式错误:数据长度不足") - } - - val iv = combined.sliceArray(0 until GCM_IV_LENGTH) - val cipherText = combined.sliceArray(GCM_IV_LENGTH until combined.size) - - // 创建解密器 - 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(cipherText) - - // 返回解密后的字符串 - String(decryptedBytes, StandardCharsets.UTF_8) - } catch (e: Exception) { - logger.error(e) { "AES-256-GCM解密任务数据失败" } - throw RuntimeException("解密任务数据失败: ${e.message}", e) - } - } - - /** - * 使用 licence + fingerprint 生成 AES-256 密钥(256位 = 32字节) - * 使用 SHA-256 哈希的全部32字节作为密钥 - */ - private fun generateKey(licence: String, fingerprint: String): SecretKeySpec { - val combined = "$licence$fingerprint" - val digest = MessageDigest.getInstance("SHA-256") - val hash = digest.digest(combined.toByteArray(StandardCharsets.UTF_8)) - - // 使用全部32字节作为 AES-256 密钥 - return SecretKeySpec(hash, ALGORITHM) - } -} - diff --git a/docs/工具箱端-授权对接指南/utils/ZipVerifierUtil.kt b/docs/工具箱端-授权对接指南/utils/ZipVerifierUtil.kt deleted file mode 100644 index c5c567a..0000000 --- a/docs/工具箱端-授权对接指南/utils/ZipVerifierUtil.kt +++ /dev/null @@ -1,134 +0,0 @@ -package top.tangyh.lamp.filing.utils - -import com.fasterxml.jackson.databind.ObjectMapper -import io.github.oshai.kotlinlogging.KotlinLogging -import org.bouncycastle.openpgp.* -import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.security.MessageDigest -import java.security.Security -import java.util.zip.ZipFile - -object ZipVerifierUtil { - - private val logger = KotlinLogging.logger {} - -// @JvmStatic -// fun main(args: Array) { -// verifyZip("signed.zip", "public.key") -// } - - /** - * 验证 ZIP 文件 - */ - @Throws(Exception::class) - fun verifyZip(zipPath: String, pubkeyContent: String):Boolean { - - println(Security.getProviders().joinToString { it.name }) - val publicKey = readPublicKey( - ByteArrayInputStream(pubkeyContent.toByteArray()) - ) - - val zip = ZipFile(zipPath) - - // 1. 读取 manifest.json - val manifestEntry = zip.getEntry("META-INF/manifest.json") - ?: throw RuntimeException("manifest.json is missing!") - val manifestJson = zip.getInputStream(manifestEntry).readAllBytes().toString(Charsets.UTF_8) - - // 2. 读取 signature.asc - val sigEntry = zip.getEntry("META-INF/signature.asc") - ?: throw RuntimeException("signature.asc is missing!") - val signature = zip.getInputStream(sigEntry).readAllBytes() - - // 3. 使用 OpenPGP 验证签名 - val ok = verifyDetachedSignature(publicKey, manifestJson.toByteArray(), signature) - if (!ok) throw RuntimeException("PGP signature invalid!") - - // 4. 校验 manifest 里每个文件的 SHA-256 - val mapper = ObjectMapper() - val manifest = mapper.readValue(manifestJson, Map::class.java) - val files = manifest["files"] as? Map - ?: throw RuntimeException("Invalid manifest.json: missing 'files'") - - for ((name, expectedHash) in files) { - val entry = zip.getEntry(name) - ?: throw RuntimeException("文件不存在: $name") - - val data = zip.getInputStream(entry).readAllBytes() - val hash = sha256Hex(data) - - if (!hash.equals(expectedHash, ignoreCase = true)) { - throw RuntimeException("Hash mismatch: $name") - } - } - return true - } - - @Throws(Exception::class) - private fun sha256Hex(data: ByteArray): String { - val md = MessageDigest.getInstance("SHA-256") - return bytesToHex(md.digest(data)) - } - - private fun bytesToHex(bytes: ByteArray): String { - return bytes.joinToString("") { "%02x".format(it) } - } - - @Throws(Exception::class) - private fun readPublicKey(keyIn: InputStream): PGPPublicKey { - val keyRings = PGPPublicKeyRingCollection( - PGPUtil.getDecoderStream(keyIn), - JcaKeyFingerprintCalculator() - ) - - for (keyRing in keyRings) { - for (key in keyRing) { - if (key.isEncryptionKey || key.isMasterKey) { - return key - } - } - } - - throw IllegalArgumentException("Can't find public key") - } - - @Throws(Exception::class) - private fun verifyDetachedSignature( - key: PGPPublicKey, - data: ByteArray, - sigBytes: ByteArray - ): Boolean { - - val decoder = PGPUtil.getDecoderStream(ByteArrayInputStream(sigBytes)) - val factory = PGPObjectFactory(decoder, JcaKeyFingerprintCalculator()) - - val message = factory.nextObject() - ?: throw IllegalArgumentException("Invalid signature file") - - val sigList = when (message) { - is PGPSignatureList -> message - is PGPCompressedData -> { - val compressedFactory = PGPObjectFactory( - message.dataStream, - JcaKeyFingerprintCalculator() - ) - val compressedObj = compressedFactory.nextObject() - compressedObj as? PGPSignatureList - ?: throw IllegalArgumentException("Invalid PGP signature (not signature list)") - } - else -> - throw IllegalArgumentException("Unsupported PGP signature format: ${message::class.java}") - } - - val sig = sigList[0] - - sig.init(JcaPGPContentVerifierBuilderProvider().setProvider("BC"), key) - sig.update(data) - - return sig.verify() - } - -} diff --git a/docs/工具箱端-授权对接指南/工具箱端-任务二维码解密指南.md b/docs/工具箱端-授权对接指南/工具箱端-任务二维码解密指南.md deleted file mode 100644 index 88d01e6..0000000 --- a/docs/工具箱端-授权对接指南/工具箱端-任务二维码解密指南.md +++ /dev/null @@ -1,644 +0,0 @@ -# 工具箱端 - 任务二维码解密指南 - -## 概述 - -本文档说明工具箱端如何解密任务二维码数据。App 创建任务后,平台会生成加密的任务数据并返回给 App,App 将其生成二维码。工具箱扫描二维码后,需要使用自己的 `licence` 和 `fingerprint` 解密任务数据。 - -> ### UX 集成模式补充(当前项目实现) -> -> 在当前集成模式中,工具箱扫描二维码后将密文提交给 UX 的 `crypto.decryptTask`。 -> UX 从本地配置读取 licence/fingerprint 执行底层解密并返回明文字符串。 - -## 一、业务流程 - -``` -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 库(二维码扫描) - -## 十、联系支持 - -如有问题,请联系平台技术支持团队获取: -- 测试环境地址 -- 技术支持 - diff --git a/docs/工具箱端-授权对接指南/工具箱端-报告加密与签名生成指南.md b/docs/工具箱端-授权对接指南/工具箱端-报告加密与签名生成指南.md deleted file mode 100644 index d7dd2ff..0000000 --- a/docs/工具箱端-授权对接指南/工具箱端-报告加密与签名生成指南.md +++ /dev/null @@ -1,647 +0,0 @@ -# 工具箱端 - 报告加密与签名生成指南 - -## 概述 - -本文档说明工具箱端如何生成加密和签名的检查报告 ZIP 文件,以确保: -1. **授权校验**:只有合法授权的工具箱才能生成有效的报告 -2. **防篡改校验**:确保报告内容在传输过程中未被篡改 - -> ### UX 集成模式补充(当前项目实现) -> -> 在当前集成模式中,工具箱可将原始报告 ZIP 直接上传到 UX 的 `crypto.signAndPackReport`: -> -> 1. 工具箱先通过 `config.setLicence` 完成本地 licence 配置; -> 2. 工具箱传入 `pgpPrivateKey`、`signingContext`、`summaryJson` 与 `rawZip`; -> 3. UX 从本地配置读取 licence/fingerprint,执行签名与打包能力,生成 `summary.json`、`META-INF/manifest.json`、`META-INF/signature.asc`; -> 4. UX 返回签名后的 ZIP(二进制文件响应),工具箱再用于离线介质回传平台。 - -## 一、ZIP 文件结构要求 - -工具箱生成的 ZIP 文件必须包含以下文件: - -``` -report.zip -├── summary.json # 摘要信息(必须包含授权和签名字段) -├── assets.json # 资产信息(用于签名校验) -├── vulnerabilities.json # 漏洞信息(用于签名校验) -├── weakPasswords.json # 弱密码信息(用于签名校验) -├── 漏洞评估报告.html # 漏洞评估报告(用于签名校验) -└── META-INF/ - ├── manifest.json # 文件清单(用于 OpenPGP 签名) - └── signature.asc # OpenPGP 签名文件(防篡改) -``` - -## 二、授权校验 - 设备签名(device_signature) - -### 2.1 目的 - -设备签名用于验证报告是由合法授权的工具箱生成的,防止第三方伪造扫描结果。 - -### 2.2 密钥派生 - -使用 **HKDF-SHA256** 从设备的 `licence` 和 `fingerprint` 派生签名密钥: - -``` -K = HKDF( - input = licence + fingerprint, # 输入密钥材料(字符串拼接) - salt = "AUTH_V3_SALT", # 固定盐值 - info = "device_report_signature", # 固定信息参数 - hash = SHA-256, # 哈希算法 - length = 32 # 输出密钥长度(32字节 = 256位) -) -``` - -**伪代码示例**: -```python -import hkdf - -# 输入密钥材料 -ikm = licence + fingerprint # 字符串直接拼接 - -# HKDF 参数 -salt = "AUTH_V3_SALT" -info = "device_report_signature" -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() -``` - -### 2.3 签名数据组装(严格顺序) - -签名数据必须按照以下**严格顺序**组装: - -``` -sign_payload = - taskId + # 任务ID(字符串) - inspectionId + # 检查ID(数字转字符串) - SHA256(assets.json) + # assets.json 的 SHA256(hex字符串,小写) - SHA256(vulnerabilities.json) + # vulnerabilities.json 的 SHA256(hex字符串,小写) - SHA256(weakPasswords.json) + # weakPasswords.json 的 SHA256(hex字符串,小写) - SHA256(漏洞评估报告.html) # 漏洞评估报告.html 的 SHA256(hex字符串,小写) -``` - -**重要说明**: -- 所有字符串直接拼接,**不添加任何分隔符** -- SHA256 哈希值必须是 **hex 字符串(小写)**,例如:`a1b2c3d4...` -- 文件内容必须是**原始字节**,不能进行任何编码转换 -- 顺序必须严格一致,任何顺序错误都会导致签名验证失败 - -**伪代码示例**: -```python -import hashlib - -# 1. 读取文件内容(原始字节) -assets_content = read_file("assets.json") -vulnerabilities_content = read_file("vulnerabilities.json") -weak_passwords_content = read_file("weakPasswords.json") -report_html_content = read_file("漏洞评估报告.html") - -# 2. 计算 SHA256(hex字符串,小写) -def sha256_hex(content: bytes) -> str: - return hashlib.sha256(content).hexdigest() - -assets_sha256 = sha256_hex(assets_content) -vulnerabilities_sha256 = sha256_hex(vulnerabilities_content) -weak_passwords_sha256 = sha256_hex(weak_passwords_content) -report_html_sha256 = sha256_hex(report_html_content) - -# 3. 组装签名数据(严格顺序,直接拼接) -sign_payload = ( - str(task_id) + - str(inspection_id) + - assets_sha256 + - vulnerabilities_sha256 + - weak_passwords_sha256 + - report_html_sha256 -) -``` - -### 2.4 生成设备签名 - -使用 **HMAC-SHA256** 计算签名: - -``` -device_signature = Base64(HMAC-SHA256(key=K, data=sign_payload)) -``` - -**伪代码示例**: -```python -import hmac -import base64 - -# 使用派生密钥计算 HMAC-SHA256 -mac = hmac.new( - key=derived_key, # 派生密钥(32字节) - msg=sign_payload.encode('utf-8'), # 签名数据(UTF-8编码) - digestmod=hashlib.sha256 -) - -# 计算签名 -signature_bytes = mac.digest() - -# Base64 编码 -device_signature = base64.b64encode(signature_bytes).decode('utf-8') -``` - -### 2.5 写入 summary.json - -将 `device_signature` 写入 `summary.json`: - -```json -{ - "orgId": 1173040813421105152, - "checkId": 702286470691215417, - "taskId": "TASK-20260115-4875", - "licence": "LIC-8F2A-XXXX", - "fingerprint": "FP-2c91e9f3", - "deviceSignature": "Base64编码的签名值", - "summary": "检查摘要信息", - ...其他字段... -} -``` - -**必需字段**: -- `licence`:设备授权码(字符串) -- `fingerprint`:设备硬件指纹(字符串) -- `taskId`:任务ID(字符串) -- `deviceSignature`:设备签名(Base64字符串) -- `checkId` 或 `inspectionId`:检查ID(数字) - -## 三、防篡改校验 - OpenPGP 签名 - -### 3.1 目的 - -OpenPGP 签名用于验证 ZIP 文件在传输过程中未被篡改,确保文件完整性。 - -### 3.2 生成 manifest.json - -创建 `META-INF/manifest.json` 文件,包含所有文件的 SHA-256 哈希值: - -```json -{ - "files": { - "summary.json": "a1b2c3d4e5f6...", - "assets.json": "b2c3d4e5f6a1...", - "vulnerabilities.json": "c3d4e5f6a1b2...", - "weakPasswords.json": "d4e5f6a1b2c3...", - "漏洞评估报告.html": "e5f6a1b2c3d4..." - } -} -``` - -**伪代码示例**: -```python -import hashlib -import json - -def calculate_sha256_hex(content: bytes) -> str: - return hashlib.sha256(content).hexdigest() - -# 计算所有文件的 SHA256 -files_hashes = { - "summary.json": calculate_sha256_hex(summary_content), - "assets.json": calculate_sha256_hex(assets_content), - "vulnerabilities.json": calculate_sha256_hex(vulnerabilities_content), - "weakPasswords.json": calculate_sha256_hex(weak_passwords_content), - "漏洞评估报告.html": calculate_sha256_hex(report_html_content) -} - -# 生成 manifest.json -manifest = { - "files": files_hashes -} - -manifest_json = json.dumps(manifest, ensure_ascii=False, indent=2) -``` - -### 3.3 生成 OpenPGP 签名 - -使用工具箱的**私钥**对 `manifest.json` 进行 OpenPGP 签名,生成 `META-INF/signature.asc`: - -**伪代码示例(使用 Python gnupg)**: -```python -import gnupg - -# 初始化 GPG -gpg = gnupg.GPG() - -# 导入私钥(或使用已配置的密钥) -# gpg.import_keys(private_key_data) - -# 对 manifest.json 进行签名 -with open('META-INF/manifest.json', 'rb') as f: - signed_data = gpg.sign_file( - f, - detach=True, # 分离式签名 - clearsign=False, # 不使用明文签名 - output='META-INF/signature.asc' - ) -``` - -**伪代码示例(使用 BouncyCastle - Java/Kotlin)**: -```kotlin -import org.bouncycastle.openpgp.* -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPPrivateKey -import java.io.ByteArrayOutputStream -import java.io.FileOutputStream - -fun generatePGPSignature( - manifestContent: ByteArray, - privateKey: PGPPrivateKey, - publicKey: PGPPublicKey -): ByteArray { - val signatureGenerator = PGPSignatureGenerator( - JcaPGPContentSignerBuilder(publicKey.algorithm, PGPUtil.SHA256) - ) - signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey) - signatureGenerator.update(manifestContent) - - val signature = signatureGenerator.generate() - val signatureList = PGPSignatureList(signature) - - val out = ByteArrayOutputStream() - val pgpOut = PGPObjectFactory(PGPUtil.getEncoderStream(out)) - signatureList.encode(out) - - return out.toByteArray() -} -``` - -### 3.4 打包 ZIP 文件 - -将所有文件打包成 ZIP 文件,确保包含: -- 所有报告文件(summary.json、assets.json 等) -- `META-INF/manifest.json` -- `META-INF/signature.asc` - -**伪代码示例**: -```python -import zipfile - -def create_signed_zip(output_path: str): - with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf: - # 添加报告文件 - zipf.write('summary.json', 'summary.json') - zipf.write('assets.json', 'assets.json') - zipf.write('vulnerabilities.json', 'vulnerabilities.json') - zipf.write('weakPasswords.json', 'weakPasswords.json') - zipf.write('漏洞评估报告.html', '漏洞评估报告.html') - - # 添加签名文件 - zipf.write('META-INF/manifest.json', 'META-INF/manifest.json') - zipf.write('META-INF/signature.asc', 'META-INF/signature.asc') -``` - -## 四、完整流程示例 - -### 4.1 Python 完整示例 - -```python -import hashlib -import hmac -import base64 -import json -import zipfile -import hkdf -import gnupg - -def generate_report_zip( - licence: str, - fingerprint: str, - task_id: str, - inspection_id: int, - output_path: str -): - """ - 生成带签名和加密的检查报告 ZIP 文件 - """ - - # ========== 1. 读取报告文件 ========== - assets_content = read_file("assets.json") - vulnerabilities_content = read_file("vulnerabilities.json") - weak_passwords_content = read_file("weakPasswords.json") - report_html_content = read_file("漏洞评估报告.html") - - # ========== 2. 生成设备签名 ========== - - # 2.1 密钥派生 - ikm = licence + fingerprint - salt = "AUTH_V3_SALT" - info = "device_report_signature" - key_length = 32 - - 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() - - # 2.2 计算文件 SHA256 - def sha256_hex(content: bytes) -> str: - return hashlib.sha256(content).hexdigest() - - assets_sha256 = sha256_hex(assets_content) - vulnerabilities_sha256 = sha256_hex(vulnerabilities_content) - weak_passwords_sha256 = sha256_hex(weak_passwords_content) - report_html_sha256 = sha256_hex(report_html_content) - - # 2.3 组装签名数据(严格顺序) - sign_payload = ( - str(task_id) + - str(inspection_id) + - assets_sha256 + - vulnerabilities_sha256 + - weak_passwords_sha256 + - report_html_sha256 - ) - - # 2.4 计算 HMAC-SHA256 - mac = hmac.new( - key=derived_key, - msg=sign_payload.encode('utf-8'), - digestmod=hashlib.sha256 - ) - device_signature = base64.b64encode(mac.digest()).decode('utf-8') - - # 2.5 生成 summary.json - summary = { - "orgId": 1173040813421105152, - "checkId": inspection_id, - "taskId": task_id, - "licence": licence, - "fingerprint": fingerprint, - "deviceSignature": device_signature, - "summary": "检查摘要信息" - } - summary_content = json.dumps(summary, ensure_ascii=False).encode('utf-8') - - # ========== 3. 生成 OpenPGP 签名 ========== - - # 3.1 生成 manifest.json - files_hashes = { - "summary.json": sha256_hex(summary_content), - "assets.json": assets_sha256, - "vulnerabilities.json": vulnerabilities_sha256, - "weakPasswords.json": weak_passwords_sha256, - "漏洞评估报告.html": report_html_sha256 - } - manifest = {"files": files_hashes} - manifest_content = json.dumps(manifest, ensure_ascii=False, indent=2).encode('utf-8') - - # 3.2 生成 OpenPGP 签名 - gpg = gnupg.GPG() - with open('META-INF/manifest.json', 'wb') as f: - f.write(manifest_content) - - with open('META-INF/manifest.json', 'rb') as f: - signed_data = gpg.sign_file( - f, - detach=True, - output='META-INF/signature.asc' - ) - - # ========== 4. 打包 ZIP 文件 ========== - with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf: - zipf.writestr('summary.json', summary_content) - zipf.writestr('assets.json', assets_content) - zipf.writestr('vulnerabilities.json', vulnerabilities_content) - zipf.writestr('weakPasswords.json', weak_passwords_content) - zipf.writestr('漏洞评估报告.html', report_html_content) - zipf.writestr('META-INF/manifest.json', manifest_content) - zipf.write('META-INF/signature.asc', 'META-INF/signature.asc') - - print(f"报告 ZIP 文件生成成功: {output_path}") -``` - -### 4.2 Java/Kotlin 完整示例 - -```kotlin -import org.bouncycastle.crypto.digests.SHA256Digest -import org.bouncycastle.crypto.generators.HKDFBytesGenerator -import org.bouncycastle.crypto.params.HKDFParameters -import java.security.MessageDigest -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec -import java.util.Base64 -import java.util.zip.ZipOutputStream -import java.io.FileOutputStream - -fun generateReportZip( - licence: String, - fingerprint: String, - taskId: String, - inspectionId: Long, - outputPath: String -) { - // ========== 1. 读取报告文件 ========== - val assetsContent = readFile("assets.json") - val vulnerabilitiesContent = readFile("vulnerabilities.json") - val weakPasswordsContent = readFile("weakPasswords.json") - val reportHtmlContent = readFile("漏洞评估报告.html") - - // ========== 2. 生成设备签名 ========== - - // 2.1 密钥派生 - val ikm = (licence + fingerprint).toByteArray(Charsets.UTF_8) - val salt = "AUTH_V3_SALT".toByteArray(Charsets.UTF_8) - val info = "device_report_signature".toByteArray(Charsets.UTF_8) - val keyLength = 32 - - val hkdf = HKDFBytesGenerator(SHA256Digest()) - hkdf.init(HKDFParameters(ikm, salt, info)) - val derivedKey = ByteArray(keyLength) - hkdf.generateBytes(derivedKey, 0, keyLength) - - // 2.2 计算文件 SHA256 - fun sha256Hex(content: ByteArray): String { - val digest = MessageDigest.getInstance("SHA-256") - val hashBytes = digest.digest(content) - return hashBytes.joinToString("") { "%02x".format(it) } - } - - val assetsSha256 = sha256Hex(assetsContent) - val vulnerabilitiesSha256 = sha256Hex(vulnerabilitiesContent) - val weakPasswordsSha256 = sha256Hex(weakPasswordsContent) - val reportHtmlSha256 = sha256Hex(reportHtmlContent) - - // 2.3 组装签名数据(严格顺序) - val signPayload = buildString { - append(taskId) - append(inspectionId) - append(assetsSha256) - append(vulnerabilitiesSha256) - append(weakPasswordsSha256) - append(reportHtmlSha256) - } - - // 2.4 计算 HMAC-SHA256 - val mac = Mac.getInstance("HmacSHA256") - val secretKey = SecretKeySpec(derivedKey, "HmacSHA256") - mac.init(secretKey) - val signatureBytes = mac.doFinal(signPayload.toByteArray(Charsets.UTF_8)) - val deviceSignature = Base64.getEncoder().encodeToString(signatureBytes) - - // 2.5 生成 summary.json - val summary = mapOf( - "orgId" to 1173040813421105152L, - "checkId" to inspectionId, - "taskId" to taskId, - "licence" to licence, - "fingerprint" to fingerprint, - "deviceSignature" to deviceSignature, - "summary" to "检查摘要信息" - ) - val summaryContent = objectMapper.writeValueAsString(summary).toByteArray(Charsets.UTF_8) - - // ========== 3. 生成 OpenPGP 签名 ========== - - // 3.1 生成 manifest.json - val filesHashes = mapOf( - "summary.json" to sha256Hex(summaryContent), - "assets.json" to assetsSha256, - "vulnerabilities.json" to vulnerabilitiesSha256, - "weakPasswords.json" to weakPasswordsSha256, - "漏洞评估报告.html" to reportHtmlSha256 - ) - val manifest = mapOf("files" to filesHashes) - val manifestContent = objectMapper.writeValueAsString(manifest).toByteArray(Charsets.UTF_8) - - // 3.2 生成 OpenPGP 签名(使用 BouncyCastle) - val signatureAsc = generatePGPSignature(manifestContent, privateKey, publicKey) - - // ========== 4. 打包 ZIP 文件 ========== - ZipOutputStream(FileOutputStream(outputPath)).use { zipOut -> - zipOut.putNextEntry(ZipEntry("summary.json")) - zipOut.write(summaryContent) - zipOut.closeEntry() - - zipOut.putNextEntry(ZipEntry("assets.json")) - zipOut.write(assetsContent) - zipOut.closeEntry() - - zipOut.putNextEntry(ZipEntry("vulnerabilities.json")) - zipOut.write(vulnerabilitiesContent) - zipOut.closeEntry() - - zipOut.putNextEntry(ZipEntry("weakPasswords.json")) - zipOut.write(weakPasswordsContent) - zipOut.closeEntry() - - zipOut.putNextEntry(ZipEntry("漏洞评估报告.html")) - zipOut.write(reportHtmlContent) - zipOut.closeEntry() - - zipOut.putNextEntry(ZipEntry("META-INF/manifest.json")) - zipOut.write(manifestContent) - zipOut.closeEntry() - - zipOut.putNextEntry(ZipEntry("META-INF/signature.asc")) - zipOut.write(signatureAsc) - zipOut.closeEntry() - } - - println("报告 ZIP 文件生成成功: $outputPath") -} -``` - -## 五、平台端验证流程 - -平台端会按以下顺序验证: - -1. **OpenPGP 签名验证**(防篡改) - - 读取 `META-INF/manifest.json` 和 `META-INF/signature.asc` - - 使用平台公钥验证签名 - - 验证所有文件的 SHA256 是否与 manifest.json 中的哈希值匹配 - -2. **设备签名验证**(授权) - - 从 `summary.json` 提取 `licence`、`fingerprint`、`taskId`、`deviceSignature` - - 验证 `licence + fingerprint` 是否已绑定 - - 验证 `taskId` 是否存在且属于该设备 - - 使用相同的 HKDF 派生密钥 - - 重新计算签名并与 `deviceSignature` 比较 - -## 六、常见错误和注意事项 - -### 6.1 设备签名验证失败 - -**可能原因**: -1. **密钥派生错误**:确保使用正确的 `salt` 和 `info` 参数 -2. **签名数据顺序错误**:必须严格按照 `taskId + inspectionId + SHA256(...)` 的顺序 -3. **SHA256 格式错误**:必须是 hex 字符串(小写),不能包含分隔符 -4. **文件内容错误**:确保使用原始文件内容,不能进行编码转换 -5. **licence 或 fingerprint 不匹配**:确保与平台绑定的值一致 - -### 6.2 OpenPGP 签名验证失败 - -**可能原因**: -1. **私钥不匹配**:确保使用与平台公钥对应的私钥 -2. **manifest.json 格式错误**:确保 JSON 格式正确 -3. **文件哈希值错误**:确保 manifest.json 中的哈希值与实际文件匹配 - -### 6.3 文件缺失 - -**必需文件**: -- `summary.json`(必须包含授权字段) -- `assets.json` -- `vulnerabilities.json` -- `weakPasswords.json`(文件名大小写不敏感) -- `漏洞评估报告.html`(文件名包含"漏洞评估报告"且以".html"结尾) -- `META-INF/manifest.json` -- `META-INF/signature.asc` - -## 七、安全设计说明 - -### 7.1 为什么第三方无法伪造 - -1. **设备签名**: - - 只有拥有正确 `licence + fingerprint` 的设备才能派生正确的签名密钥 - - 即使第三方获取了某个设备的签名,也无法用于其他任务(`taskId` 绑定) - - 即使第三方修改了报告内容,签名也会失效(多个文件的 SHA256 绑定) - -2. **OpenPGP 签名**: - - 只有拥有私钥的工具箱才能生成有效签名 - - 任何文件修改都会导致哈希值不匹配 - -### 7.2 密钥分离 - -使用 HKDF 的 `info` 参数区分不同用途的密钥: -- `device_report_signature`:用于设备签名 -- 其他用途可以使用不同的 `info` 值,确保密钥隔离 - -## 八、测试建议 - -1. **单元测试**: - - 测试密钥派生是否正确 - - 测试签名生成和验证是否匹配 - - 测试文件 SHA256 计算是否正确 - -2. **集成测试**: - - 使用真实数据生成 ZIP 文件 - - 上传到平台验证是否通过 - - 测试篡改文件后验证是否失败 - -3. **边界测试**: - - 测试文件缺失的情况 - - 测试签名数据顺序错误的情况 - - 测试错误的 `licence` 或 `fingerprint` 的情况 - -## 九、参考实现 - -- **HKDF 实现**:BouncyCastle(Java/Kotlin)、`hkdf` 库(Python) -- **HMAC-SHA256**:Java `javax.crypto.Mac`、Python `hmac` -- **OpenPGP**:BouncyCastle(Java/Kotlin)、`gnupg` 库(Python) - -## 十、联系支持 - -如有问题,请联系平台技术支持团队。 - diff --git a/docs/工具箱端-授权对接指南/工具箱端-摘要信息二维码生成指南.md b/docs/工具箱端-授权对接指南/工具箱端-摘要信息二维码生成指南.md deleted file mode 100644 index 7ed686e..0000000 --- a/docs/工具箱端-授权对接指南/工具箱端-摘要信息二维码生成指南.md +++ /dev/null @@ -1,756 +0,0 @@ -# 工具箱端 - 摘要信息二维码生成指南 - -## 概述 - -本文档说明工具箱端如何生成摘要信息二维码。工具箱完成检查任务后,需要将摘要信息加密并生成二维码,供 App 扫描后上传到平台。 - -> ### UX 集成模式补充(当前项目实现) -> -> 在当前集成模式中,工具箱将明文文本传给 UX 的 `crypto.encryptSummary`,并提供 `salt`。 -> UX 从本地配置读取 licence/fingerprint,执行 HKDF + AES-256-GCM 并返回 Base64 密文。 - -## 一、业务流程 - -``` -工具箱完成检查 → 准备摘要信息 → 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().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 库(二维码生成) - -## 十三、联系支持 - -如有问题,请联系平台技术支持团队获取: -- 测试环境地址 -- 技术支持 - diff --git a/docs/工具箱端-授权对接指南/工具箱端-设备授权二维码生成指南.md b/docs/工具箱端-授权对接指南/工具箱端-设备授权二维码生成指南.md deleted file mode 100644 index 5e08b87..0000000 --- a/docs/工具箱端-授权对接指南/工具箱端-设备授权二维码生成指南.md +++ /dev/null @@ -1,601 +0,0 @@ -# 工具箱端 - 设备授权二维码生成指南 - -## 概述 - -本文档说明工具箱端如何生成设备授权二维码,用于设备首次授权和绑定。App 扫描二维码后,会将加密的设备信息提交到平台完成授权校验和绑定。 - -> ### UX 集成模式补充(当前项目实现) -> -> 调用前提:工具箱先调用 `config.setLicence` 写入本地 licence(fingerprint 由 UX 本机计算并持久化)。 -> -> 在当前集成模式中,工具箱调用 UX 的 `crypto.encryptDeviceInfo`,直接传入 -> `platformPublicKey` 获取加密后的 Base64 密文。 -> UX 不保存业务设备实体,仅保存本机身份材料(licence/fingerprint)。 - -## 一、业务流程 - -``` -工具箱 → 生成设备信息 → RSA-OAEP加密 → Base64编码 → 生成二维码 - ↓ -App扫描二维码 → 提取加密数据 → 调用平台接口 → 平台解密验证 → 授权成功 -``` - -## 二、设备信息准备 - -### 2.1 设备信息字段 - -工具箱需要准备以下设备信息: - -| 字段名 | 类型 | 说明 | 示例 | -|--------|------|------|------| -| `licence` | String | 设备授权码(工具箱唯一标识) | `"LIC-8F2A-XXXX"` | -| `fingerprint` | String | 设备硬件指纹(设备唯一标识) | `"FP-2c91e9f3"` | - -### 2.2 生成设备信息 JSON - -将设备信息组装成 JSON 格式: - -```json -{ - "licence": "LIC-8F2A-XXXX", - "fingerprint": "FP-2c91e9f3" -} -``` - -**重要说明**: -- `licence` 和 `fingerprint` 必须是字符串类型 -- JSON 格式必须正确,不能有多余的逗号或格式错误 -- 建议使用标准的 JSON 库生成,避免手动拼接 - -**伪代码示例**: -```python -import json - -device_info = { - "licence": "LIC-8F2A-XXXX", # 工具箱授权码 - "fingerprint": "FP-2c91e9f3" # 设备硬件指纹 -} - -# 转换为 JSON 字符串 -device_info_json = json.dumps(device_info, ensure_ascii=False) -# 结果: {"licence":"LIC-8F2A-XXXX","fingerprint":"FP-2c91e9f3"} -``` - -## 三、RSA-OAEP 加密 - -### 3.1 加密算法 - -使用 **RSA-OAEP** 非对称加密算法: - -- **算法名称**:`RSA/ECB/OAEPWithSHA-256AndMGF1Padding` -- **密钥长度**:2048 位(推荐) -- **填充方式**:OAEP with SHA-256 and MGF1 -- **加密方向**:使用**平台公钥**加密,平台使用私钥解密 - -### 3.2 获取平台公钥 - -平台公钥需要从平台获取,通常以 **Base64 编码**的字符串形式提供。 - -**公钥格式**: -- 格式:X.509 标准格式(DER 编码) -- 存储:Base64 编码的字符串 -- 示例:`MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzDlZvMDVaL+fjl05Hi182JOAUAaN4gh9rOF+1NhKfO4J6e0HLy8lBuylp3A4xoTiyUejNm22h0dqAgDSPnY/xZR76POFTD1soHr2LaFCN8JAbQ96P8gE7wC9qpoTssVvIVRH7QbVd260J6eD0Szwcx9cg591RSN69pMpe5IVRi8T99Hhql6/wnZHORPr18eESLOY93jRskLzc0q18r68RRoTJiQf+9YC8ub5iKp7rCjVnPi1UbIYmXmL08tk5mksYA0NqWQAa1ofKxx/9tQtB9uTjhTxuTu94XU9jlGU87qaHZs+kpqa8CAbYYJFbSP1xHwoZzpU2jpw2aF22HBYxwIDAQAB` - -### 3.3 加密步骤 - -1. **加载平台公钥**:从 Base64 字符串加载公钥对象 -2. **初始化加密器**:使用 `RSA/ECB/OAEPWithSHA-256AndMGF1Padding` 算法 -3. **加密数据**:使用公钥加密设备信息 JSON 字符串(UTF-8 编码) -4. **Base64 编码**:将加密后的字节数组进行 Base64 编码 - -### 3.4 Python 实现示例 - -```python -import base64 -import json -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import padding -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.backends import default_backend - -def encrypt_device_info(licence: str, fingerprint: str, platform_public_key_base64: str) -> str: - """ - 使用平台公钥加密设备信息 - - Args: - licence: 设备授权码 - fingerprint: 设备硬件指纹 - platform_public_key_base64: 平台公钥(Base64编码) - - Returns: - Base64编码的加密数据 - """ - # 1. 组装设备信息 JSON - device_info = { - "licence": licence, - "fingerprint": fingerprint - } - device_info_json = json.dumps(device_info, ensure_ascii=False) - - # 2. 加载平台公钥 - public_key_bytes = base64.b64decode(platform_public_key_base64) - public_key = serialization.load_der_public_key( - public_key_bytes, - backend=default_backend() - ) - - # 3. 使用 RSA-OAEP 加密 - # OAEP padding with SHA-256 and MGF1 - encrypted_bytes = public_key.encrypt( - device_info_json.encode('utf-8'), - padding.OAEP( - mgf=padding.MGF1(algorithm=hashes.SHA256()), - algorithm=hashes.SHA256(), - label=None - ) - ) - - # 4. Base64 编码 - encrypted_base64 = base64.b64encode(encrypted_bytes).decode('utf-8') - - return encrypted_base64 -``` - -### 3.5 Java/Kotlin 实现示例 - -```kotlin -import java.security.KeyFactory -import java.security.PublicKey -import java.security.spec.X509EncodedKeySpec -import java.util.Base64 -import javax.crypto.Cipher -import java.nio.charset.StandardCharsets - -object DeviceAuthorizationUtil { - - private const val CIPHER_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" - - /** - * 使用平台公钥加密设备信息 - * - * @param licence 设备授权码 - * @param fingerprint 设备硬件指纹 - * @param platformPublicKeyBase64 平台公钥(Base64编码) - * @return Base64编码的加密数据 - */ - fun encryptDeviceInfo( - licence: String, - fingerprint: String, - platformPublicKeyBase64: String - ): String { - // 1. 组装设备信息 JSON - val deviceInfo = mapOf( - "licence" to licence, - "fingerprint" to fingerprint - ) - val deviceInfoJson = objectMapper.writeValueAsString(deviceInfo) - - // 2. 加载平台公钥 - val publicKeyBytes = Base64.getDecoder().decode(platformPublicKeyBase64) - val keySpec = X509EncodedKeySpec(publicKeyBytes) - val keyFactory = KeyFactory.getInstance("RSA") - val publicKey = keyFactory.generatePublic(keySpec) - - // 3. 使用 RSA-OAEP 加密 - val cipher = Cipher.getInstance(CIPHER_ALGORITHM) - cipher.init(Cipher.ENCRYPT_MODE, publicKey) - val encryptedBytes = cipher.doFinal(deviceInfoJson.toByteArray(StandardCharsets.UTF_8)) - - // 4. Base64 编码 - return Base64.getEncoder().encodeToString(encryptedBytes) - } -} -``` - -### 3.6 C# 实现示例 - -```csharp -using System; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; - -public class DeviceAuthorizationUtil -{ - private const string CipherAlgorithm = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; - - /// - /// 使用平台公钥加密设备信息 - /// - public static string EncryptDeviceInfo( - string licence, - string fingerprint, - string platformPublicKeyBase64) - { - // 1. 组装设备信息 JSON - var deviceInfo = new - { - licence = licence, - fingerprint = fingerprint - }; - var deviceInfoJson = JsonSerializer.Serialize(deviceInfo); - - // 2. 加载平台公钥 - var publicKeyBytes = Convert.FromBase64String(platformPublicKeyBase64); - using var rsa = RSA.Create(); - rsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _); - - // 3. 使用 RSA-OAEP 加密 - var encryptedBytes = rsa.Encrypt( - Encoding.UTF8.GetBytes(deviceInfoJson), - RSAEncryptionPadding.OaepSHA256 - ); - - // 4. Base64 编码 - return Convert.ToBase64String(encryptedBytes); - } -} -``` - -## 四、生成二维码 - -### 4.1 二维码内容 - -二维码内容就是加密后的 **Base64 编码字符串**(不是 JSON 格式)。 - -**示例**: -``` -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzDlZvMDVaL+fjl05Hi182JOAUAaN4gh9rOF+1NhKfO4J6e0HLy8lBuylp3A4xoTiyUejNm22h0dqAgDSPnY/xZR76POFTD1soHr2LaFCN8JAbQ96P8gE7wC9qpoTssVvIVRH7QbVd260J6eD0Szwcx9cg591RSN69pMpe5IVRi8T99Hhql6/wnZHORPr18eESLOY93jRskLzc0q18r68RRoTJiQf+9YC8ub5iKp7rCjVnPi1UbIYmXmL08tk5mksYA0NqWQAa1ofKxx/9tQtB9uTjhTxuTu94XU9jlGU87qaHZs+kpqa8CAbYYJFbSP1xHwoZzpU2jpw2aF22HBYxwIDAQAB... -``` - -### 4.2 二维码生成 - -使用标准的二维码生成库生成二维码图片。 - -**Python 示例(使用 qrcode 库)**: -```python -import qrcode -from PIL import Image - -def generate_qr_code(encrypted_data: str, output_path: str = "device_qr.png"): - """ - 生成设备授权二维码 - - Args: - encrypted_data: Base64编码的加密数据 - output_path: 二维码图片保存路径 - """ - qr = qrcode.QRCode( - version=1, # 控制二维码大小(1-40) - error_correction=qrcode.constants.ERROR_CORRECT_M, # 错误纠正级别 - box_size=10, # 每个小方块的像素数 - border=4, # 边框的厚度 - ) - qr.add_data(encrypted_data) - qr.make(fit=True) - - # 创建二维码图片 - img = qr.make_image(fill_color="black", back_color="white") - img.save(output_path) - - print(f"二维码已生成: {output_path}") -``` - -**Java/Kotlin 示例(使用 ZXing 库)**: -```kotlin -import com.google.zxing.BarcodeFormat -import com.google.zxing.EncodeHintType -import com.google.zxing.qrcode.QRCodeWriter -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel -import java.awt.image.BufferedImage -import javax.imageio.ImageIO -import java.io.File - -fun generateQRCode(encryptedData: String, outputPath: String = "device_qr.png") { - val hints = hashMapOf().apply { - put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M) - put(EncodeHintType.CHARACTER_SET, "UTF-8") - put(EncodeHintType.MARGIN, 1) - } - - val writer = QRCodeWriter() - val bitMatrix = writer.encode(encryptedData, 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") -} -``` - -## 五、完整流程示例 - -### 5.1 Python 完整示例 - -```python -import json -import base64 -import qrcode -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import padding -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.backends import default_backend - -def generate_device_authorization_qr( - licence: str, - fingerprint: str, - platform_public_key_base64: str, - qr_output_path: str = "device_qr.png" -) -> str: - """ - 生成设备授权二维码 - - Args: - licence: 设备授权码 - fingerprint: 设备硬件指纹 - platform_public_key_base64: 平台公钥(Base64编码) - qr_output_path: 二维码图片保存路径 - - Returns: - 加密后的Base64字符串(二维码内容) - """ - # 1. 组装设备信息 JSON - device_info = { - "licence": licence, - "fingerprint": fingerprint - } - device_info_json = json.dumps(device_info, ensure_ascii=False) - print(f"设备信息 JSON: {device_info_json}") - - # 2. 加载平台公钥 - public_key_bytes = base64.b64decode(platform_public_key_base64) - public_key = serialization.load_der_public_key( - public_key_bytes, - backend=default_backend() - ) - - # 3. 使用 RSA-OAEP 加密 - encrypted_bytes = public_key.encrypt( - device_info_json.encode('utf-8'), - padding.OAEP( - mgf=padding.MGF1(algorithm=hashes.SHA256()), - algorithm=hashes.SHA256(), - label=None - ) - ) - - # 4. Base64 编码 - encrypted_base64 = base64.b64encode(encrypted_bytes).decode('utf-8') - print(f"加密后的 Base64: {encrypted_base64[:100]}...") # 只显示前100个字符 - - # 5. 生成二维码 - qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_M, - box_size=10, - border=4, - ) - qr.add_data(encrypted_base64) - qr.make(fit=True) - - img = qr.make_image(fill_color="black", back_color="white") - img.save(qr_output_path) - print(f"二维码已生成: {qr_output_path}") - - return encrypted_base64 - -# 使用示例 -if __name__ == "__main__": - # 平台公钥(示例,实际使用时需要从平台获取) - platform_public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzDlZvMDVaL+fjl05Hi182JOAUAaN4gh9rOF+1NhKfO4J6e0HLy8lBuylp3A4xoTiyUejNm22h0dqAgDSPnY/xZR76POFTD1soHr2LaFCN8JAbQ96P8gE7wC9qpoTssVvIVRH7QbVd260J6eD0Szwcx9cg591RSN69pMpe5IVRi8T99Hhql6/wnZHORPr18eESLOY93jRskLzc0q18r68RRoTJiQf+9YC8ub5iKp7rCjVnPi1UbIYmXmL08tk5mksYA0NqWQAa1ofKxx/9tQtB9uTjhTxuTu94XU9jlGU87qaHZs+kpqa8CAbYYJFbSP1xHwoZzpU2jpw2aF22HBYxwIDAQAB" - - # 设备信息 - licence = "LIC-8F2A-XXXX" - fingerprint = "FP-2c91e9f3" - - # 生成二维码 - encrypted_data = generate_device_authorization_qr( - licence=licence, - fingerprint=fingerprint, - platform_public_key_base64=platform_public_key, - qr_output_path="device_authorization_qr.png" - ) - - print(f"\n二维码内容(加密后的Base64):\n{encrypted_data}") -``` - -### 5.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 java.awt.image.BufferedImage -import java.security.KeyFactory -import java.security.PublicKey -import java.security.spec.X509EncodedKeySpec -import java.util.Base64 -import javax.crypto.Cipher -import javax.imageio.ImageIO -import java.io.File -import java.nio.charset.StandardCharsets - -object DeviceAuthorizationQRGenerator { - - private const val CIPHER_ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" - private val objectMapper = ObjectMapper() - - /** - * 生成设备授权二维码 - */ - fun generateDeviceAuthorizationQR( - licence: String, - fingerprint: String, - platformPublicKeyBase64: String, - qrOutputPath: String = "device_qr.png" - ): String { - // 1. 组装设备信息 JSON - val deviceInfo = mapOf( - "licence" to licence, - "fingerprint" to fingerprint - ) - val deviceInfoJson = objectMapper.writeValueAsString(deviceInfo) - println("设备信息 JSON: $deviceInfoJson") - - // 2. 加载平台公钥 - val publicKeyBytes = Base64.getDecoder().decode(platformPublicKeyBase64) - val keySpec = X509EncodedKeySpec(publicKeyBytes) - val keyFactory = KeyFactory.getInstance("RSA") - val publicKey = keyFactory.generatePublic(keySpec) - - // 3. 使用 RSA-OAEP 加密 - val cipher = Cipher.getInstance(CIPHER_ALGORITHM) - cipher.init(Cipher.ENCRYPT_MODE, publicKey) - val encryptedBytes = cipher.doFinal(deviceInfoJson.toByteArray(StandardCharsets.UTF_8)) - - // 4. Base64 编码 - val encryptedBase64 = Base64.getEncoder().encodeToString(encryptedBytes) - println("加密后的 Base64: ${encryptedBase64.take(100)}...") - - // 5. 生成二维码 - val hints = hashMapOf().apply { - put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M) - put(EncodeHintType.CHARACTER_SET, "UTF-8") - put(EncodeHintType.MARGIN, 1) - } - - val writer = QRCodeWriter() - val bitMatrix = writer.encode(encryptedBase64, 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(qrOutputPath)) - println("二维码已生成: $qrOutputPath") - - return encryptedBase64 - } -} - -// 使用示例 -fun main() { - // 平台公钥(示例,实际使用时需要从平台获取) - val platformPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzDlZvMDVaL+fjl05Hi182JOAUAaN4gh9rOF+1NhKfO4J6e0HLy8lBuylp3A4xoTiyUejNm22h0dqAgDSPnY/xZR76POFTD1soHr2LaFCN8JAbQ96P8gE7wC9qpoTssVvIVRH7QbVd260J6eD0Szwcx9cg591RSN69pMpe5IVRi8T99Hhql6/wnZHORPr18eESLOY93jRskLzc0q18r68RRoTJiQf+9YC8ub5iKp7rCjVnPi1UbIYmXmL08tk5mksYA0NqWQAa1ofKxx/9tQtB9uTjhTxuTu94XU9jlGU87qaHZs+kpqa8CAbYYJFbSP1xHwoZzpU2jpw2aF22HBYxwIDAQAB" - - // 设备信息 - val licence = "LIC-8F2A-XXXX" - val fingerprint = "FP-2c91e9f3" - - // 生成二维码 - val encryptedData = DeviceAuthorizationQRGenerator.generateDeviceAuthorizationQR( - licence = licence, - fingerprint = fingerprint, - platformPublicKeyBase64 = platformPublicKey, - qrOutputPath = "device_authorization_qr.png" - ) - - println("\n二维码内容(加密后的Base64):\n$encryptedData") -} -``` - -## 六、平台端验证流程 - -平台端会按以下流程验证: - -1. **接收请求**:App 扫描二维码后,将 `encryptedDeviceInfo` 和 `appid` 提交到平台 -2. **RSA-OAEP 解密**:使用平台私钥解密 `encryptedDeviceInfo` -3. **提取设备信息**:从解密后的 JSON 中提取 `licence` 和 `fingerprint` -4. **设备验证**: - - 检查 `filing_device_licence` 表中是否存在该 `licence` - - 如果存在,验证 `fingerprint` 是否匹配 - - 如果 `fingerprint` 不匹配,记录非法授权日志并返回错误 -5. **App 绑定**:检查 `filing_app_licence` 表中是否存在绑定关系 - - 如果不存在,创建新的绑定记录 - - 如果已存在,返回已绑定信息 -6. **返回响应**:返回 `deviceLicenceId` 和 `licence` - -## 七、常见错误和注意事项 - -### 7.1 加密失败 - -**可能原因**: -1. **公钥格式错误**:确保使用正确的 Base64 编码的公钥 -2. **算法不匹配**:必须使用 `RSA/ECB/OAEPWithSHA-256AndMGF1Padding` -3. **数据长度超限**:RSA-2048 最多加密 245 字节(设备信息 JSON 通常不会超过) -4. **字符编码错误**:确保使用 UTF-8 编码 - -### 7.2 二维码扫描失败 - -**可能原因**: -1. **二维码内容过长**:如果加密后的数据过长,可能需要使用更高版本的二维码(version) -2. **错误纠正级别过低**:建议使用 `ERROR_CORRECT_M` 或更高 -3. **二维码图片质量差**:确保二维码图片清晰,有足够的对比度 - -### 7.3 平台验证失败 - -**可能原因**: -1. **licence 已存在但 fingerprint 不匹配**:设备被替换或授权码被复用 -2. **JSON 格式错误**:确保 JSON 格式正确,字段名和类型匹配 -3. **加密数据损坏**:确保 Base64 编码和解码正确 - -## 八、安全设计说明 - -### 8.1 为什么使用 RSA-OAEP - -1. **非对称加密**:只有平台拥有私钥,可以解密数据 -2. **OAEP 填充**:提供更好的安全性,防止某些攻击 -3. **SHA-256**:使用强哈希算法,提供更好的安全性 - -### 8.2 为什么第三方无法伪造 - -1. **只有平台能解密**:第三方无法获取平台私钥,无法解密数据 -2. **fingerprint 验证**:平台会验证硬件指纹,防止授权码被复用 -3. **非法授权日志**:平台会记录所有非法授权尝试 - -## 九、测试建议 - -1. **单元测试**: - - 测试 JSON 生成是否正确 - - 测试加密和解密是否匹配 - - 测试 Base64 编码和解码是否正确 - -2. **集成测试**: - - 使用真实平台公钥生成二维码 - - App 扫描二维码并提交到平台 - - 验证平台是否能正确解密和验证 - -3. **边界测试**: - - 测试超长的 licence 或 fingerprint - - 测试特殊字符的处理 - - 测试错误的公钥格式 - -## 十、参考实现 - -- **Python**:`cryptography` 库(RSA 加密)、`qrcode` 库(二维码生成) -- **Java/Kotlin**:JDK `javax.crypto`(RSA 加密)、ZXing 库(二维码生成) -- **C#**:`System.Security.Cryptography`(RSA 加密)、ZXing.Net 库(二维码生成) - -## 十一、联系支持 - -如有问题,请联系平台技术支持团队获取: -- 平台公钥(Base64 编码) -- 测试环境地址 -- 技术支持 -