《中间件技术》实验3:跨语言加密解密调用

简单的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__':
#获取本机JVM路径
jvmPath = jpype.getDefaultJVMPath()
#开启JVM,调用编译好的jar包
jpype.startJVM(
jvmPath, "-ea", "-Djava.class.path=%s" %
('../out/artifacts/EncryptorDecryptor_jar/EncryptorDecryptor.jar'))
#指定要操作的类
MyAES = jpype.JClass("top.enatsu.MyAES")
... #调用MyAES类的方法。由于如前代码所示,AES加密解密均为静态方法,故不需创建MyAES实例
#关闭JVM
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,需要实现MyAESServiceIface接口中的AESEncodeAESEncode两个方法(减少耦合,直接在实现类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);
//指定Thrift二进制序列化协议。除TBinaryProtocol外还有压缩密集的TCompactProtocol等
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) #服务端的socket
transport = TTransport.TBufferedTransport(transport) #指定缓存传输
protocol = TBinaryProtocol.TBinaryProtocol(transport) #指定协议
client = MyAESService.Client(protocol)
transport.open()
... #具体调用client实例的方法
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)); //服务端的socket
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则可以用mavengradle。而之前C++一直没有较为合适的包管理工具。

如今,VC++可以使用微软开源的vcpkg管理C++程序包,从而省却了大量的解决方案配置。

本实验中需要的操作:

1
2
3
vcpkg install thrift
vcpkg install thrift:x64-windows
vcpkg integrate install <# 集成到全局,在vcpkg根目录的scripts\buildsystems下生成nuget配置文件 #>

打开 Visual Studio 2019,选择工具-NuGet包管理器-程序包管理器设置。在选项窗口中,点击NuGet包管理器-程序包源,添加vcpkg生成的NuGet配置文件路径。

nuget程序包源

最后管理NuGet解决方案包,安装vcpkg源下的配置文件,即可在C++代码中#include需要的Thrift头文件。

管理nuget解决方案包

L2语言的调用程序命令结构

首先输入命令。e编码,d解码,q输出。

接着依次输入编码规则和要编码/解码的字符串,输出相应的结果,并重新解码/编码,校验无误。

控制台输出示例

技术总结

本次实验实现了一个简单的AES加密/解密的跨语言调用编程。尝试多种方法,完成了实验所有要求。

跨语言开发,能充分利用不同语言的优势,提高开发上的效率;但另一方面,由于调用框架协议的限制,往往需要经历序列化/反序列化过程,乃至基于网络,跨语言调用的运行效率往往不如本语言原生调用来得高。

如果无法实现直接的跨语言调用,可以考虑读取控制台输出,或者设计必要的Web API。

本实验尚有以下不足之处:

  • 未研究其他的加密解密算法
  • 未尝试对文件的加密解密