1. 背景
双线性映射是现代密码学中的常用数学工具,经常被用来构造包括基于身份加密在内的各种密码算法。笔者近日开发的一款B/S架构支持基于身份加密和关键字可搜索加密的邮件系统就使用到了双线性映射,而通过搜索,发现浏览器中的常用前端语言JavaScript并没有提供文档与功能齐全的支持双线性映射的代码库可供使用。为了使开发工作可以正常进行,笔者另寻他路解决了这个问题,最终可以在Chrome浏览器实现需要的加密算法,本文就记录了问题的解决过程。但是这个解决方案并不通用,目前仅能在FireFox和Chrome浏览器中使用,在IE和Edge浏览器中则需要使用另外的技术。
1.1. 双线性映射
双线性映射的定义十分抽象,这里仅对其做简单介绍。
1.1.1. 定义
设G1、G2、GT都是阶p为素数的循环群,如果映射e:G1xG2→GT满足如下性质:
1、双线性:对于任意的g1∈G1,g1∈G1,a,b∈Zp,均有e(g1a,g2b)=e(g1,g2)ab成立;
2、非退化性:∃g1∈G1和g2∈G2,满足e(g1,g2)≠1GT;
3、可计算性:存在有效的算法,对于∀g1∈G1,g2∈G2,均可计算e(g1,g2)。
如果G1和G2群相同,就称双线性映射e是对称的,否则称其为非对称的。
另外,上述双线性映射中各群是素数阶的,其实在2005年boneh也实现了合数阶群的双线性映射,具体可以参考文献[1]。
1.1.2. 使用场景
双线性映射在现代密码学中应用广泛,比如基于身份加密[2]、基于属性加密[3]、组密钥分配协议[4]、基于属性签名[5]和公钥可搜索加密[6]。可以看到双线性映射对于现代密码学是多么重要。
1.1.3. 常用的函数库
目前有非常多的函数库提供了双线性映射的计算功能,如笔者最常用的PBC Library[7],这是一个开源的C语言项目。此外还有C语言的Miracl[8]、Relic[9],Java的jPBC[10]等。这些函数库都可以从互联网上进行下载并使用。
1.2. 浏览器
浏览器的编程环境比较单一,不像客户端或服务器开发中那样可以使用多种开源软件以及系统调用,因此其中的编程方式选择也很匮乏。
1.2.1. 编程环境及不足
浏览器中的编程环境,主要是前端语言HTML和JavaScript,对于FireFox和Chrome来说,还可以使用C++开发Native Client,对于IE来说可以使用C#或者C++开发ActiveX空间。其中Native Client中的代码主要是平台无关代码,其作用更多的是优化网页中特定组件的运行速度,如实时3D动画等。
对于HTML来说,其主要作用是绘制网页内容,并不具备执行复杂内容的能力;对JavaScript来说,存在面向JavaScript的双线性映射运算库jspairing[11],但是其已多年无人维护,并且缺乏文档和教程,缺少实用性;对Native Client来说,其更多的是面向代码的可移植性,对于已有代码的兼容性以及系统中动态库的调用流程没有过多考虑;而ActiveX技术则非常古老,其只对Windows平台中的IE浏览器兼容,并且缺乏足够的文档和教程。因此这些技术都被一一否定。
1.2.2. 其他选项
直接编写浏览器可用的功能代码是行不通的,那么就只能考虑其他选项了。这里我的选择是使用由FireFox和Chrome兼容的Native Messaging[12]技术。这项技术允许浏览器中的JavaScript代码与操作系统中的应用程序进行通信,从而实现调用操作系统中程序功能的目的。如果使用这项技术,就可以在应用程序中提供基于双线性映射的密码算法的底层功能,并由JavaScript实现上层接口,从而在浏览器中实现双线性映射的功能。
2. 方案实现
虽然Native Messaging可以帮助我们实现想要的功能,但是其本身的编程规范较为复杂,并且编程过程中需要注意的地方也很多,因此整个方案实现起来比较麻烦。这里使用Chrome浏览器的Native Messaging技术来进行实现,理论上Firefox也是兼容这套技术的,但是有些细节部分需要修改,这其中的细节笔者没有去求证。
2.1. 架构
Chrome的Native Messaging技术是依托于浏览器扩展的,也就是说与本地应用程序直接进行通信的是浏览器中的扩展程序,而网页中的JavaScript代码通过与扩展程序通讯,来对本地应用程序进行间接调用。其整个通信框架如图 1所示。
图 1 Native Messaging的通信框架[13]
从图中,可以看到,网页首先向浏览器扩展发送请求,接着扩展向本地应用程序发送请求,当本地应用程序将请求数据返回给扩展后,扩展再将请求结果返回给网页。因此在整个方案实现中,需要进行三个部分的开发:本地应用程序、浏览器扩展和网页代码。
2.2. 通信方式
在开始开发工作之前,首先要确定浏览器扩展与本地应用之间的通信方式,以确定通信的具体数据结构以及封装形式,并及早确定所选编程语言,避免后面遇到问题。
根据Native Messaging的官方文档,浏览器扩展和本地应用程序之间使用标准输入输出进行通信,通信的数据必须是JSON格式,并且本地应用程序在读取数据的时候,会先从标准输入中读取一个4字节的二进制数据,其代表后续JSON数据的长度;同样地,本地应用程序在向浏览器扩展返回结果时,也会先向标准输出中写入返回的数据的总长度,再将整个数据写入到标准输出中。同时,因为JSON数据格式不支持二进制字符,因此需要对通信过程中的二进制数据进行Base64编码。
2.3. 底层库编译
因为非常熟悉PBC的用法和文档,以及我手头上还有比较多的PBC代码,因此这里我选择PBC作为底层的函数库来使用。PBC的官方下载地址为https://crypto.stanford.edu/pbc/download.html,可以看到官方提供了源代码以及编译好的win32平台二进制dll文件。但是由于官方没有提供该二进制文件对应的符号文件,因此没有办法直接使用它来进行编程,因此需要自己动手进行编译。
在编译时使用MinGW32环境,这里说明一下,即使平台是64位Windows,也要使用32位的MinGW来进行编译,否则编译好的PBC库的数据输入输出会存在问题。
首先从中科大的开源软件站https://mirrors.ustc.edu.cn/msys2/distrib/ 下载最新的msys2安装包,接着一路点击下一步进行安装,安装完毕后,启动MinGW32.exe文件,并在弹出的shell窗口中输入以下命令,安装解压和编译gmp大数运算库所需的软件:
pacman –S mingw-w64-i686-gcc make mingw-w64-i686-gdb xzip
接着从https://gmplib.org/ 下载最新的gmp大数运算库。解压后从MinGW32 Shell进入其解压的目录。依次执行命令
./configure --enable-shared --disable-static make make install
来将gmp库编译为动态链接库dll,并将其安装到MinGW32的相关目录中。
编译完gmp后,执行命令
pacman –S flex bison
来安装编译pbc所需的软件,pbc的源码及依赖文件准备就绪后,就可以像gmp库一样依次执行命令
./configure --enable-shared --disable-static make make install
来将其编译为动态链接库并安装到MinGw32的相关目录中了。
2.4. 本地应用程序底层代码
本地应用程序分为两个部分:底层函数库和Python接口层。其中底层函数库使用C语言编写,Python接口层使用Python语言编写。这里就有两种实现方式:
- 将底层函数库编译为DLL动态连接库,通过Python的ctypes进行调用;
- 将底层函数库编译为Python模块,直接由Python来进行调用。
这两种方法中,第二种方法更加便捷,但是实现起来问题比较多,这里我将重点介绍第一种方法,第二种方法我会介绍一下要注意的地方。
2.4.1. 使用ctypes
ctypes是python内置的非常有用的模块,它能够赋予Python直接调用外部动态链接库的能力,并且提供了多种C语言数据类型,是利用Python兼容调用已有代码的非常好的工具。
这里挑选一个典型的函数来进行举例讲解。
设有一个C语言结构体和一个函数
typedef struct IBECipher_ { unsigned char *cip1, *cip2; int cip1_len, cip2_len; } IBECipher; int generate_ibe_cipher(char *receiver, IBECipher *cip, unsigned char *aes_key, int *aes_key_len);
其中结构体IBECipher中的cip1和cip2以及aes_key都是用于输出的,其所指空间在函数调用前就已经分配完毕,并且全部都是二进制数据。而receiver是传入的以NULL结尾的ASCII字符串,aes_key_len是指向一个整数的指针,用来表明aes_key的字节长度。
首先,要将包含函数generate_ibe_cipher的C语言实现文件编译为动态链接库。假设其文件名为foo.c,那么使用gcc来将其编译为动态链接库的命令为
gcc -shared foo.c -o foo.dll
这个命令会产生名为foo.dll的动态链接库,供其他程序调用了。
那么为了在Python中利用ctypes调用这个函数,还需要需要模拟4种数据结构:整数指针、以NULL结尾的C语言字符串、二进制缓冲区和C语言结构体。这里要说明的是,在C语言中,以NULL结尾的字符串与二进制缓冲区的表达都是字节数组,但是在python中两者是不一样的,因为二进制缓冲区中的数据是可能存在NULL字符的,此时如果将其看做以NULL结尾的字符串,就会导致数据的截断,从而造成错误。这四种C语言数据结构在Python中的表示为:
1、整数指针
整数指针非常简单,就是指向整数的指针,使用ctypes来表达的话,就是这样的
from ctypes import POINTER, c_int32
p_int = POINTER(c_int32)
其中p_int是一个整数指针的类型,如果要使用这个类型,需要用一个c_int32型的变量来对它进行初始化,也就是这样的
p_some_int = p_int(c_int32())
这样,p_some_int就是一个指向某个整数的指针了。
2、以NULL结尾的C语言字符串
以NULL结尾的C语言字符串非常简单,它在ctypes中的类型是c_char_p,如果将某个已有的Python字符串转换为以NULL结尾的C语言字符串,代码也非常简单
from ctypes import c_char_p
some_str = 'This is som string' p_some_ptr = c_char_p(some_str)
3、二进制缓冲区
前面说过,在C语言中二进制缓冲区和字符串没有本质上的区别,但是在Python中,这两者是有区别的,因为C语言字符串会被从NULL处截断。因此,为了与动态链接库交换任意二进制数据,就不能使用c_char_p来直接从Python中的字节数据来构造,而要使用另一个数据类型——字节数组。在ctypes中,定义一个数组类型非常简单,比如要定义一个有64个字节的整形数组,代码如下
from ctypes import c_int32
arr_64 = c_int32*64
这里的arr_64是一个数组类型,其中有64个整形元素。如果要定义一个此类型的变量,就直接使用代码
arr = arr_64()
这会将arr中所有的元素初始化为0,当然也可以提供部分初始化值,比如
arr = arr_64(1,2,3,4,5)
这会将arr中的前5个元素分别初始化为1,2,3,4,5.并且字节数组中的元素在Python中支持按下标读写,特别方便。那么定义字节数组的方式与定义整形数组的方式也是一样的
from ctypes import c_ubyte
arr_byte = c_ubyte*128
这个代码定义了长度为128字节的字节数组类型,如果要使用它就直接像使用整形数组那样就好
4、C语言结构体
ctypes也提供了定义C语言结构体的功能,以上面的IBECipher为例,在Python中,它的定义为
from ctypes import Structure, POINTER, c_ubyte, c_int32
class IBECipher(Structure): _fields_ = [ ("cip1", POINTER(c_ubyte)), ("cip2", POINTER(c_ubyte)), ("cip1_len", c_int32), ("cip2_len", c_int32) ]
p_IBECipher = POINTER(IBECipher)
就是说,结构体在ctypes中的定义是ctypes中Structure类的子类,并且自己按需填充其_feilds_字段就可以了。
接下来,就是要用ctype调用编译好的foo.dll中的函数了,代码如下
from ctypes import CDLL, PONTER, c_ubyte, c_int32, c_char_p, Structure dll_lib = CDLL('foo.dll') receiver = c_char_p('receiver') ibe_cipher = IBECipher() ibe_cipher.cip1 = arr_byte() ibe_cipher.cip2 = arr_byte() aes_key = arr_byte() aes_key_len = PONTER(c_int32)(c_int32()) dll_lib.generate_ibe_cipher(receiver, PONTER(IBECihper)(ibe_cipher), aes_key, aes_key_len)
执行完毕后就可以从ibe_cipher和aes_key中获取输出结果了。
这个方法的缺点是要对dll_lib中的函数进行封装,优点是可以直接使用现有的dll文件,而不需要专门为Python编写相应的版本。
如果要将编译好的dll模块拷贝出来,在MinGW32环境外使用,就需要将该dll以及它依赖的dll全部拷贝出来,这其中,出来gmp和pbc等编程库之外,还要拷贝libgcc_s_dw2-1.dll和libwinpthread-1.dll这两个MinGW32本身的依赖文件。
2.4.2. 编译为Python模块
将C语言代码编译为Python模块的方法,在网上有非常多的参考资料,基本上都是使用Python的distutils.core模块,编写Setup文件来编译安装模块的,但是在Windows平台上,就会出现几个问题:
1、所用编译器版本与当前系统中编译器的版本不匹配,找不到所需的lib导入库文件;
2、无法自动调用Visual Studio编译器;
3、没有安装Visual Studio编译器,无法编译。
也就是说,如果使用默认的方法,就需要安装对应版本的Visual Studio编译器或者对应版本的SDK,并且还要保证不出各种奇奇怪怪的错误。因此,这里介绍使用MinGW32中的gcc来编译模块的方法。
首先进入要使用的Python的安装目录,进入Lib/distutils子文件夹,找到cygwinccompiler.py文件,找到
if msc_ver == ‘1300’: # MSVC 7.0 returen [‘msvcr70’]
这一行,然后在这个判断语句中添加缺少的版本的判断,比如1900,并且将列表中的字符串msvcr*替换为任意可在MinGW32中找到的库,比如gmp。我这里的文件替换后的代码为
msc_pos = sys.version.find('MSC v.') if msc_pos != -1: msc_ver = sys.version[msc_pos+6:msc_pos+10] if msc_ver == '1300': # MSVC 7.0 return ['msvcr70'] elif msc_ver == '1310': # MSVC 7.1 return ['msvcr71'] elif msc_ver == '1400': # VS2005 / MSVC 8.0 return ['msvcr80'] elif msc_ver == '1500': # VS2008 / MSVC 9.0 return ['msvcr90'] elif msc_ver == '1600': # VS2010 / MSVC 10.0 return ['msvcr100'] elif msc_ver == '1900': return ['gmp'] else: raise ValueError("Unknown MS Compiler version %s " % msc_ver)
此时,再在MinGW32的shell中利用此Python来执行编译安装所用的setup文件,并传入参数--compiler=mingw32,即可完成编译,具体的命令行为
'/c/Program Files (x86)/Python36-32/python.exe' setup.py build --compiler=mingw32
此时可在生成的build子目录中的lib.w32-3.6目录中找到编译好的pyd文件,将此pyd文件与其依赖的动态链接库一起拷贝到对应的Python根目录,就可以直接在Python中导入其中内容了。
2.5. 本地应用程序接口代码、浏览器扩展和网页JavaScript
关于本地应用程序接口代码、浏览器扩展和网页JavaScript的编写,可以参考谷歌官方的教程,地址是https://developer.chrome.com/apps/nativeMessaging,这个教程中的本地应用程序也是Python编写的,因此与上面介绍的内容是很吻合的。这里只说几个很容易踩坑的地方:
1、启动本地应用程序的bat脚本,一定要在开头加上@echo off命令,因为脚本与浏览器扩展之间是通过标准输入与标准输出进行数据交换的,因此如果不关闭脚本的回显功能,那么脚本产生的输出就会被传递给本地应用程序,造成错误;
2、浏览器与本地应用程序之间交换的JSON数据必须是JSON的数组或者字典结构,不能单独传递数字或者字符串,并且具体的JSON内容与意义是由自己设计的;
3、在进行数据的读取和写入时,一定要先读写4个字节长度的数据长度整数值,这样好确定后续数据的长度,并能避免出错;
4、网页向浏览器扩展传递请求信息时,提供的JavaScript回调函数是一次性的,即最多只能使用一次,并且默认情况下,其生命周期是在浏览器扩展内的chrome.runtime.onMessageExternal.addListener中注册的监听器函数的内部,如果要想保留该回调函数,以等待处理结果产生后再返回数据,就要让监听器函数返回true值,并且即使如此,该回调函数也只能调用一次;
5、为了让网页调用浏览器扩展,需要提供浏览器扩展的ID,这个ID是在chrome在Debug下安装该扩展后就自动分配的,在扩展页面可以看到。
这样一来就可以通过由浏览器扩展中转请求的方式来在浏览器环境下实现双线性映射运算了。
参考文献
[1] Boneh, Dan, Eu-Jin Goh, and Kobbi Nissim. "Evaluating 2-DNF formulas on ciphertexts." Theory of Cryptography Conference. Springer, Berlin, Heidelberg, 2005.
[2] Boneh, Dan, and Matt Franklin. "Identity-based encryption from the Weil pairing." Annual international cryptology conference. Springer, Berlin, Heidelberg, 2001.
[3] Goyal, Vipul, et al. "Attribute-based encryption for fine-grained access control of encrypted data." Proceedings of the 13th ACM conference on Computer and communications security. Acm, 2006.
[4] Lee, Sangwon, et al. "An efficient tree-based group key agreement using bilinear map." International Conference on Applied Cryptography and Network Security. Springer, Berlin, Heidelberg, 2003.
[5] Shanqing, Guo, and Zeng Yingpei. "Attribute-based signature scheme." Information Security and Assurance, 2008. ISA 2008. International Conference on. IEEE, 2008.
[6] Boneh, Dan, et al. "Public key encryption with keyword search." International conference on the theory and applications of cryptographic techniques. Springer, Berlin, Heidelberg, 2004.
[7] Lynn, Ben. "PBC library." Online: http://crypto. stanford. edu/pbc (2006).
[8] MRACL Trust. “Miracl Library.” Online: https://github.com/miracl/MIRACL (2018).
[9] Aranha, Diego F. "RELIC is an Efficient LIbrary for Cryptography." http://code. google. com/p/relic-toolkit/ (2013).
[10] De Caro, Angelo. "jPBC Library." Online at http://libeccio. dia. unisa. it/projects/jpbc/download. html 67.
[11] jorgenhoc@gnamil.com. “jspairings.” Online: https://github.com/jorgenhoc/jspairings (2014).
[12] Chrom. “Native Messaging.” Online: https://developer.chrome.com/apps/nativeMessaging (2018).
[13] kagula. “Chrome Native Messaging技术示例.” Online: https://blog.csdn.net/lee353086/article/details/49362811 (2015).