ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C#加密解密注册码实战:AES-GCM+RSA签名构建软件授权机制

2026/9/26 4:56:51 拓冰建站 浏览量
C#加密解密注册码实战:AES-GCM+RSA签名构建软件授权机制 简介《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 字节的 IVGCM 是最省心的它是 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-16C 默认 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# 加密与解密注册码这个方向上省下几晚加班时间少踩几个我已经替你踩过的坑。本文还有配套的精品资源点击获取