构建安全的离线软件授权:实践指南(三)

系列文章:

  1. 构建安全的离线软件授权:实践指南(一)

  2. 构建安全的离线软件授权:实践指南(二)

接上篇文章,在这篇文章中我将提供授权码生成与验证的示例代码,实现离线软件授权码的生成与验证机制。

这个代码包将涵盖以下关键技术点:

  • RSA 加密算法的使用;

  • 获取硬件机器码信息;

  • 获取当前网络时间;

  • 授权码签名与验证。

咱们话不多说,直接进入主题。

1. 客户端提供机器码

使用下面的方法来获取机器码,并将机器码信息发送至服务器以生成授权码。

示例代码如下:

// GetHashedMachineID 获取哈希后的机器标识
func GetHashedMachineID() (string, error) {
 // 根据操作系统类型选择合适的命令来获取机器标识
 var cmd *exec.Cmd
 switch runtime.GOOS {
 case "windows":
  cmd = exec.Command("wmic", "csproduct", "get", "UUID")
 case "darwin":
  cmd = exec.Command("system_profiler", "SPHardwareDataType")
 default:
  cmd = exec.Command("cat", "/etc/machine-id")
 }

 // 执行命令并捕获输出
 output, err := cmd.Output()
 if err != nil {
  return "", fmt.Errorf("无法获取机器标识: %v", err)
 }

 // 去除多余的空白字符
 machineID := strings.TrimSpace(string(output))

 // 针对 Windows 系统特殊处理,提取 UUID 部分
 if runtime.GOOS == "windows" {
  lines := strings.Split(machineID, "\n")
  if len(lines) > 1 {
   machineID = strings.TrimSpace(lines[1])
  }
 }

 // 使用 SHA-256 哈希算法对机器标识进行哈希处理
 hasher := sha256.New()
 hasher.Write([]byte(machineID))
 hashedID := hex.EncodeToString(hasher.Sum(nil))

 return hashedID, nil
}

使用示例:

func main() {
 hardwareID, err := softwareverify.GetHashedMachineID()
 if err != nil {
  fmt.Println("获取机器码失败:", err)
  return
 }

 fmt.Println("机器码:", hardwareID)
}

输出:

机器码: f840c3221e54b90e305dc6a03ce3a1716a53c8f1ca98169dcff3917ed5600c79

2. 服务端生成授权码

软件验证包(softwareverify) 中的 server.go:

package softwareverify

import (
 "crypto"
 "crypto/rand"
 "crypto/rsa"
 "crypto/sha256"
 "crypto/x509"
 "encoding/base64"
 "encoding/pem"
 "fmt"
 "time"
)

// GenerateRSAKeys 生成 RSA 密钥对
func GenerateRSAKeys() (*rsa.PrivateKey, *rsa.PublicKey, error) {
 privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
 if err != nil {
  return nil, nil, err
 }
 return privateKey, &privateKey.PublicKey, nil
}

// GenerateLicense 生成授权码
func GenerateLicense(privateKey *rsa.PrivateKey, hardwareID string, validityDays int) (string, error) {
 expiration := time.Now().AddDate(0, 0, validityDays).Unix()
 licenseData := fmt.Sprintf("%s:%d", hardwareID, expiration)

 // 使用私钥签名授权数据
 hashed := sha256.Sum256([]byte(licenseData))
 signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
 if err != nil {
  return "", err
 }

 // 编码授权码(授权数据 + 签名)
 license := fmt.Sprintf("%s:%s", licenseData, base64.StdEncoding.EncodeToString(signature))
 return license, nil
}

// ExportPublicKey 导出公钥
func ExportPublicKey(pubKey *rsa.PublicKey) (string, error) {
 pubASN1, err := x509.MarshalPKIXPublicKey(pubKey)
 if err != nil {
  return "", err
 }
 pubPEM := pem.EncodeToMemory(&pem.Block{
  Type:  "RSA PUBLIC KEY",
  Bytes: pubASN1,
 })
 return string(pubPEM), nil
}

使用示例:

func main() {
 // 硬件机器码示例(客户提供的硬件机器码)
 hardwareID := "f840c3221e54b90e305dc6a03ce3a1716a53c8f1ca98169dcff3917ed5600c79"

 // 授权天数
 validityDays := 30

 // 生成 RSA 密钥对
 privateKey, publicKey, err := softwareverify.GenerateRSAKeys()
 if err != nil {
  fmt.Println("生成密钥失败:", err)
  return
 }

 // 生成授权码
 license, err := softwareverify.GenerateLicense(privateKey, hardwareID, validityDays)
 if err != nil {
  fmt.Println("生成授权码失败:", err)
  return
 }

 fmt.Println("授权码:")
 fmt.Println(license)

 // 导出并保存公钥
 pubKeyStr, _ := softwareverify.ExportPublicKey(publicKey)
 fmt.Println("公钥:")
 fmt.Println(pubKeyStr)
}

输出:

授权码:
f840c3221e54b90e305dc6a03ce3a1716a53c8f1ca98169dcff3917ed5600c79:1734582971:HvUBFbL8bacUhuVYYZ9N/BDXQ5ns15Q+t/MpwoRcZLaup7ltAxQ6NrhCBU1C5JfmmMb0MgYNk2NYogfLd7GdcFAx/eLjxx+SpAQ4MfG5rb3fHDWhHnKhyt37cn8RbvhqlOrmCwdv+28oaG3fpd2aGKt3BC/wx9kg/lZjiH1E8Fl4IhQramP/sS0qRxWWhy14uLvkFz6xBfSH8LYL7Lj6qT2erDSElnwpnG3VaLGIsJgZFzDe6q+oh/PfTGvY9NADQy/rffNbHmObdc0zOEsDgDpSf9fGS+IV2jhA1jbnoRfOD3R+qU9+7x+g4vY3bKulWi/BD9DPiWlA/uyzjB3eFQ==

公钥:
-----BEGIN RSA PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAngACnt+cr3Yijlgw1wTa
6LkM+7dXQu/9a+PckTSZtN2QZBuxvu6sLempuRR9BbB20VF4L86pg8KuvpwXliJf
mcdpVfn2aucFg0/JtvNdRoMembwoP2sFtYoCVbgHWdhlI8Z5k6ovbg06XGCy2ZbR
9epINx1IaFdqnrdGW2vJ4jBXdOlln5Tc9zojSTkSK59ONpdvpQCDLaAFDm9/Hu/E
OVenPWZ86THfiiRFpzu6qxQCOadNeSbK1d3PE6YSnUEyxk63Ny/F+6hXv2IT3JJw
c9/wkEikr21u9dCSO5PT80jCC8WFS7xz1Em4KczsSfAxoMtqxDX6pPuJzfcLoO7B
8wIDAQAB
-----END RSA PUBLIC KEY-----

3. 客户端验证授权码

在客户端,咱们使用服务器提供的公钥对授权码进行验证。

提取出授权数据和签名,然后通过公钥解密验证签名的有效性。

软件验证包(softwareverify) 中的 client.go:

package softwareverify

import (
 "crypto"
 "crypto/rsa"
 "crypto/sha256"
 "crypto/x509"
 "encoding/base64"
 "encoding/hex"
 "encoding/pem"
 "errors"
 "fmt"
 "net/http"
 "os/exec"
 "runtime"
 "strconv"
 "strings"
 "time"
)

// 定义自定义错误类型
var (
 ErrInvalidPublicKey     = errors.New("无效的公钥")
 ErrInvalidLicenseFormat = errors.New("授权码格式错误")
 ErrInvalidSignature     = errors.New("授权签名验证失败")
 ErrInvalidTimestamp     = errors.New("授权时间验证失败")
 ErrExpiredLicense       = errors.New("授权已过期")
 ErrHardwareIDMismatch   = errors.New("硬件机器码不匹配")
)

// ImportPublicKey 导入公钥
func ImportPublicKey(pubPEM string) (*rsa.PublicKey, error) {
 block, _ := pem.Decode([]byte(pubPEM))
 if block == nil || block.Type != "RSA PUBLIC KEY" {
  return nil, ErrInvalidPublicKey
 }
 pub, err := x509.ParsePKIXPublicKey(block.Bytes)
 if err != nil {
  return nil, err
 }
 return pub.(*rsa.PublicKey), nil
}

// VerifyLicense 验证授权码
func VerifyLicense(pubKey *rsa.PublicKey, license, hardwareID string) (bool, error) {
 parts := strings.Split(license, ":")
 if len(parts) != 3 {
  return false, ErrInvalidLicenseFormat
 }

 // 提取授权数据和签名
 licenseData := parts[0] + ":" + parts[1]

 // 解码签名
 signature, err := base64.StdEncoding.DecodeString(parts[2])
 if err != nil {
  return false, fmt.Errorf("base64 decode failed: %v", err)
 }

 // 获取网络时间
 currentTimeUnix, err := GetNetworkTime()
 if err != nil {
  return false, ErrInvalidTimestamp
 }

 // 校验硬件 ID 和有效期
 expiration, err := strconv.ParseInt(parts[1], 10, 64)
 if err != nil || currentTimeUnix > expiration {
  return false, ErrExpiredLicense
 }
 if parts[0] != hardwareID {
  return false, ErrHardwareIDMismatch
 }

 // 验证签名
 hashed := sha256.Sum256([]byte(licenseData))
 err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hashed[:], signature)
 if err != nil {
  return false, ErrInvalidSignature
 }
 return true, nil
}

// GetNetworkTime 获取网络时间
func GetNetworkTime() (int64, error) {
 maxRetries := 3
 retryDelay := 500 * time.Millisecond

 client := &http.Client{Timeout: 5 * time.Second}
 var dateHeader string
 var resp *http.Response
 var err error

 // 尝试从网络获取时间
 for i := 0; i < 3; i++ {
  req, err := http.NewRequest("GET", "http://www.baidu.com", nil)
  if err != nil {
   return 0, errors.New("创建请求失败: " + err.Error())
  }

  resp, err = client.Do(req)
  if err != nil {
   if i == maxRetries-1 {
    return 0, errors.New("请求失败(重试 " + fmt.Sprintf("%d/%d", i+1, maxRetries) + "): " + err.Error())
   }
   time.Sleep(retryDelay) // 等待一段时间后重试
   continue
  }

  // 获取响应头中的日期
  dateHeader = resp.Header.Get("Date")
  if dateHeader != "" {
   break
  }
  if i == maxRetries-1 {
   return 0, errors.New("未找到 Date 头部信息,尝试使用本地时间")
  }
 }

 // 确保在函数返回前关闭响应体
 defer func() {
  if resp != nil && resp.Body != nil {
   resp.Body.Close()
  }
 }()

 // 如果网络时间获取失败,则返回本地时间
 if dateHeader == "" {
  return 0, errors.New("网络时间不可用")
 }

 // 解析日期为时间戳
 t, err := time.Parse(time.RFC1123, dateHeader)
 if err != nil {
  return 0, errors.New("日期解析失败: " + err.Error())
 }

 // 返回东八区(北京时间)时间戳
 beijingTime := t.In(time.FixedZone("CST", 8*3600))
 return beijingTime.Unix(), nil
}

// GetHashedMachineID 获取哈希后的机器标识
func GetHashedMachineID() (string, error) {
 // 根据操作系统类型选择合适的命令来获取机器标识
 var cmd *exec.Cmd
 switch runtime.GOOS {
 case "windows":
  cmd = exec.Command("wmic", "csproduct", "get", "UUID")
 case "darwin":
  cmd = exec.Command("system_profiler", "SPHardwareDataType")
 default:
  cmd = exec.Command("cat", "/etc/machine-id")
 }

 // 执行命令并捕获输出
 output, err := cmd.Output()
 if err != nil {
  return "", fmt.Errorf("无法获取机器标识: %v", err)
 }

 // 去除多余的空白字符
 machineID := strings.TrimSpace(string(output))

 // 针对 Windows 系统特殊处理,提取 UUID 部分
 if runtime.GOOS == "windows" {
  lines := strings.Split(machineID, "\n")
  if len(lines) > 1 {
   machineID = strings.TrimSpace(lines[1])
  }
 }

 // 使用 SHA-256 哈希算法对机器标识进行哈希处理
 hasher := sha256.New()
 hasher.Write([]byte(machineID))
 hashedID := hex.EncodeToString(hasher.Sum(nil))

 return hashedID, nil
}

使用示例:

func main() {
 // 示例公钥(应从服务端获得)
 pubPEM := `
-----BEGIN RSA PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAngACnt+cr3Yijlgw1wTa
6LkM+7dXQu/9a+PckTSZtN2QZBuxvu6sLempuRR9BbB20VF4L86pg8KuvpwXliJf
mcdpVfn2aucFg0/JtvNdRoMembwoP2sFtYoCVbgHWdhlI8Z5k6ovbg06XGCy2ZbR
9epINx1IaFdqnrdGW2vJ4jBXdOlln5Tc9zojSTkSK59ONpdvpQCDLaAFDm9/Hu/E
OVenPWZ86THfiiRFpzu6qxQCOadNeSbK1d3PE6YSnUEyxk63Ny/F+6hXv2IT3JJw
c9/wkEikr21u9dCSO5PT80jCC8WFS7xz1Em4KczsSfAxoMtqxDX6pPuJzfcLoO7B
8wIDAQAB
-----END RSA PUBLIC KEY-----
`

 license := "f840c3221e54b90e305dc6a03ce3a1716a53c8f1ca98169dcff3917ed5600c79:1734582971:HvUBFbL8bacUhuVYYZ9N/BDXQ5ns15Q+t/MpwoRcZLaup7ltAxQ6NrhCBU1C5JfmmMb0MgYNk2NYogfLd7GdcFAx/eLjxx+SpAQ4MfG5rb3fHDWhHnKhyt37cn8RbvhqlOrmCwdv+28oaG3fpd2aGKt3BC/wx9kg/lZjiH1E8Fl4IhQramP/sS0qRxWWhy14uLvkFz6xBfSH8LYL7Lj6qT2erDSElnwpnG3VaLGIsJgZFzDe6q+oh/PfTGvY9NADQy/rffNbHmObdc0zOEsDgDpSf9fGS+IV2jhA1jbnoRfOD3R+qU9+7x+g4vY3bKulWi/BD9DPiWlA/uyzjB3eFQ=="

 // 导入公钥
 publicKey, err := softwareverify.ImportPublicKey(pubPEM)
 if err != nil {
  fmt.Println("导入公钥失败:", err)
  return
 }

 hardwareID, err := softwareverify.GetHashedMachineID()
 if err != nil {
  fmt.Println("获取机器码失败:", err)
  return
 }

 // 验证授权码
 valid, err := softwareverify.VerifyLicense(publicKey, license, hardwareID)
 if err != nil {
  fmt.Println("授权验证失败:", err)
 } else if valid {
  fmt.Println("授权验证成功,授权有效")
 }
}

输出:

授权验证成功,授权有效

到这儿,客户端就验证成功了。

小结

通过代码示例详细的阐述了授权码的生成和验证流程。

具体步骤如下:

  1. 客户端首先获取机器码。

  2. 然后将该机器码发送至服务器。

  3. 服务器接收到机器码后,生成相应的授权码并返回给客户端。

  4. 客户端随后对收到的授权码进行验证。

  5. 如果验证通过,则输出授权码验证成功。

好了,这篇就到这,在下一篇文章中,我将介绍如何使用 Fyne 库编写一个生成授权码的软件,欢迎关注。

可点击下方👇 关注公众号

添加作者微信 👇 技术沟通交流

b0ef0760374f04771d6f3794d4532d16.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值