// 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 }