Virbox API 软件加密+授权
概述
本小节将向您介绍如何通过 Runtime API 将软件和许可进行关联。本文会介绍最常规的许可访问操作,以及深度使用许可加解密对软件进行强关联的操作。内容的最后,还会介绍一些常见的案例场景的实现方法。
为了最直观的展示开发示例,我们将直接使用 C/C++ 语言书写示例,其他开发语言,请参考 SDK 安装目录下对应的示例代码。
使用 API 保护的优点
- 可以灵活使用授权,自定义授权检测机制
- 可以操作许可数据区
- 可以自主操作许可加解密功能,保护软件关键数据
- 实现代码移植,可以执行锁内关键算法程序
- 可以根据自己的需求,设计不同的应用场景
写在前面
不同操作系统下的 Runtime API 库文件
库类型 | Windows | Linux | macOS | Android |
---|---|---|---|---|
动态库 | slm_runtime.dll | libslm_runtime.so | libslm_runtime.dylib | libslm_runtime.so |
静态库 | slm_runtime_api.lib | libslm_runtime.a | libslm_runtime.a | libslm_runtime.a |
动态调试库 | slm_runtime_dev.dll | -- | -- | -- |
静态调试库 | slm_runtime_api_dev.lib | -- | -- | -- |
说明
什么是动态调试库和静态调试库?
在 Windows 版本中,Runtime API 提供了 Runtime 库的发布版和调试版。为了提供更高的安全性,并持续和黑客进行对抗,Runtime 发布版带有反调试功能,和 Virbox 用户工具相互认证后启用反调试功能,禁止一切调试操作,包括开发过程中对本地业务代码的调试(Debug)操作。
为了方便开发者在开发过程中调试代码,我们提供了调试版本的 Runtime 库,调试版的开发库不具备反调试功能,不会对调试工具进行干扰,可正常调试。
您在开发过程中,可以对两种库文件做区分,开发时使用 slm_runtime_dev.dll 或 slm_runtime_api_dev.lib 进行开发,发布时使用 slm_runtime.dll 或 slm_runtime_api.lib。
基础应用
常规保护是指最基础的软件和许可进行关联,通过 API 接口访问许可,实现软件的使用条款限制,主要可以分为以下几步:
- 全局环境初始化
- 通过许可登录访问许可
- 通过心跳保持,保持许可会话的有效性
- 软件退出时登出许可
- 清理环境资源,防止资源泄露
示例代码
int main()
{
SS_UINT32 ret = SS_OK;
ST_INIT_PARAM init_param = {0};
ST_LOGIN_PARAM login_param = {0};
SLM_HANDLE_INDEX slm_handle = 0;
// 1.初始化(必须)
init_param.version = SLM_CALLBACK_VERSION02;
init_param.pfn = NULL;
init_param.timeout = 0;
// 设置API密码,访问 Virbox 开发者中心:https://developer.lm.virbox.com
memcpy(init_param.password, g_api_password, sizeof(g_api_password));
ret = slm_init(&(init_param));
if(SS_OK == ret)
{
printf("slm_init ok\n");
}
else
{
printf("slm_init error : 0x%08X\n", ret);
goto CLEAR;
}
// 2.登录许可
login_param.size = sizeof(ST_LOGIN_PARAM);
login_param.license_id = 1; // 登录访问许可ID
login_param.login_mode = SLM_LOGIN_MODE_AUTO; // 默认
login_param.timeout = 600; // 设置许可会话超时时间(单位:秒)
ret = slm_login(&login_param, STRUCT, &(slm_handle), NULL);
if(SS_OK == ret)
{
printf("slm_login ok\n");
}
else
{
printf("slm_login error : 0x%08X\n", ret);
goto CLEAR;
}
// 维持许可会话心跳。定时发送心跳包请求,维持当前会话可用,不会超时。
// 注意:许可使用过程中,建议使用独立线程自定义间隔调用slm_keep_alive(建议:600秒),
// 如果在维持心跳过程中发现错误,需要及时处理。
ret = slm_keep_alive(slm_handle);
if(SS_OK != ret)
{
printf("slm_keep_alive error : 0x%08X\n", ret);
}
else
{
printf("slm_keep_alive ok\n");
}
CLEAR:
// 4.许可使用完毕,登出许可,登出后不可执行与许可相关的操作。
if ( 0 != slm_handle )
{
ret = slm_logout(slm_handle);
if (SS_OK == ret)
{
printf("slm_logout ok.\n");
}
else
{
printf("slm_logout error : 0x%08X", ret);
}
}
// 5.清空初始化申请资源(必须),全局调用一次即可。
slm_cleanup();
printf("\npress any key exit process.\n");
getchar();
return ret;
}
如何使用许可加解密
如果在访问许可的过程中,使用到许可加解密操作,则相当于将软件和许可做了一个更深层次的保护。开发者在开发软件的过程中,可以将一些重要的数据使用许可密钥加密存储在软件或数据文件中,然后在软件运行的时候,使用再许可密钥解密运行,这样可以将软件和许可进行一次强行关联。有关许可密钥的描述请参考 Virbox 许可体系简介 中的描述,这里不多说。
示例代码
... ...
// 开发时,对程序数据代码加密
char *key = "0123456789abcdef"; // 加解密使用AES对称加密,明文数据必须16字节对齐
SS_BYTE enc[16] = {0};
ret = slm_encrypt(slm_handle, key, enc, sizeof(enc));
... ...
// 软件运行时,解密数据
char key[16] = {0};
status = slm_decrypt(slm_handle, enc, key, sizeof(key));
// 程序中使用 key
... ...
如何应用许可数据区
软件在运行过程中,有些关键数据需要使用,有些是配置信息,需要启动时读取,有些是运行数据,需要在运行时记录。Virbox LM 的每一条许可都提供了 三个数据区,即:公开区、只读区、读写区。
公开区一般用于存储一些公共可见的数据,例如产品信息,产品版本等,是可以直接向最终用户展现的数据。修改公开区只能通过远程升级的形式进行修改。
只读区一般存储一些配置信息和关键数据信息,一般不向用户展现。修改只读区只能通过远程升级的方式进行修改。
读写区一般存储软件运行过程中产生的数据信息,可以存储在加密锁中。只有读写区可以在软件运行过程中被写入数据。
关于软件三区数据的配置,可以参考 硬件锁许可数据区使用流程 和 云、软许可数据区使用流程。下面的例子讲解如何使用三区数据,而本文后续的内容 绑定机器信息 则是对于 读写区 的实际应用。
示例
{
....
/************************************************************************/
/*
只读数据区(ROM)
可以用此区作软件所需要的数据的存储,例如一些配置信息。
只读区较读写区更为常用,原因是所存储的内容不会在未授权的情况下被任意更改
更改只读区内容只有一种方法:远程升级包
*/
/************************************************************************/
ret = slm_user_data_getsize(slm_handle, ROM, &ulROWLen);
if(SS_OK != ret)
{
printf("slm_user_data_getsize[ROM] error : 0x%08X\n", ret);
goto CLEAR;
}
else
{
printf( "slm_user_data_getsize[ROM] ok rom_size : %d\n", ulROWLen );
}
if (ret == SS_OK && ulROWLen > 0)
{
pData = (SS_BYTE *)calloc(sizeof(SS_BYTE), ulROWLen);
ret = slm_user_data_read(slm_handle, ROM, pData, 0, ulROWLen);
if(SS_OK != ret)
{
printf("slm_user_data_read[ROM] error : 0x%08X\n", ret);
goto CLEAR;
}
else
{
printf( "slm_user_data_read[ROM] ok\n" );
}
// 可在此处理获取到的数据
free(pData);
pData = NULL;
}
/************************************************************************/
/*
读写数据区(RAW)
读写区可以让开发商把运行过程中需要保存的数据保存在锁内,下次启动的时候可以访问。
访问之前一定要先登录到许可,如果许可失效,则无法读取使用任何该内存
*/
/************************************************************************/
// 1、RAW数据区写入(使用偏移进行写入演示)
ret = slm_user_data_getsize(slm_handle, RAW, &ulRAWLen);
if(SS_OK != ret)
{
printf("slm_user_data_getsize[RAW] error : 0x%08X\n", ret);
goto CLEAR;
}
else
{
printf( "slm_user_data_getsize[RAW] ok ram_size : %d\n", ulRAWLen );
}
iOffset = 0;
ret = slm_user_data_write(slm_handle, (SS_BYTE*)testWrite, iOffset, 5);
if(SS_OK != ret)
{
printf("slm_user_data_write[RAW][OFFSET] offset=0 error : 0x%08X\n", ret);
goto CLEAR;
}
iOffset += 5;
ret = slm_user_data_write(slm_handle, (SS_BYTE*)testWrite+5, iOffset, 4);
if(SS_OK != ret)
{
printf("slm_user_data_write[RAW][OFFSET] offset=5 error : 0x%08X\n", ret);
goto CLEAR;
}
printf( "slm_user_data_write[RAW][OFFSET]: %s\n", testWrite);
ret = slm_user_data_read(slm_handle, RAW, testdata, 0, 9);
if(SS_OK != ret)
{
printf("slm_user_data_read[RAW][OFFSET] error : 0x%08X\n", ret);
goto CLEAR;
}
else
{
printf("slm_user_data_read[RAW][OFFSET]: %s\n",testdata);
}
// 2、RAW数据区写入(短数据、长数据写入演示),每次最多可写入 SLM_MAX_WRITE_SIZE(1904)字节
//短数据写入
memset(testdata, '1', sizeof(testdata) );
ulTestData = 10;
if( ulTestData <= SLM_MAX_WRITE_SIZE )
{
ret = slm_user_data_write(slm_handle, testdata, 0, ulTestData);
if(SS_OK != ret)
{
printf("slm_user_data_write[RAW][SHORT] error : 0x%08X\n", ret);
goto CLEAR;
}
else
{
printf("slm_user_data_write[RAW][SHORT] ok\n");
}
}
//长数据写入
ulTestData = ulRAWLen;
if( ( ulTestData >= SLM_MAX_WRITE_SIZE ) && ( ulTestData <= ulRAWLen ) )
{
nCount = ulTestData / SLM_MAX_WRITE_SIZE;
nRemainLen = ulTestData % SLM_MAX_WRITE_SIZE;
for(index = 0; index < nCount; index++ )
{
ret = slm_user_data_write(slm_handle, &testdata[SLM_MAX_WRITE_SIZE*index], SLM_MAX_WRITE_SIZE * index, SLM_MAX_WRITE_SIZE);
if(SS_OK != ret)
{
printf("slm_user_data_write[RAW][LONG] %d error : 0x%08X\n", index, ret);
goto CLEAR;
}
}
if( nRemainLen )
{
ret = slm_user_data_write(slm_handle, &testdata[SLM_MAX_WRITE_SIZE*index], SLM_MAX_WRITE_SIZE * index, nRemainLen);
if(SS_OK != ret)
{
printf("slm_user_data_write[RAW][LONG](remain) error : 0x%08X\n", index, ret);
goto CLEAR;
}
}
}
// 3、RAW数据区读取(短数据、长数据写读取演示),每次最多可读取 MAX_USER_DATA_SIZE(64*1024) 字节
//短数据读取
memset(testdata, 0, sizeof(testdata) );
ulTestData = 10;
if( ulTestData <= MAX_USER_DATA_SIZE )
{
ret = slm_user_data_read(slm_handle, RAW, testdata, 0, ulTestData);
if(SS_OK != ret)
{
printf("slm_user_data_read[RAW][SHORT] error : 0x%08X\n", ret);
goto CLEAR;
}
}
//长数据读取
ulTestData = ulRAWLen;
if( ( ulTestData >= MAX_USER_DATA_SIZE ) && ( ulTestData <= ulRAWLen ) )
{
nCount = ulTestData / MAX_USER_DATA_SIZE;
nRemainLen = ulTestData % MAX_USER_DATA_SIZE;
for(index = 0; index < nCount; index++ )
{
ret = slm_user_data_read(slm_handle, RAW, &testdata[MAX_USER_DATA_SIZE*index], MAX_USER_DATA_SIZE * index, MAX_USER_DATA_SIZE);
if(SS_OK != ret)
{
printf("slm_user_data_read[RAW][LONG] %d error : 0x%08X\n", index, ret);
goto CLEAR;
}
}
if( nRemainLen )
{
ret = slm_user_data_read(slm_handle, RAW, &testdata[MAX_USER_DATA_SIZE*index], MAX_USER_DATA_SIZE * index, nRemainLen);
if(SS_OK != ret)
{
printf("slm_user_data_read[RAW][LONG](remain) error : 0x%08X\n", index, ret);
goto CLEAR;
}
}
}
/************************************************************************/
/*
全局数据区(PUB)
全局数据区和ROM区数据访问区别是:全局数据区数据是绑定在0号许可的
当用户不希望通过登录相应的许可,从而获取到公开区的时候
用户可用通过调用slm_pub_data_getsize和slm_pub_data_read进行访问,前提是需要登录0号许可,代码演示在下方子函数pub_data_demo
更改全局数据区内容只有一种方法:远程的安全升级包(详见许可签发DEMO模块)
*/
/************************************************************************/
ret = slm_user_data_getsize(slm_handle, PUB, &ulPUBLen);
if (ret == SS_OK && ulPUBLen > 0)
{
pData = (SS_BYTE *)calloc(sizeof(SS_BYTE), ulPUBLen);
ret = slm_user_data_read(slm_handle, PUB, pData, 0, ulPUBLen);
if(SS_OK != ret)
{
printf("slm_user_data_read[PUB] error : 0x%08X\n", ret);
goto CLEAR;
}
// 可在此处理获取到的数据
free(pData);
pData = NULL;
}
}
更详细的示例代码请参考 Virbox SDK 安装目录下 /sdk/API/C/Sample/sample_03 。
业务场景举例
硬件锁绑定计算机使用
许多客户在使用 Virbox LM 产品时,希望其软件不仅针对许可进行关联,还要对用户使用的计算机进行关联,仅第一台使用的电脑可以访问许可,软件拷贝到其他电脑上,即便插入了加密锁或登录了云账号也不能使用。针对这种使用场景,我们提供了如下的解决方案供您参考。
我们前面说到过,每一条 Virbox 许可,都拥有三条数据区,读写区将是我们这个方案中用到的一个数据区,简单的步骤描述如下:
- 软件第一次启动,并成功访问许可
- 获取本地计算机硬件信息(最好是可以唯一标识该计算机的信息,如 MAC地址、主板ID 等)
- 将计算机硬件信息写入到读写区内
- 下次软件启动时,访问许可成功后,优先读取读写区的数据和本地计算机硬件信息做对比。若信息一致则软件可以正常启动;若信息不一致,则退出许可,关闭软件。
示例代码
// 省略初始化、许可登录等操作
... ...
// 这里以MAC地址作为硬件唯一标识,写入到RAW区
char *mac_addr = "AA-BB-CC-DD-EE-FF";
// 1. 程序第一次运行时
ret = slm_user_data_getsize(slm_handle, RAW, &length);
if (ret == SS_OK && length> 0 && length>= strlen(mac_addr) )
{
ret = slm_user_data_write(slm_handle, (SS_BYTE*)mac_addr, 0, strlen(mac_addr) );
printf( "slm_user_data_write(RAW) : %s\n\n", mac_addr);
}
else
{
printf("RAW size is too small to write PC_MAC\n");
return -1;
}
... ...
// 2、程序再次运行,再次获取PC_MAC信息,并读取出之前写入RAW区的PC_MAC信息
char *mac_addr = "AA-BB-CC-DD-EE-FF";
SS_BYTE pData[64] = {0};
ret = slm_user_data_read(slm_handle, RAW, pData, 0, strlen(mac_addr));
printf( "\nslm_user_data_read(RAW) : %s\n\n", pData);
//3、对比信息,如相同,则证明加密锁对应此PC,允许使用,否则不允许使用
if( strcmp(mac_addr, (char*)pData) == 0 )
{
printf( "the sense device can use in this computer!\n");
}
else
{
printf( "the sense device can not use in this computer!\n");
return -1;
}
... ...
查看加密锁内许可信息
通过如下代码,我们可以枚举当前计算机上插入的加密锁信息和锁内的许可信息
示例代码
... ...
char *lic_id = NULL;
char *dev_desc = NULL;
char *lic_info = NULL;
SS_UINT32 ret = SS_OK;
Json::Reader reader; // 此处选择jsoncpp处理json数据
Json::Value root;
Json::Value lic;
ret = slm_enum_device(&dev_desc); // 首先要先遍历所有设备
if ( status == SS_OK && dev_desc != NULL && reader.parse(dev_desc, root))
{
for (int i = 0; i < root.size(); i++)
{
// 其次获取每个设备的许可ID
status = slm_enum_license_id(root[i].toStyledString().c_str(), &lic_id);
if (status == SS_OK && lic_id != NULL)
{
printf(lic_id);
printf("\n");
if (reader.parse(lic_id, lic))
{
for (int j = 0; j < lic.size(); j++)
{
// 最后获取许可的详细信息
status = slm_get_license_info(root[i].toStyledString().c_str(), lic[j].asInt(), &lic_info);
if (lic_info)
{
printf(lic_info);
printf("\n");
slm_free(lic_info);
lic_info = NULL;
}
}
}
slm_free(lic_id);
lic_id = NULL;
}
}
slm_free(dev_desc);
dev_desc = NULL;
}
... ...
许可功能模块区判断方法
每一条许可都有 64 个功能模块可以做限制,开发商在许可中设置好每个功能模块的值,在调用 API slm_check_module 时判断模块区是否存在,通过此功能来限制软件的模块是否可用。
示例代码
void function_1(SLM_HANDLE_INDEX handle, uint32_t mid)
{
int r = 0;
r = slm_check_module(handle, mid); // 检查该模块是否在许可中存在,mid:1--64
if(r == SS_OK)
{
// 模块区存在,可以执行此功能
}
else if(r == SS_ERROR_LICENSE_MODULE_NOT_EXISTS)
{
// 模块区不存在,此功能不可用
}
else
{
// 出现其他错误,请查明原因
}
}
许可过期后如何获取公开区数据
我们前面讲到过,公开区数据是可以直接公开给用户查看的,但是想要读取某条许可的公开区数据,也是需要在成功登陆该许可的情况下才能查看,那么当软件许可过期后,是否还能查到该条许可的公开区数据?
答案是可以的,Runtime API 提供了免登录相关许可,即可获取该许可公开区数据的应用接口,开发者可以通过登录0号许可(请查看 0号许可),使用 0 号许可的登录状态,通过接口 slm_pub_data_read 来获取公开区数据。请看下面示例代码。
示例代码
{
// 省略初始化等其他操作
// ...
// 假如 1 号许可已过期,如何获取 1 号许可的公开区数据
SS_UINT32 license_1 = 1;
// 安全登录许可 0
ST_LOGIN_PARAM login_param = {0};
login_param.license_id = 0;
login_param.size = sizeof(ST_LOGIN_PARAM);
login_param.timeout = 86400;
ret = slm_login( &login_param, STRUCT, &(slm_handle), NULL );
ret = slm_pub_data_getsize(slm_handle, ulLicenseID, &ulPUBLen);
if (ret == SS_OK && ulPUBLen > 0)
{
pData = (SS_BYTE *)calloc(sizeof(SS_BYTE), ulPUBLen);
ret = slm_pub_data_read(slm_handle, license_1, pData, 0, ulPUBLen);
// 可在此处理获取到的数据
free(pData);
pData = NULL;
}
if ( 0 != slm_handle )
{
slm_logout(slm_handle);
}
slm_cleanup();
CLEAR:
printf("slm_pub_data_getsize/read(PUB) ok\n");
return ;
}
更多示例
更多保护案例和示例代码,请参考 Virbox SDK 安装目录下 /sdk/API/C/Sample/ 。
说明
如何快速找到示例代码?
首先,打开 Virbox 开发者工具盒
然后,找到 【API】 →【打开文件目录】,我们会打开 API 所在的目录
最后,依次打开 /C/Sample/ 文件夹即可看到所有示例代码
小结
程序编译好之后,我们的软件就和许可关联了起来,如何运行经过保护后的软件,可以查看 软件发布 相关的内容。