0x01概述使用electron开发桌面程序似乎是WEB前端开发者转桌面的首选程序开发。最近,对在电子中使用加密狗有一些要求。学习了如何通过Node.js中的ffi-napi模块调用动态链接库,并用javascript封装了几个dongle产品的动态库,实现了使用加密锁的功能。开发过程中遇到了一些问题,踩了一些坑。这里做一个总结和记录。这里使用接口函数参数类型比较复杂的ROCKEY-ARM的动态链接库进行开发。注:本人分享了javascript封装的ROCKEY-ARM接口模块源码。如果只是需要在electron或者node.js项目中使用ROCKEY-ARM的网友,可以直接使用。#clone$gitclonehttps://github.com/youngbug/js-rockeyarm.git0x02准备工作1.安装依赖模块首先需要安装模块ffi-napi,ref-napi,ref-array-napi,ref-结构-napi。npminstallffi-napinpminstallref-napinpminstallref-array-napinpminstallstruct-napi下面简单介绍一下这几个模块的用途:ffi-napi:在javascript中调用动态链接库(.dll/.so),在UsethisNode.js中的模块,无需编写任何C/C++代码即可创建到本机库的绑定。ref-napi:该模块定义了很多常见的C/C++数据类型,在声明和调用动态库时可以直接使用。ref-array-napi:该模块提供了Node.js中的数组实现。在声明和调用函数中,所有指针都可以声明为uchar数组。ref-struct-napi:该模块提供了Node.js中的结构类型实现。ROCKEY-ARM函数的很多参数都是结构指针。如果声明了一个叫uchar的数组,那么传输的数据就是一个uchar的数组,解析的时候不方便。你需要自己拼接。除了麻烦,还需要考虑字节顺序的问题。如果使用结构体,定义一个结构体数组作为指针传入,函数返回的结构体参数可以直接用结构体解析,会更方便。2.要调用的动态库。购买飞天诚信的ROCKEY-ARM加密狗产品,可以获得ROCKEY-ARM的SDK,可以获得Windows和Linux的动态链接库。文件名一般为Dongle_d。而libRockeyARM.so.0.3.0x03声明的函数接口ffi-napi支持Windows和Linux系统,所以.dll和.so都可以支持,不同的操作系统可以加载不同的动态库文件。加载动态库的方法如下:import{Libraryasffi_Library}from'ffi-napi'libRockey=newffi_Library('d:/rockey/x64/Dongle_d.dll',rockeyInterface)Library()第一个参数为.dll路径,linux系统是.so的路径。第二个参数rokeyInterface是动态库导出函数的声明。ROCKEY-ARM的导出函数比较多,我单独定义一下。详情将在下文提及。1、声明几个简单的函数先从ROCKEY-ARM中找几个参数简单的函数声明一下。typedefvoid*DONGLE_HANDLE;DWORDWINAPIDongle_Open(DONGLE_HANDLE*phDongle,intnIndex);DWORDWINAPIDongle_ResetState(DONGLE_HANDLEhDongle);DWORDWINAPIDongle_Close(DONGLE_HANDLEhDongle);DWORDWINAPIDongle_GenRandom(DONGLE_HANDLEhDongle,intnLendom,BYTE);以上接口使用的数据类型有:DONGLE_HANDLE、DWORD、DONGLE_HANDLE、int、BYTE。查看ref-napi支持的ffi-napi支持的数据类型如下:void,int64,ushort,int,uint64,float,uint,long,double,int8,ulong,Object,uint8,longlong,CString,int16,char,byte,int32,uchar,size_t,uint32,short参数在这里应该使用相同长度的数据类型,可以有如下匹配。C类型长度ref-npai类型说明DONGLE_HANDLE4,8uintC定义为void*,是一个长度为4/8字节的指针。使用uintDONGLE_HANDLE*4,8ptrHandle定义指向DONGLE_HANDLE的指针。应该可以用uint,但是我没有测试int4intBYTE*4,8prtByte定义一个uchar的指针,应该可以用uint,但是我没有测试声明如下:constrockeyInterface={'Dongle_Open':['int',[ptrHandle,'int']],'Dongle_ResetState':['int',[ryHandle]],'Dongle_Close':['int',[ryHandle]],'Dongle_GenRandom':['int',[ryHandle,'int',ptrByte]]}一个json,key是动态库导出的函数名,比如'Dongle_Open',value是一个列表,第一个元素是返回值,第二个元素是参数。该参数仍然是一个列表。这个ref-napi里面有合适的类型,具体类型直接写就行了,比如返回值DWORD和传入的长度int,我这里用的是'int'。其他参数,我额外定义了句柄ryHandle,句柄指针ptrHandle,字节指针ptrByte。其中ryHandle、ptrryHandle、ptrByte定义如下:.uchar)2.DONGLE_HANDLE的void*类型参数本质上是void*类型。void*类型一开始试图定义一个void数组,然后用void数组来表示void,然后发现断言错误,数组不支持void类型。所以void指针直接用无符号数表示,64位系统是8字节,32位系统是4字节,uint类型就够了。DONGLE_HANDLE。3、结构体数组类型参数ROCKEY-ARM函数中有很多带参数的接口,如:typedefstruct{unsignedintbits;无符号整数模数;无符号字符指数[256];}RSA_PUBLIC_KEY;typedefstruct{unsignedintbitsunsignedintmodulus;unsignedcharpublicExponent[256];无符号字符指数[256];}RSA_PRIVATE_KEY;typedefstruct{unsignedshortm_Ver;无符号短m_Type;无符号字符m_BirthDay[8];无符号长m_IDong;m_UserID;无符号字符m_HID[8];无符号长m_IsMother;无符号长m_DevType;}DONGLE_INFO;DWORDWINAPIDongle_Enum(DONGLE_INFO*pDongleInfo,int*pCount);DWORDWINAPIDongle_RsaGenPubPriKey(DONGLE_HANDLEhDongle,WORDwPriFileID,RSA_PUBLIC_KEY*pPubBakup,RSA_PRIVATE_KEY*pPriBakup);以上两个函数接口为例,Dongle_Enum中的第一个参数是指向DONGLE_INFO结构体的指针,运行后返回一个设备信息列表。使用ROCKEY-ARM时,需要通过枚举函数获取设备信息列表,然后通过比较产品ID或硬件ID来决定打开哪个设备。为了方便从枚举函数返回的设备信息中解析出产品ID或硬件ID等信息,需要将参数DONGLE_INFOpDongleInfo声明为结构体数组。Dongle_RsaGenPubPriKey()函数中有RSA_PUBLIC_KEY和RSA_PRIVATE_KEIY*两个结构体指针参数,因为一般用户不需要解析RSA密钥中的n、d、e分量,直接做成字节数组声明即可直接转换成上面的ptrByte类型就可以了。所以声明如下:constref=require('ref-napi')constrefArray=require('ref-array-napi')constStructType=require('ref-struct-napi')vardongleInfo=StructType({m_VerL:ref.types.uchar,m_VerR:ref.types.uchar,m_Type:ref.types.ushort,m_BirthdayL:ref.types.uint32,m_BirthdayR:ref.types.uint32,m_Agent:ref.types.uint32,m_PID:ref.types.uint32,m_UserID:ref.types.uint32,m_HIDL:ref.types.uint32,m_HIDR:ref.types.uint32,m_IsMother:ref.types.uint32,m_DevType:ref.types.uint32})varptrInt=refArray(ref.types.int)varryHandle=refArray(ref.types.uint)varptrHandle=refArray(ryHandle)varptrDongleInfo=refArray(dongleInfo)varptrByte=refArray(ref.types.uchar)constrockeyInterface={'Dongle_Enum':['int',[ptrDongleInfo,ptrInt]],'Dongle_RsaGenPubPriKey':['int',[ryHandle,'ushort',ptrByte,ptrByte]]}0x04调用声明的函数调用ffi-napi声明的函数,主要是为自定义数据类型赋初值并获取自定义参数返回值说明如下。1.int*这里的int*是让函数返回设备的个数,或者传入的输入数据的长度或者传出的输出数据的长度,所以只要定义一个长度为1的int数组即可,如下:varpiCount=newptrInt(1)//piCount[0]=0给传入的数据赋值,只要给下标为0的元素赋值即可。2.参数DONGLE_INFO*为设备信息列表由枚举函数发送。由于枚举了多少设备,发出了多少DONGLE_INFO,所以必须传入足够数量的DONGLE_INFO,如下:libRockey.Dongle_Enum(null,piCount)//获取设备数量varDongleList=newptrDongleInfo(piCount[0])libRockey.Dongle_Enum(DongleList,piCount)console.log(DongleList[0].m_PID)//输出第一个设备枚举的PID3。BYTE*参数一般作为传入传出数据的缓冲区,所以在创建数组的时候需要创建足够长的空间,如下:varbuffer=newptrByte(len)0x05踩坑开发总结process过程中,花了很多时间踩了一些坑,所以这里总结一下。ROCKEY-ARM的结构是按字节对齐的,ref-struct-napi没有找到设置字节对齐的方法。当时声明的结构如下:vardongleInfo=StructType({m_VerL:ref.types.uchar,m_VerR:ref.types.uchar,m_Type:ref.types.ushort,m_Birthday:ref.types.uint64,m_Agent:ref.types.uint32,m_PID:ref.types.uint32,m_UserID:ref.types.uint32,m_HID:ref.types.uint64,m_IsMother:ref.types.uint32,m_DevType:ref.types.uint32})会找到测试时定义结构体的对齐方式和ROCKEY-ARM定义的结构体不一样,所以将m_Birthday和m_HID这两个成员从ref.types.uint64拆分成左右两个uint32,这样结构体的对齐方式可以做到和ROCKEY-ARM一致。在使用m_Birthday和m_HID时,需要将左右uint32进行拼接,有点麻烦,但是如果没有找到StructType对齐,保证结果正确是可以接受的。
