这是我写的第一篇online的技术文章,之前自己学习过程中的笔记都是写在本地的,但是最近在找资料的时候,感觉到很多时候,自己都能在网上找到自己的所需,为何不把自己学的也记录到网上?闭门造车是愚蠢的,分享才能提高。
于是,在做了《信息安全》的课程实验后,想把他作为牛刀小使的第一步。好吧,进入正题:
首先,我们来看一下什么是SPN,其中文是代换-置换网络,其实就是将一个比特串主要经过两种变换,分别是代换和置换,得到另一个比特串,从而实现加密的效果。但是前提是,可以经过类似的手段将这个比特串还原,这也就是解密了。
SPN的比较正式的定义:给定一个l m比特的串x=(x<1>,x<2>,...,x<m>),可以看成是m个长度为l比特的子串的并联,即x<1> || x<2> || ... || x<m>.对此串进行Nr轮变换,每一轮先用异或操作混入该轮的轮密钥,然后对每个子串x<i>使用 进行m次代换,然后使用 进行一次置换。也叫S盒,为P盒。
代换,实质是数值的代换,4个bit的二进制数对应的16进制范围是0~f,下图就是的一个示例:
Z | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | a | b | c | d | e | f |
e | 4 | d | 1 | 2 | f | b | 8 | 3 | a | 6 | c | 5 | 9 | 0 | 7 |
置换,实际上是位置的置换,具体含义是Z=2时对应的5,是指原串的第5个bit放到新串的第2个bit的位置上,而不是指第原串的第2个bit放到新串的第5个bit的位置上,可以通过求该置换对应的逆置换来实现该操作,下图是的一个示例:
Z | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
9 | 6 | 10 | 1 | 13 | 14 | 8 | 12 | 7 | 15 | 2 | 5 | 16 | 4 | 11 | 3 |
SPN的算法模型如下:
正常的SPN算法过程就如以上所示,代换的实现相对与置换来说要容易,置换对于“位”的操作要求比较熟悉,在我的实现中将做S盒的代换改成是一个置换,如下:
Z | 1 | 2 | 3 | 4 |
2 | 3 | 1 | 4 |
而置换 不变。
在实现中,我们取m=4,l=4,也就是一个加密单元为16位。轮数Nr取16,当然,相同条件下,轮数越多越安全。对应的密钥也要有16个,我们通过一个初始密钥循环左移1,2...,16位获得,并设该初始密钥为6A8E。实现对“华南吃饭大学”一字符串的加密以及解密。
在实现之前,有必要总结一下常见的位运算技巧以及在实现S、P盒的时候要用到这些技巧的简单运用:
(1)几个超级简单的公式:
- a == (a & 1);//true
- a == (a | 0);//true
- 0 == (a & 0);//true
- 1 == (a | 1);//true
(2)基于(1)的运用
- //bit是一个指定位是1,其他是0的串
- //打开位,将bit中指定的位总设为1
- a = a | bit;
- //关闭位,将bit中指定的位总设为0
- a = a & ~bit;
- //切换位,将bit中指定的位取相反值,即是1变0,0变1
- a = a ^ bit;
(3) 将一个二进制串中的某几位置换成指定的几位,就是例如想将a串1010101010111011中的 第8位到第11位 置换为1100,即是得到b串1010110010111011。
其实方法很简单,只需要执行以下2个操作:
- a = a & 1111000011111111;//得到a=1010000010111011
- b = a | 0000110000000000;//得到b=1010110010111011
对与第一行代码,首先把a的第8到第11位都置为0,其他位不变,这通过和一个第8到11位是0,其他位是1的串作与运算获得;然后和一个第8到第11为指定的值1100,其他位是0的串作或运算来得到最终要的b串。其实,这里就是基于(1)、(2)的灵活使用。
(4)实现“循环左移”操作
- //使unit循环左移nBit位
- unsigned short ROL(unsigned short unit,int nBit){
- unsigned short temp = getBitOne(nBit);
- temp &= unit;
- temp >>= (16-nBit);
- return (unit<<nBit)|temp;
- }
- //取得高nBIt位是1,其他位是0的二进制串对应的无符号短整形
- unsigned short getBitOne(int num){
- unsigned short base_value = 0xffff;
- return base_value <<= (16-num);
- }
过程是,先将unit的高nBit位截取下来,然后unit使用"<<"进行无循环左移,最后把之前保存好的高nBit位放回低nBit位。其实,这就是得到轮密钥的过程。
以下主要的实现:
前提:
- typedef unsigned short UINT16;
- typedef unsigned char UINT8;
(1)如何求S、P盒置换的逆置换
- //pBox[4]={2,3,1,4},求得的逆是prBox[4]={3,1,2,4}
- //pBox是原S、P盒的指针,prBox是求逆后的指针,len是S、P盒的长度
- void getReverseBox(int * pBox,int * prBox,int len){
- for (int i=0; i<len; i++)
- {
- prBox[pBox[i]-1] = i+1;
- }
- }
则求逆得到:
Z | 1 | 2 | 3 | 4 |
|
3 | 1 | 2 | 4 |
(2)取得需加密字符串的加密单元,也就是取得一个16位的数,使用无符号短整形,在加密过程中的操作都是基于这样的加密单元。
思路:因为每个字符占一个字节(8位),所以每两个字符合并成一个加密单元,主要还是要用到之前的技巧
- a == (a | 0);//true
具体过程如下:
- //str指向需加密的字符串,方法执行后pUnits指向一组加密单元,len是加密单元数组长度
- void getUnit(const char * str,UINT16 * pUnits,int & len){
- int count=0;
- for (int i=0; i<strlen(str); i=i+2,count++)//每个循环获得一个操作数,unit1做低位,unit2做高位
- {
- UINT8 unit1 = (UINT8)str[i];
- UINT8 unit2 = (UINT8)str[i+1];
- UINT16 temp = 0x0;
- temp |= unit2;
- temp <<= 8;
- temp |= unit1;
- pUnits[count] = temp;
- }
- len = count;
- }
(3)将加密单元还原为字符串,一个加密单元可还原成两个字符或者一个汉字。
思路:其实这是(2)中的逆过程,但是必须注意字符存放的位置顺序,以及注意对动态内存的管理,注意释放不再使用的内存。因为C++中有时候没有释放一两个字节的内存看似无关痛痒,main函数反正也是返回给操作系统的,但是正由于这种意识的存在使得在处理一些更复杂的内存问题时不知所措。
- //pUnit指向待还原的加密单元,len是其长度
- char * Unit2String(UINT16 * pUnit,int len){
- char * pStr = new char[2*len+1];
- pStr[2*len] = '\0';
- for (int i=0; i<len; i++)
- {
- UINT16 temp = 0x00ff;
- pStr[2*i]= char(pUnit[i] & temp);
- pStr[2*i+1]= char(pUnit[i] >> 8);
- }
- return pStr;
- }
(3)S盒置换
因为我们使用的加密单元是16位,但是使用的置换是对于4位而言的,要进行分组处理,所以需要经过比较多的位操作来得到S盒置换的目的。
- //unit指向加密单元的引用,pBox指向S盒
- void SBoxReplacement(UINT16 & unit,int * pBox){
- for (int i=1; i<=4; i++)//逐步完成子盒的置换
- {
- subSBoxReplacement(unit,i,pBox);
- }
- }
- //完成第whichSubBox子盒的置换,1 <= whichSubBox <= 4
- void subSBoxReplacement(UINT16 & unit,int whichSubBox,int * pBox){
- //例:unit:1010101010111011 ,whichSubBox:3,1010->1100
- UINT16 temp = getBitOne(4);//1111000000000000
- temp >>= ((4-whichSubBox)*4);//0000111100000000
- UINT16 temp2 = ~temp;//1111000011111111
- temp &= unit;
- temp >>= ((whichSubBox-1)*4);//0000000000001010
- basicSBoxReplacement(temp,pBox);//temp:0000000000001100
- temp <<= ((whichSubBox-1)*4);//0000110000000000
- temp2 &= unit;//1010000010111011
- temp2 |= temp;
- unit = temp2;//1010110010111011
- }
- //最底层的s盒置换
- void basicSBoxReplacement(UINT16 & unit,int * pBox){
- UINT16 temps[4];
- for (int i=0; i<4; i++)
- {
- temps[i] = getBitSingle(i+1);
- temps[i] &= unit;
- temps[i] >>= i;
- }
- UINT16 result = 0x0;
- int prBox[4];
- getReverseBox(pBox,prBox,4); //求逆来更容易实现S盒置换
- for (int j=0; j<4; j++)
- {
- temps[j] <<= (prBox[j]-1);
- result |= temps[j];
- }
- unit = result;
- }
在此,使用了3个层次来完成该置换,第一层SBoxReplacement纯碎是划分了子盒,第一子盒是0到3位,第二子盒是4到7位,第三子盒是8到11位,第四子盒是12到15位。
第二层subSBoxReplacement是完成某个子盒的置换,也就是完成一个加密单元中某4个连续位的置换。过程是将第x组的四位推到低四位,即形如000000000000XXXX,然后再调用basicSBoxReplacement()进行最底层的置换,最后把低4位放回原来的位置。
第三层basicSBoxReplacement可以看成是一个工具函数,直接对一个16位数的低4位进行置换。思路是先把输入参数000000000000XXXX(unit)产生4个16位的数,第i个数的最低位对应原来000000000000XXXX中的第i位,形如000000000000000X,然后再根据S盒来将每个000000000000000X中的X移动到其最后应处的位置,从而达到置换的目的。
(4)P盒置换,与S盒中的 basicSBoxReplacement的思路一致,只是实质操作的位数是16,而S盒中的是4位罢了。也就不贴出代码了。
(5)加密
加密的过程是按照SPN加密算法的模型进行的,但加密的轮数略有减少。
- //str指向待加密的串,pSBox指向S盒,pPBox指向P盒
- //keys指向一组轮密钥,p指向加密后的加密单元(可利用Unit2String进行显示)
- void encrypt(const char * str,int * pSBox,int * pPBox,UINT16 * keys,UINT16 * & p){
- int len;
- UINT16 * pUnit = new UINT16[strlen(str)/2];
- getUnit(str,pUnit,len);
- for (int i=0; i<(strlen(str)/2); i++)//加密单元遍历每个
- {
- UINT16 u = pUnit[i];
- for (int j=0; j<14; j++)//14轮
- {
- u = u^keys[j];
- SBoxReplacement(u,pSBox); //S盒置换
- PBoxReplacement(u,pPBox); //P盒置换
- }
- u ^= keys[14];
- SBoxReplacement(u,pSBox);
- u ^= keys[15];
- pUnit[i] = u;
- }
- p = pUnit;
- //输出每个数的二进制形式,作调试之用
- for (int i=0; i<len; i++)
- {
- if(i%3==0){
- cout<<endl;
- }
- cout<<bitset<16>(pUnit[i])<<"\t";
- }
- cout<<endl;
- }
(6)解密
解密其实是加密的逆过程,只需按着加密的步骤到过来边可解密,但是使用轮密钥时顺序与加密的时候相反。
- //pUnit指向待解密的加密单元,pSBox应指向S盒的逆,pPBox应指向P盒的逆
- //keys指向轮密钥,len是该组加密单元的长度
- void decrypt(UINT16 * & pUnit,int * pSBox,int * pPBox,UINT16 * keys,int len){
- for (int i=0; i<len; i++)
- {
- UINT16 y;
- y = pUnit[i]^keys[15];
- SBoxReplacement(y,pSBox);
- y ^= keys[14];
- for (int j=13; j>=0; j--)
- {
- PBoxReplacement(y,pPBox);
- SBoxReplacement(y,pSBox);
- y ^=keys[j];
- }
- pUnit[i] = y;
- }
- // 输出每个数的二进制形式,作调试之用
- for (int i=0; i<len; i++)
- {
- if(i%3==0){
- cout<<endl;
- }
- cout<<bitset<16>(pUnit[i])<<"\t";
- }
- cout<<endl;
- }
(7)运行
附件有整个程序的源代码,可以下载下来看看。还有欢迎大家拍砖指正。