# RuoVea.KM
**Repository Path**: ruovea-tools/RuoVea.KM
## Basic Information
- **Project Name**: RuoVea.KM
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-05-08
- **Last Updated**: 2026-10-16
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# RuoVea.KM - 授权管理系统
## 📖 项目简介
RuoVea.KM 是一个完整的授权管理系统,基于国密算法(SM2/SM3/SM4)实现,提供授权码生成、验证、激活等功能,以及机器硬件指纹识别能力。
该库面向 **软件产品集成** 场景设计,可以直接作为组件被其它项目引用。典型用法是:产品内置一份由供应商签发的试用策略令牌,用户在机器上可直接试用;试用到期后必须由供应商按其机器码签发授权码才能继续使用。
支持 .NET 5/6/7/8/9/10。仓库另附两个 WinForms + AntdUI 界面工具:**`RuoVea.KM.Licenser`(客户端激活界面,只含公钥)** 与 **`RuoVea.KM.Issuer`(供应商签发界面,持有私钥)**。
## ✨ 功能特性
- 🔒 **签名保护的试用策略**:试用天数由 SM2 私钥签名后随程序分发,客户端只验签,无法通过改常量或改配置延长试用期
- 🖥️ **机器指纹识别**:基于 CPU / 主板 / BIOS / 硬盘序列号 / MAC 地址 / 系统 MachineGuid 生成机器码,多因子排序归一,硬件小改动不会导致授权失效
- 🔐 **国密算法支持**:SM2 签名验签、SM3 摘要、SM4 加解密
- 🛡️ **防篡改状态存储**:本地状态经 SM4-CBC 加密 + HMAC 校验,密钥由机器码派生;同时写入多个相互独立的存储位置,读取时取"最不利"值合并
- ⏱️ **时间回撤检测**:记录已观测的最大系统时间作为高水位,配合程序安装时间下界识别系统时间被回拨
- 🚦 **激活限流**:失败次数超过阈值后进入指数退避锁定,抵御离线暴力枚举授权码
- 🔑 **密钥轮换**:授权码携带 KeyId,可在签名与公钥集合中同时维护新旧密钥,私钥疑似泄露时可平滑作废
- 🚫 **一码一机**:SM4 密钥由机器码派生、不随授权码传输,授权码复制到其它机器无法解密使用
- 📦 **多目标框架支持**:.NET 5.0 / 6.0 / 7.0 / 8.0 / 9.0 / 10.0
- 📱 **跨平台支持**:支持 Windows 和 Linux 系统(注册表副本为 Windows 专属增强,缺失时自动降级)
## 📦 安装
### NuGet 包管理器
```bash
Install-Package RuoVea.KM
```
### .NET CLI
```bash
dotnet add package RuoVea.KM
```
### PackageReference
```xml
```
## 🚀 快速开始
集成分为两个角色,**务必分清**:
| 角色 | 运行位置 | 持有密钥 | 主要 API |
| --- | --- | --- | --- |
| **供应商** | 签发服务器 / 离线签发机 | SM2 **私钥** | `LicenseIssuer` |
| **客户端** | 用户机器上的产品 | 仅 SM2 **公钥** | `LicenseGuard` / `LicenseManager` |
### 一、供应商侧:签发试用策略与授权码
```csharp
using RuoVea.KM;
// 1) 一次性生成密钥对
var (publicKey, privateKey) = LicenseIssuer.GenerateKeyPair();
// publicKey → 写进客户端代码或配置(可以公开)
// privateKey → 存入密钥管理系统或硬件加密机(绝不下发,不要写进客户端)
// 2) 整个产品只签发一次:2 年试用策略令牌
// 试用起算锚点默认是"用户首次运行",因此程序交付后长期未安装也不会白白消耗试用期
string trialToken = LicenseIssuer.CreateTrialToken(
appId: "MyProduct",
privateKey: privateKey,
trialDays: 730); // 730 天 = 2 年
// 3) 客户购买后,按其提供的机器码签发正式授权
string licenseCode = LicenseIssuer.IssueLicense(
appId: "MyProduct",
customerName: "某某公司",
machineCode: "8QH2-3K9M-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX",
privateKey: privateKey,
years: 1,
keyId: "k1");
```
> 不想写代码时,上述签发动作在 **服务端签发工具 `RuoVea.KM.Issuer`** 里有对应界面(见下方「🛠️ 服务端签发工具」)。
### 二、客户端侧:校验与激活
```csharp
using RuoVea.KM;
var options = new LicenseOptions
{
AppId = "MyProduct",
PublicKey = publicKey, // 供应商公钥
TrialToken = trialToken, // 供应商签发的 2 年试用策略令牌
ExpiringSoonDays = 15
};
// ========== 启动校验 ==========
LicenseState state = LicenseGuard.Check(options);
if (state.IsValid)
{
Console.WriteLine($"可用,剩余 {(state.RemainingDays < 0 ? "永久" : state.RemainingDays + " 天")}");
}
else
{
Console.WriteLine($"不可用:{state.Message}");
}
// 也可在启动路径上一行完成"校验 + 终止"
// LicenseGuard.EnforceOrExit(options); // 不可用时写错误到 stderr 并以退出码 78 结束
// ========== 展示机器码,供用户申请授权 ==========
string machineCode = LicenseGuard.GetActivationRequestCode();
Console.WriteLine($"请将此机器码提供给供应商:{machineCode}");
// ========== 激活 ==========
var activation = LicenseGuard.Activate("用户粘贴的授权码", options);
if (activation.Code == RuoVea.ExEnum.CodeStatus.OK)
{
Console.WriteLine(activation.Message);
}
else
{
Console.WriteLine($"激活失败:{activation.Message}");
}
// ========== 到期提醒 ==========
if (state.IsExpiringSoon)
{
Console.WriteLine($"授权将在 {state.RemainingDays} 天后到期,请及时续期");
}
```
### 三、售后换机:重置码
客户更换机器后需要释放旧机器的授权状态。由于本地状态受完整性保护,用户不能自行重置(否则等同于重开试用期),正确做法是由供应商签发一次性重置码:
```csharp
// 供应商侧:为客户的旧机器码签发一个 3 天内有效的重置码
string resetCode = LicenseIssuer.CreateResetCode(oldMachineCode, privateKey, validDays: 3);
// 客户端侧:校验通过后才清除本地状态
string error;
if (LicenseIssuer.TryVerifyResetCode(resetCode, options, out error))
{
LicenseGuard.ResetLocalState(options);
}
else
{
Console.WriteLine($"重置码无效:{error}");
}
```
## 🖥️ 授权界面工具(RuoVea.KM.Licenser)
`RuoVea.KM.Licenser` 是一个基于 AntdUI 的 WinForms 界面工具,用于替代"让用户自己找机器码、自己复制授权码"的原始流程。
它**不签发授权码,也不含私钥**:只展示机器码、接收用户粘贴的授权码并调用 `LicenseGuard` 完成激活。签发仍在供应商内部的离线流程里用 `LicenseIssuer` 完成。
**该工具运行时需要 .NET 8.0(Windows)**;授权组件本身仍支持 net6.0 ~ net10.0,界面工具与组件版本互相独立。
### 方式一:独立运行
```bash
# 1) 在程序目录生成配置模板
RuoVea.KM.Licenser.exe --write-template
# 2) 填写 licenser.json 后直接运行,弹出授权窗口
RuoVea.KM.Licenser.exe
# 脚本化采集机器码(不弹界面)
RuoVea.KM.Licenser.exe --machine-code
```
`licenser.json` 字段:
| 字段 | 说明 |
| --- | --- |
| `appId` | 产品标识,必须与目标软件的 `AppId` 一致 |
| `publicKey` | 供应商 SM2 公钥(十六进制) |
| `trialToken` | 供应商签发的试用策略令牌 |
| `publicKeys` | 多公钥字典(KeyId → 公钥),可选,用于密钥轮换 |
| `windowTitle` | 窗口标题,留空使用内置多语言文案 |
| `language` | 界面语言(`zh-CN` / `zh-TW` / `zh-HK` / `en-US` / `ja-JP` / `fr-FR` / `vi-VN`),留空跟随系统 |
| `expiringSoonDays` | 距到期多少天开始提示续期 |
### 方式二:嵌进自己的软件
界面主体是 `LicensePanel`(一个 `UserControl`),可以放进设置页或关于页:
```csharp
using RuoVea.KM;
using RuoVea.KM.Licenser;
var options = new LicenseOptions { AppId = "MyProduct", PublicKey = publicKey, TrialToken = trialToken };
// 第二个参数 showHeader 控制是否显示面板自带的标题区:
// 宿主页面已有标题时传 false,避免出现两行同样的字。
var panel = new LicensePanel(options, showHeader: false) { Dock = DockStyle.Fill };
settingsPage.Controls.Add(panel);
// 用户完成外部操作(如手工替换状态文件)后可主动刷新
panel.RefreshState();
```
需要独立弹窗时用 `LicenseDialog`:
```csharp
LicenseDialog.Show(options); // 非模态
DialogResult r = LicenseDialog.ShowDialog(options); // 模态,激活成功返回 OK
```
### 本地操作日志
工具会把"配置读取失败 / 激活成功 / 激活失败(含原因) / 复制机器码"追加写入状态目录下的 `licenser-log.txt`,
用于售后排障(客户说"激活了但用不了"时,可对照时间点确认当时究竟发生了什么)。
日志中的机器码**只保留 SHA256 指纹前 16 位**,不落机器码明文;日志只写不读,不参与任何授权判定。
单条记录形如:
```
2026-10-16 16:37:43 | 0281 | MyProduct | 400D7ACB19E75FF2 | 激活失败(已记入本地日志) | 授权码与硬件不匹配
```
## 🛠️ 服务端签发工具(RuoVea.KM.Issuer)
`RuoVea.KM.Licenser` 是给**客户**用的激活界面,只含公钥;`RuoVea.KM.Issuer` 是给**供应商自己**用的签发界面,**持有 SM2 私钥**,把 `LicenseIssuer` 的四个动作做成表单:
| 功能区 | 作用 | 对应 API |
| --- | --- | --- |
| **密钥对** | 生成 / 校验 SM2 密钥对,加密保存到本机密钥库 | `GenerateKeyPair`、`ValidateKeyPair` |
| **试用令牌** | 签发声明"试用天数 + 起算方式"的策略令牌,随客户端分发 | `CreateTrialToken` |
| **正式授权码** | 绑定机器码签发授权码(永久 / 年限 / 天数 / 功能项) | `IssueLicense` |
| **售后重置码** | 客户换硬件或重装后,让客户端清掉旧试用锚点与失败计数 | `CreateResetCode` |
> ⚠️ **本工具持有私钥,仅供供应商内部使用:请勿随软件分发,切勿把私钥写入客户端配置、日志或聊天记录。** 私钥泄露等于整套授权体系失效。
**该工具运行时需要 .NET 8.0(Windows)**,与授权组件的多目标框架(net6.0 ~ net10.0)互相独立。
### 运行
```bash
# 打开签发窗口(四个功能区,左侧导航切换)
RuoVea.KM.Issuer.exe
# 脚本化采集本机机器码(不弹界面)
RuoVea.KM.Issuer.exe --machine-code
# 生成一对密钥并打印(不落盘,自行抄录进密钥库)
RuoVea.KM.Issuer.exe --gen-keypair
# 端到端自检:签发试用令牌 / 授权码 / 重置码,再用客户端侧 API 反向验证
RuoVea.KM.Issuer.exe --self-test
```
### 密钥库与口令
私钥以**口令加密**的形式保存在 `%LOCALAPPDATA%\RuoVea\KM.Issuer\keys.vault`(单行文本,SM3 派生密钥 + SM3 完整性校验)。口令**不落盘**:忘了口令只能重新生成密钥对,已下发的授权码随之全部失效。
密钥库字段:产品标识、密钥标识(KeyId,用于密钥轮换)、备注名称、公钥、私钥、密钥库口令。
### 签发日志
每次签发都会追加一行到 `%LOCALAPPDATA%\RuoVea\KM.Issuer\issuer-log.txt`,用于事后核对"某天到底给谁签了什么"。
日志**绝不写入私钥明文,也绝不写入密钥库口令**;机器码只保留 SM3 指纹前 16 位(`IssueLog.MachineTag`),不落机器码明文。单条记录形如:
```
2026-10-16 17:57:29 | 6528 | 生成密钥对 | 命令行
```
时间 | 随机序号 | 动作 | 参数摘要(`keyId=` / `appId=` / `machine=` / `years=` …)。
### 与客户端工具的配对关系
```
密钥对页 ──生成──► 公钥 ──► 填进客户端 licenser.json
└► 私钥 ──► 只留在本机密钥库
试用令牌页 ──签发──► 试用令牌 ──► 填进客户端 licenser.json(随软件分发)
正式授权码页 ──按客户机器码签发──► 授权码 ──► 客户在客户端界面粘贴激活
售后重置码页 ──按客户机器码签发──► 重置码 ──► 客户在客户端执行重置
```
## 🔐 安全能力与边界
### 已提供的防护
| 需求 | 实现手段 | 说明 |
| --- | --- | --- |
| **用户可使用 2 年** | 签名试用策略令牌 + 首启时间锚定 | 试用天数由 `LicenseIssuer` 签名,客户端无法篡改;起算点为用户首次运行 |
| **2 年后不能继续使用** | 到期校验 + 令牌硬截止时间双重约束 | 到期后 `LicenseGuard.Check` 返回 `Expired`,`EnforceOrExit` 直接终止进程 |
| **防暴力破解** | 激活失败限流 + 指数退避锁定 | 授权码由 SM2 私钥签名,本来就无法伪造;限流进一步拖慢盲猜 |
| **防时间回撤** | 高水位记录 + 程序安装时间下界 + 容忍窗口 | 把系统时间调回过去会被识别并拒绝使用 |
| **防密钥泄露** | 客户端只持有公钥;KeyId 轮换 | 私钥不下发,公钥泄露无安全影响;私钥疑似泄露时可换 KeyId 平滑作废 |
| 额外:防授权码转发 | SM4 密钥由机器码派生,不随授权码传输 | 授权码复制到别的机器后解密得到乱码,无法使用 |
| 额外:防删状态重置试用 | 多副本冗余存储 + 对抗性合并 | 用户级文件、系统级文件、Windows 注册表三处互为备份,只删一处无法重置 |
### 必须知情的边界
**纯客户端授权方案无法抵御拥有本机管理员权限且愿意逆向的对手。** 本组件能可靠挡住的是低成本绕过:改系统时间、删授权文件、把授权码发给别人、复制授权文件到另一台机器、篡改试用天数常量。对"反编译后直接改 IL 跳过校验调用"这类攻击,本组件只能提高门槛,不能根除。
若业务上必须做到"绝对无法绕过",需要把关键校验移到服务端(由服务端下发短期票据),或使用硬件加密狗。本组件的定位是把常见绕过手段的收益降到接近零,同时保持离线可用、集成成本低。
### 务必遵守的集成纪律
1. **私钥绝不下发**。`LicenseIssuer` 只能在供应商内部环境运行;客户端代码里不要出现私钥,也不要把私钥写进配置文件或环境变量。
2. **每个产品用独立的 AppId**。AppId 参与状态文件路径与密钥派生,同机多产品互不干扰;它同时是试用令牌的产品绑定依据。
3. **`ResetLocalState` 只在供应商指令下调用**。它会连同试用首启时间一起清掉,客户端流程里自行调用等于重开试用期。
4. **`MaxTrialDays` 与 `HardMaxTrialDays` 是编译期上限**。即便令牌被伪造,客户端也不会接受超过该值的试用期(`HardMaxTrialDays` 为 3650 天)。
5. **签发工具与客户端工具不可混发**。`RuoVea.KM.Issuer` 会解锁私钥,只能放在供应商内部机器上;对外分发的一律是 `RuoVea.KM.Licenser`。密钥库口令与私钥都不进版本库、不进聊天记录、不写日志。
## 🏗️ 项目结构
```
RuoVea.KM/
├── RuoVea.KM/ # 核心库
│ ├── LicenseOptions.cs # 集成配置(AppId / 公钥 / 试用令牌 / 限流参数)
│ ├── LicenseState.cs # 状态、错误码、本地状态记录
│ ├── LicenseInfo.cs # 授权信息实体
│ ├── LicenseGuard.cs # 客户端集成主入口
│ ├── LicenseManager.cs # 授权码生成、验证、激活
│ ├── LicenseIssuer.cs # 供应商签发工具(需私钥)
│ ├── LicenseEngine.cs # 内部校验引擎
│ ├── TrialToken.cs # 试用策略令牌编解码与验签
│ ├── SecureStateStore.cs # 多副本防篡改状态存储
│ ├── ClockGuard.cs # 时间回撤检测
│ ├── ActivationThrottle.cs # 激活限流
│ ├── MachineFingerprint.cs # 机器码与硬件指纹
│ ├── CryptoUtil.cs # 密码学小工具
│ ├── Language/ # 8 种语言的提示资源
│ └── *.csproj # 多目标框架项目文件
├── RuoVea.KM.Demo/ # 示例项目(含 16 项防护自检)
├── RuoVea.KM.Licenser/ # 授权界面工具(WinForms + AntdUI,net8.0-windows)
│ ├── LicensePanel.cs # 可嵌入宿主软件的授权面板(含状态/机器码/激活三块)
│ ├── LicenseDialog.cs # 独立授权窗口(含自绘标题栏与拖动)
│ ├── LicenserConfig.cs # licenser.json 读写
│ ├── ActivationLog.cs # 本地操作日志(辅助售后排障,不参与授权判定)
│ ├── LicenseVisualMap.cs # 授权状态 → 界面呈现的映射
│ ├── UiTheme.cs # AntdUI 主题色与字体
│ ├── Program.cs # 宿主入口与命令行参数
│ └── Language/ # 工具界面文案(7 种语言)
├── RuoVea.KM.Issuer/ # 服务端签发工具(WinForms + AntdUI,net8.0-windows,★持有私钥)
│ ├── IssuerDialog.cs # 签发窗口:左侧导航 + 安全横幅 + 可缩放标题栏
│ ├── IssuerPage.cs # 功能页基类(标题 / 正文滚动 / 页尾提示)
│ ├── KeyPairPage.cs # 密钥对:生成、校验、存取密钥库
│ ├── TrialTokenPage.cs # 试用令牌:试用天数、起算方式、硬截止日期
│ ├── LicensePage.cs # 正式授权码:绑定机器码签发
│ ├── ResetCodePage.cs # 售后重置码:按机器码签发短期重置码
│ ├── KeyVault.cs # 口令加密的密钥库(keys.vault)
│ ├── KeySession.cs # 当前会话持有的密钥状态
│ ├── IssueLog.cs # 签发日志(不写私钥、机器码只留 SM3 前 16 位)
│ ├── IssuerText.cs # 工具界面文案(中文)
│ ├── UiTheme.cs / UiKit.cs # AntdUI 主题色、字体与控件工厂
│ └── Program.cs # 宿主入口与命令行参数
├── doc/ # 图标等文档资源
├── LICENSE
├── README.md
├── RuoVea.KM.sln
└── dotnet build.bat # 逐目标框架 Release 构建脚本
```
## 📚 核心类说明
### LicenseOptions(集成配置)
| 属性 | 说明 |
| --- | --- |
| `AppId` | 产品标识,用于隔离状态目录与绑定试用令牌 |
| `PublicKey` | 供应商 SM2 公钥(十六进制) |
| `TrialToken` | 供应商签发的试用策略令牌 |
| `PublicKeys` | 多公钥字典(KeyId → 公钥),用于密钥轮换 |
| `MaxTrialDays` | 客户端额外施加的试用天数上限 |
| `AllowTrial` | 是否允许试用,关闭后必须有正式授权 |
| `ClockToleranceSeconds` | 时钟校验容忍秒数,默认 1 天 |
| `Anchor` | 试用起算锚定方式(首启 / 令牌签发时间) |
| `MaxFailedActivations` | 触发锁定的失败次数阈值,默认 5 |
| `LockoutMinutes` | 基础锁定时长(分钟),默认 1440,按失败次数翻倍 |
| `ExpiringSoonDays` | 距到期多少天开始提示续期,默认 15 |
| `StateDirectory` | 自定义状态目录(测试或多实例部署时使用) |
### LicenseGuard(客户端主入口)
- `Check(options)` — 执行一次完整校验,返回 `LicenseState`
- `Check(options, now)` — 指定时间基准校验,便于接入可信时间源或编写确定性测试
- `EnsureLicensed(options)` — 校验失败时抛出 `LicenseException`
- `EnforceOrExit(options)` — 校验失败时写错误到 stderr 并以退出码 78 结束
- `Activate(licenseCode, options)` — 激活正式授权码
- `GetActivationRequestCode()` — 获取本机机器码
- `ResetLocalState(options)` — 清除本地状态(**仅供供应商指令下的售后流程**)
- `GetStateLocation(options)` — 获取状态存储位置,用于诊断
### LicenseIssuer(供应商签发,需要私钥)
- `GenerateKeyPair()` / `ValidateKeyPair(pub, priv)` — 生成与自检密钥对
- `CreateTrialToken(...)` — 签发试用策略令牌
- `IssueLicense(...)` — 按机器码签发正式授权码
- `CreateResetCode(...)` / `TryVerifyResetCode(...)` — 售后重置码
### LicenseManager(授权码生成与验证)
签发方 API(需私钥):`GenerateSm2KeyPair`、`GenerateLicense`
客户端 API(需公钥):`ValidateLicense`、`ActivateLicense`、`LoadAndValidateStoredLicense`、`CheckExpiringSoon`、`GetActivationRequestCode`
客户端方法一律以 `LicenseOptions` 为参数。这是刻意的约束:`AppId` 参与产品绑定校验,
若允许"只传公钥"的简写形式,产品绑定就无从执行,一份为产品 A 签发的授权码将能在产品 B 上使用。
### MachineFingerprint(机器指纹)
- `GetMachineId()` / `GetActivationRequestCode()` — 获取机器码(32 位 Crockford Base32,按 4 字符分组)
- `GetMachineFingerprint()` — 历史 SM3 指纹,**为兼容已发放授权而冻结算法,新代码不要使用**
- `MachineTagHint()` — 生成授权码中的机器标识提示值
- `IsMatch(binding)` — 校验绑定标识(恒定时间比较,同时兼容新机器码与历史指纹)
- `IsHardwareInfoReliable` — 硬件信息是否充足(容器环境下可能为 false)
## 🔧 支持的框架
| 框架版本 | 支持状态 | 包版本 |
| --- | --- | --- |
| .NET 10.0 | ✅ 支持 | 10.0.x |
| .NET 9.0 | ✅ 支持 | 9.0.x |
| .NET 8.0 | ✅ 支持 | 8.0.x |
| .NET 7.0 | ✅ 支持 | 7.0.x |
| .NET 6.0 | ✅ 支持 | 6.0.x |
| .NET 5.0 | ✅ 支持 | 5.0.x |
## 📝 依赖项
- **RuoVea.SM**:国密算法实现(SM2/SM3/SM4)
- **RuoVea.ExDto**:`RestfulResult` 数据传输对象
- **RuoVea.ExEnum**:`CodeStatus` 状态码
- **RuoVea.ExIdGen**:ID 生成
- **System.Management**:WMI 硬件信息采集(Windows)
## 🔒 明文与密文对照
授权码采用 **KM2** 格式(Base64Url 编码):
```
KM2 . . . <机器标识> . . .
```
- SM4 密钥不随授权码传输,而是由 `SM3(机器码 | nonce | KeyId)` 派生
- SM2 签名覆盖密文、IV、机器标识、nonce、keyId
- 旧版授权码(`Base64("<密文>.<签名>.")`)仍可解析,但新签发一律使用 KM2
## 🎯 应用场景
- **商业软件授权**:为客户端软件提供离线可用的试用与正式授权
- **企业内部软件**:部门级软件的使用期限管理
- **试用版管理**:2 年试用,到期后引导购买正式授权
- **按机器授权**:一码一机,防止授权在客户之间扩散
## 🤝 贡献指南
欢迎提交 Pull Request 和 Issue。请确保:
1. 代码符合项目风格
2. 保持代码风格一致
3. 添加必要的注释
4. 更新相关文档
## 📄 许可协议
本项目遵循 MIT 许可协议,详细信息请参见 [LICENSE](LICENSE) 文件。
## 🔗 相关资源
- [国家密码管理局](https://www.oscca.gov.cn)
- [国密算法标准文档](https://www.oscca.gov.cn)
- [NuGet 包页面](https://www.nuget.org/packages/RuoVea.KM)
- [Gitee 仓库](https://gitee.com/ruovea/ruovea-km)
## 📧 联系方式
如有问题或建议,欢迎通过以下方式联系:
- 提交 [Issue](https://gitee.com/ruovea/ruovea-km/issues)
- 发送邮件至项目维护者
***
**RuoVea.KM** - 让软件授权管理更简单!🇨🇳