概述

本小节将向您介绍如何通过 Runtime API 将软件和许可进行关联。本文会介绍最常规的许可访问操作,以及深度使用许可加解密对软件进行强关联的操作。内容的最后,还会介绍一些常见的案例场景的实现方法。

为了最直观的展示开发示例,我们将直接使用 C/C++ 语言书写示例,其他开发语言,请参考 SDK 安装目录下对应的示例代码。

使用 API 保护的优点

  • 可以灵活使用授权,自定义授权检测机制
  • 可以操作许可数据区
  • 可以自主操作许可加解密功能,保护软件关键数据
  • 实现代码移植,可以执行锁内关键算法程序
  • 可以根据自己的需求,设计不同的应用场景

写在前面

不同操作系统下的 Runtime API 库文件

库类型WindowsLinuxmacOSAndroid
动态库slm_runtime.dlllibslm_runtime.solibslm_runtime.dyliblibslm_runtime.so
静态库slm_runtime_api.liblibslm_runtime.alibslm_runtime.alibslm_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 接口访问许可,实现软件的使用条款限制,主要可以分为以下几步:

  1. 全局环境初始化
  2. 通过许可登录访问许可
  3. 通过心跳保持,保持许可会话的有效性
  4. 软件退出时登出许可
  5. 清理环境资源,防止资源泄露

示例代码

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;
}
CPP

如何使用许可加解密

如果在访问许可的过程中,使用到许可加解密操作,则相当于将软件和许可做了一个更深层次的保护。开发者在开发软件的过程中,可以将一些重要的数据使用许可密钥加密存储在软件或数据文件中,然后在软件运行的时候,使用再许可密钥解密运行,这样可以将软件和许可进行一次强行关联。有关许可密钥的描述请参考 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
... ...
CPP

如何应用许可数据区

软件在运行过程中,有些关键数据需要使用,有些是配置信息,需要启动时读取,有些是运行数据,需要在运行时记录。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;    
	}
}
CPP

更详细的示例代码请参考 Virbox SDK 安装目录下  /sdk/API/C/Sample/sample_03 。

业务场景举例

硬件锁绑定计算机使用

许多客户在使用 Virbox LM 产品时,希望其软件不仅针对许可进行关联,还要对用户使用的计算机进行关联,仅第一台使用的电脑可以访问许可,软件拷贝到其他电脑上,即便插入了加密锁或登录了云账号也不能使用。针对这种使用场景,我们提供了如下的解决方案供您参考。

我们前面说到过,每一条 Virbox 许可,都拥有三条数据区,读写区将是我们这个方案中用到的一个数据区,简单的步骤描述如下:

  1. 软件第一次启动,并成功访问许可
  2. 获取本地计算机硬件信息(最好是可以唯一标识该计算机的信息,如 MAC地址、主板ID 等)
  3. 将计算机硬件信息写入到读写区内
  4. 下次软件启动时,访问许可成功后,优先读取读写区的数据和本地计算机硬件信息做对比。若信息一致则软件可以正常启动;若信息不一致,则退出许可,关闭软件。

示例代码

	// 省略初始化、许可登录等操作
	... ... 
    // 这里以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;
	}
	... ...

CPP

查看加密锁内许可信息

通过如下代码,我们可以枚举当前计算机上插入的加密锁信息和锁内的许可信息

示例代码

... ...
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;
}

... ... 
CPP

许可功能模块区判断方法

每一条许可都有 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
	{
		// 出现其他错误,请查明原因
	}
}
CPP

许可过期后如何获取公开区数据

我们前面讲到过,公开区数据是可以直接公开给用户查看的,但是想要读取某条许可的公开区数据,也是需要在成功登陆该许可的情况下才能查看,那么当软件许可过期后,是否还能查到该条许可的公开区数据?

答案是可以的,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 ;
}
CPP

更多示例

更多保护案例和示例代码,请参考 Virbox SDK 安装目录下  /sdk/API/C/Sample/ 。

说明

如何快速找到示例代码?

首先,打开 Virbox 开发者工具盒

然后,找到 【API】 →【打开文件目录】,我们会打开 API 所在的目录

最后,依次打开 /C/Sample/ 文件夹即可看到所有示例代码

小结

程序编译好之后,我们的软件就和许可关联了起来,如何运行经过保护后的软件,可以查看 软件发布 相关的内容。