简介:《C#加密与解密注册码DEMO程序》是一份面向.NET开发者和软件版权保护学习者的完整示例,演示了通过读取CPU编码与硬盘编码生成唯一机器码,再经MD5加密后写入注册表和本地文件,以实现软件授权控制。程序分为用户程序加密模块与后台权限注册解密模块,并设置限定日期和硬件绑定,可有效应对修改电脑时间或更换电脑等破解尝试。
资源共104个文件,压缩包2.11MB,核心为21个cs源文件,另含6个exe可执行程序、若干dll动态库、config/resx配置资源文件以及sln/csproj工程文件,便于直接打开工程运行和阅读源码。目前已有413人浏览学习,适合正在学习C#加密解密、需要注册码方案或做软件授权方向毕业设计的开发者。包内包含“生成注册码”和“加密”两个工程,可对照研究系统信息采集、MD5摘要、注册表读写及防篡改校验等关键实现,是一份结构清晰、可直接调试的实践参考。
1. 一套 C# 加密与解密注册码 Demo 程序,解决的是用户敢不敢复制你的 exe
一个 C# 加密与解密注册码 Demo 程序,解决的是绝大多数商业工具都要面对的问题:分发的 exe 怎么防止被复制到别的机器上继续用。尤其做 c# 上位机和工业现场设备的,程序编译出来交给客户,客户拿 U 盘拷贝一份就能换台电脑跑,授权形同虚设。这个标题里的“加密与解密”,说的不是 HTTPS 那类传输加密,而是把授权信息本身做成密文,再在程序启动时解开校验。
这篇不是泛泛介绍 Aes 类的用法,而是给你一套能落地的最小方案:机器指纹怎么取、授权载荷怎么设计、注册码怎么生成、客户端怎么校验、以及上线后最常踩的坑在哪。适合谁:正在给 WinForms、WPF 或工业上位机加授权逻辑的 C# 开发者,想先用最少代码把链路跑通,再按项目需要升级算法或加壳。
2. 加密选型与算法分工:AES、RSA 在注册码里各咬哪一环
2.1 注册码不是“密码”,真正要保护的是一个授权载荷
很多人拿到注册码需求,第一反应是“我给个字符串,用户填对了就放行”。这个思路只能防不懂技术的操作工,根本算不上授权。注册码背后应该是一个授权载荷(License Payload),里面至少包含三样东西:机器特征指纹、过期日期、功能模块标记。
你的程序要做的是“解密载荷 + 校验指纹 + 判断有效期”,而不是“比对注册码字符串是否相等”。字符串相等的校验,用记事本就能改内存绕过;而解密校验链路,至少能让普通用户和半吊子破解者停住一段时间。
授权载荷建议用分隔符拼成明文,再统一加密。我常用的格式是yyyyMMdd|指纹|模块标记,例如:
20261231|A3F2C9D1B7E4|BASE为什么要用“|”而不是 JSON 或 XML?注册码通常要控制在 48~64 个字符内,JSON 带引号冒号,序列化后太长,还不方便复制。工业上位机现场,操作工网速慢、屏幕小,短字符串更友好。
2.2 AES-GCM 加密:新项目最快能跑通的对称方案
对称加密是注册码场景里最直接的一环:开发者用同一个密钥把授权载荷变成密文,程序启动时用同一个密钥解回来。AES 是当前唯一合理的选择,DES 和 3DES 直接不要考虑。
AES 有三种常见模式:ECB、CBC、GCM。ECB 不能用于任何真实授权,同样的明文会产生同样的密文,泄露字段关系;CBC 需要填充和 IV,能做但要在代码里处理 PKCS7 和多 16 字节的 IV;GCM 是最省心的,它是 AEAD 模式,加密的同时生成认证标签,解密时会自动校验数据有没有被篡改。
.NET 从 Core 3.0 开始提供AesGcm类,如果你在 .NET 6 以上开发,直接用它。但要注意,老的 .NET Framework 4.8 项目没有这个类,装了System.Security.Cryptography.Algorithms包也不一定完全兼容。旧项目里更稳的做法是AesCbc加密再加 HMAC 做完整性校验,也就是 Encrypt-then-MAC。
下面是 .NET 6 下 AES-GCM 的最小加解密用法:
using System.Security.Cryptography; using System.Text; byte[] key = SHA256.HashData(Encoding.UTF8.GetBytes("your-secret")); byte[] nonce = new byte[12]; RandomNumberGenerator.Fill(nonce); byte[] plain = Encoding.UTF8.GetBytes("20261231|A3F2C9D1B7E4|BASE"); byte[] cipher = new byte[plain.Length]; byte[] tag = new byte[16]; using var aes = new AesGcm(key, tag.Length); aes.Encrypt(nonce, plain, cipher, tag);这里nonce也叫 IV,推荐 12 字节(96 位),这是 GCM 的标准长度。tag是认证标签,16 字节表示 128 位安全性。key用 SHA256 把原始口令散列成 32 字节,是因为 AES-256 需要正好 32 字节密钥,直接拿一串中文密码当 key 会报长度错误。
GCM 的解密端更有价值:验证tag失败会直接抛异常,说明数据被人改过。这个特性正是注册码需要的——防止用户自己篡改有效期或机器指纹。
2.3 RSA 签名:让注册码不可伪造的正路,以及什么时候必须升级
AES 有个天生弱点:密钥放在客户端 exe 里,逆向者把密钥提出来后,就可以自己写个注册码生成器,等于你这套授权体系当场失效。所以如果你的软件单价不算低,或者有被批量破解的风险,那在 AES 之上必须加 RSA 数字签名。
RSA 的核心不是加密,而是签名。私钥留在你手里,客户端只放公钥。私钥给授权载荷签名,客户端拿公钥验签。因为客户端没有私钥,伪造注册码在数学上不可行。
// 生成端:用私钥签名 using var rsa = RSA.Create(); rsa.ImportFromPem(File.ReadAllText("private.pem")); byte[] signature = rsa.SignData( Encoding.UTF8.GetBytes(payload), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // 客户端:用公钥验签 using var rsaVerify = RSA.Create(); rsaVerify.ImportFromPem(File.ReadAllText("public.pem")); bool valid = rsaVerify.VerifyData( Encoding.UTF8.GetBytes(payload), signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);签名的载荷长度没有硬限制,但注册码输入框最长不过 64 位,RSA-2048 签名本身就占 344 个 Base64 字符,输注册码不现实。常见的落地方式是混合:短授权数据直接做 Signed Data,授权内容复杂时用 AES 加密内容、RSA 签名摘要。Demo 阶段从 AES-GCM 起步没问题,交付生产前再把 RSA 签名加上。
一句话结论:DEMO 用 AES-GCM 建立完整链路,商业交付用“RSA 签名 + AES 加密”双保险。第 5 章我会讲怎么从前者平稳升级到后者。
3. 最小可运行 Demo:在 .NET 6 上把机器码和试用期编进注册码
3.1 第一步:采集机器指纹,把 CPU、主板、BIOS 哈希拼接成机器码
机器指纹是整个注册码体系的锚点。你选的硬件特征决定了用户换张网卡、换个 CPU 是不是要重新授权。工业上位机里最常见的组合是 CPU ProcessorId、主板序列号、BIOS 序列号。这三个字段在 Windows 下好取,而且在品牌机上可读性稳定。
using System.Management; using System.Security.Cryptography; using System.Text; static string GetMachineFingerprint() { string cpu = ""; string board = ""; string bios = ""; using (var searcher = new ManagementObjectSearcher( "SELECT ProcessorId FROM Win32_Processor")) { foreach (var obj in searcher.Get()) cpu = obj["ProcessorId"]?.ToString() ?? ""; } using (var searcher = new ManagementObjectSearcher( "SELECT SerialNumber FROM Win32_BaseBoard")) { foreach (var obj in searcher.Get()) board = obj["SerialNumber"]?.ToString() ?? ""; } using (var searcher = new ManagementObjectSearcher( "SELECT SerialNumber FROM Win32_BIOS")) { foreach (var obj in searcher.Get()) bios = obj["SerialNumber"]?.ToString() ?? ""; } string raw = $"{cpu}|{board}|{bios}".Trim('|'); byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(raw)); return Convert.ToHexString(hash).Substring(0, 16); }这段代码逻辑很直白:从 WMI 拿三个硬件 ID,拼成字符串,算 SHA256,取前 16 个十六进制字符。取前 16 位不是图省事,是为了让注册码短些,而且 64 位哈希已经足够区分不同机器,碰撞概率可以忽略。
要注意的是System.Management在 .NET 6 控制台项目默认没有引用,需要先Install-Package System.Management。在工业上位机上线时还得验证:如果你跑的 Windows 版本是精简版,WMI 服务可能没启用,调用会超时。后面避坑章节细说。
3.2 第二步:写注册码生成器,用 AES-GCM 加密授权载荷
生成器跑在开发者自己机器上,不发布给客户。它的职责是:接收机器指纹和过期日期,输出注册码字符串。此时注册码不是简单加密,而是把“过期时间 + 指纹 + 模块”打包成密文。
public static string GenerateRegCode( string fingerprint, DateTime expire, string secret) { string payload = $"{expire:yyyyMMdd}|{fingerprint}|BASE"; byte[] key = SHA256.HashData(Encoding.UTF8.GetBytes(secret)); byte[] nonce = new byte[12]; RandomNumberGenerator.Fill(nonce); byte[] plain = Encoding.UTF8.GetBytes(payload); byte[] cipher = new byte[plain.Length]; byte[] tag = new byte[16]; using var aes = new AesGcm(key, tag.Length); aes.Encrypt(nonce, plain, cipher, tag); byte[] all = new byte[nonce.Length + cipher.Length + tag.Length]; Buffer.BlockCopy(nonce, 0, all, 0, nonce.Length); Buffer.BlockCopy(cipher, 0, all, nonce.Length, cipher.Length); Buffer.BlockCopy(tag, 0, all, nonce.Length + cipher.Length, tag.Length); return Convert.ToBase64String(all) .TrimEnd('=').Replace('+', '-').Replace('/', '_'); }这段代码有一个关键点:密文、nonce、tag 必须一起送给客户端,解密时一个都不能少。我直接把 nonce 放数组头、tag 放数组尾,统一 Base64 输出。输出字符串里把+/换成-_,并去掉=填充,是为了让注册码在输入框里不容易被误选和错传。
参数说明:secret是你自己掌握的口令,建议 20 位以上随机字符。expire用yyyyMMdd格式,避免时区解析歧义。fingerprint来自 3.1 的GetMachineFingerprint(),长度 16 位。最终注册码大约 44 个 Base64URL 字符,手工录入可以接受。
3.3 第三步:客户端校验,解密、比对指纹、判断有效期
客户端拿到用户输入的注册码,要完成三步:Base64URL 还原、AES-GCM 解密、比对指纹和有效期。如果中间任何一步失败,直接拒绝启动。
public static (bool ok, string reason) VerifyRegCode( string code, string fingerprint, string secret) { try { string input = code.Replace('-', '+').Replace('_', '/'); input += new string('=', (4 - input.Length % 4) % 4); byte[] all = Convert.FromBase64String(input); if (all.Length < 12 + 16 + 4) return (false, "注册码格式太短"); byte[] nonce = all[..12]; byte[] cipher = all[12..^16]; byte[] tag = all[^16..]; byte[] key = SHA256.HashData(Encoding.UTF8.GetBytes(secret)); byte[] plain = new byte[cipher.Length]; using var aes = new AesGcm(key, tag.Length); aes.Decrypt(nonce, cipher, tag, plain); string payload = Encoding.UTF8.GetString(plain); string expire = payload[..8]; string fp = payload.Split('|')[1]; if (!string.Equals(fp, fingerprint, StringComparison.OrdinalIgnoreCase)) return (false, "注册码与当前机器不匹配"); if (DateTime.Now > DateTime.ParseExact(expire, "yyyyMMdd", null)) return (false, "授权已过期"); return (true, "OK"); } catch (Exception ex) { return (false, ex.Message); } }这里最容易出错的是 Base64 还原。生成端去掉了=,还原时必须补齐,否则FromBase64String抛异常提示长度无效。我用的补法是(4 - input.Length % 4) % 4,注意当长度刚好是 4 的倍数时补 0 个=,不要补 4 个。
解密顺序也要对齐生成端:nonce 在最前、cipher 居中、tag 最后。aes.Decrypt在 tag 校验失败时会抛CryptographicException,这个异常被 catch 住后直接作为“注册码无效”返回即可。千万不要在 UI 里把 ex.Message 完整显示给用户,暴露密钥派生细节。
3.4 授权文件模式:注册码太长时,退一步用 .lic 文件
有些客户代码里要用到 RSA 签名,或者要拉入多个功能模块,注册码字符串可能膨胀到 300 字符以上,再让人手工输入就说不过去了。常见做法是改成交付一个.lic文件,用户把文件放到 exe 同级目录,程序启动时读取。
static bool VerifyLicenseFile(string filePath, string fingerprint, string secret) { string code = File.ReadAllText(filePath).Trim(); var result = VerifyRegCode(code, fingerprint, secret); return result.ok; }文件模式和注册码模式本质一样,只是输入渠道从键盘变成了文件。文件名的命名习惯我一般用.lic,但要注意不要把文件路径做成硬编码,避免升级包覆盖掉授权文件。更稳妥的是约定读取appDir/*.lic里第一个可用文件,这样客户换文件名不折腾。
4. 注册码 Demo 落地避坑:5 个让用户破口大骂的现象
4.1 换了 CPU 或重装系统,授权直接失效
现象:用户按流程装完软件,填了注册码,用了三个月。某天车间电脑换了块 CPU 或者主板电池报错刷了 BIOS,程序启动就提示注册码与机器不匹配。
原因:指纹里绑了 ProcessorId 和 BIOS 序列号,这两项一变化,整个 SHA256 全变。工业现场升级硬件太平常,你绑得越死,客诉越多。
解决:指纹采集时加一个“宽松模式”。三个硬件特征里至少匹配两个才算合法,而不是全量比对。实现方式是把指纹拆成三段分别哈希保存,校验时逐段比对,允许一段不匹配。同时给用户留一条人工换绑通道:拿旧注册码 + 新指纹找售后换新码。别把用户逼到卸载重装。
4.2 解密提示“要解密的数据长度无效”
现象:同一个注册码,自己测试机验证通过,发给客户后报“要解密的数据长度无效”,程序直接闪退。或者用户在输入框手敲注册码,空格和换行粘贴进去后立刻失败。
原因:多半是 Base64 转换时长度不对。生成端去掉了=填充,校验端没补,Convert.FromBase64String就会报长度无效。另一个常见原因是剪贴板带入了不可见字符,或者换行\r\n混进了字符串。
解决:校验入口统一做code = code.Trim(),去除空白和换行。在FromBase64String之前,按上文说的长度补位逻辑处理。这里也提醒一句:网上搜“md5解密”搜到手软的,要清楚指纹哈希本身就是不可逆的,你做的只是把哈希和原值比对;而注册码里的“解密”用的是 AES-GCM,两者不是一回事。
4.3 用户把系统时间改回一年前,软件永久续期
现象:试用 30 天的注册码,客户把 Windows 时间调成 2020 年,软件每次都能正常启动,试用期形同虚设。
原因:校验时用的是本地时间DateTime.Now,而本地时间可被随意修改。客户端不知道真实时间,除非程序联网校时。
解决:在线场景,启动时向自己的时间服务器或公共 NTP 服务器校时,用校时结果替代DateTime.Now。离线场景,至少记录本机“最近一次运行时间”,检测到当前时间小于上次运行时间,说明时钟被回拨,直接判为非法。Demo 阶段我会把上次运行时间的 SHA256 写进注册表或文件,下次启动比对。别用明文LastRun,破解者改一行文本就能骗过去。
4.4 与原生 C++ 模块联调时,出现 access violation c0000005
现象:C# 上位机调用 C++ 动态库里的解密函数,传入解密后的字节数组,程序直接崩,事件日志里出现access violation c0000005。大多数时候不是算法问题,是内存传递方式错了。
原因:C# 的byte[]受 GC 管理,GC 在压缩堆时可能移动对象地址。你把byte[]直接作为参数传给非托管入口,C++ 侧拿着一个可能被移动的指针读写,自然访问违规。
解决:用Marshal.AllocHGlobal分配非托管内存,把字节拷进去,再把指针传给 C++。用完立即释放:
byte[] cipher = DecodeRegCode(code); IntPtr buf = Marshal.AllocHGlobal(cipher.Length); try { Marshal.Copy(cipher, 0, buf, cipher.Length); NativeCppDecrypt(buf, cipher.Length); } finally { Marshal.FreeHGlobal(buf); }注意AllocHGlobal分配的是非托管堆,不会受 GC 移动影响,但必须在 finally 里释放,否则内存泄漏。传字符串时还要考虑 C++ 侧的编码,C# 默认 UTF-16,C++ 默认 ANSI 或 UTF-8,建议统一转 UTF-8 字节再传。这也算把“注册码 Demo”和“C++ 调用 Access Violation”这两件事的因果关系说清了:绝大多数崩盘都来自身份不清的指针,而不是加密算法。
4.5 加了混淆还是被一键脱壳,密钥被提出来批量造码
现象:花了半天给 exe 加混淆,结果对方用 ILSpy、de4dot 几分钟就把密钥找出来了,仿冒注册码开始流通。
原因:混淆改变的是类名和方法名,不是密钥存储位置。只要你把secret明文写在代码里,string常量就会出现在程序的元数据中。而 AES-GCM 的密钥又必须同时存在于生成端和校验端,这个秘密一定会埋在客户端二进制里。
解决:至少做到三层。第一,密钥不要以一个完整字符串出现,拆成三段分别存字段,运行时拼接;第二,不要为这个 Demo 单留下一段后台代码,把密钥派生逻辑藏进其他业务调用里;第三,生产交付时放弃“一把 AES 密钥走天下”,改用第 2.3 节的 RSA 签名,私钥不出你公司,客户端只有公钥。客户端泄露公钥没有任何风险,这是 RSA 比 AES 更适合授权场景的根本原因。
5. 往生产走:授权缓存、日志暗桩与 RSA 签名升级
5.1 用带 HMAC 的缓存文件,避免每次启动都走完整解密
AES-GCM 解密每次启动只花十几毫秒,但在工控机上开机自启、断电重启频繁,反复刷新授权并不必要。我的习惯是首次校验通过后,把机器指纹和过期日期的哈希写入应用目录下的.lic缓存文件。下次启动先算缓存,匹配则直接放行,不再走解密链路。
string cacheFile = Path.Combine(appDir, ".lic"); string cacheValue = string.Empty; if (File.Exists(cacheFile)) cacheValue = File.ReadAllText(cacheFile).Trim(); string expected = Convert.ToHexString( SHA256.HashData(Encoding.UTF8.GetBytes(fingerprint + "|" + expireStr))); if (!string.Equals(cacheValue, expected, StringComparison.OrdinalIgnoreCase)) { // 缓存不匹配,走完整 VerifyRegCode 流程 }这个缓存的核心价值是:把“授权校验”从每次启动必跑,变成“只有首次和异常时才跑”。缓存文件名义上是.lic,其实内容既不是注册码也不是明文信息,而是一个摘要。用户手动改这个文件没有任何意义,因为摘要算不出来。
另一个经验是在授权失败时写“日志暗桩”,不要一概阻塞。我会把失败原因、指纹哈希、系统时间写到一个auth.log文件里,客户遇到问题把日志发回来,十秒钟就能看出是时钟漂移、硬件变更还是注册码输入错误。没有日志,售后就被动。
5.2 从 Demo 升级到交付:加 RSA 签名、放宽硬件绑定、加暗桩
生产级别的授权系统里,我不太建议继续用“AES + 统一口令”的结构。上线成本最低的升级路线是这样是一条清晰路径:
- 第 1 步:把第 3 章 Demo 里的
secret换成 32 字节随机字节数组,程序里做分段拼装,并用 DPAPI 保护。 - 第 2 步:生成 RSA 密钥对,私钥留售后,公钥埋进程序。校验顺序变为“先用公钥验签,再比对机器指纹”,这样即使二进制被脱壳,也没有伪造注册码的能力。
- 第 3 步:指纹匹配从全量改为“多数一致 + 人工换绑通道”,同时把授权到期时间备份到注册表和文件两个位置,防止用户清缓存续期。
- 第 4 步:在核心逻辑里埋一两个行为暗桩。比如校验失败时,偶发地不弹错误框,而是在某个全局变量里记下异常状态,延迟到功能调用时报错。这样破解者即使跳过了第一次检查,也会在后续业务里露馅。
我一般在 Demo 阶段用 AES-GCM 跑通链路,交付阶段切到 RSA 签名,这台本也值得所有新项目复用一遍。如果你在做 c# 上位机且现场通过 OPC 连接西门子 PLC,那授权校验千万别放进 OPC 回调线程里,否则设备通信被卡一下可能就是一次停机事故。授权校验只放主进程启动路径,跑一遍就退出。
做注册码这事,没有绝对的安全,只有递增的成本。你让破解者多花两天,就能劝退大部分只想复制粘贴的客户。先把这套最小 Demo 跑起来,再按项目预算逐步加固,这条路比一开始就找各种混淆壳靠谱得多。希望这篇能帮你在 C# 加密与解密注册码这个方向上省下几晚加班时间,少踩几个我已经替你踩过的坑。
本文还有配套的精品资源,点击获取