package utils import ( "crypto/ed25519" "encoding/base64" "encoding/hex" "encoding/json" "errors" "strings" "time" ) const ( // OfflineLicensePrefix 离线序列号前缀(与在线 LIC- 区分) // Logan.Li OfflineLicensePrefix = "LICOFF-1." // OfflineMaxDurationDays 离线证最长有效期(禁止永久离线证) // Logan.Li OfflineMaxDurationDays = 365 ) // OfflineLicenseClaims 离线许可证声明(自包含,桌面本地验签) // Logan.Li type OfflineLicenseClaims struct { V int `json:"v"` ProductCode string `json:"pid"` DeviceID string `json:"did"` Email string `json:"email,omitempty"` PlanType string `json:"pt"` IssuedAt int64 `json:"iat"` ExpiresAt int64 `json:"exp"` MaxDevices int `json:"md"` CustomerName string `json:"cn,omitempty"` } // OfflineKeyPair 离线签发密钥对 // Logan.Li type OfflineKeyPair struct { Private ed25519.PrivateKey Public ed25519.PublicKey } // DeriveOfflineKeyPair 从 32 字节 seed(必须 64 位 hex)派生 Ed25519 密钥对 // Logan.Li func DeriveOfflineKeyPair(seedHex string) (*OfflineKeyPair, error) { seedHex = strings.TrimSpace(seedHex) if seedHex == "" { return nil, errors.New("离线签发 seed 为空") } raw, err := hex.DecodeString(seedHex) if err != nil || len(raw) != ed25519.SeedSize { return nil, errors.New("OFFLINE_LICENSE_SEED 必须是 64 位 hex(32 字节),请用 openssl rand -hex 32 生成") } priv := ed25519.NewKeyFromSeed(raw) pub := priv.Public().(ed25519.PublicKey) return &OfflineKeyPair{Private: priv, Public: pub}, nil } // OfflinePublicKeyHex 返回公钥 hex(供桌面端嵌入) // Logan.Li func (k *OfflineKeyPair) OfflinePublicKeyHex() string { return hex.EncodeToString(k.Public) } // SignHexString 对任意字符串签名,返回 hex 编码的 Ed25519 签名(用于在线验证响应防篡改) // Logan.Li func (k *OfflineKeyPair) SignHexString(msg string) string { if k == nil || len(k.Private) == 0 { return "" } return hex.EncodeToString(ed25519.Sign(k.Private, []byte(msg))) } // VerifyHexString 校验 SignHexString 产生的 hex 签名(供测试与桌面端对照实现) // Logan.Li func (k *OfflineKeyPair) VerifyHexString(msg, sigHex string) bool { if k == nil || len(k.Public) == 0 { return false } sig, err := hex.DecodeString(strings.TrimSpace(sigHex)) if err != nil || len(sig) != ed25519.SignatureSize { return false } return ed25519.Verify(k.Public, []byte(msg), sig) } // IsOfflineLicenseKey 判断是否为离线序列号 // Logan.Li func IsOfflineLicenseKey(key string) bool { return strings.HasPrefix(strings.TrimSpace(key), "LICOFF-") } // SignOfflineLicense 签发离线序列号:LICOFF-1.. // Logan.Li func SignOfflineLicense(kp *OfflineKeyPair, claims OfflineLicenseClaims) (string, error) { if kp == nil || len(kp.Private) == 0 { return "", errors.New("离线签发密钥未配置") } if err := validateOfflineClaims(claims, true); err != nil { return "", err } claims.V = 1 payload, err := json.Marshal(claims) if err != nil { return "", err } payloadB64 := base64.RawURLEncoding.EncodeToString(payload) sig := ed25519.Sign(kp.Private, []byte(payloadB64)) sigB64 := base64.RawURLEncoding.EncodeToString(sig) return OfflineLicensePrefix + payloadB64 + "." + sigB64, nil } // VerifyOfflineLicense 验签并解析离线序列号(不校验设备与过期,由调用方决定) // Logan.Li func VerifyOfflineLicense(publicKey ed25519.PublicKey, serial string) (*OfflineLicenseClaims, error) { serial = strings.TrimSpace(serial) if !strings.HasPrefix(serial, OfflineLicensePrefix) { return nil, errors.New("不是合法的离线序列号(需 LICOFF-1. 前缀)") } body := strings.TrimPrefix(serial, OfflineLicensePrefix) parts := strings.Split(body, ".") if len(parts) != 2 { return nil, errors.New("离线序列号格式错误") } payloadB64, sigB64 := parts[0], parts[1] sig, err := base64.RawURLEncoding.DecodeString(sigB64) if err != nil { return nil, errors.New("离线序列号签名无法解码") } if len(publicKey) != ed25519.PublicKeySize { return nil, errors.New("离线验签公钥无效") } if !ed25519.Verify(publicKey, []byte(payloadB64), sig) { return nil, errors.New("离线序列号签名无效") } payload, err := base64.RawURLEncoding.DecodeString(payloadB64) if err != nil { return nil, errors.New("离线序列号载荷无法解码") } var claims OfflineLicenseClaims if err := json.Unmarshal(payload, &claims); err != nil { return nil, errors.New("离线序列号载荷无法解析") } if err := validateOfflineClaims(claims, false); err != nil { return nil, err } return &claims, nil } // ParseOfflinePublicKeyHex 解析公钥 hex // Logan.Li func ParseOfflinePublicKeyHex(hexKey string) (ed25519.PublicKey, error) { raw, err := hex.DecodeString(strings.TrimSpace(hexKey)) if err != nil { return nil, err } if len(raw) != ed25519.PublicKeySize { return nil, errors.New("公钥长度错误") } return ed25519.PublicKey(raw), nil } func validateOfflineClaims(c OfflineLicenseClaims, issuing bool) error { if strings.TrimSpace(c.DeviceID) == "" { return errors.New("device_id 不能为空") } pt := strings.TrimSpace(c.PlanType) if pt != "trial" && pt != "yearly" { return errors.New("离线证仅允许 trial / yearly,禁止永久") } if c.ExpiresAt <= 0 || c.IssuedAt <= 0 { return errors.New("签发/到期时间无效") } if c.ExpiresAt <= c.IssuedAt { return errors.New("到期时间必须晚于签发时间") } maxSpan := int64(OfflineMaxDurationDays) * 24 * 3600 if c.ExpiresAt-c.IssuedAt > maxSpan+3600 { // 容许 1h 时钟 return errors.New("离线证有效期不得超过 365 天") } if issuing { now := time.Now().Unix() if c.ExpiresAt <= now { return errors.New("到期时间已过") } if c.ExpiresAt-now > maxSpan+3600 { return errors.New("离线证有效期不得超过 365 天") } } if c.MaxDevices <= 0 { c.MaxDevices = 1 } if c.MaxDevices > 1 { // 离线证绑定单设备,强制 1 // Logan.Li return errors.New("离线证仅支持 1 台设备") } if strings.TrimSpace(c.ProductCode) == "" { return errors.New("product_code 不能为空") } return nil }