You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

143 lines
5.0 KiB

// Package license 授权码编解码。
//
// 授权码固定 10 字节(20 个 hex 字符),分 5 段:段0 是随机种子,段1-4 是被种子
// 派生的混淆序列异或过的数据。码长与混淆结构在 v1/v2 之间保持不变。
//
// v1 把序号切成 批次(8位) + 批内序号(20位),批次每次生产 +1、上限 255,用尽即废。
// v2 取消批次字段,序号按产品累加、占满 32 位;批次改由独立的批次表承载。
//
// 逻辑布局(异或前。密文经混淆后不可肉眼分辨版本):
//
// hex位 1-4 5-8 9 10-12 13-16 17 18-20
// v1 seed productid probatch高4位 probatch低4位 随机4位 随机 随机
// +序号高8位 +序号低12位
// v2 seed productid 0xF(标记) 序号[31:20] 序号[19:4] 序号[3:0] 随机
//
// 第 9 个 hex 字符是版本判别位:v1 存 probatch>>4,历史 probatch 恒小于
// V1ProbatchLimit,故不可能等于 v2 标记。productid 位于第 5-8 位,两版一致,
// 因此提取 productid 无需先判版本(见 ProductID)。
package license
import (
cryptorand "crypto/rand"
"errors"
"fmt"
"math/rand"
"strconv"
)
const (
totalSegments = 5 // 5段,每段4字符 = 20字符 = 10字节
segmentLength = 4
// v2Tag v2 格式标记,占第 9 个 hex 字符
v2Tag = 0xF
// V1ProbatchLimit v1 批次号上界(不含)。历史 probatch 必须全部小于该值,
// 否则越界的 v1 码会在判别位上撞上 v2 标记而被误判为 v2。
// 迁移脚本必须先校验 MAX(probatch) < V1ProbatchLimit 再继续。
V1ProbatchLimit = 0xF0
)
// Version 授权码格式版本
type Version uint8
const (
V1 Version = 1 // 批次(8位) + 批内序号(20位)
V2 Version = 2 // 产品级全局序号(32位)
)
// Info 授权码解析结果
type Info struct {
ProductID uint16 // 产品ID
Version Version // 格式版本
Serial uint32 // v2:产品级全局序号(32位) v1:批内序号(20位)
Batch byte // 批次号,仅 v1 有效
}
// decode 拆出 5 个数据段,并用种子重建混淆序列。v1/v2 共用,不涉及版本判别。
func decode(code string) (segments, confusion []int, err error) {
expectedLength := totalSegments * segmentLength
if len(code) != expectedLength {
return nil, nil, fmt.Errorf("授权码长度错误,期望%d字符,实际%d字符", expectedLength, len(code))
}
segments = make([]int, totalSegments)
for i := 0; i < totalSegments; i++ {
value, perr := strconv.ParseUint(code[i*segmentLength:(i+1)*segmentLength], 16, 32)
if perr != nil {
return nil, nil, fmt.Errorf("第%d段包含无效字符", i+1)
}
segments[i] = int(value)
}
rng := rand.New(rand.NewSource(int64(segments[0])))
confusion = make([]int, totalSegments-1)
for i := range confusion {
confusion[i] = rng.Intn(65536)
}
return segments, confusion, nil
}
// GenerateV2 生成 v2 授权码。
// serial 为产品级全局序号,占满 uint32,按产品累加,不随批次重置。
func GenerateV2(productid uint16, serial uint32) (string, error) {
seedBytes := make([]byte, 2)
if _, err := cryptorand.Read(seedBytes); err != nil {
return "", fmt.Errorf("生成随机种子失败: %v", err)
}
seed := int(seedBytes[0])<<8 | int(seedBytes[1])
rng := rand.New(rand.NewSource(int64(seed)))
confusion := make([]int, totalSegments-1)
for i := range confusion {
confusion[i] = rng.Intn(65536)
}
confusion[0] ^= int(productid)
confusion[1] ^= v2Tag<<12 | int(serial>>20) // 标记 + 序号[31:20]
confusion[2] ^= int(serial>>4) & 0xFFFF // 序号[19:4]
confusion[3] ^= int(serial&0xF)<<12 | rng.Intn(1<<12) // 序号[3:0] + 12位随机填充
code := fmt.Sprintf("%04X", seed)
for _, c := range confusion {
code += fmt.Sprintf("%04X", c)
}
return code, nil
}
// Parse 解析授权码,自动识别 v1/v2 格式
func Parse(code string) (*Info, error) {
segments, confusion, err := decode(code)
if err != nil {
return nil, err
}
info := &Info{ProductID: uint16((segments[1] ^ confusion[0]) & 0xFFFF)}
data2 := segments[2] ^ confusion[1]
data3 := segments[3] ^ confusion[2]
if (data2>>12)&0xF == v2Tag {
data4 := segments[4] ^ confusion[3]
info.Version = V2
info.Serial = uint32(data2&0xFFF)<<20 | uint32(data3&0xFFFF)<<4 | uint32((data4>>12)&0xF)
return info, nil
}
info.Version = V1
info.Batch = byte((data2 >> 8) & 0xFF)
info.Serial = uint32(data2&0xFF)<<12 | uint32(data3&0xFFF)
if info.Batch < 1 {
return nil, errors.New("批次数据异常")
}
return info, nil
}
// ProductID 提取授权码中的产品ID。
// productid 在 v1/v2 中位置相同,故不做版本判别、也不校验批次,
// 新旧码混存期间恒可用(调用方通常只需要它来定位 license 分表)。
func ProductID(code string) (uint16, error) {
segments, confusion, err := decode(code)
if err != nil {
return 0, err
}
return uint16((segments[1] ^ confusion[0]) & 0xFFFF), nil
}