刘 聪
(德州学院 信息管理学院,山东 德州 253000)
分组密码“Kuznyechik”是俄罗斯密码标准算法集合(称为GOST算法)之一。该算法能够保证信息在传输过程中的机密性和完整性,它符合现代密码的要求,具有安全、高效率、易用和灵活等优点。适用于开发、运用到各种用途的现代信息系统中,保护数据的安全。为了提高Kuznyechik算法在实际应用中的加密速度,本文根据Kuznyechik算法的特点,与Java线程池技术结合,提出采用线程池技术并行加密Kuznyechik的方法。
通常情况下,加密程序都是单线程运行的。为了提高程序的加密速度,可以设置多个线程去运行。Java语言对多线程提供了非常优秀的支持,基本的Java线程模型有Thread类、Runnable接口、Callable接口、Future接口等。
线程池就是在程序运行时创建一定数量的线程保存起来,当需要时,从线程池中取出一个线程。完成相应任务后再放回到线程池中。通过线程池,可以有效降低资源消耗、提高程序响应速度、提高线程的可管理性。根据线程池的工作原理,算法程序需要线程池管理器、工作线程、任务队列、数据加密实体几个部分。
Kuznyechik在俄罗斯联邦GOST R 34.12-2015的国家标准中定义,它有128比特的块大小和256比特的密钥长度。该算法是俄罗斯密码标准算法集合(称为GOST算法)之一。该算法可以在硬件和软件中实现,能够保证信息在传输过程中的机密性和完整性,它符合现代密码的要求。该标准适用于开发,运用到各种用途的现代信息系统。
该算法包括密钥编排和加密两部分,密钥编排算法是Feistel结构的,有左右两支,在密钥编排算法中,主密钥k为256比特,分为k1和k2,各128比特。首先k1与常数c1相加,将得到的结果经过s盒,L置换,然后与k2相加,得左支到k1′,之前k1的值变成k2′。密钥编排流程如图1所示:
图1 密钥编排流程图
经过8轮相同的运算,最后得到编排后的密钥k3和k4,然后以同样的方式生成k5和k6、k7和k8、k9和k10。其中L置换经过下面公式得到:
其中a是128位的,经过线性反馈移位寄存器转16拍,其中转换的定义如下:
其中ai是8位,a16是经过Ⅰ运算得到的,Ⅰ运算的定义如下:
I(a15,…,a0)=∇(148·Δ(a15)+32·Δ(a14)
+133·Δ(a13)+16·Δ(a12)+194·Δ(a11)
+192·Δ(a10)+1·Δ(a9)+251·Δ(a8)
+1·Δ(a7)+192·Δ(a6)+194·Δ(a5)+16·Δ(a4))
+133·Δ(a3)+32·Δ(a2)+148·Δ(a1)+1·Δ(a0))
I运算的输入是16个8位二进制数,输出为1个8位二进制数。表示将8位二进制数转换成有限域内的元素,▽是它的逆运算。经过有限域内的乘法运算,然后相加,最后转换成1个8位二进制输出。计算有限域乘法,首先要确定GF(2)上的8次不可约多项式。在该算法中,这个不可约多项式为
在加密算法中,明文块a为128比特,密钥k1,…,k10分别为128位,经过10轮运算,分别使用密钥k1,…,k10。加密算法定义如下:
具体加密流程如图2所示:
图2 加密流程图
Kuznyechik要进行10轮循环加密,因为根据算法的特点,事先进行密钥编排,得到10个轮密钥进行循环加密。根据加密算法的特点,本文设计了适用于Kuznyechik算法加密的线程池。首先创建任务实体,要加密数据的输入和输出保存到任务实体的属性中。
public class encryptionData{
public String inputData;
public String outputData;
public String getInputData(){
return inputData;
}
public String getOutputData(){
return outputData;
}
public void setInputData(String inputData) {
this.inputData=inputData;
}
public void setOutputData(String outputData) {
this.outputData=outputData;
}
在主线程中,首先创建队列和工作线程,然后创建任务实体,通过set方法给实体的属性赋值,然后将加密数据对象实体加入到任务队列中,最后不断循环检测工作线程的队列是否为空,如果为空,说明数据已经加密完成,线程池销毁[3-4]。在主线程工作的同时,工作线程不断从任务队列中取出任务实体,获取要加密的内容,不断进行加密,加密结束存储密文,当工作线程将所有任务实体处理完后,队列为空,线程池销毁。具体运行流程如图3所示。
图3 Java线程池的kuznyechik算法流程图
根据算法原理和加密流程,本文采用128bit的密钥,需要加密10轮。首先需要设计线程池管理类,该类内部成员包括线程池实体、任务队列和工作线程实体,线程池实体使用单例模式来设计,并加入同步锁保证线程池对象的唯一[5]。任务队列用来存放加密实体对象,工作线程不断从任务队列中取出任务实体,从任务实体中获取需要加密的明文,然后调用加密方法进行轮加密运算。以下是线程池管理类的主要代码:
public class ThreadPool{
private static ThreadPool instance=null;
public List<encryptionData> taskQueue = Collections.synchronizedList (new LinkedList<encryptionData>());//任务队列
//工作线程(真正执行任务的线程)
private WorkThread workQueue;
private ThreadPool(){
workQueue=new WorkThread();
}
public static synchronized ThreadPool getInstance(){
if(instance==null)
instance=new ThreadPool();
return instance;
}
public void addTask(encryptionData task){
//对任务队列的操作上锁
synchronized(taskQueue){
if(task!=null){
taskQueue.add(task);
taskQueue.notifyAll();
}
}
}
public void destory(){
workQueue.stopThread();
workQueue=null;
//对任务队列的操作上锁
synchronized(taskQueue){
taskQueue.clear();
}
}
工作线程的主要代码如下:
public void run(){
while(isRuning){
encryptionData temp=null;
//对任务队列的操作要上锁
synchronized(taskQueue){
//任务队列为空,等待新的任务加入
while(isRuning&&taskQueue.isEmpty()){
try{
taskQueue.wait(10);
}catch(InterruptedException e){
e.printStackTrace();
}
}
i( fisRuning)
temp=taskQueue.remove(0);
}
i(ftemp!=null){
isWaiting=false;
//执行加密函数
Encryption.encresult(temp.getInputData());
isWaiting=true;
}
}
在加密函数中,主要通过明文与密钥相加运算,然后通过S盒和L运算,得到密文,其中,S盒和L运算的主要代码如下:
进s盒运算,使用以下方法来实现,其中s盒的长度为256,返回的结果是s盒的输出。
public static char[] sbox(char x[]){
for(int i=0;i<16;i++){
a[i]=(char)x[i];
c[i]= (char) s[a[i]];
}
return c;
}
L运算主要进行了16次R运算,主要代码如下:
for(int i=0;i<16;i++){
char e= ltrans(k11);
for(int j=0;j<15;j++){
k11[j]=k11[j+1];
}
k11[15] =e;
}
其中,ltrans表示l运算。在l运算中,主要用到的是GF(2^8)有限域内乘法,这个域的最大值为256。有限域内的乘法使用以下的方法实现:
public static char multiply(char a, char b){
char temp[]={a,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
char tempmultiply=0x00;
int i=0;
for(i=1;i< 8;i++){
temp[i] =XTIME(temp[i-1]);
}
tempmultiply=(char)((b&0x01)*a);
for(i=1;i<=7;i++){
tempmultiply^=(((b >> i)&0x01)*temp[i]);
}
return tempmultiply;
}
在二进制中,所有的数都能用0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80异或得到,任何一个数b和a进行相乘,都可以表示为b和0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80分别相乘,然后异或得到。只要计算出b和这几个数的乘积,一切结果都可以得到。XTIME方法的含义是求一个数与0x02的乘积,如果求一个数乘以2,一般是左移一位,本算法中,不可约多项式为p(x)=x8+x7+x6+x+1,如果最高位为1,再继续左移会超出最大值,这时,左移1位的同时要异或0xc3。XTIME的具体实现代码如下:
public static char XTIME(char x) {
int k=0x00;
if((x&0x80)!=0)
k=0xc3;
return(char)((((x << 1)^k))&0xff);
}
经过8次temp[i]=XTIME(temp[i-1])运算,temp数组中包含8个字符,分别为:a*0x01,a*0x02,a*0x04,a*0x08,a*0x10,a*0x20,a*0x40,a*0x80。然后经过8次tempmulti⁃ply^=(((b >> i)&0x01)*temp[i])运算,另一个乘数b首先进行1位右移,再和0x01与运算,然后分别和这8个字符相乘,把相乘的结果进行相加,得到a和b的乘积。
本文使用Java语句,在eclipse平台进行测试,硬件采用Intel(R)CoreI-5(TM)i5-8400(主频2.8GHZ,6核)32G内存的台式机作为平台进行测试。测试用到了加速比,未采用线程池和采用线程池程序加密算法的运行时间分别记为τa和τb,经过测试,结果如下表1所示:
表1 加密验证结果
测试表明,在数据量比较少时,采用线程池比未采用线程池加密效率略高,但是随着数据量的增大,采用线程池明显比不采用线程池加密效率明显增高。随着数据量的增加,加速比逐渐变陡。下面是原始的测试数据,图4-图7:
图4 1k数据加密示意图
图5 10k数据加密示意图
图6 100k数据加密示意图
图7 1000k数据加密示意图
本文通过设计线程池,基于Java线程池技术,对Kuznyechik算法进行了改进,通过eclipse平台进行测试,测试结果表明,随着数据量的增大,能够明显增加加密的效率,缩短加密时间,为大数据环境下的加密奠定了基础。在未来的研究中,将通过GPU的并行计算,来进一步提高加密效率。