百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

为你的python程序上锁:软件序列号生成器

bigegpt 2025-03-14 15:08 9 浏览

序列号

很多同学可能开发了非常多的程序了,并且进行了 exe 的打包,可是由于没有使用序列号,程序被无限复制,导致收益下降。

接下来我们来自己实现序列号的生成及使用,通过本文的学习,希望能够帮助到你!

本文适合 windows 系统,linux 系统原理相通,但代码有所不同。

安装库

pip install wmi
pip install pycryptodome

结构流程图

结构图

我们首先要通过 硬件信息、UUID和时间戳 来生成一个 协议文件,再通过非对称加密 RSA 生成公钥和私钥。

  1. 当我们给客户程序的时候,会附带一个 私钥
  2. 程序启动时会判断是否存在 协议文件 ,如果没有 协议文件 将会生成一个 序列号,客户需要把序列号发送给管理员
  3. 管理员获取 序列号,使用 公钥 进行加密生成 协议文件,将其发送给客户
  4. 客户将 协议文件 放在相应位置,程序使用 私钥 进行解密,与相应的 序列号 进行对比
  5. 协议文件 解密成功,通过 协议文件序列号 中的时间戳信息判断是否过期
  6. 当时间戳未过期,则运行程序,反之无法启动,提示 序列号过期

结构代码

1. 生成RSA公钥与私钥文件

from Crypto import Random
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
import os
import datetime
import base64

CURRENT_FOLDER_PATH = os.path.dirname(os.path.abspath(__file__))


def make_rsa_key(length=1024):
    """
    生成公钥和私钥
    :return:
    """
    # 伪随机数生成器
    random_gen = Random.new().read
    # 生成秘钥对实例对象:1024是秘钥的长度
    rsa = RSA.generate(length, random_gen)
    private_pem = rsa.exportKey()
    public_pem = rsa.publickey().exportKey()

    return private_pem, public_pem


def rsa_encrypt(pub_key, content, length=128):
    """
    rsa数据加密,单次加密串的长度最大为 (key_size/8)-11
    1024bit的证书用100, 2048bit的证书用 200
    :param pub_key:
    :param content:
    :param length:
    :return:
    """
    pub_key = RSA.importKey(pub_key.decode())
    cipher = PKCS1_v1_5.new(pub_key)
    content = content.encode()
    res = []
    for i in range(0, len(content), length):
        res.append(cipher.encrypt(content[i: i + length]))
    return base64.b64encode(base64.b64encode(b''.join(res)))


def rsa_decrypt(pri_key, encrypt_txt, length=128):
    """
    rsa信息解密
    1024bit的证书用128,2048bit证书用256位
    :param pri_key:
    :param encrypt_txt:
    :param length:
    :return:
    """
    try:
        encrypt_txt = base64.b64decode(encrypt_txt)
        encrypt_txt = base64.b64decode(encrypt_txt.decode())
        pri_obj = RSA.importKey(pri_key)
        pri_obj = PKCS1_v1_5.new(pri_obj)
        res = []
        for i in range(0, len(encrypt_txt), length):
            res.append(pri_obj.decrypt(encrypt_txt[i:i + length], ''))

        return b''.join(res)
    except Exception as e:
        return None


def rsa_file_generator(pri_path, pub_path, length=1024):
    """
    生成公私钥文件
    :param pri_path: 私钥文件地址
    :param pub_path: 公钥文件地址
    :return:
    """
    pri_key, pub_key = make_rsa_key(length)

    with open(pri_path, 'wb') as f:
        f.write(pri_key)
    with open(pub_path, 'wb') as f:
        f.write(pub_key)

    return pri_key, pub_key


def get_or_make_rsa(refresh=False):
    """
    生成或获取公私钥内容
    :return:
    """

    folder_path = os.path.join(CURRENT_FOLDER_PATH, 'pem')
    if not os.path.exists(folder_path):
        os.makedirs(folder_path)

    file_list = os.listdir(folder_path)
    now = datetime.datetime.now()
    now_str = now.strftime('%Y%m%d%H%M%S')

    new_pri_path = os.path.join(folder_path, f'{now_str}_private.pem')
    new_pub_path = os.path.join(folder_path, f'{now_str}_public.pem')

    # 公钥和私钥文件不存在
    if len(file_list) == 0:
        pri_key, pub_key = rsa_file_generator(
            new_pri_path,
            new_pub_path,
            1024
        )
    else:
        pri_path = ''
        pub_path = ''

        for file in file_list:
            if file.endswith('private.pem'):
                pri_path = os.path.join(folder_path, file)
            elif file.endswith('public.pem'):
                pub_path = os.path.join(folder_path, file)

        if not pri_path or not pub_path:
            pri_key, pub_key = rsa_file_generator(
                new_pri_path,
                new_pub_path,
                1024
            )
        else:
            # 手动更新私钥
            if refresh:
                pri_key, pub_key = rsa_file_generator(
                    new_pri_path,
                    new_pub_path,
                    1024
                )
                os.remove(pri_path)
                os.remove(pub_path)
            else:
                with open(pri_path, 'rb') as f:
                    pri_key = f.read()
                with open(pub_path, 'rb') as f:
                    pub_key = f.read()

    return pri_key, pub_key

使用 get_or_make_rsa() 方法即可获取公钥与私钥,程序会生成一个 pem 文件夹保存相应的公钥和私钥。

如果需要更新公钥与私钥,只要调用 get_or_make_rsa() 方法时,传入 refresh 的值为 True 即可。

CURRENT_FOLDER_PATH 全局变量保存的是当前程序的绝对路径,就算是打包后的 exe 也可以正确获取。

2. 序列号生成

import uuid
import wmi
import hashlib
from itertools import zip_longest


def get_license_txt():
    c = wmi.WMI()

    # 获取第一个硬盘的序列号
    disk_number = ''
    for physical_disk in c.Win32_DiskDrive():
        disk_number = physical_disk.SerialNumber.strip()
        break

    # 获取第一个CPU的序列号
    cpu_number = ''
    for cpu in c.Win32_Processor():
        cpu_number = cpu.ProcessorId.strip()
        break

    # 获取第一个BIOS的序列号
    bios_number = ''
    for bios in c.Win32_BIOS():
        bios_number = bios.SerialNumber.strip()
        break

    uid = get_a_guid()
    paired = zip_longest(disk_number, cpu_number, bios_number, uid, fillvalue='_')
    result = '|'.join([''.join(pair) for pair in paired])
    content = hashlib.md5(result.encode()).hexdigest().upper()
    return f'{content}=={uid}'

我们使用了一个相对复杂的方式将 硬盘、cpu、BIOS 的序列号与一个 UUID 进行组合,竟将其进行 md5 加密,最后将这个 加密结果UUID 明文组合在一起作为一个 完整的软件序列号

现在,客户将在程序启动时拿到这个 软件序列号 ,再将这个软件序列号发送给管理员进行加密即可。

加密方法将使用之前的 rsa_encrypt() 方法。

3. 加密软件序列号

def make_license_key_file(license_txt='', days=365):
    '''
    创建协议文件
    :param license_txt: 软件序列号
    :param days: 有效天数
    :return: 
    '''
    pri_key, pub_key = get_or_make_rsa()

    s_list = license_txt.split('==')
    if len(s_list) != 2:
        return None

    uid = s_list[1]

    timestamp = int(datetime.datetime.now().timestamp()) + days * 86400
    new_license_text = f'{license_txt}=={timestamp}'
    res = rsa_encrypt(pub_key, new_license_text).decode()

    folder_path = os.path.join(CURRENT_FOLDER_PATH, 'key')
    if not os.path.exists(folder_path):
        os.makedirs(folder_path)

    file_path = os.path.join(folder_path, f'{uid}.key')
    with open(file_path, 'wb') as f:
        f.write(res.encode())

    return file_path

以当前时间戳为基准添加有效天数,然后将客户发送过来的序列号再进行一次组合,最后通过 公钥 进行加密,生成一个 协议文件 发送给客户。

4. 验证协议文件

def auth_license(key_file_path='', pem_file_path=''):
    c = wmi.WMI()

    disk_number = ''
    for physical_disk in c.Win32_DiskDrive():
        disk_number = physical_disk.SerialNumber.strip()
        break

    cpu_number = ''
    for cpu in c.Win32_Processor():
        cpu_number = cpu.ProcessorId.strip()
        break

    bios_number = ''
    for bios in c.Win32_BIOS():
        bios_number = bios.SerialNumber.strip()
        break

    # 协议文件不存在,无法通过
    if not os.path.exists(key_file_path):
        return False
    # 私钥文件不存在,无法通过
    if not os.path.exists(pem_file_path):
        return False

    # 读取协议文件内容
    with open(key_file_path, 'rb') as f:
        key_res = f.read().decode()

    # 读取私钥内容
    with open(pem_file_path, 'rb') as f:
        pem_res = f.read().decode()

    res = rsa_decrypt(pem_res, key_res).decode()
    # 解密失败,无法通过
    if not res:
        return False

    s_list = res.split('==')

    # 没有两个等号的内容,无法通过
    if len(s_list) != 3:
        return False

    uid = s_list[1]
    paired = zip_longest(disk_number, cpu_number, bios_number, uid, fillvalue='_')
    result = '|'.join([''.join(pair) for pair in paired])
    content = hashlib.md5(result.encode()).hexdigest().upper()

    # 序列号不一致,无法通过
    if content != s_list[0]:
        return False

    try:
        timestamp = int(s_list[2])
    except Exception as e:
        # 无法变为时间戳,无法通过
        return False

    now_timestamp = int(datetime.datetime.now().timestamp())

    # 在有效时间内,通过
    if now_timestamp <= timestamp:
        return True

    return False

这个验证过程其实就是再次进行一次 序列号 组合,来判断是否与 协议文件 一致,其后还需判断是否在有效期内。

结尾

我相信如果认真看完文章的朋友已经可以实现自己的序列号生成器了,不过在此之前我需要申明这个文章的代码还不是完整版,这里提几个优化点:

  1. 定时验证:定时判断是否超过有效期,防止客户程序未关闭,就算超过有效期还能继续使用
  2. 打包为加密模块:当前代码为明文代码,由于 python 为解释器语言,如果不加密打包,很容易被破解规则
  3. 添加可视化窗口:为了方便使用,可以将程序设计为可视化窗口模式,最简单的方式使用 TK 创建

当然,如果你不想自己写,推荐可以直接使用 pyarmor ,这也是国人开发的一个加密库,包含加密、有效期、许可证。

如果这篇文章对你有帮助,点个赞让我知道哦!

相关推荐

【机器学习】数据挖掘神器LightGBM详解(附代码)

来源:机器学习初学者本文约11000字,建议阅读20分钟本文为你介绍数据挖掘神器LightGBM。LightGBM是微软开发的boosting集成模型,和XGBoost一样是对GBDT...

3分钟,用DeepSeek全自动生成语音计算器,还带括号表达式!

最近,大家慢慢了解到了DeepSeek的强大功能,特别是它在编程领域也同样强大。编程零基础小白,一行代码不用写,也能全自动生成一个完整的、可运行的软件来!很多程序员一直不相信小白不写代码也能编软件!下...

python学习笔记 3.表达式

在Python中,表达式是由值、变量和运算符组成的组合。以下是一些常见的Python表达式:算术表达式:由数值和算术运算符组成的表达式,如加减乘除等。例如:5+3、7*2、10/3等。字符...

5.7 VS 8.x,为什么用户不升级MySql

一般来说为了更好的功能和性能,都需要将软件升级到最新的版本,然而在开源软件中,由于一些开发商变化或其他的问题(开源授权变化),致使人们不愿使用最新的版本,一个最典型的问题就是CentOS操作系统。还有...

大厂高频:讲一下MySQL主从复制

大家经常听说主从复制,那么主从复制的意义?能解决的问题有哪些?主从复制能解决的问题就是在我们平时开发的程序中操作数据库的时候,大多数的情况查询的操作大大超过了写的操作,也就说对数据库读取数据的压力比较...

MYSQL数据库的五大安全防护措施

以技术为基础的企业里最有价值的资产莫过于是客户或者其数据库中的产品信息了。因此,在这样的企业中,保证数据库免受外界攻击是数据库管理的重要环节。很多数据库管理员并没有实施什么数据库保护措施,只是因为觉得...

docker安装mysql

准备工作已安装Docker环境(官方安装文档)终端/命令行工具(Linux/macOS/WSL)步骤1:拉取MySQL镜像打开终端执行以下命令,拉取官方MySQL镜像(默认最新版本):d...

Zabbix监控系统系列之六:监控 mysql

zabbix监控mysql1、监控规划在创建监控项之前要尽量考虑清楚要监控什么,怎么监控,监控数据如何存储,监控数据如何展现,如何处理报警等。要进行监控的系统规划需要对Zabbix很了解,这里只是...

详解MySQL的配置文件及优化

#头条创作挑战赛#在Windows系统中,MySQL服务器启动时最先读取的是my.ini这个配置文件。在Linux系统中,配置文件为my.cnf,其路径一般为/etc/my.cnf或/etc/mysq...

Mysql 几个批处理执行脚本

学习mysql过程中,需要创建测试数据,并让多人每人一个数据库连接并进行作业检查。整合部分批处理创建数据批量创建数据库DELIMITER$CREATEPROCEDURECreateDatab...

MySQL学到什么程度?才有可以在简历上写精通

前言如今互联网行业用的最多就是MySQL,然而对于高级Web面试者,尤其对于寻找30k下工作的求职者,很多MySQL相关知识点基本都会涉及,如果面试中,你的相关知识答的模糊和不切要点,基...

mysql 主、从服务器配置“Slave_IO_Running: Connecting” 问题分析

#在进行mysql主、从服务器配置时,”SHOWSLAVESTATUS;“查看从库状态Slave_IO_Runing,出现错误:“Slave_IO_Running:Connectin...

MYSQL数据同步

java开发工程师在实际的开发经常会需要实现两台不同机器上的MySQL数据库的数据同步,要解决这个问题不难,无非就是mysql数据库的数据同步问题。但要看你是一次性的数据同步需求,还是定时数据同步,亦...

「MySQL 8」MySQL 5.7都即将停只维护了,是时候学习一波MySQL 8了

MySQL8新特性选择MySQL8的背景:MySQL5.6已经停止版本更新了,对于MySQL5.7版本,其将于2023年10月31日停止支持。后续官方将不再进行后续的代码维护。另外,...

Prometheus监控mysql

通过Prometheus监控Mysql,我们需要在Mysql端安装一个mysql-exporter,然后Prometheus通过mysql-exporter暴露的端口抓取数据。1.安装一个MYSQL配...