简单的AES,雕虫小技的跨语言调用……
引言
实验目的:搜索跨语言开发调用的常用框架/库,阅读文档;实现简单的跨语言调用。
实验内容:
一个功能A,用的是L1语言进行编程实现的;请把该功能,在L2语言的环境下进行调用/合并,并能正确的返回结果。
请先自己编写或找到实现A功能的代码,或仅有可执行文件,并进行跨语言开发。
多语言开发一般基于第三方的库或解决方案。
(2.3 A:加密和解密功能, L1: Java, L2:C++ 和 Python)
问题分析
首先,如果不考虑利用其他框架来进行跨语言调用,最简单的办法自然是直接调用控制台输出。或者,设计RESTful API也可满足要求。
不过,对于Python调用Java,我们容易找到jpype
这个工具,通过开启JVM,调用已经编译好的jar包中的类。另外,C++也可利用JNI来调用jar包。
此外,通过Apache Thrift这一个远程过程调用(RPC)框架,约定传输的类,建立相应的C/S程序,我们也可以很容易地实现跨语言的编程与调用。
对于加密和解密功能,由于Base64过于简单,这里选择了AES加密算法。通过约定相同的密钥进行加密和加密。
项目仓库地址:https://github.com/unsioer/EncryptorDecryptor
环境
编译器:
- JDK 11
- MSVC 14
- Python 3.8
- Thrift 1.4
IDE:
- IDEA 社区版(Java代码开发)
- Visual Studio 2019(C++代码开发)
- Visual Studio Code(Python代码开发)
包管理工具:
- maven(Java)
- vcpkg(C++)
- pip(Python)
详细设计
概要
如下表所示。
Java |
C++ |
Python |
实现了Base64和AES加密/解密算法。 选用AES测试跨语言调用实验。 1. 将项目打包成jar包,供Python的jpype 调用其中的MyAES 类。 2. 通过Thrift生成代码,编写相应的服务端(供C++和Python调用)和客户端程序。 |
通过Thrift生成代码,编写客户端程序调用Java的方法。 |
1. 通过jpype 调用Java打包生成的的jar包中的类。 2. 通过Thrift生成代码,编写客户端程序调用Java的方法。 |
附注:Base64在Java中的实现
首先简要介绍Base64。Base64就是一种基于64个可打印字符来表示二进制数据的方法。采用Base64编码具有不可读性,需要解码后才能阅读。它要求每三个8Bit的字节转换为四个6Bit($2^6=64$)的字节,然后把6Bit再添两位高位0,组成四个8Bit的字节。最后得到的每个字节数值范围为00000000~00111111,依次转换为大写字母、小写字母、数字(0-9)、+和/,共计26+26+10+1+1=64个字符。原文的字节数量如果是3的倍数,自然按上述规则转换;如不满足,则将剩余的字节按原有规则转换(余1字节,转2字节;余2字节,转3字节。每个字节最高2位补零,剩余凑不满6的倍数的低位也补零),最后用2个或1个=号补满四个字节。
我们可以直接调用java.util.Base64
的静态方法实现Base64的功能。读取内容为字符串,故统一先作UTF-8编码。由于编码方法简单,也不算真正意义上的加/解“密”,故本次实验不对其做跨语言调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import java.nio.charset.StandardCharsets; import java.util.Base64;
public class MyBase64 { static String encode(String content) { Base64.Encoder encoder = Base64.getEncoder(); byte[] contentByte = content.getBytes(StandardCharsets.UTF_8); return encoder.encodeToString(contentByte); }
static String decode(String content) { Base64.Decoder decoder = Base64.getDecoder(); byte[] contentByte = content.getBytes(StandardCharsets.UTF_8); return new String(decoder.decode(contentByte), StandardCharsets.UTF_8); } }
|
AES在Java中的实现
密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法。该算法为比利时密码学家Joan Daemen和Vincent Rijmen所设计,结合两位作者的姓氏,以Rijdael命名。AES采用置换-组合架构进行加解密。严格地说,AES只是Rijndael的特例。AES的区块长度固定为128位,密钥长度则可以是128,192或256位;而Rijndael使用的密钥和区块长度可以是32位的整数倍,以128位为下限,256位为上限。AES加解密过程中使用的密钥是由Rijndael密钥生成方案产生。
下面的AES加密/解密代码流程如下:
首先把encodeRules
作为随机数种子,生成128位密钥。
然后,加密过程对content
字符串进行加密,将最后的字节编码结果转换为Base64串输出;
解密过程则相反,先把content
(Base64串)解码成字节内容,最后通过AES解码输出结果。
这里尝试的都是字符串,故对编码输入和解码输出均按UTF-8编码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| import javax.crypto.*; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Base64;
public class MyAES { public static String AESEncode(String encodeRules, String content) { try { KeyGenerator keygen = KeyGenerator.getInstance("AES"); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.setSeed(encodeRules.getBytes()); keygen.init(128, random); SecretKey originalKey = keygen.generateKey(); byte[] raw = originalKey.getEncoded(); SecretKey key = new SecretKeySpec(raw, "AES"); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] contentByte = content.getBytes(StandardCharsets.UTF_8); byte[] aesByte = cipher.doFinal(contentByte); Base64.Encoder encoder = Base64.getEncoder(); return encoder.encodeToString(aesByte); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchPaddingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } return null; }
public static String AESDecode(String encodeRules, String content) { try { KeyGenerator keygen = KeyGenerator.getInstance("AES"); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.setSeed(encodeRules.getBytes()); keygen.init(128, random); SecretKey originalKey = keygen.generateKey(); byte[] raw = originalKey.getEncoded(); SecretKey key = new SecretKeySpec(raw, "AES"); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, key); Base64.Decoder decoder = Base64.getDecoder(); byte[] contentByte = decoder.decode(content.getBytes()); byte[] aesByte = cipher.doFinal(contentByte); return new String(aesByte, StandardCharsets.UTF_8); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchPaddingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } return null; } }
|
jpype
对jar包的调用
JPype是一个能够让Python代码方便地调用Java代码的工具,它的原理即调用JAVA虚拟机,并将Java方法的返回类型转换为Python类型。
本实验的jpype调用格式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import jpype import os
if __name__ == '__main__': jvmPath = jpype.getDefaultJVMPath() jpype.startJVM( jvmPath, "-ea", "-Djava.class.path=%s" % ('../out/artifacts/EncryptorDecryptor_jar/EncryptorDecryptor.jar')) MyAES = jpype.JClass("top.enatsu.MyAES") ... jpype.shutdownJVM()
|
Thrift介绍
Thrift是一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务,可视为一种远程过程调用(RPC)框架。它通过一个代码生成引擎联合了一个软件栈,来创建不同程度的、无缝的跨平台高效服务。它最早有Facebook开发,后来Facebook把它捐献给Apache软件基金会作为开源项目。
本实验需要编写thrift脚本:
1 2 3 4
| service MyAESService { string AESEncode(1:string encodeRules, 2:string content) string AESDecode(1:string encodeRules, 2:string content) }
|
将脚本保存为MyAESService.thrift
,执行以下指令,thrift自动生成目标语言的代码,分别保存在gen-java
/gen-cpp
/gen-py
文件夹。
1 2 3
| thrift --gen java MyAESService.thrift thrift --gen cpp MyAESService.thrift thrift --gen py MyAESService.thrift
|
在对应代码基础上实现即可。
对于作为服务端的Java,需要实现MyAESService
的Iface
接口中的AESEncode
和AESEncode
两个方法(减少耦合,直接在实现类MyAESImpl
中调用前文所述方法即可)。
服务器端启动服务的代码结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import org.apache.thrift.TProcessor; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.server.TServer; import org.apache.thrift.server.TSimpleServer; import org.apache.thrift.transport.TServerSocket; import top.enatsu.thrift.common.MyAESImpl; import top.enatsu.thrift.common.MyAESService;
public class ServerMain { public static final int SERVER_PORT = 8090;
public void startServer() { try { System.out.println("Start ...."); TProcessor tprocessor = new MyAESService.Processor<MyAESService.Iface>(new MyAESImpl());
TServerSocket serverTransport = new TServerSocket(SERVER_PORT); TServer.Args tArgs = new TServer.Args(serverTransport); tArgs.processor(tprocessor); tArgs.protocolFactory(new TBinaryProtocol.Factory());
TServer server = new TSimpleServer(tArgs); server.serve();
} catch (Exception e) { System.out.println("Server start error!!!"); e.printStackTrace(); } } ... }
|
相应的客户端代码结构如下:
Python:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| from MyAESService import MyAESService from thrift import Thrift from thrift.transport import TSocket from thrift.transport import TTransport from thrift.protocol import TBinaryProtocol
if __name__ == '__main__': transport = TSocket.TSocket('localhost', 8090) transport = TTransport.TBufferedTransport(transport) protocol = TBinaryProtocol.TBinaryProtocol(transport) client = MyAESService.Client(protocol) transport.open() ... transport.close()
|
C++:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include "MyAESService.h" #include <thrift/protocol/TBinaryProtocol.h> #include <thrift/transport/TSocket.h> #include <thrift/transport/TBufferTransports.h>
#include <iostream>
using namespace apache::thrift; using namespace apache::thrift::protocol; using namespace apache::thrift::transport;
std::string serverIP = "localhost"; int serverPort = 8090;
std::string AESEncode(std::string encodeRules, std::string content) {
std::shared_ptr<TSocket> socket(new TSocket(serverIP, serverPort)); std::shared_ptr<TTransport> transport(new TBufferedTransport(socket)); std::shared_ptr<TProtocol> protocol(new TBinaryProtocol(transport)); transport->open(); MyAESServiceClient* client=new MyAESServiceClient(protocol); std::string _return; client->AESEncode(_return,encodeRules, content); transport->close();
return _return; }
|
附注:vcpkg
简介
我们再考虑安装包管理。Python的官方包管理工具为pip
,也可用conda
等;Java则可以用maven
或gradle
。而之前C++一直没有较为合适的包管理工具。
如今,VC++可以使用微软开源的vcpkg
管理C++程序包,从而省却了大量的解决方案配置。
本实验中需要的操作:
1 2 3
| vcpkg install thrift vcpkg install thrift:x64-windows vcpkg integrate install
|
打开 Visual Studio 2019,选择工具-NuGet包管理器-程序包管理器设置。在选项窗口中,点击NuGet包管理器-程序包源,添加vcpkg
生成的NuGet配置文件路径。
最后管理NuGet解决方案包,安装vcpkg
源下的配置文件,即可在C++代码中#include
需要的Thrift头文件。
L2语言的调用程序命令结构
首先输入命令。e
编码,d
解码,q
输出。
接着依次输入编码规则和要编码/解码的字符串,输出相应的结果,并重新解码/编码,校验无误。
技术总结
本次实验实现了一个简单的AES加密/解密的跨语言调用编程。尝试多种方法,完成了实验所有要求。
跨语言开发,能充分利用不同语言的优势,提高开发上的效率;但另一方面,由于调用框架协议的限制,往往需要经历序列化/反序列化过程,乃至基于网络,跨语言调用的运行效率往往不如本语言原生调用来得高。
如果无法实现直接的跨语言调用,可以考虑读取控制台输出,或者设计必要的Web API。
本实验尚有以下不足之处: